From 9093afac103661ba8a89506223aea81ef8b8177f Mon Sep 17 00:00:00 2001 From: Mert Dede Date: Fri, 30 Oct 2026 05:26:27 +0300 Subject: [PATCH] M8: an explicit variables file is refused past 256 MiB or when it isn't a regular file (part of #1114) variable.import and the variables parameter of file.open and file.import read an explicit path with the uncapped reader, while the sibling variables.json next to a REST file already went through read_capped with MAX_VARIABLES_FILE. variables_param now uses that capped read for paths on native builds; the browser keeps its no-file-system error. Inline objects and JSON text, and access roots, are unchanged. The check reads the size before the file, so it does not bound a file that grows or is replaced in between. Primary file reads and image.place stay uncapped. variable.import's help and docs/mcp.md name the limit. Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Claude Fable 5.1 Co-Authored-By: GPT-6.1 Sol Co-Authored-By: GPT-6 Astra --- ROADMAP.md | 3 +- crates/engine/src/cmd/file/load.rs | 4 ++ crates/engine/src/cmd/variables_io.rs | 2 +- crates/engine/src/import_export_tests.rs | 57 ++++++++++++++++++++++++ crates/engine/src/variables_io_tests.rs | 30 +++++++++++++ docs/components-parity.md | 5 ++- docs/mcp.md | 3 +- docs/roadmap.md | 5 ++- 8 files changed, 102 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 38252d8c..d77d149e 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-30 · **Change:** minor (hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); components can't contain their own instances, directly or through other components; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); offline Penpot 2.x import/export (#1143); optional batch continue-on-error with per-step outcomes (#1386); the Design panel's sections on one grid (#858); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); images placed at their own size, 2x screenshots in points (#1360); the toolbar on one 32 pt grid, without its Resources button (#1210); M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; 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; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); 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, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); components can't contain their own instances, directly or through other components; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); offline Penpot 2.x import/export (#1143); optional batch continue-on-error with per-step outcomes (#1386); the Design panel's sections on one grid (#858); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); images placed at their own size, 2x screenshots in points (#1360); the toolbar on one 32 pt grid, without its Resources button (#1210); M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; 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; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); 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, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **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 @@ -355,6 +355,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M8, #1114 (explicit variables files only): sibling variables files were read with a 256 MiB cap and a regular-file check, but an explicit path given to `variable.import`, or as the `variables` of `file.open` and `file.import`, was read whole with no limit. That path now goes through the same capped read: an oversized file, a folder or a device fails with "not a file of at most 268435456 bytes" and the document, tabs and recent files stay as they were. The check reads the file's size before reading it, so a file that grows or is replaced in between is not bounded; primary document reads and `image.place` are left for the rest of #1114. Tests: `import_export_tests::an_explicit_variables_path_is_capped_like_the_sibling_file`, `variables_io_tests::variables_import_caps_the_file_it_reads`. Numbers unchanged | | 2026-10-30 | minor | M8, #1571 (part): laggy drags and high CPU while the pointer only moves. Measured on a synthetic file of about 1,900 layers (24 auto layout frames of 48 rows, 600 shapes, 24 images; release build, 300 to 600 pointer moves at 60 per second): every pointer move draws about three egui frames (the move, egui's follow-up, a finished render), and the canvas hit-tested the hover on each of them; a drag of an auto layout frame laid its contents out again on every move, though a move can't change them. Now a hover where nothing changed since the last (same place, file, page, revision, selection, tool and modes) isn't sent to the tools again, and a frame that only moved keeps its own layout (its parent's still runs). Hover: the UI pass 0.91 to 0.49 ms a frame, CPU 33% to 22% of a core; dragging 648 layers: the UI pass 3.8 to 3.2 ms, the engine's normalise 3.8 to 2.1 ms a move, CPU about the same (117% to 116%). `ui.inspect` reports `frameTimes` ({frames, meanMs, maxMs}). Not yet: the selection overlay and the Design panel recompute every frame for a large selection (about 2 ms each at 648 layers), the canvas texture is uploaded whole after each render, and egui draws its follow-up frames. Tests: `canvas_hover_tests.rs`, `auto_layout_move_tests.rs`. | | 2026-10-30 | minor | M8, #1077: two quadratic costs in the `.fig` import's instance expansion. Variable and collection nodes sat in the layer tree, so every instance of a main walked the ones under it again, and as they never become layers the layer cap never stopped them; they are now left out of the tree (variables are read from the node list as before), so everything an expansion visits counts against the cap. A nested instance copied all of its outer instance's override rows, and its property values and slot contents; it now takes only the rows addressed below it (they sort together, so the rest aren't visited) and shares the outer values, keeping only its own. Normal files map as before. Crafted files: 20 000 variables under a main with 2 000 instances 13.8 s → 21 ms; 50 000 override rows over 2 000 nested instances 40.5 s → 98 ms (native release, the same output). Numbers unchanged. Tests: `expansion_cost_tests`. | | 2026-10-30 | minor | M9, #1415 (the rest): `comment.people` lists every name a message @mentions besides those who wrote or reacted (once each in any case, a known name spelt as the file spells it, never an email address), so a name that has only been tagged shows up. `comment.next` takes the `mentions` filter and, like `comment.open`, returns the thread as `comment.list` lists it, with each message's `mentions`; `comment.prompt`'s bounded `comments` carry each thread's mentions (at most 32, names cut to 80 characters). One mention helper serves them all. Numbers unchanged. Tests: `comment_mentions_tests`. | diff --git a/crates/engine/src/cmd/file/load.rs b/crates/engine/src/cmd/file/load.rs index a8462f19..8234ad09 100644 --- a/crates/engine/src/cmd/file/load.rs +++ b/crates/engine/src/cmd/file/load.rs @@ -699,6 +699,10 @@ pub(crate) fn variables_param(cmd: &str, p: &Value) -> Result> { None | Some(Value::Null) => return Ok(None), Some(v @ Value::Object(_)) => v.clone(), Some(Value::String(s)) if s.trim_start().starts_with('{') => parse(s.as_bytes())?, + // Capped like the sibling file; the browser has no file system, which `read` says. + #[cfg(not(target_arch = "wasm32"))] + Some(Value::String(path)) => parse(&read_capped(path, MAX_VARIABLES_FILE)?)?, + #[cfg(target_arch = "wasm32")] Some(Value::String(path)) => parse(&read(path)?)?, Some(_) => return Err(bad(cmd, "variables: the local variables JSON (an object, JSON text or a path)")), }; diff --git a/crates/engine/src/cmd/variables_io.rs b/crates/engine/src/cmd/variables_io.rs index f829785b..0648376b 100644 --- a/crates/engine/src/cmd/variables_io.rs +++ b/crates/engine/src/cmd/variables_io.rs @@ -25,7 +25,7 @@ pub fn specs() -> Vec { spec!( "variable.import", "Import variables", - "{variables (the local variables JSON, as variable.export writes it: an object, JSON text or a path; `variableCollections` and `variables` under `meta` or at the top level)} collections join this file's collection of the same name (values by mode name; new modes and variables are added, a variable of another type is skipped), others are added → {collections (added), variables (added), updated, warnings (also when it holds no variables, or fewer than its collections list)}", + "{variables (the local variables JSON, as variable.export writes it: an object, JSON text or a path to a regular file of at most 256 MiB; `variableCollections` and `variables` under `meta` or at the top level)} collections join this file's collection of the same name (values by mode name; new modes and variables are added, a variable of another type is skipped), others are added → {collections (added), variables (added), updated, warnings (also when it holds no variables, or fewer than its collections list)}", true, import ), diff --git a/crates/engine/src/import_export_tests.rs b/crates/engine/src/import_export_tests.rs index a60f85b0..b6897b2a 100644 --- a/crates/engine/src/import_export_tests.rs +++ b/crates/engine/src/import_export_tests.rs @@ -94,6 +94,63 @@ fn open_reads_variables_from_param_or_next_to_the_file() { let _ = std::fs::remove_dir_all(&dir); } +/// Every document, the active tab and the recent files, compared before and after a refused command. +fn state(s: &Session) -> (String, usize, Vec) { + (format!("{:?}", s.docs), s.active, s.prefs.recent_files.clone()) +} + +#[test] +fn an_explicit_variables_path_is_capped_like_the_sibling_file() { + let dir = temp_dir("restvars-cap"); + let path = dir.join("proto.json"); + std::fs::write(&path, serde_json::to_vec(&file()).unwrap()).unwrap(); + let path = path.to_string_lossy().to_string(); + let vars = dir.join("theme.json"); + std::fs::write(&vars, serde_json::to_vec(&variables()).unwrap()).unwrap(); + let vars = vars.to_string_lossy().to_string(); + + // A small file opens and imports as before. + let mut s = Session::new(); + s.execute("file.open", &json!({"path": path, "variables": vars})).unwrap(); + assert_eq!(named(&s, "Button").bound.get("fill").map(String::as_str), Some(BRAND)); + + // One byte over the limit (sparse, so nothing is written), a folder, and a device are refused + // before the document changes, by the same check the sibling file goes through. + let big = dir.join("big.json"); + std::fs::File::create(&big).unwrap().set_len(256 * 1024 * 1024 + 1).unwrap(); + let mut refused = vec![big.to_string_lossy().to_string(), dir.to_string_lossy().to_string()]; + if cfg!(unix) { + refused.push("/dev/null".to_string()); + } + s.prefs.recent_files = vec![path.clone()]; + s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 20, "height": 10})).unwrap(); + for bad in &refused { + for cmd in ["file.open", "file.import"] { + let before = state(&s); + let e = s.execute(cmd, &json!({"path": path, "variables": bad})).unwrap_err().to_string(); + assert!(e.contains("not a file of at most 268435456 bytes"), "{cmd} {bad}: {e}"); + assert_eq!(state(&s), before, "{cmd} {bad}"); + } + } + + // Access roots still apply: inside them the file reads, outside it is rejected. + let inside = temp_dir("restvars-cap-root"); + std::fs::copy(&vars, inside.join("theme.json")).unwrap(); + std::fs::copy(&path, inside.join("proto.json")).unwrap(); + let roots = std::sync::Arc::new(crate::file_access::Roots::new(&[inside.to_string_lossy().to_string()], &[]).unwrap()); + let inside_vars = inside.join("theme.json").to_string_lossy().to_string(); + let inside_path = inside.join("proto.json").to_string_lossy().to_string(); + crate::file_access::confined(Some(&roots), || { + s.execute("file.import", &json!({"path": inside_path, "variables": inside_vars})).unwrap(); + let before = state(&s); + let e = s.execute("file.import", &json!({"path": inside_path, "variables": vars})).unwrap_err().to_string(); + assert!(e.contains("automation path rejected"), "{e}"); + assert_eq!(state(&s), before); + }); + let _ = std::fs::remove_dir_all(&dir); + let _ = std::fs::remove_dir_all(&inside); +} + #[test] fn import_appends_flows_interactions_and_variables() { let mut s = Session::new(); diff --git a/crates/engine/src/variables_io_tests.rs b/crates/engine/src/variables_io_tests.rs index 55a2336b..b34294a8 100644 --- a/crates/engine/src/variables_io_tests.rs +++ b/crates/engine/src/variables_io_tests.rs @@ -270,6 +270,36 @@ fn variables_import_checks_what_it_reads() { healthy(&s); } +#[test] +fn variables_import_caps_the_file_it_reads() { + let dir = std::env::temp_dir().join(format!("figcraft-vars-io-cap-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let mut s = Session::new(); + theme(&mut s); + let small = dir.join("theme.json").to_string_lossy().into_owned(); + ex(&mut s, "variable.export", json!({"path": small})); + let mut t = Session::new(); + ex(&mut t, "variable.createCollection", json!({"name": "Other"})); + // One byte over the limit (sparse, so nothing is written), a folder, and a device: refused, + // nothing changes. + let big = dir.join("big.json"); + std::fs::File::create(&big).unwrap().set_len(256 * 1024 * 1024 + 1).unwrap(); + let mut refused = vec![big.to_string_lossy().into_owned(), dir.to_string_lossy().into_owned()]; + if cfg!(unix) { + refused.push("/dev/null".to_string()); + } + for bad in &refused { + let before = (format!("{:?}", t.docs), t.active); + let e = t.execute("variable.import", &json!({"variables": bad})).unwrap_err().to_string(); + assert!(e.contains("not a file of at most 268435456 bytes"), "{bad}: {e}"); + assert_eq!((format!("{:?}", t.docs), t.active), before, "{bad}"); + } + // A file under the limit still imports. + ex(&mut t, "variable.import", json!({"variables": small})); + assert_eq!(t.doc().unwrap().variables.collections.len(), 2); + let _ = std::fs::remove_dir_all(&dir); +} + /// A rectangle filled with a three-stop linear gradient, and colour variables `brand` and `ink`. fn gradient_rect(s: &mut Session) -> NodeId { let r = idv(&ex(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 40}))); diff --git a/docs/components-parity.md b/docs/components-parity.md index 6dc4bfae..4cf94617 100644 --- a/docs/components-parity.md +++ b/docs/components-parity.md @@ -1,6 +1,6 @@ # Components, variants, styles and variables parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7) Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300 @@ -71,13 +71,14 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`. | Explicit modes per frame, library collections' modes | done | | | Gradient stops and variant properties bound to variables | done | a stop's binding follows it when earlier stops are removed | | Extended collections | done | library collections, including a library's own extended collections (chains through libraries); overrides per mode with reset; follow library updates, variables new to the library collection being an update of their own; modes inherited. Checked with synthetic libraries only ([gaps](gaps.md) #14) | -| Variables import/export (JSON) | done | the REST local variables shape (round-trips; merges by collection, mode and variable name; `variableCollections` and `variables` read under `meta` or at the top level, a warning when no variables are found or fewer than the collections list); CSS custom properties and Dart constants (Flutter) export; in the browser the export arrives as a download (import there still needs a file picker) | +| Variables import/export (JSON) | done | the REST local variables shape (round-trips; merges by collection, mode and variable name; `variableCollections` and `variables` read under `meta` or at the top level, a warning when no variables are found or fewer than the collections list); CSS custom properties and Dart constants (Flutter) export; a variables file named by path must be a regular file of at most 256 MiB; in the browser the export arrives as a download (import there still needs a file picker) | | Expressions in prototype set-variable actions | done | see [prototyping-parity](prototyping-parity.md) | ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M8, #1114: variables import from an explicit file path (`variable.import`, the `variables` of `file.open` and `file.import`) takes regular files of at most 256 MiB, the limit the sibling `variables.json` already had; anything else is refused before the document changes. Grade unchanged. Tests: `import_export_tests::an_explicit_variables_path_is_capped_like_the_sibling_file`, `variables_io_tests::variables_import_caps_the_file_it_reads` | | 2026-10-30 | minor | M3, #1536 and #1537: component properties are listed in the order they were made in, and renaming one keeps its place (they were sorted by name, so a rename moved the property); that holds for every kind, on every variant of a set and on instances, in `component.properties`, the Design panel and `.fig` export. Files from before keep their properties by name until one is added or renamed. Make slot (the Slot menu of a frame inside a component) asks for the slot's name first: a field under the Slot row with the frame's name selected, Enter or Make slot to make it, Escape to cancel; a name taken keeps the field open. Renaming stays in the component's Properties list. Tests: `engine/src/prop_order_tests.rs`, `ui-egui/src/make_slot_naming_tests.rs`. Left: dragging the shared properties to reorder them, and renaming a slot with only its frame selected. Numbers unchanged | | 2026-10-29 | minor | M3, #1338: the instance-swap property's menu (on an instance, its main component's default and the component playground) and the instance's swap menu open with a search field at the top that has the keyboard; typing filters the components by name (any case, headings without matches drop out), an empty result reads "No matching components", Up and Down step through the matches and Enter picks one (the first without a step), Escape clears the search and then closes the menu. Only the list scrolls; the menu grows back when the search is cleared. Tests: `component_swap_search_tests`. Numbers unchanged | | 2026-10-26 | minor | An instance-swap property's default menu scrolls (#1338): on the main component's Properties row the menu listed every component in the file at full height, so in a file with many (an icon library) the bottom ones ran off the screen and couldn't be picked. It now scrolls past 320 pt like the instance's swap menu, is at least 200 pt wide, ticks the current default and is named for assistive technology (`component_swap_default_tests`). Left: searching these menus. Numbers unchanged. | diff --git a/docs/mcp.md b/docs/mcp.md index 28730001..fcd081ea 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1140,7 +1140,8 @@ default mode's named after the collection and the others after the collection an `static const` `Color`, `double`, `String` or `bool` per variable; names become camelCase Dart identifiers kept apart, aliases are resolved to their value in that mode), and returns `{text}` (or writes `path` and returns `{path}`). `variable.import {variables}` (an object, -JSON text or a path; `variableCollections` and `variables` under `meta` or at the top level) reads +JSON text or a path to a regular file of at most 256 MiB; +`variableCollections` and `variables` under `meta` or at the top level) reads one back: a collection joins this file's collection of the same name (values matched by mode name, new modes and variables added, a same-named variable of another type skipped with a warning; library copies and extended collections are never targets), others are diff --git a/docs/roadmap.md b/docs/roadmap.md index 2e8d19b4..6f97a063 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # FigCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and [`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours @@ -56,7 +56,7 @@ blockers. | M5 Prototype & present | ~85% | media, controllers on macOS/Linux/FreeBSD, separate window | 2–5 | | M6 Interop & Dev Mode | ~65% (code connect, Dev Mode plugins and best-effort `.fig` import landed 2026-10-11; best-effort `.sketch` import 2026-10-21) | `.fig` import's tail, video/HEIC (animated GIFs and TIFF landed 2026-10-11), CMYK intent | 15–30 | | M7 Release & fidelity (signed builds, look & feel, shortcuts) | builds done; fidelity pass started | tool-by-tool pass | 40–60 | -| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 | +| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% (explicit variables files capped at 256 MiB 2026-10-30, part of #1114) | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 | | M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme); the standalone `figcraft-server` (persistence with compaction, restarts that lose nothing confirmed, publishing from the app, share links to edit or view, revocable); team libraries kept on the server and enabled by a share link, with usage counts, and swapping a library; `wss://` from the desktop app, server releases and a deployment guide | accounts, teams | 45–90 | | M10 Localization | ~39%: the layer, English, Russian and Turkish reviewed by native speakers; Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Indonesian and Czech drafts; Arabic draft: 2,508 entries, native review and panel mirroring pending | native reviews (including the Arabic draft), Hindi, Vietnamese, Arabic panel mirroring, engine messages and number formatting ([localization-parity](localization-parity.md)) | 29–51 | | M11 Plugins | runtime, API, menu, manager and SDK done (#101); plugin panels and settings (ABI v2); widgets (API 3), panels side by side, colour picker on plugin fields; a plugin directory on `figcraft-server` (publish, browse, install, update); widget fields and property menus (API 4) | a public catalogue, publisher accounts and signatures, the directory in the browser, widget hover and timers; Figma plugin compatibility is an owner decision | 6–22 | @@ -75,6 +75,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M8, #1114 (explicit variables files): `variable.import` and the `variables` of `file.open` and `file.import` read a path with the same 256 MiB cap and regular-file check as the sibling `variables.json`, so an oversized file, a folder or a device is refused before the document changes; inline objects and JSON text, access roots and the browser's error are unchanged. Primary file reads and `image.place` are still uncapped. Tests: `import_export_tests::an_explicit_variables_path_is_capped_like_the_sibling_file`, `variables_io_tests::variables_import_caps_the_file_it_reads`. Numbers unchanged | | 2026-10-30 | minor | M8, #1571 (part): laggy drags and high CPU while the pointer only moves. Measured on a synthetic file of about 1,900 layers (24 auto layout frames of 48 rows, 600 shapes, 24 images; release build, 300 to 600 pointer moves at 60 per second): every pointer move draws about three egui frames (the move, egui's follow-up, a finished render), and the canvas hit-tested the hover on each of them; a drag of an auto layout frame laid its contents out again on every move, though a move can't change them. Now a hover where nothing changed since the last (same place, file, page, revision, selection, tool and modes) isn't sent to the tools again, and a frame that only moved keeps its own layout (its parent's still runs). Hover: the UI pass 0.91 to 0.49 ms a frame, CPU 33% to 22% of a core; dragging 648 layers: the UI pass 3.8 to 3.2 ms, the engine's normalise 3.8 to 2.1 ms a move, CPU about the same (117% to 116%). `ui.inspect` reports `frameTimes` ({frames, meanMs, maxMs}). Not yet: the selection overlay and the Design panel recompute every frame for a large selection (about 2 ms each at 648 layers), the canvas texture is uploaded whole after each render, and egui draws its follow-up frames. Tests: `canvas_hover_tests.rs`, `auto_layout_move_tests.rs`. | | 2026-10-30 | minor | M8, #1077: `.fig` instance expansion near linear on crafted files: variable nodes are out of the layer tree, nested instances take only the override rows below them and share property values; 13.8 s → 21 ms and 40.5 s → 98 ms on crafted files. Tests: `expansion_cost_tests` | | 2026-10-30 | minor | M9, #1415 (the rest): `comment.people` lists names that were only @mentioned; `comment.next` (with the `mentions` filter) and `comment.prompt` carry the same mentions as `comment.list`. Tests: `comment_mentions_tests` |