1use 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#[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 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 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#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
165pub enum Origin {
166 Copy,
167 Cut { from: DocumentNonce },
168}
169
170#[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 pub fn to_json(&self) -> Option<String> {
190 serde_json::to_string(self).ok()
191 }
192
193 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 #[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 pub fn is_pin_paste(&self) -> bool {
225 matches!(self, Clipboard::PinsV2 { .. })
226 }
227}
228
229fn 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
242pub 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
254pub 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#[derive(Clone, Copy, Debug)]
279pub struct PasteTarget {
280 pub scope: Scope,
281 pub offset: GridVec,
282}
283
284#[derive(Clone, Copy, Debug)]
289pub struct Paste<'a> {
290 pub clip: &'a Clipboard,
291 pub into: DocumentNonce,
294 pub target: PasteTarget,
295}
296
297#[derive(Clone, Copy, Debug)]
300pub struct PinPaste<'a> {
301 pub pins: &'a [(PinId, Pin)],
302 pub owner: Scope,
303}
304
305#[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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
357enum Landing {
358 Carried(BlockId),
361 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
400fn pasted_icon(icon: &Icon, delta: Vec2) -> Icon {
403 artwork(icon).map_or_else(Icon::default, |icon| shifted_icon(icon, delta))
404}
405
406struct 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 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 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
467pub 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
489pub 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#[derive(Clone, Copy, Debug)]
524pub struct FreshScope<'a> {
525 pub id: BlockId,
526 pub block: &'a Block,
527}
528
529#[derive(Clone, Copy, Debug)]
533struct Insertion<'a> {
534 clip: &'a Clipboard,
535 target: PasteTarget,
536}
537
538enum 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 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 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
584fn 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 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 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
735fn 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
752pub 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 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 #[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 fn ids(target: &Document) -> Allocator {
1016 target.ids()
1017 }
1018
1019 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 #[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 #[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 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 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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}