mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 18:36:44 -06:00
Merge #281: fix(format): import explicit variable modes referenced by asset key (@notdezzi)
This commit is contained in:
@@ -25,6 +25,8 @@ const MAX_DEPTH: usize = 256;
|
||||
const MAX_COMMANDS: usize = 200_000;
|
||||
/// Id of "no style".
|
||||
const NO_STYLE: &str = "4294967295:4294967295";
|
||||
/// Longest asset `key` or `version` kept for resolving references given as assets.
|
||||
const MAX_ASSET_PART: usize = 256;
|
||||
|
||||
/// The REST API file JSON for message `msg`, with the warnings about what was skipped.
|
||||
pub(super) fn to_rest(msg: &Kv, name: &str) -> (Value, Vec<String>) {
|
||||
@@ -249,6 +251,10 @@ struct Mapper<'a> {
|
||||
blobs: Vec<&'a [u8]>,
|
||||
/// Component property definitions: id → (REST name "Name#id", type).
|
||||
prop_defs: BTreeMap<String, (String, &'a str)>,
|
||||
/// File nodes by asset `key` and `version`: references can name a collection, variable or
|
||||
/// style as the asset instead of the guid, and the file carries the resources to resolve
|
||||
/// them (the collection node itself says the same `key`).
|
||||
assets: BTreeMap<String, BTreeMap<String, String>>,
|
||||
styles: Obj,
|
||||
/// Descriptions of components and component sets (REST `components` / `componentSets`).
|
||||
components: Obj,
|
||||
@@ -287,7 +293,37 @@ impl<'a> Mapper<'a> {
|
||||
}
|
||||
let blobs =
|
||||
msg.get("blobs").map(Kv::list).unwrap_or_default().iter().map(|b| b.get("bytes").and_then(Kv::bytes).unwrap_or_default()).collect();
|
||||
Self { nodes, kids, blobs, prop_defs, styles: Obj::new(), components: Obj::new(), sets: Obj::new(), warnings: Vec::new(), count: 0 }
|
||||
let mut assets: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
|
||||
for n in msg.get("nodeChanges").map(Kv::list).unwrap_or_default() {
|
||||
let (Some(key), Some(id)) = (n.get("key").and_then(Kv::str), n.get("guid").and_then(guid)) else { continue };
|
||||
if key.is_empty() || key.len() > MAX_ASSET_PART {
|
||||
continue;
|
||||
}
|
||||
let version = n.get("version").and_then(Kv::str).filter(|v| !v.is_empty() && v.len() <= MAX_ASSET_PART).unwrap_or("");
|
||||
assets.entry(key.to_string()).or_default().entry(version.to_string()).or_insert(id);
|
||||
}
|
||||
Self { nodes, kids, blobs, prop_defs, assets, styles: Obj::new(), components: Obj::new(), sets: Obj::new(), warnings: Vec::new(), count: 0 }
|
||||
}
|
||||
|
||||
/// The id of the resource a `VariableSetID` (or `VariableID`, `StyleId`) names: its `guid`,
|
||||
/// else the file node its asset `key` and `version` names (a collection from a library is
|
||||
/// referenced as the asset, and the file's own node for it says the same `key`). `None` when
|
||||
/// the reference has neither or the asset names nothing (or ambiguously) in the file.
|
||||
/// Original code for this importer: the field names are those of the schema each file embeds.
|
||||
fn resource_id(&self, v: &Kv) -> Option<String> {
|
||||
if let Some(g) = v.get("guid").and_then(guid).filter(|g| *g != NO_STYLE) {
|
||||
return Some(g);
|
||||
}
|
||||
let asset = v.get("assetRef")?;
|
||||
let key = asset.get("key").and_then(Kv::str)?;
|
||||
let versions = self.assets.get(key)?;
|
||||
let version = asset.get("version").and_then(Kv::str);
|
||||
match version {
|
||||
Some(v) => versions.get(v),
|
||||
// Without a version: an unversioned record, else the file's only one.
|
||||
None => versions.get("").or_else(|| (versions.len() == 1).then(|| versions.values().next()).flatten()),
|
||||
}
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn warn(&mut self, w: impl Into<String>) {
|
||||
@@ -1236,11 +1272,21 @@ impl<'a> Mapper<'a> {
|
||||
o.insert("style".into(), json!({"boundVariables": text_bound}));
|
||||
}
|
||||
let mut modes = Obj::new();
|
||||
let mut lost = false;
|
||||
for e in v.get("variableModeBySetMap").and_then(|m| m.get("entries")).map(Kv::list).unwrap_or_default() {
|
||||
if let (Some(set), Some(mode)) = (e.get("variableSetID").and_then(ref_guid), e.get("variableModeID").and_then(guid)) {
|
||||
modes.insert(set, json!(mode));
|
||||
let (Some(set), Some(mode)) = (e.get("variableSetID"), e.get("variableModeID").and_then(guid)) else { continue };
|
||||
match self.resource_id(set) {
|
||||
Some(id) => {
|
||||
modes.insert(id, json!(mode));
|
||||
}
|
||||
// A mode of a collection neither named by guid nor resolvable in the file.
|
||||
None if set.get("guid").is_some() || set.get("assetRef").is_some() => lost = true,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if lost {
|
||||
self.warn("variable modes of collections that aren't in the file were skipped");
|
||||
}
|
||||
if !modes.is_empty() {
|
||||
o.insert("explicitVariableModes".into(), Value::Object(modes));
|
||||
}
|
||||
@@ -1305,7 +1351,7 @@ impl<'a> Mapper<'a> {
|
||||
let mut variable_order = Vec::new();
|
||||
let mut variables = Obj::new();
|
||||
for (id, n) in of_type("VARIABLE", "sortPosition") {
|
||||
let Some(set) = n.get("variableSetID").and_then(ref_guid).filter(|s| collections.contains_key(s)) else { continue };
|
||||
let Some(set) = n.get("variableSetID").and_then(|s| self.resource_id(s)).filter(|s| collections.contains_key(s)) else { continue };
|
||||
let ty = n.get("variableResolvedType").and_then(Kv::str).unwrap_or("");
|
||||
if !matches!(ty, "BOOLEAN" | "FLOAT" | "STRING" | "COLOR") {
|
||||
continue;
|
||||
|
||||
@@ -39,12 +39,13 @@ struct Vector float x, float y
|
||||
struct Matrix float m00, float m01, float m02, float m10, float m11, float m12
|
||||
struct ColorStop Color color, float position
|
||||
message Image byte[] hash, string name
|
||||
message VariableID GUID guid
|
||||
message AssetRef string key, string version
|
||||
message VariableID GUID guid, AssetRef assetRef
|
||||
message PropRefValue GUID defId
|
||||
message AnyValue bool boolValue, float floatValue, VariableID alias, Color colorValue, PropRefValue propRefValue, TextData textDataValue, string textValue, Expression expressionValue
|
||||
message Expression ExpressionFunction expressionFunction, VariableData[] expressionArguments
|
||||
message VariableTarget VariableID id
|
||||
message VariableSetID GUID guid
|
||||
message VariableSetID GUID guid, AssetRef assetRef
|
||||
message ConditionalActions Action[] actions, VariableData condition
|
||||
message VectorData uint vectorNetworkBlob, Vector normalizedSize, NodeChange[] styleOverrideTable
|
||||
message ConnectorEndpoint GUID endpointNodeID, Vector position, Vector relativePosition
|
||||
@@ -71,7 +72,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
|
||||
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
|
||||
message Blob byte[] bytes
|
||||
message Message NodeChange[] nodeChanges, Blob[] blobs
|
||||
";
|
||||
@@ -666,6 +667,81 @@ fn instances_inside_their_own_main_stop() {
|
||||
assert!(doc.nodes().any(|n| n.name == "Self"));
|
||||
}
|
||||
|
||||
/// A collection from a library is named by its asset key, not a guid — also in the explicit mode
|
||||
/// map of the frames, components and instances set it on, and in the variables' collection link.
|
||||
#[test]
|
||||
fn modes_of_collections_named_by_asset_import() {
|
||||
let modes = arr(vec![
|
||||
o(vec![("id", g(90)), ("name", s("Dark")), ("sortPosition", s("!"))]),
|
||||
o(vec![("id", g(91)), ("name", s("Light")), ("sortPosition", s("\""))]),
|
||||
]);
|
||||
let color_value = |c| o(vec![("value", o(vec![("colorValue", c)])), ("dataType", e("COLOR"))]);
|
||||
let values = o(vec![(
|
||||
"entries",
|
||||
arr(vec![
|
||||
o(vec![("modeID", g(90)), ("variableData", color_value(rgb(0.0, 0.0, 0.0)))]),
|
||||
o(vec![("modeID", g(91)), ("variableData", color_value(rgb(1.0, 1.0, 1.0)))]),
|
||||
]),
|
||||
)]);
|
||||
let by_asset = |key: &'static str, version: Option<&'static str>| {
|
||||
let mut r = vec![("key", s(key))];
|
||||
r.extend(version.map(|v| ("version", s(v))));
|
||||
o(vec![("assetRef", o(r))])
|
||||
};
|
||||
let mode_map = |set: Kv<'static>| o(vec![("entries", arr(vec![o(vec![("variableSetID", set), ("variableModeID", g(91))])]))]);
|
||||
let nodes = vec![
|
||||
node(0, None, "DOCUMENT", "Document", vec![]),
|
||||
node(1, Some((0, "!")), "CANVAS", "Page A", vec![]),
|
||||
node(2, Some((0, "~")), "CANVAS", "Internal", vec![("internalOnly", Kv::Bool(true))]),
|
||||
// The collection itself says the asset key its references name.
|
||||
node(4, Some((2, "!")), "VARIABLE_SET", "Schema", vec![("variableSetModes", modes), ("key", s("schema-collection")), ("version", s("7"))]),
|
||||
node(
|
||||
5,
|
||||
Some((2, "\"")),
|
||||
"VARIABLE",
|
||||
"surface",
|
||||
vec![("variableSetID", by_asset("schema-collection", Some("7"))), ("variableResolvedType", e("COLOR")), ("variableDataValues", values)],
|
||||
),
|
||||
node(
|
||||
10,
|
||||
Some((1, "!")),
|
||||
"FRAME",
|
||||
"Light section",
|
||||
vec![("size", xy(400.0, 300.0)), ("variableModeBySetMap", mode_map(by_asset("schema-collection", Some("7"))))],
|
||||
),
|
||||
node(
|
||||
20,
|
||||
Some((1, "\"")),
|
||||
"SYMBOL",
|
||||
"Button",
|
||||
vec![("size", xy(100.0, 40.0)), ("variableModeBySetMap", mode_map(by_asset("schema-collection", None)))],
|
||||
),
|
||||
node(
|
||||
21,
|
||||
Some((1, "#")),
|
||||
"INSTANCE",
|
||||
"Button copy",
|
||||
vec![
|
||||
("size", xy(100.0, 40.0)),
|
||||
("symbolData", o(vec![("symbolID", g(20))])),
|
||||
("variableModeBySetMap", mode_map(by_asset("schema-collection", Some("7")))),
|
||||
],
|
||||
),
|
||||
// One names a collection that isn't in the file: its mode is skipped, with a warning.
|
||||
node(11, Some((1, "$")), "FRAME", "Gone", vec![("size", xy(400.0, 300.0)), ("variableModeBySetMap", mode_map(by_asset("gone", None)))]),
|
||||
];
|
||||
let (doc, warnings) = import(&canvas(&schema(), &o(vec![("nodeChanges", arr(nodes))])), "x").unwrap();
|
||||
assert_eq!(doc.variables.collections.len(), 1);
|
||||
assert_eq!(doc.variables.variables.len(), 1);
|
||||
assert_eq!(doc.variables.variables[0].collection, "1:4");
|
||||
for name in ["Light section", "Button", "Button copy"] {
|
||||
let n = named(&doc, name);
|
||||
assert_eq!(n.var_modes.get("1:4").map(String::as_str), Some("1:91"), "{name}");
|
||||
}
|
||||
assert!(named(&doc, "Gone").var_modes.is_empty());
|
||||
assert!(warnings.iter().any(|w| w.contains("variable modes of collections that aren't in the file")), "{warnings:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resized_instances_lay_their_layers_out_by_their_constraints() {
|
||||
let pin = |h: &'static str, v: &'static str| vec![("horizontalConstraint", e(h)), ("verticalConstraint", e(v))];
|
||||
|
||||
Reference in New Issue
Block a user