The Links panel lists links about twice a second, and each listing
hashed every linked text file. A linked text file now also remembers
its modification time (as Data Merge does for its source, through the
same size-and-time fingerprint): while size and time match, it isn't
read. A different size is Modified without reading. When only the time
changed, the content hash still decides (a file saved or synced again
without changes stays OK), and the session remembers that check by
size and time, so the next poll doesn't read the file again. Update and
Relink record the size and time from before their read. Test: polls of
an unchanged file don't read it (it is made unreadable), a touched but
unchanged file is read once and stays OK, a changed file is Modified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7b33c981dc)
InDesign's Effects dialog sets an Inner Glow's blend Mode (Screen by
default) and Technique (Softer or Precise), and a Basic Feather's Choke
and Corners (Sharp, Rounded, Diffused). LYH Studio episode 14 shows them
at 12:05–12:20 (Screen 75 %, Softer, 0.25 in; 0.0625 in, Diffused).
- doc: InnerGlow.mode and .technique (GlowTechnique), Effects.
feather_options (FeatherOptions: choke %, FeatherCorners), with
InDesign's defaults; old documents read as before.
- render: the glow uses its mode; Precise keeps it nearer the edge; the
feather's choke shortens the fade, Sharp and Rounded corners follow
the shape more tightly or softly than Diffused (unchanged, so the
goldens stay).
- render fix: an inner glow's (and a bevel's) blend mode was applied
inside the clip layer, against its empty backdrop, so Screen and
Multiply acted as Normal; the blend layer now wraps the finished edge
and mixes with the object below.
- engine: choke clamped to 0–100; the params documented.
- Effects dialog: Mode and Technique on Inner Glow; Choke and Corners on
Basic Feather; strings translated.
Builds on #643 (the Effects dialog).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a3b971654f)
A gradient swatch on a stroke drew nothing: strokes were painted with a
single resolved colour. LYH Studio episode 6 gives the loop on the right
page (9:13) and, at first by accident, the circle (1:19, "right now it
is on the stroke") a gradient stroke.
- doc: Stroke::gradient_angle / gradient_vector (as Fill's) and
Stroke::as_fill(), the stroke's paint as a fill.
- render and PDF: a gradient stroke is painted like a gradient fill,
across the path's box (dash gaps and arrowheads keep their paints).
- engine: object.gradient {target: stroke} edits the stroke's
gradient (kind, stops, angle, the Gradient Swatch tool's from/to);
scaling moves the stroke's vector with the shape.
- IDML: GradientStrokeStart/Length/Angle round-trip.
- Gradient panel: acts on the stroke when the toolbox's Stroke box is
in front, as InDesign's follows the Fill/Stroke proxy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Links panel lists links about twice a second, and each listing
hashed every linked text file. A linked text file now also remembers
its modification time (as Data Merge does for its source, through the
same size-and-time fingerprint): while size and time match, it isn't
read. A different size is Modified without reading. When only the time
changed, the content hash still decides (a file saved or synced again
without changes stays OK), and the session remembers that check by
size and time, so the next poll doesn't read the file again. Update and
Relink record the size and time from before their read. Test: polls of
an unchanged file don't read it (it is made unreadable), a touched but
unchanged file is read once and stays OK, a changed file is Modified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- describe style.paragraph.create (and .edit, style.character.create,
.edit) gave chars?: {…} and para?: {…}. The descriptions now say they
take type.char's and type.para's attributes and name the common ones.
- A wrong attribute gave "invalid parameters for `style`: unknown
attribute `font`". The error now names the command and the group,
and the attribute meant (nearest name, or one the key starts or ends)
or else lists them all: "invalid parameters for
`style.paragraph.create`: chars: unknown attribute `font`; did you
mean `fontFamily`?". The suggestion comes from set_json, so
type.char, type.para and find.change get it too.
- chars or para that isn't an object is an error instead of ignored.
Fixes#777
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>
A frame holding items pasted into it isn't a group. Ungroup took it
apart like one, leaving its content loose and losing the clipping shape,
and the Properties panel called it "Group" with an Ungroup quick action.
As in InDesign, Ungroup now only takes real groups apart; other objects
stay as they are and stay selected (Ungroup used to clear them from the
selection). Such frames are labelled by their shape (<ellipse>, ...), so
Properties shows "Ellipse" with Convert Shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backspace at the start of the second of two consecutive tables merged their
anchor paragraphs: Story::delete kept one table id, the other table was
dropped and the paragraph held two anchors. Pasting a table next to an anchor
replaced the existing table the same way, and pasting several paragraphs
right before an anchor left its table unreferenced. Saving succeeded;
reopening failed with "paragraph must hold exactly one anchor".
fix_tables now splits a table paragraph that holds further anchors, so every
anchor gets its own paragraph again. delete and insert_story tell it which
table each surviving anchor belongs to (fix_tables_with), and insert_story
fixes tables up once after the whole paste instead of after every run.
Regression tests in doc (Backspace between tables, deleting the middle of
three, paste before/after an anchor, paragraphs pasted before an anchor) and
engine (text.delete and edit.paste through commands, then a file round-trip);
all failed before the fix.
Claude-Session: https://claude.ai/code/session_01La6SDgsmdZfgfDDmx3wiGU
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>
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>
Cycle checks walk based_on with a seen-set over all parents, so a cycle
longer than the drawing depth cap is still found. Snapping lists parent
items through shown_parent_pages instead of recursing, and offers base
items on a parent spread. A base parent's text on a parent spread is
composed for that spread, so page-number markers show its prefix.
Cmd+Shift-click hits what is drawn: it honours Show Parent Items, hidden
and locked layers, the facing-page filter and layer order. IDML import
warns when a parent spread's pages name different bases or their own
spread.
Pages draw the base parent's items, then the parent's, then their own,
on the canvas, in PDF and image export, and on the parent spread itself.
Chains are followed base first with a depth bound. Overrides, parent hit
testing and the flattener's transparency check include base items.
layout.parents.options {parent, basedOn} sets or clears the base and
refuses cycles; document.inspect reports basedOn per parent. Opening a
.designcraft or IDML file clears a cyclic or dangling base with a warning.
IDML already carried the base as the parent pages' AppliedMaster; a test
covers the round trip.
Conflict: crates/render/src/text.rs: the branch's paint_glyphs with next's fill and stroke overprint (overprint_fill / overprint_stroke, an overprinting() Multiply layer around each paint); next's cell_stroke(kind, weight, length); path text strokes don't overprint, as before
Conflict: crates/pdf/src/text.rs: the branch's text_run / paint_outlines / glyph_outlines split with next's skew tan, vertical mark offsets and split_clusters glyph order (#421)
Conflict: crates/engine/src/cmd/paths.rs: next's shatai transform + the branch's stroke_behind_fill key
Conflict: crates/idml/src/tests.rs: tests both
Conflict: docs/typography-parity.md, ROADMAP.md: headers combined, logs both
The Type tool's clicks, drags and double-clicks on type on a path went to
the character at the same spot of the straightened line, and the caret
and highlight were drawn straight. A click now finds the character where
its nearest point along the path is (compose::PathTextMap, from the line
to the path and back, as path_glyphs places the glyphs); the caret stands
across the path there and the highlight bends with the text. A click on
the letters, above the path, hits the frame.
Depends on #636 (PathWarp::nearest).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Object > Effects (Transparency..., Drop Shadow... and the rest, and their
shortcuts) and the Effects panel's fx button open the Effects dialog on
the chosen effect, turned on: the effect list with checkboxes on the left
and a summary line, the chosen page's settings on the right in the
document's units (Blending, Position with X/Y offsets, Options...),
Preview on the page while it is open, Cancel putting the page back, and
OK as one undo step.
- engine: object.effects {effects (merged, kept in range), opacity, blend}
- doc: drop and inner shadows default to InDesign's 120 degree light
- ui: dialogs::cancel (a previewing dialog reverts)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A character attribute strokeBehindFill paints a text stroke under the fill
at double width, so its full weight lies outside the letters and they keep
their shape. Screen and PNG rendering, PDF export (stroke as outlines
first, then the filled text, so the text is not duplicated; clipped to the
outside of the glyphs when there is no fill), Create Outlines (outside-
aligned stroke), the Properties panel and Character Style Options.
IDML StrokeAlignment on CharacterStyleRange, CharacterStyle and
ParagraphStyle imports and exports as the attribute (OutsideAlignment =
on, CenterAlignment = off).
With the Type tool, Option/Alt+Left/Right with a caret changes the manual kerning of the pair at the caret by the Kerning/Tracking increment (a new document preference, 20/1000 em by default, in Preferences > Units & Increments); with a selection it changes the tracking of the selected text. Cmd (Ctrl on Windows and Linux) multiplies the step by five. A pair without manual kerning starts from 0. The new type.kernStep command does the work, one undo step per press.
Moving by words is Cmd+Left/Right on macOS and Ctrl+Left/Right on Windows and Linux; Option/Alt+Shift+Left/Right still extends the selection by words. Fixes#621.
Selected type on a path shows its start and end brackets across the path,
as in InDesign, and the Selection and Direct Selection tools drag them
along it (Move Bracket, one undo step): the text starts and ends where
they are, and text past the end bracket is overset.
- doc: PathType.end (None: the path's end)
- geom: PathWarp::nearest, the arc length of the nearest point
- engine: type.pathOptions {end}; brackets stay on the path and can't cross
- compose: the line runs from the start to the end bracket
- idml: EndBracket read and written
- tools: Cursor::PathBracket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
text-autosize 60551322.
Conflict: crates/doc/src/item.rs: next's reference-point doc and default auto_size_ref 4 (centre, as IDML leaves it) plus the branch's minimum-size and No Line Breaks fields
Conflict: crates/engine/src/lib.rs: autosize pass before next's record_undo_step on the success path; bump_revision for next's global revisions
Conflict: crates/engine/src/cmd/interchange.rs: next's grep_style_warnings and .indd comment plus the branch's autosize::apply on import
Conflict: crates/engine/src/cmd/object.rs: branch's textFrameOptions params doc with next's firstBaseline values
Conflict: ROADMAP.md: progress log both
Conflict: docs/gaps.md: next's optical kerning wording with the branch's auto-size note on #180
Height Only, Width Only, Height and Width and Keep Proportions frames fit
their text after every undoable command (same undo step) and after IDML
import, around the reference point, with minimum height/width and No Line
Breaks. Text Frame Options gains an Auto-Size section; IDML reads and writes
the minimum and no-line-break attributes. Applies to unthreaded frames and
the last frame of a thread; vertical frames and type on a path are left as
they are. (#180)
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
Conflict: crates/engine/src/cmd/layout.rs: Document Setup keeps next's refusal of an out-of-range start page; layout.section's spec keeps next's styles (kanji) with the PR's 1–99999 bound; the section body takes the PR's SectionEdit, whose start number goes through next's start_number check; tests keep next's section_start_numbers_are_bounded with the PR's one-undo-step test (the PR's start_numbers_are_bounded expects Document Setup to clamp, which next refuses, so it is left out)
Conflict: crates/doc/src/lib.rs: kept next's iterative saturating page_number; dropped the PR's second MAX_PAGE_NUMBER (next has it in page.rs)
Conflict: crates/ui-egui/src/dialogs.rs: next already leaves Enter and Escape to an open popup; kept next's side and added the PR's Style-menu key test
Conflict: crates/engine/src/cmd/mod.rs, crates/ui-egui/src/{i18n.rs,i18n/it.rs}: rebased duplicates; kept next's side