Commit Graph
1203 Commits
Author SHA1 Message Date
Trevor BrownandClaude Fable 5.1 5e82adfaa9 Merge upstream/main into feat/variables-multi-select-2002
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:51:34 -05:00
Trevor BrownandClaude Fable 5.1 cf2dd3bcf8 Merge upstream/main into fix/justify-cjk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:50:38 -05:00
Trevor BrownandClaude Fable 5.1 a695f15e4b Merge upstream/main into fix/vector-rounded-fill
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:50:37 -05:00
Trevor BrownandClaude Fable 5.1 42d67b97b7 Merge upstream/main into fix/ui-script-fallbacks-2014
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:50:37 -05:00
Trevor BrownandClaude Fable 5.1 50ea611f63 Merge upstream/main into feat/suggest-auto-layout-1823
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:50:36 -05:00
Trevor BrownandClaude Fable 5.1 25cc0eb643 Merge upstream/main into fix/place-crosshair
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:45:02 -05:00
Trevor BrownandClaude Fable 5.1 2e331ecd70 Merge upstream/main into feat/text-style-paragraph
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:45:00 -05:00
857d7089be M3: glass's light is a line on its bevel, and splay turns the bend towards the centre (#1888)
* M3: glass bends with the reference tool's thick-lens profile and splits its colours over 8 steps (#1885)

The backdrop bends most a 1-unit bevel in from the edge, falls off as the
3.5th power of the way out of the curve to nothing `depth` further in, and
moves a fifth as much on the bevel itself (it fell off as the square of the
way in from the very edge). Dispersion samples each channel at 8 steps along
the bend with a triangular window, red moving furthest and blue least (it was
three samples, blue furthest), as far apart as the reference tool's: at 100%
red and blue move 0.16 of the bend apart (`Glass::DISPERSION_SPREAD`).
`Glass::max_bend` bounds red's furthest step, so the backdrop read around a
pane covers every sample, and the field margin covers the bevel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: glass's bend stays within `max_bend`, checked on a single bright pixel too (#1885)

Review follow-up: the bend reaches `depth` × `refraction` a bevel in from
the edge, so `Glass::max_bend` drops its 0.999 and bounds it, as a test now
checks for several refractions. A single white pixel on black pins the
dispersion's triangular window, which a ramp and a flat backdrop can't tell
from one centred sample. The parity assessment records the bend, the
dispersion and how they were measured against the reference tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: glass's light is a line on its bevel, and splay turns the bend towards the centre (#1885)

The light is a line on the 1-unit bevel added to the backdrop, as the
reference tool's: fully where the edge faces the light, none below 0.35 of
facing it and 0.6 as much on the opposite side, with a faint gradient across
the curve, 5% darker facing the light and lighter opposite. It was a glow
mixed towards white, as deep as `splay` made it, so `light_reach` goes.

`splay` turns the bend from along the edge's normal towards the way away
from the layer's centre, and no longer widens the light: files with a splay
above 0 look different. The direction to the centre is worked out in the
shape's own frame (`own_frame`, shared with the field cache), so cut renders
match the whole one byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: cut renders of glass match with splay turning a dispersed bend (#1885)

Review follow-up: the cut-versus-whole test had splay only on looks without
refraction, so no turned bend reached the dispersion. A look with splay 1 and
the default bend and dispersion now renders the same however it is cut. The
parity assessment records the light on the bevel and splay's new meaning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: glass's flat inside skips the bend's curve, and full dispersion stays within `max_bend` (#1885)

Review follow-up: `bend_at` returns 0 straight away past `depth` + the
bevel, where the curve and the bevel's easing are both 0, instead of
working out a power of 0 for every pixel of a pane's flat inside. A test
moves one white pixel along a row under every bend of the profile at full
dispersion and checks that no channel ever reads it further out than
`Glass::max_bend`, which sizes the backdrop read around a pane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: glass's light shows over a transparent backdrop, and its flat inside skips the light (#1885)

Review follow-up: the light was added to the backdrop's colour and then
clamped to the backdrop's alpha, so with nothing under the glass (an
export of the pane alone) the line on the bevel vanished, where the old
glow raised the alpha and showed. Now the alpha rises as far as the lit
colour needs (`lit`), and stays the backdrop's wherever the light fits
under it, so renders over an opaque backdrop don't change.

