diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 513800e3..71c6da39 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -781,6 +781,23 @@ impl Document { self.paste_nodes(&src_nodes, &map, id, parent, index) } + /// Deep-copy each of `ids`' subtrees to the end of `parent`, in order, returning the new root + /// ids. One id map covers every copy, so references between the copied subtrees (a navigation + /// from one copied frame to another) point at the copies. + pub fn duplicate_all_into(&mut self, ids: &[NodeId], parent: NodeId) -> DocResult> { + let mut map = BTreeMap::new(); + let mut trees = Vec::with_capacity(ids.len()); + for &id in ids { + let src_nodes: Vec = self.subtree(id).into_iter().filter_map(|i| self.node(i).cloned()).collect(); + for n in &src_nodes { + let nid = self.alloc_id(); + map.insert(n.id, nid); + } + trees.push((id, src_nodes)); + } + trees.into_iter().map(|(id, src_nodes)| self.paste_nodes(&src_nodes, &map, id, parent, None)).collect() + } + /// Insert copies of `nodes` (a subtree rooted at `root`, from this or another document) with ids /// remapped by `map`. Interactions, instance references and pattern sources pointing inside the /// subtree are remapped too. diff --git a/crates/engine/src/cmd/page.rs b/crates/engine/src/cmd/page.rs index 52531aa3..08e65b0b 100644 --- a/crates/engine/src/cmd/page.rs +++ b/crates/engine/src/cmd/page.rs @@ -97,11 +97,13 @@ pub fn specs() -> Vec { let doc = s.doc_mut()?; let name = format!("Copy of {}", doc.get(src)?.name); let np = doc.add_page(&name); - // Each layer and its copy (also those in sections), so flows can follow their start frames. + // One copy with one id map, so navigation between the copied frames points at the copies; + // then each layer and its copy (also those in sections), so flows can follow their start frames. + let children = doc.get(src)?.children.clone(); + let copied = doc.duplicate_all_into(&children, np)?; let mut copies = Vec::new(); - for c in doc.get(src)?.children.clone() { - let copy = doc.duplicate_into(c, np, None)?; - copies.extend(doc.subtree(c).into_iter().zip(doc.subtree(copy))); + for (c, copy) in children.iter().zip(&copied) { + copies.extend(doc.subtree(*c).into_iter().zip(doc.subtree(*copy))); } // The copy keeps the page's colour, flows (pointing at the copied frames), prototype settings and guides. if let NodeKind::Page { background, flows, prototype, guides } = doc.get(src)?.kind.clone() diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index a462fc0a..561699e1 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1588,6 +1588,8 @@ mod open_issues_tests; #[cfg(test)] mod outline_options_tests; #[cfg(test)] +mod page_copy_navigation_tests; +#[cfg(test)] mod page_target_tests; #[cfg(test)] mod paint_reorder_tests; diff --git a/crates/engine/src/page_copy_navigation_tests.rs b/crates/engine/src/page_copy_navigation_tests.rs new file mode 100644 index 00000000..ddbc956a --- /dev/null +++ b/crates/engine/src/page_copy_navigation_tests.rs @@ -0,0 +1,48 @@ +//! Duplicating a page copies its prototype connections: navigation between two frames that are both +//! on the page leads between the copies, while a destination outside the copied contents is kept. + +use figcraft_doc::NodeId; +use serde_json::{Value, json}; + +use crate::Session; +use crate::params::id_of; + +fn ex(s: &mut Session, cmd: &str, p: Value) -> Value { + s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}")) +} + +fn frame(s: &mut Session, x: f64) -> NodeId { + id_of(&ex(s, "frame.create", json!({"x": x, "y": 0, "width": 100, "height": 100}))["id"]).unwrap() +} + +fn dest(s: &Session, id: NodeId) -> NodeId { + s.doc().unwrap().node(id).unwrap().interactions[0].actions[0].dest().unwrap() +} + +#[test] +fn page_copy_navigation_uses_the_copied_destination() { + let mut s = Session::new(); + let first = frame(&mut s, 0.0); + let second = frame(&mut s, 200.0); + ex(&mut s, "proto.interaction.add", json!({"id": first, "action": {"type": "navigate", "dest": second}})); + let copy = id_of(&ex(&mut s, "page.duplicate", json!({}))["id"]).unwrap(); + let children = s.doc().unwrap().node(copy).unwrap().children.clone(); + assert_eq!(children.len(), 2); + assert_eq!(dest(&s, children[0]), children[1]); + let visited = Default::default(); + assert_eq!(crate::proto_run::navigate_target(s.doc().unwrap(), dest(&s, children[0]), &visited), Some(children[1])); + assert_eq!(dest(&s, first), second, "the original keeps its destination"); +} + +#[test] +fn frame_copy_keeps_an_outside_destination() { + let mut s = Session::new(); + let first = frame(&mut s, 0.0); + let second = frame(&mut s, 200.0); + ex(&mut s, "proto.interaction.add", json!({"id": first, "action": {"type": "navigate", "dest": second}})); + ex(&mut s, "select.set", json!({"ids": [first]})); + ex(&mut s, "edit.duplicate", json!({})); + let copy = s.selection()[0]; + assert_ne!(copy, first); + assert_eq!(dest(&s, copy), second); +}