The scaled-instance round-trip test now asserts the scaled width and height instead of printing them. text_path read the layer's raw size, so a text on a path inside a scaled instance kept the main's path length; it now uses the scaled or fitted size.
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
- A nested instance's override and derived rows (and its text property values) are written in the enclosing main's space: OverrideRow carries the factor of the layer that supplied it and View::with takes (row, scale) pairs. Test: a_nested_instances_own_rows_are_at_its_enclosing_mains_scale.
- Progressive blur startOffset and endOffset are unit-box fractions, not lengths: only offset and noiseSize scale. Test: a_progressive_blurs_positions_are_not_lengths.
- An effect or grid style takes the factor of the layer that links it (1 when an instance row swaps it), not the main's. Test: an_effect_style_takes_the_scale_of_the_layer_that_links_it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 reviewed; Sonnet 5.5 wrote at high
An instance scaled with the Scale tool in Figma stores `symbolData.uniformScaleFactor`; the importer never read it, so the layers of a 128 px instance of a 256 px component came in at 256 px with the main's radii, strokes and spacing. Layers of an instance now take the main's lengths times the factor (composed through nested instances), the same values the Scale tool scales, unless an override or derived row gives the value, which is already at the instance's scale. Eight tests: the reproducer, the round trip, a derived size row not scaled twice, override rows, auto layout gap/padding/text, nested composition, factor 1 or none, and junk factors ignored.
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 high
* M3: glass effects take variables, and their frost stays within 0–100 (#1885)
Glass's light angle, intensity, refraction, depth, dispersion, frost and
splay bind to number variables, on a layer (`variable.bind`,
`effects/<i>/<field>`, `radius` being the frost) and in an effect style
(`style.bind`), and the Design panel's glass fields take a variable like
the shadow and blur fields. A variable reads as the field shows it:
intensity, refraction, dispersion and splay in percent.
A glass's frost stays within 0–100, the reference tool's range: a larger
frost from an older file draws at 100, and the field, a bound variable and
scaling stop there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: a frost from an older file scales and shows as it draws (#1885)
Review follow-up: a frost saved past 100 draws at 100, so scaling a layer
starts from that (150 halved draws at 50, not 75) and the Design panel's
Frost field shows it. The revision row's binding path escapes its pipes, so
the table keeps three columns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: Dev Mode and .fig export show a frost from an older file as it draws (#1885)
A frost saved before the 0–100 range draws at 100, and the canvas, CSS and
scaling already read it that way, but Dev Mode's Glass row and the .fig
export still read the stored number, so they said 150 where the canvas
shows 100. Both now read the sanitized frost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Oleksii Sukholita <oleksii.sukholita@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
From review: `text-align: left` beside `direction: rtl` (Left is absolute on the canvas); the REST import reads the four parallel paragraph arrays up to the paragraph count, not the 256-entry list limit; `rtl_para` gets an entry for the position after the last char so an empty last paragraph keeps its forced direction; the localization status line.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Direction came from the letters only; Add right-to-left text says controls override it per paragraph (Text > Text direction, Left to right or Right to left). `TextDirection` on `ParaList`, `Bidi::of` with a base level per paragraph, `text.direction` and the three menu commands (the edited paragraphs while editing), `node.set textDirection`, the Type settings control once a right-to-left script is in the text, inspect `textDirection` / `lineDirections`, the CSS `direction`, and the `.fig` lines' `sourceDirectionality` in and out.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: the first and last runs were checked for lying at right angles to their sides, not for
going out of the object, so a path whose first run went up through the start's box (or whose last
arrived from inside the end's) was kept and drawn through it. Both runs now have to go the way the
side faces; one along the side or back into the object leaves the connector to the router. Two more
cases in the routed-here test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `.jam` importer routed every elbowed connector itself, so one drawn around a layer by hand in
FigJam came in routed through it. FigJam saves the path as `connectorControlPoints`: one per run
between the first and the last, each a point on its run (in the connector's parent's coordinates)
with the run's axis; the first run leaves the start's side and the last arrives at the end's. They
become the `ConnectorPath::Runs` a board connector reshaped by hand has, walked by
`figcraft_doc::route_along`, so the path is kept exactly. Points that don't make such a path (off
both axes, two runs the same way in a row, a first or last run sideways to its side, more runs than
a hand could make) leave the connector to the router as before, and only connectors routed here
count in the "own routing" note. `figcraft_doc::attach_points` and `MAX_RUNS` are exported for it.
Docs: file-format-parity.md, boards-parity.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A connector end attached to a side was routed from the middle of that side, so the stroke's
centre line started on the border and half its width plus the round cap lay on the fill, and
several connectors leaving one side piled up into a knot sticking into the card. FigJam starts
the line a few pixels outside the border and connectors leaving one side run together along the
side's normal before they fan out (the issue's screenshots). `board_route::END_GAP` (4 pt): an end
attached to a side (`RouteEnd::Box`) starts that far outside it along the side's normal; an end
placed by hand on the object (`RouteEnd::On`) keeps its exact point; a free end is unchanged.
The pinned route coordinates in the engine, format and UI tests move with it; the overlay's
attach points stay on the border. Docs: the Connectors rows, status line and history of
boards-parity.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kurbo turns an arc into cubics by radius divided by tolerance, so the fixed
0.01 let an ellipse, polygon corner, rounded rectangle, per-side stroke
corner or SVG arc with a radius like 1e300 ask for more cubics than memory
holds before any size check saw the geometry. Arcs now use a tolerance
that scales with the radius, and SVG path data from documents, commands and
imports is refused before parsing when a number passes 1e9.
An end whose magnet is AUTO has no fixed point: FigJam picks the side when it draws, and the file
keeps whatever relativePosition the end last had, so the importer sent every automatic end out of
the stale side (the left one in the reporter's file). AUTO and CENTER ends now attach to the side
facing the other end, as a board connector drawn here does, for straight, elbowed and curved
connectors alike; named magnets and ends without one are unchanged.
Test: connectors_with_an_automatic_side_leave_the_side_facing_the_target (the fixture schema gains
the ConnectorMagnet enum). Docs: file-format-parity .jam row, status line and revision history.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shared unpacking total (and Sketch's) was debited only when a member
read succeeded, so a snapshot or page member damaged near its end could be
named again and again by an index and unpacked each time without ever
spending the budget. A failed read is now charged its recorded size; one
refused for that size before any unpacking still costs nothing.