mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-11 16:30:12 -06:00
Merge main into #1397
# Conflicts: # ROADMAP.md # contributors/people.toml # crates/format/src/fig/map/layer.rs # docs/file-format-parity.md # docs/gaps.md
This commit is contained in:
@@ -274,19 +274,30 @@ fn variables_modes_and_bindings_round_trip() {
|
||||
#[test]
|
||||
fn variables_are_written_as_nodes_on_a_hidden_page() {
|
||||
let nodes = written(&themed());
|
||||
let hidden = named(&nodes, "Internal Only Canvas");
|
||||
assert_hidden_theme(&nodes);
|
||||
assert_variable_values(&nodes);
|
||||
assert_variable_bindings(&nodes);
|
||||
}
|
||||
|
||||
fn assert_hidden_theme(nodes: &[Value]) {
|
||||
let hidden = named(nodes, "Internal Only Canvas");
|
||||
assert_eq!((hidden["type"].as_str(), hidden["internalOnly"].as_bool(), hidden["visible"].as_bool()), (Some("CANVAS"), Some(true), Some(false)));
|
||||
let set = named(&nodes, "Theme");
|
||||
let set = named(nodes, "Theme");
|
||||
assert_eq!(set["type"], "VARIABLE_SET");
|
||||
assert_eq!(set["parentIndex"]["guid"], hidden["guid"]);
|
||||
let modes = set["variableSetModes"].as_array().unwrap();
|
||||
assert_eq!(modes.iter().map(|m| m["name"].as_str().unwrap()).collect::<Vec<_>>(), ["Light", "Dark"]);
|
||||
let bg = named(&nodes, "color/bg");
|
||||
}
|
||||
|
||||
fn assert_variable_values(nodes: &[Value]) {
|
||||
let set = named(nodes, "Theme");
|
||||
let modes = set["variableSetModes"].as_array().unwrap();
|
||||
let bg = named(nodes, "color/bg");
|
||||
assert_eq!((bg["type"].as_str(), bg["variableResolvedType"].as_str()), (Some("VARIABLE"), Some("COLOR")));
|
||||
assert_eq!(bg["variableSetID"]["guid"], set["guid"]);
|
||||
assert_eq!(bg["description"], "Behind everything");
|
||||
assert_eq!(bg["variableScopes"], json!(["FRAME_FILL", "SHAPE_FILL"]));
|
||||
assert_eq!(named(&nodes, "color/fg")["variableScopes"], json!(["TEXT_FILL", "STROKE", "EFFECT_COLOR"]));
|
||||
assert_eq!(named(nodes, "color/fg")["variableScopes"], json!(["TEXT_FILL", "STROKE", "EFFECT_COLOR"]));
|
||||
let values = bg["variableDataValues"]["entries"].as_array().unwrap();
|
||||
assert_eq!(values.len(), 2);
|
||||
assert_eq!(values[1]["modeID"], modes[1]["id"]);
|
||||
@@ -295,19 +306,25 @@ fn variables_are_written_as_nodes_on_a_hidden_page() {
|
||||
(Some("COLOR"), Some(0.25))
|
||||
);
|
||||
// An alias names the variable's node; every value says the type it resolves to.
|
||||
let accent = &named(&nodes, "color/accent")["variableDataValues"]["entries"];
|
||||
let accent = &named(nodes, "color/accent")["variableDataValues"]["entries"];
|
||||
assert_eq!(accent[0]["variableData"]["dataType"], "ALIAS");
|
||||
assert_eq!(accent[0]["variableData"]["resolvedDataType"], "COLOR");
|
||||
assert_eq!(accent[0]["variableData"]["value"]["alias"]["guid"], named(&nodes, "color/fg")["guid"]);
|
||||
assert_eq!(named(&nodes, "title")["variableDataValues"]["entries"][1]["variableData"]["value"]["textValue"], "Grüße 😀");
|
||||
assert_eq!(named(&nodes, "shown")["variableDataValues"]["entries"][1]["variableData"]["value"]["boolValue"], false);
|
||||
assert_eq!(named(&nodes, "space/gap")["variableDataValues"]["entries"][0]["variableData"]["value"]["floatValue"], 8.0);
|
||||
assert_eq!(accent[0]["variableData"]["value"]["alias"]["guid"], named(nodes, "color/fg")["guid"]);
|
||||
assert_eq!(named(nodes, "title")["variableDataValues"]["entries"][1]["variableData"]["value"]["textValue"], "Grüße 😀");
|
||||
assert_eq!(named(nodes, "shown")["variableDataValues"]["entries"][1]["variableData"]["value"]["boolValue"], false);
|
||||
assert_eq!(named(nodes, "space/gap")["variableDataValues"]["entries"][0]["variableData"]["value"]["floatValue"], 8.0);
|
||||
}
|
||||
|
||||
fn assert_variable_bindings(nodes: &[Value]) {
|
||||
let bg = named(nodes, "color/bg");
|
||||
let set = named(nodes, "Theme");
|
||||
let modes = set["variableSetModes"].as_array().unwrap();
|
||||
// Bindings: on the paint, the gradient stop, the effect and the grid, and in the layer's map.
|
||||
let card = named(&nodes, "Card");
|
||||
let card = named(nodes, "Card");
|
||||
assert_eq!(card["fillPaints"][0]["colorVar"]["value"]["alias"]["guid"], bg["guid"]);
|
||||
assert_eq!(card["variableModeBySetMap"]["entries"][0]["variableSetID"]["guid"], set["guid"]);
|
||||
assert_eq!(card["variableModeBySetMap"]["entries"][0]["variableModeID"], modes[1]["id"]);
|
||||
assert_eq!(card["layoutGrids"][0]["gutterSizeVar"]["value"]["alias"]["guid"], named(&nodes, "space/gap")["guid"]);
|
||||
assert_eq!(card["layoutGrids"][0]["gutterSizeVar"]["value"]["alias"]["guid"], named(nodes, "space/gap")["guid"]);
|
||||
let fields: Vec<&str> =
|
||||
card["variableConsumptionMap"]["entries"].as_array().unwrap().iter().map(|e| e["variableField"].as_str().unwrap()).collect();
|
||||
for f in
|
||||
@@ -315,7 +332,7 @@ fn variables_are_written_as_nodes_on_a_hidden_page() {
|
||||
{
|
||||
assert!(fields.contains(&f), "{f} in {fields:?}");
|
||||
}
|
||||
let swatch = named(&nodes, "Swatch");
|
||||
let swatch = named(nodes, "Swatch");
|
||||
assert!(swatch["fillPaints"][1]["stopsVar"][0].get("colorVar").is_none());
|
||||
assert_eq!(swatch["fillPaints"][1]["stopsVar"][1]["colorVar"]["value"]["alias"]["guid"], bg["guid"]);
|
||||
assert_eq!(swatch["fillPaints"][1]["stopsVar"].as_array().unwrap().len(), 2);
|
||||
@@ -324,7 +341,7 @@ fn variables_are_written_as_nodes_on_a_hidden_page() {
|
||||
assert_eq!(swatch["effects"][0][key]["dataType"], "ALIAS", "{key}");
|
||||
}
|
||||
// The page sets a mode too.
|
||||
assert_eq!(named(&nodes, "Page 1")["variableModeBySetMap"]["entries"][0]["variableSetID"]["guid"], named(&nodes, "Spacing")["guid"]);
|
||||
assert_eq!(named(nodes, "Page 1")["variableModeBySetMap"]["entries"][0]["variableSetID"]["guid"], named(nodes, "Spacing")["guid"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::common::{layout_grid, node_field, prop_type, resolved_type, scope_name, variant_pairs};
|
||||
use super::*;
|
||||
use super::{
|
||||
AUX, Action, Affine, Bound, Color, Exporter, Fields, Kv, LAYERS, MAX_NODES, Node, NodeId, NodeKind, Overlay, OverlayPosition, Paint, StyleKind,
|
||||
TextData, VariableValue, arr, b, color, e, fl, guid, matrix, o, position, root_fields, s, u, xy,
|
||||
};
|
||||
|
||||
impl<'d> Exporter<'d> {
|
||||
/// GUIDs for every layer (pages first, then depth-first), component property definitions,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
Bound, ExportSize, Fields, GAMEPAD_BUTTONS, GridAlign, GridKind, KeyMods, Kv, LetterSpacing, LineHeight, MAX_NETWORK, Node, NodeKind, Paint,
|
||||
PropDef, SCHEMA, TextCase, TextDecoration, TextStyle, VariableType, VectorNetwork, WindingRule, arr, b, color, e, face_style, fl, guid, int, o,
|
||||
s, xy,
|
||||
};
|
||||
|
||||
/// Longest kept description of such a layer (bytes of JSON).
|
||||
const MAX_STASH: usize = 1 << 20;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::common::{
|
||||
bool_data, changed, prop_type, resolved_alike, scaled, sort_position, symbol_data, text_data, text_value, var_data, variant_pairs,
|
||||
};
|
||||
use super::*;
|
||||
use super::{AUX, BTreeMap, Exporter, Fields, Kv, MAX_NODES, Node, NodeId, NodeKind, PropDef, Value, arr, b, e, fl, guid, o, s};
|
||||
|
||||
impl<'d> Exporter<'d> {
|
||||
/// A component property definition of `owner` (a component, or a set), `index`-th in its list.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use super::common::{export_setting, layout_grid, outlined, prop_type, variant_pairs};
|
||||
use super::*;
|
||||
use super::{
|
||||
AUX, AutoLayout, BoolOp, Bound, ContentAlign, CrossAlign, Exporter, Fields, HConstraint, Kv, LAYERS, LayoutMode, MAX_DEPTH, MainAlign, MaskType,
|
||||
Node, NodeId, NodeKind, PropDef, STASH, Sizing, TextData, TextStyle, VConstraint, WindingRule, arr, b, blend_name, color, e, fl, grid_sizing,
|
||||
guid, int, matrix, o, position, root_fields, s, u, xy,
|
||||
};
|
||||
|
||||
impl<'d> Exporter<'d> {
|
||||
/// Write layer `id` (child `index` of the layer with GUID `parent_guid`) and its subtree.
|
||||
@@ -15,11 +19,11 @@ impl<'d> Exporter<'d> {
|
||||
("phase", e("CREATED")),
|
||||
("parentIndex", o(vec![("guid", parent_guid), ("position", s(&position(index)))])),
|
||||
];
|
||||
let children = if let NodeKind::Page { background, guides, prototype, .. } = &n.kind {
|
||||
let children = if let NodeKind::Page { background, prototype, .. } = &n.kind {
|
||||
f.extend([("type", e("CANVAS")), ("name", s(&n.name))]);
|
||||
f.extend(root_fields());
|
||||
f.extend([("backgroundColor", color(*background)), ("backgroundOpacity", fl(background.a.into())), ("backgroundEnabled", b(true))]);
|
||||
if !guides.is_empty() {
|
||||
if !n.guides.is_empty() {
|
||||
self.warn("ruler guides were left out");
|
||||
}
|
||||
if *prototype != Default::default() {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::common::one_sided;
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, Bound, EffectKind, Exporter, Fields, GradientKind, ImageScaleMode, Kv, MAX_IMAGE, Node, NodeKind, NoiseMode, Paint, PaintKind,
|
||||
StrokeAlign, StrokeCap, arr, b, blend_name, cap_name, color, e, fl, join_name, matrix, o, s, sha1_hex, xy,
|
||||
};
|
||||
|
||||
impl<'d> Exporter<'d> {
|
||||
/// A paint list: a layer's fills (`fills`) or strokes with the variables `bound` to them.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::common::key_codes;
|
||||
use super::*;
|
||||
use super::{
|
||||
Action, Direction, Easing, Exporter, Fields, Interaction, Kv, Node, NodeId, Overflow, OverlayPosition, ScrollPosition, Transition, Trigger, arr,
|
||||
b, color, e, fl, int, o, position, s,
|
||||
};
|
||||
|
||||
impl<'d> Exporter<'d> {
|
||||
/// Interactions, scrolling, flow starts and the overlay settings this layer is opened with.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use super::common::{style_fields, unsupported_decoration_look};
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, BezPath, Bound, Exporter, Fields, Kv, ListStyle, MAX_CANVAS, MAX_CHARS, MAX_NETWORK, Node, NodeKind, Paint, StrokeAlign, StrokeCap,
|
||||
StrokeJoin, TextAlignH, TextAlignV, TextAutoResize, TextData, TextStyle, VectorNetwork, WindingRule, arr, b, cap_name, e, finite, fl, int,
|
||||
join_name, network, o, path_blob, s, u, xy,
|
||||
};
|
||||
|
||||
impl<'d> Exporter<'d> {
|
||||
/// Text: its style, box settings, and characters with their styled runs. `fills`: the layer's
|
||||
|
||||
@@ -400,7 +400,7 @@ impl<'s> Decoder<'s, '_> {
|
||||
/// An encoder for the same format: the `.fig` export writes its schema and message with it (and
|
||||
/// test fixtures are built with it). Values are checked against the schema; it never panics.
|
||||
pub mod encode {
|
||||
use super::*;
|
||||
use super::{BUILTINS, Field, Kind, KiwiError, Kv, MAX_DEPTH, Schema, Type, err};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Writer(pub Vec<u8>);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::layer::own_text;
|
||||
use super::text::char_styles;
|
||||
use super::*;
|
||||
use super::{BTreeMap, BTreeSet, Kv, MAX_DEPTH, Mapper, Obj, Rc, Value, View, guid, json};
|
||||
|
||||
/// An override row names a layer of the main by its current GUID or by the `overrideKey` the
|
||||
/// layer had when the row was written: the key becomes the current GUID, unless the layer is
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::layer::{cap_name, own_text, svg_path, valid_size};
|
||||
use super::*;
|
||||
use super::{Affine, Kv, Mapper, NO_STYLE, Obj, Point, Value, View, guid, json, matrix, network, num, rows};
|
||||
|
||||
impl<'a> Mapper<'a> {
|
||||
/// A connector's end caps, and when it has no saved outline or network a straight line
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::component::{assigned, prop_guid};
|
||||
use super::paint::color;
|
||||
use super::variables::variable_value;
|
||||
use super::*;
|
||||
use super::{Affine, BTreeMap, BTreeSet, Kv, MAX_DEPTH, Mapper, Obj, Value, View, copy_str, guid, json, rows};
|
||||
|
||||
impl<'a> Mapper<'a> {
|
||||
/// The DOCUMENT with its pages.
|
||||
|
||||
@@ -2,7 +2,10 @@ use super::component::{Entry, Inst, OverrideRow, prop_ref};
|
||||
use super::connector::connector_has_saved_label;
|
||||
use super::layout::{layout_grid, tracks};
|
||||
use super::paint::{effect, export_setting, paint, paint_bound, stroke};
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, BTreeMap, BTreeSet, Kv, MAX_COMMANDS, MAX_DEPTH, Mapper, Obj, Value, View, copy_num, copy_str, guid, json, matrix, network, num, rows,
|
||||
size,
|
||||
};
|
||||
|
||||
/// 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.
|
||||
@@ -61,6 +64,22 @@ impl Fit {
|
||||
}
|
||||
}
|
||||
|
||||
struct LayerChildren<'s, 'a> {
|
||||
id: &'s str,
|
||||
inst: Option<&'s Inst<'a>>,
|
||||
base: &'s Kv<'a>,
|
||||
child_space: Affine,
|
||||
view: &'s View<'a>,
|
||||
object: &'s mut Obj,
|
||||
ty: &'s str,
|
||||
rest_id: &'s str,
|
||||
connector_snapshot: bool,
|
||||
connector_terminal: bool,
|
||||
depth: usize,
|
||||
included: Option<&'s BTreeSet<String>>,
|
||||
slot_content: Option<String>,
|
||||
}
|
||||
|
||||
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
|
||||
@@ -136,16 +155,7 @@ impl<'a> Mapper<'a> {
|
||||
connector_snapshot && (!connector_has_saved_label(&view) || depth >= MAX_DEPTH || self.count.saturating_add(1) >= self.budget());
|
||||
// A supported connector snapshot has its own note below instead of the generic stand-in one.
|
||||
let stand = stand_in(own).filter(|_| !connector_snapshot);
|
||||
let ty = match own {
|
||||
"ROUNDED_RECTANGLE" => "RECTANGLE",
|
||||
"SYMBOL" => "COMPONENT",
|
||||
"FRAME" if view.flag("isStateGroup") == Some(true) => "COMPONENT_SET",
|
||||
"FRAME" if view.flag("resizeToFit") == Some(true) => "GROUP",
|
||||
"VARIABLE" | "VARIABLE_SET" => return None,
|
||||
"CONNECTOR" if connector_snapshot && connector_terminal => "VECTOR",
|
||||
"CONNECTOR" if connector_snapshot => "FRAME",
|
||||
t => stand.map_or(t, |s| s.0),
|
||||
};
|
||||
let ty = Self::mapped_type(own, &view, connector_snapshot, connector_terminal, stand)?;
|
||||
if let Some((_, what)) = stand.filter(|_| own != "TEXT_PATH") {
|
||||
self.warn(format!("{} layers import as {what}", own.to_ascii_lowercase().replace('_', " ")));
|
||||
}
|
||||
@@ -225,18 +235,63 @@ impl<'a> Mapper<'a> {
|
||||
source.insert("slotOverride".into(), json!(true));
|
||||
}
|
||||
let child_space = if matches!(ty, "GROUP" | "BOOLEAN_OPERATION") { space * view.transform().unwrap_or_default() } else { Affine::IDENTITY };
|
||||
let mut kids = if connector_snapshot && !connector_terminal { self.connector_snapshot_children(&view, &rest_id) } else { Vec::new() };
|
||||
let mut kids = self.map_layer_children(LayerChildren {
|
||||
id,
|
||||
inst,
|
||||
base,
|
||||
child_space,
|
||||
view: &view,
|
||||
object: &mut o,
|
||||
ty,
|
||||
rest_id: &rest_id,
|
||||
connector_snapshot,
|
||||
connector_terminal,
|
||||
depth,
|
||||
included,
|
||||
slot_content,
|
||||
});
|
||||
if stand.is_some_and(|s| s.0 == "FRAME") {
|
||||
if let Some(snapshot) = self.board_snapshot(&mut o, &view, own, &rest_id, depth) {
|
||||
kids.splice(0..0, snapshot);
|
||||
} else {
|
||||
kids.splice(0..0, self.shape_with_text(&mut o, &view, own, &rest_id));
|
||||
kids.extend(self.inner_text(&o, &view, &rest_id, own == "STICKY"));
|
||||
}
|
||||
}
|
||||
if !kids.is_empty() {
|
||||
o.insert("children".into(), Value::Array(kids));
|
||||
}
|
||||
Some(Value::Object(o))
|
||||
}
|
||||
|
||||
fn map_layer_children(&mut self, context: LayerChildren<'_, 'a>) -> Vec<Value> {
|
||||
let LayerChildren {
|
||||
id,
|
||||
inst,
|
||||
base,
|
||||
child_space,
|
||||
view,
|
||||
object: o,
|
||||
ty,
|
||||
rest_id,
|
||||
connector_snapshot,
|
||||
connector_terminal,
|
||||
depth,
|
||||
included,
|
||||
slot_content,
|
||||
} = context;
|
||||
let mut kids = if connector_snapshot && !connector_terminal { self.connector_snapshot_children(view, rest_id) } else { Vec::new() };
|
||||
if connector_terminal {
|
||||
// The saved vector is flattened into the connector node when no child slot remains.
|
||||
} else if ty == "INSTANCE" {
|
||||
if let Some((sub, sym)) = self.instance(&mut o, &view, id, inst, &rest_id) {
|
||||
if let Some((sub, sym)) = self.instance(o, view, id, inst, rest_id) {
|
||||
let main = self.nodes.get(&sym).map(|m| View::of(m));
|
||||
let fit = Fit::frame(main.as_ref().and_then(View::size), view.size());
|
||||
// An instance that records no auto layout of its own has its main's.
|
||||
let view = match main {
|
||||
Some(m) if view.get("stackMode").is_none() && m.get("stackMode").is_some() => {
|
||||
let layered = View { fitted: view.fitted, ..m.with(view.layers.iter().copied()) };
|
||||
self.auto_layout(&mut o, &layered);
|
||||
self.auto_layout(o, &layered);
|
||||
layered
|
||||
}
|
||||
_ => view.clone(),
|
||||
@@ -252,7 +307,7 @@ impl<'a> Mapper<'a> {
|
||||
content_inst.slot_content = true;
|
||||
}
|
||||
for c in self.children(&content) {
|
||||
kids.extend(self.layer_filtered(&c, content_inst.as_ref(), child_space, Some(&view), None, depth + 1, included));
|
||||
kids.extend(self.layer_filtered(&c, content_inst.as_ref(), child_space, Some(view), None, depth + 1, included));
|
||||
}
|
||||
} else {
|
||||
// Inside an instance a layer's children are its main's: they follow it from the main's
|
||||
@@ -263,21 +318,29 @@ impl<'a> Mapper<'a> {
|
||||
(Some(_), _) => Fit::frame(View::of(base).size(), view.size()),
|
||||
};
|
||||
for c in self.children(id) {
|
||||
kids.extend(self.layer_filtered(&c, inst, child_space, Some(&view), fit, depth + 1, included));
|
||||
kids.extend(self.layer_filtered(&c, inst, child_space, Some(view), fit, depth + 1, included));
|
||||
}
|
||||
}
|
||||
if stand.is_some_and(|s| s.0 == "FRAME") {
|
||||
if let Some(snapshot) = self.board_snapshot(&mut o, &view, own, &rest_id, depth) {
|
||||
kids.splice(0..0, snapshot);
|
||||
} else {
|
||||
kids.splice(0..0, self.shape_with_text(&mut o, &view, own, &rest_id));
|
||||
kids.extend(self.inner_text(&o, &view, &rest_id, own == "STICKY"));
|
||||
}
|
||||
kids
|
||||
}
|
||||
|
||||
fn mapped_type<'s>(
|
||||
own: &'s str,
|
||||
view: &View<'a>,
|
||||
connector_snapshot: bool,
|
||||
connector_terminal: bool,
|
||||
stand: Option<(&'static str, &'static str)>,
|
||||
) -> Option<&'s str> {
|
||||
match own {
|
||||
"ROUNDED_RECTANGLE" => Some("RECTANGLE"),
|
||||
"SYMBOL" => Some("COMPONENT"),
|
||||
"FRAME" if view.flag("isStateGroup") == Some(true) => Some("COMPONENT_SET"),
|
||||
"FRAME" if view.flag("resizeToFit") == Some(true) => Some("GROUP"),
|
||||
"VARIABLE" | "VARIABLE_SET" => None,
|
||||
"CONNECTOR" if connector_snapshot && connector_terminal => Some("VECTOR"),
|
||||
"CONNECTOR" if connector_snapshot => Some("FRAME"),
|
||||
t => Some(stand.map_or(t, |s| s.0)),
|
||||
}
|
||||
if !kids.is_empty() {
|
||||
o.insert("children".into(), Value::Array(kids));
|
||||
}
|
||||
Some(Value::Object(o))
|
||||
}
|
||||
|
||||
/// FigJam notes and shapes keep their paints and text on generated template children,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::paint::{bound, color};
|
||||
use super::*;
|
||||
use super::{BTreeMap, BTreeSet, Kv, MAX_GRID_TRACK_VALUE, MAX_GRID_TRACKS, Mapper, Obj, Value, View, copy_num, guid, json, num};
|
||||
|
||||
impl<'a> Mapper<'a> {
|
||||
pub(super) fn auto_layout(&mut self, o: &mut Obj, v: &View<'a>) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::layer::cap_name;
|
||||
use super::variables::alias;
|
||||
use super::*;
|
||||
use super::{Affine, Kv, Mapper, NO_STYLE, Obj, Point, Value, View, copy_num, copy_str, guid, json, matrix, num, rows, vec2};
|
||||
|
||||
fn filter_number(value: &Kv) -> Option<f64> {
|
||||
let number = match value {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::paint::color;
|
||||
use super::variables::variable_data;
|
||||
use super::*;
|
||||
use super::{Kv, MAX_ACTION_DEPTH, Mapper, Obj, Value, View, copy_str, guid, json, num, ref_guid};
|
||||
|
||||
impl<'a> Mapper<'a> {
|
||||
pub(super) fn prototype(&mut self, o: &mut Obj, v: &View<'a>) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::paint::{paint, style_link};
|
||||
use super::*;
|
||||
use super::{Kv, Mapper, Obj, Value, View, copy_num, copy_str, json, network, num};
|
||||
|
||||
/// The value from the last override layer holding a valid one for an optional text setting; invalid instance rows fall through.
|
||||
fn text_num(v: &View, key: &str) -> Option<f64> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::paint::color;
|
||||
use super::*;
|
||||
use super::{Kv, MAX_EXPR_DEPTH, Mapper, Obj, Value, View, guid, json, num, ref_guid};
|
||||
|
||||
impl<'a> Mapper<'a> {
|
||||
pub(super) fn variable_bindings(&mut self, o: &mut Obj, v: &View<'a>) {
|
||||
|
||||
@@ -983,6 +983,11 @@ fn assert_external_namespace(doc: &Document) {
|
||||
let copied_frame = child(doc, copied_body, 0);
|
||||
assert_eq!(doc.node(frame).unwrap().source, None, "materialized ordinary layers are owned by the frame");
|
||||
assert_eq!(doc.node(copied_frame).unwrap().source, Some(frame));
|
||||
assert_external_slots(doc, frame, copied_frame, main);
|
||||
assert_external_copy_state(doc, frame, copied_frame);
|
||||
}
|
||||
|
||||
fn assert_external_slots(doc: &Document, frame: NodeId, copied_frame: NodeId, main: NodeId) {
|
||||
for index in 0..2 {
|
||||
let slot = child(doc, frame, index);
|
||||
let copied_slot = child(doc, copied_frame, index);
|
||||
@@ -999,6 +1004,9 @@ fn assert_external_namespace(doc: &Document) {
|
||||
assert_eq!(doc.node(child(doc, current, 0)).unwrap().source, Some(child(doc, inherited, 0)));
|
||||
assert_eq!(doc.node(child(doc, current, 0)).unwrap().transform.translation().x, 4.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_external_copy_state(doc: &Document, frame: NodeId, copied_frame: NodeId) {
|
||||
let old = doc.node(child(doc, frame, 0)).unwrap();
|
||||
let current = doc.node(child(doc, copied_frame, 0)).unwrap();
|
||||
assert!(!old.visible && old.is_mask && old.opacity < 1.0);
|
||||
|
||||
@@ -630,63 +630,82 @@ fn imports_the_fixture() {
|
||||
assert!(is_fig(&bytes));
|
||||
let (doc, warnings) = import(&bytes, "fallback").unwrap();
|
||||
assert_eq!(doc.name, "Fixture");
|
||||
assert_fixture_layers(&doc);
|
||||
assert_fixture_styles(&doc);
|
||||
assert_fixture_variables(&doc);
|
||||
assert_fixture_components(&doc);
|
||||
assert_fixture_prototype(&doc, &warnings);
|
||||
}
|
||||
|
||||
fn assert_fixture_layers(doc: &Document) {
|
||||
// The internal page is skipped.
|
||||
assert_eq!(doc.pages.len(), 1);
|
||||
let page = doc.node(doc.pages[0]).unwrap();
|
||||
assert_eq!(page.name, "Page A");
|
||||
// Siblings by position: the frame comes first even though it's listed after the component.
|
||||
assert_eq!(doc.node(page.children[0]).unwrap().name, "Screen");
|
||||
let screen = named(&doc, "Screen");
|
||||
let screen = named(doc, "Screen");
|
||||
let names: Vec<&str> = screen.children.iter().map(|c| doc.node(*c).unwrap().name.as_str()).collect();
|
||||
assert_eq!(names, ["Photo", "Title", "Group", "Styled", "Card", "Button copy", "Note"]);
|
||||
// Image fill from the archive.
|
||||
let photo = named(&doc, "Photo");
|
||||
let photo = named(doc, "Photo");
|
||||
let figcraft_doc::color::PaintKind::Image(img) = &photo.fills[0].kind else { panic!("not an image fill") };
|
||||
assert_eq!(doc.images.get(&img.image).map(|i| (i.width, i.height)), Some((2, 1)));
|
||||
assert_eq!((photo.corners.radii[0], photo.stroke.weight), (6.0, 2.0));
|
||||
// Text with a bold run over "the".
|
||||
let NodeKind::Text(t) = &named(&doc, "Title").kind else { panic!("not text") };
|
||||
let NodeKind::Text(t) = &named(doc, "Title").kind else { panic!("not text") };
|
||||
assert_eq!(t.characters, "Hi there");
|
||||
assert_eq!(t.style.font_size, 16.0);
|
||||
assert_eq!(t.leading_trim, figcraft_doc::LeadingTrim::CapHeight, "vertical trim (#466)");
|
||||
assert_eq!(t.runs.len(), 1);
|
||||
assert_eq!((t.runs[0].start, t.runs[0].end, t.runs[0].style.font_style.as_str()), (3, 6, "Bold"));
|
||||
// The group's children keep their place on the page (group at 100, 100).
|
||||
assert!(matches!(named(&doc, "Group").kind, NodeKind::Group));
|
||||
let tri = named(&doc, "Tri");
|
||||
assert!(matches!(named(doc, "Group").kind, NodeKind::Group));
|
||||
let tri = named(doc, "Tri");
|
||||
assert!(matches!(&tri.kind, NodeKind::Vector { network } if !network.regions.is_empty()));
|
||||
let abs = doc.world_transform(tri.id).translation();
|
||||
assert!((abs.x - 105.0).abs() < 1e-6 && (abs.y - 106.0).abs() < 1e-6, "{abs:?}");
|
||||
assert!(matches!(named(&doc, "Star").kind, NodeKind::Star { count: 6, .. }));
|
||||
// The linked style supplies the paint.
|
||||
let styled = named(&doc, "Styled");
|
||||
assert!(matches!(named(doc, "Star").kind, NodeKind::Star { count: 6, .. }));
|
||||
}
|
||||
|
||||
fn assert_fixture_styles(doc: &Document) {
|
||||
let styled = named(doc, "Styled");
|
||||
assert_eq!(styled.styles.fill.as_deref(), Some("1:3"));
|
||||
assert!(doc.styles.iter().any(|s| s.name == "Brand"));
|
||||
let figcraft_doc::color::PaintKind::Solid { color } = styled.fills[0].kind else { panic!("not solid") };
|
||||
assert_eq!((color.r, color.g, color.b), (0.0, 0.0, 1.0));
|
||||
// Variables, the fill binding and the explicit mode.
|
||||
}
|
||||
|
||||
fn assert_fixture_variables(doc: &Document) {
|
||||
let screen = named(doc, "Screen");
|
||||
assert_eq!(doc.variables.collections.len(), 1);
|
||||
assert_eq!(doc.variables.variables[0].kind, VariableType::Color);
|
||||
assert_eq!(named(&doc, "Card").bound.get("fill").map(String::as_str), Some("1:5"));
|
||||
assert_eq!(named(doc, "Card").bound.get("fill").map(String::as_str), Some("1:5"));
|
||||
assert_eq!(screen.var_modes.get("1:4").map(String::as_str), Some("1:91"));
|
||||
// The component keeps its property; the instance is expanded with the value applied.
|
||||
let button = named(&doc, "Button");
|
||||
}
|
||||
|
||||
fn assert_fixture_components(doc: &Document) {
|
||||
let button = named(doc, "Button");
|
||||
assert!(matches!(&button.kind, NodeKind::Component { props, .. } if props.contains_key("Show icon")));
|
||||
let inst = named(&doc, "Button copy");
|
||||
let inst = named(doc, "Button copy");
|
||||
assert!(
|
||||
matches!(&inst.kind, NodeKind::Instance { component, props } if *component == button.id && props.get("Show icon") == Some(&Value::Bool(false)))
|
||||
);
|
||||
let icon = doc.node(inst.children[0]).unwrap();
|
||||
assert_eq!(icon.name, "Renamed icon");
|
||||
assert!(!icon.visible);
|
||||
// Prototype: the click navigates to the component; the flow starts at the screen.
|
||||
let it = &photo.interactions[0];
|
||||
}
|
||||
|
||||
fn assert_fixture_prototype(doc: &Document, warnings: &[String]) {
|
||||
let button = named(doc, "Button");
|
||||
let screen = named(doc, "Screen");
|
||||
let it = &named(doc, "Photo").interactions[0];
|
||||
assert!(matches!(it.actions[0], figcraft_doc::proto::Action::Navigate { dest, .. } if dest == button.id));
|
||||
assert_eq!(it.duration_ms, 250);
|
||||
let Some(NodeKind::Page { flows, .. }) = doc.node(doc.pages[0]).map(|p| &p.kind) else { panic!("no page") };
|
||||
assert_eq!((flows[0].name.as_str(), flows[0].start), ("Main flow", screen.id));
|
||||
// The sticky note comes in as a frame, with a warning.
|
||||
assert!(matches!(named(&doc, "Note").kind, NodeKind::Frame));
|
||||
assert!(matches!(named(doc, "Note").kind, NodeKind::Frame));
|
||||
assert!(warnings.iter().any(|w| w.contains("sticky layers import as frames")), "{warnings:?}");
|
||||
}
|
||||
|
||||
@@ -758,6 +777,23 @@ fn a_data_blob_past_the_blobs_stays_a_placeholder() {
|
||||
assert!(warnings.iter().any(|w| w.contains("isn't in the file")), "{warnings:?}");
|
||||
}
|
||||
|
||||
/// A file can point any number of image hashes at one large blob; each used to get its own copy
|
||||
/// of it (#1059).
|
||||
#[test]
|
||||
fn hashes_naming_one_blob_share_one_copy_of_it() {
|
||||
let image = |hash: u8, blob: u64| o(vec![("image", o(vec![("hash", Kv::Bytes(vec![hash; 20])), ("dataBlob", u(blob))]))]);
|
||||
let msg = o(vec![
|
||||
("blobs", Kv::Array(vec![o(vec![("bytes", Kv::Bytes(png()))]), o(vec![("bytes", Kv::Bytes(vec![7; 16]))])])),
|
||||
("nodeChanges", Kv::Array((1..=50).map(|h| image(h, 0)).chain([image(99, 1), image(98, 2)]).collect())),
|
||||
]);
|
||||
let copies = embedded_copies(&msg);
|
||||
assert_eq!(copies.len(), 51, "the hash naming no blob is left out");
|
||||
let first = &copies[&"01".repeat(20)];
|
||||
assert_eq!(**first, *png());
|
||||
assert_eq!(copies.values().filter(|c| Arc::ptr_eq(c, first)).count(), 50);
|
||||
assert_eq!(*copies[&"63".repeat(20)], [7; 16]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_files_are_not_fig() {
|
||||
assert!(try_import(b"{\"document\": {}}", "x").is_none());
|
||||
|
||||
Reference in New Issue
Block a user