Past `depth` + the bevel the rim light, the gradient and the bend are all
0, so those pixels take one sample of the backdrop and skip the light's
direction, facing and splay work, as the old glow's `reach` early-out did.

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>
2026-11-05 22:44:38 +01:00
Trevor BrownandClaude Fable 5.1 cd1409e089 Merge upstream/main into fix/control-arrows-pan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:41 -05:00
Trevor BrownandClaude Fable 5.1 fffdf98c0f Merge upstream/main into fix/field-alt-scrub
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:40 -05:00
Trevor BrownandClaude Fable 5.1 1cc9fb5535 Merge upstream/main into fix/text-mixed-fill-section
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:39 -05:00
Trevor BrownandClaude Fable 5.1 b7a7f2891a Merge upstream/main into fix/justify-cjk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:36 -05:00
Trevor BrownandClaude Fable 5.1 d5d8f1e8ed Merge upstream/main into fix/vector-rounded-fill
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:35 -05:00
Trevor BrownandClaude Fable 5.1 7c6bc49e9f Merge upstream/main into feat/text-direction-1830
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:34 -05:00
Trevor BrownandClaude Fable 5.1 81a2afe724 Merge upstream/main into fix/ui-script-fallbacks-2014
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:34 -05:00
Trevor BrownandClaude Fable 5.1 2baaa452ec Merge upstream/main into feat/boolean-button-icons-2003
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:33 -05:00
Trevor BrownandClaude Fable 5.1 6b83eaa331 Merge upstream/main into feat/suggest-auto-layout-1823
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:33 -05:00
Trevor BrownandClaude Fable 5.1 527307178e Merge upstream/main into feat/variables-multi-select-2002
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:32 -05:00
fe3567a56b M3: glass bends with the reference tool's thick-lens profile and splits its colours over 8 steps (#1887)
* M3: glass bends with the reference tool's thick-lens profile and splits its colours over 8 steps (#1885)

The backdrop bends most a 1-unit bevel in from the edge, falls off as the
3.5th power of the way out of the curve to nothing `depth` further in, and
moves a fifth as much on the bevel itself (it fell off as the square of the
way in from the very edge). Dispersion samples each channel at 8 steps along
the bend with a triangular window, red moving furthest and blue least (it was
three samples, blue furthest), as far apart as the reference tool's: at 100%
red and blue move 0.16 of the bend apart (`Glass::DISPERSION_SPREAD`).
`Glass::max_bend` bounds red's furthest step, so the backdrop read around a
pane covers every sample, and the field margin covers the bevel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: glass's bend stays within `max_bend`, checked on a single bright pixel too (#1885)

