LYH Studio episode 6 (1:36-2:05) builds its gradient in the Gradient
panel: it drags C=0 M=100 Y=0 K=0 from the Swatches panel onto the left
stop, drops C=100 M=0 Y=0 K=0 on the ramp (a new stop at 68.636 %), then
drags the black stop off. DesignCraft's Gradient panel took no swatches,
showed only a hint until the fill was a gradient, and laid out its
fields differently.
- Gradient panel: InDesign's layout (thumbnail; Type; Location % and
Angle; Reverse; the ramp with midpoint diamonds above and stops below).
Until the fill is a gradient it shows the default White -> Black ramp;
clicking the thumbnail or using the ramp starts one. Midpoints drag
and take a Location. A stop's colour picker moved to its right-click
menu, so selecting a stop no longer grows the panel.
- Swatches panel rows are drag sources (the chip follows the pointer):
dropped on a stop a swatch colours it, elsewhere on the ramp it adds
a stop there.
- Stops keep their colour model through panel edits (CMYK used to turn
into hex RGB whenever a stop moved).
- engine: `object.gradient` stops accept `swatch` (its colour; gradient
swatches and [None] are refused) and colours accept `{gray}`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InDesign draws a radial gradient in the object's bounding box: on a
non-square frame it's an ellipse reaching the frame's sides, and a
Gradient Swatch tool drag keeps those proportions. LYH Studio episode 6
(3:16) gives the left page's background a radial gradient: magenta in
the middle, cyan out to every edge. DesignCraft drew a circle reaching
only the longer side, so a portrait page stayed purple at its sides.
- doc: `Fill::radial_ellipse(bounds)`: centre and radii (default: the
box's centre and half its width and height; with a tool vector, its
start, and its length measured with the box made square).
- render: the radial paint is that circle, stretched by the paint
transform; PDF: the shading's transform does the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InDesign's Swatches panel New Swatch takes what is current: a gradient
built in the Gradient panel becomes "New Gradient Swatch" (applied, and
listed), an unnamed colour a listed swatch, a selected named swatch a
"… copy". DesignCraft's button always added the same orange swatch, and
gradient swatches could not be created at all (LYH Studio episode 6,
2:12: "this is our Swatch … create a new swatch for it").
- engine: `swatch.new {swatch?, target?}` does the above for the
selection's fill (or stroke) or a named swatch; `swatch.create` also
takes `gradient: {kind?, stops: [{location, swatch | color, opacity?,
midpoint?}]}`, its stops taking colour swatches by name, as swatches
dragged onto the Gradient panel's stops.
- Swatches panel: New Swatch runs `swatch.new`, and with nothing
selected still adds a colour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: crates/engine/src/cmd/style.rs: object.color params doc and enabled = next's (text?, has_text or selection) + the model form; apply_color = next's text targets and caret typing format + the branch's rounding-based swatch reuse
Conflict: crates/ui-egui/src/dialogs.rs: next's deleteCharacterStyle / swatchOptions arms; colorPicker = the branch's (no-op without edits, model-form colour) with next's `text`
Conflict: crates/ui-egui/src/panels/swatches.rs: double-click opens the picker with picker_fields + next's `text`; spectrum = the branch's model form + next's `text`
Conflict: docs/target-app-parity.md: the branch's Color panel line + next's Threading and Place text lines
Conflict: ROADMAP.md: logs both
The Color panel's C/M/Y/K and R/G/B fields keep the typed number until Enter
or leaving the field, and send {model, ...} colours in 0..1, which object.color
reads without guessing the scale (M 1 is 1 %, R 1 is not pure red). Colours
that round to the same swatch name reuse that swatch instead of adding hidden
twins. The hex parser rejects signs and other non-hex characters (#323).
As in InDesign, the Eyedropper applies a colour sampled from a placed image
to the fill or the stroke, whichever is in front in the toolbox: with the
Stroke box in front a selected line takes it on its stroke (it has no
fill to show it). The toolbox's state reaches tools through
ViewInfo/ToolContext::stroke_in_front.
Depends on #572.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
color.sample takes `matchFrom`: with no opaque image pixel under the point (an empty part of an image frame, a transparent pixel), it applies that object's attributes as object.matchAttributes does, in the same command; the tool passes the object under the pointer. sample_item and the tool walk shown_children(), so only a multi-state object's active state is sampled, and sample_item skips points outside the graphic's clipping path. object.matchAttributes's body is a function both commands share.
Conflict: crates/engine/src/cmd/style.rs: object.color keeps next's apply_color, spec and enabled check (#84); takes the PR's color.sample (with next's enabled check), image_color_at and sample_item; drops the PR's color_swatch, color_items and has_text_or_selection; explicit `id`/`ids` win over a text selection in apply_color; the PR's tests follow next's caret behaviour (a caret takes the sampled colour for the next typing)
Conflict: crates/tools/src/tests.rs: both sides; the eyedropper test's Asset and Graphic literals gain next's pdf_crop, pdf_box, clip and wrap fields
Conflict: crates/ui-egui/src/i18n.rs, i18n/{ar,it,ja,pt_br,uk}.rs, ROADMAP.md: both sides
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.
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>
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>
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>
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.
#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.
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)
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)
Conflict: ROADMAP.md: took #83's (main's restructured roadmap); the Threading inventory line goes into docs/target-app-parity.md
Conflict: crates/compose/src/lib.rs: kept this branch's undrawn trailing spaces after warichu; comment covers both the spaces and the drop cap
Conflict: crates/idml/src/tests.rs: took #83's (drop cap style as a Dropcap nested style, table style fills through the cascade); this branch has no changes of its own there
Reject unknown character fill swatches before modifying text or undo history. Registered swatch names remain valid, including names that resemble hex colours.
Validation: invalid-name atomicity and save/reopen regressions, plus all six cargo xtask ci gates on main after #514 and #523 (1,186 native tests passed, 3 ignored; 15 crates passed WASM compilation checks).
Task: #403
A CMYK component of 1 or less was read as a fraction even when the other
components were percentages, so {"c": 0.7, "m": 0, "y": 12.2, "k": 0}
(a pale cream) became C 70 % Y 12.2 % and exported as
`0.7019608 0 0.12156863 0 k`. The same applied to RGB arrays.
Now a colour is read as percentages (0..100, or 0..255 for RGB) when any
component is above 1, and as fractions only when every component is
within 0..1.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A CMYK component of 1 or less was read as a fraction even when the other
components were percentages, so {"c": 0.7, "m": 0, "y": 12.2, "k": 0}
(a pale cream) became C 70 % Y 12.2 % and exported as
`0.7019608 0 0.12156863 0 k`. The same applied to RGB arrays.
Now a colour is read as percentages (0..100, or 0..255 for RGB) when any
component is above 1, and as fractions only when every component is
within 0..1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: crates/doc/src/edit.rs: next's thread_frames (append_story); every rev += 1 is rev.wrapping_add(1)
Conflict: crates/engine/src/cmd/fonts.rs: next's replace (the local-formatting pass moved out of this hunk)
Conflict: crates/engine/src/cmd/text.rs: next's format_chars (apply_char_attrs, typing format); rev += 1 is rev.wrapping_add(1)
Conflict: ROADMAP.md: both progress-log entries, next's newer ones first
Conflict: apps/designcraft/src/gpu.rs: next's side; the branch's test fix targets the startup record tests next replaced
Conflict: apps/designcraft/src/main.rs: next's gpu startup and macOS IME helper; config_dir() dropped for the branch's profile::config_dir(), recovery_dir from profile::recovery_dir()
Conflict: crates/ui-egui/src/canvas.rs: next's guard (menu_open, keyboard_pane already limits keys to the focused pane)
Conflict: crates/ui-egui/src/lib.rs: modules from both sides
Documents, books, libraries, snippets, swatches and every export are
written to a temporary file beside the target, flushed, renamed over it
and the folder flushed, so a crash, kill or full disk mid-write leaves
the old file intact. Replaced files keep their permissions, symbolic
links keep pointing at their file, and Windows retries a rename that a
scanner briefly blocks.
Fixes#436
Conflicts:
- engine cmd/file.rs, cmd/interchange.rs: kept next's file.open and
file.openIdml docs (content detection, INDD conversion, package parts)
and added "GREP styles whose pattern can't compile" to their warnings.
open_idml keeps next's conversion and import-report warnings and appends
grep_style_warnings after the font warnings.
- engine cmd/style.rs: style.paragraph.create/edit docs combine next's
startAt and kinsoku notes with the grepStyles rule. create_para and
edit_para keep next's set_json_over, continue_numbering and
explicit_default_kinsoku, then call check_grep_styles. The branch's
grep_style_error, check_grep_styles and grep_style_warnings follow
explicit_default_kinsoku. Kept next's delete_tests and
swatch_options_tests; grep_style_tests goes at the end of the file.
- ui-egui dialogs.rs: kept next's style_section_edit tests and added the
branch's bad-GREP-pattern test.
confirm_paragraph_style_options, grep_error and the dialog status line
merged without conflicts; next had not changed the OK arm they replace.
style.paragraph.create, style.paragraph.edit and type.para return the
compile error (bad GREP: ...) for a GREP style pattern that can't compile
and change nothing. Paragraph Style Options shows the error under the
pattern as it is typed, and on OK keeps the dialog open on the GREP Style
section with the command's error.
Opening an IDML or native document keeps such a style and reports a
warning naming the style (or paragraph) and the pattern; composition
keeps skipping it. Adds a lookbehind GREP style test.
Conflict resolutions:
- crates/engine/src/cmd/layout.rs: next's start_number, chapter_numbering and
layer.move validation kept; the branch's set_layer added after them;
spread_rotation_input_tests and layer_options_tests both kept.
- crates/engine/src/cmd/style.rs: next's delete_tests and the branch's
swatch_options_tests kept as separate modules.
- crates/ui-egui/src/dialogs.rs: both module declarations (style_cjk,
options) and both confirm arms (deleteCharacterStyle, swatchOptions /
colorGroupOptions / layerSettings) kept.
- crates/ui-egui/src/i18n.rs, i18n/{ar,it,ja,pt_br,uk}.rs: both sides' rows
appended; no key was duplicated.
- crates/ui-egui/src/panels/layers.rs: #378's drag-and-drop row
(Sense::click_and_drag, deferred cmds, payloads) kept; the branch's
accessibility name, double-click -> app.layerOptionsDialog (after the
eye/lock/triangle/selection-square branches, before activate) and the
Layer Options for "<name>"... context entry added through cmds.
- crates/ui-egui/src/panels/swatches.rs: next's apply_swatch kept, with the
branch's single-click-only guard so a double-click opens Swatch Options.
Integration:
- Document::rename_swatch_refs also renames table cell stroke gap colours
(CellStroke and CellStrokeAttrs gap_color), kenten and ruby fill/stroke
colours (CharAttrs kenten_fill, kenten_stroke, ruby_fill, ruby_stroke)
and paragraph border gap colours (ParaAttrs border_gap_color).
- The swatch rename test covers a table's cell gap colour, kenten fill,
ruby stroke and border gap colour.
- The Layer Options double-click test checks that the double-click does
not reorder layers.
- German rows for the 29 new shared keys in i18n/de.rs.
- swatch.options: rename, Name with Color Value, process/spot, colour mode
and values, tint, in one undo step; special swatches keep their names,
and a taken or empty name changes nothing.
- Document::rename_swatch_refs points every use at a renamed swatch:
tints, colour groups, inks, paragraph/character/object/cell/table
styles, item fills, strokes, gaps, effects and column rules, text
colours, rules, shading, borders, table cells and the footnote rule.
- layer.set takes every Layer field (show guides, suppress wrap when
hidden), a layer colour name, and rejects empty or duplicate names and
unknown colours.
- Swatches panel: double-click or Swatch Options… opens the dialog;
colour groups get Color Group Options…. Layers panel: double-click or
Layer Options for "name"… opens Layer Options (app.layerOptionsDialog).