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