mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 21:06:50 -06:00
259 lines
12 KiB
Rust
259 lines
12 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_marks_a_nested_roots_own_mode_but_not_a_top_level_instances() {
|
|
let mut s = scene();
|
|
let modes = BTreeMap::from([("Tone".to_string(), "B".to_string())]);
|
|
s.doc.node_mut(s.nested).unwrap().var_modes = modes.clone();
|
|
both(&mut s);
|
|
assert!(s.doc.node(s.nested).unwrap().overrides.contains("modes"), "its slot in Outer sets no mode");
|
|
let page = s.doc.first_page().unwrap();
|
|
let detached = mirror(&mut s.doc, s.nested, page);
|
|
rebase_instance(&mut s.doc, detached);
|
|
let n = s.doc.node(detached).unwrap();
|
|
assert_eq!((&n.var_modes, n.overrides.contains("modes")), (&modes, false), "a top-level instance's modes are its own");
|
|
}
|
|
|
|
#[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));
|
|
}
|