mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 19:36:46 -06:00
Merge #279: M6: .fig import lays out the layers of resized instances by their constraints (@marcoarnulfo)
This commit is contained in:
+153
-22
@@ -86,6 +86,10 @@ fn vec2(v: &Kv) -> Option<Value> {
|
||||
Some(json!({"x": num(v, "x")?, "y": num(v, "y")?}))
|
||||
}
|
||||
|
||||
fn size(v: &Kv) -> Option<(f64, f64)> {
|
||||
Some((num(v, "x")?, num(v, "y")?))
|
||||
}
|
||||
|
||||
fn matrix(m: &Kv) -> Option<Affine> {
|
||||
let c = |k: &str| num(m, k);
|
||||
let a = Affine::new([c("m00")?, c("m10")?, c("m01")?, c("m11")?, c("m02")?, c("m12")?]);
|
||||
@@ -113,11 +117,21 @@ fn copy_str(o: &mut Obj, n: &View, from: &str, to: &str) {
|
||||
#[derive(Clone)]
|
||||
struct View<'a> {
|
||||
layers: Vec<&'a Kv<'a>>,
|
||||
/// Size and transform worked out from the constraints, over the layers' own.
|
||||
fitted: Option<((f64, f64), Affine)>,
|
||||
}
|
||||
|
||||
impl<'a> View<'a> {
|
||||
fn of(n: &'a Kv<'a>) -> Self {
|
||||
Self { layers: vec![n] }
|
||||
Self { layers: vec![n], fitted: None }
|
||||
}
|
||||
|
||||
fn size(&self) -> Option<(f64, f64)> {
|
||||
self.fitted.map(|f| f.0).or_else(|| self.get("size").and_then(size))
|
||||
}
|
||||
|
||||
fn transform(&self) -> Option<Affine> {
|
||||
self.fitted.map(|f| f.1).or_else(|| self.get("transform").and_then(matrix))
|
||||
}
|
||||
|
||||
fn with(&self, top: impl IntoIterator<Item = &'a Kv<'a>>) -> Self {
|
||||
@@ -147,13 +161,83 @@ impl<'a> View<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// How the layers of a resized container follow it when the file gives no derived layout for
|
||||
/// them: a frame's by their constraints, from its main's size to its own; a group's scale with it.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Fit {
|
||||
Frame { from: (f64, f64), to: (f64, f64) },
|
||||
Scale(f64, f64),
|
||||
}
|
||||
|
||||
impl Fit {
|
||||
fn frame(from: Option<(f64, f64)>, to: Option<(f64, f64)>) -> Option<Self> {
|
||||
let (from, to) = (from?, to?);
|
||||
((from.0 - to.0).abs() > 1e-9 || (from.1 - to.1).abs() > 1e-9).then_some(Fit::Frame { from, to })
|
||||
}
|
||||
|
||||
fn scale(from: Option<(f64, f64)>, to: Option<(f64, f64)>) -> Option<Self> {
|
||||
let (from, to) = (from?, to?);
|
||||
let k = |a: f64, b: f64| if a > 1e-9 { b / a } else { 1.0 };
|
||||
let (kx, ky) = (k(from.0, to.0), k(from.1, to.1));
|
||||
((kx - 1.0).abs() > 1e-9 || (ky - 1.0).abs() > 1e-9).then_some(Fit::Scale(kx, ky))
|
||||
}
|
||||
|
||||
/// The size and transform (in the parent's coordinates) of a `w` × `h` layer at `t` with
|
||||
/// constraints `h_con`, `v_con`. Like the editor's constraint solver, a layer keeps its
|
||||
/// rotation and flip and stretches along its own axes.
|
||||
fn apply(self, w: f64, h: f64, t: Affine, h_con: Option<&str>, v_con: Option<&str>) -> ((f64, f64), Affine) {
|
||||
let [a, b, c, d, _, _] = t.as_coeffs();
|
||||
let lin = Affine::new([a, b, c, d, 0.0, 0.0]);
|
||||
let bb = figcraft_doc::geom::transform_rect_bbox(t, figcraft_doc::geom::Rect::new(0.0, 0.0, w, h));
|
||||
// Where the new box goes: its top-left (constraints) or its centre (a group's scale).
|
||||
let (kx, ky, x0, y0, centred) = match self {
|
||||
Fit::Frame { from, to } => {
|
||||
let axis = |con: Option<&str>, lo: f64, hi: f64, old: f64, new: f64| match con {
|
||||
Some("MAX" | "FIXED_MAX") => (lo + new - old, hi + new - old),
|
||||
Some("STRETCH") => (lo, hi + new - old),
|
||||
Some("CENTER") => (lo + (new - old) / 2.0, hi + (new - old) / 2.0),
|
||||
Some("SCALE") if old > 1e-9 => (lo * new / old, hi * new / old),
|
||||
_ => (lo, hi),
|
||||
};
|
||||
let (x0, x1) = axis(h_con, bb.x0, bb.x1, from.0, to.0);
|
||||
let (y0, y1) = axis(v_con, bb.y0, bb.y1, from.1, to.1);
|
||||
let k = |new: f64, old: f64| if old > 1e-9 { (new / old).max(0.0) } else { 1.0 };
|
||||
(k(x1 - x0, bb.width()), k(y1 - y0, bb.height()), x0, y0, false)
|
||||
}
|
||||
Fit::Scale(kx, ky) => (kx, ky, bb.center().x * kx, bb.center().y * ky, true),
|
||||
};
|
||||
let along = |x: f64, y: f64, fallback: f64| {
|
||||
let len = x.hypot(y);
|
||||
let k = (kx * x).hypot(ky * y) / len;
|
||||
if len > 1e-12 && k.is_finite() { k } else { fallback }
|
||||
};
|
||||
let size = (w * along(a, b, kx), h * along(c, d, ky));
|
||||
let nb = figcraft_doc::geom::transform_rect_bbox(lin, figcraft_doc::geom::Rect::new(0.0, 0.0, size.0, size.1));
|
||||
let at = if centred { nb.center() } else { figcraft_doc::geom::Point::new(nb.x0, nb.y0) };
|
||||
(size, Affine::translate((x0 - at.x, y0 - at.y)) * lin)
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides and derived layout by id path.
|
||||
type Overrides<'a> = BTreeMap<Vec<String>, 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.
|
||||
#[derive(Clone)]
|
||||
struct Inst<'a> {
|
||||
root: String,
|
||||
path: Vec<String>,
|
||||
overrides: Rc<BTreeMap<Vec<String>, Vec<&'a Kv<'a>>>>,
|
||||
overrides: Rc<Overrides<'a>>,
|
||||
props: Rc<BTreeMap<String, &'a Kv<'a>>>,
|
||||
/// Mains being expanded (an instance inside its own main stops there).
|
||||
symbols: Vec<String>,
|
||||
@@ -263,7 +347,7 @@ impl<'a> Mapper<'a> {
|
||||
let name = sp.get("name").and_then(Kv::str).filter(|s| !s.is_empty()).unwrap_or("Flow");
|
||||
flows.push((sp.get("position").and_then(Kv::str).unwrap_or(""), json!({"nodeId": c, "name": name})));
|
||||
}
|
||||
kids.extend(self.layer(&c, None, Affine::IDENTITY, None, 0));
|
||||
kids.extend(self.layer(&c, None, Affine::IDENTITY, None, None, 0));
|
||||
}
|
||||
flows.sort_by(|a, b| a.0.cmp(b.0));
|
||||
if !flows.is_empty() {
|
||||
@@ -276,7 +360,15 @@ impl<'a> Mapper<'a> {
|
||||
/// Layer `id` (inside instance `inst` when expanding one) and its subtree. `space`: from the
|
||||
/// parent's coordinates to the enclosing frame's (groups and booleans place their children in
|
||||
/// the frame's space, like the REST JSON). `parent`: the parent's view (grid tracks).
|
||||
fn layer(&mut self, id: &str, inst: Option<&Inst<'a>>, space: Affine, parent: Option<&View<'a>>, depth: usize) -> Option<Value> {
|
||||
fn layer(
|
||||
&mut self,
|
||||
id: &str,
|
||||
inst: Option<&Inst<'a>>,
|
||||
space: Affine,
|
||||
parent: Option<&View<'a>>,
|
||||
fit: Option<Fit>,
|
||||
depth: usize,
|
||||
) -> Option<Value> {
|
||||
if depth > MAX_DEPTH {
|
||||
self.warn(format!("layers nested deeper than {MAX_DEPTH} levels were skipped"));
|
||||
return None;
|
||||
@@ -287,10 +379,28 @@ impl<'a> Mapper<'a> {
|
||||
}
|
||||
let base = *self.nodes.get(id)?;
|
||||
let path = inst.map(|i| [i.path.as_slice(), &[id.to_string()]].concat());
|
||||
let view = match (inst, &path) {
|
||||
(Some(i), Some(p)) => View::of(base).with(i.overrides.get(p).into_iter().flatten().copied()),
|
||||
_ => View::of(base),
|
||||
let entries = match (inst, &path) {
|
||||
(Some(i), Some(p)) => i.overrides.get(p).map(Vec::as_slice).unwrap_or_default(),
|
||||
_ => &[],
|
||||
};
|
||||
let mut view = View::of(base).with(entries.iter().map(|e| e.1));
|
||||
// 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 == 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"));
|
||||
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(";")),
|
||||
_ => id.to_string(),
|
||||
@@ -338,21 +448,26 @@ impl<'a> Mapper<'a> {
|
||||
self.component(&mut o, &view, &rest_id);
|
||||
}
|
||||
self.prop_refs(&mut o, &view, inst);
|
||||
let child_space = if matches!(ty, "GROUP" | "BOOLEAN_OPERATION") {
|
||||
space * view.get("transform").and_then(matrix).unwrap_or_default()
|
||||
} else {
|
||||
Affine::IDENTITY
|
||||
};
|
||||
let child_space = if matches!(ty, "GROUP" | "BOOLEAN_OPERATION") { space * view.transform().unwrap_or_default() } else { Affine::IDENTITY };
|
||||
let mut kids = Vec::new();
|
||||
if ty == "INSTANCE" {
|
||||
if let Some((sub, sym)) = self.instance(&mut o, &view, id, inst, &rest_id) {
|
||||
let main = self.nodes.get(&sym).map(|m| View::of(m));
|
||||
let fit = Fit::frame(main.and_then(|m| m.size()), view.size());
|
||||
for c in self.children(&sym) {
|
||||
kids.extend(self.layer(&c, Some(&sub), child_space, Some(&view), depth + 1));
|
||||
kids.extend(self.layer(&c, Some(&sub), child_space, Some(&view), fit, depth + 1));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Inside an instance a layer's children are its main's: they follow it from the main's
|
||||
// size to its own.
|
||||
let fit = match (inst, ty) {
|
||||
(None, _) => None,
|
||||
(Some(_), "GROUP" | "BOOLEAN_OPERATION") => Fit::scale(View::of(base).size(), view.size()),
|
||||
(Some(_), _) => Fit::frame(View::of(base).size(), view.size()),
|
||||
};
|
||||
for c in self.children(id) {
|
||||
kids.extend(self.layer(&c, inst, child_space, Some(&view), depth + 1));
|
||||
kids.extend(self.layer(&c, inst, child_space, Some(&view), fit, depth + 1));
|
||||
}
|
||||
}
|
||||
if stand.is_some_and(|s| s.0 == "FRAME") {
|
||||
@@ -487,10 +602,10 @@ impl<'a> Mapper<'a> {
|
||||
if let Some(b) = v.str("blendMode").filter(|b| *b != "UNSET") {
|
||||
o.insert("blendMode".into(), json!(b));
|
||||
}
|
||||
if let Some(s) = v.get("size").and_then(vec2) {
|
||||
o.insert("size".into(), s);
|
||||
if let Some((x, y)) = v.size() {
|
||||
o.insert("size".into(), json!({"x": x, "y": y}));
|
||||
}
|
||||
let t = v.get("transform").and_then(matrix).unwrap_or_default();
|
||||
let t = v.transform().unwrap_or_default();
|
||||
o.insert("relativeTransform".into(), rows(space * t));
|
||||
// Paints, from the shared style when one is linked (the layer's own copy can be stale).
|
||||
let mut styles = Obj::new();
|
||||
@@ -739,7 +854,8 @@ impl<'a> Mapper<'a> {
|
||||
vd.get("styleOverrideTable").map(Kv::list).unwrap_or_default().iter().filter_map(|s| Some((num(s, "styleID")? as u64, s))).collect();
|
||||
let style = |id: u32| styles.get(&u64::from(id)).copied().filter(|_| id != 0);
|
||||
let scale = |k: &str| {
|
||||
let (size, norm) = (v.get("size").and_then(|s| num(s, k)), vd.get("normalizedSize").and_then(|s| num(s, k)));
|
||||
let size = v.size().map(|s| if k == "x" { s.0 } else { s.1 });
|
||||
let norm = vd.get("normalizedSize").and_then(|s| num(s, k));
|
||||
size.zip(norm).filter(|(s, n)| s.is_finite() && *n > 1e-9).map_or(1.0, |(s, n)| s / n)
|
||||
};
|
||||
let (sx, sy) = (scale("x"), scale("y"));
|
||||
@@ -908,7 +1024,15 @@ 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<String> = outer.map(|i| [i.path.as_slice(), &[id.to_string()]].concat()).unwrap_or_default();
|
||||
let mut overrides: BTreeMap<Vec<String>, Vec<&'a Kv<'a>>> = outer.map(|i| (*i.overrides).clone()).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, 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() {
|
||||
for ov in list {
|
||||
@@ -920,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, ov);
|
||||
e.insert(0, (Entry::Override, ov));
|
||||
} else {
|
||||
e.push(ov);
|
||||
e.push((
|
||||
match (layer, resized) {
|
||||
(0, _) => Entry::Override,
|
||||
(_, false) => Entry::Derived,
|
||||
(_, true) => Entry::Stale,
|
||||
},
|
||||
ov,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1128,7 +1259,7 @@ impl<'a> Mapper<'a> {
|
||||
found.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
|
||||
let mut out = Vec::new();
|
||||
for (_, id, n) in found {
|
||||
let Some(node) = self.layer(&id, None, Affine::IDENTITY, None, 0) else { break };
|
||||
let Some(node) = self.layer(&id, None, Affine::IDENTITY, None, None, 0) else { break };
|
||||
let name = n.get("name").and_then(Kv::str).unwrap_or("Style");
|
||||
let desc = n.get("styleDescription").or_else(|| n.get("description")).and_then(Kv::str).unwrap_or("");
|
||||
let ty = n.get("styleType").and_then(Kv::str).unwrap_or("");
|
||||
|
||||
@@ -32,6 +32,7 @@ enum ExpressionFunction ADDITION SUBTRACTION RESOLVE_VARIANT MULTIPLY DIVIDE EQU
|
||||
enum NavigationType NAVIGATE OVERLAY
|
||||
enum TransitionType INSTANT_TRANSITION DISSOLVE SMART_ANIMATE
|
||||
enum EasingType IN_CUBIC OUT_CUBIC
|
||||
enum ConstraintType MIN CENTER MAX STRETCH SCALE
|
||||
struct GUID uint sessionID, uint localID
|
||||
struct Color float r, float g, float b, float a
|
||||
struct Vector float x, float y
|
||||
@@ -70,7 +71,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
|
||||
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 Blob byte[] bytes
|
||||
message Message NodeChange[] nodeChanges, Blob[] blobs
|
||||
";
|
||||
@@ -665,6 +666,134 @@ fn instances_inside_their_own_main_stop() {
|
||||
assert!(doc.nodes().any(|n| n.name == "Self"));
|
||||
}
|
||||
|
||||
#[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))];
|
||||
let layer = |id, pos, ty, name, w, h, x, y, c: Vec<(&'static str, Kv<'static>)>| {
|
||||
node(id, Some((5, pos)), ty, name, [vec![("size", xy(w, h)), ("transform", at(x, y))], c].concat())
|
||||
};
|
||||
let nodes = vec![
|
||||
node(0, None, "DOCUMENT", "D", vec![]),
|
||||
node(1, Some((0, "!")), "CANVAS", "P", vec![]),
|
||||
node(5, Some((1, "!")), "SYMBOL", "Logo", vec![("size", xy(132.0, 22.0)), ("transform", at(0.0, 0.0))]),
|
||||
layer(6, "!", "ROUNDED_RECTANGLE", "Mark", 132.0, 22.0, 0.0, 0.0, pin("SCALE", "SCALE")),
|
||||
layer(7, "\"", "ROUNDED_RECTANGLE", "Badge", 32.0, 10.0, 100.0, 12.0, pin("MAX", "MAX")),
|
||||
layer(8, "#", "ROUNDED_RECTANGLE", "Bar", 112.0, 2.0, 10.0, 0.0, pin("STRETCH", "MIN")),
|
||||
// A group scales its layers with it.
|
||||
layer(11, "$", "FRAME", "Wordmark", 66.0, 22.0, 66.0, 0.0, [pin("SCALE", "SCALE"), vec![("resizeToFit", Kv::Bool(true))]].concat()),
|
||||
node(12, Some((11, "!")), "ROUNDED_RECTANGLE", "Letter", vec![("size", xy(66.0, 22.0)), ("transform", at(0.0, 0.0))]),
|
||||
node(
|
||||
9,
|
||||
Some((1, "\"")),
|
||||
"INSTANCE",
|
||||
"Small logo",
|
||||
vec![("size", xy(90.0, 15.0)), ("transform", at(0.0, 100.0)), ("symbolData", o(vec![("symbolID", g(5))]))],
|
||||
),
|
||||
// Derived layout the file gives wins over the constraints.
|
||||
node(
|
||||
10,
|
||||
Some((1, "#")),
|
||||
"INSTANCE",
|
||||
"Derived logo",
|
||||
vec![
|
||||
("size", xy(90.0, 15.0)),
|
||||
("transform", at(0.0, 200.0)),
|
||||
("symbolData", o(vec![("symbolID", g(5))])),
|
||||
("derivedSymbolData", arr(vec![o(vec![("guidPath", o(vec![("guids", arr(vec![g(6)]))])), ("size", xy(50.0, 5.0))])])),
|
||||
],
|
||||
),
|
||||
// A size the instance overrides: the place still follows the constraints.
|
||||
node(
|
||||
13,
|
||||
Some((1, "$")),
|
||||
"INSTANCE",
|
||||
"Resized badge",
|
||||
vec![
|
||||
("size", xy(90.0, 15.0)),
|
||||
("transform", at(0.0, 300.0)),
|
||||
(
|
||||
"symbolData",
|
||||
o(vec![
|
||||
("symbolID", g(5)),
|
||||
("symbolOverrides", arr(vec![o(vec![("guidPath", o(vec![("guids", arr(vec![g(7)]))])), ("size", xy(20.0, 10.0))])])),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
let (doc, _) = import(&canvas(&schema(), &o(vec![("nodeChanges", arr(nodes))])), "x").unwrap();
|
||||
let box_of = |inst: &str, name: &str| {
|
||||
let i = named(&doc, inst);
|
||||
let n = i.children.iter().map(|c| doc.node(*c).unwrap()).find(|n| n.name == name).unwrap();
|
||||
let t = n.transform.translation();
|
||||
[t.x, t.y, n.width, n.height].map(|x| (x * 1e3).round() / 1e3)
|
||||
};
|
||||
assert_eq!(box_of("Small logo", "Mark"), [0.0, 0.0, 90.0, 15.0]);
|
||||
assert_eq!(box_of("Small logo", "Badge"), [58.0, 5.0, 32.0, 10.0]);
|
||||
assert_eq!(box_of("Small logo", "Bar"), [10.0, 0.0, 70.0, 2.0]);
|
||||
// The main's own, then the instances'.
|
||||
let mut letters: Vec<[f64; 2]> =
|
||||
doc.nodes().filter(|n| n.name == "Letter").map(|n| [n.width, n.height].map(|x| (x * 1e3).round() / 1e3)).collect();
|
||||
letters.sort_by(|a, b| a[0].total_cmp(&b[0]));
|
||||
assert_eq!(letters, [[45.0, 15.0], [45.0, 15.0], [45.0, 15.0], [66.0, 22.0]]);
|
||||
assert_eq!(box_of("Derived logo", "Mark")[2..], [50.0, 5.0]);
|
||||
assert_eq!(box_of("Resized badge", "Badge"), [58.0, 5.0, 20.0, 10.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_instances_resized_again_drop_the_layout_derived_for_their_old_size() {
|
||||
let stretch = || vec![("horizontalConstraint", e("STRETCH")), ("verticalConstraint", e("MIN"))];
|
||||
let nodes = vec![
|
||||
node(0, None, "DOCUMENT", "D", vec![]),
|
||||
node(1, Some((0, "!")), "CANVAS", "P", vec![]),
|
||||
node(20, Some((1, "!")), "SYMBOL", "Track", vec![("size", xy(100.0, 10.0)), ("transform", at(0.0, 0.0))]),
|
||||
node(21, Some((20, "!")), "ROUNDED_RECTANGLE", "Fill", [vec![("size", xy(100.0, 10.0)), ("transform", at(0.0, 0.0))], stretch()].concat()),
|
||||
node(30, Some((1, "\"")), "SYMBOL", "Slider", vec![("size", xy(200.0, 50.0)), ("transform", at(0.0, 100.0))]),
|
||||
// The nested instance is 150 wide in its main, with the layout derived for that.
|
||||
node(
|
||||
31,
|
||||
Some((30, "!")),
|
||||
"INSTANCE",
|
||||
"Inner",
|
||||
[
|
||||
vec![
|
||||
("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)),
|
||||
("fillPaints", arr(vec![solid(1.0, 0.0, 0.0)])),
|
||||
])]),
|
||||
),
|
||||
],
|
||||
stretch(),
|
||||
]
|
||||
.concat(),
|
||||
),
|
||||
node(
|
||||
40,
|
||||
Some((1, "#")),
|
||||
"INSTANCE",
|
||||
"Wide slider",
|
||||
vec![("size", xy(300.0, 50.0)), ("transform", at(0.0, 200.0)), ("symbolData", o(vec![("symbolID", g(30))]))],
|
||||
),
|
||||
];
|
||||
let (doc, _) = import(&canvas(&schema(), &o(vec![("nodeChanges", arr(nodes))])), "x").unwrap();
|
||||
let width = |outer: &str| {
|
||||
let inner = doc.node(named(&doc, outer).children[0]).unwrap();
|
||||
(inner.width, doc.node(inner.children[0]).unwrap().width)
|
||||
};
|
||||
// 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"]
|
||||
mod tail;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user