1use ahash::HashSet;
6use blockworx_doc::{
7 block_model::{Area, Block, BlockUpdate, Icon, Label, Pin, Route, RouteLabel, Text},
8 commit::CommitBuilder,
9 document::{Document, IndexedDocument, TitleBlockUpdate},
10 geometry::{FracVal, GridPoint, GridRect, GridSize, GridVec, PinSlot, Waypoint},
11 id::{AreaId, BlockId, PinId, RouteId, RouteLabelId, TextId},
12 opcode::{Crud, OpCodes},
13 values::{LabelSide, PinDir, PinSide, Role},
14};
15use blockworx_geom::{Pos2, Rect};
16
17use crate::edit::lock::UnlockedScope;
18use crate::edit::lower::{block_rect, slot_capacity};
19use crate::grid::{
20 DEFAULT_SCALE_FOR_NEW_VIEW, GRID_SIZE, PIN_PITCH_GRID, PIN_TOP_MARGIN, ceil_block_height,
21 grid_point, grid_rect, pin_offset_y, px_rect, snap_block_height_cells,
22};
23use crate::path::{Resolved, Scope, resolve};
24use crate::shape::port::{PORT_HEIGHT, width_for_labels};
25
26const UNTITLED: &str = "Untitled";
31
32const BLOCK_PREFIX: &str = "Block ";
36
37const PORT_PREFIX: &str = "Port ";
39
40fn label(name: &str, side: LabelSide) -> Label {
43 Label {
44 name: name.into(),
45 side,
46 offset: FracVal::default(),
47 hidden: false,
48 }
49}
50
51#[derive(Clone, Copy, Debug)]
55pub struct NewBlock {
56 pub id: BlockId,
57 pub scope: Scope,
58 pub start: Pos2,
59 pub end: Pos2,
60}
61
62pub fn block(doc: &Document, new: NewBlock, builder: &mut CommitBuilder) {
73 builder.push(OpCodes::Block(new.id, Crud::Create(block_value(doc, new))));
74}
75
76pub fn block_value(doc: &Document, new: NewBlock) -> Block {
81 let NewBlock {
82 id: _,
83 scope,
84 start,
85 end,
86 } = new;
87 Block {
88 parent: scope.wire_id(),
89 rect: block_rect(start, end),
90 locked: false,
91 role: Role::default(),
92 title: label(&next_free_title(doc, BLOCK_PREFIX), LabelSide::Bottom),
93 type_label: label("", LabelSide::Top),
94 icon: Icon::default(),
95 }
96}
97
98pub fn block_title(name: &str) -> Label {
100 label(name, LabelSide::Bottom)
101}
102
103pub fn stamped_block(at: Pos2) -> Rect {
108 stamped(at, TOP_BLOCK_DEFAULT_RECT.size)
109}
110
111fn stamped(at: Pos2, size: GridSize) -> Rect {
114 px_rect(GridRect {
115 top_left: grid_point(at),
116 size,
117 })
118}
119
120pub fn area(id: AreaId, owner: Scope, start: Pos2, end: Pos2, builder: &mut CommitBuilder) {
124 builder.push(OpCodes::Area(
125 id,
126 Crud::Create(Area {
127 owner: owner.wire_id(),
128 rect: grid_rect(start, end),
129 role: Role::default(),
130 title: label(UNTITLED, LabelSide::Top),
131 }),
132 ));
133}
134
135pub fn stamped_area(at: Pos2) -> Rect {
139 stamped(at, STAMPED_AREA)
140}
141
142const STAMPED_AREA: GridSize = GridSize { w: 20, h: 14 };
143
144pub fn text_box(id: TextId, owner: Scope, pos: Pos2, builder: &mut CommitBuilder) {
148 builder.push(OpCodes::Text(
149 id,
150 Crud::Create(Text {
151 owner: owner.wire_id(),
152 text: String::new(),
153 pos: grid_point(pos),
154 role: Role::default(),
155 width: None,
156 }),
157 ));
158}
159
160#[derive(Clone, Copy, Debug)]
163pub struct NewPin {
164 pub id: PinId,
165 pub owner: UnlockedScope,
170 pub slot: PinSlot,
171}
172
173#[derive(Clone, Copy, Debug)]
177pub struct NewPort {
178 pub id: PinId,
179 pub owner: UnlockedScope,
181 pub start: Pos2,
182 pub end: Pos2,
183}
184
185pub(crate) fn owner_pins<'a>(
186 indexed: &'a IndexedDocument<'a>,
187 owner: Scope,
188) -> impl Iterator<Item = &'a Pin> + 'a {
189 indexed
190 .index
191 .blocks
192 .get(&owner.wire_id())
193 .into_iter()
194 .flat_map(|entry| entry.pins.iter())
195 .filter_map(|id| indexed.doc.pin(id))
196}
197
198fn next_pin_ordinal(indexed: &IndexedDocument<'_>, owner: Scope) -> u32 {
203 owner_pins(indexed, owner)
204 .filter_map(|pin| {
205 pin.name
206 .strip_prefix(PORT_PREFIX)
207 .and_then(|ordinal| ordinal.parse::<u32>().ok())
208 })
209 .max()
210 .unwrap_or(0)
211 + 1
212}
213
214pub(crate) fn default_port_rect(
224 rect: GridRect,
225 siblings: &[GridRect],
226 name: &str,
227 slot: PinSlot,
228) -> GridRect {
229 let origin = rect.top_left;
230 let scaled_w = rect.size.w as i32 * DEFAULT_SCALE_FOR_NEW_VIEW;
231 let width = width_for_labels(name, "");
232 let top = origin.y + slot.offset as i32 * PIN_PITCH_GRID * DEFAULT_SCALE_FOR_NEW_VIEW;
233 let (left, step) = match slot.side {
234 PinSide::East => (
235 (origin.x + scaled_w - width as i32).max(origin.x),
236 width as i32,
237 ),
238 PinSide::West => (origin.x, -(width as i32)),
239 };
240 let mut candidate = GridRect {
241 top_left: GridPoint { x: left, y: top },
242 size: GridSize {
243 w: width,
244 h: PORT_HEIGHT,
245 },
246 };
247 let mut steps = 0;
248 while siblings.iter().any(|body| candidate.intersects(*body)) && steps <= siblings.len() {
249 candidate = candidate.translate(GridVec::new(step, 0));
250 steps += 1;
251 }
252 candidate
253}
254
255fn pin_init(indexed: &IndexedDocument<'_>, new: NewPin) -> Pin {
261 let owner = resolve(indexed, new.owner.scope());
266 let ordinal = next_pin_ordinal(indexed, new.owner.scope());
267 let name = format!("{PORT_PREFIX}{ordinal}");
268 let siblings: Vec<GridRect> = owner_pins(indexed, new.owner.scope())
269 .map(|pin| pin.rect)
270 .collect();
271 Pin {
272 owner: new.owner.scope().wire_id(),
273 rect: match owner {
274 Resolved::Block(block) => default_port_rect(block.rect, &siblings, &name, new.slot),
275 Resolved::Root | Resolved::Absent => GridRect::default(),
276 },
277 name,
278 type_name: String::new(),
279 tag: ordinal.to_string(),
280 tag_hidden: false,
281 slot: new.slot,
282 dir: PinDir::InOut,
283 port_accent: Role::default(),
284 flip_lr: false,
285 }
286}
287
288pub fn pin(indexed: &IndexedDocument<'_>, new: NewPin, builder: &mut CommitBuilder) {
291 let init = pin_init(indexed, new);
292 builder.push(OpCodes::Pin(new.id, Crud::Create(init)));
293}
294
295fn height_for_slot(slot: u32) -> u32 {
299 (ceil_block_height(pin_offset_y(0.0, slot) + PIN_TOP_MARGIN) / GRID_SIZE).round() as u32
300}
301
302pub(crate) fn first_free_slot(occupied: &HashSet<PinSlot>) -> PinSlot {
309 (0..=occupied.len() as u32)
310 .flat_map(|offset| [PinSide::West, PinSide::East].map(|side| PinSlot { side, offset }))
311 .find(|slot| !occupied.contains(slot))
312 .unwrap_or_default()
313}
314
315pub(crate) fn grown_to_fit(block: &Block, slot: PinSlot) -> Option<GridRect> {
318 let rect = block.rect;
319 let mut height = rect.size.h;
320 while slot_capacity(height) < slot.offset {
321 height += 2;
322 }
323 (height != rect.size.h).then_some(GridRect {
324 size: GridSize {
325 h: height,
326 ..rect.size
327 },
328 ..rect
329 })
330}
331
332pub fn stamped_port(at: Pos2) -> Rect {
336 stamped(
337 at,
338 GridSize {
339 w: STAMPED_PORT_WIDTH,
340 h: PORT_HEIGHT,
341 },
342 )
343}
344
345pub const STAMPED_PORT_WIDTH: u32 = 8;
346
347pub fn port(indexed: &IndexedDocument<'_>, new: NewPort, builder: &mut CommitBuilder) {
351 let owner = resolve(indexed, new.owner.scope());
352 if let Resolved::Absent = owner {
353 return;
354 }
355 let occupied: HashSet<PinSlot> = owner_pins(indexed, new.owner.scope())
356 .map(|pin| pin.slot)
357 .collect();
358 let slot = first_free_slot(&occupied);
359 let stamp = NewPin {
360 id: new.id,
361 owner: new.owner,
362 slot,
363 };
364 let init = pin_init(indexed, stamp);
365 let body = grid_rect(new.start, new.end);
366 builder.push(OpCodes::Pin(
367 new.id,
368 Crud::Create(Pin {
369 rect: GridRect {
372 size: GridSize {
373 h: PORT_HEIGHT,
374 ..body.size
375 },
376 ..body
377 },
378 ..init
379 }),
380 ));
381 if let Resolved::Block(block) = owner
383 && let Some(rect) = grown_to_fit(block, slot)
384 {
385 builder.push(OpCodes::Block(
386 new.owner.scope().wire_id(),
387 Crud::Update(BlockUpdate::Rect(rect)),
388 ));
389 }
390}
391
392#[derive(Clone, Copy, Debug)]
395pub enum RouteEnd {
396 Pin(PinId),
397 Fresh(NewPin),
398}
399
400#[derive(Clone, Debug)]
402pub struct NewRoute {
403 pub id: RouteId,
404 pub owner: Scope,
405 pub from: PinId,
406 pub to: RouteEnd,
407 pub waypoints: Vec<Waypoint>,
409}
410
411pub fn route(indexed: &IndexedDocument<'_>, new: NewRoute, builder: &mut CommitBuilder) {
416 let to = match new.to {
417 RouteEnd::Pin(id) => id,
418 RouteEnd::Fresh(stamp) => {
419 let init = pin_init(indexed, stamp);
422 builder.push(OpCodes::Pin(stamp.id, Crud::Create(init)));
423 stamp.id
424 }
425 };
426 builder.push(OpCodes::Route(
427 new.id,
428 Crud::Create(Route {
429 owner: new.owner.wire_id(),
430 name: String::new(),
431 from: new.from,
432 to,
433 role: Role::default(),
434 waypoints: new.waypoints,
435 }),
436 ));
437}
438
439#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
443pub struct PathOrdinal(usize);
444
445impl PathOrdinal {
446 pub fn new(index: usize) -> Self {
447 Self(index)
448 }
449}
450
451impl From<PathOrdinal> for usize {
452 fn from(ordinal: PathOrdinal) -> usize {
453 ordinal.0
454 }
455}
456
457pub fn wire_label(
460 doc: &Document,
461 id: RouteLabelId,
462 route: RouteId,
463 pos: FracVal,
464 builder: &mut CommitBuilder,
465) {
466 if doc.route(&route).is_none() {
467 return;
468 }
469 builder.push(OpCodes::RouteLabel(
470 id,
471 Crud::Create(RouteLabel { owner: route, pos }),
472 ));
473}
474
475pub(crate) const TOP_BLOCK_DEFAULT_WIDTH: u32 = 8;
476pub(crate) const TOP_BLOCK_DEFAULT_HEIGHT: u32 = 16;
477
478const TOP_BLOCK_DEFAULT_RECT: GridRect = GridRect {
481 top_left: GridPoint { x: 0, y: 0 },
482 size: GridSize {
483 w: TOP_BLOCK_DEFAULT_WIDTH,
484 h: TOP_BLOCK_DEFAULT_HEIGHT,
485 },
486};
487
488const TOP_PREFIX: &str = "top_";
490
491fn next_free_title(doc: &Document, prefix: &str) -> String {
500 let taken: HashSet<&str> = doc
501 .blocks()
502 .map(|(_, block)| block.title.name.as_str())
503 .collect();
504 let last = taken.len() + 1;
507 (1..=last)
508 .map(|n| format!("{prefix}{n}"))
509 .find(|candidate| !taken.contains(candidate.as_str()))
510 .unwrap_or_else(|| format!("{prefix}{}", last + 1))
511}
512
513fn demoted_rect(block: &Block, lowest_slot: u32) -> GridRect {
520 let own = block.rect;
521 let width = if own.size.w == 0 {
522 TOP_BLOCK_DEFAULT_WIDTH
523 } else {
524 own.size.w
525 };
526 let height = if own.size.h == 0 {
527 TOP_BLOCK_DEFAULT_HEIGHT
528 } else {
529 own.size.h
530 };
531 GridRect {
532 top_left: own.top_left,
533 size: GridSize {
534 w: width,
535 h: snap_block_height_cells(height.max(height_for_slot(lowest_slot))),
536 },
537 }
538}
539
540pub fn wrap_top(indexed: &IndexedDocument<'_>, new_root: BlockId, builder: &mut CommitBuilder) {
545 builder.push(OpCodes::Block(
546 new_root,
547 Crud::Create(Block {
548 parent: Scope::Root.wire_id(),
549 rect: TOP_BLOCK_DEFAULT_RECT,
550 locked: false,
551 role: Role::default(),
552 title: label(&next_free_title(indexed.doc, TOP_PREFIX), LabelSide::Bottom),
553 type_label: label("", LabelSide::Top),
554 icon: Icon::default(),
555 }),
556 ));
557 let old_root = indexed.doc.title_block().top;
558 if let Some(old) = indexed.doc.block(&old_root) {
559 builder.push(OpCodes::Block(
560 old_root,
561 Crud::Update(BlockUpdate::Parent(new_root)),
562 ));
563 let lowest_slot = owner_pins(indexed, Scope::Block(old_root))
564 .map(|pin| pin.slot.offset)
565 .max()
566 .unwrap_or(0);
567 let demoted = demoted_rect(old, lowest_slot);
568 if demoted != old.rect {
569 builder.push(OpCodes::Block(
570 old_root,
571 Crud::Update(BlockUpdate::Rect(demoted)),
572 ));
573 }
574 }
575 builder.push(OpCodes::Document(TitleBlockUpdate::Top(new_root)));
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581 use blockworx_geom::pos2;
582
583 fn scope(indexed: &IndexedDocument<'_>, owner: Scope) -> UnlockedScope {
587 UnlockedScope::of(indexed, owner).expect("the owner is unlocked")
588 }
589 use crate::edit::harness::{fold, seals_to_nothing, wired};
590 use blockworx_doc::document::DocIndex;
591 use blockworx_doc::{
592 block_model::PinUpdate,
593 fixtures::{area_id, block_id, pin_id, route_id, route_label_id, text_id},
594 };
595
596 fn at(x: f32, y: f32) -> Pos2 {
598 pos2(x * GRID_SIZE, y * GRID_SIZE)
599 }
600
601 fn rect(x: i32, y: i32, w: u32, h: u32) -> GridRect {
602 GridRect {
603 top_left: GridPoint { x, y },
604 size: GridSize { w, h },
605 }
606 }
607
608 fn slot(side: PinSide, offset: u32) -> PinSlot {
609 PinSlot { side, offset }
610 }
611
612 fn corner(x: i32, y: i32) -> Waypoint {
614 Waypoint {
615 pos: GridPoint { x, y },
616 locked: false,
617 }
618 }
619
620 fn pinned(x: i32, y: i32) -> Waypoint {
622 Waypoint {
623 locked: true,
624 ..corner(x, y)
625 }
626 }
627
628 fn block_of(doc: &Document, id: u32) -> &Block {
629 doc.block(&block_id(id)).expect("the scene's block exists")
630 }
631
632 fn pin_of(doc: &Document, id: u32) -> &Pin {
633 doc.pin(&pin_id(id)).expect("the scene's pin exists")
634 }
635
636 fn body_width() -> u32 {
639 width_for_labels("Port 1", "")
640 }
641
642 fn sized() -> Document {
645 let doc = wired();
646 let mut builder = CommitBuilder::new("Sized the block");
647 builder.push(OpCodes::Block(
648 block_id(1),
649 Crud::Update(BlockUpdate::Rect(rect(0, 0, 8, 16))),
650 ));
651 let doc = fold(builder, &doc);
652 assert!(
653 8 * DEFAULT_SCALE_FOR_NEW_VIEW > body_width() as i32,
654 "precondition: the scaled interior is wider than a port body, \
655 so the two edges are distinguishable placements"
656 );
657 doc
658 }
659
660 fn packed() -> Document {
663 let doc = wired();
664 let mut builder = CommitBuilder::new("Packed the boundary");
665 builder.push(OpCodes::Block(
666 block_id(1),
667 Crud::Update(BlockUpdate::Rect(rect(0, 0, 8, 4))),
668 ));
669 builder.push(OpCodes::Pin(
670 pin_id(3),
671 Crud::Update(PinUpdate::Slot(slot(PinSide::West, 0))),
672 ));
673 builder.push(OpCodes::Pin(
674 pin_id(4),
675 Crud::Update(PinUpdate::Slot(slot(PinSide::East, 0))),
676 ));
677 let doc = fold(builder, &doc);
678 assert_eq!(
679 slot_capacity(4),
680 0,
681 "precondition: a four-cell block offers slot 0 and nothing else"
682 );
683 doc
684 }
685
686 fn locked_scene() -> Document {
689 let doc = wired();
690 let mut builder = CommitBuilder::new("Locked the block");
691 builder.push(OpCodes::Block(
692 block_id(1),
693 Crud::Update(BlockUpdate::Locked(true)),
694 ));
695 let doc = fold(builder, &doc);
696 assert!(
697 block_of(&doc, 1).locked,
698 "precondition: the owner is locked"
699 );
700 doc
701 }
702
703 #[test]
704 fn block_stamps_a_numbered_child_on_the_height_ladder() {
705 let doc = wired();
706 let mut builder = CommitBuilder::new("Drew a block");
707 block(
708 &doc,
709 NewBlock {
710 id: block_id(10),
711 scope: Scope::Block(block_id(1)),
712 start: at(1.0, 2.0),
713 end: at(9.0, 7.0),
714 },
715 &mut builder,
716 );
717 let doc = fold(builder, &doc);
718
719 let new = block_of(&doc, 10);
720 assert_eq!(new.parent, block_id(1));
721 assert_eq!(
722 new.rect,
723 rect(1, 2, 8, 4),
724 "the drag's five-cell height settles onto the ladder's first rung"
725 );
726 assert_eq!(new.title.name, "Block 1");
727 assert_eq!(new.title.side, LabelSide::Bottom);
728 assert_eq!(new.type_label.side, LabelSide::Top);
729 assert_eq!(new.type_label.name, "");
730 assert!(!new.locked);
731 assert_eq!(new.role, Role::Accent0);
732 assert_eq!(new.icon, Icon::default());
733 assert!(
734 DocIndex::of(&doc).blocks[&block_id(1)]
735 .children
736 .contains(&block_id(10)),
737 "the scope's child list is presentation from the parent register"
738 );
739 }
740
741 #[test]
742 fn a_block_at_the_document_root_takes_the_null_parent() {
743 let doc = wired();
744 let mut builder = CommitBuilder::new("Drew a top-level block");
745 block(
746 &doc,
747 NewBlock {
748 id: block_id(10),
749 scope: Scope::Root,
750 start: at(0.0, 0.0),
751 end: at(4.0, 4.0),
752 },
753 &mut builder,
754 );
755 let doc = fold(builder, &doc);
756
757 assert_eq!(Scope::from_wire(block_of(&doc, 10).parent), Scope::Root);
758 assert!(
759 DocIndex::of(&doc).blocks[&Scope::Root.wire_id()]
760 .children
761 .contains(&block_id(10))
762 );
763 }
764
765 #[test]
771 fn a_stamped_block_takes_the_lowest_free_number_and_refills_gaps() {
772 let stamp = |doc: &Document, id: u32| {
773 let mut builder = CommitBuilder::new("Drew a block");
774 block(
775 doc,
776 NewBlock {
777 id: block_id(id),
778 scope: Scope::Root,
779 start: at(0.0, 0.0),
780 end: at(4.0, 4.0),
781 },
782 &mut builder,
783 );
784 fold(builder, doc)
785 };
786 let doc = stamp(&wired(), 10);
787 assert_eq!(block_of(&doc, 10).title.name, "Block 1");
788
789 let doc = stamp(&doc, 11);
790 assert_eq!(
791 block_of(&doc, 11).title.name,
792 "Block 2",
793 "a second stamp reused the first block's name",
794 );
795
796 let mut builder = CommitBuilder::new("Deleted a block");
797 let mut index = DocIndex::default();
798 crate::edit::delete::selection(
799 &index.view(&doc),
800 &[crate::edit::delete::Target::Block(block_id(10))],
801 &mut builder,
802 );
803 let doc = fold(builder, &doc);
804 assert!(
805 doc.block(&block_id(10)).is_none(),
806 "precondition: the block holding \"Block 1\" is gone",
807 );
808
809 let doc = stamp(&doc, 12);
810 assert_eq!(
811 block_of(&doc, 12).title.name,
812 "Block 1",
813 "the freed number was climbed past instead of reused",
814 );
815 }
816
817 #[test]
818 fn area_stamps_an_untitled_box_off_the_height_ladder() {
819 let doc = wired();
820 let mut builder = CommitBuilder::new("Drew an area");
821 area(
822 area_id(10),
823 Scope::Block(block_id(1)),
824 at(1.0, 2.0),
825 at(9.0, 7.0),
826 &mut builder,
827 );
828 let doc = fold(builder, &doc);
829
830 let new = doc.area(&area_id(10)).expect("the area exists");
831 assert_eq!(new.owner, block_id(1));
832 assert_eq!(
833 new.rect,
834 rect(1, 2, 8, 5),
835 "an area holds no pins, so its height is the one drawn"
836 );
837 assert_eq!(new.title.name, "Untitled");
838 assert_eq!(new.title.side, LabelSide::Top);
839 assert_eq!(new.role, Role::Accent0);
840 }
841
842 #[test]
843 fn text_box_stamps_an_empty_box_at_the_snapped_click() {
844 let doc = wired();
845 let mut builder = CommitBuilder::new("Placed a text box");
846 text_box(
847 text_id(10),
848 Scope::Block(block_id(1)),
849 at(3.4, 5.6),
850 &mut builder,
851 );
852 let doc = fold(builder, &doc);
853
854 let new = doc.text(&text_id(10)).expect("the text exists");
855 assert_eq!(new.owner, block_id(1));
856 assert_eq!(new.text, "");
857 assert_eq!(new.pos, GridPoint { x: 3, y: 6 });
858 assert_eq!(new.role, Role::Accent0);
859 }
860
861 #[test]
862 fn port_takes_the_first_free_slot_and_grows_the_block_to_fit() {
863 let doc = packed();
864 let mut index = DocIndex::default();
865 let mut builder = CommitBuilder::new("Stamped a port");
866 let view = index.view(&doc);
867 port(
868 &view,
869 NewPort {
870 id: pin_id(10),
871 owner: scope(&view, Scope::Block(block_id(1))),
872 start: at(2.0, 2.0),
873 end: at(7.0, 5.0),
874 },
875 &mut builder,
876 );
877 let doc = fold(builder, &doc);
878
879 let new = pin_of(&doc, 10);
880 assert_eq!(
881 new.slot,
882 slot(PinSide::West, 1),
883 "slot 0 is taken on both sides, so the next offset's West place wins"
884 );
885 assert_eq!(new.name, "Port 1");
886 assert_eq!(new.tag, "1");
887 assert_eq!(new.dir, PinDir::InOut);
888 assert_eq!(
889 new.rect,
890 rect(2, 2, 5, PORT_HEIGHT),
891 "the gesture box places and widens the body; its height is fixed"
892 );
893
894 assert_eq!(
895 slot_capacity(6),
896 0,
897 "precondition: one two-cell step is not enough, so the loop must run twice"
898 );
899 assert_eq!(
900 block_of(&doc, 1).rect.size,
901 GridSize { w: 8, h: 8 },
902 "the owner grew two cells at a time until the slot fit"
903 );
904 }
905
906 #[test]
907 fn ports_number_themselves_from_the_owners_pins() {
908 let doc = packed();
909 let stamp = |doc: &Document, id: u32| {
910 let mut builder = CommitBuilder::new("Stamped a port");
911 let mut index = DocIndex::default();
912 let view = index.view(doc);
913 port(
914 &view,
915 NewPort {
916 id: pin_id(id),
917 owner: scope(&view, Scope::Block(block_id(1))),
918 start: at(0.0, 0.0),
919 end: at(5.0, 2.0),
920 },
921 &mut builder,
922 );
923 fold(builder, doc)
924 };
925 assert!(
926 !pin_of(&doc, 3).name.starts_with(PORT_PREFIX),
927 "precondition: the scene's hand-named pins carry no ordinal"
928 );
929
930 let doc = stamp(&doc, 10);
931 let doc = stamp(&doc, 11);
932 assert_eq!(pin_of(&doc, 10).name, "Port 1");
933 assert_eq!(pin_of(&doc, 11).name, "Port 2");
934 assert_eq!(pin_of(&doc, 11).tag, "2");
935 assert_ne!(
936 pin_of(&doc, 10).slot,
937 pin_of(&doc, 11).slot,
938 "each stamp takes a slot the last one left free"
939 );
940 }
941
942 #[test]
943 fn pin_stamps_at_the_given_slot() {
944 let doc = sized();
945 let mut index = DocIndex::default();
946 let mut builder = CommitBuilder::new("Stamped a pin");
947 let view = index.view(&doc);
948 pin(
949 &view,
950 NewPin {
951 id: pin_id(10),
952 owner: scope(&view, Scope::Block(block_id(1))),
953 slot: slot(PinSide::East, 2),
954 },
955 &mut builder,
956 );
957 let doc = fold(builder, &doc);
958
959 let new = pin_of(&doc, 10);
960 assert_eq!(new.owner, block_id(1));
961 assert_eq!(new.slot, slot(PinSide::East, 2));
962 assert_eq!(new.name, "Port 1");
963 assert_eq!(new.tag, "1");
964 assert_eq!(new.dir, PinDir::InOut);
965 assert!(!new.tag_hidden);
966 assert!(!new.flip_lr);
967 assert_eq!(
968 block_of(&doc, 1).rect.size,
969 GridSize { w: 8, h: 16 },
970 "a pin at a named slot never grows its block"
971 );
972 }
973
974 #[test]
975 fn a_stamped_pins_body_hugs_the_edge_its_slot_sits_on() {
976 let doc = sized();
977 let stamp = |doc: &Document, id: u32, side| {
978 let mut builder = CommitBuilder::new("Stamped a pin");
979 let mut index = DocIndex::default();
980 let view = index.view(doc);
981 pin(
982 &view,
983 NewPin {
984 id: pin_id(id),
985 owner: scope(&view, Scope::Block(block_id(1))),
986 slot: slot(side, 1),
987 },
988 &mut builder,
989 );
990 fold(builder, doc)
991 };
992
993 let doc = stamp(&doc, 10, PinSide::West);
994 let doc = stamp(&doc, 11, PinSide::East);
995 let interior = block_of(&doc, 1).rect;
996 let west = pin_of(&doc, 10).rect;
997 let east = pin_of(&doc, 11).rect;
998
999 assert_eq!(
1000 west,
1001 rect(
1002 interior.left(),
1003 interior.top() + PIN_PITCH_GRID * DEFAULT_SCALE_FOR_NEW_VIEW,
1004 body_width(),
1005 PORT_HEIGHT
1006 ),
1007 "a West slot's body hugs the left of the magnified interior, one slot down"
1008 );
1009 assert_eq!(
1010 east.right(),
1011 interior.left() + interior.size.w as i32 * DEFAULT_SCALE_FOR_NEW_VIEW,
1012 "an East slot's body hugs the right of the magnified interior"
1013 );
1014 assert_eq!(east.top(), west.top(), "both track the same slot");
1015 assert!(
1016 west.right() < east.left(),
1017 "the two edges are placements apart, not the same box"
1018 );
1019 }
1020
1021 #[test]
1022 fn a_body_that_would_cover_a_sibling_steps_outward() {
1023 let doc = sized();
1024 let mut index = DocIndex::default();
1025 let taken = default_port_rect(
1026 block_of(&doc, 1).rect,
1027 &[],
1028 "Port 1",
1029 slot(PinSide::West, 1),
1030 );
1031 let mut builder = CommitBuilder::new("Parked a body in the way");
1032 builder.push(OpCodes::Pin(
1033 pin_id(3),
1034 Crud::Update(PinUpdate::Rect(taken)),
1035 ));
1036 let doc = fold(builder, &doc);
1037 assert!(
1038 default_port_rect(
1039 block_of(&doc, 1).rect,
1040 &[],
1041 "Port 1",
1042 slot(PinSide::West, 1),
1043 )
1044 .intersects(taken),
1045 "precondition: unstepped, the stamp would land on the parked body"
1046 );
1047
1048 let mut builder = CommitBuilder::new("Stamped a pin behind it");
1049 let view = index.view(&doc);
1050 pin(
1051 &view,
1052 NewPin {
1053 id: pin_id(10),
1054 owner: scope(&view, Scope::Block(block_id(1))),
1055 slot: slot(PinSide::West, 1),
1056 },
1057 &mut builder,
1058 );
1059 let doc = fold(builder, &doc);
1060
1061 let stepped = pin_of(&doc, 10).rect;
1062 assert!(
1063 !stepped.intersects(taken),
1064 "the body stepped clear of its sibling"
1065 );
1066 assert_eq!(
1067 stepped,
1068 taken.translate(GridVec::new(-(body_width() as i32), 0)),
1069 "a West body steps outward by its own width — away from the interior"
1070 );
1071 }
1072
1073 #[test]
1077 fn the_document_root_takes_a_port_though_it_has_no_block() {
1078 let doc = packed();
1079 let mut index = DocIndex::default();
1080 assert!(
1081 doc.block(&Scope::Root.wire_id()).is_none(),
1082 "precondition: the root is a scope, not an entity"
1083 );
1084
1085 let mut builder = CommitBuilder::new("Stamped a port on the root");
1086 let view = index.view(&doc);
1087 port(
1088 &view,
1089 NewPort {
1090 id: pin_id(10),
1091 owner: scope(&view, Scope::Root),
1092 start: at(2.0, 2.0),
1093 end: at(7.0, 5.0),
1094 },
1095 &mut builder,
1096 );
1097 let doc = fold(builder, &doc);
1098
1099 let new = pin_of(&doc, 10);
1100 assert_eq!(Scope::from_wire(new.owner), Scope::Root);
1101 assert_eq!(
1102 new.slot,
1103 slot(PinSide::West, 0),
1104 "the root held no pins, so the first slot is free"
1105 );
1106 assert_eq!(
1107 new.rect,
1108 rect(2, 2, 5, PORT_HEIGHT),
1109 "the gesture box places the body; the root has none to place it in"
1110 );
1111 }
1112
1113 #[test]
1116 fn a_locked_owner_stamps_nothing() {
1117 let doc = locked_scene();
1118 let mut index = DocIndex::default();
1119 let view = index.view(&doc);
1120 assert_eq!(
1121 UnlockedScope::of(&view, Scope::Block(block_id(1))),
1122 None,
1123 "a locked block hands out no proof, so a stamp cannot name it",
1124 );
1125
1126 let stamp = NewPin {
1130 id: pin_id(10),
1131 owner: scope(&view, Scope::Root),
1132 slot: slot(PinSide::East, 2),
1133 };
1134 let mut builder = CommitBuilder::new("Stamped a pin at the root");
1135 pin(&view, stamp, &mut builder);
1136 let doc = fold(builder, &doc);
1137 assert_eq!(
1138 Scope::from_wire(pin_of(&doc, 10).owner),
1139 Scope::Root,
1140 "the stamp landed where the proof came from, not where it was aimed",
1141 );
1142 assert!(
1143 block_of(&doc, 1).locked,
1144 "and the frozen block is untouched",
1145 );
1146 }
1147
1148 #[test]
1150 fn a_locked_owner_takes_no_port_either() {
1151 let doc = locked_scene();
1152 let mut index = DocIndex::default();
1153 let view = index.view(&doc);
1154 assert_eq!(UnlockedScope::of(&view, Scope::Block(block_id(1))), None);
1155 }
1156
1157 #[test]
1158 fn an_absent_owner_hands_out_no_proof() {
1159 let doc = wired();
1160 let mut index = DocIndex::default();
1161 let view = index.view(&doc);
1162 assert_eq!(
1163 UnlockedScope::of(&view, Scope::Block(block_id(99))),
1164 None,
1165 "a block the document does not hold takes no pin, material or not",
1166 );
1167 }
1168
1169 #[test]
1170 fn route_stamps_the_wire_along_the_solved_corners() {
1171 let doc = wired();
1172 let mut index = DocIndex::default();
1173 let corners = vec![corner(1, 1), pinned(2, 2)];
1174 let mut builder = CommitBuilder::new("Drew a wire");
1175 let view = index.view(&doc);
1176 route(
1177 &view,
1178 NewRoute {
1179 id: route_id(10),
1180 owner: Scope::Block(block_id(1)),
1181 from: pin_id(3),
1182 to: RouteEnd::Pin(pin_id(4)),
1183 waypoints: corners.clone(),
1184 },
1185 &mut builder,
1186 );
1187 let doc = fold(builder, &doc);
1188
1189 let new = doc.route(&route_id(10)).expect("the wire exists");
1190 assert_eq!(new.owner, block_id(1));
1191 assert_eq!(new.from, pin_id(3));
1192 assert_eq!(new.to, pin_id(4));
1193 assert_eq!(
1194 new.name, "",
1195 "a wire is born unnamed; naming it is the label's job"
1196 );
1197 assert_eq!(new.role, Role::Accent0);
1198 assert_eq!(new.waypoints, corners);
1199 assert!(
1200 [new.from, new.to].iter().all(|pin| doc.pin(pin).is_some()),
1201 "both endpoints stand, so the wire draws"
1202 );
1203 }
1204
1205 #[test]
1206 fn a_wire_landing_on_a_free_slot_stamps_its_destination_pin() {
1207 let doc = wired();
1208 let mut index = DocIndex::default();
1209 let mut builder = CommitBuilder::new("Drew a wire onto a fresh pin");
1210 let view = index.view(&doc);
1211 route(
1212 &view,
1213 NewRoute {
1214 id: route_id(11),
1215 owner: Scope::Block(block_id(1)),
1216 from: pin_id(3),
1217 to: RouteEnd::Fresh(NewPin {
1218 id: pin_id(10),
1219 owner: scope(&view, Scope::Block(block_id(1))),
1220 slot: slot(PinSide::East, 1),
1221 }),
1222 waypoints: Vec::new(),
1223 },
1224 &mut builder,
1225 );
1226 let doc = fold(builder, &doc);
1227
1228 assert_eq!(pin_of(&doc, 10).slot, slot(PinSide::East, 1));
1229 assert_eq!(
1230 doc.route(&route_id(11)).expect("the wire exists").to,
1231 pin_id(10),
1232 "the wire and the pin it landed on arrive in one commit"
1233 );
1234 }
1235
1236 #[test]
1237 fn an_absent_wire_takes_no_label() {
1238 let doc = wired();
1239 let mut builder = CommitBuilder::new("Deleted the wire");
1240 builder.push(OpCodes::Route(route_id(5), Crud::Delete));
1241 let doc = fold(builder, &doc);
1242 assert!(
1243 doc.route(&route_id(5)).is_none(),
1244 "precondition: the wire is gone"
1245 );
1246
1247 let mut builder = CommitBuilder::new("Labelled a dead wire");
1248 wire_label(
1249 &doc,
1250 route_label_id(10),
1251 route_id(5),
1252 FracVal::from(3.5),
1253 &mut builder,
1254 );
1255 seals_to_nothing(builder);
1256
1257 let mut builder = CommitBuilder::new("Labelled a wire that never was");
1258 wire_label(
1259 &doc,
1260 route_label_id(10),
1261 route_id(99),
1262 FracVal::from(3.5),
1263 &mut builder,
1264 );
1265 seals_to_nothing(builder);
1266 }
1267
1268 #[test]
1269 fn wire_label_anchors_at_the_arc_length_the_click_projected() {
1270 let doc = wired();
1271 let mut builder = CommitBuilder::new("Labelled a wire");
1272 wire_label(
1273 &doc,
1274 route_label_id(10),
1275 route_id(5),
1276 FracVal::from(37.5),
1277 &mut builder,
1278 );
1279 let doc = fold(builder, &doc);
1280
1281 let new = doc
1282 .route_label(&route_label_id(10))
1283 .expect("the label exists");
1284 assert_eq!(new.owner, route_id(5));
1285 assert_eq!(new.pos, FracVal::from(37.5));
1286 assert!(
1287 DocIndex::of(&doc).routes[&route_id(5)]
1288 .labels
1289 .contains(&route_label_id(10))
1290 );
1291 }
1292
1293 fn rooted() -> Document {
1296 let doc = wired();
1297 let mut builder = CommitBuilder::new("Designated the root");
1298 builder.push(OpCodes::Document(TitleBlockUpdate::Top(block_id(1))));
1299 let doc = fold(builder, &doc);
1300 assert_eq!(
1301 doc.title_block().top,
1302 block_id(1),
1303 "precondition: the document has a root to demote"
1304 );
1305 doc
1306 }
1307
1308 #[test]
1309 fn wrap_top_creates_a_root_demotes_the_old_one_and_repoints_the_top() {
1310 let doc = rooted();
1311 let mut index = DocIndex::default();
1312 let mut builder = CommitBuilder::new("Wrapped the top");
1313 wrap_top(&index.view(&doc), block_id(10), &mut builder);
1314 let doc = fold(builder, &doc);
1315
1316 assert_eq!(doc.title_block().top, block_id(10));
1317 let new_root = block_of(&doc, 10);
1318 assert_eq!(Scope::from_wire(new_root.parent), Scope::Root);
1319 assert_eq!(new_root.rect, TOP_BLOCK_DEFAULT_RECT);
1320 assert_eq!(new_root.title.name, "top_1");
1321 assert_eq!(
1322 block_of(&doc, 1).parent,
1323 block_id(10),
1324 "the old root demotes to the new one's child"
1325 );
1326 assert_eq!(
1327 block_of(&doc, 1).rect,
1328 rect(0, 0, TOP_BLOCK_DEFAULT_WIDTH, TOP_BLOCK_DEFAULT_HEIGHT),
1329 "a root with no size of its own demotes to a visible child, not a sliver"
1330 );
1331 assert_eq!(
1332 DocIndex::of(&doc).blocks[&Scope::Root.wire_id()].children,
1333 [block_id(10)].into_iter().collect(),
1334 "the document root holds exactly the new level"
1335 );
1336 }
1337
1338 #[test]
1339 fn repeated_wraps_name_each_level_distinctly() {
1340 let doc = rooted();
1341 let mut index = DocIndex::default();
1342 let mut builder = CommitBuilder::new("Wrapped the top");
1343 wrap_top(&index.view(&doc), block_id(10), &mut builder);
1344 let doc = fold(builder, &doc);
1345
1346 let mut builder = CommitBuilder::new("Wrapped it again");
1347 wrap_top(&index.view(&doc), block_id(11), &mut builder);
1348 let commit = builder.seal().expect("the wrap produced ops");
1349 assert_eq!(
1350 commit.ops().len(),
1351 3,
1352 "a root already at its demoted rect takes no geometry write"
1353 );
1354 let doc = doc
1355 .try_apply(&commit)
1356 .expect("the fold accepts the gesture");
1357
1358 assert_eq!(block_of(&doc, 11).title.name, "top_2");
1359 assert_eq!(doc.title_block().top, block_id(11));
1360 assert_eq!(block_of(&doc, 10).parent, block_id(11));
1361 }
1362
1363 #[test]
1364 fn wrapping_a_rootless_document_just_names_a_root() {
1365 let doc = wired();
1366 let mut index = DocIndex::default();
1367 assert_eq!(
1368 Scope::from_wire(doc.title_block().top),
1369 Scope::Root,
1370 "precondition: the document has no root yet"
1371 );
1372
1373 let mut builder = CommitBuilder::new("Wrapped the top");
1374 wrap_top(&index.view(&doc), block_id(10), &mut builder);
1375 let commit = builder.seal().expect("the wrap produced ops");
1376 assert_eq!(commit.ops().len(), 2, "there is nothing to demote");
1377 let doc = doc
1378 .try_apply(&commit)
1379 .expect("the fold accepts the gesture");
1380
1381 assert_eq!(doc.title_block().top, block_id(10));
1382 assert_eq!(
1383 Scope::from_wire(block_of(&doc, 1).parent),
1384 Scope::Root,
1385 "a block that was never the root is left where it is"
1386 );
1387 }
1388
1389 #[test]
1390 fn the_demoted_root_grows_to_fit_its_lowest_pin() {
1391 let doc = rooted();
1392 let mut index = DocIndex::default();
1393 let mut builder = CommitBuilder::new("Sized the root and its pins");
1394 builder.push(OpCodes::Block(
1395 block_id(1),
1396 Crud::Update(BlockUpdate::Rect(rect(0, 0, 8, 4))),
1397 ));
1398 builder.push(OpCodes::Pin(
1399 pin_id(3),
1400 Crud::Update(PinUpdate::Slot(slot(PinSide::West, 3))),
1401 ));
1402 let doc = fold(builder, &doc);
1403 assert!(
1404 slot_capacity(4) < 3,
1405 "precondition: the root is too short for its own lowest pin"
1406 );
1407
1408 let mut builder = CommitBuilder::new("Wrapped the top");
1409 wrap_top(&index.view(&doc), block_id(10), &mut builder);
1410 let doc = fold(builder, &doc);
1411
1412 let demoted = block_of(&doc, 1).rect;
1413 assert_eq!(demoted.size.w, 8, "the demoted root keeps its own width");
1414 assert!(
1415 slot_capacity(demoted.size.h) >= 3,
1416 "the demoted height offers the lowest pin's slot"
1417 );
1418 assert_eq!(demoted.size.h, height_for_slot(3));
1419 }
1420
1421 #[test]
1422 fn the_free_slot_search_finds_the_hole_in_a_packed_boundary() {
1423 let both = |offset| [PinSide::West, PinSide::East].map(|side| slot(side, offset));
1424 let mut occupied: HashSet<PinSlot> = (0..4).flat_map(both).collect();
1425 occupied.remove(&slot(PinSide::East, 2));
1426 assert_eq!(first_free_slot(&occupied), slot(PinSide::East, 2));
1427
1428 assert_eq!(
1429 first_free_slot(&HashSet::default()),
1430 slot(PinSide::West, 0),
1431 "West comes before East at each offset"
1432 );
1433 let full: HashSet<PinSlot> = (0..4).flat_map(both).collect();
1434 assert_eq!(
1435 first_free_slot(&full),
1436 slot(PinSide::West, 4),
1437 "a full boundary hands back the first slot past its end"
1438 );
1439 }
1440
1441 #[test]
1442 fn every_rung_of_the_height_ladder_offers_exactly_its_slot() {
1443 assert_eq!(height_for_slot(0), 4, "the shortest block holds one slot");
1444 for offset in 0..6 {
1445 let height = height_for_slot(offset);
1446 assert_eq!(slot_capacity(height), offset);
1447 assert_eq!(
1448 snap_block_height_cells(height),
1449 height,
1450 "the fitting height is itself a rung"
1451 );
1452 }
1453 }
1454
1455 #[test]
1456 fn a_top_block_is_born_at_a_height_the_resize_snap_keeps() {
1457 assert_eq!(
1458 snap_block_height_cells(TOP_BLOCK_DEFAULT_HEIGHT),
1459 TOP_BLOCK_DEFAULT_HEIGHT
1460 );
1461 }
1462}