Merge main into #2121

This commit is contained in:
bflatastic committed 2026-11-12 11:47:19 +07:00
commit 05c8d6b2e4
4 files changed
+60 -22

No files matched your search

+29 -8
View File
@@ -674,27 +674,48 @@ fn grid_rules() {
rects: &[(0.0, 0.0, 60.0, 20.0), (60.0, 0.0, 240.0, 20.0)],
},
Case {
rule: "fr tracks behave like the CSS fr unit: a fixed child taller than its share keeps its row",
// Share 50 < 80: row 1 keeps 80, row 2 gets the remaining 20.
rule: "fr tracks as measured in Figma: a fixed child taller than its share keeps its row's share and overflows",
// Share 50 < 80: rows stay 50 / 50, child 1 overflows into row 2.
frame: Frame { mode: Grid, size: (100.0, 100.0), columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
kids: &[kid(20.0, 80.0), Kid { sizing: (Fixed, Fill), ..KID }],
want: (100.0, 100.0),
rects: &[(0.0, 0.0, 20.0, 80.0), (0.0, 80.0, 20.0, 20.0)],
rects: &[(0.0, 0.0, 20.0, 80.0), (0.0, 50.0, 20.0, 50.0)],
},
Case {
rule: "fr tracks: children bigger than every share keep their sizes (the grid overflows)",
rule: "fr tracks as measured in Figma: children bigger than every share keep their sizes (the grid overflows)",
frame: Frame { mode: Grid, size: (100.0, 100.0), columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
kids: &[kid(20.0, 80.0), kid(20.0, 70.0)],
want: (100.0, 100.0),
rects: &[(0.0, 0.0, 20.0, 80.0), (0.0, 80.0, 20.0, 70.0)],
rects: &[(0.0, 0.0, 20.0, 80.0), (0.0, 50.0, 20.0, 70.0)],
},
Case {
rule: "fr columns: a fixed child wider than its share keeps its column",
// Share 100 < 150: column 1 keeps 150; column 2 (2fr alone) gets the other 150.
rule: "fr columns as measured in Figma: a fixed child wider than its share overflows its column",
// Share 100 < 150: columns stay 100 / 200.
frame: Frame { mode: Grid, size: (300.0, 20.0), columns: &[fr(1.0), fr(2.0)], ..FRAME },
kids: &[kid(150.0, 20.0), Kid { sizing: (Fill, Fixed), ..KID }],
want: (300.0, 20.0),
rects: &[(0.0, 0.0, 150.0, 20.0), (150.0, 0.0, 150.0, 20.0)],
rects: &[(0.0, 0.0, 150.0, 20.0), (100.0, 0.0, 200.0, 20.0)],
},
Case {
rule: "fr rows as measured in Figma: 300 x 200, 1fr / 1fr, a 150-tall child does not widen its row",
frame: Frame { mode: Grid, size: (300.0, 200.0), columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
kids: &[kid(100.0, 150.0), kid(100.0, 20.0)],
want: (300.0, 200.0),
rects: &[(0.0, 0.0, 100.0, 150.0), (0.0, 100.0, 100.0, 20.0)],
},
Case {
rule: "fr rows as measured in Figma: the row gap comes off first (95 / 95, second child at 105)",
frame: Frame { mode: Grid, size: (300.0, 200.0), cross_gap: 10.0, columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
kids: &[kid(100.0, 150.0), kid(100.0, 20.0)],
want: (300.0, 200.0),
rects: &[(0.0, 0.0, 100.0, 150.0), (0.0, 105.0, 100.0, 20.0)],
},
Case {
rule: "fr rows as measured in Figma: with the frame on Hug the rows size to content (150 / 20, frame 170)",
frame: Frame { mode: Grid, size: (300.0, 200.0), sizing: (Fixed, Hug), columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
kids: &[kid(100.0, 150.0), kid(100.0, 20.0)],
want: (300.0, 170.0),
rects: &[(0.0, 0.0, 100.0, 150.0), (0.0, 150.0, 100.0, 20.0)],
},
Case {
rule: "placement: left to right, top to bottom; rows grow to fit the children",
+22 -6
View File
@@ -794,9 +794,9 @@ fn track_spec(specs: &[figcraft_doc::GridTrack], i: usize) -> (Sizing, f64) {
}
/// Resolve track sizes: fixed tracks keep their size, hug tracks fit their content, fill tracks
/// share what is left by their fractional units like the CSS `fr` unit (a track never gets less
/// than its content: one whose share is too small keeps its content size and the others share the
/// rest). `avail` is None when the frame hugs that axis (fill tracks then hug their content too).
/// share what is left by their fractional units (a track whose share is below its content, which
/// only a Fill child's minimum or a spanning child asks for, keeps that size and the others share
/// the rest). `avail` is None when the frame hugs that axis (fill tracks then hug their content too).
fn resolve_tracks(specs: &[figcraft_doc::GridTrack], content: &[f64], avail: Option<f64>) -> Vec<f64> {
let mut out: Vec<f64> = (0..content.len())
.map(|i| match track_spec(specs, i) {
@@ -891,13 +891,29 @@ pub fn grid_geometry(doc: &Document, frame: NodeId) -> Option<GridGeometry> {
let gy = finite_or_zero(al.cross_gap).max(0.0);
let hug_w = f.child_layout.sizing_h == Sizing::Hug;
let hug_h = f.child_layout.sizing_v == Sizing::Hug;
// What each child asks of its tracks: its size, or (filling a non-hugging axis) its minimum.
// What each child asks of its tracks: its size, its minimum when it fills a non-hugging axis, or
// nothing when it sits alone in a Fill track of a non-hugging axis.
let (mut ws, mut hs) = (Vec::with_capacity(cells.len()), Vec::with_capacity(cells.len()));
for c in &cells {
let (w, h) = outer_size(doc, c.id);
let cl = doc.node(c.id).map(|n| n.child_layout.clone()).unwrap_or_default();
ws.push(if cl.sizing_h == Sizing::Fill && !hug_w { cl.min_w.unwrap_or(0.0) } else { w });
hs.push(if cl.sizing_v == Sizing::Fill && !hug_h { cl.min_h.unwrap_or(0.0) } else { h });
// Measured in Figma: a Fill track of a fixed-size axis shares the space by its fraction and
// ignores a fixed-size (or hug-size) child in it, which overflows into the next track.
let in_fill = |tracks: &[figcraft_doc::GridTrack], start: usize, span: usize| span == 1 && track_spec(tracks, start).0 == Sizing::Fill;
ws.push(if cl.sizing_h == Sizing::Fill && !hug_w {
cl.min_w.unwrap_or(0.0)
} else if !hug_w && in_fill(&al.column_tracks, c.column, c.column_span) {
0.0
} else {
w
});
hs.push(if cl.sizing_v == Sizing::Fill && !hug_h {
cl.min_h.unwrap_or(0.0)
} else if !hug_h && in_fill(&al.row_tracks, c.row, c.row_span) {
0.0
} else {
h
});
}
let col_content = track_content(&cells, &ws, &al.column_tracks, cols, gx, true, hug_w);
let row_content = track_content(&cells, &hs, &al.row_tracks, rows, gy, false, hug_h);
+5 -4
View File
@@ -1,11 +1,11 @@
# Auto layout and constraints parity
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-09 · **Change:** patch (a child dragged within its own auto layout row or column passes a sibling when its leading edge reaches the sibling's middle, as measured in Figma's web app, wherever it is grabbed; a Cmd/Ctrl-click on a padding field sets all four sides with the value typed, as the Guide to auto layout's shortcut guide says; Suggest auto layout is its own command, as the help center says: it adds as many auto layout frames as needed, splitting a frame's layers into rows or columns by the gaps between them, and keeps the placement of the design where the gaps are even (#1823); baseline alignment puts a layer without text with its bottom on the line, as the help center's figure shows an icon next to text (#1825); fill children share the free space by their content area, border-box fashion, as the Flexbox article says (#1824); double-click an edge of an auto layout frame's box for Hug contents, Alt+double-click an edge of a child's box for Fill container, as the Guide to auto layout says (#1838); a child resized to the full available space of its auto layout frame becomes Fill container on that axis, as the Guide to auto layout says; a text keeps a max height or a number of max lines, not both, as the Guide to auto layout says: a max height sets max lines to Auto and max lines remove the max height; Remove all auto layout, as the help center's More layout options has it: `layout.removeAll` takes the auto layout off a frame and every frame nested in it in one undo step; grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); Layout sidebar: Add auto layout (+) is available for groups and converts a single group to a frame in place, with mask order, flow-only inference and Fill sizing preserved; adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys only to frames, with warnings (#603); core editor leftovers: auto gap handles, wrapped-line alignment; behaviour suite written from the public documentation, every rule's status; fixes and the missing settings it found) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-09 · **Change:** patch (grid Fill (`fr`) tracks share the space of a fixed-size axis by their fractions and let a fixed-size child overflow, as measured in Figma (300 x 200, two rows, 100 x 150 and 100 x 20 children: rows 100 / 100, second child at y 100; with a row gap of 10, rows 95 / 95 and y 105; on Hug the rows fit their content, 150 / 20) -- this replaces the 2026-10-11 rule from the documentation; a child dragged within its own auto layout row or column passes a sibling when its leading edge reaches the sibling's middle, as measured in Figma's web app, wherever it is grabbed; a Cmd/Ctrl-click on a padding field sets all four sides with the value typed, as the Guide to auto layout's shortcut guide says; Suggest auto layout is its own command, as the help center says: it adds as many auto layout frames as needed, splitting a frame's layers into rows or columns by the gaps between them, and keeps the placement of the design where the gaps are even (#1823); baseline alignment puts a layer without text with its bottom on the line, as the help center's figure shows an icon next to text (#1825); fill children share the free space by their content area, border-box fashion, as the Flexbox article says (#1824); double-click an edge of an auto layout frame's box for Hug contents, Alt+double-click an edge of a child's box for Fill container, as the Guide to auto layout says (#1838); a child resized to the full available space of its auto layout frame becomes Fill container on that axis, as the Guide to auto layout says; a text keeps a max height or a number of max lines, not both, as the Guide to auto layout says: a max height sets max lines to Auto and max lines remove the max height; Remove all auto layout, as the help center's More layout options has it: `layout.removeAll` takes the auto layout off a frame and every frame nested in it in one undo step; grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); Layout sidebar: Add auto layout (+) is available for groups and converts a single group to a frame in place, with mask order, flow-only inference and Fill sizing preserved; adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys only to frames, with warnings (#603); core editor leftovers: auto gap handles, wrapped-line alignment; behaviour suite written from the public documentation, every rule's status; fixes and the missing settings it found) · **Target:** Figma Design (desktop app 126.10.7)
Auto layout is the most used layout feature in Figma and the one where small differences (a
hugging frame one pixel off, text wrapping at another width) break real files. **Breadth ~98%,
ready ~74% (estimated)**: every rule of the public documentation is a test case with the sizes and
positions it implies (`crates/layout/src/behaviour_tests.rs`, 64 cases, and
positions it implies (`crates/layout/src/behaviour_tests.rs`, 68 cases, and
`crates/engine/src/al_behaviour_tests.rs` through commands), and all pass; nothing yet against real
Figma files. Remaining: 3–6 h.
@@ -67,7 +67,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver
| Ignore auto layout: out of the flow, kept in the frame; constraints apply only then | pass | `absolute_position_rules` |
| Canvas stacking: last on top by default, first on top on request; the layer order stays | **fixed** + **added** | `reverseZ` was stored but never drawn: now the canvas, PNG, SVG and PDF exports and hit testing paint in that order; Design panel control |
| Grid: `fr` tracks share by fraction; fixed tracks keep their size; hug tracks fit their content | pass | `grid_rules` |
| Grid `fr` tracks behave like the CSS `fr` unit: a track never gets less than its content | **fixed** | was `minmax(0, 1fr)`: a fixed-size child taller than its share overflowed into the next row |
| Grid `fr` tracks as measured in Figma: on a fixed-size axis they share the space left after gaps and padding by their fractions and a fixed-size child bigger than its share overflows into the next track (`minmax(0, 1fr)` for fixed-size and hug-size children); a Fill child's own minimum width or height still keeps its track at least that big (not measured, kept from before: two 1fr rows in 200 with a 150 minimum resolve to 150 / 50); on a Hug axis they size to their content | **fixed** | 300 x 200 frame, rows 1fr / 1fr, children 100 x 150 and 100 x 20: rows 100 / 100, second child at y 100; row gap 10: rows 95 / 95, y 105; frame height Hug: rows 150 / 20, frame 170; columns the same. Measured in Figma 2026-11-11; was the CSS rule from the documentation (2026-10-11) |
| Grid placement left to right, top to bottom; rows grow; anchors; spans; gaps; padding | pass | `grid_rules` |
| Grid children align independently in their cells | **added** | `gridAlignH` / `gridAlignV` (Auto follows the frame), Design panel cell alignment |
| A hugging grid with `fr` tracks | silent | the file format calls it invalid; `fr` tracks hug their content |
@@ -79,7 +79,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver
| Feature | Status | Evidence / notes |
|---|---|---|
| Horizontal, vertical, wrap (both directions) | done | `crates/layout` |
| Grid auto layout: fixed / hug / `fr` tracks, cells, spans, gaps, cell alignment | done | `fr` like CSS `fr` |
| Grid auto layout: fixed / hug / `fr` tracks, cells, spans, gaps, cell alignment | done | `fr` as measured |
| Gap, auto gap (between / evenly / around), padding (per side), alignment 3×3 | done | a Cmd/Ctrl-click on a padding field (the pair or a side) makes the value typed go to all four sides in one undo step (Guide to auto layout, Keyboard shortcut guide; `padding_individual_tests`) |
| Text baseline alignment | done | |
| Hug / Fill / Fixed sizing with coherent rules (Fill turns a hugging parent Fixed) | done | `al_nesting_tests.rs`, `al_behaviour_tests.rs` |
@@ -101,6 +101,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver
| Date | Change | Summary |
|---|---|---|
| 2026-11-11 | patch | M2.1: grid Fill (`fr`) tracks share the space of a fixed-size axis by their fractions and let a fixed-size child overflow, as measured in Figma (300 x 200, two rows, 100 x 150 and 100 x 20 children: rows 100 / 100, second child at y 100; with a row gap of 10, rows 95 / 95 and y 105; on Hug the rows fit their content, 150 / 20) (this replaces the 2026-10-11 rule from the documentation). Layout behaviour suite: three cases flipped, three added |
| 2026-11-05 | patch | M2, #1823: Suggest auto layout is no longer Add auto layout under another name: `layout.suggest` splits a frame's layers into rows (Vertical) or else columns (Horizontal) by the gaps between their boxes, wraps each band of several layers in a new frame without fill or clipping that hugs it, and repeats inside it (16 levels, 1,000 layers per frame at most). The frame keeps its size; gap and padding come from the bands, so layers keep their boxes where the gaps are even and the bands start at the same offset; instances and layers inside them are refused; uneven gaps (smallest used), bands that start at different offsets across the flow (lined up at the smallest) and layers that overlap along both axes come back as `warnings`. A band frame hugs only when its own split succeeds: a band whose layers overlap along both axes gets no auto layout and stays Fixed, so it cannot reflow those children. `direction` sets the top level only; one undo step; `layout.add` is unchanged. Tests in `layout_suggest_tests.rs` |
| 2026-11-02 | patch | M2: a Cmd/Ctrl-click on a padding field (the horizontal or vertical pair, or a side with Individual padding open) makes the value typed go to all four sides in one undo step (Guide to auto layout, Keyboard shortcut guide); a plain click still edits that axis or side alone. The modifier was ignored before. Test: `padding_individual_tests` |
| 2026-11-03 | patch | M2, #1825: baseline alignment of a layer without text (an icon next to a text) puts its bottom on the line's baseline, as the help center's figure shows ("the bottoms of the icon and the word home are aligned on the red line"); it kept to the top of the line. A layer reaching below the text's baseline moves the text down, and a line with no text lines the layers' bottoms up (the documentation is silent; CSS baseline alignment does the same). Tests: `alignment_rules` (three cases), engine `baseline_alignment_puts_an_icons_bottom_on_the_text_baseline`. Numbers unchanged. |
+4 -4
View File
@@ -1,6 +1,6 @@
# FigCraft parity with Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (the Point section's X and Y in vector edit mode show Mixed and set every selected point of the layer being edited, as in Figma; an exported layer with a background blur or glass keeps the blurred content behind it within its bounds (#2213); a typed number and a full stop at the start of a paragraph starts a numbered list at that number, as measured in Figma 2026-11-11; justified Chinese, Japanese and Korean text, written without spaces, spreads each line's room between its characters so its right edge aligns (#1970); joining the two ends of a path broken open by a deleted point closes it again, so its fill and selection outline follow the new segment and its corner radius instead of a straight line across the gap (#1951); glass's docs say which centre splay bends away from and where a shape gets no core; glass bends towards its core, rounded rectangles in closed form; glass's light is a line on its bevel and splay turns the bend towards the centre; glass bends with a thick-lens profile and splits its colours over 8 steps, measured against the reference tool; bulleted lists use the same bullet at every level of indentation, as the help center says, instead of a white bullet and a square at the deeper levels; typing `1)` and a space at the start of a paragraph starts a numbered list, as `1.` does, both being the help center's creation characters; a link on text is underlined by default, as the help center says, and Underline takes the underline off while the link stays; a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; gap 6: widgets draw lines and SVG pictures (plugin API 6); gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); plugin widgets have hover styles: a layer's `hover` fill, stroke, opacity or text colour shows while the pointer is over it (gap 6); Thai, Lao, Khmer and Burmese lines break between words (#1219); MCP and control calls name their document (#998); shadows of images with transparent pixels follow their alpha (#1289); Arabic draft catalog; truncated text fills its last line before the ellipsis and keeps truncating when resized; auto-height text with max lines (#1375, #1376, #1377), numbers unchanged; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; `.sketch` import, best effort: import/export breadth 86% → 87%, file formats 60% → 61%, headline numbers unchanged; Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds; boards: area 22 (weight 0, outside the weighted sum); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, iteration marks 〱–〵 and the halfwidth parentheses; Turkish reviewed, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84Line truncated
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (the Point section's X and Y in vector edit mode show Mixed and set every selected point of the layer being edited, as in Figma; grid Fill (`fr`) tracks share the space of a fixed-size axis by their fractions and let a fixed-size child overflow, as measured in Figma (300 x 200, two rows, 100 x 150 and 100 x 20 children: rows 100 / 100, second child at y 100; with a row gap of 10, rows 95 / 95 and y 105; on Hug the rows fit their content, 150 / 20) -- this replaces the 2026-10-11 rule from the documentation; an exported layer with a background blur or glass keeps the blurred content behind it within its bounds (#2213); a typed number and a full stop at the start of a paragraph starts a numbered list at that number, as measured in Figma 2026-11-11; justified Chinese, Japanese and Korean text, written without spaces, spreads each line's room between its characters so its right edge aligns (#1970); joining the two ends of a path broken open by a deleted point closes it again, so its fill and selection outline follow the new segment and its corner radius instead of a straight line across the gap (#1951); glass's docs say which centre splay bends away from and where a shape gets no core; glass bends towards its core, rounded rectangles in closed form; glass's light is a line on its bevel and splay turns the bend towards the centre; glass bends with a thick-lens profile and splits its colours over 8 steps, measured against the reference tool; bulleted lists use the same bullet at every level of indentation, as the help center says, instead of a white bullet and a square at the deeper levels; typing `1)` and a space at the start of a paragraph starts a numbered list, as `1.` does, both being the help center's creation characters; a link on text is underlined by default, as the help center says, and Underline takes the underline off while the link stays; a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; gap 6: widgets draw lines and SVG pictures (plugin API 6); gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); plugin widgets have hover styles: a layer's `hover` fill, stroke, opacity or text colour shows while the pointer is over it (gap 6); Thai, Lao, Khmer and Burmese lines break between words (#1219); MCP and control calls name their document (#998); shadows of images with transparent pixels follow their alpha (#1289); Arabic draft catalog; truncated text fills its last line before the ellipsis and keeps truncating when resized; auto-height text with max lines (#1375, #1376, #1377), numbers unchanged; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; `.sketch` import, best effort: import/export breadth 86% → 87%, file formats 60% → 61%, headline numbers unchanged; Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds; boards: area 22 (weight 0, outside the weighted sum); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, iteration marks 〱–〵 and the halfwidth parentheses; Turkish reviewed, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin wiLine truncated
The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md)
summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is
@@ -104,7 +104,7 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
| 6 | Strokes | 3 | 95% | 75% | Stroke QA with real files (colour styles on strokes, stroke weight and per-side weight variables, brush strokes landed, see 20) | 1–2 |
| 7 | Effects | 4 | 100% | 75% | Glass and progressive blur tuned side by side (behaviour suite from the documentation, inner shadows and background blur on groups, shadows hidden behind translucent layers on the canvas and in every export landed; a background blur or glass exported on its own keeps the content behind it within its bounds, measured 2026-11-10, #2213) | 2–5 |
| 8 | Frames, groups, sections, masks | 5 | 97% | 80% | Visual QA with real files (a one-step frame/group convert, and a behaviour suite from the documentation landed: masked-out areas don't take clicks, mask group bounds and exports follow the mask, sections kept out of frames and groups, frames and sections drawn around layers take them, section resizing leaves contents in place, X/Y measured in the frame) | 1–3 |
| 9 | Auto layout & constraints | 9 | 98% | 74% | Behaviour against real Figma files (behaviour suite from the documentation, `fr` tracks like CSS, vertical wrap, auto gaps evenly/around, cell alignment and wrapped-line alignment landed) ([auto-layout-parity](auto-layout-parity.md)) | 3–6 |
| 9 | Auto layout & constraints | 9 | 98% | 74% | Behaviour against real Figma files (behaviour suite from the documentation, `fr` tracks as measured, vertical wrap, auto gaps evenly/around, cell alignment and wrapped-line alignment landed) ([auto-layout-parity](auto-layout-parity.md)) | 3–6 |
| 10 | Text | 8 | 90% | 70% | Spell check, text metrics on real files and in other fonts (text box sizes measured against documents re-saved by other software: all 230 self-sizing boxes in 7 documents come out to the pixel, one font family; behaviour suite with CJK line breaking, Thai, Lao, Khmer and Burmese breaks between words, vertical trim and carets by grapheme cluster landed; lists per paragraph with nesting and list spacing (typing `-`, `*`, `1.` or `1)` and a space starts one), hanging punctuation and lists, decoration style, thickness, offset, skip ink and colour landed) | 3–7 |
| 11 | Components & variants | 9 | 96% | 72% | Real (not synthetic) libraries, `.fig` libraries (a component playground for variant, boolean, text and instance-swap properties, a behaviour suite from the documentation, overrides of padding, gap, grids, exports and interactions, refused geometry overrides, variant-switch and Assets-swap override rules, top-left default variant landed) ([components-parity](components-parity.md)) | 4–9 |
| 12 | Styles & variables | 5 | 95% | 76% | Real-library QA (variables inside styles, behaviour suite from the documentation, style folders and order, mode order, code syntax, extended collections with their tail (new library variables as updates, chains through libraries) and variables import/export landed) ([components-parity](components-parity.md)) | 2–4 |
@@ -330,8 +330,7 @@ Hours in this narrative are the builders' estimates; the tables above supersede
"Navigate to" a section shows its frame shown last, else its first; ready for dev turns
"Changed" when the contents are edited, resolved with a reason; Compare changes shows what a
changed frame or section looked like when it was marked next to how it is now, with the
changed layers and their property changes. *Left:* whether `fr` rows of a fixed-height grid
should make room for fixed-size children (now `minmax(0, 1fr)`-like). 1–2 h.
changed layers and their property changes. 1–2 h.
4. **Components depth**: *landed:* component properties (edit/delete/bind) with the properties
panel, add variant, instance swap UI, per-layer overrides with reset, restore main, instance
structure protection; Assets panel with live thumbnails, search and drag-to-canvas (drops into
@@ -652,6 +651,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
|---|---|---|
| 2026-11-08 | minor | M8: measured in Figma on 2026-11-08, vector edit mode's Point section: X and Y show the value the selected points share, or Mixed when they differ, and a typed value sets every selected point's own coordinate in the layer being edited, points of other edited layers left as they are (it moved the group's top-left before); a relative edit (`+10`, `*2`, `/2`) typed into a Mixed field applies to each point's own value (`vector.edit.position` takes a number or `{add}`, `{mul}`, `{div}` per axis); the W and H row and other edited layers' points are unchanged. |
| 2026-11-11 | patch | M4: typing a number, a full stop and a space at the start of a plain paragraph starts a numbered list at that number (`5. ` then "x", Enter, "y" reads 5. then 6.), as measured in Figma web on 2026-11-11 (it has no start-number field: the start comes only from the typed number); before, only `1. ` and `1)` started a list. `0. `, `12a. ` and a bracket after a number other than 1 stay text (not measured), and the number is capped at 9999. Also measured: a plain paragraph or a bullet item between numbered items restarts the count at 1, and an indented sub-item does not break the outer count; those rows were pinned as silent. Tests: `text_tail_tests::typed_list_starts`, `typed_number_starts_the_list_there`, `tail_tests` restart rows. Numbers unchanged |
| 2026-11-11 | patch | M2.1: grid Fill (`fr`) tracks share the space of a fixed-size axis by their fractions and let a fixed-size child overflow, as measured in Figma (300 x 200, two rows, 100 x 150 and 100 x 20 children: rows 100 / 100, second child at y 100; with a row gap of 10, rows 95 / 95 and y 105; on Hug the rows fit their content, 150 / 20) (this replaces the 2026-10-11 rule from the documentation). Percentages unchanged |
| 2026-11-03 | minor | Traditional Chinese (Taiwan) (`zh-Hant`) draft catalog: 1,975 UI strings, 641 command labels and 31 menu wordings, clean-room from the English source; zh-TW, zh-HK, zh-MO and zh-Hant read it, explicit Hans stays Simplified; Noto Sans CJK TC then Taiwan system faces; no bundled font; native review pending. Numbers unchanged |
| 2026-11-04 | patch | M4, #1970: justified Chinese, Japanese and Korean text, written without spaces between words, aligns its right edge: the room on each line but a paragraph's last goes between its characters, each a word of its own as for line breaking ("Justify aligns the first word in each line of text to the left-most edge, and the last to the right-most edge"), wherever a line could break there, so none opens before a full stop or closing bracket, nor after an opening one. Latin text widens its spaces as before; mixed text widens both. A gap is reordered with the line's runs as a space of no width would be, so one beside a Hebrew word in Chinese text, or beside a Chinese word within that Hebrew, opens beside the word, not inside it. `figcraft_text::breaks::justify_between`, beside `break_between`, says where. Evidence: `tests::justified_ideographs_spread_the_room_between_characters` (fails on main). |
| 2026-11-04 | patch | M8, #1951 (#1506 again): a path broken open by deleting a point (Delete, not Shift+Delete) is filled closed by a straight line across the gap, as an open path's fill is; joining its two ends again (the pen onto the other end, Connect, ⌘J) left the loop without the new segment, so once the points were rounded the fill and the selection outline ran straight across while the stroke followed the segment and its arcs. Joining now closes the loop through the new segment ("Click to close the path."; "You can use the Paint tool to add fills to any closed regions of a vector network."): `ops::connect` puts it into the loop whose segments are the chain between the ends, at the gap and in the loop's own order, so a hole keeps its winding whichever end was clicked first; a loop listed out of walking order is left as it was. Evidence: `vectoredit::tests::a_rounded_chamfer_made_other_ways_fills_along_its_arcs` (fails on main: the loop kept 4 of 5 segments), `a_rounded_chamfer_cut_in_the_editor_fills_along_its_arcs` (Shift+Delete, with and without a region fill), `doc::tests::a_chamfered_rectangles_rounded_fill_is_cut_at_every_point` (fill, region path and stroke agree for every loop order and segment direction of a chamfered rectangle). |