Commit Graph
1054 Commits
Author SHA1 Message Date
moppeeerandClaude Opus 5.5 0ac04151c9 fix(engine): Every command that writes story text reads CR as a paragraph return
frame.create and text.insert turned \r and \r\n into paragraph returns,
but story.replaceRange, story.setText, note.new, footnote and endnote
text, type on a path and table cells kept the CR in the text, where it
drew as a missing glyph and joined the paragraphs. They all use
text_param now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:26:48 +02:00
moppeeerandClaude Opus 5.5 d6635f180a fix(engine): The selected-text preview reads only the 60 characters it reports
type.selectionAttrs copied the whole selection to a string before taking
the first 60 characters, so selecting a long story copied all of it on
every query. It now skips leading blanks and stops after 60 characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:22:03 +02:00
moppeeerandClaude Opus 5.5 ef87428481 feat(engine): Report the selected text; remember recently used fonts
type.selectionAttrs reports the start of the selected text on one line
(`selectedText`, up to 60 characters), for previews such as the font
menu's Selected Text sample. The preferences keep `recentFonts`, the
fonts last picked in the font menus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:20:17 +02:00
KnottyDyes 04319fce0f Styles: selection capture accepts deleted-swatch fills
Validate caller-supplied character fills before merging selection attributes.
Deleted swatch names already retained in text no longer block New Style from
Selection; explicit unknown fills still fail without changing state.

