From c411152b877508613bee62f332c91f77c0a480b0 Mon Sep 17 00:00:00 2001 From: mortezanaeemi-ops Date: Sun, 1 Nov 2026 15:34:41 +0330 Subject: [PATCH] Add files via upload --- crates/format/src/fig/map.rs | 45 +++++++++++++++++++++------- crates/format/src/fig/tests.rs | 55 +++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/crates/format/src/fig/map.rs b/crates/format/src/fig/map.rs index e24a93b2d..97e758e7f 100644 --- a/crates/format/src/fig/map.rs +++ b/crates/format/src/fig/map.rs @@ -372,22 +372,23 @@ impl<'a> Mapper<'a> { /// while the mode ids stay the same across them, so every copy maps to the newest one (the /// one [`Self::variables`] keeps, with the modes of the others added). Without this, a page set /// to a mode an older copy didn't have yet lost its mode, and the layers fell back to the - /// default mode of the newest copy. + /// default mode of the newest copy. A reference by asset that names no copy by itself (no + /// version, or a version the file doesn't hold, while it holds several) still names the asset: + /// its newest live copy. pub(super) fn collection_id(&self, v: &Kv) -> Option { - let id = self.resource_id(v)?; - Some(self.canonical_collection(&id).unwrap_or(id)) + let by_asset = || v.get("assetRef")?.get("key").and_then(Kv::str).and_then(VarCanon::newest_set); + match self.resource_id(v) { + Some(id) if self.nodes.contains_key(&id) => Some(self.canonical_collection(&id).unwrap_or(id)), + // The asset index can name a node a later change removed: the asset's live copy instead. + Some(id) => by_asset().or(Some(id)), + None => by_asset(), + } } /// The newest live copy of collection node `id`'s library asset, when it has a key. pub(super) fn canonical_collection(&self, id: &str) -> Option { let key = self.nodes.get(id)?.get("key").and_then(Kv::str).filter(|k| !k.is_empty())?; - let live_set = |n: &Kv| n.get("type").and_then(Kv::str) == Some("VARIABLE_SET") && n.get("isSoftDeleted").and_then(Kv::bool) != Some(true); - self.assets - .get(key)? - .iter() - .filter(|(_, nid)| self.nodes.get(nid.as_str()).is_some_and(|n| live_set(n))) - .max_by_key(|(ver, _)| version_rank(ver)) - .map(|(_, nid)| nid.clone()) + VarCanon::newest_set(key) } /// A note about what this file's mapping left out or changed. Counted, as the REST importer @@ -429,6 +430,10 @@ pub(super) struct VarCanon { by_id: HashMap, /// Every live variable node of the file, with a key or not. local: BTreeSet, + /// The newest live copy of each library collection, by asset key. Read from the file's live + /// nodes (the last change of each guid, REMOVED ones gone), not from the asset index, which + /// keeps the first node it met for a key and version even when a later change removed it. + sets_by_key: HashMap, } thread_local! { @@ -449,8 +454,20 @@ impl VarCanon { let mut newest: HashMap = HashMap::new(); let mut copies: Vec<(String, String)> = Vec::new(); let mut local = BTreeSet::new(); + let mut sets: HashMap = HashMap::new(); for (id, n) in &m.nodes { - if n.get("type").and_then(Kv::str) != Some("VARIABLE") || n.get("isSoftDeleted").and_then(Kv::bool) == Some(true) { + let ty = n.get("type").and_then(Kv::str); + if !matches!(ty, Some("VARIABLE" | "VARIABLE_SET")) || n.get("isSoftDeleted").and_then(Kv::bool) == Some(true) { + continue; + } + if ty == Some("VARIABLE_SET") { + if let Some(key) = n.get("key").and_then(Kv::str).filter(|k| !k.is_empty()) { + let rank = version_rank(n.get("version").and_then(Kv::str).unwrap_or("")); + let e = sets.entry(key.to_string()).or_insert_with(|| (rank, id.clone())); + if rank > e.0 || (rank == e.0 && *id > e.1) { + *e = (rank, id.clone()); + } + } continue; } local.insert(id.clone()); @@ -470,6 +487,7 @@ impl VarCanon { } t.by_key = newest.into_iter().map(|(k, (_, id))| (k, id)).collect(); t.local = local; + t.sets_by_key = sets.into_iter().map(|(k, (_, id))| (k, id)).collect(); VAR_CANON.with(|c| *c.borrow_mut() = Some(t)); VarCanonGuard } @@ -491,6 +509,11 @@ impl VarCanon { }) } + /// The newest live copy of the library collection with asset key `key`. + pub(super) fn newest_set(key: &str) -> Option { + VAR_CANON.with(|c| c.borrow().as_ref().and_then(|t| t.sets_by_key.get(key).cloned())) + } + /// Whether variable node `id` is an older copy another one replaces. pub(super) fn superseded(id: &str) -> bool { VAR_CANON.with(|c| c.borrow().as_ref().and_then(|t| t.by_id.get(id)).is_some_and(|canon| canon != id)) diff --git a/crates/format/src/fig/tests.rs b/crates/format/src/fig/tests.rs index d0cea1932..d365ff9f7 100644 --- a/crates/format/src/fig/tests.rs +++ b/crates/format/src/fig/tests.rs @@ -28,6 +28,7 @@ enum StrokeCap NONE ROUND SQUARE ARROW_LINES ARROW_EQUILATERAL enum StrokeJoin MITER BEVEL ROUND enum VectorMirror NONE ANGLE ANGLE_AND_LENGTH enum ShapeWithTextType SQUARE ELLIPSE +enum NodePhase CREATED REMOVED enum ConnectorLineStyle ELBOWED STRAIGHT CURVED enum ExpressionFunction ADDITION SUBTRACTION RESOLVE_VARIANT MULTIPLY DIVIDE EQUALS enum NavigationType NAVIGATE OVERLAY @@ -74,7 +75,7 @@ message Event InteractionType interactionType message Action GUID transitionNodeID, TransitionType transitionType, float transitionDuration, EasingType easingType, ConnectionType connectionType, NavigationType navigationType, VariableTarget targetVariable, VariableData targetVariableData, VariableSetID targetVariableSetID, GUID targetVariableModeID, ConditionalActions[] conditionalActions message Interaction Event event, Action[] actions message StartingPoint string name, string position -message NodeChange GUID guid, ParentIndex parentIndex, NodeType type, string name, bool visible, float opacity, Vector size, Matrix transform, Paint[] fillPaints, Paint[] strokePaints, float strokeWeight, float cornerRadius, bool resizeToFit, bool internalOnly, float fontSize, FontName fontName, TextData textData, uint styleID, Path[] fillGeometry, Path[] strokeGeometry, StyleType styleType, StyleId styleIdForFill, SymbolData symbolData, PropDef[] componentPropDefs, PropAssignment[] componentPropAssignments, VariableDataMap parameterConsumptionMap, SetMode[] variableSetModes, VariableID variableSetID, ResolvedType variableResolvedType, Values variableDataValues, ModeMap variableModeBySetMap, Interaction[] prototypeInteractions, StartingPoint prototypeStartingPoint, GUIDPath guidPath, StackMode stackMode, float stackSpacing, float stackHorizontalPadding, float stackVerticalPadding, uint count, float starInnerScale, VectorData vectorData, StrokeCap strokeCap, StrokeJoin strokeJoin, VectorMirror handleMirroring, ShapeWithTextType shapeWithTextType, ConnectorEndpoint connectorStart, ConnectorEndpoint connectorEnd, StrokeCap connectorStartCap, StrokeCap connectorEndCap, ConnectorLineStyle connectorLineStyle, ConstraintType horizontalConstraint, ConstraintType verticalConstraint, NodeChange[] derivedSymbolData, string key, string version, LeadingTrim leadingTrim, float[] dashPattern +message NodeChange GUID guid, ParentIndex parentIndex, NodeType type, string name, bool visible, float opacity, Vector size, Matrix transform, Paint[] fillPaints, Paint[] strokePaints, float strokeWeight, float cornerRadius, bool resizeToFit, bool internalOnly, float fontSize, FontName fontName, TextData textData, uint styleID, Path[] fillGeometry, Path[] strokeGeometry, StyleType styleType, StyleId styleIdForFill, SymbolData symbolData, PropDef[] componentPropDefs, PropAssignment[] componentPropAssignments, VariableDataMap parameterConsumptionMap, SetMode[] variableSetModes, VariableID variableSetID, ResolvedType variableResolvedType, Values variableDataValues, ModeMap variableModeBySetMap, Interaction[] prototypeInteractions, StartingPoint prototypeStartingPoint, GUIDPath guidPath, StackMode stackMode, float stackSpacing, float stackHorizontalPadding, float stackVerticalPadding, uint count, float starInnerScale, VectorData vectorData, StrokeCap strokeCap, StrokeJoin strokeJoin, VectorMirror handleMirroring, ShapeWithTextType shapeWithTextType, ConnectorEndpoint connectorStart, ConnectorEndpoint connectorEnd, StrokeCap connectorStartCap, StrokeCap connectorEndCap, ConnectorLineStyle connectorLineStyle, ConstraintType horizontalConstraint, ConstraintType verticalConstraint, NodeChange[] derivedSymbolData, string key, string version, LeadingTrim leadingTrim, float[] dashPattern, NodePhase phase message Blob byte[] bytes message Message NodeChange[] nodeChanges, Blob[] blobs "; @@ -1589,6 +1590,23 @@ fn library_copies_of_several_versions_become_the_newest() { ), ], ), + // A page section set by a versionless reference, while the file holds several copies. + node( + 12, + Some((1, "#")), + "FRAME", + "Versionless", + vec![ + ("size", xy(100.0, 40.0)), + ( + "variableModeBySetMap", + o(vec![( + "entries", + arr(vec![o(vec![("variableSetID", o(vec![("assetRef", o(vec![("key", s("products"))]))])), ("variableModeID", g(92))])]), + )]), + ), + ], + ), // The button names the older copy of the variable. node( 10, @@ -1618,7 +1636,42 @@ fn library_copies_of_several_versions_become_the_newest() { assert_eq!(page.var_modes.get(&products.id).map(String::as_str), Some("1:92")); let button = named(&doc, "Button"); assert_eq!(button.bound.values().next(), Some(&var.id), "{:?}", button.bound); + assert_eq!(named(&doc, "Versionless").var_modes.get(&products.id).map(String::as_str), Some("1:92")); let pasted = named(&doc, "Pasted button"); assert_eq!(pasted.bound.values().next(), Some(&var.id), "{:?}", pasted.bound); assert!(!warnings.iter().any(|w| w.contains("variable modes of collections")), "{warnings:?}"); } + +/// A copy of a library collection that a later change removed, followed by a live node for the +/// same asset key and version: the live one is the collection, not the removed one the asset +/// index met first. +#[test] +fn removed_library_collection_copies_give_way_to_live_ones() { + let mode = |id, name: &'static str, pos: &'static str| o(vec![("id", g(id)), ("name", s(name)), ("sortPosition", s(pos))]); + let by_asset = |version: &'static str| o(vec![("assetRef", o(vec![("key", s("products")), ("version", s(version))]))]); + let set = |id, version: &'static str, modes: Vec>| { + node(id, Some((2, "!")), "VARIABLE_SET", "Products", vec![("variableSetModes", arr(modes)), ("key", s("products")), ("version", s(version))]) + }; + let removed = o(vec![("guid", g(8)), ("phase", e("REMOVED"))]); + let nodes = vec![ + node(0, None, "DOCUMENT", "Document", vec![]), + node( + 1, + Some((0, "!")), + "CANVAS", + "Page", + vec![("variableModeBySetMap", o(vec![("entries", arr(vec![o(vec![("variableSetID", by_asset("30:1")), ("variableModeID", g(92))])]))]))], + ), + node(2, Some((0, "~")), "CANVAS", "Internal", vec![("internalOnly", Kv::Bool(true))]), + set(4, "10:1", vec![mode(90, "Blue", "!")]), + set(8, "30:1", vec![mode(90, "Blue", "!"), mode(92, "Purple", "\"")]), + removed, + set(9, "30:1", vec![mode(90, "Blue", "!"), mode(92, "Purple", "\"")]), + ]; + let (doc, warnings) = import(&canvas(&schema(), &o(vec![("nodeChanges", arr(nodes))])), "x").unwrap(); + assert_eq!(doc.variables.collections.len(), 1, "{:?}", doc.variables.collections); + let products = &doc.variables.collections[0]; + assert_eq!(products.id, "1:9"); + let page = doc.nodes().find(|n| n.name == "Page").unwrap(); + assert_eq!(page.var_modes.get("1:9").map(String::as_str), Some("1:92"), "{warnings:?}"); +}