From e211bf5476c44512bf3238e3698c085267f5da9b Mon Sep 17 00:00:00 2001 From: Ridzzz0Alam Date: Wed, 21 Oct 2026 18:22:16 +0200 Subject: [PATCH 1/2] M1: large .fig files keep their layers and their library links (#981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A design-system .fig file (54 pages) stopped at 244 248 layers, the rest of its pages skipped, and most of its instances came in as frames "of components that aren't in the file", with the cap note shown twice. - The layer budget follows the compressed canvas at 8 bytes a layer, not 32: design systems are mostly instances, which pack tightly. Files under about 1.6 MB keep the 200 000 floor and the 2 million ceiling stays (measured on synthetic files: about 5.4 KB and 19 µs a layer). - The library copies the pages use (the file's hidden internal canvas) are mapped before the pages: at the cap the pages' last layers are skipped, not the mains their instances link to. The REST stage keeps building pages first (its ids are first-wins) and allows for the wrapper frame of each library copy, the one layer the mapper doesn't count; that layer tripped the REST stage's own cap and repeated the note. - Instances and swap defaults whose main is in the file but past the cap say so instead of "aren't in the file" (en and es catalogs). - The .fig import gives a note the mapper already gave only once. Tests in fig/layer_cap_tests.rs: the budget for a #981-sized canvas, library mains ahead of the pages at the cap, the note for a capped local main, and a whole 210 000-layer file with one cap note and its library links kept. All four fail on the previous code. --- crates/format/src/fig/layer_cap_tests.rs | 117 +++++++++++++++++++++++ crates/format/src/fig/map.rs | 32 ++++++- crates/format/src/fig/mod.rs | 17 +++- crates/format/src/rest/mod.rs | 33 +++++-- crates/ui-egui/locales/en.toml | 2 + crates/ui-egui/locales/es.toml | 2 + 6 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 crates/format/src/fig/layer_cap_tests.rs diff --git a/crates/format/src/fig/layer_cap_tests.rs b/crates/format/src/fig/layer_cap_tests.rs new file mode 100644 index 00000000..6eeb15f4 --- /dev/null +++ b/crates/format/src/fig/layer_cap_tests.rs @@ -0,0 +1,117 @@ +//! Large `.fig` files at the layer cap (#981): the budget a design-system file gets, the library +//! copies its instances need kept ahead of the pages, honest notes for what the cap kept out, and +//! one note for the cap itself. + +use figcraft_doc::{Document, NodeKind}; + +use super::kiwi::{Kv, Schema}; +use super::*; + +type Fields = Vec<(&'static str, Kv<'static>)>; + +fn o(fields: Fields) -> Kv<'static> { + Kv::Object(fields) +} + +fn g(id: u64) -> Kv<'static> { + o(vec![("sessionID", Kv::Uint(1)), ("localID", Kv::Uint(id))]) +} + +fn xy(x: f64, y: f64) -> Kv<'static> { + o(vec![("x", Kv::Float(x as f32)), ("y", Kv::Float(y as f32))]) +} + +/// A layer under `parent`; siblings come in id order. +fn node(id: u64, parent: Option, ty: &'static str, name: &str, more: Fields) -> Kv<'static> { + let mut fields = vec![("guid", g(id)), ("phase", Kv::Enum("CREATED")), ("type", Kv::Enum(ty)), ("name", Kv::Str(name.into()))]; + if let Some(parent) = parent { + fields.push(("parentIndex", o(vec![("guid", g(parent)), ("position", Kv::Str(format!("{id:08}")))]))); + } + fields.extend(more); + o(fields) +} + +fn instance(id: u64, page: u64, main: u64) -> Kv<'static> { + node(id, Some(page), "INSTANCE", "Instance", vec![("symbolData", o(vec![("symbolID", g(main))]))]) +} + +/// A main `id` of `layers` rectangles under `parent`. +fn main(id: u64, parent: u64, name: &str, layers: u64) -> Vec> { + let mut out = vec![node(id, Some(parent), "SYMBOL", name, vec![("size", xy(10.0, 10.0))])]; + out.extend((1..=layers).map(|i| node(id * 10_000 + i, Some(id), "RECTANGLE", "R", vec![("size", xy(1.0, 1.0))]))); + out +} + +/// A page (2) of `count` instances of a main kept on the file's hidden library canvas (3), as a +/// design system's file carries the library components it uses. +fn library_message(count: u64, main_layers: u64) -> Kv<'static> { + let mut nodes = vec![ + node(1, None, "DOCUMENT", "Document", vec![]), + node(2, Some(1), "CANVAS", "Screens", vec![]), + node(3, Some(1), "CANVAS", "Internal", vec![("internalOnly", Kv::Bool(true))]), + ]; + nodes.extend(main(20, 3, "Library main", main_layers)); + nodes.extend((0..count).map(|i| instance(1_000 + i, 2, 20))); + o(vec![("nodeChanges", Kv::Array(nodes))]) +} + +/// The `.fig` import's two stages at layer cap `cap`. +fn import_at(msg: &Kv, cap: usize) -> (Document, Vec) { + let (json, mut warnings, networks) = map::to_rest(msg, "Capped", cap); + let (doc, more) = crate::rest::import_with_networks(&json, &|_: &str| None, networks, cap).unwrap(); + warnings.extend(more); + (doc, warnings) +} + +fn linked_to(doc: &Document, main_name: &str) -> usize { + doc.nodes().filter(|n| matches!(n.kind, NodeKind::Instance { component, .. } if doc.node(component).is_some_and(|c| c.name == main_name))).count() +} + +#[test] +fn a_design_system_sized_canvas_gets_room() { + // The file in #981 stopped at 244 248 layers: a 7.8 MB canvas at 32 bytes a layer. + let reported = 244_248 * 32; + assert!(layer_budget(reported) >= 4 * 244_248, "{}", layer_budget(reported)); + // Small files still can't claim more than the REST import's cap, nor any file the ceiling. + assert_eq!(layer_budget(1 << 20), crate::rest::MAX_NODES); + assert_eq!(layer_budget(usize::MAX), MAX_FIG_NODES); +} + +#[test] +fn library_mains_come_before_the_pages_at_the_cap() { + // 40 instances of an 11-layer library main on a page, with room for 100 layers: mapped after + // the pages, the main found no room and every instance became a frame of "a component that + // isn't in the file". + let (doc, warnings) = import_at(&library_message(40, 10), 100); + assert!(warnings.iter().any(|w| w.contains("more than 100 layers")), "{warnings:?}"); + assert!(!warnings.iter().any(|w| w.contains("aren't in the file")), "{warnings:?}"); + let linked = linked_to(&doc, "Library main"); + assert!(linked >= 5, "{linked} instances linked to the library main: {warnings:?}"); + assert!(doc.nodes().any(|n| n.name == "Library main" && doc.is_remote(n.id))); +} + +#[test] +fn instances_of_a_main_the_cap_kept_out_say_so() { + // The page's instances come before their main: at the cap, the main is skipped though the + // file has it. + let mut nodes = vec![node(1, None, "DOCUMENT", "Document", vec![]), node(2, Some(1), "CANVAS", "Page", vec![])]; + nodes.extend((0..20).map(|i| instance(100 + i, 2, 900))); + nodes.extend(main(900, 2, "Local main", 10)); + let (doc, warnings) = import_at(&o(vec![("nodeChanges", Kv::Array(nodes))]), 60); + assert!(warnings.iter().any(|w| w.starts_with("instances whose component was past the layer limit were imported as frames")), "{warnings:?}"); + assert!(!warnings.iter().any(|w| w.contains("aren't in the file")), "{warnings:?}"); + assert!(!doc.nodes().any(|n| n.name == "Local main")); +} + +#[test] +fn a_whole_file_past_the_cap_keeps_its_library_links_and_says_so_once() { + // A 1 001-layer library main used 210 times: about 210 000 layers in a file far too small to + // lift the cap above the REST import's 200 000. + let file = super::tests::canvas(&Schema::parse(export::SCHEMA).unwrap(), &library_message(210, 1_000)); + assert_eq!(layer_budget(file.len()), crate::rest::MAX_NODES); + let (doc, warnings) = import(&file, "Design system.fig").unwrap(); + let cap: Vec<_> = warnings.iter().filter(|w| w.contains("layers; the rest were skipped")).collect(); + assert_eq!(cap.len(), 1, "{warnings:?}"); + assert!(!warnings.iter().any(|w| w.contains("aren't in the file")), "{warnings:?}"); + assert!(linked_to(&doc, "Library main") >= 190, "{warnings:?}"); +} diff --git a/crates/format/src/fig/map.rs b/crates/format/src/fig/map.rs index 8e467151..7eb974da 100644 --- a/crates/format/src/fig/map.rs +++ b/crates/format/src/fig/map.rs @@ -42,6 +42,14 @@ pub(super) fn to_rest(msg: &Kv, name: &str, max_nodes: usize) -> (Value, Vec= m.max_nodes { + let capped: Vec = m.instance_mains.difference(&m.mapped_mains).map(|id| json!(id)).collect(); + if !capped.is_empty() { + out[crate::rest::FIG_CAPPED_MAINS_FIELD] = Value::Array(capped); + } + } if !m.styles.is_empty() { out["styles"] = Value::Object(std::mem::take(&mut m.styles)); } @@ -389,6 +397,10 @@ struct Mapper<'a> { library_scan_count: usize, warnings: Vec, count: usize, + /// The mains in the file that instances name, and the mains mapped as layers: the difference + /// is what the layer cap kept out. + instance_mains: BTreeSet, + mapped_mains: BTreeSet, /// Most layers produced or scanned, instance expansion included; the one place the cap comes /// from (see [`super::layer_budget`]). max_nodes: usize, @@ -466,6 +478,8 @@ impl<'a> Mapper<'a> { library_scan_count: 0, warnings: Vec::new(), count: 0, + instance_mains: BTreeSet::new(), + mapped_mains: BTreeSet::new(), max_nodes, networks: Vec::new(), network_ids: HashMap::new(), @@ -556,9 +570,8 @@ impl<'a> Mapper<'a> { Some(d) => self.children(d), None => self.nodes.iter().filter(|(_, n)| n.get("type").and_then(Kv::str) == Some("CANVAS")).map(|(id, _)| id.clone()).collect(), }; - let mut out = Vec::new(); let mut internal = Vec::new(); - let mut visible_pages = Vec::new(); + let mut visible = Vec::new(); for p in pages { let Some(&n) = self.nodes.get(&p) else { continue }; if n.get("type").and_then(Kv::str) != Some("CANVAS") { @@ -568,17 +581,20 @@ impl<'a> Mapper<'a> { // The hidden page that holds styles, variables and library copies. if n.get("internalOnly").and_then(Kv::bool) == Some(true) { internal.push((p, n)); - continue; + } else { + visible.push((p, n)); } - visible_pages.push(p.clone()); - out.push(self.page(&p, n)); } + // The library copies the pages use come first: in a file past the layer cap, the pages' + // last layers are skipped rather than the mains every instance of them needs (#981). + let visible_pages: Vec = visible.iter().map(|(p, _)| p.clone()).collect(); let required_mains = self.referenced_component_mains(&visible_pages); for (id, canvas) in internal { if let Some(context) = self.library_context(&id, canvas, &required_mains) { self.library_contexts.push(context); } } + let out: Vec = visible.into_iter().map(|(p, n)| self.page(&p, n)).collect(); if out.is_empty() { self.warn("the file has no pages"); } @@ -879,6 +895,9 @@ impl<'a> Mapper<'a> { self.warn("connector layers import as static frame/vector snapshots with labels when present; live endpoint bindings are not retained"); } self.count += 1; + if own == "SYMBOL" && inst.is_none() { + self.mapped_mains.insert(id.to_string()); + } let mut o = Obj::new(); o.insert("id".into(), json!(rest_id)); // Private provenance for the .fig importer; ordinary REST import ignores it. @@ -1994,6 +2013,9 @@ impl<'a> Mapper<'a> { self.warn("instances of components that aren't in the file show no layers"); return None; } + if !self.instance_mains.contains(&sym) { + self.instance_mains.insert(sym.clone()); + } if outer.is_some_and(|i| i.symbols.contains(&sym)) { return None; } diff --git a/crates/format/src/fig/mod.rs b/crates/format/src/fig/mod.rs index 4c103d08..c04043ae 100644 --- a/crates/format/src/fig/mod.rs +++ b/crates/format/src/fig/mod.rs @@ -23,6 +23,8 @@ mod export_tests; #[cfg(test)] mod interop_tests; pub mod kiwi; +#[cfg(test)] +mod layer_cap_tests; mod map; mod network; #[cfg(test)] @@ -64,8 +66,11 @@ const MAX_CHUNKS: usize = 16; /// compressed canvas, at least the REST import's cap and at most [`MAX_FIG_NODES`]. Instances /// expand into a layer per layer of their component, and chunks inflate up to [`MAX_CHUNK`], so the /// budget follows what the file takes, not what it unpacks to: a small file can't claim a large -/// count, however it's built; a large one gets room. -const PACKED_BYTES_PER_LAYER: usize = 32; +/// count, however it's built; a large one gets room. Design systems are mostly instances, which +/// pack tightly: a 7.8 MB canvas stopped at 244 248 layers at 32 bytes a layer (#981). At 8, files +/// under about 1.6 MB keep the floor and the ceiling comes at a 16 MB canvas (about 11 GB and 40 s +/// to import 2 million synthetic layers; memory grows with the count, about 5.4 KB a layer). +const PACKED_BYTES_PER_LAYER: usize = 8; const MAX_FIG_NODES: usize = 2_000_000; /// Most layers imported from a `.fig` file whose compressed canvas is `packed` bytes. @@ -108,7 +113,13 @@ pub fn import(bytes: &[u8], name: &str) -> Result<(Document, Vec), Forma drop(msg); let images = |r: &str| archive.image(r).or_else(|| embedded.get(r).cloned()); let (doc, more) = crate::rest::import_with_networks(&json, &images, networks, max_nodes)?; - warnings.extend(more.into_iter().filter_map(map::adapt_warning)); + // Both stages stop at the same cap and say so in the same words: one note (#981). + for w in more.into_iter().filter_map(map::adapt_warning) { + let base = w.rsplit_once(" (×").map_or(w.as_str(), |(b, _)| b); + if !warnings.iter().any(|seen| seen == base) { + warnings.push(w); + } + } Ok((doc, warnings)) } diff --git a/crates/format/src/rest/mod.rs b/crates/format/src/rest/mod.rs index 78279e91..40c091c6 100644 --- a/crates/format/src/rest/mod.rs +++ b/crates/format/src/rest/mod.rs @@ -65,6 +65,8 @@ const MAX_DEPTH: usize = 256; pub(crate) const FIG_SOURCE_FIELD: &str = "_figSource"; /// Hidden .fig library context; accepted only by the `.fig` import ([`import_with_networks`]). pub(crate) const FIG_LIBRARY_CONTEXTS_FIELD: &str = "_figLibraryContexts"; +/// Ids of mains in a `.fig` file that its layer cap kept out; read only by the `.fig` import. +pub(crate) const FIG_CAPPED_MAINS_FIELD: &str = "_figCappedMains"; /// Longest SVG path string read from one geometry entry. const MAX_PATH_BYTES: usize = 4 << 20; /// Most path elements in one vector. @@ -215,6 +217,14 @@ fn import_parts( cx.style_definitions(v); cx.networks = networks; cx.max_nodes = max_nodes; + // The `.fig` mapper already kept the layers, library copies included, within the cap; each + // library copy's wrapper frame is the one layer here it doesn't count (#981). + let contexts = if fig_sources { v.get(FIG_LIBRARY_CONTEXTS_FIELD).and_then(Value::as_array).map(Vec::as_slice).unwrap_or_default() } else { &[] }; + if fig_sources { + cx.capped_mains = + v.get(FIG_CAPPED_MAINS_FIELD).and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).map(str::to_owned).collect(); + cx.max_nodes = max_nodes.saturating_add(contexts.len()); + } for e in entries { let Some(root) = e.get("document") else { continue }; match text(root, "type") { @@ -236,11 +246,9 @@ fn import_parts( None => cx.warn("entries without a node type were skipped"), } } - if fig_sources { - for context in v.get(FIG_LIBRARY_CONTEXTS_FIELD).and_then(Value::as_array).into_iter().flatten() { - let store = cx.doc.ensure_library_store(); - cx.build(context, store, Space::default(), None, 0, false); - } + for context in contexts { + let store = cx.doc.ensure_library_store(); + cx.build(context, store, Space::default(), None, 0, false); } // Drop the empty page the new document started with once real pages exist. if let Some(p) = blank @@ -404,6 +412,8 @@ pub(crate) struct Ctx<'a> { networks: Vec>, /// Most layers imported: [`MAX_NODES`], or the `.fig` import's own cap. max_nodes: usize, + /// Mains the `.fig` file has but its layer cap kept out ([`FIG_CAPPED_MAINS_FIELD`]). + capped_mains: BTreeSet, } impl<'a> Ctx<'a> { @@ -432,6 +442,7 @@ impl<'a> Ctx<'a> { var_modes: BTreeMap::new(), networks: Vec::new(), max_nodes: MAX_NODES, + capped_mains: BTreeSet::new(), } } @@ -998,7 +1009,11 @@ impl<'a> Ctx<'a> { fn resolve_swap_defaults(&mut self) { for (owner, name, rest_id) in std::mem::take(&mut self.swap_defaults) { let Some(c) = self.component(&rest_id) else { - self.warn("instance swap properties whose default component isn't in the file were skipped"); + if self.capped_mains.contains(&rest_id) { + self.warn("instance swap properties whose default component was past the layer limit were skipped"); + } else { + self.warn("instance swap properties whose default component isn't in the file were skipped"); + } continue; }; let def = PropDef::InstanceSwap { default: c, preferred: Vec::new() }; @@ -1017,7 +1032,11 @@ impl<'a> Ctx<'a> { let frames: BTreeSet = self.instances.iter().map(|(i, _)| *i).collect(); let mut links: Vec<(NodeId, NodeId, &'a Value)> = Vec::new(); for (inst, j) in std::mem::take(&mut self.instances) { - match text(j, "componentId").and_then(|r| self.component(r)) { + let main = text(j, "componentId"); + match main.and_then(|r| self.component(r)) { + None if main.is_some_and(|r| self.capped_mains.contains(r)) => { + self.warn("instances whose component was past the layer limit were imported as frames"); + } None => self.warn("instances of components that aren't in the file were imported as frames"), Some(c) if self.doc.is_ancestor(c, inst) => self.warn("instances inside their own main component were imported as frames"), Some(c) => links.push((inst, c, j)), diff --git a/crates/ui-egui/locales/en.toml b/crates/ui-egui/locales/en.toml index ae566fc8..32045d4e 100644 --- a/crates/ui-egui/locales/en.toml +++ b/crates/ui-egui/locales/en.toml @@ -1595,10 +1595,12 @@ name = "English" "image filters are ambiguous because both filter containers are present; filter conversion was skipped" = "image filters are ambiguous because both filter containers are present; filter conversion was skipped" "image filters use FigCraft's approximate renderer" = "image filters use FigCraft's approximate renderer" "instance swap properties whose default component isn't in the file were skipped" = "instance swap properties whose default component isn't in the file were skipped" +"instance swap properties whose default component was past the layer limit were skipped" = "instance swap properties whose default component was past the layer limit were skipped" "instances inside their own main component were imported as frames" = "instances inside their own main component were imported as frames" "instances of components that aren't in the file show no layers" = "instances of components that aren't in the file show no layers" "instances of components that aren't in the file were imported as frames" = "instances of components that aren't in the file were imported as frames" "instances that would nest a component inside itself were imported as frames" = "instances that would nest a component inside itself were imported as frames" +"instances whose component was past the layer limit were imported as frames" = "instances whose component was past the layer limit were imported as frames" "instances without a main component were imported as empty frames" = "instances without a main component were imported as empty frames" "invalid internal .fig text path data was ignored" = "invalid internal .fig text path data was ignored" "invalid or oversized vector networks import as outlines" = "invalid or oversized vector networks import as outlines" diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index 596feda6..c6e77866 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -1596,10 +1596,12 @@ status = "draft — needs native review" "image filters are ambiguous because both filter containers are present; filter conversion was skipped" = "los filtros de imagen son ambiguos porque están los dos contenedores de filtros; se omitió la conversión de filtros" "image filters use FigCraft's approximate renderer" = "los filtros de imagen usan el renderizado aproximado de FigCraft" "instance swap properties whose default component isn't in the file were skipped" = "se omitieron las propiedades de intercambio de instancia cuyo componente predeterminado no está en el archivo" +"instance swap properties whose default component was past the layer limit were skipped" = "se omitieron las propiedades de intercambio de instancia cuyo componente predeterminado quedó fuera del límite de capas" "instances inside their own main component were imported as frames" = "las instancias dentro de su propio componente principal se importaron como marcos" "instances of components that aren't in the file show no layers" = "las instancias de componentes que no están en el archivo no muestran capas" "instances of components that aren't in the file were imported as frames" = "las instancias de componentes que no están en el archivo se importaron como marcos" "instances that would nest a component inside itself were imported as frames" = "las instancias que anidarían un componente dentro de sí mismo se importaron como marcos" +"instances whose component was past the layer limit were imported as frames" = "las instancias cuyo componente quedó fuera del límite de capas se importaron como marcos" "instances without a main component were imported as empty frames" = "las instancias sin componente principal se importaron como marcos vacíos" "invalid internal .fig text path data was ignored" = "se ignoraron datos internos no válidos de texto en trazado del .fig" "invalid or oversized vector networks import as outlines" = "las redes vectoriales no válidas o demasiado grandes se importan como contornos" From f0e75e183b86b31c16334aea39be2fd9e98768dd Mon Sep 17 00:00:00 2001 From: Ridzzz0Alam Date: Wed, 21 Oct 2026 21:06:59 +0200 Subject: [PATCH 2/2] M1: swap defaults past the layer cap say so (#981) Instance swap defaults that name an in-file main skipped by the layer cap were reported as "isn't in the file": only instance targets fed the capped-mains list. Defaults now feed it too. --- crates/format/src/fig/layer_cap_tests.rs | 21 +++++++++++++++++++++ crates/format/src/fig/map.rs | 21 +++++++++++++-------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/format/src/fig/layer_cap_tests.rs b/crates/format/src/fig/layer_cap_tests.rs index 6eeb15f4..2c562ba7 100644 --- a/crates/format/src/fig/layer_cap_tests.rs +++ b/crates/format/src/fig/layer_cap_tests.rs @@ -103,6 +103,27 @@ fn instances_of_a_main_the_cap_kept_out_say_so() { assert!(!doc.nodes().any(|n| n.name == "Local main")); } +#[test] +fn swap_defaults_the_cap_kept_out_say_so() { + // A kept main's instance swap default names a main with no instances, which comes after + // enough layers to be skipped at the cap. + let swap = o(vec![ + ("id", g(70)), + ("name", Kv::Str("Icon".into())), + ("type", Kv::Enum("INSTANCE_SWAP")), + ("initialValue", o(vec![("guidValue", g(900))])), + ]); + let mut nodes = vec![node(1, None, "DOCUMENT", "Document", vec![]), node(2, Some(1), "CANVAS", "Page", vec![])]; + nodes.push(node(50, Some(2), "SYMBOL", "Card", vec![("size", xy(10.0, 10.0)), ("componentPropDefs", Kv::Array(vec![swap]))])); + nodes.extend(main(60, 2, "Filler", 40)); + nodes.extend(main(900, 2, "Icon main", 10)); + let (doc, warnings) = import_at(&o(vec![("nodeChanges", Kv::Array(nodes))]), 30); + assert!(doc.nodes().any(|n| n.name == "Card")); + assert!(!doc.nodes().any(|n| n.name == "Icon main")); + assert!(warnings.iter().any(|w| w.starts_with("instance swap properties whose default component was past the layer limit")), "{warnings:?}"); + assert!(!warnings.iter().any(|w| w.contains("isn't in the file")), "{warnings:?}"); +} + #[test] fn a_whole_file_past_the_cap_keeps_its_library_links_and_says_so_once() { // A 1 001-layer library main used 210 times: about 210 000 layers in a file far too small to diff --git a/crates/format/src/fig/map.rs b/crates/format/src/fig/map.rs index 7eb974da..b18f8a2b 100644 --- a/crates/format/src/fig/map.rs +++ b/crates/format/src/fig/map.rs @@ -45,7 +45,7 @@ pub(super) fn to_rest(msg: &Kv, name: &str, max_nodes: usize) -> (Value, Vec= m.max_nodes { - let capped: Vec = m.instance_mains.difference(&m.mapped_mains).map(|id| json!(id)).collect(); + let capped: Vec = m.referenced_mains.difference(&m.mapped_mains).map(|id| json!(id)).collect(); if !capped.is_empty() { out[crate::rest::FIG_CAPPED_MAINS_FIELD] = Value::Array(capped); } @@ -397,9 +397,9 @@ struct Mapper<'a> { library_scan_count: usize, warnings: Vec, count: usize, - /// The mains in the file that instances name, and the mains mapped as layers: the difference - /// is what the layer cap kept out. - instance_mains: BTreeSet, + /// The mains in the file that instances or instance swap defaults name, and the mains mapped + /// as layers: the difference is what the layer cap kept out. + referenced_mains: BTreeSet, mapped_mains: BTreeSet, /// Most layers produced or scanned, instance expansion included; the one place the cap comes /// from (see [`super::layer_budget`]). @@ -478,7 +478,7 @@ impl<'a> Mapper<'a> { library_scan_count: 0, warnings: Vec::new(), count: 0, - instance_mains: BTreeSet::new(), + referenced_mains: BTreeSet::new(), mapped_mains: BTreeSet::new(), max_nodes, networks: Vec::new(), @@ -1869,7 +1869,12 @@ impl<'a> Mapper<'a> { "BOOL" => json!({"type": "BOOLEAN", "defaultValue": init.and_then(|i| i.get("boolValue")).and_then(Kv::bool).unwrap_or(true)}), "TEXT" => json!({"type": "TEXT", "defaultValue": init.and_then(prop_text).unwrap_or("")}), "INSTANCE_SWAP" => match init.and_then(|i| i.get("guidValue")).and_then(guid) { - Some(g) => json!({"type": "INSTANCE_SWAP", "defaultValue": g}), + Some(g) => { + if self.nodes.contains_key(&g) { + self.referenced_mains.insert(g.clone()); + } + json!({"type": "INSTANCE_SWAP", "defaultValue": g}) + } None => continue, }, "VARIANT" => json!({"type": "VARIANT"}), @@ -2013,8 +2018,8 @@ impl<'a> Mapper<'a> { self.warn("instances of components that aren't in the file show no layers"); return None; } - if !self.instance_mains.contains(&sym) { - self.instance_mains.insert(sym.clone()); + if !self.referenced_mains.contains(&sym) { + self.referenced_mains.insert(sym.clone()); } if outer.is_some_and(|i| i.symbols.contains(&sym)) { return None;