Review follow-up: the bend reaches `depth` × `refraction` a bevel in from
the edge, so `Glass::max_bend` drops its 0.999 and bounds it, as a test now
checks for several refractions. A single white pixel on black pins the
dispersion's triangular window, which a ramp and a flat backdrop can't tell
from one centred sample. The parity assessment records the bend, the
dispersion and how they were measured against the reference tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: glass's flat inside skips the bend's curve, and full dispersion stays within `max_bend` (#1885)

Review follow-up: `bend_at` returns 0 straight away past `depth` + the
bevel, where the curve and the bevel's easing are both 0, instead of
working out a power of 0 for every pixel of a pane's flat inside. A test
moves one white pixel along a row under every bend of the profile at full
dispersion and checks that no channel ever reads it further out than
`Glass::max_bend`, which sizes the backdrop read around a pane.

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>
2026-11-05 22:27:51 +01:00
8da11395e9 M3: glass effects take variables, and their frost stays within 0–100 (#1886)
* 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>
2026-11-05 22:08:15 +01:00
64f254a823 Colour picker: dragging a gradient handle on the canvas keeps the picker and the handles up (#1852) (#1985)
* M1: dragging a gradient handle on the canvas keeps the colour picker open (#1852)

A press on one of the open gradient's handles or stops closed the picker: the canvas took the press
and began the drag, then the picker, drawn later in the same frame, saw a press outside its card and
closed, committing the drag's interaction at once. The handles vanished with the picker and the drag
went on without an undo step of its own. The handles are the picker's controls: while a gradient
handle or stop drags (`handles::gradient_dragging`, the press included) the picker doesn't close on
an outside press, as it already doesn't while the eyedropper samples. A press elsewhere on the
canvas still closes it. Docs: ui-parity.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* M1: a handle drag released off the canvas ends there (#1852)

Review: a gradient handle drag whose button went up over a panel, over the colour picker or outside
the window stayed stashed, its interaction uncommitted, until the pointer was back on the canvas,
and the next press there moved the handle to wherever it landed; with the picker now held open while
a gradient drag is stashed, it would also have stayed open meanwhile. `handles::end_released_drag`
commits and clears a stashed drag when the button is up, called from the canvas input's off-canvas
branch and when the pointer is over something drawn above the canvas. Test:
`a_gradient_drag_released_off_the_canvas_ends_there` (released over the sidebar and over the picker).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* M1: a handle drag ends on a release over a ruler, and when Escape or Undo closes its gesture (#1852)

The release check ran only where the pointer was not on the canvas, after the
comment pins and ruler guides had their turn; both return early, so a gradient
handle released over a ruler stayed dragged, uncommitted, until the next press.
It now runs first, wherever the pointer is, and the frame it ends a drag on is
the handle's, as the release on the handle itself was.

Escape (the picker closes and commits) or Undo can also close the gesture while
the button is still down. The stashed drag then kept setting the fill every
frame with no gesture open, one undo step a frame; with no gesture open the
drag is now over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 22:07:52 +01:00
Trevor BrownandClaude Fable 5.1 4fcb95eeab M3: the Local variables window selects several variables with Shift and Cmd/Ctrl, and acts on them at once (#2002)
The window keeps a selection of variable ids: a click selects one row, Shift+click the range from the anchor through the rows shown, Cmd/Ctrl+click toggles a row, and Escape or a click on the empty space clears it. Selected rows use the sidebar's selected-row colour.

Right-clicking a row that is part of a selection of several offers Delete, Move to group and Scope for all of them, each sent as one command.batch so it is one undo step. Rename and value edits stay single-row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 15:54:11 -05:00
Trevor BrownandClaude Fable 5.1 877d0c61dd M1: the boolean operation buttons carry original icons (#2003)
The Design panel's Boolean groups menu listed Union, Subtract, Intersect and Exclude as text only. Each entry now leads with its operation's icon, drawn in code from polygons and rectangles: two offset squares with the result filled at reduced alpha. The commands, enabled rules and layout are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 14:44:32 -05:00
Trevor BrownandClaude Fable 5.1 19e3728fa5 M2: Suggest auto layout warns about uneven cross offsets and refuses instances (#1823)
Bands that start at different offsets across the flow (a centred title above a card) are lined up at the smallest offset, which moves some of them, so the result now carries a warning for it. Instances and layers inside them are refused before anything changes, with the same error Convert to group gives; a main component still works.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 11:55:12 -05:00
Trevor BrownandClaude Fable 5.1 4c793faf51 M2: Suggest auto layout adds nested auto layout frames by rows and columns and keeps the placement (#1823)
layout.suggest used to run layout.add and made one row of everything. It now splits a frame's layers into rows or columns by the gaps between them, wraps each band of several layers in a hugging frame without fill and repeats inside it, so the design keeps its placement where the gaps are even. The frame keeps its size, and uneven gaps and layers that overlap along both axes come back as warnings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 11:06:33 -05:00
Trevor BrownandClaude Fable 5.1 a63f0d7937 Docs: name the Indic scripts the UI fallback covers (#2014)
From review: "the Indic scripts" promised more than the sample list holds. The localization doc, the ROADMAP and the families comment now say Devanagari, Gurmukhi, Gujarati, Tamil, Telugu, Kannada and Malayalam.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 10:41:00 -05:00
Trevor BrownandClaude Fable 5.1 2c92a16944 M10: UI script fallbacks leave Bengali and Thai to the CJK path; Hindi row and ROADMAP status (#2014)
From review: crate::cjk picks the Bengali and Thai faces after this list in its own order, so the two scripts come out of SCRIPT_SAMPLES and their families out of SCRIPT_FAMILIES; the Hindi row says the desktop UI takes an installed Devanagari face; the ROADMAP status line and Localization row carry the change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 09:43:47 -05:00
Trevor BrownandClaude Fable 5.1 facc2a2178 M10: the UI takes a system face for Ethiopic, the Indic scripts and the other scripts its fonts lack (#2014)
A String variable's Amharic value drew as boxes in the panels: `bidi::script_fallbacks` sampled Hebrew and Arabic only. It now samples one letter of each of 18 scripts against the system families that draw them on Windows, macOS and Linux; `theme::system_fallbacks` takes the first installed one per script, once. Test: `bidi::tests::other_scripts_take_an_installed_system_face`; localization doc and ROADMAP updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 08:50:45 -05:00
Trevor BrownandClaude Fable 5.1 c16c3676c2 M4: text direction: CSS keeps Left beside a forced rtl, paragraph arrays read to the paragraph count, the empty last paragraph keeps its direction (#1830)
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>
2026-11-05 03:46:28 -05:00
Trevor BrownandClaude Fable 5.1 cd5c9aeb09 M4: a paragraph's base direction can be forced: Text ▸ Text direction (#1830)
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>
2026-11-05 03:23:07 -05:00
ashokram-si f35af507ec M1, #1758: mouse buttons 4 and 5 pan the canvas like the middle button 2026-11-05 13:04:17 +05:30
JonRC a55a728c4c M6: JPG exports take a quality and embed the sRGB profile (#2004)
file.export always encoded JPG at quality 90, and an sRGB document's JPGs
carried no colour profile, so viewers could only assume sRGB. PNG exports
always get an sRGB chunk, and Display P3 JPGs already embedded their ICC
profile.

- ExportSetting gets a quality (1 to 100, default 90, saved only when it
  isn't 90), parsed with the other export options: file.export, export.add,
  export.update, file.exportLayers and MCP export take it.
- encode_export_with takes the quality; encode_export keeps quality 90.
  Every JPG now embeds the document's profile, sRGB included.
2026-11-05 00:09:19 -03:00
Andreas Haraminac cd77b017bd M6: correct architecture guide for existing .fig support 2026-11-04 19:06:37 +00:00
Trevor BrownandClaude Fable 5.1 cacf73a094 M8: a reconnected loop is closed only where the segment makes it one continuous chain (#1951)
A loop whose segments are the chain between the two ends but are listed out of walking order has
no place for the new segment that makes it continuous; inserting it at the first gap would leave
the other gaps. Such a loop is left as it was (the walkers' bridging still draws it), as a loop
broken in two places is. Test: the chain A-B-C-D-E with its loop listed out of order, joined A to E.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 06:45:00 -05:00
Trevor BrownandClaude Fable 5.1 9ff2105d9b M8: a reconnected loop keeps its direction, so a hole stays a hole whichever end was clicked first (#1951)
Cause: `ops::connect` rebuilt the broken loop along the path from the second end to the first, so
connecting the ends in the other order reversed the loop's walk; a hole wound against its outline
then wound with it and the non-zero rule filled it.

Fix: the new segment goes into the loop at its gap, in the loop's own order. Test: a ring whose
hole loses a corner with Delete and is connected again, either end first (red before for one
order).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 06:03:00 -05:00
Trevor BrownandClaude Fable 5.1 4cd01f0a04 M4: a justify gap is reordered with the line's runs, so a nested embedding gets its gaps at its edges (#1970)
Cause: `visual_gaps` mapped a gap at a run boundary to the gap before the later run's first item,
which for a run nested inside another (a Chinese word in a Hebrew phrase in Chinese text, made
with embedding marks) is a gap inside the embedding: one boundary took it, another got none, and
the line came up a gap short of the box's edge.

Fix: each gap is reordered with the line as a glyph of no width at the lower of its two
neighbours' levels (rule N1 for a space between them), through the same `visual_order`: items and
gaps keep alternating and each gap lands once at the boundary it belongs to. Test row with explicit
embedding marks (red before: "char 8 moved 2 (want 1)").

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 06:01:39 -05:00
Trevor BrownandClaude Fable 5.1 de881846a8 M8: joining the ends of a path broken open closes its fill loop through the new segment (#1951)
Cause: deleting a point with Delete (not Shift+Delete) opens the path and leaves its region's
loop with a gap, filled closed by a straight line across it; joining the two ends again (the pen
onto the other end, Connect, Join) added the segment to the network but not to the loop, which
still spanned the gap. Unrounded, the straight line and the segment coincide; once the points are
rounded, the fill and the selection outline run straight across while the stroke, walked from the
network itself, follows the segment and its arcs (#1506's picture).

Fix: `ops::connect` splices the new segment into the loop whose segments are the chain between
the two ends, so the loop closes through it; a chain no loop fills still becomes a region of its
own. Tests: the chamfer made by Shift+Delete, by Delete then Connect, and drawn with the pen, each
rounded, with the fill tracing the stroke (engine); every loop order and segment direction of a
chamfered rectangle (doc).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 04:35:37 -05:00
Trevor BrownandClaude Fable 5.1 95d71a6825 M4: justified Chinese, Japanese and Korean text spreads the room between its characters (#1970)
Cause: justified lines only widened their spaces, so a line of ideographs, written without any,
kept its natural width and its right edge never reached the box's.

Fix: the room on a line (but a paragraph's last) also goes into the gaps next to an ideograph,
kana, Hangul or full-width form wherever a line could break there (`breaks::justify_between`,
beside `break_between`): each is a word of its own, as for line breaking, and no gap opens before a
closing mark or after an opening one. Spaces widen as before; mixed text widens both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 03:59:46 -05:00
Trevor BrownandClaude Fable 5.1 31dcd0e5fe M6: a saved connector path going back into its object is routed instead (#1910)
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>
2026-11-04 03:46:49 -05:00
Trevor BrownandClaude Fable 5.1 5855c8705c docs: boards-parity's status line and revision history move with its .jam row (#1910)
Review: the row changed without the status line and a history row, which land together.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 02:39:24 -05:00
Trevor BrownandClaude Fable 5.1 bda7ca910d M6: a FigJam elbow connector saved with its path keeps it on import (#1910)
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>
2026-11-04 01:52:02 -05:00
Trevor BrownandClaude Fable 5.1 7be7c054eb M12: a connector joined to a layer inside a moving section is no snap target; the docs say which connectors share a run (#1916)
Review: a section moving carries its stickies and their connectors, so those connectors and
their labels are no snap targets for the section either (an end counts as carried when its node
or any ancestor is moving). The parity rows, the END_GAP doc and the test's doc no longer say
connectors from one side run together: elbows share their first run along the normal, curves
leave tangent to it, straight ones fan out from the same point.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 21:46:34 -05:00
Trevor BrownandClaude Fable 5.1 2dc7bed2a0 M12: connectors attached to a side start 4 pt outside the border (#1916)
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>
2026-11-03 21:08:19 -05:00
Trevor BrownandClaude Fable 5.1 5eb28b310a M12: the bar above the selection stays away while a quick-create handle is dragged (#1919)
Review: letting a handle go over the selection bar made the next shape behind it. The bar
hides while a "+" is dragged, as it does for any drag (board_connect::dragging), so there
is nothing to drop behind; the drop makes the next one where the bar was, and the bar is
back once typing in it ends. Test the_bar_stays_away_while_a_handle_is_dragged; the docs
rows say the toolbar and the bar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 14:47:46 -05:00
Trevor BrownandClaude Fable 5.1 246a89733c M12: quick-create drag: stickies and shapes in sections, parent coordinates, the handle's side, no drop off the canvas (#1919)
Review: the drop target is the topmost sticky or shape under the pointer, sections and frames
looked into (board::sticky_or_shape_at, instead of the stamp's any-layer hit test on the page's
children); the drop point is placed in the parent's coordinates; the connector leaves by the
handle's side (as the preview draws it) and arrives by the facing side; a handle let go off the
canvas makes nothing; the parity rows on quick-create handles agree.

Tests: dropping_a_handle_on_a_shape_inside_a_frame_joins_it, letting_a_handle_go_over_a_panel_makes_nothing,
the_next_one_goes_where_it_is_dropped (frame case).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 12:44:59 -05:00
Trevor BrownandClaude Fable 5.1 d48aa59ec7 M12: drag a "+" handle to drop the next shape anywhere or join another object (#1919)
The quick-create handles only took a click. As the help says ("drag the preview to any location
on your board"), a handle can now be dragged: the preview follows the pointer and, let go on empty
board, the next sticky or shape is made there, centred on the pointer, joined by a connector whose
ends take the sides facing each other (board.addConnected gains at: [x, y]); let go on another
sticky or shape, a connector joins the two instead (board.connector), that object outlined while
over it. figcraft_engine::board::object_at exposes the hit test a stamp uses. The click is as
before.

Tests: board_connect_tests (two), board_connected_tests::the_next_one_goes_where_it_is_dropped.
Docs: boards-parity row, status line and revision history.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 11:52:41 -05:00
Trevor BrownandClaude Fable 5.1 be04efc4fb M10: embeddable script faces as document fallbacks; the Arabic UI with Arab embedded (#1902)
Review: Noto Sans Hebrew, Devanagari and Georgian join Arabic and Thai in FALLBACK_FAMILIES, so a
web build that embeds one draws that script in an Inter document; with Arab embedded the picker
offers the Arabic UI catalog, as cjk::can_show intends (the docs said otherwise); the docs
example lists one tag per script.

Test: every_embeddable_script_face_is_a_document_fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 09:41:15 -05:00
Trevor BrownandClaude Fable 5.1 1354c6bb6f M10: FIGCRAFT_EMBED_SCRIPTS embeds other scripts' craft-fonts faces in a web build (#1902)
The browser has no system fonts and the text crate's build script keeps the web catalog to Inter,
Noto Sans Georgian, the wordmark and (with FIGCRAFT_EMBED_CJK) Noto Sans CJK, so craft-fonts'
Noto Sans Arabic never reached a web build and Persian or Arabic text drew as missing-glyph
boxes. FIGCRAFT_EMBED_SCRIPTS names script tags as the manifest lists them (Arab, Hebr, ...);
a web build keeps the faces of those scripts as well. The default build is unchanged.

Test: web_embeds_other_scripts_faces_only_when_asked (build script). Docs: localization-parity
(web fonts paragraph, Arabic row, status line, revision history), translating.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 09:23:44 -05:00
Trevor BrownandClaude Fable 5.1 bb7d568d49 M2: baseline alignment puts a layer without text on the line with its bottom (#1825)
In the baseline branch of the flow layout only an item with a text baseline got one; any other
layer stayed at the top of the line. A layer without a text baseline now uses its bottom edge, as
the help center's figure shows ("the bottoms of the icon and the word home are aligned on the red
line"): an icon next to a text sits on the text's baseline, a layer reaching below it moves the
text down, and a line with no text lines the layers' bottoms up (CSS does the same).

Tests: alignment_rules (three cases), baseline_alignment_horizontal, engine
baseline_alignment_puts_an_icons_bottom_on_the_text_baseline. Docs: auto-layout-parity row,
status line and revision history.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 09:13:19 -05:00
Carbocode 901bc6d4fc M8: compute layer selection roots once per visible row 2026-11-03 10:39:24 +01:00