mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 18:36:44 -06:00
A .fig import now links the layers of an instance to the layers of the component they came from by the ids the file stores, instead of leaving it to the engine to guess from names and places. Renamed slots and layers, siblings that share a name and swapped mains keep their source link and the override they carry. Components that live only on the file's hidden internal pages (cached copies of library components) are kept in the hidden library store when a visible layer uses them, so their instances stay instances. The read-only guard for library copies now compares the copies themselves rather than the dirty set, which an import leaves full until the first edit. Rebasing an instance (after detaching an outer instance or restoring a main) follows the source chain inside the right enclosing instance, so contextual links survive it. The last change of a GUID wins and a removed change deletes it. Ordinary file JSON never reads the private provenance. Importing such a file into an open document, or pasting its layers into another one, copies the cached components along (import) or leaves them behind as a missing main (paste), so no link points at a layer of the target.
245 lines
11 KiB
Rust
245 lines
11 KiB
Rust
//! Contextual import sources survive rebasing; detached and swapped subtrees use their current mains.
|
|
|
|
use super::*;
|
|
|
|
fn add(doc: &mut Document, parent: NodeId, kind: NodeKind, name: &str) -> NodeId {
|
|
doc.create(parent, kind, name, 0.0, 0.0, 20.0, 20.0).unwrap()
|
|
}
|
|
|
|
fn component(doc: &mut Document, parent: NodeId, name: &str) -> NodeId {
|
|
add(doc, parent, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, name)
|
|
}
|
|
|
|
fn mirror(doc: &mut Document, source: NodeId, parent: NodeId) -> NodeId {
|
|
let mut n = doc.node(source).unwrap().clone();
|
|
let children = std::mem::take(&mut n.children);
|
|
n.id = doc.alloc_id();
|
|
n.parent = None;
|
|
n.source = Some(source);
|
|
let id = n.id;
|
|
doc.insert(n, parent, None).unwrap();
|
|
for child in children {
|
|
mirror(doc, child, id);
|
|
}
|
|
id
|
|
}
|
|
|
|
fn instance(doc: &mut Document, main: NodeId, parent: NodeId, name: &str) -> NodeId {
|
|
let mut n = doc.node(main).unwrap().clone();
|
|
let children = std::mem::take(&mut n.children);
|
|
n.id = doc.alloc_id();
|
|
n.kind = NodeKind::Instance { component: main, props: BTreeMap::new() };
|
|
n.name = name.into();
|
|
n.parent = None;
|
|
n.source = None;
|
|
let id = n.id;
|
|
doc.insert(n, parent, None).unwrap();
|
|
for child in children {
|
|
mirror(doc, child, id);
|
|
}
|
|
id
|
|
}
|
|
|
|
struct Scene {
|
|
doc: Document,
|
|
inner: NodeId,
|
|
base: NodeId,
|
|
slot: NodeId,
|
|
context: NodeId,
|
|
root: NodeId,
|
|
nested: NodeId,
|
|
child: NodeId,
|
|
}
|
|
|
|
fn scene() -> Scene {
|
|
let mut doc = Document::new("Source context");
|
|
let page = doc.first_page().unwrap();
|
|
let inner = component(&mut doc, page, "Inner");
|
|
let base = add(&mut doc, inner, NodeKind::Rectangle, "Body");
|
|
doc.node_mut(base).unwrap().transform = Affine::translate((2.0, 0.0));
|
|
let outer = component(&mut doc, page, "Outer");
|
|
let slot = instance(&mut doc, inner, outer, "Nested");
|
|
doc.node_mut(slot).unwrap().source = Some(inner);
|
|
let context = doc.node(slot).unwrap().children[0];
|
|
doc.node_mut(context).unwrap().transform = Affine::translate((3.0, 0.0));
|
|
doc.node_mut(context).unwrap().opacity = 0.3;
|
|
let root = instance(&mut doc, outer, page, "Outer copy");
|
|
let nested = doc.node(root).unwrap().children[0];
|
|
let child = doc.node(nested).unwrap().children[0];
|
|
doc.node_mut(child).unwrap().transform = Affine::translate((2.0, 0.0));
|
|
doc.node_mut(child).unwrap().opacity = 0.2;
|
|
Scene { doc, inner, base, slot, context, root, nested, child }
|
|
}
|
|
|
|
fn both(s: &mut Scene) {
|
|
rebase_instance(&mut s.doc, s.nested);
|
|
rebase_instance(&mut s.doc, s.root);
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_default_only_sources_promote_to_the_actual_outer_context() {
|
|
let mut s = scene();
|
|
s.doc.node_mut(s.nested).unwrap().source = None;
|
|
s.doc.node_mut(s.child).unwrap().source = None;
|
|
both(&mut s);
|
|
assert_eq!(s.doc.node(s.nested).unwrap().source, Some(s.slot));
|
|
assert_eq!(s.doc.node(s.child).unwrap().source, Some(s.context));
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_rejects_a_context_source_from_another_nested_slot() {
|
|
let mut s = scene();
|
|
let outer = s.doc.node(s.slot).unwrap().parent.unwrap();
|
|
let sibling = instance(&mut s.doc, s.inner, outer, "Other nested slot");
|
|
s.doc.node_mut(sibling).unwrap().source = Some(s.inner);
|
|
let sibling_child = s.doc.node(sibling).unwrap().children[0];
|
|
mirror(&mut s.doc, sibling, s.root);
|
|
s.doc.node_mut(s.child).unwrap().source = Some(sibling_child);
|
|
both(&mut s);
|
|
assert_eq!(s.doc.node(s.nested).unwrap().source, Some(s.slot));
|
|
assert_eq!(s.doc.node(s.child).unwrap().source, Some(s.context));
|
|
assert_eq!(s.doc.node(sibling_child).unwrap().transform.translation().x, 2.0);
|
|
assert_eq!(s.doc.node(s.context).unwrap().transform.translation().x, 3.0);
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_does_not_mark_inherited_context_values_as_local_overrides() {
|
|
let mut s = scene();
|
|
s.doc.node_mut(s.child).unwrap().opacity = 0.3;
|
|
both(&mut s);
|
|
assert_eq!(s.doc.node(s.child).unwrap().source, Some(s.context));
|
|
assert!(!s.doc.node(s.child).unwrap().overrides.contains("opacity"));
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_preserves_multilevel_context_without_shortening_source_chains() {
|
|
let mut doc = Document::new("Nested source contexts");
|
|
let page = doc.first_page().unwrap();
|
|
let inner = component(&mut doc, page, "Inner");
|
|
let base = add(&mut doc, inner, NodeKind::Rectangle, "Body");
|
|
doc.node_mut(base).unwrap().transform = Affine::translate((2.0, 0.0));
|
|
let wrapper = component(&mut doc, page, "Wrapper");
|
|
let default_slot = instance(&mut doc, inner, wrapper, "Nested");
|
|
doc.node_mut(default_slot).unwrap().source = Some(inner);
|
|
let outer = component(&mut doc, page, "Outer");
|
|
let wrapper_slot = instance(&mut doc, wrapper, outer, "Wrapper slot");
|
|
doc.node_mut(wrapper_slot).unwrap().source = Some(wrapper);
|
|
let source_slot = doc.node(wrapper_slot).unwrap().children[0];
|
|
let context = doc.node(source_slot).unwrap().children[0];
|
|
doc.node_mut(context).unwrap().transform = Affine::translate((3.0, 0.0));
|
|
let root = instance(&mut doc, outer, page, "Outer copy");
|
|
let wrapper_copy = doc.node(root).unwrap().children[0];
|
|
let nested = doc.node(wrapper_copy).unwrap().children[0];
|
|
let child = doc.node(nested).unwrap().children[0];
|
|
doc.node_mut(nested).unwrap().name = "Renamed nested".into();
|
|
doc.node_mut(child).unwrap().transform = Affine::translate((2.0, 0.0));
|
|
for id in [nested, wrapper_copy, root] {
|
|
rebase_instance(&mut doc, id);
|
|
assert_eq!(doc.node(nested).unwrap().source, Some(source_slot));
|
|
assert_eq!(doc.node(child).unwrap().source, Some(context));
|
|
}
|
|
assert_eq!(doc.node(context).unwrap().source, Some(doc.node(default_slot).unwrap().children[0]));
|
|
assert_eq!(doc.node(context).unwrap().transform.translation().x, 3.0);
|
|
assert_eq!(doc.node(child).unwrap().transform.translation().x, 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_preserves_placed_frame_to_instance_and_current_main_children() {
|
|
let mut doc = Document::new("Typed nested source");
|
|
let page = doc.first_page().unwrap();
|
|
let inner = component(&mut doc, page, "Inner");
|
|
let body = add(&mut doc, inner, NodeKind::Rectangle, "Body");
|
|
let outer = component(&mut doc, page, "Outer");
|
|
let slot = add(&mut doc, outer, NodeKind::Frame, "Empty slot");
|
|
let root = instance(&mut doc, outer, page, "Outer copy");
|
|
let nested = doc.node(root).unwrap().children[0];
|
|
doc.node_mut(nested).unwrap().kind = NodeKind::Instance { component: inner, props: BTreeMap::new() };
|
|
let child = mirror(&mut doc, body, nested);
|
|
rebase_instance(&mut doc, nested);
|
|
rebase_instance(&mut doc, root);
|
|
assert_eq!(doc.node(nested).unwrap().source, Some(slot));
|
|
assert_eq!(doc.node(child).unwrap().source, Some(body));
|
|
assert!(matches!(doc.node(nested).unwrap().kind, NodeKind::Instance { component, .. } if component == inner));
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_keeps_current_main_sources_and_component_delta_beneath_swap() {
|
|
let mut s = scene();
|
|
let page = s.doc.first_page().unwrap();
|
|
let replacement = component(&mut s.doc, page, "Replacement");
|
|
let body = add(&mut s.doc, replacement, NodeKind::Rectangle, "Body");
|
|
s.doc.node_mut(body).unwrap().transform = Affine::translate((7.0, 0.0));
|
|
s.doc.node_mut(s.nested).unwrap().kind = NodeKind::Instance { component: replacement, props: BTreeMap::new() };
|
|
s.doc.remove(s.child).unwrap();
|
|
s.child = mirror(&mut s.doc, body, s.nested);
|
|
s.doc.node_mut(s.child).unwrap().transform = Affine::translate((8.0, 0.0));
|
|
s.doc.node_mut(s.nested).unwrap().name = "Renamed swap".into();
|
|
rebase_instance(&mut s.doc, s.nested);
|
|
assert_eq!(s.doc.node(s.nested).unwrap().source, Some(s.slot));
|
|
assert!(s.doc.node(s.nested).unwrap().overrides.contains("component"));
|
|
rebase_instance(&mut s.doc, s.root);
|
|
assert_eq!(s.doc.node(s.nested).unwrap().source, Some(s.slot));
|
|
assert_eq!(s.doc.node(s.child).unwrap().source, Some(body));
|
|
assert!(s.doc.node(s.nested).unwrap().overrides.contains("component"));
|
|
assert_eq!(s.doc.node(s.child).unwrap().transform.translation().x, 8.0);
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_detached_nested_uses_own_main_and_keeps_inherited_geometry() {
|
|
let mut s = scene();
|
|
let page = s.doc.first_page().unwrap();
|
|
let detached = mirror(&mut s.doc, s.nested, page);
|
|
let child = s.doc.node(detached).unwrap().children[0];
|
|
// The old source references still exist, but their Outer context is absent here.
|
|
s.doc.node_mut(detached).unwrap().source = Some(s.slot);
|
|
s.doc.node_mut(child).unwrap().source = Some(s.context);
|
|
s.doc.node_mut(child).unwrap().transform = Affine::translate((3.0, 0.0));
|
|
rebase_instance(&mut s.doc, detached);
|
|
assert_eq!(s.doc.node(detached).unwrap().source, Some(s.inner));
|
|
assert_eq!(s.doc.node(child).unwrap().source, Some(s.base));
|
|
assert_eq!(s.doc.node(child).unwrap().transform.translation().x, 3.0);
|
|
assert_eq!(s.doc.node(s.base).unwrap().transform.translation().x, 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_does_not_preserve_existing_foreign_or_stale_sources() {
|
|
let mut s = scene();
|
|
let page = s.doc.first_page().unwrap();
|
|
let foreign = component(&mut s.doc, page, "Foreign");
|
|
let foreign_body = add(&mut s.doc, foreign, NodeKind::Rectangle, "Body");
|
|
s.doc.node_mut(s.child).unwrap().source = Some(foreign_body);
|
|
both(&mut s);
|
|
assert_ne!(s.doc.node(s.child).unwrap().source, Some(foreign_body));
|
|
assert_eq!(s.doc.node(s.child).unwrap().source, Some(s.context));
|
|
let mut s = scene();
|
|
let page = s.doc.first_page().unwrap();
|
|
let replacement = component(&mut s.doc, page, "Replacement");
|
|
let body = add(&mut s.doc, replacement, NodeKind::Rectangle, "Body");
|
|
s.doc.node_mut(s.nested).unwrap().kind = NodeKind::Instance { component: replacement, props: BTreeMap::new() };
|
|
s.doc.node_mut(s.child).unwrap().source = Some(s.context);
|
|
both(&mut s);
|
|
assert_ne!(s.doc.node(s.child).unwrap().source, Some(s.context), "the old default-main child is stale after a swap");
|
|
assert!(s.doc.node(s.child).unwrap().source.is_none() || s.doc.node(s.child).unwrap().source == Some(body));
|
|
}
|
|
|
|
#[test]
|
|
fn rebase_rejects_source_and_parent_cycles_with_bounded_lookups() {
|
|
let mut s = scene();
|
|
s.doc.node_mut(s.base).unwrap().source = Some(s.context);
|
|
both(&mut s);
|
|
assert_eq!(s.doc.node(s.child).unwrap().source, None);
|
|
let mut s = scene();
|
|
s.doc.node_mut(s.context).unwrap().parent = Some(s.context);
|
|
both(&mut s);
|
|
assert_ne!(s.doc.node(s.child).unwrap().source, Some(s.context));
|
|
let mut s = scene();
|
|
s.doc.node_mut(s.nested).unwrap().parent = Some(s.child);
|
|
s.doc.node_mut(s.child).unwrap().parent = Some(s.nested);
|
|
let sources = RebaseSources::new(&s.doc);
|
|
assert!(sources.owners(s.child).is_none());
|
|
let mut s = scene();
|
|
s.doc.node_mut(s.root).unwrap().kind = NodeKind::Instance { component: s.root, props: BTreeMap::new() };
|
|
rebase_instance(&mut s.doc, s.root);
|
|
assert_ne!(s.doc.node(s.root).unwrap().source, Some(s.root));
|
|
}
|