From 30dceb9482afc4aaafcbb7b457e54dd1ff58eed7 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 02:32:07 +0700 Subject: [PATCH] M12: Boards: copies and pastes of joined objects stay joined --- crates/doc/src/board.rs | 22 ++++++++++++++++++++++ crates/doc/src/lib.rs | 4 ++++ crates/engine/src/board.rs | 12 ++++++++++++ crates/engine/src/board_tests.rs | 29 +++++++++++++++++++++++++++++ crates/engine/src/cmd/edit.rs | 4 ++++ 5 files changed, 71 insertions(+) diff --git a/crates/doc/src/board.rs b/crates/doc/src/board.rs index 0cade8ab..28ed0c1d 100644 --- a/crates/doc/src/board.rs +++ b/crates/doc/src/board.rs @@ -79,6 +79,28 @@ impl BoardItem { } } + /// Point the layers this object refers to (a connector's ends, a label's connector) at their + /// copies in `map`; the others stay. + pub fn remap(&mut self, map: &std::collections::BTreeMap) { + match self { + BoardItem::Connector(c) => { + for end in [&mut c.from, &mut c.to] { + if let End::Node { node, .. } = end + && let Some(m) = map.get(node) + { + *node = *m; + } + } + } + BoardItem::Label { connector } => { + if let Some(m) = map.get(connector) { + *connector = *m; + } + } + _ => {} + } + } + pub fn kind_id(&self) -> &'static str { match self { BoardItem::Sticky { .. } => "sticky", diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index fea6ac66..3097181c 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -841,6 +841,10 @@ impl Document { { *component = *m; } + // A connector (or label) copied with what it joins joins the copies. + if let Some(b) = c.board.as_mut() { + b.remap(map); + } // A pattern tiling a layer of the copy tiles the copy's layer. for p in c.fills.iter_mut().chain(c.strokes.iter_mut()) { if let figcraft_color::PaintKind::Pattern(pt) = &mut p.kind diff --git a/crates/engine/src/board.rs b/crates/engine/src/board.rs index 0c6fd6dd..f205a420 100644 --- a/crates/engine/src/board.rs +++ b/crates/engine/src/board.rs @@ -778,6 +778,18 @@ fn set_font_size(td: &mut TextData, size: f64) { } } +/// After layers were copied one by one (`map`: each original's copy), the copied connectors and +/// labels among `copies` refer to the copies of what they referred to. +pub(crate) fn rejoin(doc: &mut Document, copies: &[NodeId], map: &std::collections::BTreeMap) { + for id in copies { + if doc.node(*id).is_some_and(|n| matches!(n.board, Some(BoardItem::Connector(_) | BoardItem::Label { .. }))) + && let Some(b) = doc.node_mut(*id).and_then(|n| n.board.as_mut()) + { + b.remap(map); + } + } +} + // --------------------------------------------------------------------------------------------- // Derived data (the normalize pass). diff --git a/crates/engine/src/board_tests.rs b/crates/engine/src/board_tests.rs index f7a57db8..6980d018 100644 --- a/crates/engine/src/board_tests.rs +++ b/crates/engine/src/board_tests.rs @@ -330,3 +330,32 @@ fn a_press_on_a_stickys_background_picks_and_drags_it() { assert_eq!(node(&s, a).transform.translation().y, 300.0); assert!(near(drawn(&s, c)[0], Point::new(200.0, 400.0)), "the connector followed the drag: {:?}", drawn(&s, c)); } + +#[test] +fn a_copy_of_joined_objects_joins_the_copies() { + let mut s = board(); + let a = id(&s.execute("board.sticky", &json!({"x": 0, "y": 0})).unwrap(), "id"); + let b = id(&s.execute("board.sticky", &json!({"x": 400, "y": 0})).unwrap(), "id"); + let c = id(&s.execute("board.connector", &json!({"from": a, "to": b, "label": "x"})).unwrap(), "id"); + let label = crate::board::label_of(s.doc().unwrap(), c).unwrap(); + s.execute("edit.duplicate", &json!({"ids": [a, b, c, label]})).unwrap(); + let copies = s.selection(); + assert_eq!(copies.len(), 4, "{copies:?}"); + let k = copies.iter().find_map(|i| match node(&s, *i).board { + Some(BoardItem::Connector(k)) => Some(k), + _ => None, + }); + let k = k.unwrap(); + assert!(copies.contains(&k.from.node().unwrap()) && copies.contains(&k.to.node().unwrap()), "{k:?} {copies:?}"); + // Copy and paste too. + s.execute("select.set", &json!({"ids": [a, b, c]})).unwrap(); + s.execute("edit.copy", &json!({})).unwrap(); + s.execute("edit.paste", &json!({})).unwrap(); + let pasted = s.selection(); + let k = pasted.iter().find_map(|i| match node(&s, *i).board { + Some(BoardItem::Connector(k)) => Some(k), + _ => None, + }); + let k = k.unwrap(); + assert!(pasted.contains(&k.from.node().unwrap()) && pasted.contains(&k.to.node().unwrap()), "{k:?} {pasted:?}"); +} diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index f33b0612..af237b87 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -494,6 +494,8 @@ fn duplicate(s: &mut Session, p: &Value) -> Result { } out.push(nid); } + // Board connectors copied with what they join join the copies. + crate::board::rejoin(doc, &out, &ids.iter().copied().zip(out.iter().copied()).collect()); s.set_selection(out.clone()); s.ds_mut()?.last_duplicate = Some(Duplicated { sources: ids, copies: out.clone() }); Ok(json!({"ids": out})) @@ -675,6 +677,8 @@ pub fn paste_into(s: &mut Session, parent: NodeId, at: Option<(f64, f64)>) -> Re out.push(doc.paste_nodes(&nodes, &map, *root, into, None)?); all.extend(map); } + // Board connectors pasted with what they join join the pasted copies. + crate::board::rejoin(doc, &out, &all); if let Some(src) = foreign { crate::library::adopt_pasted(doc, src, clip.source_path.as_deref(), &all)?; }