diff --git a/ROADMAP.md b/ROADMAP.md index 0bd801f..eb7f06b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (core editor leftovers: 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; core editor leftovers: 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; core editor leftovers: command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; 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 ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (core editor leftovers: 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; core editor leftovers: 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; core editor leftovers: 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; core editor leftovers: command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; 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 ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -131,6 +131,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). ## Progress log +- **2026-10-13** · Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. - **2026-10-13** · Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. - **2026-10-13** · Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. - **2026-10-13** · Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. @@ -303,6 +304,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | | 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | | 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | | 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. | diff --git a/crates/ui-egui/src/gap_handle_tests.rs b/crates/ui-egui/src/gap_handle_tests.rs new file mode 100644 index 0000000..d28b28f --- /dev/null +++ b/crates/ui-egui/src/gap_handle_tests.rs @@ -0,0 +1,75 @@ +//! The on-canvas gap handles of an auto layout frame whose gap is automatic (the lines of a +//! wrapping layout spread across it, or the items spread along it): the handle shows the distance +//! the layout gives the gap, and dragging it makes the gap a fixed one from there, as typing a gap +//! in the panel does. Driven through egui headless. + +use egui::{Event, vec2}; +use figcraft_doc::{ContentAlign, MainAlign, NodeId}; +use figcraft_engine::Session; +use serde_json::{Value, json}; + +use crate::layers_drag_tests::Ui; + +fn id(v: Value) -> NodeId { + figcraft_engine::params::id_of(&v["id"]).unwrap() +} + +/// A 300 × 300 frame of three 120 × 60 rectangles with `props` on top of a horizontal layout +/// without padding (gap 10): selected, on screen. +fn scene(props: Value) -> (Ui, NodeId) { + let mut s = Session::new(); + let mut all = json!({"layout": "horizontal", "padding": 0, "gap": 10, "crossGap": 10}); + all.as_object_mut().unwrap().extend(props.as_object().unwrap().clone()); + let f = id(s.execute("frame.create", &json!({"x": 100, "y": 100, "width": 300, "height": 300, "props": all})).unwrap()); + for _ in 0..3 { + s.execute("shape.rectangle", &json!({"parent": f, "x": 0, "y": 0, "width": 120, "height": 60})).unwrap(); + } + s.execute("select.set", &json!({"ids": [f]})).unwrap(); + (Ui::new(s), f) +} + +/// Press at page point `from`, move by `by` page units in steps, release. +fn drag(u: &mut Ui, from: (f64, f64), by: (f32, f32)) { + let p = u.app.to_screen(from.0, from.1); + let zoom = u.app.session.ds().unwrap().view.zoom as f32; + u.frame(vec![Event::PointerMoved(p)]); + u.frame(vec![Event::PointerMoved(p)]); + u.button(p, true); + for k in 1..=4 { + u.frame(vec![Event::PointerMoved(p + vec2(by.0, by.1) * zoom * (k as f32 / 4.0))]); + } + u.button(p + vec2(by.0, by.1) * zoom, false); + u.frame(vec![]); +} + +#[test] +fn dragging_the_gap_between_spread_rows_fixes_it_from_where_it_was() { + // Two rows of 60 in 300: spread, they are 180 apart (the stored row gap of 10 doesn't count). + let (mut u, f) = scene(json!({"wrap": true, "contentAlign": "spaceBetween"})); + // Between the rows, at the frame's middle: page (250, 250). 20 down: 180 + 20. + drag(&mut u, (250.0, 250.0), (0.0, 20.0)); + let l = u.app.session.doc().unwrap().node(f).unwrap().layout.clone().unwrap(); + assert_eq!((l.content_align, l.cross_gap), (ContentAlign::Auto, 200.0)); + // One undo step for the whole drag. + u.app.run("edit.undo", json!({})); + let l = u.app.session.doc().unwrap().node(f).unwrap().layout.clone().unwrap(); + assert_eq!((l.content_align, l.cross_gap), (ContentAlign::SpaceBetween, 10.0)); +} + +#[test] +fn dragging_an_auto_gap_between_items_fixes_it_too_and_fixed_gaps_drag_as_before() { + // Three 60-wide items spread along 300: (300 - 3 × 60) / 2 = 60 apart. + let (mut u, f) = scene(json!({"mainAlign": "spaceBetween"})); + for k in u.app.session.doc().unwrap().node(f).unwrap().children.clone() { + u.app.run("node.set", json!({"id": k, "props": {"width": 60}})); + } + u.frame(vec![]); + // Between the first two items: x from 160 to 220 on the page, in the items' height. + drag(&mut u, (190.0, 130.0), (-10.0, 0.0)); + let l = u.app.session.doc().unwrap().node(f).unwrap().layout.clone().unwrap(); + assert_eq!((l.main_align, l.gap), (MainAlign::Min, 50.0)); + // A fixed gap: the drag changes the value and nothing else. + drag(&mut u, (185.0, 130.0), (6.0, 0.0)); + let l = u.app.session.doc().unwrap().node(f).unwrap().layout.clone().unwrap(); + assert_eq!((l.main_align, l.gap), (MainAlign::Min, 56.0)); +} diff --git a/crates/ui-egui/src/handles.rs b/crates/ui-egui/src/handles.rs index 32a8933..282e351 100644 --- a/crates/ui-egui/src/handles.rs +++ b/crates/ui-egui/src/handles.rs @@ -6,7 +6,7 @@ use egui::{Align2, Color32, CornerRadius, Id, Pos2, Rect, Shape, Stroke, StrokeKind, pos2, vec2}; use figcraft_color::{ImagePaint, ImageScaleMode, Paint, PaintKind}; -use figcraft_doc::{Blur, Effect, LayoutMode, NodeId}; +use figcraft_doc::{Blur, ContentAlign, Effect, LayoutMode, NodeId}; use figcraft_geom::Point; use serde_json::json; @@ -196,8 +196,13 @@ fn al_zones(app: &FigApp) -> Option { (2, band(0.0, fh - pb.max(4.0), fw, fh), pb), (3, band(0.0, 0.0, pl.max(4.0), fh), pl), ]; - let main = |rect: Rect, vertical: bool| GapZone { rect, key: "gap", value: al.gap, vertical }; - let cross = |rect: Rect, vertical: bool| GapZone { rect, key: "crossGap", value: al.cross_gap, vertical }; + // An auto gap shows (and is dragged from) the distance the layout gives it; the drag makes it a + // fixed gap again, as typing one does. + let (auto_main, auto_cross) = + (al.main_align.is_auto_gap() && al.mode != LayoutMode::Grid, al.wrap && al.content_align == ContentAlign::SpaceBetween); + let main = |rect: Rect, vertical: bool, dist: f64| GapZone { rect, key: "gap", value: if auto_main { dist } else { al.gap }, vertical }; + let cross = + |rect: Rect, vertical: bool, dist: f64| GapZone { rect, key: "crossGap", value: if auto_cross { dist } else { al.cross_gap }, vertical }; let mut gaps = Vec::new(); let mut tracks = Vec::new(); if let Some(g) = figcraft_layout::grid_geometry(doc, id) { @@ -206,12 +211,12 @@ fn al_zones(app: &FigApp) -> Option { let (left, right) = (g.columns.first().map(|c| c.0).unwrap_or(pl), g.columns.last().map(|c| c.1).unwrap_or(fw - pr)); for pair in g.columns.windows(2) { if let [a, b] = pair { - gaps.push(main(band(a.1, top, b.0, bottom), false)); + gaps.push(main(band(a.1, top, b.0, bottom), false, al.gap)); } } for pair in g.rows.windows(2) { if let [a, b] = pair { - gaps.push(cross(band(left, a.1, right, b.0), true)); + gaps.push(cross(band(left, a.1, right, b.0), true, al.cross_gap)); } } for c in &g.columns { @@ -248,13 +253,13 @@ fn al_zones(app: &FigApp) -> Option { let (y0, y1) = if al.wrap { span(line) } else { (cp0, cw - cp1) }; for pair in line.windows(2) { if let [ra, rb] = pair { - gaps.push(main(fband(ra.x1, y0, rb.x0, y1), vertical)); + gaps.push(main(fband(ra.x1, y0, rb.x0, y1), vertical, rb.x0 - ra.x1)); } } } for pair in lines.windows(2) { if let [a, b] = pair { - gaps.push(cross(fband(mp0, span(a).1, mw - mp1, span(b).0), !vertical)); + gaps.push(cross(fband(mp0, span(a).1, mw - mp1, span(b).0), !vertical, span(b).0 - span(a).1)); } } } @@ -584,7 +589,14 @@ fn apply(app: &mut FigApp, drag: &Drag, pos: Pos2, shift: bool) { Drag::Gap { id, start, value, vertical, key } => { let d = f64::from(if *vertical { pos.y - start.y } else { pos.x - start.x }) / zoom; let v = (value + d).round().max(0.0); - let _ = app.session.execute("node.set", &json!({"ids": [id], "props": { *key: v }})); + // Dragging an auto gap fixes it at what the drag makes of the distance it had. + let mut props = json!({ *key: v }); + match (*key, app.session.doc().ok().and_then(|d| d.node(*id)).and_then(|n| n.layout.as_ref())) { + ("crossGap", Some(l)) if l.content_align == ContentAlign::SpaceBetween => props["contentAlign"] = json!("auto"), + ("gap", Some(l)) if l.main_align.is_auto_gap() && l.mode != LayoutMode::Grid => props["mainAlign"] = json!("min"), + _ => {} + } + let _ = app.session.execute("node.set", &json!({"ids": [id], "props": props})); } } } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9d85210..9600dcd 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -81,6 +81,8 @@ mod field_tests; #[cfg(test)] mod frame_label_tests; #[cfg(test)] +mod gap_handle_tests; +#[cfg(test)] mod home_tests; #[cfg(test)] mod idle_tests; diff --git a/docs/auto-layout-parity.md b/docs/auto-layout-parity.md index 49be0e4..ae23060 100644 --- a/docs/auto-layout-parity.md +++ b/docs/auto-layout-parity.md @@ -1,6 +1,6 @@ # Auto layout and constraints parity -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (core editor leftovers: 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-10-13 · **Change:** minor (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%, @@ -64,7 +64,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver | Nested hug/fill settling in one pass (three levels, wrapping fill text, grid rows) | done | | | Absolute position (ignore auto layout) with constraints | done | | | Strokes included in layout (inside strokes), canvas stacking | done | | -| On-canvas padding and gap handles, incl. wrap row/column gap and grid gaps | done | | +| On-canvas padding and gap handles, incl. wrap row/column gap and grid gaps | done | an automatic gap's handle shows the distance it has; dragging it fixes the gap from there (silent in the documentation) | | Drag to reorder (wrap-aware, by row or column), into grid cells, across nested frames | done | `al_drag_tests.rs` | | Arrow keys move children one place (flows, rows, columns, grid cells, into neighbours) | done | `al_keys_tests.rs` | | Constraints: left, right, left and right, centre, scale; top/bottom likewise | done | | @@ -77,6 +77,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | | 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. | | 2026-10-11 | minor | Behaviour suite from the public documentation (64 layout cases, 8 command tests). Fixed: padding floor, inside-only included strokes (and included by default), auto gaps stop at 0, grid `fr` like CSS `fr`, canvas stacking actually drawn and hit. Added: auto gaps evenly/around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Breadth 95% → 98%, ready 68% → 74%, remaining 6–12 h → 3–6 h | | 2026-10-10 | major | Created from the 2026-10-10 re-measure | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index aae5777..5e47482 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # FigCraft parity with Figma -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (core editor leftovers: 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; core editor leftovers: 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; core editor leftovers: command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: 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 ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (core editor leftovers: 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; core editor leftovers: 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; core editor leftovers: 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; core editor leftovers: command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: delays on mouse triggers and manual overlay offsets, wrapped-line alignment; core editor leftovers: 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 ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) 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 @@ -625,6 +625,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | | 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | | 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | | 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. |