Skip to main content

blockworx/edit/
clipboard.rs

1//! The clipboard family (`docs/op-emitter-playbook.md`, 10g): copy, cut,
2//! and the two pastes.
3//!
4//! The clipboard holds a **value snapshot** — every copied entity's source
5//! id and its creation values — never live references
6//! (`docs/collab-architecture.md` §9). Those source ids are what makes a
7//! cut-and-paste a *move*: the cut removes exactly them, so a paste that
8//! finds none of them here re-creates them under their own ids rather than
9//! building copies. Every other paste duplicates, with fresh ids and every
10//! internal reference remapped onto them.
11
12use ahash::{HashMap, HashMapExt, HashSet};
13use blockworx_doc::{
14    block_model::{Area, Asset, Block, BlockUpdate, Icon, Image, Pin, Route, RouteLabel, Text},
15    commit::CommitBuilder,
16    document::{Document, IndexedDocument},
17    geometry::{GridPoint, GridVec, PinSlot, ScreenRect, Waypoint},
18    hash::AssetHash,
19    id::{Allocator, AreaId, BlockId, Id, IdKind, ImageId, PinId, RouteId, RouteLabelId, TextId},
20    opcode::{Crud, OpCodes},
21};
22use blockworx_geom::Vec2;
23
24use serde::{Deserialize, Serialize};
25
26use crate::edit::assets::push_payload;
27use crate::edit::create::{first_free_slot, grown_to_fit, owner_pins};
28use crate::edit::delete::{Closure, Target, closure};
29use crate::edit::geometry::{Shape, artwork, shifted_icon};
30use crate::grid::{artwork_rect, grid_point, px_vec, screen_rect};
31
32use crate::path::Scope;
33use blockworx_store::doc::DocumentNonce;
34
35/// The copied closure as values: each entity's source id beside the init
36/// that rebuilds it, plus the artwork payloads the copies reference. Self
37/// contained — blocks carry their parent links as copied, pins their
38/// slots, wires their endpoints and corners — so a paste needs nothing of
39/// the document it came from.
40#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
41pub struct Snapshot {
42    pub blocks: Vec<(BlockId, Block)>,
43    pub pins: Vec<(PinId, Pin)>,
44    pub routes: Vec<(RouteId, Route)>,
45    pub labels: Vec<(RouteLabelId, RouteLabel)>,
46    pub texts: Vec<(TextId, Text)>,
47    pub areas: Vec<(AreaId, Area)>,
48    pub images: Vec<(ImageId, Image)>,
49    pub assets: Vec<Asset>,
50}
51
52impl Snapshot {
53    /// Every entity of `closure`, cloned whole as it stands, so a field
54    /// added to a kind travels on the clipboard without anyone
55    /// remembering to add it here.
56    fn of(doc: &Document, closure: &Closure) -> Self {
57        let blocks: Vec<(BlockId, Block)> = closure
58            .blocks
59            .iter()
60            .filter_map(|&id| Some((id, doc.block(&id)?.clone())))
61            .collect();
62        let images: Vec<(ImageId, Image)> = closure
63            .images
64            .iter()
65            .filter_map(|&id| Some((id, doc.image(&id)?.clone())))
66            .collect();
67        let referenced = blocks
68            .iter()
69            .map(|(_, block)| block.icon.asset)
70            .chain(images.iter().map(|(_, image)| image.asset));
71        Self {
72            pins: closure
73                .pins
74                .iter()
75                .filter_map(|&id| Some((id, doc.pin(&id)?.clone())))
76                .collect(),
77            routes: closure
78                .routes
79                .iter()
80                .filter_map(|&id| Some((id, doc.route(&id)?.clone())))
81                .collect(),
82            labels: closure
83                .labels
84                .iter()
85                .filter_map(|&id| Some((id, doc.route_label(&id)?.clone())))
86                .collect(),
87            texts: closure
88                .texts
89                .iter()
90                .filter_map(|&id| Some((id, doc.text(&id)?.clone())))
91                .collect(),
92            areas: closure
93                .areas
94                .iter()
95                .filter_map(|&id| Some((id, doc.area(&id)?.clone())))
96                .collect(),
97            assets: payloads(doc, referenced),
98            blocks,
99            images,
100        }
101    }
102
103    /// The top-left corner of what a paste would *drop*: the roots — the
104    /// entities whose owner did not travel with them. The nested structure
105    /// inside a copied block rides that block, so it never widens the box
106    /// the drop is aligned by.
107    pub fn origin(&self) -> Option<GridPoint> {
108        let carried: HashSet<BlockId> = self.blocks.iter().map(|(id, _)| *id).collect();
109        let root = |owner: &BlockId| !carried.contains(owner);
110        let corners = self
111            .blocks
112            .iter()
113            .filter(|(_, init)| root(&init.parent))
114            .map(|(_, init)| init.rect.top_left)
115            .chain(
116                self.pins
117                    .iter()
118                    .filter(|(_, init)| root(&init.owner))
119                    .map(|(_, init)| init.rect.top_left),
120            )
121            .chain(
122                self.texts
123                    .iter()
124                    .filter(|(_, init)| root(&init.owner))
125                    .map(|(_, init)| init.pos),
126            )
127            .chain(
128                self.areas
129                    .iter()
130                    .filter(|(_, init)| root(&init.owner))
131                    .map(|(_, init)| init.rect.top_left),
132            )
133            .chain(
134                self.images
135                    .iter()
136                    .filter(|(_, init)| root(&init.owner))
137                    .map(|(_, init)| grid_point(artwork_rect(init.rect).min)),
138            );
139        corners.reduce(|a, b| GridPoint {
140            x: a.x.min(b.x),
141            y: a.y.min(b.y),
142        })
143    }
144
145    pub fn is_empty(&self) -> bool {
146        self.blocks.is_empty()
147            && self.pins.is_empty()
148            && self.routes.is_empty()
149            && self.texts.is_empty()
150            && self.areas.is_empty()
151            && self.images.is_empty()
152    }
153}
154
155/// Which gesture made a payload, and — for a cut — the document it was
156/// taken out of.
157///
158/// A cut is the only payload whose ids a paste may keep, and only where it
159/// has come home: the ids in a snapshot from *another* document name
160/// entities that document minted, and re-creating them here would plant a
161/// foreign numbering in a document that never issued it. §5.1 of
162/// `docs/log-vs-snapshot.md` reversed D10 on the premise that every
163/// cross-document flow re-mints, and this is what keeps that true now that
164/// "deleted" and "never here" are one state.
165#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
166pub enum Origin {
167    Copy,
168    Cut { from: DocumentNonce },
169}
170
171/// The clipboard payload, versioned per variant: a payload from a newer
172/// build fails to deserialize here rather than being reinterpreted, and
173/// clipboard text that is not ours at all refuses the same way — which is
174/// what keeps an ordinary text paste from being read as a diagram.
175///
176/// The two live variants are the same snapshot under different *gestures*:
177/// a shape copy lands its roots where they are dropped, a pin copy slots
178/// its pins onto a boundary. Which one a payload is cannot be read off its
179/// contents — copying a lone boundary port yields a snapshot of exactly one
180/// pin either way — so the copy that made it says so here (the legacy's two
181/// clipboard tags, kept).
182#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
183pub enum Clipboard {
184    V2 { snapshot: Snapshot, origin: Origin },
185    PinsV2 { snapshot: Snapshot, origin: Origin },
186}
187
188impl Clipboard {
189    /// The OS clipboard carries text, so the payload travels as JSON.
190    /// `None` only where the payload cannot be written at all.
191    pub fn to_json(&self) -> Option<String> {
192        serde_json::to_string(self).ok()
193    }
194
195    /// `None` for anything that is not one of our payloads: unrelated
196    /// text, a truncated blob, or a version this build does not know.
197    pub fn from_json(text: &str) -> Option<Self> {
198        serde_json::from_str(text).ok()
199    }
200
201    pub fn snapshot(&self) -> &Snapshot {
202        match self {
203            Clipboard::V2 { snapshot, .. } | Clipboard::PinsV2 { snapshot, .. } => snapshot,
204        }
205    }
206
207    pub fn origin(&self) -> Origin {
208        match self {
209            Clipboard::V2 { origin, .. } | Clipboard::PinsV2 { origin, .. } => *origin,
210        }
211    }
212
213    /// The same payload read as a pin copy — what a pin selection's copy or
214    /// cut hands the OS clipboard.
215    pub fn into_pins(self) -> Self {
216        match self {
217            Clipboard::V2 { snapshot, origin } | Clipboard::PinsV2 { snapshot, origin } => {
218                Clipboard::PinsV2 { snapshot, origin }
219            }
220        }
221    }
222
223    /// Whether a paste of this payload slots pins onto a boundary rather
224    /// than dropping shapes at the paste target.
225    pub fn is_pin_paste(&self) -> bool {
226        matches!(self, Clipboard::PinsV2 { .. })
227    }
228}
229
230/// The bytes behind the hashes the copies reference, each carried once —
231/// a payload the document does not hold cannot travel, and its reference
232/// pastes as the "no artwork" zero it already is.
233fn payloads(doc: &Document, hashes: impl Iterator<Item = AssetHash>) -> Vec<Asset> {
234    let mut carried: Vec<AssetHash> = hashes.collect();
235    carried.sort_unstable();
236    carried.dedup();
237    carried
238        .iter()
239        .filter_map(|hash| doc.asset(hash).cloned())
240        .collect()
241}
242
243/// Inventory rows "Paste" / "Cut Selection"'s read half: the value
244/// snapshot of a selection, taken from the same closure a delete cascades
245/// over (`delete::Closure`) — so a copied block brings its subtree, the
246/// wires landing on any of their pins with their labels, and the
247/// annotations they own.
248pub fn copy(indexed: &IndexedDocument<'_>, targets: &[Target]) -> Clipboard {
249    Clipboard::V2 {
250        snapshot: Snapshot::of(indexed.doc, &Closure::of(indexed, targets)),
251        origin: Origin::Copy,
252    }
253}
254
255/// Inventory row "Cut Selection / Cut Pins": the snapshot and the cascade
256/// delete in one commit (legacy `Drawing::cut_selection` / `cut_pins`,
257/// `src/widget/clipboard.rs:555,564`).
258///
259/// What the delete declines — a pin whose owner froze its interface — the
260/// copy declines too, which is why this walks the delete's own closure
261/// rather than [`copy`]'s. A cut that left something on the clipboard it
262/// also left in the document would make the next paste read a mix of held
263/// and departed sources, and duplicate where the user asked for a move.
264pub fn cut(
265    indexed: &IndexedDocument<'_>,
266    targets: &[Target],
267    from: DocumentNonce,
268    builder: &mut CommitBuilder,
269) -> Clipboard {
270    let cascade = closure(indexed, targets);
271    let snapshot = Snapshot::of(indexed.doc, &cascade);
272    cascade.push_deletes(builder);
273    Clipboard::V2 {
274        snapshot,
275        origin: Origin::Cut { from },
276    }
277}
278
279/// Where a paste lands: the scope its roots join, and the whole-cell delta
280/// the drop applies to them.
281#[derive(Clone, Copy, Debug)]
282pub struct PasteTarget {
283    pub scope: Scope,
284    pub offset: GridVec,
285}
286
287/// One paste gesture — the payload, the session document it is landing
288/// in, and where in that document it lands. Bundled because the emitter's
289/// other three parameters are the document, the id source, and the
290/// builder.
291#[derive(Clone, Copy, Debug)]
292pub struct Paste<'a> {
293    pub clip: &'a Clipboard,
294    /// The document being pasted into, which is what a cut payload
295    /// compares itself against ([`Origin`]).
296    pub into: DocumentNonce,
297    pub target: PasteTarget,
298}
299
300/// The Paste Pins gesture: copied pins, and the block whose boundary they
301/// join.
302#[derive(Clone, Copy, Debug)]
303pub struct PinPaste<'a> {
304    pub pins: &'a [(PinId, Pin)],
305    pub owner: Scope,
306}
307
308/// Which paste this is. Two questions, and a move needs both answered:
309/// the payload must be the *cut this document made* ([`Origin`]), and
310/// every source it names must still be gone from it — a source standing
311/// again is a second paste of that cut, and duplicates.
312///
313/// The second question alone used to answer both, because a cut left
314/// tombstones: "here but dead" meant the cut this paste completes, and
315/// "not here at all" meant a payload from elsewhere. Those are one state
316/// now (D22), so the payload carries which document it came out of and
317/// the paste believes it or mints.
318///
319/// A partial state (some gone, some standing) duplicates **wholesale**,
320/// never a mixed move: half a snapshot re-created and half re-minted would
321/// wire a copied route across one original endpoint and one duplicate.
322#[derive(Clone, Copy, PartialEq, Eq, Debug)]
323enum Mode {
324    Move,
325    Duplicate,
326}
327
328fn mode(doc: &Document, clip: &Clipboard, into: DocumentNonce) -> Mode {
329    let Origin::Cut { from } = clip.origin() else {
330        return Mode::Duplicate;
331    };
332    let snapshot = clip.snapshot();
333    let gone_from_here = from == into
334        && snapshot
335            .blocks
336            .iter()
337            .all(|(id, _)| doc.block(id).is_none())
338        && snapshot.pins.iter().all(|(id, _)| doc.pin(id).is_none())
339        && snapshot
340            .routes
341            .iter()
342            .all(|(id, _)| doc.route(id).is_none())
343        && snapshot
344            .labels
345            .iter()
346            .all(|(id, _)| doc.route_label(id).is_none())
347        && snapshot.texts.iter().all(|(id, _)| doc.text(id).is_none())
348        && snapshot.areas.iter().all(|(id, _)| doc.area(id).is_none())
349        && snapshot
350            .images
351            .iter()
352            .all(|(id, _)| doc.image(id).is_none());
353    if gone_from_here {
354        Mode::Move
355    } else {
356        Mode::Duplicate
357    }
358}
359
360/// Where one copied entity lands.
361#[derive(Clone, Copy, PartialEq, Eq, Debug)]
362enum Landing {
363    /// Inside the structure that carried it — a nested interior travels
364    /// with its parent, so nothing of it shifts (legacy `Drawing::paste`,
365    /// `src/widget/clipboard.rs:403`).
366    Carried(BlockId),
367    /// At the target scope, as a root of the paste, riding the drop
368    /// offset.
369    Root,
370}
371
372impl Landing {
373    fn of(carried: Option<BlockId>) -> Self {
374        carried.map_or(Landing::Root, Landing::Carried)
375    }
376
377    fn owner(self, target: PasteTarget) -> Scope {
378        match self {
379            Landing::Carried(owner) => Scope::Block(owner),
380            Landing::Root => target.scope,
381        }
382    }
383
384    fn shift(self, target: PasteTarget) -> GridVec {
385        match self {
386            Landing::Carried(_) => GridVec::ZERO,
387            Landing::Root => target.offset,
388        }
389    }
390}
391
392fn translated(waypoints: &[Waypoint], shift: GridVec) -> Vec<Waypoint> {
393    waypoints
394        .iter()
395        .map(|wp| Waypoint {
396            pos: wp.pos + shift,
397            ..*wp
398        })
399        .collect()
400}
401
402fn shifted_artwork(rect: ScreenRect, delta: Vec2) -> ScreenRect {
403    screen_rect(artwork_rect(rect).translate(delta))
404}
405
406/// A copied icon at the paste offset; the zero icon is no artwork, so it
407/// has no box to carry.
408fn pasted_icon(icon: &Icon, delta: Vec2) -> Icon {
409    artwork(icon).map_or_else(Icon::default, |icon| shifted_icon(icon, delta))
410}
411
412/// The boundary a paste slots pins onto: the slots the owner's pins hold,
413/// plus the ones this gesture has handed out, and the growth that keeps
414/// the block tall enough for them — the 10c policy
415/// ([`first_free_slot`], [`grown_to_fit`]) driven by a whole group rather
416/// than one stamp. A block the document does not hold, or one whose
417/// interface is frozen, offers no slots at all.
418struct Boundary<'a> {
419    scope: Scope,
420    block: Option<&'a Block>,
421    occupied: HashSet<PinSlot>,
422    last: Option<PinSlot>,
423}
424
425impl<'a> Boundary<'a> {
426    fn of(indexed: &'a IndexedDocument<'a>, scope: Scope) -> Self {
427        Self {
428            scope,
429            block: indexed
430                .doc
431                .block(&scope.wire_id())
432                .filter(|block| !block.locked),
433            occupied: owner_pins(indexed, scope).map(|pin| pin.slot).collect(),
434            last: None,
435        }
436    }
437
438    fn take(&mut self) -> Option<PinSlot> {
439        self.block?;
440        let slot = first_free_slot(&self.occupied);
441        self.occupied.insert(slot);
442        self.last = Some(slot);
443        Some(slot)
444    }
445
446    /// The growth rider, once for the whole group: the search hands out
447    /// slots in ascending order, so the last one taken is the lowest the
448    /// block has to reach.
449    fn push_growth(&self, builder: &mut CommitBuilder) {
450        let (Some(block), Some(slot)) = (self.block, self.last) else {
451            return;
452        };
453        if let Some(rect) = grown_to_fit(block, slot) {
454            builder.push(OpCodes::Block(
455                self.scope.wire_id(),
456                Crud::Update(BlockUpdate::Rect(rect)),
457            ));
458        }
459    }
460}
461
462/// Inventory row "Paste": insert a snapshot at the target scope, as a
463/// move or as a duplicate ([`Mode`]). Returns the roots — what the caller
464/// selects — leaving the nested structure that travelled inside them
465/// unnamed.
466pub fn paste(
467    indexed: &IndexedDocument<'_>,
468    paste: Paste<'_>,
469    fresh: &mut Allocator,
470    builder: &mut CommitBuilder,
471) -> Vec<Shape> {
472    let naming = match mode(indexed.doc, paste.clip, paste.into) {
473        Mode::Move => Naming::Keep,
474        Mode::Duplicate => Naming::Mint(fresh),
475    };
476    insert(indexed, paste, naming, builder)
477}
478
479/// How a paste names what it inserts. Keeping the ids is what makes a
480/// cut and its paste one displacement rather than a deletion and an
481/// unrelated arrival: the entity that comes back is the entity that left,
482/// so an undo of either half addresses the same thing. Minting is the
483/// copying half, and every reference inside the snapshot is remapped onto
484/// the mint.
485enum Naming<'a> {
486    Keep,
487    Mint(&'a mut Allocator),
488}
489
490impl Naming<'_> {
491    fn name<K: IdKind>(&mut self, source: Id<K>) -> Id<K> {
492        match self {
493            Naming::Keep => source,
494            Naming::Mint(fresh) => fresh.mint(),
495        }
496    }
497
498    /// The endpoint a pasted wire lands on: the pin this paste made from
499    /// the one it was drawn to, or — where the ids are kept — the pin it
500    /// already names, which the document may still hold outside the
501    /// snapshot. `None` leaves the wire nothing to land on.
502    fn endpoint(&self, doc: &Document, pasted: Option<PinId>, source: PinId) -> Option<PinId> {
503        match self {
504            Naming::Keep => pasted.or_else(|| doc.pin(&source).map(|_| source)),
505            Naming::Mint(_) => pasted,
506        }
507    }
508
509    /// The slot a pasted pin takes. It keeps the one it carried unless it
510    /// is a *copy* joining the target boundary, which has to find room
511    /// among the pins already there.
512    fn slot(
513        &self,
514        landing: Landing,
515        carried: PinSlot,
516        boundary: &mut Boundary<'_>,
517    ) -> Option<PinSlot> {
518        match (self, landing) {
519            (Naming::Mint(_), Landing::Root) => boundary.take(),
520            _ => Some(carried),
521        }
522    }
523}
524
525/// One paste, both halves ([`Naming`]): every entity re-created at its
526/// landing, the roots re-pointed at the target scope and shifted by the
527/// drop (legacy `Drawing::paste`, `src/widget/clipboard.rs:315`).
528fn insert(
529    indexed: &IndexedDocument<'_>,
530    paste: Paste<'_>,
531    mut naming: Naming<'_>,
532    builder: &mut CommitBuilder,
533) -> Vec<Shape> {
534    let Paste { clip, target, .. } = paste;
535    let snapshot = clip.snapshot();
536    let doc = indexed.doc;
537    let mut roots = Vec::new();
538
539    for asset in &snapshot.assets {
540        push_payload(doc, asset, builder);
541    }
542
543    // Ancestors first (the closure's own order), so a child's parent is
544    // always named by the time the child needs it.
545    let mut blocks: HashMap<BlockId, BlockId> = HashMap::new();
546    for (src, init) in &snapshot.blocks {
547        let landing = Landing::of(blocks.get(&init.parent).copied());
548        let shift = landing.shift(target);
549        let id = naming.name(*src);
550        blocks.insert(*src, id);
551        if landing == Landing::Root {
552            roots.push(Shape::Block(id));
553        }
554        builder.push(OpCodes::Block(
555            id,
556            Crud::Create(Block {
557                parent: landing.owner(target).wire_id(),
558                rect: init.rect.translate(shift),
559                icon: pasted_icon(&init.icon, px_vec(shift)),
560                ..init.clone()
561            }),
562        ));
563    }
564
565    let mut boundary = Boundary::of(indexed, target.scope);
566    let mut pins: HashMap<PinId, PinId> = HashMap::new();
567    for (src, init) in &snapshot.pins {
568        let landing = Landing::of(blocks.get(&init.owner).copied());
569        let Some(slot) = naming.slot(landing, init.slot, &mut boundary) else {
570            continue;
571        };
572        let id = naming.name(*src);
573        pins.insert(*src, id);
574        if landing == Landing::Root {
575            roots.push(Shape::Port(id));
576        }
577        builder.push(OpCodes::Pin(
578            id,
579            Crud::Create(Pin {
580                owner: landing.owner(target).wire_id(),
581                rect: init.rect.translate(landing.shift(target)),
582                slot,
583                ..init.clone()
584            }),
585        ));
586    }
587    boundary.push_growth(builder);
588
589    let mut routes: HashMap<RouteId, RouteId> = HashMap::new();
590    for (src, init) in &snapshot.routes {
591        // An endpoint the paste did not carry leaves the wire nothing to
592        // land on, so the wire is dropped and its labels with it (legacy
593        // `Drawing::paste`, `src/widget/clipboard.rs:493`).
594        let (Some(from), Some(to)) = (
595            naming.endpoint(doc, pins.get(&init.from).copied(), init.from),
596            naming.endpoint(doc, pins.get(&init.to).copied(), init.to),
597        ) else {
598            continue;
599        };
600        let landing = Landing::of(blocks.get(&init.owner).copied());
601        let id = naming.name(*src);
602        routes.insert(*src, id);
603        builder.push(OpCodes::Route(
604            id,
605            Crud::Create(Route {
606                owner: landing.owner(target).wire_id(),
607                from,
608                to,
609                waypoints: translated(&init.waypoints, landing.shift(target)),
610                ..init.clone()
611            }),
612        ));
613    }
614    report_omissions(snapshot, pins.len(), routes.len());
615
616    for (src, init) in &snapshot.labels {
617        let Some(owner) = routes.get(&init.owner).copied() else {
618            continue;
619        };
620        builder.push(OpCodes::RouteLabel(
621            naming.name(*src),
622            Crud::Create(RouteLabel { owner, ..*init }),
623        ));
624    }
625
626    for (src, init) in &snapshot.texts {
627        let landing = Landing::of(blocks.get(&init.owner).copied());
628        let id = naming.name(*src);
629        if landing == Landing::Root {
630            roots.push(Shape::Text(id));
631        }
632        builder.push(OpCodes::Text(
633            id,
634            Crud::Create(Text {
635                owner: landing.owner(target).wire_id(),
636                pos: init.pos + landing.shift(target),
637                ..init.clone()
638            }),
639        ));
640    }
641
642    for (src, init) in &snapshot.areas {
643        let landing = Landing::of(blocks.get(&init.owner).copied());
644        let id = naming.name(*src);
645        if landing == Landing::Root {
646            roots.push(Shape::Area(id));
647        }
648        builder.push(OpCodes::Area(
649            id,
650            Crud::Create(Area {
651                owner: landing.owner(target).wire_id(),
652                rect: init.rect.translate(landing.shift(target)),
653                ..init.clone()
654            }),
655        ));
656    }
657
658    for (src, init) in &snapshot.images {
659        let landing = Landing::of(blocks.get(&init.owner).copied());
660        let id = naming.name(*src);
661        if landing == Landing::Root {
662            roots.push(Shape::Image(id));
663        }
664        builder.push(OpCodes::Image(
665            id,
666            Crud::Create(Image {
667                owner: landing.owner(target).wire_id(),
668                rect: shifted_artwork(init.rect, px_vec(landing.shift(target))),
669                ..*init
670            }),
671        ));
672    }
673
674    roots
675}
676
677/// What a duplicate could not rebuild: a pin the target boundary had no
678/// room for, and a wire whose other endpoint the copy did not carry. Both
679/// are legitimate — there is nothing to land them on — but a paste that
680/// quietly delivers less than was copied is indistinguishable from one that
681/// lost it, so it says so.
682fn report_omissions(snapshot: &Snapshot, pins: usize, routes: usize) {
683    if pins == snapshot.pins.len() && routes == snapshot.routes.len() {
684        return;
685    }
686    tracing::warn!(
687        "paste landed {pins} of {} pins and {routes} of {} wires: the rest name a boundary \
688         or an endpoint that did not travel with them",
689        snapshot.pins.len(),
690        snapshot.routes.len(),
691    );
692}
693
694/// Inventory row "Paste Pins": copied pins join `owner`'s boundary, each
695/// at the next free slot, growing the block until they fit. One emitter
696/// for both legacy halves — `paste_pins_into_block` and
697/// `paste_pins_as_ports` (`src/widget/clipboard.rs:598,625`) differed only
698/// in which block they named, and the port/pin split died with
699/// `LineAnchor`. A frozen interface takes none of them.
700pub fn paste_pins(
701    indexed: &IndexedDocument<'_>,
702    onto: PinPaste<'_>,
703    fresh: &mut Allocator,
704    builder: &mut CommitBuilder,
705) -> Vec<PinId> {
706    let mut boundary = Boundary::of(indexed, onto.owner);
707    let mut pasted = Vec::new();
708    for (_, init) in onto.pins {
709        let Some(slot) = boundary.take() else {
710            break;
711        };
712        let id = fresh.mint();
713        pasted.push(id);
714        builder.push(OpCodes::Pin(
715            id,
716            Crud::Create(Pin {
717                owner: onto.owner.wire_id(),
718                slot,
719                ..init.clone()
720            }),
721        ));
722    }
723    boundary.push_growth(builder);
724    pasted
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730    use crate::edit::delete;
731    use crate::edit::harness::{
732        area_create, block_create, fold, image_create, pin_create, route_create,
733        route_label_create, seals_to_nothing, text_create, wired,
734    };
735    use crate::grid::GRID_SIZE;
736    use blockworx_doc::document::DocIndex;
737    use blockworx_doc::{
738        block_model::{ImageUpdate, LabelUpdate, PinUpdate, RouteUpdate},
739        document::Document,
740        fixtures::{area_id, block_id, image_id, pin_id, route_id, route_label_id, text_id},
741        geometry::{GridPoint, GridRect, GridSize, ScreenPoint, ScreenSize},
742        values::{PinDir, PinSide, Role},
743    };
744
745    fn rect(x: i32, y: i32, w: u32, h: u32) -> GridRect {
746        GridRect {
747            top_left: GridPoint { x, y },
748            size: GridSize { w, h },
749        }
750    }
751
752    fn slot(side: PinSide, offset: u32) -> PinSlot {
753        PinSlot { side, offset }
754    }
755
756    fn artwork_box(x: f32, y: f32) -> ScreenRect {
757        ScreenRect {
758            top_left: ScreenPoint {
759                x: x.into(),
760                y: y.into(),
761            },
762            size: ScreenSize {
763                w: 20.0.into(),
764                h: 10.0.into(),
765            },
766        }
767    }
768
769    fn svg(source: &str) -> Asset {
770        Asset::Svg(source.as_bytes().into())
771    }
772
773    fn empty() -> Document {
774        Document::default()
775    }
776
777    fn rename(id: u32, name: &str) -> OpCodes {
778        OpCodes::Block(
779            block_id(id),
780            Crud::Update(BlockUpdate::Title(LabelUpdate::Name(name.into()))),
781        )
782    }
783
784    fn resize(id: u32, to: GridRect) -> OpCodes {
785        OpCodes::Block(block_id(id), Crud::Update(BlockUpdate::Rect(to)))
786    }
787
788    fn reparent(child: u32, parent: u32) -> OpCodes {
789        OpCodes::Block(
790            block_id(child),
791            Crud::Update(BlockUpdate::Parent(block_id(parent))),
792        )
793    }
794
795    fn reslot(id: u32, to: PinSlot) -> OpCodes {
796        OpCodes::Pin(pin_id(id), Crud::Update(PinUpdate::Slot(to)))
797    }
798
799    fn rename_pin(id: u32, name: &str) -> OpCodes {
800        OpCodes::Pin(pin_id(id), Crud::Update(PinUpdate::Name(name.into())))
801    }
802
803    fn waypoints(id: u32, corners: &[(i32, i32)]) -> OpCodes {
804        OpCodes::Route(
805            route_id(id),
806            Crud::Update(RouteUpdate::Waypoints(
807                corners
808                    .iter()
809                    .map(|&(x, y)| Waypoint {
810                        pos: GridPoint { x, y },
811                        locked: true,
812                    })
813                    .collect(),
814            )),
815        )
816    }
817
818    /// Block 1 at top level with pins 3 (west) and 4 (east), wire 5
819    /// between them carrying label 20, text 7, area 8; block 2 nested
820    /// inside it with pins 10 and 11, wire 12 between them (with a
821    /// hand-placed corner), text 14, area 15 and image 9. Every name is
822    /// distinct, so a projection can name a wire's endpoints and prove a
823    /// remap.
824    fn scene() -> Document {
825        let doc = wired();
826        let mut builder = CommitBuilder::new("Furnished two levels");
827        builder.extend([
828            block_create(2),
829            reparent(2, 1),
830            pin_create(10, 2),
831            pin_create(11, 2),
832            route_create(12, 2, 10, 11),
833            text_create(14, 2),
834            area_create(15, 2),
835            image_create(9, 2),
836            route_label_create(20, 5),
837            route_label_create(21, 12),
838            rename(1, "outer"),
839            rename(2, "inner"),
840            resize(1, rect(0, 0, 10, 10)),
841            resize(2, rect(2, 2, 4, 4)),
842            reslot(3, slot(PinSide::West, 0)),
843            reslot(4, slot(PinSide::East, 0)),
844            reslot(10, slot(PinSide::West, 1)),
845            reslot(11, slot(PinSide::East, 1)),
846            waypoints(5, &[(3, 3)]),
847            waypoints(12, &[(4, 4)]),
848        ]);
849        let doc = fold(builder, &doc);
850        assert_eq!(
851            doc.block(&block_id(2))
852                .expect("the nested block exists")
853                .parent,
854            block_id(1),
855            "precondition: the scene really nests"
856        );
857        doc
858    }
859
860    /// What a subtree looks like with its ids taken away: the values, and
861    /// the structure named by the values (a wire by its endpoints' names,
862    /// a child by its title). Two subtrees compare equal exactly when one
863    /// is a faithful copy of the other.
864    #[derive(Debug, PartialEq)]
865    struct Projection {
866        title: String,
867        rect: GridRect,
868        locked: bool,
869        role: Role,
870        icon: Icon,
871        pins: Vec<(String, PinSlot, GridRect, PinDir)>,
872        routes: Vec<(String, String, String, Vec<Waypoint>)>,
873        texts: Vec<(String, GridPoint)>,
874        areas: Vec<(String, GridRect)>,
875        images: Vec<(AssetHash, ScreenRect)>,
876        children: Vec<Projection>,
877    }
878
879    fn pin_name(doc: &Document, id: PinId) -> String {
880        doc.pin(&id)
881            .map(|live| live.name.clone())
882            .unwrap_or_default()
883    }
884
885    fn project(indexed: &IndexedDocument<'_>, id: BlockId) -> Projection {
886        let block = indexed.doc.block(&id).expect("the projected block exists");
887        let entry = &indexed.index.blocks[&id];
888        let mut pins: Vec<(String, PinSlot, GridRect, PinDir)> = entry
889            .pins
890            .iter()
891            .filter_map(|id| indexed.doc.pin(id))
892            .map(|pin| (pin.name.clone(), pin.slot, pin.rect, pin.dir))
893            .collect();
894        pins.sort_by(|a, b| a.0.cmp(&b.0));
895        let mut routes: Vec<(String, String, String, Vec<Waypoint>)> = entry
896            .routes
897            .iter()
898            .filter_map(|id| indexed.doc.route(id))
899            .map(|route| {
900                (
901                    route.name.clone(),
902                    pin_name(indexed.doc, route.from),
903                    pin_name(indexed.doc, route.to),
904                    route.waypoints.clone(),
905                )
906            })
907            .collect();
908        routes.sort_by(|a, b| (&a.1, &a.2).cmp(&(&b.1, &b.2)));
909        let mut texts: Vec<(String, GridPoint)> = entry
910            .texts
911            .iter()
912            .filter_map(|id| indexed.doc.text(id))
913            .map(|text| (text.text.clone(), text.pos))
914            .collect();
915        texts.sort_by(|a, b| a.0.cmp(&b.0));
916        let mut areas: Vec<(String, GridRect)> = entry
917            .areas
918            .iter()
919            .filter_map(|id| indexed.doc.area(id))
920            .map(|area| (area.title.name.clone(), area.rect))
921            .collect();
922        areas.sort_by(|a, b| a.0.cmp(&b.0));
923        let mut images: Vec<(AssetHash, ScreenRect)> = entry
924            .images
925            .iter()
926            .filter_map(|id| indexed.doc.image(id))
927            .map(|image| (image.asset, image.rect))
928            .collect();
929        images.sort_by_key(|(asset, _)| *asset);
930        let mut children: Vec<Projection> = entry
931            .children
932            .iter()
933            .map(|&child| project(indexed, child))
934            .collect();
935        children.sort_by(|a, b| a.title.cmp(&b.title));
936        Projection {
937            title: block.title.name.clone(),
938            rect: block.rect,
939            locked: block.locked,
940            role: block.role,
941            icon: block.icon.clone(),
942            pins,
943            routes,
944            texts,
945            areas,
946            images,
947            children,
948        }
949    }
950
951    fn only_block(roots: &[Shape]) -> BlockId {
952        match roots {
953            [Shape::Block(id)] => *id,
954            other => unreachable!("expected exactly one pasted block, got {other:?}"),
955        }
956    }
957
958    /// The allocator a paste mints from: the *target* document's marks,
959    /// which is what the gesture hands the emitter in the editor.
960    fn ids(target: &Document) -> Allocator {
961        target.ids()
962    }
963
964    /// The session document these probes cut from and paste into — one
965    /// value for the whole module, so a probe that wants a *different*
966    /// document says so by minting one of its own.
967    fn home() -> DocumentNonce {
968        static HOME: std::sync::OnceLock<DocumentNonce> = std::sync::OnceLock::new();
969        *HOME.get_or_init(DocumentNonce::mint)
970    }
971
972    fn drop_at(scope: Scope, dx: i32, dy: i32) -> PasteTarget {
973        PasteTarget {
974            scope,
975            offset: GridVec::new(dx, dy),
976        }
977    }
978
979    /// The round trip the format exists for: a copied subtree pastes into
980    /// a document that has never seen it as the same subtree — new ids,
981    /// same everything else, shifted by the drop.
982    #[test]
983    fn a_copy_pastes_into_an_empty_document_as_the_same_subtree() {
984        let source = scene();
985        let mut index = DocIndex::default();
986        let clip = copy(&index.view(&source), &[delete::Target::Block(block_id(1))]);
987        let json = clip.to_json().expect("the payload serializes");
988        let clip = Clipboard::from_json(&json).expect("its own payload parses");
989
990        let target = empty();
991        let mut builder = CommitBuilder::new("Pasted a subtree");
992        let roots = paste(
993            &index.view(&target),
994            Paste {
995                clip: &clip,
996                into: home(),
997                target: drop_at(Scope::Root, 5, 7),
998            },
999            &mut ids(&target),
1000            &mut builder,
1001        );
1002        let target = fold(builder, &target);
1003
1004        let root = only_block(&roots);
1005        assert_eq!(
1006            root,
1007            block_id(1),
1008            "the ids are the empty target's own, counted from 1 — that they \
1009             coincide with the source's is what an unrelated document means",
1010        );
1011        let pasted = project(&index.view(&target), root);
1012        let expected = project(&index.view(&source), block_id(1));
1013        assert_eq!(
1014            pasted,
1015            Projection {
1016                rect: expected.rect.translate(GridVec::new(5, 7)),
1017                ..expected
1018            },
1019            "the paste is the source subtree modulo ids and the drop offset"
1020        );
1021        assert_eq!(
1022            pasted.children.len(),
1023            1,
1024            "the fixture must nest, or the subtree walk proves nothing"
1025        );
1026    }
1027
1028    /// A nested block travels inside its parent's interior, which did not
1029    /// move: only the roots ride the drop.
1030    #[test]
1031    fn only_the_roots_of_a_paste_ride_the_drop_offset() {
1032        let source = scene();
1033        let mut index = DocIndex::default();
1034        let clip = copy(&index.view(&source), &[delete::Target::Block(block_id(1))]);
1035
1036        let target = empty();
1037        let mut builder = CommitBuilder::new("Pasted a subtree");
1038        let roots = paste(
1039            &index.view(&target),
1040            Paste {
1041                clip: &clip,
1042                into: home(),
1043                target: drop_at(Scope::Root, 5, 7),
1044            },
1045            &mut ids(&target),
1046            &mut builder,
1047        );
1048        let target = fold(builder, &target);
1049        let pasted = project(&index.view(&target), only_block(&roots));
1050
1051        assert_eq!(pasted.rect, rect(5, 7, 10, 10), "the root rode the drop");
1052        assert_eq!(
1053            pasted.children[0].rect,
1054            rect(2, 2, 4, 4),
1055            "the nested block kept its place inside the parent that moved"
1056        );
1057        assert_eq!(
1058            pasted.children[0].routes[0].3,
1059            vec![Waypoint {
1060                pos: GridPoint { x: 4, y: 4 },
1061                locked: true,
1062            }],
1063            "a nested wire's corners are interior coordinates and did not shift"
1064        );
1065        assert_eq!(
1066            pasted.routes[0].3,
1067            vec![Waypoint {
1068                pos: GridPoint { x: 3, y: 3 },
1069                locked: true,
1070            }],
1071            "the root's own interior did not move either — only its rect did"
1072        );
1073    }
1074
1075    /// Two siblings wired at their parent's level: copying both carries
1076    /// the wire, which lands at the target scope and so takes its corners
1077    /// along (legacy `paste_offsets_route_waypoints`,
1078    /// `src/widget/clipboard.rs:958`).
1079    fn siblings() -> Document {
1080        let doc = scene();
1081        let mut builder = CommitBuilder::new("Wired two siblings");
1082        builder.extend([
1083            block_create(40),
1084            reparent(40, 1),
1085            resize(40, rect(0, 0, 4, 4)),
1086            block_create(41),
1087            reparent(41, 1),
1088            resize(41, rect(8, 0, 4, 4)),
1089            pin_create(42, 40),
1090            pin_create(43, 41),
1091            route_create(44, 1, 42, 43),
1092            waypoints(44, &[(6, 2)]),
1093        ]);
1094        fold(builder, &doc)
1095    }
1096
1097    #[test]
1098    fn a_wire_landing_at_the_target_scope_takes_its_corners_along() {
1099        let doc = siblings();
1100        let mut index = DocIndex::default();
1101        let clip = copy(
1102            &index.view(&doc),
1103            &[
1104                delete::Target::Block(block_id(40)),
1105                delete::Target::Block(block_id(41)),
1106            ],
1107        );
1108        assert_eq!(
1109            clip.snapshot().routes.len(),
1110            1,
1111            "precondition: the wire between the copied siblings came along"
1112        );
1113        assert_eq!(
1114            clip.snapshot().routes[0].1.owner,
1115            block_id(1),
1116            "precondition: the wire is owned outside the copy, so it lands at the target"
1117        );
1118
1119        let mut builder = CommitBuilder::new("Pasted two wired blocks");
1120        let roots = paste(
1121            &index.view(&doc),
1122            Paste {
1123                clip: &clip,
1124                into: home(),
1125                target: drop_at(Scope::Block(block_id(2)), 5, 7),
1126            },
1127            &mut ids(&doc),
1128            &mut builder,
1129        );
1130        let doc = fold(builder, &doc);
1131
1132        assert_eq!(roots.len(), 2, "both siblings pasted");
1133        let indexed = index.view(&doc);
1134        // The destination owns a wire of its own; the copy is the other.
1135        let landed: Vec<RouteId> = indexed.index.blocks[&block_id(2)]
1136            .routes
1137            .iter()
1138            .copied()
1139            .filter(|id| *id != route_id(12))
1140            .collect();
1141        let [wire] = landed[..] else {
1142            unreachable!("exactly one wire landed in the target scope: {landed:?}");
1143        };
1144        let route = doc.route(&wire).expect("the pasted wire");
1145        assert_eq!(
1146            route.waypoints,
1147            vec![Waypoint {
1148                pos: GridPoint { x: 11, y: 9 },
1149                locked: true,
1150            }],
1151            "the corners rode the same delta the blocks did"
1152        );
1153    }
1154
1155    /// The correction of 2026-08-19: cut then paste is a *move*. The
1156    /// entities come back under their own ids, so the gesture reads as one
1157    /// displacement — and the commit proves it, minting nothing.
1158    #[test]
1159    fn a_cut_and_paste_moves_the_originals_instead_of_recreating_them() {
1160        let doc = scene();
1161        let mut index = DocIndex::default();
1162        let mut host = CommitBuilder::new("Added a destination");
1163        host.push(block_create(30));
1164        let doc = fold(host, &doc);
1165
1166        let mut builder = CommitBuilder::new("Cut a subtree");
1167        let clip = cut(
1168            &index.view(&doc),
1169            &[delete::Target::Block(block_id(1))],
1170            home(),
1171            &mut builder,
1172        );
1173        let doc = fold(builder, &doc);
1174        assert!(
1175            doc.block(&block_id(1)).is_none(),
1176            "precondition: the cut removed the source"
1177        );
1178
1179        let mut builder = CommitBuilder::new("Pasted a subtree");
1180        let roots = paste(
1181            &index.view(&doc),
1182            Paste {
1183                clip: &clip,
1184                into: home(),
1185                target: drop_at(Scope::Block(block_id(30)), 2, 2),
1186            },
1187            &mut ids(&doc),
1188            &mut builder,
1189        );
1190        let commit = builder.seal().expect("the move produced ops");
1191        assert!(
1192            commit
1193                .ops()
1194                .iter()
1195                .any(|op| matches!(op, OpCodes::Block(id, Crud::Create(_)) if *id == block_id(1))),
1196            "the moved subtree's own root comes back under its own id"
1197        );
1198        assert!(
1199            !commit.ops().iter().any(|op| matches!(
1200                op,
1201                OpCodes::Block(id, _) if *id == block_id(31)
1202            )),
1203            "a move mints nothing: {:?}",
1204            commit.ops()
1205        );
1206        let doc = doc.try_apply(&commit).expect("the move folds");
1207
1208        assert_eq!(
1209            roots,
1210            vec![Shape::Block(block_id(1))],
1211            "the move hands back the ids it moved"
1212        );
1213        for id in [block_id(1), block_id(2)] {
1214            assert!(doc.block(&id).is_some(), "{id} is back under its own id");
1215        }
1216        let root = doc.block(&block_id(1)).expect("the root");
1217        assert_eq!(
1218            root.parent,
1219            block_id(30),
1220            "the move lands its roots in the target scope"
1221        );
1222        assert_eq!(root.rect, rect(2, 2, 10, 10), "the drop applied");
1223        assert_eq!(
1224            doc.block(&block_id(2)).expect("the child").parent,
1225            block_id(1),
1226            "the nested structure kept its own chain"
1227        );
1228        assert!(
1229            doc.route(&route_id(5)).is_some() && doc.route_label(&route_label_id(20)).is_some(),
1230            "the wires and labels the cut took come back with it"
1231        );
1232    }
1233
1234    /// §5.1 reversed D10 on the premise that every cross-document flow
1235    /// re-mints, and the absence check alone cannot keep that true: a cut
1236    /// carried to another document names ids that document is not using
1237    /// either. The payload says which document it came out of, and a paste
1238    /// anywhere else duplicates however free the ids look.
1239    #[test]
1240    fn a_cut_pasted_into_another_document_duplicates() {
1241        let doc = scene();
1242        let mut index = DocIndex::default();
1243        let mut builder = CommitBuilder::new("Cut a subtree");
1244        let clip = cut(
1245            &index.view(&doc),
1246            &[delete::Target::Block(block_id(1))],
1247            home(),
1248            &mut builder,
1249        );
1250
1251        // A document that has held — and deleted — blocks up to `b5`: the
1252        // payload's own ids are absent from it *and* below its marks, so
1253        // the absence check passes and only the origin can refuse.
1254        let elsewhere = blockworx_doc::fixtures::commit(
1255            "Built and cleared another document",
1256            (1..=5)
1257                .map(block_create)
1258                .chain((1..=5).map(|n| OpCodes::Block(block_id(n), Crud::Delete)))
1259                .collect(),
1260        );
1261        let elsewhere = Document::default()
1262            .try_apply(&elsewhere)
1263            .expect("the other document folds");
1264        assert!(
1265            clip.snapshot()
1266                .blocks
1267                .iter()
1268                .all(|(id, _)| elsewhere.block(id).is_none()),
1269            "precondition: no source id collides, so only the payload's origin can refuse",
1270        );
1271
1272        let mut index = DocIndex::default();
1273        let mut builder = CommitBuilder::new("Pasted into another document");
1274        let roots = paste(
1275            &index.view(&elsewhere),
1276            Paste {
1277                clip: &clip,
1278                into: DocumentNonce::mint(),
1279                target: drop_at(Scope::Root, 0, 0),
1280            },
1281            &mut ids(&elsewhere),
1282            &mut builder,
1283        );
1284        let commit = builder.seal().expect("the paste produced ops");
1285        assert!(
1286            !commit
1287                .ops()
1288                .iter()
1289                .any(|op| matches!(op, OpCodes::Block(id, _) if *id == block_id(1))),
1290            "a foreign cut names none of its source ids: {:?}",
1291            commit.ops()
1292        );
1293        elsewhere
1294            .try_apply(&commit)
1295            .expect("the duplicate folds into the other document");
1296        assert_ne!(only_block(&roots), block_id(1));
1297    }
1298
1299    /// The other half of the same rule: a *copy* mints even where its
1300    /// sources have since left the document, so the discriminator is the
1301    /// gesture that made the payload rather than what happened afterwards.
1302    #[test]
1303    fn a_copy_whose_sources_are_gone_still_duplicates() {
1304        let doc = scene();
1305        let mut index = DocIndex::default();
1306        let clip = copy(&index.view(&doc), &[delete::Target::Block(block_id(1))]);
1307
1308        let mut builder = CommitBuilder::new("Deleted the copied subtree");
1309        crate::edit::delete::selection(
1310            &index.view(&doc),
1311            &[delete::Target::Block(block_id(1))],
1312            &mut builder,
1313        );
1314        let doc = fold(builder, &doc);
1315        assert!(
1316            doc.block(&block_id(1)).is_none(),
1317            "precondition: the copy's sources are gone, as a cut's would be",
1318        );
1319
1320        let mut builder = CommitBuilder::new("Pasted the copy");
1321        let roots = paste(
1322            &index.view(&doc),
1323            Paste {
1324                clip: &clip,
1325                into: home(),
1326                target: drop_at(Scope::Root, 0, 0),
1327            },
1328            &mut ids(&doc),
1329            &mut builder,
1330        );
1331        let commit = builder.seal().expect("the paste produced ops");
1332        assert!(
1333            !commit
1334                .ops()
1335                .iter()
1336                .any(|op| matches!(op, OpCodes::Block(id, _) if *id == block_id(1))),
1337            "a copy names none of its source ids: {:?}",
1338            commit.ops()
1339        );
1340        assert_ne!(only_block(&roots), block_id(1));
1341    }
1342
1343    /// The first paste put the originals back, so the document holds them
1344    /// again — and a source still standing is a second paste, which
1345    /// duplicates.
1346    #[test]
1347    fn a_second_paste_after_a_move_duplicates() {
1348        let doc = scene();
1349        let mut index = DocIndex::default();
1350        let mut builder = CommitBuilder::new("Cut a subtree");
1351        let clip = cut(
1352            &index.view(&doc),
1353            &[delete::Target::Block(block_id(1))],
1354            home(),
1355            &mut builder,
1356        );
1357        let doc = fold(builder, &doc);
1358
1359        let mut builder = CommitBuilder::new("Pasted a subtree");
1360        let mut fresh = ids(&doc);
1361        paste(
1362            &index.view(&doc),
1363            Paste {
1364                clip: &clip,
1365                into: home(),
1366                target: drop_at(Scope::Root, 0, 0),
1367            },
1368            &mut fresh,
1369            &mut builder,
1370        );
1371        let doc = fold(builder, &doc);
1372        assert!(
1373            doc.block(&block_id(1)).is_some(),
1374            "precondition: the first paste revived the source ids"
1375        );
1376
1377        let mut builder = CommitBuilder::new("Pasted it again");
1378        let roots = paste(
1379            &index.view(&doc),
1380            Paste {
1381                clip: &clip,
1382                into: home(),
1383                target: drop_at(Scope::Root, 4, 4),
1384            },
1385            &mut fresh,
1386            &mut builder,
1387        );
1388        let doc = fold(builder, &doc);
1389
1390        let root = only_block(&roots);
1391        assert_ne!(root, block_id(1), "the second paste minted fresh ids");
1392        assert!(
1393            doc.block(&block_id(1)).is_some(),
1394            "the original stayed where the first paste left it"
1395        );
1396        assert_eq!(
1397            DocIndex::of(&doc).blocks[&Scope::Root.wire_id()].children,
1398            [block_id(1), root].into_iter().collect(),
1399            "the level now holds the original and its copy"
1400        );
1401    }
1402
1403    /// Wholesale, never mixed: one source standing again turns the whole
1404    /// paste into a duplicate, so nothing is half-moved.
1405    #[test]
1406    fn a_partially_present_snapshot_duplicates_wholesale() {
1407        let doc = scene();
1408        let mut index = DocIndex::default();
1409        let mut builder = CommitBuilder::new("Cut a subtree");
1410        let clip = cut(
1411            &index.view(&doc),
1412            &[delete::Target::Block(block_id(1))],
1413            home(),
1414            &mut builder,
1415        );
1416        let doc = fold(builder, &doc);
1417
1418        let mut builder = CommitBuilder::new("A later commit put one block back");
1419        let (_, nested) = clip
1420            .snapshot()
1421            .blocks
1422            .iter()
1423            .find(|(id, _)| *id == block_id(2))
1424            .expect("the cut carried the nested block");
1425        builder.push(OpCodes::Block(
1426            block_id(2),
1427            Crud::Create(Block {
1428                parent: Scope::Root.wire_id(),
1429                ..nested.clone()
1430            }),
1431        ));
1432        let doc = fold(builder, &doc);
1433        assert!(
1434            doc.block(&block_id(2)).is_some() && doc.block(&block_id(1)).is_none(),
1435            "precondition: the snapshot's sources are now part present, part gone"
1436        );
1437
1438        let mut builder = CommitBuilder::new("Pasted a subtree");
1439        let roots = paste(
1440            &index.view(&doc),
1441            Paste {
1442                clip: &clip,
1443                into: home(),
1444                target: drop_at(Scope::Root, 0, 0),
1445            },
1446            &mut ids(&doc),
1447            &mut builder,
1448        );
1449        let commit = builder.seal().expect("the paste produced ops");
1450        assert!(
1451            !commit
1452                .ops()
1453                .iter()
1454                .any(|op| matches!(op, OpCodes::Block(id, _) if *id == block_id(1))),
1455            "a partial state names no source id: {:?}",
1456            commit.ops()
1457        );
1458        let doc = doc.try_apply(&commit).expect("the duplicate folds");
1459
1460        let root = only_block(&roots);
1461        assert_ne!(root, block_id(1));
1462        assert!(
1463            doc.block(&block_id(1)).is_none(),
1464            "the source the cut removed stayed gone"
1465        );
1466    }
1467
1468    /// The remap is what a duplicate is for: the copied wire lands on the
1469    /// copied pins, and the copied label on the copied wire.
1470    #[test]
1471    fn a_duplicate_rewires_its_own_copies() {
1472        let source = scene();
1473        let mut index = DocIndex::default();
1474        let clip = copy(&index.view(&source), &[delete::Target::Block(block_id(1))]);
1475
1476        let mut builder = CommitBuilder::new("Pasted a subtree");
1477        let roots = paste(
1478            &index.view(&source),
1479            Paste {
1480                clip: &clip,
1481                into: home(),
1482                target: drop_at(Scope::Root, 20, 0),
1483            },
1484            &mut ids(&source),
1485            &mut builder,
1486        );
1487        let doc = fold(builder, &source);
1488
1489        let root = only_block(&roots);
1490        let indexed = index.view(&doc);
1491        let copied_pins = &indexed.index.blocks[&root].pins;
1492        let copied_routes = &indexed.index.blocks[&root].routes;
1493        assert_eq!(copied_routes.len(), 1, "the copied level owns one wire");
1494        let wire = *copied_routes.iter().next().expect("the copied wire");
1495        let route = doc.route(&wire).expect("the copied wire exists");
1496        assert!(
1497            copied_pins.contains(&route.from) && copied_pins.contains(&route.to),
1498            "the copied wire lands on the copied pins, not the originals"
1499        );
1500        assert!(
1501            ![route.from, route.to].contains(&pin_id(3)),
1502            "and never on a source id"
1503        );
1504        assert_eq!(
1505            indexed.index.routes[&wire].labels.len(),
1506            1,
1507            "the copied label found its copied wire"
1508        );
1509    }
1510
1511    /// A wire whose far endpoint stayed behind has nothing to land on:
1512    /// dropped, and its labels with it.
1513    #[test]
1514    fn a_wire_with_an_uncopied_endpoint_is_dropped_with_its_labels() {
1515        let doc = scene();
1516        let mut index = DocIndex::default();
1517        // Copy only the nested block's pin 10 — wire 12 also lands on pin
1518        // 11, which stays behind.
1519        let clip = copy(&index.view(&doc), &[delete::Target::Pin(pin_id(10))]);
1520        assert_eq!(
1521            clip.snapshot().routes.len(),
1522            1,
1523            "precondition: the closure carried the wire that lands on the copied pin"
1524        );
1525        assert_eq!(
1526            clip.snapshot().labels.len(),
1527            1,
1528            "precondition: that wire carries a label"
1529        );
1530        assert_eq!(
1531            clip.snapshot().pins.len(),
1532            1,
1533            "precondition: only one of the wire's two endpoints was copied"
1534        );
1535
1536        let mut builder = CommitBuilder::new("Pasted a pin");
1537        let roots = paste(
1538            &index.view(&doc),
1539            Paste {
1540                clip: &clip,
1541                into: home(),
1542                target: drop_at(Scope::Block(block_id(2)), 0, 0),
1543            },
1544            &mut ids(&doc),
1545            &mut builder,
1546        );
1547        let commit = builder.seal().expect("the paste produced ops");
1548        assert!(
1549            commit.ops().iter().all(|op| !matches!(
1550                op,
1551                OpCodes::Route(_, Crud::Create(_)) | OpCodes::RouteLabel(_, Crud::Create(_))
1552            )),
1553            "the wire and its label vanished with the endpoint that stayed: {:?}",
1554            commit.ops()
1555        );
1556        let doc = doc.try_apply(&commit).expect("the paste folds");
1557        assert_eq!(roots.len(), 1, "the pin itself still pasted");
1558        assert_eq!(
1559            DocIndex::of(&doc).blocks[&block_id(2)].pins.len(),
1560            3,
1561            "the destination gained exactly the pasted pin"
1562        );
1563    }
1564
1565    /// The 10e rider through the paste: bytes the target already holds are
1566    /// referenced, never re-sent, and bytes it lacks travel with the copy.
1567    #[test]
1568    fn a_paste_carries_the_payloads_the_target_lacks_and_no_others() {
1569        let icon = svg("<svg>icon</svg>");
1570        let mut index = DocIndex::default();
1571        let picture = svg("<svg>picture</svg>");
1572        let doc = scene();
1573        let mut builder = CommitBuilder::new("Dressed the scene");
1574        builder.extend([
1575            OpCodes::Asset(icon.hash(), icon.clone()),
1576            OpCodes::Asset(picture.hash(), picture.clone()),
1577            OpCodes::Block(
1578                block_id(1),
1579                Crud::Update(BlockUpdate::Icon(Icon {
1580                    asset: icon.hash(),
1581                    rect: artwork_box(0.0, 0.0),
1582                })),
1583            ),
1584            OpCodes::Image(
1585                image_id(9),
1586                Crud::Update(ImageUpdate::Asset(picture.hash())),
1587            ),
1588        ]);
1589        let source = fold(builder, &doc);
1590        let clip = copy(&index.view(&source), &[delete::Target::Block(block_id(1))]);
1591        assert_eq!(
1592            clip.snapshot().assets.len(),
1593            2,
1594            "precondition: the copy carries both payloads it references"
1595        );
1596
1597        // A destination that already holds the icon's bytes but not the
1598        // picture's.
1599        let target = empty();
1600        let mut builder = CommitBuilder::new("Held one payload already");
1601        builder.push(OpCodes::Asset(icon.hash(), icon.clone()));
1602        let target = fold(builder, &target);
1603
1604        let mut builder = CommitBuilder::new("Pasted a subtree");
1605        paste(
1606            &index.view(&target),
1607            Paste {
1608                clip: &clip,
1609                into: home(),
1610                target: drop_at(Scope::Root, 0, 0),
1611            },
1612            &mut ids(&target),
1613            &mut builder,
1614        );
1615        let commit = builder.seal().expect("the paste produced ops");
1616        let payloads: Vec<&OpCodes> = commit
1617            .ops()
1618            .iter()
1619            .filter(|op| matches!(op, OpCodes::Asset(..)))
1620            .collect();
1621        assert_eq!(
1622            payloads,
1623            [&OpCodes::Asset(picture.hash(), picture.clone())],
1624            "only the bytes the target lacked travelled"
1625        );
1626        let target = target.try_apply(&commit).expect("the paste folds");
1627        assert!(
1628            target.asset(&icon.hash()).is_some() && target.asset(&picture.hash()).is_some(),
1629            "both references resolve in the target"
1630        );
1631    }
1632
1633    /// Pins pasted onto a block take free slots — West before East at each
1634    /// offset — and the block grows until the last one fits.
1635    #[test]
1636    fn pasted_pins_take_free_slots_west_before_east_and_grow_the_block() {
1637        let doc = scene();
1638        let mut index = DocIndex::default();
1639        let mut builder = CommitBuilder::new("Named the pins");
1640        builder.extend([
1641            resize(2, rect(2, 2, 4, 4)),
1642            reslot(10, slot(PinSide::West, 0)),
1643            reslot(11, slot(PinSide::East, 0)),
1644            rename_pin(3, "a"),
1645            rename_pin(4, "b"),
1646        ]);
1647        let doc = fold(builder, &doc);
1648        assert_eq!(
1649            doc.block(&block_id(2)).expect("the destination").rect.size,
1650            GridSize { w: 4, h: 4 },
1651            "precondition: the destination is one slot tall and that slot is full"
1652        );
1653
1654        let clip = copy(
1655            &index.view(&doc),
1656            &[
1657                delete::Target::Pin(pin_id(3)),
1658                delete::Target::Pin(pin_id(4)),
1659            ],
1660        );
1661        let mut builder = CommitBuilder::new("Pasted pins");
1662        let pasted = paste_pins(
1663            &index.view(&doc),
1664            PinPaste {
1665                pins: &clip.snapshot().pins,
1666                owner: Scope::Block(block_id(2)),
1667            },
1668            &mut ids(&doc),
1669            &mut builder,
1670        );
1671        let doc = fold(builder, &doc);
1672
1673        assert_eq!(pasted.len(), 2, "both pins landed");
1674        let slots: Vec<(String, PinSlot)> = pasted
1675            .iter()
1676            .map(|id| {
1677                let pin = doc.pin(id).expect("the pasted pin");
1678                (pin.name.clone(), pin.slot)
1679            })
1680            .collect();
1681        assert_eq!(
1682            slots,
1683            vec![
1684                ("a".to_string(), slot(PinSide::West, 1)),
1685                ("b".to_string(), slot(PinSide::East, 1)),
1686            ],
1687            "the search hands out West before East at the first free offset"
1688        );
1689        assert_eq!(
1690            doc.block(&block_id(2)).expect("the destination").rect.size,
1691            GridSize { w: 4, h: 8 },
1692            "one growth rider, sized for the lowest slot the group took"
1693        );
1694    }
1695
1696    /// Legacy `paste_pins_into_block`'s refusal (`src/widget/clipboard.rs:
1697    /// 605`) and E4's absent target, in one place.
1698    #[test]
1699    fn pasting_pins_onto_a_frozen_or_absent_block_pushes_nothing() {
1700        let doc = scene();
1701        let mut index = DocIndex::default();
1702        let mut builder = CommitBuilder::new("Froze the interface");
1703        builder.push(OpCodes::Block(
1704            block_id(2),
1705            Crud::Update(BlockUpdate::Locked(true)),
1706        ));
1707        let doc = fold(builder, &doc);
1708        let clip = copy(&index.view(&doc), &[delete::Target::Pin(pin_id(3))]);
1709
1710        let mut builder = CommitBuilder::new("Pasted pins onto a frozen block");
1711        let pasted = paste_pins(
1712            &index.view(&doc),
1713            PinPaste {
1714                pins: &clip.snapshot().pins,
1715                owner: Scope::Block(block_id(2)),
1716            },
1717            &mut ids(&doc),
1718            &mut builder,
1719        );
1720        assert!(pasted.is_empty(), "a frozen interface takes no pins");
1721        seals_to_nothing(builder);
1722
1723        let mut builder = CommitBuilder::new("Pasted pins onto a ghost");
1724        let pasted = paste_pins(
1725            &index.view(&doc),
1726            PinPaste {
1727                pins: &clip.snapshot().pins,
1728                owner: Scope::Block(block_id(99)),
1729            },
1730            &mut ids(&doc),
1731            &mut builder,
1732        );
1733        assert!(pasted.is_empty());
1734        seals_to_nothing(builder);
1735    }
1736
1737    /// The same refusal reached through the whole-selection paste: a pin
1738    /// landing on the target scope's own boundary needs that boundary
1739    /// unfrozen, and a wire that loses its endpoint goes with it.
1740    #[test]
1741    fn a_paste_onto_a_frozen_scope_drops_the_pins_it_cannot_slot() {
1742        let doc = scene();
1743        let mut index = DocIndex::default();
1744        let clip = copy(&index.view(&doc), &[delete::Target::Pin(pin_id(3))]);
1745        let mut builder = CommitBuilder::new("Froze the destination");
1746        builder.push(OpCodes::Block(
1747            block_id(2),
1748            Crud::Update(BlockUpdate::Locked(true)),
1749        ));
1750        let doc = fold(builder, &doc);
1751
1752        let mut builder = CommitBuilder::new("Pasted onto a frozen scope");
1753        let roots = paste(
1754            &index.view(&doc),
1755            Paste {
1756                clip: &clip,
1757                into: home(),
1758                target: drop_at(Scope::Block(block_id(2)), 0, 0),
1759            },
1760            &mut ids(&doc),
1761            &mut builder,
1762        );
1763        assert!(roots.is_empty(), "nothing landed");
1764        seals_to_nothing(builder);
1765    }
1766
1767    /// A pin pasted as a port on another block re-slots and rides the drop
1768    /// with its body — the legacy `paste`'s free-floating port arm
1769    /// (`src/widget/clipboard.rs:436`).
1770    #[test]
1771    fn a_pin_pasted_into_another_block_re_slots_and_rides_the_drop() {
1772        let doc = scene();
1773        let mut index = DocIndex::default();
1774        let clip = copy(&index.view(&doc), &[delete::Target::Pin(pin_id(10))]);
1775        let body = doc.pin(&pin_id(10)).expect("the copied pin").rect;
1776        assert_eq!(
1777            doc.pin(&pin_id(10)).expect("the copied pin").slot,
1778            slot(PinSide::West, 1),
1779            "precondition: the source slot is taken on the destination"
1780        );
1781
1782        let mut builder = CommitBuilder::new("Pasted a pin");
1783        let roots = paste(
1784            &index.view(&doc),
1785            Paste {
1786                clip: &clip,
1787                into: home(),
1788                target: drop_at(Scope::Block(block_id(1)), 3, 4),
1789            },
1790            &mut ids(&doc),
1791            &mut builder,
1792        );
1793        let doc = fold(builder, &doc);
1794
1795        let [Shape::Port(id)] = roots[..] else {
1796            unreachable!("a pasted pin is a port shape: {roots:?}");
1797        };
1798        let pasted = doc.pin(&id).expect("the pasted pin");
1799        assert_eq!(pasted.owner, block_id(1));
1800        assert_eq!(
1801            pasted.slot,
1802            slot(PinSide::West, 1),
1803            "the first slot free on the destination, not the one it came from"
1804        );
1805        assert_eq!(
1806            pasted.rect,
1807            body.translate(GridVec::new(3, 4)),
1808            "the body rode the drop"
1809        );
1810    }
1811
1812    /// E4 across the family: nothing named, nothing found, nothing to
1813    /// paste.
1814    #[test]
1815    fn absent_targets_and_empty_snapshots_do_nothing() {
1816        let doc = scene();
1817        let mut index = DocIndex::default();
1818        let strangers = [
1819            delete::Target::Block(block_id(99)),
1820            delete::Target::Pin(pin_id(98)),
1821            delete::Target::Route(route_id(97)),
1822            delete::Target::Text(text_id(96)),
1823            delete::Target::Area(area_id(95)),
1824            delete::Target::Image(image_id(94)),
1825        ];
1826        let clip = copy(&index.view(&doc), &strangers);
1827        assert!(
1828            clip.snapshot().is_empty(),
1829            "a selection of ghosts copies nothing"
1830        );
1831
1832        let mut builder = CommitBuilder::new("Cut ghosts");
1833        let cut_clip = cut(&index.view(&doc), &strangers, home(), &mut builder);
1834        assert!(cut_clip.snapshot().is_empty());
1835        seals_to_nothing(builder);
1836
1837        let mut builder = CommitBuilder::new("Pasted nothing");
1838        let roots = paste(
1839            &index.view(&doc),
1840            Paste {
1841                clip: &clip,
1842                into: home(),
1843                target: drop_at(Scope::Block(block_id(1)), 2, 2),
1844            },
1845            &mut ids(&doc),
1846            &mut builder,
1847        );
1848        assert!(roots.is_empty());
1849        seals_to_nothing(builder);
1850
1851        let mut builder = CommitBuilder::new("Pasted no pins");
1852        let pasted = paste_pins(
1853            &index.view(&doc),
1854            PinPaste {
1855                pins: &[],
1856                owner: Scope::Block(block_id(1)),
1857            },
1858            &mut ids(&doc),
1859            &mut builder,
1860        );
1861        assert!(pasted.is_empty());
1862        seals_to_nothing(builder);
1863    }
1864
1865    /// A cut declines what the delete declines, so the two halves cannot
1866    /// disagree — the frozen pin stays in the document *and* off the
1867    /// clipboard.
1868    #[test]
1869    fn a_cut_leaves_a_frozen_pin_in_the_document_and_off_the_clipboard() {
1870        let doc = scene();
1871        let mut index = DocIndex::default();
1872        let mut builder = CommitBuilder::new("Froze the interface");
1873        builder.push(OpCodes::Block(
1874            block_id(2),
1875            Crud::Update(BlockUpdate::Locked(true)),
1876        ));
1877        let doc = fold(builder, &doc);
1878
1879        let mut builder = CommitBuilder::new("Cut a frozen pin");
1880        let clip = cut(
1881            &index.view(&doc),
1882            &[delete::Target::Pin(pin_id(10))],
1883            home(),
1884            &mut builder,
1885        );
1886        assert!(clip.snapshot().is_empty(), "the copy declined it too");
1887        seals_to_nothing(builder);
1888    }
1889
1890    /// The decode boundary: our own payload round-trips, and anything else
1891    /// refuses rather than being reinterpreted.
1892    #[test]
1893    fn the_envelope_round_trips_and_refuses_everything_else() {
1894        let doc = scene();
1895        let mut index = DocIndex::default();
1896        let clip = copy(&index.view(&doc), &[delete::Target::Block(block_id(1))]);
1897        let json = clip.to_json().expect("the payload serializes");
1898        assert_eq!(
1899            Clipboard::from_json(&json).as_ref(),
1900            Some(&clip),
1901            "our own payload comes back unchanged"
1902        );
1903
1904        for refused in [
1905            "",
1906            "hello",
1907            "{}",
1908            r#"{"V1":{"blocks":[]}}"#,
1909            r#"{"V3":{"blocks":[]}}"#,
1910            &json[..json.len() / 2],
1911        ] {
1912            assert!(
1913                Clipboard::from_json(refused).is_none(),
1914                "{refused:?} is not one of our payloads"
1915            );
1916        }
1917    }
1918
1919    /// The `V2` tag is the wire name, and a payload written under it must
1920    /// keep decoding: pin the tag itself, not just the round trip.
1921    #[test]
1922    fn the_payload_is_tagged_by_its_version() {
1923        let json = Clipboard::V2 {
1924            snapshot: Snapshot::default(),
1925            origin: Origin::Copy,
1926        }
1927        .to_json()
1928        .expect("the payload serializes");
1929        assert!(json.starts_with(r#"{"V2":"#), "{json}");
1930    }
1931
1932    /// The offset is a whole-cell delta, so the artwork riding it moves by
1933    /// the same distance in world pixels.
1934    #[test]
1935    fn artwork_rides_the_drop_in_world_pixels() {
1936        let picture = svg("<svg>picture</svg>");
1937        let mut index = DocIndex::default();
1938        let doc = scene();
1939        let mut builder = CommitBuilder::new("Placed artwork at the top level");
1940        builder.extend([
1941            OpCodes::Asset(picture.hash(), picture.clone()),
1942            image_create(40, 1),
1943            OpCodes::Image(
1944                image_id(40),
1945                Crud::Update(ImageUpdate::Asset(picture.hash())),
1946            ),
1947            OpCodes::Image(
1948                image_id(40),
1949                Crud::Update(ImageUpdate::Rect(artwork_box(30.0, 45.0))),
1950            ),
1951        ]);
1952        let doc = fold(builder, &doc);
1953
1954        let clip = copy(&index.view(&doc), &[delete::Target::Image(image_id(40))]);
1955        let mut builder = CommitBuilder::new("Pasted artwork");
1956        let roots = paste(
1957            &index.view(&doc),
1958            Paste {
1959                clip: &clip,
1960                into: home(),
1961                target: drop_at(Scope::Block(block_id(1)), 2, 3),
1962            },
1963            &mut ids(&doc),
1964            &mut builder,
1965        );
1966        let doc = fold(builder, &doc);
1967
1968        let [Shape::Image(id)] = roots[..] else {
1969            unreachable!("a pasted image is an image shape: {roots:?}");
1970        };
1971        assert_eq!(
1972            artwork_rect(doc.image(&id).expect("the copy").rect),
1973            artwork_rect(artwork_box(30.0, 45.0))
1974                .translate(Vec2::new(2.0 * GRID_SIZE, 3.0 * GRID_SIZE)),
1975            "the copy sits one whole-cell delta from its source"
1976        );
1977    }
1978}