From ce366ea3258c9b1e40999ff75be4c6eea8ac1c61 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Wed, 14 Oct 2026 12:14:45 +0200 Subject: [PATCH] M6: .fig import: a resized nested instance keeps the non-geometric derived data; resize check with a tolerance (#266) --- crates/format/src/fig/map.rs | 47 +++++++++++++++++++++++++--------- crates/format/src/fig/tests.rs | 12 ++++++++- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/crates/format/src/fig/map.rs b/crates/format/src/fig/map.rs index 338bbcd2..5a08dd40 100644 --- a/crates/format/src/fig/map.rs +++ b/crates/format/src/fig/map.rs @@ -218,8 +218,18 @@ impl Fit { } } -/// Overrides and derived layout by id path; each entry says whether it's derived layout. -type Overrides<'a> = BTreeMap, Vec<(bool, &'a Kv<'a>)>>; +/// Overrides and derived layout by id path. +type Overrides<'a> = BTreeMap, Vec<(Entry, &'a Kv<'a>)>>; + +/// Where an override entry comes from. +#[derive(Clone, Copy, PartialEq)] +enum Entry { + Override, + Derived, + /// Derived layout of a nested instance for a size it no longer has: its size and place no + /// longer hold, the rest (resolved colours, text) still does. + Stale, +} /// Where an instance's expanded layers are: the overrides and derived layout of the outermost /// instance (by id path), the path of the instance being expanded, and the property values. @@ -377,15 +387,19 @@ impl<'a> Mapper<'a> { // A layer of a resized instance the file gives no derived layout for follows its // constraints from the main's geometry, then takes the size and place overridden on it // (auto layout places the others, as in the editor). - let derived = entries.iter().any(|(d, l)| *d && (l.get("size").is_some() || l.get("transform").is_some())); + let derived = entries.iter().any(|(d, l)| *d == Entry::Derived && (l.get("size").is_some() || l.get("transform").is_some())); let stacked = parent.and_then(|p| p.str("stackMode")).is_some_and(|m| matches!(m, "HORIZONTAL" | "VERTICAL" | "GRID")) && view.str("stackPositioning") != Some("ABSOLUTE"); let main = View::of(base); + let set = |k: &str| entries.iter().rev().filter(|e| e.0 != Entry::Stale).find_map(|(_, l)| l.get(k)); if let (Some(f), false, false, Some((w, h))) = (fit, derived, stacked, main.size()) { let t = main.transform().unwrap_or_default(); let (fw, ft) = f.apply(w, h, t, view.str("horizontalConstraint"), view.str("verticalConstraint")); - let set = |k: &str| entries.iter().rev().find_map(|(_, l)| l.get(k)); view.fitted = Some((set("size").and_then(size).unwrap_or(fw), set("transform").and_then(matrix).unwrap_or(ft))); + } else if entries.iter().any(|(d, l)| *d == Entry::Stale && (l.get("size").is_some() || l.get("transform").is_some())) + && let Some(s) = set("size").and_then(size).or_else(|| main.size()) + { + view.fitted = Some((s, set("transform").and_then(matrix).or_else(|| main.transform()).unwrap_or_default())); } let rest_id = match (inst, &path) { (Some(i), Some(p)) => format!("I{};{}", i.root, p.join(";")), @@ -1011,14 +1025,16 @@ impl<'a> Mapper<'a> { // Overrides by id path below this instance (the instance itself is addressed by its main's id). let own_path: Vec = outer.map(|i| [i.path.as_slice(), &[id.to_string()]].concat()).unwrap_or_default(); let mut overrides: Overrides<'a> = outer.map(|i| (*i.overrides).clone()).unwrap_or_default(); - // A nested instance's derived layout is for the size it has in its main: resized here, it - // no longer holds and its layers follow their constraints instead. - let resized = outer.is_some() && self.nodes.get(id).and_then(|b| View::of(b).size()) != v.size(); + // A nested instance's derived layout is for the size it has in its main: resized here, the + // sizes and places in it no longer hold and its layers follow their constraints instead. + let differs = |a: f64, b: f64| (a - b).abs() > 1e-6 * a.abs().max(b.abs()).max(1.0); + let resized = outer.is_some() + && match (self.nodes.get(id).and_then(|b| View::of(b).size()), v.size()) { + (Some(a), Some(b)) => differs(a.0, b.0) || differs(a.1, b.1), + (a, b) => a.is_some() != b.is_some(), + }; let lists = [sd.and_then(|s| s.get("symbolOverrides")).map(Kv::list).unwrap_or_default(), v.list("derivedSymbolData")]; for (layer, list) in lists.into_iter().enumerate() { - if layer == 1 && resized { - continue; - } for ov in list { let rel: Vec = ov.get("guidPath").and_then(|g| g.get("guids")).map(Kv::list).unwrap_or_default().iter().filter_map(guid).collect(); @@ -1028,9 +1044,16 @@ impl<'a> Mapper<'a> { let e = overrides.entry([own_path.as_slice(), &rel].concat()).or_default(); // The outer instance's overrides win over this one's own; derived layout comes last. if layer == 0 && outer.is_some() { - e.insert(0, (false, ov)); + e.insert(0, (Entry::Override, ov)); } else { - e.push((layer == 1, ov)); + e.push(( + match (layer, resized) { + (0, _) => Entry::Override, + (_, false) => Entry::Derived, + (_, true) => Entry::Stale, + }, + ov, + )); } } } diff --git a/crates/format/src/fig/tests.rs b/crates/format/src/fig/tests.rs index 6d382c15..af8ac5a0 100644 --- a/crates/format/src/fig/tests.rs +++ b/crates/format/src/fig/tests.rs @@ -760,7 +760,14 @@ fn nested_instances_resized_again_drop_the_layout_derived_for_their_old_size() { ("size", xy(150.0, 10.0)), ("transform", at(0.0, 0.0)), ("symbolData", o(vec![("symbolID", g(20))])), - ("derivedSymbolData", arr(vec![o(vec![("guidPath", o(vec![("guids", arr(vec![g(21)]))])), ("size", xy(150.0, 10.0))])])), + ( + "derivedSymbolData", + arr(vec![o(vec![ + ("guidPath", o(vec![("guids", arr(vec![g(21)]))])), + ("size", xy(150.0, 10.0)), + ("fillPaints", arr(vec![solid(1.0, 0.0, 0.0)])), + ])]), + ), ], stretch(), ] @@ -782,6 +789,9 @@ fn nested_instances_resized_again_drop_the_layout_derived_for_their_old_size() { // In its main the derived layout holds; stretched to 250 it no longer does. assert_eq!(width("Slider"), (150.0, 150.0)); assert_eq!(width("Wide slider"), (250.0, 250.0)); + // What else the derived data gives (the colour a variable resolves to, say) still holds. + let fill = doc.node(doc.node(named(&doc, "Wide slider").children[0]).unwrap().children[0]).unwrap(); + assert!(matches!(fill.fills.first().map(|p| &p.kind), Some(figcraft_doc::color::PaintKind::Solid { color }) if color.r == 1.0 && color.g == 0.0)); } #[path = "tail_tests.rs"]