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"); +}