From f1e92749d971a30ac2523fcddd8c6aaa06ef90c0 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Thu, 12 Nov 2026 08:01:11 -0500 Subject: [PATCH 1/2] M6: .fig export writes vector region paints (#2302) Each vector region with paints of its own (an empty list included) now names an entry of the network's style table, so the paints come back on import; equal lists share one entry and a region without paints writes style 0. Only the paint frame of a region, which the importer cannot read, still warns. Co-Authored-By: Claude Sonnet 5.5 Model-Effort: Opus 5.5 drove; Fable 5.2 briefed; Sonnet 5.5 wrote at medium --- crates/format/src/fig/export/shape.rs | 22 ++++++++---- crates/format/src/fig/export_tests.rs | 51 ++++++++++++++++++--------- crates/format/src/fig/tail_tests.rs | 22 ++++++++++++ docs/file-format-parity.md | 12 ++++--- 4 files changed, 79 insertions(+), 28 deletions(-) diff --git a/crates/format/src/fig/export/shape.rs b/crates/format/src/fig/export/shape.rs index bfc2b0224..393a5d566 100644 --- a/crates/format/src/fig/export/shape.rs +++ b/crates/format/src/fig/export/shape.rs @@ -170,8 +170,8 @@ impl<'d> Exporter<'d> { /// The editable network (with per-point corner radii, handle mirroring and end caps) and, /// when it has filled areas, their outline. pub(super) fn vector(&mut self, n: &Node, net: &VectorNetwork, f: &mut Fields) { - if net.regions.iter().any(|region| region.fills.is_some() || region.paint_space.is_some()) { - self.warn("vector region paints were left out of the .fig export"); + if net.regions.iter().any(|region| region.paint_space.is_some()) { + self.warn("vector region paint frames were left out (their gradients and images follow the layer's box)"); } let entries: usize = net.regions.iter().flat_map(|r| &r.loops).map(Vec::len).sum(); if net.vertices.len() + net.segments.len() + entries > MAX_NETWORK { @@ -249,11 +249,19 @@ impl<'d> Exporter<'d> { tb: (finite(sg.tb.0), finite(sg.tb.1)), }) .collect(); - let regions = net - .regions - .iter() - .map(|r| network::Region { style: 0, non_zero: r.winding == WindingRule::NonZero, loops: r.loops.clone() }) - .collect(); + // A region with paints of its own (an empty list included) names a style entry that + // holds them; one without writes style 0 and shows the layer's fills. + let mut regions = Vec::with_capacity(net.regions.len()); + for r in &net.regions { + let style = match &r.fills { + Some(paints) => { + let list = self.paints(paints, true, &Bound::default()); + style_of(vec![("fillPaints", list)]) + } + None => 0, + }; + regions.push(network::Region { style, non_zero: r.winding == WindingRule::NonZero, loops: r.loops.clone() }); + } let blob = network::encode(&network::Network { vertices, segments, regions }); let table = styles.into_iter().zip(1u32..).map(|(mut st, id)| { st.insert(0, ("styleID", u(id))); diff --git a/crates/format/src/fig/export_tests.rs b/crates/format/src/fig/export_tests.rs index 221cd7cd9..9cb417078 100644 --- a/crates/format/src/fig/export_tests.rs +++ b/crates/format/src/fig/export_tests.rs @@ -843,21 +843,38 @@ fn round_trip_keeps_the_details() { } #[test] -fn fig_export_warns_when_region_paints_are_left_out() { - let mut doc = generate(3); - let Some(id) = doc.nodes().find_map(|node| match &node.kind { - NodeKind::Vector { network } if !network.regions.is_empty() => Some(node.id), - _ => None, - }) else { - panic!("generated document has no filled vector"); - }; - let node = doc.node_mut(id).unwrap(); - let NodeKind::Vector { network } = &mut node.kind else { panic!("expected vector fixture") }; - let region = network.regions.first_mut().unwrap(); - region.fills = Some(Vec::new()); +fn vector_region_paints_round_trip() { + let square = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)].map(|(x, y)| Point::new(x, y)); + let mut doc = Document::new("regions"); + let page = doc.first_page().unwrap(); + let mut two = VectorNetwork::from_points(&square, true); + let again = two.regions[0].clone(); + two.regions.push(again); + let mut half = Paint::solid(Color { r: 0.25, g: 0.5, b: 0.75, a: 1.0 }); + half.opacity = 0.5; + let gradient = Paint::gradient(Gradient::default_for(GradientKind::Linear, Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0 })); + two.regions[0].fills = Some(vec![half, gradient]); + two.regions[1].fills = Some(Vec::new()); + let id = doc.create(page, NodeKind::Vector { network: two }, "Painted", 0.0, 0.0, 100.0, 100.0).unwrap(); + doc.node_mut(id).unwrap().fills = vec![Paint::solid(Color::BLACK)]; + let inherit = VectorNetwork::from_points(&square, true); + let id2 = doc.create(page, NodeKind::Vector { network: inherit }, "Inherits", 0.0, 0.0, 100.0, 100.0).unwrap(); + doc.node_mut(id2).unwrap().fills = vec![Paint::solid(Color::BLACK)]; - let (_, warnings) = export(&doc, None).unwrap(); - assert!(warnings.iter().any(|warning| warning == "vector region paints were left out of the .fig export"), "{warnings:?}"); + let (back, warnings) = round_trip(&doc, "region paints"); + assert!(!warnings.iter().any(|w| w.contains("region")), "{warnings:?}"); + let regions_of = |d: &Document, name: &str| match d.nodes().find(|n| n.name == name).map(|n| &n.kind) { + Some(NodeKind::Vector { network }) => network.regions.clone(), + k => panic!("{name}: {k:?}"), + }; + let painted = regions_of(&back, "Painted"); + assert_eq!(painted.len(), 2); + let fills = painted[0].fills.as_ref().expect("the first region keeps its paints"); + assert_eq!(fills.len(), 2); + assert!(matches!(&fills[0].kind, PaintKind::Solid { .. }) && (fills[0].opacity - 0.5).abs() < 1e-6); + assert!(matches!(&fills[1].kind, PaintKind::Gradient(_))); + assert_eq!(painted[1].fills, Some(Vec::new()), "an unfilled region stays unfilled"); + assert_eq!(regions_of(&back, "Inherits")[0].fills, None, "a region with no paints of its own still shows the layer's"); } #[test] @@ -875,16 +892,18 @@ fn fig_export_warns_when_inherited_region_paints_have_a_paint_frame() { let region = network.regions.first_mut().unwrap(); region.fills = None; region.paint_space = Some(RegionPaintSpace { width: 20.0, height: 10.0, transform: Affine::translate((4.0, 8.0)) }); + let text = "vector region paint frames were left out (their gradients and images follow the layer's box)"; let (_, warnings) = export(&doc, None).unwrap(); - assert!(warnings.iter().any(|warning| warning == "vector region paints were left out of the .fig export"), "{warnings:?}"); + assert!(warnings.iter().any(|warning| warning == text), "{warnings:?}"); let mut plain = doc; let node = plain.node_mut(id).unwrap(); let NodeKind::Vector { network } = &mut node.kind else { panic!("expected vector fixture") }; network.regions[0].paint_space = None; + network.regions[0].fills = Some(vec![Paint::solid(Color::BLACK)]); let (_, warnings) = export(&plain, None).unwrap(); - assert!(!warnings.iter().any(|warning| warning == "vector region paints were left out of the .fig export"), "{warnings:?}"); + assert!(!warnings.iter().any(|warning| warning.contains("region paint")), "{warnings:?}"); } #[test] diff --git a/crates/format/src/fig/tail_tests.rs b/crates/format/src/fig/tail_tests.rs index 663d6002b..375736e77 100644 --- a/crates/format/src/fig/tail_tests.rs +++ b/crates/format/src/fig/tail_tests.rs @@ -359,6 +359,28 @@ fn vector_region_missing_and_empty_paints_remain_distinct() { assert_eq!(network_of(&empty, "Wave").regions[0].fills, Some(Vec::new())); } +#[test] +fn vector_region_fills_survive_a_fig_export() { + let solid = o(vec![("type", e("SOLID")), ("color", o(vec![("r", f(0.2)), ("g", f(0.4)), ("b", f(0.8)), ("a", f(1.0))])), ("opacity", f(0.5))]); + let gradient = o(vec![ + ("type", e("GRADIENT_LINEAR")), + ( + "stops", + arr(vec![ + o(vec![("position", f(0.0)), ("color", o(vec![("r", f(1.0)), ("g", f(0.0)), ("b", f(0.0)), ("a", f(1.0))]))]), + o(vec![("position", f(1.0)), ("color", o(vec![("r", f(0.0)), ("g", f(0.0)), ("b", f(1.0)), ("a", f(1.0))]))]), + ]), + ), + ]); + let (doc, _) = import(®ion_canvas(Some(vec![solid, gradient]), 1), "Region fills").unwrap(); + let first = network_of(&doc, "Wave").regions[0].fills.clone(); + assert!(first.as_ref().is_some_and(|fills| fills.len() == 2), "{first:?}"); + let (bytes, warnings) = crate::fig::export::export(&doc, None).unwrap(); + assert!(!warnings.iter().any(|warning| warning.contains("region paints")), "{warnings:?}"); + let (back, _) = import(&bytes, "Again").unwrap(); + assert_eq!(network_of(&back, "Wave").regions[0].fills, first); +} + #[test] fn vector_region_linked_fill_uses_style_paints_without_recording_definition() { let linked = solid(0.9, 0.1, 0.2); diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 56de5f480..5d9655120 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (`.fig` import and export read and write a boolean group's corner radius and smoothing (#2222); `.jam` import: stickies come in as native board stickies (#1808); `.fig` import scales per-point vector corner radii with the row that supplies their network and keeps different instance scales separate in the network cache (#2097); `.fig` export preserves scaled-instance lengths reset to their unscaled main values, including nested linked-style overrides (#2097); `.fig` import: the layers of an instance scaled with the Scale tool draw at its scale factor, not at their main's lengths (#2091); a color profile per PNG, JPG and WebP export setting (Same as file, sRGB or Display P3), the pixels converted; a `.jam` connector end with an `AUTO` or `CENTER` magnet leaves the side facing the other end, not the side its stale saved point named (#1909); a FigJam elbow connector saved with its path (`connectorControlPoints`) keeps it on import, as a board connector reshaped by hand does, instead of being routed again (#1910); JPG exports take a quality, 1 to 100 with 90 the default, and embed the file's colour profile when it is sRGB too (#2004); a FigJam connector without a saved snapshot took no stroke and was invisible; its line's paint, weight, cap and dashes now come from the file's generated line record (#1807); a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; pasted layers whose image the clipboard lacks are named and show a placeholder that reads as missing (#1646); paint-style colours on layers pasted from another design app keep their style instead of turning black (#1638); imported and pasted text finds an installed family under the other names its fonts give it, and with other case and spacing (#1292); `.fig` export writes colour calculations as the colour they show in each mode and leaves out, with a warning, one that resolves to nothing (#1280); `.fig` import budgets four decoded values per compressed byte instead of one, still within 20M–128M, so dense files within that budget open instead of being refused as having too many values (#574); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; folders, devices and pipes are refused before anything is read (#1114); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); a FigJam `.jam` file opens as a board (#1342); FigJam `.jam` files open like `.fig` files: File ▸ Open, drag and drop, `file.import` and the command line (#1342); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); SVG import draws each pattern tile once per scale, within the layer budget (#1080); 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); offline Penpot 2.x import/export (#1143); `.sketch` import checked against Sketch's renders: master backgrounds, symbol clipping, tints before Sketch 64, mask opacity, bitmap fills, image overrides on shapes, stretching stacks, blur radii, colour adjustments, OKLab gradients, text growth, shape group borders (#1389); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); image member names are checked on read, and REST image placeholders get archive-safe keys (#1086); Android Vector Drawable (XML) export; `.fig` export: files the incumbent imports, with component set properties, and grid track sizes at its field ids; Save and auto-save don't replace changes another app made to the open file (#1330); `.sketch` import, best effort, Sketch 48 and newer; the PNG row names ignore overlapping layers and where include bounding box is offered; the Fonts table's missing-font row describes the Missing fonts window; `.figcraft` carries the file kind (boards); `.fig` import: resized instances and variable modes by asset key; atomic saves; auto-save; SVG export clip of nested frames; SVG/PDF export depth refusal (#208); `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; `.fig` export: written files checked by opening them in other software; experimental `.fig` export; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; embedded image colour profiles; `.fig` import, best effort; animated GIF fills and TIFF images; last major: created, formats from the installed Figma's Info.plist and help center, ours from source; REST import keeps slot layers and instances' slot content (#792); REST import reads grid track sizes (#465); `.fig` import fidelity from the community batches, budgets for large files, the import report, a `.fig` copy from Save as, paste from another design app; instance layers' size and resizing and Hug on import (#731, #732)) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-12 · **Change:** minor (`.fig` export writes vector region paints (#2302); `.fig` import and export read and write a boolean group's corner radius and smoothing (#2222); `.jam` import: stickies come in as native board stickies (#1808); `.fig` import scales per-point vector corner radii with the row that supplies their network and keeps different instance scales separate in the network cache (#2097); `.fig` export preserves scaled-instance lengths reset to their unscaled main values, including nested linked-style overrides (#2097); `.fig` import: the layers of an instance scaled with the Scale tool draw at its scale factor, not at their main's lengths (#2091); a color profile per PNG, JPG and WebP export setting (Same as file, sRGB or Display P3), the pixels converted; a `.jam` connector end with an `AUTO` or `CENTER` magnet leaves the side facing the other end, not the side its stale saved point named (#1909); a FigJam elbow connector saved with its path (`connectorControlPoints`) keeps it on import, as a board connector reshaped by hand does, instead of being routed again (#1910); JPG exports take a quality, 1 to 100 with 90 the default, and embed the file's colour profile when it is sRGB too (#2004); a FigJam connector without a saved snapshot took no stroke and was invisible; its line's paint, weight, cap and dashes now come from the file's generated line record (#1807); a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; pasted layers whose image the clipboard lacks are named and show a placeholder that reads as missing (#1646); paint-style colours on layers pasted from another design app keep their style instead of turning black (#1638); imported and pasted text finds an installed family under the other names its fonts give it, and with other case and spacing (#1292); `.fig` export writes colour calculations as the colour they show in each mode and leaves out, with a warning, one that resolves to nothing (#1280); `.fig` import budgets four decoded values per compressed byte instead of one, still within 20M–128M, so dense files within that budget open instead of being refused as having too many values (#574); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; folders, devices and pipes are refused before anything is read (#1114); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); a FigJam `.jam` file opens as a board (#1342); FigJam `.jam` files open like `.fig` files: File ▸ Open, drag and drop, `file.import` and the command line (#1342); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); SVG import draws each pattern tile once per scale, within the layer budget (#1080); 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); offline Penpot 2.x import/export (#1143); `.sketch` import checked against Sketch's renders: master backgrounds, symbol clipping, tints before Sketch 64, mask opacity, bitmap fills, image overrides on shapes, stretching stacks, blur radii, colour adjustments, OKLab gradients, text growth, shape group borders (#1389); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); image member names are checked on read, and REST image placeholders get archive-safe keys (#1086); Android Vector Drawable (XML) export; `.fig` export: files the incumbent imports, with component set properties, and grid track sizes at its field ids; Save and auto-save don't replace changes another app made to the open file (#1330); `.sketch` import, best effort, Sketch 48 and newer; the PNG row names ignore overlapping layers and where include bounding box is offered; the Fonts table's missing-font row describes the Missing fonts window; `.figcraft` carries the file kind (boards); `.fig` import: resized instances and variable modes by asset key; atomic saves; auto-save; SVG export clip of nested frames; SVG/PDF export depth refusal (#208); `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; `.fig` export: written files checked by opening them in other software; experimental `.fig` export; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; embedded image colour profiles; `.fig` import, best effort; animated GIF fills and TIFF images; last major: created, formats from the installed Figma's Info.plist and help center, ours from source; REST import keeps slot layers and instances' slot content (#792); REST import reads grid track sizes (#465); `.fig` import fidelity from the community batches, budgets for large files, the import report, a `.fig` copy from Save as, paste from another design app; instance layers' size and resizing and Hug on import (#731, #732)) · **Target:** Figma Design (desktop app 126.10.7) Every format Figma reads or writes, with FigCraft's support. Sources: `Info.plist` of `/Applications/Figma.app` (document types only; nothing inside the bundle's code was read), Figma's @@ -81,8 +81,9 @@ visible and opaque (a page also its background opacity): a reader takes what is draws nothing below a hidden page. The message lists one node change per layer, parents first, each with its GUID, its parent and a position string that orders it among its siblings; vector networks and outlines go in blobs, written once -each. `.fig` export warns and drops vector region paint stacks; import and editing preserve their -source paint coordinates. +each. `.fig` export writes a vector's region paint stacks (each filled area's own paints as an entry of the +network's style table, named by the area's style id); a region's kept paint frame is left out with a +warning; import and editing preserve their source paint coordinates. Scaled instances retain an explicit override when a length is reset to the unscaled main's value, rather than losing it to the field diff. This covers corners, strokes, layout and grid lengths, @@ -93,7 +94,7 @@ scales and empty-style clears; they have not been checked in other software. - **Written:** pages (name, background); frames, groups, sections, rectangles (as rounded rectangles, with their four radii), ellipses (arcs), polygons, stars, lines, vectors (editable - network blobs with per-point corner radius, handle mirroring, caps and joins, plus the outline of + network blobs with per-point corner radius, handle mirroring, caps and joins, each filled area's own paints, plus the outline of their filled areas), text, boolean groups, slices; the fill outline of frames and shapes and the stroke outline of plain strokes (a ring around boxes and whole ellipses, the widened path of other centred strokes); @@ -133,7 +134,7 @@ scales and empty-style clears; they have not been checked in other software. - **Left out, with a warning each:** colour calculations that resolve to nothing (an operand missing, or more than 32 variables to read), variable bindings the format has no field for (component properties, arc fields, font weight: written as the values they give), text styles on part of a - text (written as values), variables' publishing, code syntax and inheritance settings, extended + text (written as values), the paint frames of vector regions, variables' publishing, code syntax and inheritance settings, extended collections (written as collections of their own), comments, Developer Mode annotations, statuses, resources, measurements and code links, plugin data, ruler guides, prototype device settings, slot properties and slot content, brush @@ -196,6 +197,7 @@ scales and empty-style clears; they have not been checked in other software. | Date | Change | Summary | |---|---|---| +| 2026-11-12 | minor | M6, #2302: the `.fig` export writes the paints of a vector's own regions: each region with paints of its own (an empty list means unfilled) names an entry of the network's style table (`styleOverrideTable[].fillPaints`), equal lists share one entry, and a region without paints writes style 0. The fill outline is still one path with style 0. A region's kept paint frame is still left out, with a narrower warning. The importer already read this; not checked in other software | | 2026-11-10 | minor | M4, #2222: a boolean group's corner radius and smoothing are read and written by the `.fig` importer and exporter, and by the REST importer | | 2026-11-07 | patch | Fonts, #2092: a font loaded with `text.loadFont` is the counterpart of a font uploaded to the organization, which Figma's help center ("Manage conflicting fonts") ranks first, ahead of installed fonts and the web fonts. FigCraft chose the first face added for a family and style, so the built-in Inter 4.x kept Regular, SemiBold and Bold after Inter 3.19 was loaded and only Medium refitted. A loaded face now wins over the other faces of its family and style, the latest load over earlier ones; style matching and fallbacks are unchanged, and `refitted` lists the text whose size changes. Installed versus built-in order is not changed here. Test: `fontdb::tests::a_loaded_face_wins_over_the_built_in_one_of_its_family_and_style`. Numbers unchanged. | | 2026-11-08 | minor | M6, #2097 follow-up: per-point vector corner radii take the scale of the row supplying `vectorData`, including nested instance rows; override and derived rows are not scaled twice. Network caching distinguishes the instance scale even when resizing gives two vectors the same final box. Synthetic tests cover missing/unit factors, half/double scale, derived size alone, overridden and derived vector data, nested scale and equal-size cache separation. Layer-radius fallback remains the separate #2115 change. Numbers unchanged. | From 98f946267d9a4838e0f30fe42e78823d2f244c97 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Thu, 12 Nov 2026 08:13:55 -0500 Subject: [PATCH 2/2] M6: gaps.md says the export writes region paints; the warning test can fail again (#2302) Gap 1's 2026-10-26 bullet still said the export drops region fills. tail_tests asserted on "region paints", which the narrowed warning can never contain; it now checks for any region warning. Co-Authored-By: Claude Opus 5.5 Model-Effort: Opus 5.5 drove and fixed; Fable 5.2 reviewed --- crates/format/src/fig/tail_tests.rs | 2 +- docs/gaps.md | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/format/src/fig/tail_tests.rs b/crates/format/src/fig/tail_tests.rs index 375736e77..264216bcc 100644 --- a/crates/format/src/fig/tail_tests.rs +++ b/crates/format/src/fig/tail_tests.rs @@ -376,7 +376,7 @@ fn vector_region_fills_survive_a_fig_export() { let first = network_of(&doc, "Wave").regions[0].fills.clone(); assert!(first.as_ref().is_some_and(|fills| fills.len() == 2), "{first:?}"); let (bytes, warnings) = crate::fig::export::export(&doc, None).unwrap(); - assert!(!warnings.iter().any(|warning| warning.contains("region paints")), "{warnings:?}"); + assert!(!warnings.iter().any(|warning| warning.contains("region")), "{warnings:?}"); let (back, _) = import(&bytes, "Again").unwrap(); assert_eq!(network_of(&back, "Wave").regions[0].fills, first); } diff --git a/docs/gaps.md b/docs/gaps.md index e7cbdb76a..66ac5bb9f 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where FigCraft falls short of Figma -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (gap 13: a boolean group's corner radius (#2222); gap 24: mind maps, first step: a first node with three children on curved branches, laid out again after every edit (`board.mindMap`, `board.mindMapAdd`, #2226); gap 10: library analytics list how many layers and files use each style and variable; the Controller menu names each button as PlayStation and Switch controllers do; gap 6: widgets draw lines and SVG pictures (plugin API 6); gap 7: text reads only the fallback fonts it draws with; a board connector's path can be reshaped by dragging its path handles (#1710); gap 10: library analytics count insertions and show insertions and detachments over time; gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the keyboard's Menu key opens the context menu, as Shift+F10 does; gap 14: a collection of this file extends, following its edits at once; colour picker review fixes for text undo, fitted sidebars, moving controls and gradient variables; gap 6: plugin widgets have hover styles; gap 7: the browser draws a zoom's full render a band at a time; `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); Arabic draft catalog; gap 7: the browser refines a zoom's preview once the zoom has paused; gap 17: branches in live sessions, branch history entries, the list-merge rows brought up to date; gap 6: plugin widgets take text input and a property menu (plugin API 4); gap 24: board templates and tables; gap 24: a shared timer and voting sessions; gap 24: the board dot grid, connector ends dragged to re-attach, curved connectors; egui/eframe 0.37 verified for gap 18; known gaps from the #1311 review in gaps 4, 14, 15 and 16; gap 16: measurements kept on the canvas, a read-only layers panel in Dev Mode; gap 24: boards and their tail; Dev Mode tail: import lines in layer trees, compare with versions, an inspect-only canvas; gap 5: reviewed Turkish, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; 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; 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; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; 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: gap 14 down to its tail; the 2026-10-17 landings: `.fig` import fidelity, budgets for large files, the import report, a `.fig` copy from Save as and paste from another design app (gap 1); menus, the Actions palette, editor fixes, Selection colors and smart-selection spacing (gap 4); catalog counts re-measured (gap 5); bounded canvas pan overscan) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-12 · **Change:** minor (gap 1: the `.fig` export writes vector region paints (#2302); gap 13: a boolean group's corner radius (#2222); gap 24: mind maps, first step: a first node with three children on curved branches, laid out again after every edit (`board.mindMap`, `board.mindMapAdd`, #2226); gap 10: library analytics list how many layers and files use each style and variable; the Controller menu names each button as PlayStation and Switch controllers do; gap 6: widgets draw lines and SVG pictures (plugin API 6); gap 7: text reads only the fallback fonts it draws with; a board connector's path can be reshaped by dragging its path handles (#1710); gap 10: library analytics count insertions and show insertions and detachments over time; gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the keyboard's Menu key opens the context menu, as Shift+F10 does; gap 14: a collection of this file extends, following its edits at once; colour picker review fixes for text undo, fitted sidebars, moving controls and gradient variables; gap 6: plugin widgets have hover styles; gap 7: the browser draws a zoom's full render a band at a time; `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); Arabic draft catalog; gap 7: the browser refines a zoom's preview once the zoom has paused; gap 17: branches in live sessions, branch history entries, the list-merge rows brought up to date; gap 6: plugin widgets take text input and a property menu (plugin API 4); gap 24: board templates and tables; gap 24: a shared timer and voting sessions; gap 24: the board dot grid, connector ends dragged to re-attach, curved connectors; egui/eframe 0.37 verified for gap 18; known gaps from the #1311 review in gaps 4, 14, 15 and 16; gap 16: measurements kept on the canvas, a read-only layers panel in Dev Mode; gap 24: boards and their tail; Dev Mode tail: import lines in layer trees, compare with versions, an inspect-only canvas; gap 5: reviewed Turkish, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; 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; 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; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; 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: gap 14 down to its tail; the 2026-10-17 landings: `.fig` import fidelity, budgets for large files, the import report, a `.fig` copy from Save as and paste from another design app (gap 1); menus, the Actions palette, editor fixes, Selection colors and smart-selection spacing (gap 4); catalog counts re-measured (gap 5); bounded canvas pan overscan) · **Target:** Figma Design (desktop app 126.10.7) Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The @@ -87,8 +87,8 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe layer budgets; the issue sample was checked locally only. Native board metadata and direct `.jam` opening (#1342) remain separate work. - **Landed (2026-10-26):** `.fig` import reads vector-region fill stacks; vector editing and path - operations retain them with their source paint coordinates. `.fig` export still warns and drops - region fills. Evidence: `crates/format/src/fig/tail_tests.rs`, `crates/engine/src/shapebuilder_region_paints_tests.rs`. + operations retain them with their source paint coordinates. `.fig` export writes them since + 2026-11-12 (#2302); a region's kept paint frame is left out, with a warning. Evidence: `crates/format/src/fig/tail_tests.rs`, `crates/engine/src/shapebuilder_region_paints_tests.rs`. - **Fixed (2026-10-29, #1236):** imported text keeps the line breaks the file saved with it (`derivedTextData`), so a font whose advances differ slightly from the file's (another build of the family, a stand-in) no longer wraps a word early or splits one in a narrow box. The @@ -595,6 +595,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe | Date | Change | Summary | |---|---|---| +| 2026-11-12 | minor | Gap 1: the `.fig` export writes vector region paints (#2302); the 2026-10-26 "Landed" bullet no longer says the export drops them | | 2026-11-10 | minor | Gap 13: a boolean group's corner radius rounds every sharp corner of its result (#2222); the boolean clause leaves the 2026-10-12 "Left" list | | 2026-11-10 | minor | Gap 24: mind maps, first step (`board.mindMap`, `board.mindMapAdd`; the map lays itself out again after every edit, #2226); the toolbar entry, shortcuts, drag to re-parent and `.jam` import are what is missing of them. | | 2026-11-03 | minor | Traditional Chinese (Taiwan) (`zh-Hant`) draft catalog: 1,975 UI strings, 641 command labels and 31 menu wordings, clean-room from the English source; zh-TW, zh-HK, zh-MO and zh-Hant read it, explicit Hans stays Simplified; Noto Sans CJK TC then Taiwan system faces; no bundled font; native review pending. Numbers unchanged |