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