Skip to main content

blockworx_editor/edit/
clipboard.rs

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