Choosing Crop from the image fill's mode dropdown only set scale_mode: crop
mode never started, the first press on a handle closed the picker and went
to the tool, and the picture lost its place (no placement crop). For one
layer's image fill it now runs image.crop, as Object > Crop image and the
Fill row's button do; other targets only set the mode, as before.
Fixes#2150
On Windows, Roots::check resolved every path (canonicalize opens it) before
comparing it with the roots, so `\server\share\x` contacted the server, and
could send it the user's credentials, before it was refused. Now the path as
given and made absolute is classified by its spelling with craft_docpath
first: another computer that no root's share is on, a device path, or a DOS
device name anywhere in it is refused, and so is a link on the way that
leads to one (followed by its target's spelling, up to 40 links).
Fixes#1094
The focus request was reset only while the Design panel drew, so choosing the tool again on the
Prototype tab left the field without the keyboard; it now follows the frame the section last
showed in. Whether the factor field had the keyboard was read before the widgets ran, so an
Enter right after a click into W or H could end the tool; it is read after them now. A number
field's leftover edit from when it was last shown no longer swallows the focus request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
The panel set the origin to the centre once per app. The origin is the Scale tool's drag pivot
too (scale_factor), so that also changed handle drags, unmeasured against Figma, while the
engine and its tests keep top-left. This PR keeps to the focus and the Enter and Escape
endings the issue asks for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
The Scale tool now opens its factor field focused with its text selected (once per activation), the origin starts at the centre, and Enter or Escape in the field returns the tool to Move with the selection kept. Measured in Figma web, 2026-11-09. Docs: ui-parity Scale tool row and revision.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and briefed; Sonnet 5.5 measured and wrote at medium
Adds the invert_zoom preference and the pref.invertZoom command, a checkbox in the Preferences window after Scroll wheel zoom. With it on, Cmd/Ctrl+wheel and a plain mouse wheel (with Scroll wheel zoom on) zoom the other way by the same factor; a pinch and panning are unchanged. Documented in docs/ui-parity.md.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and briefed; Sonnet 5.5 measured and wrote at medium
`paragraph_style` compared the paragraphs `para_span` gives for the whole text, which leaves
out an empty paragraph after a final newline, so Balance paragraphs over an Auto trailing one
made a Balance style. It now compares every paragraph by index, as the list style does. The
parity row says the wrap styles are lost through a .fig round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
Adds ToolId::Annotation and tool.annotation {id?}. With a layer selected (or an id) it asks the UI for that layer's note at once; in the tool a press on a layer selects it and asks, a press on empty canvas does nothing, and the engine never writes the note. annotation.new shows the Inspect tab (not a toggle) and focuses the layer's new-note field once. The tool passes Developer Mode's gates and stays selected until V or Escape (inferred; Figma's placement, cursor and stay-in-tool behaviour are unmeasured). Docs, shortcut counts (137 documented, 136 bound) and tests updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 briefed; Sonnet 5.5 wrote at medium
Within its own row or column, a dragged child's slot follows the box it would fill under the pointer (start bounds plus the unsnapped drag), by the edge facing the drag, not the pointer. Measured in Figma's web app, 2026-11-09. Dragging in from outside, wrapping layouts and grids keep their rules. Several moving layers use their combined box.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and briefed; Sonnet 5.5 measured and wrote at medium
docs/ui-parity.md conflicted on the wrap style and text direction rows: kept this branch's text with #2135's Developer Mode naming.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
Figma holds a US registration for DEV MODE (US 7574348). Display text only: the view.devMode label, Inspect tab, shortcut row, refusal messages, CLI help, docs and all locale catalogs now say Developer Mode. Command ids, preference keys and saved fields are unchanged; "Dev Mode" stays a search alias in the Actions palette. Adds a naming guard test.
Drops the "Figma-class" self-description from the MCP instructions and the "Accessibility at Figma" aside from pref.uiScaleUp help. Help text only; ids and preference keys unchanged. Adds a display-name guard over mcp/src and engine/src/cmd string literals.
kurbo's simplify_bezpath, which smooths Pencil samples and runs Simplify
path, fitted two common shapes with a curve that ran far off the path: a
stroke that goes out and back to the very point it left (kurbo keeps a
reversal as a smooth join) and nearly collinear samples (kurbo measures
its error across the path, not along it).
figcraft_geom::simplify_path cuts the path where it turns back by more
than a right angle, simplifies each run with kurbo, and keeps a run as it
was when its fit strays more than twice the accuracy from it. The Pencil
and vector.simplify use it.
One Escape selects nothing for a top-level frame, a top-level layer, two layers in one group,
layers in different parents, and a nested layer with a top-level one. With the focused frame
itself selected, one Escape leaves the focus view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
craft-update moves to a craft-libs branch that merges storytold/craft-libs#687 and #696.
- The check result window shows for every status but idle and stays
closed while the startup prompt is open, so the first start after an
update reports that it installed or why it failed. Restart to Update
calls Updater::restart_to_update.
- on_exit calls Updater::start_pending_update before the process ends,
which starts the install of an update waiting for the app to close.
The handler grouped equal offsets by a linear scan per point, quadratic when every point moves
by its own offset. Only a single move covering every point carries the regions' paint frames,
so it now checks in one pass whether all offsets are equal: if so the points move as one,
otherwise each moves by its own offset. The result is the same as before.
Tests: booleans, a valid x with a refused y, and the overflow case are refused with no undo
step; nothing selected is refused; a shared Y scrubs every point in one undo step and a Mixed
X doesn't scrub. selection_position's doc comment names its one user, vector.edit.bounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
Found while testing crates/engine/src/cmd/file/open.rs:33 (open_recent, `.nth(i.max(0.0) as usize)`)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Figma rounds every point whose style doesn't set a corner radius with the
layer's cornerRadius. The import read only the point's style, so those
vectors came in sharp. Points now fall back to the layer's radius, at the
scale of the layer that carries it (#2091), and the network cache keys on
it. The export writes an explicit 0 for a sharp point on a rounded vector
so it stays sharp.
CodeRabbit on #2121: with points selected in several edited layers, the X and Y fields read and set only the active layer's points (`selected_parent_points` and `vector.edit.position` use the edited layer), so the status lines, the vector edit row and the revision entries now say so, and the row lists X and Y over several edited layers under Not yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
CodeRabbit on #2121: the Vector edit mode row in ui-parity.md put the new Point-section X/Y behaviour after "Not yet:", contradicting the row's done status and the revision entry. It now sits with the implemented features; only areas of other layers edited at once stay under Not yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
Setting each point's own coordinate moved the points one at a time, so when every point of a layer moved by the same offset (`{"x": {"add": 10}}` on all of them) `ops::move_vertices` no longer saw the whole network move and left the regions' paint frames behind. Points with the same offset now move together.
Verified: `moving_every_point_by_one_offset_carries_the_regions_paint_frame` failed before this and passes; vectoredit tests 92 passed, junk sweep 49 passed, clippy and fmt clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
Cause: with several points selected, the Point section's X and Y showed the selection's top-left and `vector.edit.position` moved the group so that its top-left landed on the typed value. Figma shows the value the selected points share (Mixed when they differ) and sets every selected point's own coordinate.
Fix: `vector.edit.position {x?, y?}` takes, per axis, a number (every selected point's own coordinate becomes it, in the parent's coordinates) or `{add}`, `{mul}`, `{div}` applied to each point's own coordinate; each point moves by its own delta with its handles, in one undo step; a zero divisor, a non-finite result, unknown keys and other junk are refused. A new `selected_parent_points` gives the panel the points in the parent's coordinates. The panel's X and Y are mixed-value fields: the shared value (compared within 1e-6) or Mixed, a typed value sets every point, and a relative edit typed into a Mixed field sends the object form. A scrub only starts from a shared value and sets it absolutely each frame inside one interaction, as the other point fields do.
Verified: engine tests for two points at different x, one point, add, mul and div, curve handles, one undo, the parent's coordinates and refused junk; panel tests for Mixed, typing -200 and a relative edit on a Mixed field, and a single point's value; the engine lib tests, the vector and field panel tests, clippy and fmt. Measured in Figma on 2026-11-08.
Not changed: the W and H row, other edited layers' points, and scrubbing semantics (the Mixed field cannot be scrubbed, as no start value exists).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove, measured and briefed; Sonnet 5.5 wrote at medium
Found while testing crates/doc/src/props.rs:1394 (ExportSize::parse, `num.parse::<f64>()`)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/edit/props.rs:248 (reparent, `index = num(p, "index").map(|i| i.max(0.0) as usize)`)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- "Past year" was "Viimeinen vuosi" (the last calendar year); the filter means
the last 12 months: "Viimeiset 12 kuukautta" (CodeRabbit on #1993).
- 40 UI strings (pick select, field scrubbing, live session reconnect, text
direction, padding, variable scopes), 7 command labels, 3 menu wordings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut with nothing to copy (only points that touch no segment) is Disabled and changes nothing, as Copy now is (Copy used to fall through to the whole layer); paste borrows the clipboard instead of cloning it; a partial copy or cut mirrors its SVG to the system clipboard; x without y (or y without x) is refused; the junk test asserts refusals change nothing and accepted results are finite; the edit.copy, edit.cut and edit.paste specs state the behaviour and the mode-specific paste response.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
Found while testing crates/engine/src/cmd/file/export.rs:89 (export; same pattern at :332 in export_layers)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>