Cover swatch creation, text formatting, deletion and style capture, including
non-fill overrides and explicit-fill rejection with fromSelection.
2026-10-12 10:03:07 -06:00
moppeeerandClaude Opus 5.5 1411c29fbf fix(render): A group's gradient feather masks everything the group draws
The DestIn mask now covers the whole render target (and the PDF's
exported area), with the gradient kept in item space through the paint
transform, since past the gradient's ends the end opacity carries on: a
child's long drop shadow or thick stroke is faded, not cut off at a box.
feather_bounds counts only shown children and active states, walking
nested groups without recursion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:01:25 +02:00
moppeeerandClaude Opus 5.5 1c4b6885c7 fix(render): A group's gradient feather fades the group, on screen and in PDF
A gradient feather on a group (Gradient Feather tool over grouped text, as
in LYH Studio's poster tutorial) did nothing on screen: groups never went
through the effects path. In PDF the group's mask was the group's own path
bounds, and a group has no path, so the mask covered a box of zero size and
hid the group.

- Item::feather_bounds: the box the feather spans, the objects' bounds for
  a group.
- Render: a feathered group draws its objects in one layer, then fades it
  with the same mask as other objects (fx::gradient_feather_mask).
- PDF: the group's mask spans its objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:01:25 +02:00
KnottyDyes e3acb8e388 Place: an explicit x, y, spread or width puts the image in a new frame
Explicit image placement leaves selected frames untouched; an explicit frame still wins. Null placement parameters behave like omitted values.

Reject nonnumeric, nonfinite and nonpositive image widths before editing. Cover rejection without document changes, valid sizing and null selection behavior with regression tests.
2026-10-12 10:00:30 -06:00
moppeeerandClaude Opus 5.5 9ab226d741 fix(eyedropper): Sample rasters only; colour text through object.color
A placed PDF, EPS or SVG under the Eyedropper matches attributes again,
as before, instead of failing with "no placed image under the point"
(Asset::is_raster, shared by the tool and color.sample). color.sample
hands its colour to object.color, which now colours selected text, the
text of selected frames ({text: true}), or the typing colour of an empty
story; a caret in existing text is refused before a swatch or undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:56:17 +02:00
moppeeerandClaude Opus 5.5 d7d6f27069 feat(eyedropper): Sample a placed image's colour into the selection
The Eyedropper on a placed image now does what InDesign does: with text
selected, clicking the photo colours the text with the image's colour under
the pointer; with objects selected, it fills them. The colour is an unnamed
colour (reused when the same colour is sampled again). Images on locked
layers can be sampled, and unfilled frames over the image let it show through.

- color.sample {at, spread?, target?: fill|stroke, ids?} samples the frontmost
  visible raster image at a spread point; one undo step; a caret or a point
  with no image is refused without an edit.
- object.color shares its swatch lookup with it.
- The Eyedropper tool sends color.sample over an image, matchAttributes
  elsewhere as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:56:17 +02:00
moppeeerandClaude Opus 5.5 56d140a34a fix(tools): Fit on a mouse double-click, along the frame's own axes; skip locked frames
The canvas reports a double-click on release (after the second Up), so the
handle double-click now fires from the mouse. Side handles map to the frame's
local axis through spread and frame rotation. Graphic frames honour the axis.
Locked frames and frames on locked layers are left alone. Text frames fit by
searching the frame height with its own columns and vertical justification,
so two columns stay two columns and Bottom/Center/Justify don't inflate it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:54:41 +02:00
moppeeerandClaude Opus 5.5 4e250565ac feat(tools): Double-click a frame handle to fit the frame to its content
In InDesign double-clicking a selection handle fits the frame to its
content: a corner fits both sides, a top or bottom handle the height, a
side handle the width. LYH Studio's tutorial uses it at 9:30 to fit the
INDESIGN text frame before moving it to the corner. In DesignCraft the
double-click switched to the Type tool.

- Selection tool: a double-click on a handle runs object.fit
  {mode: fitFrameToContent, axis: both|height|width}.
- object.fit fitFrameToContent fits text frames to their composed text:
  the bottom to the last line's descent, the right side to the longest
  line (one column), plus the insets, keeping the top-left. Overset text
  grows the frame first. Before, it fitted graphic frames only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:40:35 +02:00
Noah Scholzandskylar taylor-barrick 328b4d5a52 fix(recovery): isolate recovery data between app instances
Keep each app's recovery data in an owned run folder. Validate current folder and lock ownership and disable recovery when isolation fails instead of using the shared root.

Fixes #496.

Co-authored-by: noah1400 <48360809+noah1400@users.noreply.github.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-10-12 09:26:34 -06:00
5cdcac4ace fix(save): replace files atomically
Replace native engine saves and engine/MCP exports atomically, preserving private permissions during staging and cleaning up failed writes. Add regressions for long filenames and caught writer unwinding.

Fixes #436.

Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-10-12 09:19:36 -06:00
rasnoland 99ced6cc0a UI: spring-loaded zoom (Cmd+Space / Cmd+Alt+Space) 2026-10-12 16:09:24 +01:00
aiongg a5fbf5d8d4 Merge table-border-drag: Tables: drag row and column borders with the Type tool
Conflict: crates/compose/src/lib.rs: one re-export line with next's hit_cell_with and the branch's BorderHit/hit_border
Conflict: crates/tools/src/text.rs: next's IME/preedit, guides and unit_selected kept alongside the border drag; border press checked after the Down resets; drag keeps next's `cell_drag || unit_selected` guard after the border branch; Esc cancels a border drag before next's preedit guard; BorderHit via next's `compose` alias
Conflict: crates/tools/src/tests.rs: both sides' appended tests kept
Conflict: docs/target-app-parity.md: next's IDML line, branch's Tables line
Conflict: ROADMAP.md: branch entry on top of next's entries
2026-10-12 14:56:19 +00:00
aiongg 44fde50463 Tables: drag row and column borders with the Type tool
Hovering a table's inner column or row border, or its end or bottom
edge, shows a double-arrow cursor; dragging resizes instead of selecting
text. A column drag changes the column before the border and moves later
columns; Shift keeps the table width (the next column compensates), and
Shift on the end edge scales every column. Rows work the same way, and a
dragged row becomes At Least. Sizes clamp at 3 pt. One undo step per
drag, Esc cancels, locked frames refuse, and the pointer is mapped
through the frame's transform so rotated frames and turned spreads work.

New command table.resizeBorder {story?, table?, frame?, axis, index,
delta | size, keepTableSize?}; the Type tool emits it through
Begin/Preview/Commit. compose::hit_border finds the border under a point
in frame inner space.

Closes #595
2026-10-12 14:52:31 +00:00
aiongg b22fb933f8 Integration: format 2026-10-12 14:52:17 +00:00
aiongg 1ebefe4db4 Merge fill-stroke-keys: Engine: swap and default fill and stroke in one undo step
Conflict: ROADMAP.md: both progress lines kept
Conflict: docs/target-app-parity.md: next's UI and tools lines kept, with the Fill/Stroke proxy keys added to the UI line
Conflict: crates/ui-egui/src/lib.rs: UiState keeps next's tabs_panel and the branch's stroke_front
Conflict: crates/ui-egui/src/menus.rs: UI_COMMANDS keep next's togglePresentation and formattingAffectsText entries and add the color.* entries
Conflict: crates/ui-egui/src/toolbar.rs: next's text-aware proxy buttons (color_target, colors_affect_text, apply_swatch) kept; the proxy reads stroke_front from UiState instead of egui temp data
2026-10-12 14:48:16 +00:00
aiongg c0c4973f34 Merge place-loads-cursor: Place: with no frame selected, the graphic loads into the place cursor
Conflict: crates/engine/src/cmd/file.rs: spec keeps next's pdfCrop list and adds loadCursor; file_place loads next's asset (pdf_crop/pdf_box already set) with the box size (bw, bh), so LoadedGraphic carries no separate crop and place_drop keeps next's xf with clip: None; load_cursor takes the branch's body, place_load builds the Asset with ..Default::default()
Conflict: crates/engine/src/lib.rs: LoadedGraphic is { asset, size }; on next the crop box lives in the asset
Conflict: crates/ui-egui/src/lib.rs: next's open_documents_later/open_os_documents and the branch's place_file both kept
2026-10-12 14:46:01 +00:00
aiongg bb45f12b05 Place: with no frame selected, the graphic loads into the place cursor
File › Place (native picker, browser import, Place with Import Options
and the PDF page dialog) passes loadCursor to file.place. With no empty
or graphic frame selected and no x/y given, an image is added to the
document and loaded into the place cursor instead of landing in a new
frame on the first spread; a click places it at its natural size, a drag
draws its frame. The loaded graphic keeps the chosen PDF page and crop
box. Escape or choosing another tool unloads the cursor. Dropped files
and file.place without loadCursor behave as before.
2026-10-12 14:39:14 +00:00
aiongg 4ac6e9a540 Engine: swap and default fill and stroke in one undo step
object.swapFillStroke and object.defaultFillStroke change every unlocked
target in one edit, so one undo restores them all. Swap works per object
and keeps each object's tints; a stroke that gets a colour at zero weight
gets the default 1 pt. Objects that are locked, or on a locked layer,
stay as they are. color.swapFillStroke and color.defaultFillStroke run
these commands.
2026-10-12 14:28:58 +00:00
aiongg 3ff531ea73 Merge mac-font-activation (update): faces without drawable outlines aren't listed; packaged fonts keep a font extension (#443) 2026-10-12 12:41:20 +00:00
aiongg 22eac72797 Fonts: faces without drawable outlines aren't listed; packaged fonts keep a font extension 2026-10-12 12:33:12 +00:00
aiongg 91a2155cdd Integration: a selected graphic's Transform x/y are measured from the rulers' zero point
#550 places a selected graphic at x/y from its page's left edge; next measures
transform.set x/y from the rulers' zero point (view.zeroPoint, ruler origin), and the
Transform fields show a graphic's bounds from that point. transform_content now targets
x/y from the same origin as the frame path, so a typed value lands where the fields
say it is.
2026-10-12 12:32:01 +00:00
aiongg 4dbd73a6fd Merge #550: Transform values act on a selected placed graphic, not its frame
Conflict: crates/engine/src/cmd/object.rs: transform.set doc string joins next's "measured from the rulers' zero point" with the PR's `content?`; test modules: kept next's copy_ids_tests/tint_alone_tests and the PR's content_transform_tests
Conflict: crates/ui-egui/src/panels/mod.rs: kept next's swatch_list_tests and the PR's content_selection_tests
Conflict: ROADMAP.md: kept both progress-log lines
2026-10-12 12:29:25 +00:00
aiongg 8cd0e74a33 Merge #571: Styles: Load Styles from another document or IDML file
Conflict: crates/ui-egui/src/lib.rs: import_file takes the PR's `styles` early return first, then next's content-sniffing `layout` line
Conflict: ROADMAP.md: kept both progress-log lines
2026-10-12 12:24:34 +00:00
aiongg 4fb32bdb1b Merge #567: Swatches: Load Swatches refuses .ase files over 100,000 colors and merges in linear time
Conflict: ROADMAP.md: kept both progress-log lines (the PR's 2026-10-12 line above next's 2026-10-11 lines)
2026-10-12 12:24:00 +00:00
noah1400 d298555e43 Styles: Load Styles from another document or IDML file (#405)
Styles could not be taken from one document into another; a client's
styles had to be rebuilt in every new file. The Paragraph Styles and
Character Styles panels now have Load Paragraph Styles, Load Character
Styles and Load All Text Styles: pick a .designcraft or .idml file,
tick the styles to load and choose what happens to one the document
defines differently (redefine, keep, or load under a new name). The
styles, swatches and lists a loaded style names come along.
`style.load` does it in one undo step.
2026-10-12 10:41:37 +02:00
moppeeerandClaude Opus 5.5 be4f60ff85 fix: A selected graphic's Transform values are the graphic's
With a placed graphic selected, transform.set acted on the graphic but
the Control bar and Properties still read the frame's values: after
setting the image of a -45° frame to 0°, a spinner step sent -44, and a
200 pt frame holding a 400 pt image showed W 200, so 201 halved the
image. transform.info now reports the graphic's values at the top level
in that case, and sel_info gives the graphic's bounds.

Also: `id` names the target explicitly, like `ids`, so it isn't taken
as a content selection; and a content selection of locked frames
records no empty undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:42:06 +02:00
moppeeerandClaude Opus 5.5 2f21b005f3 fix(engine): Transform values act on a selected placed graphic, not its frame
With a frame's content selected (Direct Selection, or selection.set
{content: true}), transform.set still rotated, scaled and moved the
frame. It now transforms the placed graphic inside the frame and leaves
the frame alone, as InDesign's Transform panel does: rotation, shear and
scale are absolute and measured as transform.info reports the content;
x, y, width and height are the graphic's bounds on the page. A
`content` parameter picks either explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:36:32 +02:00
dotCooCoo 591cb579b3 Swatches: Load Swatches refuses .ase files over 100,000 colors and merges in linear time
ase::read accepted any number of colors in a swatch exchange file, and
swatch.load looked up each incoming name with a linear search through
the document's swatches. The time to load a file grew with the square of
its number of colors: in a test build, 50,000 colors used 2.5 s of CPU
and 100,000 used 9.6 s. No limit applied to larger files.

ase::read now returns an error for a file with more than 100,000 colors
(ase::MAX_COLORS). load_swatches looks up each name's first swatch in a
HashMap, and each color added from the file is indexed too. A file of
100,000 colors now loads in under 0.1 s. The merge follows the same
rules as before: names already in the document are kept unless replace
is set, a name repeated in the file matches its first copy, and a
locked swatch is left unchanged.
2026-10-11 23:18:46 -07:00
aiongg af389f9a0b Merge clipping-paths: Images: clipping paths from IDML clip the image on screen and in PDF
Conflict: ROADMAP.md: both progress lines
Conflict: docs/gaps.md: next's header, clipping paths added to its change list
Conflict: docs/target-app-parity.md: next's header, clipping paths added to its change list
Conflict: crates/doc/src/item.rs: both Graphic methods (next's shown_bounds, the branch's clip_region)
Conflict: crates/idml/src/names.rs: both imports (ClippingKind, ContourType)
Conflict: crates/idml/src/export.rs: both pushes, TextWrapPreference first, then the clipping settings
Conflict: crates/idml/src/import.rs: both lets (next's wrap, the branch's clip)
Conflict: crates/engine/src/cmd/file.rs: next's size and transform, plus clip: None
Conflict: crates/engine/src/cmd/datamerge/fill.rs: next's Graphic with wrap, plus clip: None
Conflict: crates/engine/src/cmd/mathexpr.rs: next's Graphic with wrap, plus clip: None
Conflict: crates/pdf/src/tests.rs: both tests
2026-10-12 05:37:19 +00:00
aiongg 43a4e70658 Merge section-marker: Sections: the section marker shows the section's marker text (#556)
Conflict: ROADMAP.md: both progress lines (next's, then #556's)
Conflict: crates/compose/src/shape.rs: #556's empty-marker zero-width glyph; next's digit mapping, styles.at(i)/styles and sub.upright() in both arms
2026-10-12 05:34:13 +00:00
aiongg e111bb74f7 Images: declare a graphic's clipping path beside its transform 2026-10-12 05:32:11 +00:00
aiongg 8125f2490b Images: clipping paths from IDML clip the image on screen and in PDF
A placed graphic keeps its clipping path (Detect Edges, Alpha Channel,
Photoshop Path or User-Modified Path) in graphic space, with the
options IDML carries. IDML import reads ClippingPathSettings and its
saved ClippingPathGeometry (anchors capped, non-finite subpaths
dropped); export writes them back. The canvas and PDF export clip the
image to the path inside the frame clip; an inverted path shows the
image outside it. Without saved geometry nothing changes.

object.clippingPathOptions reports a graphic's clipping path, inverts
it or removes it (Object > Clipping Path > None).

Fixes #450.
2026-10-12 05:31:03 +00:00
aiongg 406f1b3d26 Sections: the section marker shows the section's marker text (#556) 2026-10-12 05:09:40 +00:00
aiongg d30cefe166 Integration: renaming a swatch reaches endnote text (#84 × #416)
#84's Add to Swatches renamed the colour in footnotes and endnotes through its own helpers; next's Document::rename_swatch_refs (#416), which add_to_swatches calls after the merge, covered footnotes but not endnotes. rename_swatch_refs now walks each story's endnotes too, and #84's test checks an endnote.
2026-10-12 04:23:54 +00:00
aiongg 70fe825045 Integration: autoflow page-break test names its command (#266 × #84)
#84 gives autoflow the id of the command that autoflows, for its errors; #266's odd/even page-break test called autoflow with the old three arguments. The test passes "file.place".
2026-10-12 04:20:54 +00:00
aiongg 89abf6b6c9 Merge #84 (update): Text editing, snapping and text threading (#23)
Review fixes for text editing, paste and threading (fix84-text,
fix84-thread), the Italian/Japanese/Portuguese/Ukrainian Swatches hint
and #83's update merged into the PR branch.

Conflict: ROADMAP.md: header combines #84's threading change with next's 2026-10-11 changes; progress lines and change-log rows both (#84's 2026-10-12 first); stage line #84's (alpha ~5–7 h away)
Conflict: docs/gaps.md: #84's header text plus next's kerning/fonts/print changes
Conflict: docs/roadmap.md: next's M1 row (#186 done), #84's M2 row
Conflict: crates/engine/src/cmd/style.rs: next's side; add_to_swatches calls next's Document::rename_swatch_refs, #84's rename_in_story/rename_in dropped (next's covers text, tables, cells, notes and items); #84's test kept
Conflict: crates/idml/src/tests.rs: next's side (#84 carries #83's tests.rs, already merged in 9196a164)
Conflict: crates/tools/src/select.rs: overlays keeps next's content-move ghosts and live corners, plus #84's snapped loaded-cursor drag frame; a Place click without a drag shows nothing
Conflict: crates/ui-egui/src/i18n/it.rs: next's side (the Swatches hint entry is already further down)
2026-10-12 04:20:34 +00:00
aiongg d2a6dd39a2 Integration: character fill swatch errors go through known_swatch (#403 × #520)
#403 added validate_char_fill with its own message; #520 added known_swatch for object fills and strokes, which also suggests the intended name. validate_char_fill now calls known_swatch with key "fill", so one error path covers both. #403's tests still pass (the message names the fill, swatch.create and the returned name).
2026-10-12 04:17:15 +00:00
aiongg 8f1c60e4fc Merge upstream main 835edae6
Brings #514 (MCP request replay), #523 final (already in next), #403
(character fill must name a swatch), #520 and #521 finals (already in
next as their PR heads).

Conflict: crates/engine/src/cmd/object.rs: next's side throughout (transform.move doc with content?, object.fill with tint alone from #525, move_content, reorder/content_move/group/tint_alone test modules); upstream's copy_ids_tests already in next
Conflict: crates/engine/src/cmd/text.rs: both; #403's validate_char_fill after next's ruby_cmd
Conflict: crates/pdf/src/export.rs: next's side (item/item_guarded cycle guard, frame_objects; upstream's anchored loop dropped as in d11ccad3)
Conflict: crates/render/src/lib.rs: next's side (drawing_items, draw_item/draw_item_inner)
2026-10-12 04:15:40 +00:00
aiongg 7eed1dffe1 Merge #84 into feat/ime 2026-10-12 04:12:45 +00:00
aiongg dd044dd8b1 Merge review fixes: threading 2026-10-12 04:10:06 +00:00
aiongg 9964025ffb Swatches: Add to Swatches renames the colour in text, tables and notes 2026-10-12 04:04:07 +00:00
aiongg 52b58c0519 Text: Bold and Italic that change nothing add no undo step 2026-10-12 04:01:12 +00:00
aiongg d163837781 Text: text.selectAt needs a point 2026-10-12 04:00:19 +00:00
aiongg 6a0f0cf386 Text: a quadruple click selects the paragraph with its return 2026-10-12 03:59:53 +00:00
aiongg 45144c5044 Text: moving the caret drops the pending typing format 2026-10-12 03:59:21 +00:00
moppeeer 835edae659 feat(commands): return IDs from move, duplicate and repeat
Return ordered item IDs from Move, Duplicate and Step and Repeat while preserving the existing moved and created counts, selection and undo behavior. Scripts can use the IDs to operate on copies.

Validation: eight independent probes covering both swatch validation and returned IDs, plus all six cargo xtask ci gates on main after #520 (1,188 native tests passed, 3 ignored; all 15 applicable crates passed WASM compilation checks). Both ROADMAP entries and command regression modules are retained.

Task: #521
2026-10-11 21:58:49 -06:00
aiongg 7b803f1ddc Text: double-click word selection keeps grapheme clusters whole 2026-10-12 03:58:28 +00:00