From 9303b73991ef793dd59ea743b6c109eb2598afda Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Sat, 7 Nov 2026 22:58:07 -0500 Subject: [PATCH] .fig import: the round-trip test asserts the scaled size; text on a path takes the instance's size (#2100 review) The scaled-instance round-trip test now asserts the scaled width and height instead of printing them. text_path read the layer's raw size, so a text on a path inside a scaled instance kept the main's path length; it now uses the scaled or fitted size. Co-Authored-By: Claude Sonnet 5.5 Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium --- crates/format/src/fig/assets_tests.rs | 5 ++- crates/format/src/fig/map/text.rs | 6 +-- crates/format/src/fig/text_path_tests.rs | 57 +++++++++++++++++++++++- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/crates/format/src/fig/assets_tests.rs b/crates/format/src/fig/assets_tests.rs index a69f5ccfa..e4b28e3cb 100644 --- a/crates/format/src/fig/assets_tests.rs +++ b/crates/format/src/fig/assets_tests.rs @@ -1048,6 +1048,9 @@ fn a_scaled_instance_keeps_its_radius_through_a_round_trip() { let (back, _) = import(&bytes, "x").unwrap(); let inst = back.nodes().find(|n| n.name == "Scaled").unwrap(); let b = inst.children.iter().map(|c| back.node(*c).unwrap()).find(|n| n.name == "Box").unwrap(); - eprintln!("scaled box back: {}x{} radii {:?}", b.width, b.height, b.corners.radii); + // The 40 px box at the instance's 1.5 scale. The exporter writes every scaled value of a layer + // in a derived row, so there is no main-layer value left unwritten here to round-trip; the + // scaling of values with no row stays covered by `scale_factor_tests`. + assert_eq!((b.width, b.height), (60.0, 60.0)); assert_eq!(b.corners.radii, [15.0; 4]); } diff --git a/crates/format/src/fig/map/text.rs b/crates/format/src/fig/map/text.rs index 86d9d24bb..d41b85185 100644 --- a/crates/format/src/fig/map/text.rs +++ b/crates/format/src/fig/map/text.rs @@ -186,11 +186,7 @@ impl<'a> Mapper<'a> { fail(self, "has invalid normalized size"); return; }; - let Some(size) = v.get("size") else { - fail(self, "has invalid dimensions"); - return; - }; - let (Some(nx), Some(ny), Some(w), Some(h)) = (num(norm, "x"), num(norm, "y"), num(size, "x"), num(size, "y")) else { + let (Some(nx), Some(ny), Some((w, h))) = (num(norm, "x"), num(norm, "y"), v.size()) else { fail(self, "has invalid dimensions"); return; }; diff --git a/crates/format/src/fig/text_path_tests.rs b/crates/format/src/fig/text_path_tests.rs index c94c6aa3d..8f7b46f43 100644 --- a/crates/format/src/fig/text_path_tests.rs +++ b/crates/format/src/fig/text_path_tests.rs @@ -3,7 +3,7 @@ use super::kiwi::Kv; const SCHEMA: &str = " -enum NodeType DOCUMENT CANVAS TEXT TEXT_PATH +enum NodeType DOCUMENT CANVAS TEXT TEXT_PATH SYMBOL INSTANCE enum StyleType NONE TEXT enum TextDecoration NONE UNDERLINE STRIKETHROUGH struct GUID uint sessionID, uint localID @@ -17,7 +17,8 @@ message TextData string characters, uint[] characterStyleIDs, TextOverride[] sty message TextPathStart float tValue, bool forward message VectorData uint vectorNetworkBlob, Vector normalizedSize message Blob byte[] bytes -message NodeChange GUID guid, ParentIndex parentIndex, NodeType type, string name, Vector size, Matrix transform, TextData textData, float fontSize, FontName fontName, TextPathStart textPathStart, VectorData vectorData, StyleType styleType, StyleId styleIdForText, TextDecoration textDecoration +message SymbolData GUID symbolID, float uniformScaleFactor +message NodeChange GUID guid, ParentIndex parentIndex, NodeType type, string name, Vector size, Matrix transform, TextData textData, float fontSize, FontName fontName, TextPathStart textPathStart, VectorData vectorData, StyleType styleType, StyleId styleIdForText, TextDecoration textDecoration, SymbolData symbolData message Message NodeChange[] nodeChanges, Blob[] blobs "; use figcraft_doc::{NodeKind, TextPath}; @@ -209,3 +210,55 @@ fn linked_text_style_decoration_warns_from_effective_mapped_style() { assert_eq!(json["document"]["children"][0]["children"][0]["style"]["textDecoration"], "UNDERLINE"); assert!(warnings.iter().any(|w| w == "text path underline or strikethrough decoration is not rendered"), "{warnings:?}"); } + +/// A component holding a text on a path (path 100 long) and a half-scale instance of it. +fn scaled_instance_message() -> Kv<'static> { + let identity = || o(vec![("m00", f(1.0)), ("m01", f(0.0)), ("m02", f(0.0)), ("m10", f(0.0)), ("m11", f(1.0)), ("m12", f(0.0))]); + let nodes = vec![ + node(0, None, "DOCUMENT", vec![]), + node(1, Some(0), "CANVAS", vec![]), + node(5, Some(1), "SYMBOL", vec![("name", s("Main")), ("size", point(100.0, 100.0)), ("transform", identity())]), + node( + 6, + Some(5), + "TEXT_PATH", + vec![ + ("name", s("arc")), + ("size", point(100.0, 0.0)), + ("transform", identity()), + ("textData", o(vec![("characters", s("alpha"))])), + ("fontSize", f(24.0)), + ("fontName", o(vec![("family", s("Inter")), ("style", s("Regular"))])), + ("textPathStart", o(vec![("tValue", f(0.5)), ("forward", Kv::Bool(true))])), + ("vectorData", o(vec![("vectorNetworkBlob", u(0)), ("normalizedSize", point(100.0, 0.0))])), + ], + ), + node( + 9, + Some(1), + "INSTANCE", + vec![ + ("name", s("Half")), + ("size", point(50.0, 50.0)), + ("transform", identity()), + ("symbolData", o(vec![("symbolID", guid(5)), ("uniformScaleFactor", f(0.5))])), + ], + ), + ]; + o(vec![("nodeChanges", a(nodes)), ("blobs", a(vec![o(vec![("bytes", Kv::Bytes(blob()))])]))]) +} + +#[test] +fn a_text_on_a_path_in_a_scaled_instance_has_its_path_scaled() { + let (json, warnings, _) = super::map::to_rest(&scaled_instance_message(), "Scaled path", crate::rest::MAX_NODES); + assert!(!warnings.iter().any(|w| w.contains("text path")), "{warnings:?}"); + let (doc, _) = crate::rest::import_fig(&json, &|_| None).unwrap(); + let path_of = |parent: &str| { + let parent = doc.nodes().find(|n| n.name == parent).unwrap(); + let node = doc.node(parent.children[0]).unwrap(); + let NodeKind::Text(text) = &node.kind else { panic!("expected text") }; + text.path.as_deref().unwrap().d.clone() + }; + assert_eq!(path_of("Main"), "M0,0 L100,0"); + assert_eq!(path_of("Half"), "M0,0 L50,0"); +}