Commit Graph
1562 Commits
Author SHA1 Message Date
BFlat f2c593ff6e Merge pull request #2117 from Silvertree2010/fix/h11295-node-reparent-silently-turns-a-n
Reject negative or fractional index in node.reparent
2026-11-12 09:20:42 +07:00
bflatastic eddddb6e3c Merge main into #2105 2026-11-12 09:20:27 +07:00
bflatastic 73682ac144 Merge main into #2121 2026-11-12 09:20:04 +07:00
bflatastic 2c1c3c974a Merge main into #2106 2026-11-12 09:19:47 +07:00
bflatastic 82da8f513a file.openRecent: check the index with the shared req_index 2026-11-12 09:19:05 +07:00
BFlat 0fd45a2eb2 Merge pull request #2107 from TrevorBrown-dev/fix/escape-deselects-measured
Selection: Escape clears the selection instead of stepping out to the parent, as in Figma
2026-11-12 09:18:27 +07:00
BFlat d3138da22f Merge pull request #2120 from Silvertree2010/fix/h11642-exportsize-parse-accepts-a-sign
Reject signs and exponents in ExportSize::parse
2026-11-12 09:17:41 +07:00
Trevor BrownandClaude Sonnet 5.5 c47479bd7f Merge upstream/main into fix/sticky-text-size-2157
boards/board.rs: kept this branch's STICKY_TEXT_SIZE export (sticky_font_size is gone) and main's settle_mind_maps export. Docs: main's status line with ours in front, both revision rows.

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
2026-11-11 20:40:08 -05:00
Trevor BrownandClaude Opus 5.5 3d8ec13850 Merge upstream/main into fix/table-resize-2172
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:39:21 -05:00
Trevor BrownandClaude Opus 5.5 71723cd428 Merge upstream/main into feat/text-style-font-weight-2174
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:38:54 -05:00
Trevor BrownandClaude Opus 5.5 132c6e9686 Merge upstream/main into fix/boolean-corner-radius-2222
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:38:29 -05:00
Trevor BrownandClaude Opus 5.5 20f02bb7c1 M12: Boards: mind maps in the document and engine (#2226, step 1) (#2228)
* M9: Boards: mind maps in the document and engine (#2226)

Part of #2226, step 1 of 4. A mind map node is a board object of its own (BoardItem::MindNode: a frame hugging its text, no box) and a branch is a connector marked `branch`; the tree is read from the branches alone. `board.mindMap` places a first node with three children and `board.mindMapAdd` adds a child or a sibling, each one undo step. The normalize pass (`board::settle_mind_maps`, `figcraft_doc::mind_layout`) lays a touched map out again from its first node: 88 between columns, 48 between siblings, each stack centred on its parent; a branch that no longer joins two layers of its page is removed. Branches are curved, stroke 4, #757575, round caps. Numbers are from a FigJam map saved and decoded. No toolbar entry, shortcuts or import yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 designed; Sonnet 5.5 wrote at medium

* M12: Mind maps: a map around its own container stays put; a moved branch re-flows its old map (#2226)

A map holding a layer and something inside it is skipped by the settle pass (it never settled). Every touched branch now stirs its old ends, so re-attaching a branch's start re-flows the map it left. Boards without branches return early, a map over MIND_MAX_NODES is left whole, shift skips non-finite moves, and the doc tests share one six-node helper.

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 medium

* M12: Mind maps: FigCraft's own starter texts (#2226)

A new map's starter children read "Idea 1", "Idea 2" and "Idea 3", as the design specified,
instead of the incumbent's wording, which the first commit had copied from the measurement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote

* M12: Docs: the mind-map revision rows go in the revision history (#2226)

The 2026-11-10 rows sat in ROADMAP.md's Headline numbers table and docs/roadmap.md's Alpha gate
table; each now opens its file's revision history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 00:47:05 +01:00
Trevor BrownandClaude Opus 5.5 b25858550b Merge upstream/main into feat/wrap-style-paragraph-and-style-1827
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:55:44 -05:00
Trevor BrownandClaude Opus 5.5 9162c5832f Merge upstream/main into fix/shift-rotate-multiples-of-15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:55:42 -05:00
Trevor BrownandClaude Opus 5.5 7d6274d6be Merge upstream/main into feat/vector-edit-clipboard-1875
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:55:32 -05:00
Trevor BrownandClaude Opus 5.5 4a9006fc29 Merge upstream/main into fix/arrow-pan-step-measured
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:55:29 -05:00
Trevor BrownandClaude Opus 5.5 0da027292e Merge upstream/main into fix/escape-deselects-measured
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:55:27 -05:00
Trevor BrownandClaude Opus 5.5 721cff21ba Merge upstream/main into feat/vector-edit-point-xy-mixed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:55:21 -05:00
Trevor BrownandClaude Opus 5.5 d5eec3ec6e Merge upstream/main into feat/annotation-tool-1840
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:49:22 -05:00
Trevor BrownandClaude Opus 5.5 02b5152c38 Merge upstream/main into feat/invert-zoom-2145
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:49:17 -05:00
Trevor BrownandClaude Opus 5.5 3132480b17 Merge upstream/main into fix/eyedropper-magnified-pixels-2189
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:49:10 -05:00
Trevor BrownandClaude Opus 5.5 4641f9deeb Merge upstream/main into fix/boolean-corner-radius-2222
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:49:04 -05:00
Trevor BrownandClaude Sonnet 5.5 604c78466c M5: marquee: a drag from inside a frame takes it whole when it takes another top-level frame, as re-measured
In top-level mode the frame the drag started in is now selected whole (re-measure 2: [F1, F2], R1 not selected); with no other frame taken only the touched shapes are. Test 9 uses the re-measure's literal marquees; docs reworded and docs/mcp.md rewrapped.

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
2026-11-11 17:40:52 -05:00
Trevor BrownandClaude Sonnet 5.5 cb52621414 M5: marquee selection follows Figma's measured rules (gap 4)
A plain marquee now selects each top-level layer it touches: a partly covered frame or section gives the layers in it the marquee touches, an enclosed one is selected itself, an empty frame on touch, a rotated shape by its outline. A drag from inside a frame never selects that frame and also takes the top-level layers it touches outside it. Once the marquee selects a frame whole, no frame opens and a partly covered frame comes whole. Measured in Figma's web app, 2026-11-11.

Flipped existing assertions: marquee_selects_what_it_touches, cmd_marquee_selects_nested_layers, a_box_selects_like_a_marquee. Tests: marquee_behaviour_tests.

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
2026-11-11 17:13:26 -05:00
Dylan M. Taylor cf77dfdabc load: an SVG that doesn't fit the allowance is taken out again; charge after cleanup 2026-11-11 21:31:26 +00:00
Trevor BrownandClaude Sonnet 5.5 7c3f9ce7e3 M4: the list start reset runs once per edit and leaves plain paragraphs' levels alone
insert_model cleared the copied start with one set_para_lists call per newline, each scanning the whole text, and set_para_lists also zeroed the level of plain paragraphs. Add TextData::clear_list_starts (one pass, touches only start_offset, no-op when no paragraph has a start) and call it once over the created paragraphs, only when the text has a newline.

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
2026-11-11 16:03:31 -05:00
Dylan M. Taylor 3b9bf8c9a2 batch: bound how much referenced data one step's params copy (#2257)
* batch: bound how much referenced data one step's params copy

* batch: check a list's size before queuing its elements
2026-11-11 15:55:11 -05:00
24cf70329b M5: Locked layers follow Figma's rules: no nudge, a locked parent's children stay locked (#1560) (#2219)
* M5: Locked layers follow Figma's rules: no nudge, a locked parent's children stay locked (#1560)

Pins Figma's locked-layer behaviour (measured in Figma's web app, 2026-11-10; help center "Lock and unlock layers") in a new lock_behaviour_tests suite. Fixes the divergences: transform.nudge and transform.nudgeSize skip locked layers, object.toggleLock without a value counts a child of a locked parent as locked (so it never locks the child on its own), and the Layers panel padlock on such a child is inert.

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

* Layers panel: a padlock sweep skips the children of a locked layer (#1560)

A padlock drag sweep no longer collects rows with `locked_above`, so
dragging across a locked frame's children leaves their own flags alone
and draws no swept value on them. An eye sweep is unchanged.

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

* M5: Lock/Unlock leaves a locked layer's child alone in a mixed selection (#1560)

With a locked frame's child and an unlocked layer selected, Lock/Unlock set both own flags, so
the child stayed locked once the frame was unlocked. With no explicit `locked` value, the command
now skips a layer whose parent is locked, as its padlock does; an explicit value (scripts) still
sets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-11 21:06:56 +01:00
Dylan M. Taylor 6e9582c0a0 codeConnect.import: index components by name and describe each once (#2265) 2026-11-11 14:48:52 -05:00
Dylan M. Taylor 6bc8bbaef1 inspect: work out a grid's cells once per inspection (#2260) 2026-11-11 14:48:38 -05:00
Trevor BrownandClaude Sonnet 5.5 14670b99d4 M3: an instance lists the properties its variant uses; long property names stay on one line (#2022)
In Figma's data a component set holds one text property per variant (plus leftovers of deleted variants), and an instance lists only the ones its current variant's layers read (measured in Figma's web app). FigCraft listed every definition on every instance, and wrapped a long property name inside an 84 x 20 box so its lines painted over the rows around it.

- components::instance_prop_list: prop_list keeping slots and the properties a layer of the component references (a nested instance counts, its layers and slot content are not entered). component.properties and the Properties panel use it for instances; sets, components and variants still list every definition.
- prop_label cuts the name to one line with an ellipsis and shows the full name on hover.
- Tests: an_instance_lists_only_the_properties_its_variant_uses, a_long_property_name_stays_on_one_line, and the pin a_set_keeps_each_named_text_definition_and_layers_read_their_own. Two tests and one setup that listed an unbound boolean on an instance now bind it to a layer.
- docs/components-parity.md: the properties row, header and revision history.

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
2026-11-11 14:42:37 -05:00
Trevor BrownandClaude Sonnet 5.5 b190e675b9 M4: a list item made by Enter has no start of its own, so a split list restarts at 1, as measured
Enter copied the item's start (a typed "5. " gives 4) onto the new item, so once a middle item was unlisted the next one restarted at its copied start. Every paragraph an insertion creates now has start_offset 0; the first item keeps its start. Measured in Figma: unlisting "b" in 5. 6. 7. leaves "c" at 1., and the same for a list started with 1.

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
2026-11-11 14:34:56 -05:00
Dylan M. Taylor a8e34e75a8 components: work out each main's layer paths once while linking imported instances 2026-11-11 18:38:59 +00:00
Dylan M. Taylor 2f8dd85f42 load: share one layer allowance across Penpot raw SVGs, sum font-run coverage once 2026-11-11 17:43:55 +00:00
Trevor BrownandClaude Opus 5.5 9f51517f23 M8: view.pan's parameter text and the MCP guide give the measured steps
64 screen pixels a step, five times as far with big (320), as the code
and the measurement have it; they still said ten times and 20 / 200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 12:39:56 -05:00
Trevor BrownandClaude Opus 5.5 a728f3fb3e Merge upstream/main into fix/shift-rotate-multiples-of-15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:39:54 -05:00
Trevor BrownandClaude Opus 5.5 a1cb5309f4 Merge upstream/main into feat/vector-edit-clipboard-1875
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:39:44 -05:00
Trevor BrownandClaude Opus 5.5 74d7cff672 Merge upstream/main into fix/arrow-pan-step-measured
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:39:41 -05:00
Trevor BrownandClaude Opus 5.5 e829590fdf Merge upstream/main into fix/escape-deselects-measured
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:39:38 -05:00
Trevor BrownandClaude Opus 5.5 c6b030ae02 Merge upstream/main into feat/vector-edit-point-xy-mixed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:39:31 -05:00
Dylan M. Taylor 9aaf7114e0 widgets: bound the decoded pixels of a widget's images, read from their headers 2026-11-11 16:37:35 +00:00
Trevor BrownandClaude Sonnet 5.5 b8f05d1948 M4: a typed number and full stop start a numbered list at that number, as measured in Figma
Typing `5. ` at the start of a plain paragraph starts a numbered list whose first item reads 5 (Figma has no start-number field); the next item goes on with 6. Digits only, 1 to 9999; `0. `, `12a. ` and `2)` stay text (not measured), `1)` works as before. The measured restart rows (plain paragraph, bullet item) are no longer marked silent.

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
2026-11-11 09:58:57 -05:00
Trevor BrownandClaude Sonnet 5.5 06ffeca801 M4: Every loop a bridge closes takes it; only a real removal closes loops (#1951)
The closing pass moves out of reopen_loops (back to main's body) into close_bridged_loops, run only by remove_segments and delete_vertices when the segment or point count changed. Every loop takes what closing_segment returns against one snapshot, so two fills that shared a deleted border both close through the bridge, as rounding already draws them. The owner map is gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 decided; Sonnet 5.5 wrote at medium
2026-11-11 05:55:33 -05:00
Trevor BrownandClaude Sonnet 5.5 641cc41594 M4: Flatten bakes a vector's point corner radius into its path (#2195)
Flatten used the vector's network as stored, so a rounded point kept its radius and the box kept the cut-off sharp vertex. It now takes the network as the layer draws it (VectorNetwork::rounded_smoothed through geometry::drawn_network): each rounded point becomes the arc's end points joined by curves, every radius reads 0, and the layer's box is re-fitted to the baked geometry. Shapes' own radii were already baked; a rounded point beside a curved segment keeps today's behaviour.

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
2026-11-11 04:54:20 -05:00
Trevor BrownandClaude Opus 5.5 153fa08d07 M4: reopen_loops looks up each closing segment's chain in a map (#1951)
Checking a loop's closing segment against every closing already accepted was quadratic in the
number of open loops. A map from closing segment to the chain that took it first answers it in
one lookup; behaviour is unchanged. A test closes 50 independent chains through their own
segments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove and wrote
2026-11-11 04:38:33 -05:00
Trevor BrownandClaude Sonnet 5.5 35d46208cf M4: Shared open loops all close through their segment; one index per rounding pass (#1951)
Two regions over one open chain both got the closing segment when rounded, but reopen_loops gave it to the first loop only, so after an unrelated deletion the second region's curved closing segment dropped out of its fill. The pass now takes the segment for every loop that walks the same chain (same segment set); a different chain still can't share it.

closing_segment no longer scans every segment and every loop per call: rounded_smoothed and reopen_loops build one LoopIndex (endpoint pair to segments, and which segments are in a loop) per pass and pass it in.

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
2026-11-11 03:48:24 -05:00
Trevor BrownandClaude Sonnet 5.5 a75d8bf04f M4: Delete closes a region's loop through the segment that joins its ends (#1951)
A Pen bridge between two degree-2 points leaves the region alone, and Delete on the point between them then rotated the loop open with the bridge in no loop, so even an unrounded fill ran straight across a curved bridge. reopen_loops now pushes the one segment in no loop that joins an open chain's ends onto the loop's end (winding kept, loop by loop), as connect does. This also covers removing segments and the eraser.

closing_segment is public, derives its own start point, walks strictly (a loop out of walking order is left alone) and gives up when two segments qualify. The render fix stays as the fallback for files already saved in that state.

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 medium
2026-11-11 02:16:29 -05:00
Trevor BrownandClaude Sonnet 5.5 529134b850 M4: a boolean group's corner radius rounds its result (#2222)
A boolean group takes a corner radius (and corner smoothing): the Design panel shows the field for it, `node.set cornerRadius` writes it, and the renderer rounds every sharp corner of the group's result after the fold, convex and concave alike. Layers inside keep their own radius and do not stack with it, a radius larger than the edges allow shrinks to fit while the field keeps the typed value, Flatten bakes the arcs, and a mixed selection reads Mixed. Measured in Figma's web app on 2026-11-10.

The rounded result is cached apart from the fold (keyed by the folded path, radius and smoothing), so scrubbing the radius never redoes the boolean steps. The .fig and REST importers and the .fig exporter now carry the radius and smoothing of a boolean group, and paste properties and radius variables reach it. Docs: ui-parity, target-app-parity, gaps, file-format-parity, ROADMAP, mcp.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 designed; Sonnet 5.5 wrote at medium
2026-11-10 13:36:10 -05:00
Trevor BrownandClaude Sonnet 5.5 281918e2f2 M7: Text styles: a face name bound after a weight clears the axis; review fixes (#2174)
A style's fontStyle binding now goes through set_font_style, so it drops the stale wght axis a weight binding left behind. Also: a test and doc line for the token_roles weight role, the apply-order comment, the weight chip opening the weight picker, junk weight values pinned, fixture assertions fixed, and the ROADMAP and parity status lines.

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 medium
2026-11-10 13:09:35 -05:00
Trevor BrownandClaude Sonnet 5.5 ccb33e0bbc M7: Text styles: bind a number variable to the weight (#2174)
A text style takes a Number variable on `fontWeight` (`style.bind`): the style gets the face nearest the number and sets the `wght` axis, so static and variable fonts both follow. A bound weight and a bound face name replace each other, and the weight wins in a file carrying both. The style editor's weight menu offers number variables beside string ones, and a variable bound to a weight is Unitless in the CSS export. A number bound to a line height stays pixels, as the reference tool applies it; a test pins that.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 designed; Sonnet 5.5 wrote at medium
2026-11-10 12:43:24 -05:00