1use ahash::{HashSet, HashSetExt};
13use blockworx_doc::{
14 block_model::{
15 AreaUpdate, Block, BlockUpdate, Icon, ImageUpdate, Pin, PinUpdate, RouteLabelUpdate,
16 RouteUpdate, TextUpdate,
17 },
18 commit::CommitBuilder,
19 document::{Document, IndexedDocument},
20 geometry::{FracVal, GridPoint, GridRect, GridSize, GridVec, PinSlot, Waypoint},
21 hash::AssetHash,
22 id::{AreaId, BlockId, ImageId, PinId, RouteId, RouteLabelId, TextId},
23 opcode::{Crud, OpCodes},
24};
25use blockworx_geom::{Rect, Vec2};
26
27use crate::edit::create::PathOrdinal;
28use crate::edit::lower::slot_capacity;
29use crate::grid::{
30 artwork_rect, grid_point, grid_rect, grid_u32, grid_vec, px_rect, px_u, px_vec, screen_rect,
31};
32use crate::shape::block::{contain_rect, icon_rect_after_resize, resize_pin_shift};
33use crate::shape::port::PORT_HEIGHT;
34
35#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
38pub enum Shape {
39 Block(BlockId),
40 Port(PinId),
41 Text(TextId),
42 Area(AreaId),
43 Image(ImageId),
44 Icon(BlockId),
45}
46
47impl From<crate::shape::ShapeId> for Shape {
52 fn from(id: crate::shape::ShapeId) -> Self {
53 use crate::shape::ShapeId;
54 match id {
55 ShapeId::Rect(id) => Shape::Block(id),
56 ShapeId::Port(id) => Shape::Port(id),
57 ShapeId::Text(id) => Shape::Text(id),
58 ShapeId::Area(id) => Shape::Area(id),
59 ShapeId::Image(id) => Shape::Image(id),
60 ShapeId::Icon(id) => Shape::Icon(id),
61 }
62 }
63}
64
65impl Shape {
66 fn affects_routing(self) -> bool {
71 matches!(self, Shape::Block(_) | Shape::Port(_))
72 }
73}
74
75pub fn placement(doc: &Document, shape: Shape) -> Option<(BlockId, GridRect)> {
79 match shape {
80 Shape::Block(id) => {
81 let block = doc.block(&id)?;
82 Some((block.parent, block.rect))
83 }
84 Shape::Port(id) => {
85 let pin = doc.pin(&id)?;
86 Some((pin.owner, pin.rect))
87 }
88 Shape::Text(_) | Shape::Area(_) | Shape::Image(_) | Shape::Icon(_) => None,
89 }
90}
91
92fn scope_shapes<'a>(
96 indexed: &'a IndexedDocument<'a>,
97 scope: BlockId,
98) -> impl Iterator<Item = (Shape, GridRect)> + 'a {
99 let entry = indexed.index.scope(scope);
100 let children = entry
101 .into_iter()
102 .flat_map(|entry| entry.children.iter())
103 .filter_map(|&id| Some((Shape::Block(id), indexed.doc.block(&id)?.rect)));
104 let ports = entry
105 .into_iter()
106 .flat_map(|entry| entry.pins.iter())
107 .filter_map(|&id| Some((Shape::Port(id), indexed.doc.pin(&id)?.rect)));
108 children.chain(ports)
109}
110
111pub fn move_blocked(
115 indexed: &IndexedDocument<'_>,
116 moving: Shape,
117 scope: BlockId,
118 dst: GridRect,
119) -> bool {
120 scope_shapes(indexed, scope).any(|(other, rect)| other != moving && dst.intersects(rect))
121}
122
123pub fn group_move_blocked(
128 indexed: &IndexedDocument<'_>,
129 members: &HashSet<Shape>,
130 moved: &[(BlockId, GridRect, GridRect)],
131) -> bool {
132 let scopes: HashSet<BlockId> = moved.iter().map(|&(scope, ..)| scope).collect();
133 scopes.into_iter().any(|scope| {
134 scope_shapes(indexed, scope)
135 .filter(|(other, _)| !members.contains(other))
136 .any(|(_, obstacle)| {
137 moved.iter().any(|&(member_scope, src, dst)| {
138 member_scope == scope && dst.intersects(obstacle) && !src.intersects(obstacle)
139 })
140 })
141 })
142}
143
144pub(crate) fn artwork(icon: &Icon) -> Option<&Icon> {
148 (icon.asset != AssetHash::default()).then_some(icon)
149}
150
151pub(crate) fn shifted_icon(icon: &Icon, delta: Vec2) -> Icon {
153 Icon {
154 rect: screen_rect(artwork_rect(icon.rect).translate(delta)),
155 ..icon.clone()
156 }
157}
158
159fn icon_of(block: &Block) -> Option<&Icon> {
160 artwork(&block.icon)
161}
162
163fn carried_icon(block: &Block, delta: Vec2) -> Option<Icon> {
165 Some(shifted_icon(icon_of(block)?, delta))
166}
167
168fn contained_icon(block: &Block, delta: Vec2) -> Option<Icon> {
171 let moved = carried_icon(block, delta)?;
172 Some(Icon {
173 rect: screen_rect(contain_rect(artwork_rect(moved.rect), px_rect(block.rect))),
174 ..moved
175 })
176}
177
178fn push_icon(block_id: BlockId, block: &Block, icon: Option<Icon>, builder: &mut CommitBuilder) {
179 let Some(icon) = icon.filter(|icon| *icon != block.icon) else {
180 return;
181 };
182 builder.push(OpCodes::Block(
183 block_id,
184 Crud::Update(BlockUpdate::Icon(icon)),
185 ));
186}
187
188fn translate(doc: &Document, shape: Shape, delta: (GridVec, Vec2), builder: &mut CommitBuilder) {
192 let (grid, px) = delta;
193 match shape {
194 Shape::Block(id) => {
195 let Some(block) = doc.block(&id) else {
196 return;
197 };
198 let rect = block.rect;
199 if grid != GridVec::ZERO {
200 builder.push(OpCodes::Block(
201 id,
202 Crud::Update(BlockUpdate::Rect(rect.translate(grid))),
203 ));
204 }
205 push_icon(id, block, carried_icon(block, px), builder);
206 }
207 Shape::Port(id) => {
208 let Some(pin) = doc.pin(&id) else {
209 return;
210 };
211 if grid != GridVec::ZERO {
212 builder.push(OpCodes::Pin(
213 id,
214 Crud::Update(PinUpdate::Rect(pin.rect.translate(grid))),
215 ));
216 }
217 }
218 Shape::Text(id) => {
219 let Some(live) = doc.text(&id) else {
220 return;
221 };
222 if grid != GridVec::ZERO {
223 builder.push(OpCodes::Text(
224 id,
225 Crud::Update(TextUpdate::Pos(live.pos + grid)),
226 ));
227 }
228 }
229 Shape::Area(id) => {
230 let Some(live) = doc.area(&id) else {
231 return;
232 };
233 if grid != GridVec::ZERO {
234 builder.push(OpCodes::Area(
235 id,
236 Crud::Update(AreaUpdate::Rect(live.rect.translate(grid))),
237 ));
238 }
239 }
240 Shape::Image(id) => {
241 let Some(live) = doc.image(&id) else {
242 return;
243 };
244 let rect = screen_rect(artwork_rect(live.rect).translate(px));
245 if rect != live.rect {
246 builder.push(OpCodes::Image(id, Crud::Update(ImageUpdate::Rect(rect))));
247 }
248 }
249 Shape::Icon(id) => {
250 let Some(block) = doc.block(&id) else {
251 return;
252 };
253 push_icon(id, block, contained_icon(block, px), builder);
254 }
255 }
256}
257
258pub fn move_shape(
263 indexed: &IndexedDocument<'_>,
264 shape: Shape,
265 delta: Vec2,
266 builder: &mut CommitBuilder,
267) {
268 let grid = grid_vec(delta);
269 if let Some((scope, rect)) = placement(indexed.doc, shape)
270 && move_blocked(indexed, shape, scope, rect.translate(grid))
271 {
272 return;
273 }
274 let px = match shape {
277 Shape::Image(_) | Shape::Icon(_) => delta,
278 Shape::Block(_) | Shape::Port(_) | Shape::Text(_) | Shape::Area(_) => px_vec(grid),
279 };
280 translate(indexed.doc, shape, (grid, px), builder);
281}
282
283pub fn move_group(
287 indexed: &IndexedDocument<'_>,
288 members: &[Shape],
289 delta: Vec2,
290 builder: &mut CommitBuilder,
291) {
292 let grid = grid_vec(delta);
293 let set: HashSet<Shape> = members.iter().copied().collect();
294 let moved: Vec<(BlockId, GridRect, GridRect)> = members
295 .iter()
296 .filter(|shape| shape.affects_routing())
297 .filter_map(|&shape| placement(indexed.doc, shape))
298 .map(|(scope, src)| (scope, src, src.translate(grid)))
299 .collect();
300 if group_move_blocked(indexed, &set, &moved) {
301 return;
302 }
303 let scopes: HashSet<BlockId> = moved.iter().map(|&(scope, ..)| scope).collect();
309 for &shape in members {
310 if matches!(shape, Shape::Icon(id) if set.contains(&Shape::Block(id))) {
314 continue;
315 }
316 translate(indexed.doc, shape, (grid, px_vec(grid)), builder);
317 }
318 push_route_riders(
319 indexed,
320 &riding_pins(indexed, members),
321 &scopes,
322 grid,
323 builder,
324 );
325}
326
327fn riding_pins(indexed: &IndexedDocument<'_>, members: &[Shape]) -> HashSet<PinId> {
330 let mut pins = HashSet::new();
331 for &shape in members {
332 match shape {
333 Shape::Block(id) => {
334 if let Some(entry) = indexed.index.blocks.get(&id) {
335 pins.extend(entry.pins.iter().copied());
336 }
337 }
338 Shape::Port(id) => {
339 if indexed.doc.pin(&id).is_some() {
340 pins.insert(id);
341 }
342 }
343 Shape::Text(_) | Shape::Area(_) | Shape::Image(_) | Shape::Icon(_) => {}
344 }
345 }
346 pins
347}
348
349#[derive(Clone, Copy, PartialEq, Eq, Debug)]
352pub enum RouteEnd {
353 From,
354 To,
355}
356
357pub fn trimmed_approach(waypoints: &[Waypoint], end: RouteEnd) -> Vec<Waypoint> {
361 fn doomed<'a>(corners: impl Iterator<Item = &'a Waypoint>) -> usize {
362 const APPROACH: usize = 2;
363 corners.take(APPROACH).take_while(|wp| !wp.locked).count()
364 }
365 match end {
366 RouteEnd::From => waypoints[doomed(waypoints.iter())..].to_vec(),
367 RouteEnd::To => waypoints[..waypoints.len() - doomed(waypoints.iter().rev())].to_vec(),
368 }
369}
370
371pub fn trimmed_ordinals(waypoints: &[Waypoint], end: RouteEnd) -> Vec<PathOrdinal> {
376 let kept = trimmed_approach(waypoints, end).len();
377 let range = match end {
378 RouteEnd::From => 0..waypoints.len() - kept,
379 RouteEnd::To => kept..waypoints.len(),
380 };
381 range.map(PathOrdinal::new).collect()
382}
383
384fn reverses_at(prev: GridPoint, cur: GridPoint, next: GridPoint) -> bool {
388 let (ax, ay) = (cur.x - prev.x, cur.y - prev.y);
389 let (bx, by) = (next.x - cur.x, next.y - cur.y);
390 (ay == 0 && by == 0 && ax != 0 && bx != 0 && (ax > 0) != (bx > 0))
391 || (ax == 0 && bx == 0 && ay != 0 && by != 0 && (ay > 0) != (by > 0))
392}
393
394pub fn backtracking_ordinals(
403 waypoints: &[Waypoint],
404 start: GridPoint,
405 end: GridPoint,
406) -> Vec<PathOrdinal> {
407 let mut seq: Vec<(usize, GridPoint)> = waypoints
408 .iter()
409 .enumerate()
410 .map(|(i, wp)| (i, wp.pos))
411 .collect();
412 let mut doomed = Vec::new();
413 loop {
414 let hit = (0..seq.len()).find(|&i| {
415 let prev = if i == 0 { start } else { seq[i - 1].1 };
416 let next = if i + 1 == seq.len() {
417 end
418 } else {
419 seq[i + 1].1
420 };
421 reverses_at(prev, seq[i].1, next)
422 });
423 match hit {
424 Some(i) => doomed.push(PathOrdinal::new(seq.remove(i).0)),
425 None => break,
426 }
427 }
428 doomed
429}
430
431pub fn pruned_waypoints(waypoints: &[Waypoint], doomed: &[PathOrdinal]) -> Vec<Waypoint> {
434 waypoints
435 .iter()
436 .enumerate()
437 .filter(|(i, _)| !doomed.contains(&PathOrdinal::new(*i)))
438 .map(|(_, wp)| *wp)
439 .collect()
440}
441
442fn push_route_riders(
449 indexed: &IndexedDocument<'_>,
450 moved: &HashSet<PinId>,
451 scopes: &HashSet<BlockId>,
452 grid: GridVec,
453 builder: &mut CommitBuilder,
454) {
455 let touched: HashSet<RouteId> = moved
456 .iter()
457 .filter_map(|pin| indexed.index.routes_by_endpoint.get(pin))
458 .flatten()
459 .copied()
460 .collect();
461 let mut touched: Vec<RouteId> = touched.into_iter().collect();
462 touched.sort();
463 for id in touched {
464 let Some(route) = indexed.doc.route(&id) else {
465 continue;
466 };
467 if !scopes.contains(&route.owner) {
468 continue;
469 }
470 let stored = route.waypoints.clone();
471 let waypoints = match (moved.contains(&route.from), moved.contains(&route.to)) {
472 (true, true) => stored
473 .iter()
474 .map(|wp| Waypoint {
475 pos: wp.pos + grid,
476 ..*wp
477 })
478 .collect(),
479 (true, false) => trimmed_approach(&stored, RouteEnd::From),
480 (false, true) => trimmed_approach(&stored, RouteEnd::To),
481 (false, false) => continue,
482 };
483 if waypoints != stored {
484 builder.push(OpCodes::Route(
485 id,
486 Crud::Update(RouteUpdate::Waypoints(waypoints)),
487 ));
488 }
489 }
490}
491
492#[derive(Clone, Copy, Debug)]
496pub enum ResizeTarget {
497 Block(BlockId),
498 Port(PinId),
499 Area(AreaId),
500 Image(ImageId),
501 Icon(BlockId),
502}
503
504fn owned_pins<'a>(
505 indexed: &'a IndexedDocument<'a>,
506 owner: BlockId,
507) -> impl Iterator<Item = (PinId, &'a Pin)> + 'a {
508 indexed
509 .index
510 .blocks
511 .get(&owner)
512 .into_iter()
513 .flat_map(|entry| entry.pins.iter())
514 .filter_map(|&id| Some((id, indexed.doc.pin(&id)?)))
515}
516
517fn owned_pins_ordered<'a>(
520 indexed: &'a IndexedDocument<'a>,
521 owner: BlockId,
522) -> Vec<(PinId, &'a Pin)> {
523 let mut pins: Vec<(PinId, &Pin)> = owned_pins(indexed, owner).collect();
524 pins.sort_by_key(|&(id, _)| id);
525 pins
526}
527
528pub fn resize(
534 indexed: &IndexedDocument<'_>,
535 target: ResizeTarget,
536 to: Rect,
537 builder: &mut CommitBuilder,
538) {
539 match target {
540 ResizeTarget::Block(id) => {
541 let Some(block) = indexed.doc.block(&id) else {
542 return;
543 };
544 let old = block.rect;
545 let new = grid_rect(to.min, to.max);
546 let pins = owned_pins_ordered(indexed, id);
547 let shift = resize_pin_shift(
548 pins.iter().map(|(_, pin)| pin.slot.offset),
549 px_u(new.size.h),
550 );
551 if shift > 0 {
552 for (pin_id, pin) in pins {
553 let slot = pin.slot;
554 builder.push(OpCodes::Pin(
555 pin_id,
556 Crud::Update(PinUpdate::Slot(PinSlot {
557 offset: slot.offset.saturating_sub(shift),
558 ..slot
559 })),
560 ));
561 }
562 }
563 if new != old {
564 builder.push(OpCodes::Block(id, Crud::Update(BlockUpdate::Rect(new))));
565 }
566 let resized = icon_of(block).map(|icon| Icon {
567 rect: screen_rect(icon_rect_after_resize(
568 artwork_rect(icon.rect),
569 px_rect(old),
570 px_rect(new),
571 )),
572 ..icon.clone()
573 });
574 push_icon(id, block, resized, builder);
575 }
576 ResizeTarget::Port(id) => {
577 let Some(pin) = indexed.doc.pin(&id) else {
578 return;
579 };
580 let rect = pin.rect;
581 let new = GridRect {
584 top_left: GridPoint {
585 x: grid_point(to.min).x,
586 y: rect.top(),
587 },
588 size: GridSize {
589 w: grid_u32(to.width()),
590 h: PORT_HEIGHT,
591 },
592 };
593 if new != rect {
594 builder.push(OpCodes::Pin(id, Crud::Update(PinUpdate::Rect(new))));
595 }
596 }
597 ResizeTarget::Area(id) => {
598 let Some(live) = indexed.doc.area(&id) else {
599 return;
600 };
601 let new = grid_rect(to.min, to.max);
602 if new != live.rect {
603 builder.push(OpCodes::Area(id, Crud::Update(AreaUpdate::Rect(new))));
604 }
605 }
606 ResizeTarget::Image(id) => {
607 let Some(live) = indexed.doc.image(&id) else {
608 return;
609 };
610 let new = screen_rect(to);
611 if new != live.rect {
612 builder.push(OpCodes::Image(id, Crud::Update(ImageUpdate::Rect(new))));
613 }
614 }
615 ResizeTarget::Icon(id) => {
620 let Some(block) = indexed.doc.block(&id) else {
621 return;
622 };
623 let resized = icon_of(block).map(|icon| Icon {
624 rect: screen_rect(contain_rect(to, px_rect(block.rect))),
625 ..icon.clone()
626 });
627 push_icon(id, block, resized, builder);
628 }
629 }
630}
631
632pub fn move_pin(
637 indexed: &IndexedDocument<'_>,
638 pin: PinId,
639 to: PinSlot,
640 builder: &mut CommitBuilder,
641) {
642 let Some(live) = indexed.doc.pin(&pin) else {
646 return;
647 };
648 if live.slot == to {
649 return;
650 }
651 let owner = live.owner;
652 if owned_pins(indexed, owner).any(|(id, other)| id != pin && other.slot == to) {
653 return;
654 }
655 builder.push(OpCodes::Pin(pin, Crud::Update(PinUpdate::Slot(to))));
656}
657
658#[derive(Clone, Copy, Debug)]
660pub struct PinMove {
661 pub pin: PinId,
662 pub to: PinSlot,
663}
664
665pub fn relocation_fits(indexed: &IndexedDocument<'_>, moves: &[PinMove]) -> bool {
670 let movers: HashSet<PinId> = moves.iter().map(|m| m.pin).collect();
671 let mut destinations: HashSet<(BlockId, PinSlot)> = HashSet::new();
672 moves.iter().all(|m| {
673 let Some(pin) = indexed.doc.pin(&m.pin) else {
674 return false;
675 };
676 let owner = pin.owner;
677 let Some(block) = indexed.doc.block(&owner) else {
678 return false;
679 };
680 destinations.insert((owner, m.to))
681 && m.to.offset <= slot_capacity(block.rect.size.h)
682 && !owned_pins(indexed, owner)
683 .any(|(id, other)| !movers.contains(&id) && other.slot == m.to)
684 })
685}
686
687pub fn relocate_pins(
691 indexed: &IndexedDocument<'_>,
692 moves: &[PinMove],
693 builder: &mut CommitBuilder,
694) {
695 if !relocation_fits(indexed, moves) {
696 return;
697 }
698 for m in moves {
699 let Some(pin) = indexed.doc.pin(&m.pin) else {
700 continue;
701 };
702 if pin.slot != m.to {
703 builder.push(OpCodes::Pin(m.pin, Crud::Update(PinUpdate::Slot(m.to))));
704 }
705 }
706}
707
708#[derive(Clone, Copy, Debug, PartialEq, Eq)]
710pub struct SlotDelta(i32);
711
712impl SlotDelta {
713 pub fn new(slots: i32) -> Self {
714 Self(slots)
715 }
716}
717
718impl From<SlotDelta> for i32 {
719 fn from(delta: SlotDelta) -> i32 {
720 delta.0
721 }
722}
723
724pub fn nudge_pins(
728 indexed: &IndexedDocument<'_>,
729 pins: &[PinId],
730 delta: SlotDelta,
731 builder: &mut CommitBuilder,
732) {
733 let (mut lowest, mut highest) = (i32::MIN, i32::MAX);
734 let mut moving: Vec<(PinId, PinSlot)> = Vec::new();
735 for &id in pins {
736 let Some(pin) = indexed.doc.pin(&id) else {
737 continue;
738 };
739 let Some(owner) = indexed.doc.block(&pin.owner) else {
740 continue;
741 };
742 let slot = pin.slot;
743 lowest = lowest.max(-(slot.offset as i32));
744 highest = highest.min(slot_capacity(owner.rect.size.h) as i32 - slot.offset as i32);
745 moving.push((id, slot));
746 }
747 if lowest > highest {
748 return;
749 }
750 let delta = i32::from(delta).clamp(lowest, highest);
751 if delta == 0 {
752 return;
753 }
754 let moves: Vec<PinMove> = moving
755 .into_iter()
756 .map(|(pin, slot)| PinMove {
757 pin,
758 to: PinSlot {
759 offset: (slot.offset as i32 + delta).max(0) as u32,
760 ..slot
761 },
762 })
763 .collect();
764 relocate_pins(indexed, &moves, builder);
765}
766
767#[derive(Clone, Copy, Debug)]
769pub enum FlipTarget {
770 Block(BlockId),
771 Port(PinId),
772}
773
774pub fn flip_pins(indexed: &IndexedDocument<'_>, target: FlipTarget, builder: &mut CommitBuilder) {
784 match target {
785 FlipTarget::Block(id) => {
786 if indexed.doc.block(&id).is_none() {
790 return;
791 }
792 for (pin_id, pin) in owned_pins_ordered(indexed, id) {
793 let slot = pin.slot;
794 builder.push(OpCodes::Pin(
795 pin_id,
796 Crud::Update(PinUpdate::Slot(PinSlot {
797 side: slot.side.flip(),
798 ..slot
799 })),
800 ));
801 builder.push(OpCodes::Pin(
802 pin_id,
803 Crud::Update(PinUpdate::FlipLR(!pin.flip_lr)),
804 ));
805 }
806 }
807 FlipTarget::Port(id) => {
808 let Some(pin) = indexed.doc.pin(&id) else {
809 return;
810 };
811 builder.push(OpCodes::Pin(
812 id,
813 Crud::Update(PinUpdate::FlipLR(!pin.flip_lr)),
814 ));
815 }
816 }
817}
818
819pub fn flip_vertical(indexed: &IndexedDocument<'_>, block: BlockId, builder: &mut CommitBuilder) {
823 let Some(live) = indexed.doc.block(&block) else {
826 return;
827 };
828 let capacity = slot_capacity(live.rect.size.h);
829 for (id, pin) in owned_pins_ordered(indexed, block) {
830 let slot = pin.slot;
831 let mirrored = PinSlot {
832 offset: capacity.saturating_sub(slot.offset),
833 ..slot
834 };
835 if mirrored != slot {
836 builder.push(OpCodes::Pin(id, Crud::Update(PinUpdate::Slot(mirrored))));
837 }
838 }
839}
840
841pub fn place_wire_label(
844 doc: &Document,
845 label: RouteLabelId,
846 pos: FracVal,
847 builder: &mut CommitBuilder,
848) {
849 let Some(live) = doc.route_label(&label) else {
850 return;
851 };
852 if live.pos == pos {
853 return;
854 }
855 builder.push(OpCodes::RouteLabel(
856 label,
857 Crud::Update(RouteLabelUpdate::Pos(pos)),
858 ));
859}
860
861#[derive(Clone, Debug)]
865pub struct RouteEdit<'a> {
866 pub route: RouteId,
867 pub waypoints: Vec<Waypoint>,
868 pub anchors: &'a [(RouteLabelId, FracVal)],
869}
870
871pub fn commit_route_edit(doc: &Document, edit: RouteEdit<'_>, builder: &mut CommitBuilder) {
874 let Some(route) = doc.route(&edit.route) else {
875 return;
876 };
877 if edit.waypoints != route.waypoints {
878 builder.push(OpCodes::Route(
879 edit.route,
880 Crud::Update(RouteUpdate::Waypoints(edit.waypoints)),
881 ));
882 }
883 for &(label, pos) in edit.anchors {
884 place_wire_label(doc, label, pos, builder);
885 }
886}
887
888pub fn reroute(
900 indexed: &IndexedDocument<'_>,
901 route: RouteId,
902 solved: &[(RouteId, Vec<Waypoint>)],
903 builder: &mut CommitBuilder,
904) {
905 let Some(live) = indexed.doc.route(&route) else {
906 return;
907 };
908 let stored = live.waypoints.clone();
909 let waypoints: Vec<Waypoint> = solved
910 .iter()
911 .find_map(|(id, list)| (*id == route).then(|| list.clone()))
912 .unwrap_or_default();
913 if waypoints != stored {
914 builder.push(OpCodes::Route(
915 route,
916 Crud::Update(RouteUpdate::Waypoints(waypoints)),
917 ));
918 }
919}
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924 use crate::edit::harness::{
925 block_create, fold, image_create, pin_create, route_create, route_label_create,
926 seals_to_nothing, wired,
927 };
928 use crate::grid::GRID_SIZE;
929 use blockworx_doc::document::DocIndex;
930 use blockworx_doc::{
931 block_model::{Area, Image, Text},
932 fixtures::{area_id, block_id, image_id, pin_id, route_id, route_label_id, text_id},
933 values::PinSide,
934 };
935 use blockworx_geom::{pos2, vec2};
936
937 fn by(x: f32, y: f32) -> Vec2 {
939 vec2(x * GRID_SIZE, y * GRID_SIZE)
940 }
941
942 fn cells(min: (f32, f32), max: (f32, f32)) -> Rect {
944 Rect::from_min_max(
945 pos2(min.0 * GRID_SIZE, min.1 * GRID_SIZE),
946 pos2(max.0 * GRID_SIZE, max.1 * GRID_SIZE),
947 )
948 }
949
950 fn rect(x: i32, y: i32, w: u32, h: u32) -> GridRect {
951 GridRect {
952 top_left: GridPoint { x, y },
953 size: GridSize { w, h },
954 }
955 }
956
957 fn slot(side: PinSide, offset: u32) -> PinSlot {
958 PinSlot { side, offset }
959 }
960
961 fn corner(x: i32, y: i32) -> Waypoint {
963 Waypoint {
964 pos: GridPoint { x, y },
965 locked: false,
966 }
967 }
968
969 fn pinned(x: i32, y: i32) -> Waypoint {
971 Waypoint {
972 locked: true,
973 ..corner(x, y)
974 }
975 }
976
977 fn asset() -> AssetHash {
981 AssetHash::of(b"icon")
982 }
983
984 fn place_block(id: u32, to: GridRect) -> OpCodes {
985 OpCodes::Block(block_id(id), Crud::Update(BlockUpdate::Rect(to)))
986 }
987
988 fn reparent(child: u32, parent: u32) -> OpCodes {
989 OpCodes::Block(
990 block_id(child),
991 Crud::Update(BlockUpdate::Parent(block_id(parent))),
992 )
993 }
994
995 fn lock(id: u32) -> OpCodes {
996 OpCodes::Block(block_id(id), Crud::Update(BlockUpdate::Locked(true)))
997 }
998
999 fn set_icon(id: u32, to: Rect) -> OpCodes {
1000 OpCodes::Block(
1001 block_id(id),
1002 Crud::Update(BlockUpdate::Icon(Icon {
1003 asset: asset(),
1004 rect: screen_rect(to),
1005 })),
1006 )
1007 }
1008
1009 fn place_pin(id: u32, to: GridRect) -> OpCodes {
1010 OpCodes::Pin(pin_id(id), Crud::Update(PinUpdate::Rect(to)))
1011 }
1012
1013 fn seat_pin(id: u32, to: PinSlot) -> OpCodes {
1014 OpCodes::Pin(pin_id(id), Crud::Update(PinUpdate::Slot(to)))
1015 }
1016
1017 fn place_image(id: u32, to: Rect) -> OpCodes {
1018 OpCodes::Image(
1019 image_id(id),
1020 Crud::Update(ImageUpdate::Rect(screen_rect(to))),
1021 )
1022 }
1023
1024 fn place_area(id: u32, to: GridRect) -> OpCodes {
1025 OpCodes::Area(area_id(id), Crud::Update(AreaUpdate::Rect(to)))
1026 }
1027
1028 fn wire(id: u32, waypoints: Vec<Waypoint>) -> OpCodes {
1029 OpCodes::Route(
1030 route_id(id),
1031 Crud::Update(RouteUpdate::Waypoints(waypoints)),
1032 )
1033 }
1034
1035 fn block_of(doc: &Document, id: u32) -> &Block {
1036 doc.block(&block_id(id)).expect("the scene's block exists")
1037 }
1038
1039 fn pin_of(doc: &Document, id: u32) -> &Pin {
1040 doc.pin(&pin_id(id)).expect("the scene's pin exists")
1041 }
1042
1043 fn text_of(doc: &Document, id: u32) -> &Text {
1044 doc.text(&text_id(id)).expect("the scene's text exists")
1045 }
1046
1047 fn area_of(doc: &Document, id: u32) -> &Area {
1048 doc.area(&area_id(id)).expect("the scene's area exists")
1049 }
1050
1051 fn image_of(doc: &Document, id: u32) -> &Image {
1052 doc.image(&image_id(id)).expect("the scene's image exists")
1053 }
1054
1055 fn box_of(doc: &Document, id: u32) -> GridRect {
1056 block_of(doc, id).rect
1057 }
1058
1059 fn icon_box(doc: &Document, id: u32) -> Rect {
1060 artwork_rect(
1061 icon_of(block_of(doc, id))
1062 .expect("the block carries an icon")
1063 .rect,
1064 )
1065 }
1066
1067 fn corners_of(doc: &Document, id: u32) -> Vec<Waypoint> {
1068 doc.route(&route_id(id))
1069 .expect("the scene's wire exists")
1070 .waypoints
1071 .clone()
1072 }
1073
1074 fn scene() -> Document {
1078 let doc = wired();
1079 let mut builder = CommitBuilder::new("Furnished an interior");
1080 builder.extend([
1081 block_create(10),
1082 block_create(11),
1083 reparent(10, 1),
1084 reparent(11, 1),
1085 place_block(1, rect(0, 0, 40, 40)),
1086 place_block(10, rect(0, 0, 8, 16)),
1087 place_block(11, rect(20, 0, 8, 16)),
1088 place_pin(3, rect(10, 20, 4, 2)),
1089 pin_create(12, 10),
1090 pin_create(13, 11),
1091 pin_create(14, 11),
1092 seat_pin(12, slot(PinSide::East, 0)),
1093 seat_pin(13, slot(PinSide::West, 0)),
1094 seat_pin(14, slot(PinSide::East, 1)),
1095 route_create(16, 1, 12, 13),
1096 route_create(17, 1, 14, 3),
1097 wire(16, vec![corner(9, 4), corner(15, 4), corner(19, 4)]),
1098 wire(17, vec![corner(30, 4), corner(35, 4), corner(38, 4)]),
1099 ]);
1100 let doc = fold(builder, &doc);
1101 assert!(
1102 !box_of(&doc, 10).intersects(box_of(&doc, 11)),
1103 "precondition: the two children start clear of each other"
1104 );
1105 assert!(
1106 !box_of(&doc, 10).intersects(pin_of(&doc, 3).rect)
1107 && !box_of(&doc, 11).intersects(pin_of(&doc, 3).rect),
1108 "precondition: the scope's own port body obstructs neither child"
1109 );
1110 doc
1111 }
1112
1113 #[test]
1114 fn move_shape_translates_a_block_and_the_icon_riding_it() {
1115 let doc = scene();
1116 let mut index = DocIndex::default();
1117 let mut builder = CommitBuilder::new("Gave the block an icon");
1118 builder.push(set_icon(10, cells((2.0, 4.0), (6.0, 8.0))));
1119 let doc = fold(builder, &doc);
1120
1121 let mut builder = CommitBuilder::new("Moved a block");
1122 move_shape(
1123 &index.view(&doc),
1124 Shape::Block(block_id(10)),
1125 by(0.0, 2.0),
1126 &mut builder,
1127 );
1128 let doc = fold(builder, &doc);
1129
1130 assert_eq!(box_of(&doc, 10), rect(0, 2, 8, 16));
1131 assert_eq!(
1132 icon_box(&doc, 10),
1133 cells((2.0, 6.0), (6.0, 10.0)),
1134 "the icon travels with its block"
1135 );
1136 }
1137
1138 #[test]
1139 fn a_single_move_is_rejected_by_any_overlap_at_its_destination() {
1140 let doc = scene();
1141 let mut index = DocIndex::default();
1142 let destination = box_of(&doc, 10).translate(GridVec::new(20, 0));
1143 assert!(
1144 destination.intersects(box_of(&doc, 11)),
1145 "precondition: the destination lands on the neighbour"
1146 );
1147
1148 let mut builder = CommitBuilder::new("Moved a block onto its neighbour");
1149 move_shape(
1150 &index.view(&doc),
1151 Shape::Block(block_id(10)),
1152 by(20.0, 0.0),
1153 &mut builder,
1154 );
1155 seals_to_nothing(builder);
1156 }
1157
1158 #[test]
1159 fn only_a_group_move_exempts_an_overlap_the_shapes_already_had() {
1160 let doc = scene();
1161 let mut index = DocIndex::default();
1162 let mut builder = CommitBuilder::new("Parked the pair on each other");
1163 builder.push(place_block(11, rect(4, 0, 8, 16)));
1164 let doc = fold(builder, &doc);
1165 assert!(
1166 box_of(&doc, 10).intersects(box_of(&doc, 11)),
1167 "precondition: the two already overlap, so only a *new* overlap can be created"
1168 );
1169
1170 let mut builder = CommitBuilder::new("Nudged one of them");
1171 move_shape(
1172 &index.view(&doc),
1173 Shape::Block(block_id(10)),
1174 by(-1.0, 0.0),
1175 &mut builder,
1176 );
1177 seals_to_nothing(builder);
1178
1179 let mut builder = CommitBuilder::new("Nudged it as a group");
1180 move_group(
1181 &index.view(&doc),
1182 &[Shape::Block(block_id(10))],
1183 by(-1.0, 0.0),
1184 &mut builder,
1185 );
1186 let doc = fold(builder, &doc);
1187 assert_eq!(
1188 box_of(&doc, 10),
1189 rect(-1, 0, 8, 16),
1190 "the group rule rejects only the overlaps a move creates"
1191 );
1192 }
1193
1194 #[test]
1195 fn move_shape_slides_a_port_body_a_text_box_and_an_area_on_the_grid() {
1196 let doc = scene();
1197 let mut index = DocIndex::default();
1198 let mut builder = CommitBuilder::new("Sized the area");
1199 builder.push(place_area(8, rect(2, 2, 6, 4)));
1200 let doc = fold(builder, &doc);
1201
1202 let mut builder = CommitBuilder::new("Moved the annotations");
1203 move_shape(
1204 &index.view(&doc),
1205 Shape::Port(pin_id(3)),
1206 by(2.0, 3.0),
1207 &mut builder,
1208 );
1209 move_shape(
1210 &index.view(&doc),
1211 Shape::Text(text_id(7)),
1212 by(1.4, -0.4),
1213 &mut builder,
1214 );
1215 move_shape(
1216 &index.view(&doc),
1217 Shape::Area(area_id(8)),
1218 by(1.0, 1.0),
1219 &mut builder,
1220 );
1221 let doc = fold(builder, &doc);
1222
1223 assert_eq!(pin_of(&doc, 3).rect, rect(12, 23, 4, 2));
1224 assert_eq!(
1225 text_of(&doc, 7).pos,
1226 GridPoint { x: 1, y: 0 },
1227 "a raw drag commits at the cell it snapped to"
1228 );
1229 assert_eq!(area_of(&doc, 8).rect, rect(3, 3, 6, 4));
1230 }
1231
1232 #[test]
1233 fn artwork_moves_in_free_pixels_and_an_icon_stays_inside_its_block() {
1234 let doc = scene();
1235 let mut index = DocIndex::default();
1236 let mut builder = CommitBuilder::new("Placed the artwork");
1237 builder.extend([
1238 image_create(20, 1),
1239 place_image(20, cells((10.0, 10.0), (14.0, 14.0))),
1240 set_icon(10, cells((2.0, 4.0), (6.0, 8.0))),
1241 ]);
1242 let doc = fold(builder, &doc);
1243
1244 let mut builder = CommitBuilder::new("Nudged the artwork");
1245 move_shape(
1246 &index.view(&doc),
1247 Shape::Image(image_id(20)),
1248 vec2(7.5, -3.25),
1249 &mut builder,
1250 );
1251 move_shape(
1252 &index.view(&doc),
1253 Shape::Icon(block_id(10)),
1254 vec2(1000.0, 1000.0),
1255 &mut builder,
1256 );
1257 let doc = fold(builder, &doc);
1258
1259 assert_eq!(
1260 artwork_rect(image_of(&doc, 20).rect),
1261 cells((10.0, 10.0), (14.0, 14.0)).translate(vec2(7.5, -3.25)),
1262 "an image keeps the pixels the drag gave it — no snap"
1263 );
1264 let block = px_rect(box_of(&doc, 10));
1265 let icon = icon_box(&doc, 10);
1266 assert!(
1267 block.contains_rect(icon),
1268 "the icon is held inside the block it belongs to"
1269 );
1270 assert_eq!(
1271 icon,
1272 Rect::from_min_max(block.max - vec2(60.0, 60.0), block.max),
1273 "a shove past the corner shifts the icon back, without shrinking it"
1274 );
1275 }
1276
1277 #[test]
1278 fn a_move_of_nothing_and_a_move_of_a_ghost_push_nothing() {
1279 let doc = scene();
1280 let mut index = DocIndex::default();
1281
1282 let mut builder = CommitBuilder::new("Moved a block by nothing");
1283 move_shape(
1284 &index.view(&doc),
1285 Shape::Block(block_id(10)),
1286 Vec2::ZERO,
1287 &mut builder,
1288 );
1289 seals_to_nothing(builder);
1290
1291 let mut builder = CommitBuilder::new("Moved a block by half a cell");
1292 move_shape(
1293 &index.view(&doc),
1294 Shape::Block(block_id(10)),
1295 by(0.4, 0.0),
1296 &mut builder,
1297 );
1298 seals_to_nothing(builder);
1299
1300 let mut builder = CommitBuilder::new("Moved a ghost");
1301 move_shape(
1302 &index.view(&doc),
1303 Shape::Block(block_id(99)),
1304 by(1.0, 0.0),
1305 &mut builder,
1306 );
1307 seals_to_nothing(builder);
1308
1309 let mut builder = CommitBuilder::new("Moved a ghost group");
1310 move_group(
1311 &index.view(&doc),
1312 &[Shape::Image(image_id(99)), Shape::Block(block_id(99))],
1313 by(1.0, 0.0),
1314 &mut builder,
1315 );
1316 seals_to_nothing(builder);
1317 }
1318
1319 #[test]
1320 fn move_group_shifts_every_member_by_one_snapped_delta() {
1321 let doc = scene();
1322 let mut index = DocIndex::default();
1323 let mut builder = CommitBuilder::new("Placed the annotations");
1324 builder.extend([
1325 image_create(20, 1),
1326 place_image(20, cells((10.0, 10.0), (14.0, 14.0))),
1327 place_area(8, rect(2, 2, 6, 4)),
1328 ]);
1329 let doc = fold(builder, &doc);
1330
1331 let mut builder = CommitBuilder::new("Moved a selection");
1332 move_group(
1333 &index.view(&doc),
1334 &[
1335 Shape::Block(block_id(10)),
1336 Shape::Block(block_id(11)),
1337 Shape::Text(text_id(7)),
1338 Shape::Area(area_id(8)),
1339 Shape::Image(image_id(20)),
1340 ],
1341 by(1.4, 0.0),
1342 &mut builder,
1343 );
1344 let doc = fold(builder, &doc);
1345
1346 assert_eq!(box_of(&doc, 10), rect(1, 0, 8, 16));
1347 assert_eq!(box_of(&doc, 11), rect(21, 0, 8, 16));
1348 assert_eq!(text_of(&doc, 7).pos, GridPoint { x: 1, y: 0 });
1349 assert_eq!(area_of(&doc, 8).rect, rect(3, 2, 6, 4));
1350 assert_eq!(
1351 artwork_rect(image_of(&doc, 20).rect),
1352 cells((11.0, 10.0), (15.0, 14.0)),
1353 "an image in a group takes the shared snapped delta, unlike a single move"
1354 );
1355 }
1356
1357 #[test]
1358 fn a_group_move_onto_a_clear_neighbour_is_rejected_wholesale() {
1359 let doc = scene();
1360 let mut index = DocIndex::default();
1361 let destination = box_of(&doc, 10).translate(GridVec::new(20, 0));
1362 assert!(
1363 !box_of(&doc, 10).intersects(box_of(&doc, 11))
1364 && destination.intersects(box_of(&doc, 11)),
1365 "precondition: the neighbour is clear now and covered at the destination"
1366 );
1367
1368 let mut builder = CommitBuilder::new("Moved a selection onto a neighbour");
1369 move_group(
1370 &index.view(&doc),
1371 &[Shape::Block(block_id(10)), Shape::Text(text_id(7))],
1372 by(20.0, 0.0),
1373 &mut builder,
1374 );
1375 seals_to_nothing(builder);
1376 }
1377
1378 #[test]
1379 fn a_wire_with_both_ends_riding_translates_its_corners_rigidly() {
1380 let doc = scene();
1381 let mut index = DocIndex::default();
1382 assert_eq!(
1383 (
1384 pin_of(&doc, 12).owner,
1385 pin_of(&doc, 13).owner,
1386 pin_of(&doc, 3).owner
1387 ),
1388 (block_id(10), block_id(11), block_id(1)),
1389 "precondition: wire 16 runs between the two moved blocks and wire 17 leaves them"
1390 );
1391
1392 let mut builder = CommitBuilder::new("Moved the pair");
1393 move_group(
1394 &index.view(&doc),
1395 &[Shape::Block(block_id(10)), Shape::Block(block_id(11))],
1396 by(0.0, 4.0),
1397 &mut builder,
1398 );
1399 let doc = fold(builder, &doc);
1400
1401 assert_eq!(
1402 corners_of(&doc, 16),
1403 vec![corner(9, 8), corner(15, 8), corner(19, 8)],
1404 "a wire that keeps its shape shifts its corners by the same cell delta"
1405 );
1406 assert_eq!(
1407 corners_of(&doc, 17),
1408 vec![corner(38, 4)],
1409 "the straddling wire drops its stale approach on the moved side"
1410 );
1411 }
1412
1413 #[test]
1414 fn a_straddling_wire_trims_from_the_end_that_moved() {
1415 let doc = scene();
1416 let mut index = DocIndex::default();
1417 let mut builder = CommitBuilder::new("Moved one block");
1418 move_group(
1419 &index.view(&doc),
1420 &[Shape::Block(block_id(11))],
1421 by(0.0, 4.0),
1422 &mut builder,
1423 );
1424 let moved = fold(builder, &doc);
1425
1426 assert_eq!(
1427 corners_of(&moved, 16),
1428 vec![corner(9, 4)],
1429 "wire 16 ends on the moved block, so its trailing corners go"
1430 );
1431 assert_eq!(
1432 corners_of(&moved, 17),
1433 vec![corner(38, 4)],
1434 "wire 17 starts on the moved block, so its leading corners go"
1435 );
1436 }
1437
1438 #[test]
1439 fn a_trim_stops_at_the_first_corner_the_user_placed() {
1440 let trimmed = |corners: Vec<Waypoint>| {
1441 let doc = scene();
1442 let mut builder = CommitBuilder::new("Pinned a corner");
1443 builder.push(wire(17, corners));
1444 let doc = fold(builder, &doc);
1445
1446 let mut builder = CommitBuilder::new("Moved one block");
1447 move_group(
1448 &DocIndex::default().view(&doc),
1449 &[Shape::Block(block_id(11))],
1450 by(0.0, 4.0),
1451 &mut builder,
1452 );
1453 corners_of(&fold(builder, &doc), 17)
1454 };
1455
1456 assert_eq!(
1457 trimmed(vec![pinned(30, 4), corner(35, 4), corner(38, 4)]),
1458 vec![pinned(30, 4), corner(35, 4), corner(38, 4)],
1459 "a locked corner nearest the moved end stops the trim before it starts"
1460 );
1461 assert_eq!(
1462 trimmed(vec![corner(30, 4), pinned(35, 4), corner(38, 4)]),
1463 vec![pinned(35, 4), corner(38, 4)],
1464 "the trim stops at the first locked corner"
1465 );
1466 assert_eq!(
1467 trimmed(vec![
1468 corner(30, 4),
1469 corner(35, 4),
1470 corner(38, 4),
1471 corner(39, 4)
1472 ]),
1473 vec![corner(38, 4), corner(39, 4)],
1474 "at most two corners go, however many are stale"
1475 );
1476 }
1477
1478 #[test]
1479 fn a_dragged_port_trims_the_wires_that_end_on_it() {
1480 let doc = scene();
1481 let mut index = DocIndex::default();
1482 let mut builder = CommitBuilder::new("Moved a port body");
1483 move_group(
1484 &index.view(&doc),
1485 &[Shape::Port(pin_id(3))],
1486 by(1.0, 0.0),
1487 &mut builder,
1488 );
1489 let doc = fold(builder, &doc);
1490
1491 assert_eq!(pin_of(&doc, 3).rect, rect(11, 20, 4, 2));
1492 assert_eq!(
1493 corners_of(&doc, 17),
1494 vec![corner(30, 4)],
1495 "the port is wire 17's far end, so the trim comes off the back"
1496 );
1497 }
1498
1499 #[test]
1504 fn a_move_leaves_the_moved_blocks_interior_wires_alone() {
1505 let doc = scene();
1506 let mut index = DocIndex::default();
1507 let mut builder = CommitBuilder::new("Wired the interior");
1510 builder.extend([
1511 block_create(30),
1512 reparent(30, 11),
1513 pin_create(31, 30),
1514 route_create(32, 11, 13, 31),
1515 wire(32, vec![corner(2, 2), corner(4, 2)]),
1516 ]);
1517 let doc = fold(builder, &doc);
1518 let interior = corners_of(&doc, 32);
1519 assert_eq!(interior.len(), 2, "precondition: the interior wire bends");
1520 assert!(
1521 index.view(&doc).index.routes_by_endpoint[&pin_id(13)].contains(&route_id(32)),
1522 "precondition: the interior wire really lands on the moved block's pin"
1523 );
1524
1525 let mut builder = CommitBuilder::new("Moved the block in its parent");
1526 move_group(
1527 &index.view(&doc),
1528 &[Shape::Block(block_id(11))],
1529 by(0.0, 4.0),
1530 &mut builder,
1531 );
1532 let doc = fold(builder, &doc);
1533
1534 assert_eq!(
1535 corners_of(&doc, 32),
1536 interior,
1537 "the interior wire is drawn in a scope the move never touched"
1538 );
1539 }
1540
1541 #[test]
1542 fn an_icon_co_selected_with_its_block_shifts_once() {
1543 let doc = scene();
1544 let mut index = DocIndex::default();
1545 let mut builder = CommitBuilder::new("Gave the block an icon");
1546 builder.push(set_icon(10, cells((2.0, 4.0), (6.0, 8.0))));
1547 let doc = fold(builder, &doc);
1548
1549 let mut builder = CommitBuilder::new("Moved a block with its icon selected");
1550 move_group(
1551 &index.view(&doc),
1552 &[Shape::Block(block_id(10)), Shape::Icon(block_id(10))],
1553 by(2.0, 0.0),
1554 &mut builder,
1555 );
1556 let doc = fold(builder, &doc);
1557
1558 assert_eq!(
1559 icon_box(&doc, 10),
1560 cells((4.0, 4.0), (8.0, 8.0)),
1561 "the icon shifts by one block move, not by two"
1562 );
1563 }
1564
1565 fn spanned() -> Document {
1568 let doc = scene();
1569 let mut builder = CommitBuilder::new("Spread the pins");
1570 builder.extend([
1571 pin_create(15, 10),
1572 seat_pin(12, slot(PinSide::East, 4)),
1573 seat_pin(15, slot(PinSide::West, 2)),
1574 ]);
1575 let doc = fold(builder, &doc);
1576 assert_eq!(
1577 slot_capacity(box_of(&doc, 10).size.h),
1578 4,
1579 "precondition: the block is exactly tall enough for its lowest pin"
1580 );
1581 doc
1582 }
1583
1584 #[test]
1585 fn resize_writes_the_new_box_and_rides_the_pins_up_when_they_stop_fitting() {
1586 let doc = spanned();
1587 let mut index = DocIndex::default();
1588 let mut builder = CommitBuilder::new("Gave the block an icon");
1589 builder.push(set_icon(10, cells((2.0, 4.0), (6.0, 8.0))));
1590 let doc = fold(builder, &doc);
1591 assert_eq!(
1592 slot_capacity(4),
1593 0,
1594 "precondition: four cells hold slot 0 alone"
1595 );
1596
1597 let mut builder = CommitBuilder::new("Shrank a block");
1598 resize(
1599 &index.view(&doc),
1600 ResizeTarget::Block(block_id(10)),
1601 cells((0.0, 0.0), (8.0, 4.0)),
1602 &mut builder,
1603 );
1604 let doc = fold(builder, &doc);
1605
1606 assert_eq!(box_of(&doc, 10), rect(0, 0, 8, 4));
1607 assert_eq!(
1608 (pin_of(&doc, 12).slot.offset, pin_of(&doc, 15).slot.offset),
1609 (2, 0),
1610 "the group rides up by the highest pin's own offset, keeping its span"
1611 );
1612 assert_eq!(
1613 pin_of(&doc, 12).slot,
1614 slot(PinSide::East, 2),
1615 "a pin keeps the edge it sits on"
1616 );
1617 assert_eq!(
1618 icon_box(&doc, 10),
1619 cells((2.0, 0.0), (6.0, 4.0)),
1620 "the icon rides the block's center, then is held inside the smaller box"
1621 );
1622 }
1623
1624 #[test]
1625 fn a_resize_that_still_fits_its_pins_leaves_them_alone() {
1626 let doc = spanned();
1627 let mut index = DocIndex::default();
1628 let mut builder = CommitBuilder::new("Widened a block");
1629 resize(
1630 &index.view(&doc),
1631 ResizeTarget::Block(block_id(10)),
1632 cells((0.0, 0.0), (12.0, 16.0)),
1633 &mut builder,
1634 );
1635 let commit = builder.seal().expect("the block widened");
1636 assert_eq!(
1637 commit.ops().len(),
1638 1,
1639 "a resize with room to spare is one write"
1640 );
1641 let doc = doc
1642 .try_apply(&commit)
1643 .expect("the fold accepts the gesture");
1644
1645 assert_eq!(box_of(&doc, 10), rect(0, 0, 12, 16));
1646 assert_eq!(pin_of(&doc, 12).slot.offset, 4);
1647 }
1648
1649 #[test]
1650 fn resize_keeps_a_ports_slot_row_and_canonical_height() {
1651 let doc = scene();
1652 let mut index = DocIndex::default();
1653 let mut builder = CommitBuilder::new("Widened a port body");
1654 resize(
1655 &index.view(&doc),
1656 ResizeTarget::Port(pin_id(3)),
1657 cells((9.0, 0.0), (15.0, 9.0)),
1658 &mut builder,
1659 );
1660 let doc = fold(builder, &doc);
1661
1662 assert_eq!(
1663 pin_of(&doc, 3).rect,
1664 rect(9, 20, 6, PORT_HEIGHT),
1665 "only the left edge and the width move"
1666 );
1667 }
1668
1669 #[test]
1670 fn resize_writes_an_area_on_the_grid_and_an_image_in_free_pixels() {
1671 let doc = scene();
1672 let mut index = DocIndex::default();
1673 let mut builder = CommitBuilder::new("Placed an image");
1674 builder.extend([
1675 image_create(20, 1),
1676 place_image(20, cells((10.0, 10.0), (14.0, 14.0))),
1677 ]);
1678 let doc = fold(builder, &doc);
1679
1680 let mut builder = CommitBuilder::new("Resized the annotations");
1681 resize(
1682 &index.view(&doc),
1683 ResizeTarget::Area(area_id(8)),
1684 cells((1.0, 1.0), (5.4, 4.0)),
1685 &mut builder,
1686 );
1687 resize(
1688 &index.view(&doc),
1689 ResizeTarget::Image(image_id(20)),
1690 Rect::from_min_max(pos2(150.0, 150.0), pos2(207.5, 190.0)),
1691 &mut builder,
1692 );
1693 let doc = fold(builder, &doc);
1694
1695 assert_eq!(area_of(&doc, 8).rect, rect(1, 1, 4, 3));
1696 assert_eq!(
1697 artwork_rect(image_of(&doc, 20).rect),
1698 Rect::from_min_max(pos2(150.0, 150.0), pos2(207.5, 190.0)),
1699 "artwork resizes off the grid"
1700 );
1701 }
1702
1703 #[test]
1704 fn a_resize_to_the_box_it_already_has_pushes_nothing() {
1705 let doc = scene();
1706 let mut index = DocIndex::default();
1707
1708 let mut builder = CommitBuilder::new("Resized a block to itself");
1709 resize(
1710 &index.view(&doc),
1711 ResizeTarget::Block(block_id(10)),
1712 cells((0.0, 0.0), (8.0, 16.0)),
1713 &mut builder,
1714 );
1715 seals_to_nothing(builder);
1716
1717 let mut builder = CommitBuilder::new("Resized a ghost");
1718 resize(
1719 &index.view(&doc),
1720 ResizeTarget::Area(area_id(99)),
1721 cells((0.0, 0.0), (4.0, 4.0)),
1722 &mut builder,
1723 );
1724 seals_to_nothing(builder);
1725 }
1726
1727 #[test]
1728 fn move_pin_takes_a_free_slot_and_declines_a_taken_one() {
1729 let doc = scene();
1730 let mut index = DocIndex::default();
1731 assert_eq!(
1732 pin_of(&doc, 14).slot,
1733 slot(PinSide::East, 1),
1734 "precondition: the destination below is already held by a sibling"
1735 );
1736
1737 let mut builder = CommitBuilder::new("Dropped a pin on a taken slot");
1738 move_pin(
1739 &index.view(&doc),
1740 pin_id(13),
1741 slot(PinSide::East, 1),
1742 &mut builder,
1743 );
1744 seals_to_nothing(builder);
1745
1746 let mut builder = CommitBuilder::new("Dropped a pin where it already sits");
1747 move_pin(
1748 &index.view(&doc),
1749 pin_id(13),
1750 slot(PinSide::West, 0),
1751 &mut builder,
1752 );
1753 seals_to_nothing(builder);
1754
1755 let mut builder = CommitBuilder::new("Dropped a pin on a free slot");
1756 move_pin(
1757 &index.view(&doc),
1758 pin_id(13),
1759 slot(PinSide::East, 0),
1760 &mut builder,
1761 );
1762 let doc = fold(builder, &doc);
1763 assert_eq!(pin_of(&doc, 13).slot, slot(PinSide::East, 0));
1764 }
1765
1766 fn frozen() -> Document {
1769 let doc = scene();
1770 let mut builder = CommitBuilder::new("Locked a block");
1771 builder.push(lock(11));
1772 let doc = fold(builder, &doc);
1773 assert!(
1774 block_of(&doc, 11).locked,
1775 "precondition: the owner is locked"
1776 );
1777 doc
1778 }
1779
1780 #[test]
1788 fn a_frozen_interface_still_takes_every_slot_edit() {
1789 let doc = frozen();
1790 let mut index = DocIndex::default();
1791
1792 let mut builder = CommitBuilder::new("Dropped a frozen pin");
1793 move_pin(
1794 &index.view(&doc),
1795 pin_id(13),
1796 slot(PinSide::East, 0),
1797 &mut builder,
1798 );
1799 assert!(builder.seal().is_some(), "a locked pin still moves slots");
1800
1801 let mut builder = CommitBuilder::new("Relocated a frozen group");
1802 relocate_pins(
1803 &index.view(&doc),
1804 &[PinMove {
1805 pin: pin_id(13),
1806 to: slot(PinSide::East, 0),
1807 }],
1808 &mut builder,
1809 );
1810 assert!(builder.seal().is_some(), "and relocates as a group");
1811
1812 let mut builder = CommitBuilder::new("Nudged a frozen group");
1813 nudge_pins(
1814 &index.view(&doc),
1815 &[pin_id(13), pin_id(14)],
1816 SlotDelta::new(1),
1817 &mut builder,
1818 );
1819 assert!(builder.seal().is_some(), "and nudges");
1820
1821 let mut builder = CommitBuilder::new("Flipped a frozen block");
1822 flip_pins(
1823 &index.view(&doc),
1824 FlipTarget::Block(block_id(11)),
1825 &mut builder,
1826 );
1827 assert!(builder.seal().is_some(), "and flips left/right");
1828
1829 let mut builder = CommitBuilder::new("Flipped a frozen port");
1830 flip_pins(
1831 &index.view(&doc),
1832 FlipTarget::Port(pin_id(13)),
1833 &mut builder,
1834 );
1835 assert!(builder.seal().is_some(), "port included");
1836
1837 let mut builder = CommitBuilder::new("Mirrored a frozen block");
1838 flip_vertical(&index.view(&doc), block_id(11), &mut builder);
1839 assert!(builder.seal().is_some(), "and mirrors top/bottom");
1840 }
1841
1842 #[test]
1845 fn a_frozen_interface_still_moves_and_resizes() {
1846 let doc = frozen();
1847 let mut index = DocIndex::default();
1848 let mut builder = CommitBuilder::new("Moved and resized a locked block");
1849 move_shape(
1850 &index.view(&doc),
1851 Shape::Block(block_id(11)),
1852 by(1.0, 0.0),
1853 &mut builder,
1854 );
1855 resize(
1856 &index.view(&doc),
1857 ResizeTarget::Block(block_id(11)),
1858 cells((20.0, 0.0), (30.0, 16.0)),
1859 &mut builder,
1860 );
1861 let doc = fold(builder, &doc);
1862
1863 assert_eq!(
1864 box_of(&doc, 11),
1865 rect(20, 0, 10, 16),
1866 "the resize is the later write of the two"
1867 );
1868 }
1869
1870 #[test]
1871 fn relocate_pins_swaps_a_group_through_the_slots_it_vacates() {
1872 let doc = scene();
1873 let mut index = DocIndex::default();
1874 let mut builder = CommitBuilder::new("Swapped two pins");
1875 relocate_pins(
1876 &index.view(&doc),
1877 &[
1878 PinMove {
1879 pin: pin_id(13),
1880 to: slot(PinSide::East, 1),
1881 },
1882 PinMove {
1883 pin: pin_id(14),
1884 to: slot(PinSide::West, 0),
1885 },
1886 ],
1887 &mut builder,
1888 );
1889 let doc = fold(builder, &doc);
1890
1891 assert_eq!(pin_of(&doc, 13).slot, slot(PinSide::East, 1));
1892 assert_eq!(pin_of(&doc, 14).slot, slot(PinSide::West, 0));
1893 }
1894
1895 #[test]
1896 fn a_group_relocation_is_all_or_nothing() {
1897 let doc = scene();
1898 let mut index = DocIndex::default();
1899 let indexed = index.view(&doc);
1900 let refused = |moves: &[PinMove], label: &'static str| {
1901 let mut builder = CommitBuilder::new(label);
1902 relocate_pins(&indexed, moves, &mut builder);
1903 seals_to_nothing(builder);
1904 };
1905
1906 refused(
1907 &[PinMove {
1908 pin: pin_id(13),
1909 to: slot(PinSide::East, 1),
1910 }],
1911 "Moved one pin onto a sibling that stays put",
1912 );
1913 assert_eq!(
1914 slot_capacity(box_of(&doc, 11).size.h),
1915 4,
1916 "precondition: slot 5 is past the block's own capacity"
1917 );
1918 refused(
1919 &[
1920 PinMove {
1921 pin: pin_id(13),
1922 to: slot(PinSide::West, 1),
1923 },
1924 PinMove {
1925 pin: pin_id(14),
1926 to: slot(PinSide::East, 5),
1927 },
1928 ],
1929 "Moved a group off the end of its block",
1930 );
1931 refused(
1932 &[
1933 PinMove {
1934 pin: pin_id(13),
1935 to: slot(PinSide::West, 2),
1936 },
1937 PinMove {
1938 pin: pin_id(14),
1939 to: slot(PinSide::West, 2),
1940 },
1941 ],
1942 "Moved two pins onto one slot",
1943 );
1944 refused(
1945 &[PinMove {
1946 pin: pin_id(99),
1947 to: slot(PinSide::West, 2),
1948 }],
1949 "Moved a ghost",
1950 );
1951 }
1952
1953 #[test]
1954 fn nudge_pins_shifts_a_group_rigidly_and_clamps_at_the_block_edges() {
1955 let doc = scene();
1956 let mut index = DocIndex::default();
1957 assert_eq!(
1958 (pin_of(&doc, 13).slot.offset, pin_of(&doc, 14).slot.offset),
1959 (0, 1),
1960 "precondition: the group starts against the top edge"
1961 );
1962 let group = [pin_id(13), pin_id(14)];
1963
1964 let mut builder = CommitBuilder::new("Nudged the group up");
1965 nudge_pins(&index.view(&doc), &group, SlotDelta::new(-1), &mut builder);
1966 seals_to_nothing(builder);
1967
1968 let mut builder = CommitBuilder::new("Nudged the group down");
1969 nudge_pins(&index.view(&doc), &group, SlotDelta::new(1), &mut builder);
1970 let doc = fold(builder, &doc);
1971 assert_eq!(
1972 (pin_of(&doc, 13).slot.offset, pin_of(&doc, 14).slot.offset),
1973 (1, 2)
1974 );
1975
1976 let mut builder = CommitBuilder::new("Nudged the group off the bottom");
1977 nudge_pins(&index.view(&doc), &group, SlotDelta::new(10), &mut builder);
1978 let doc = fold(builder, &doc);
1979 assert_eq!(
1980 (pin_of(&doc, 13).slot.offset, pin_of(&doc, 14).slot.offset),
1981 (3, 4),
1982 "the shift clamps to the tightest range that keeps every pin on its block"
1983 );
1984 }
1985
1986 #[test]
1989 fn a_keyboard_nudge_is_one_cell_or_one_slot_through_the_rows_above() {
1990 let doc = scene();
1991 let mut index = DocIndex::default();
1992
1993 let mut builder = CommitBuilder::new("Nudged a block");
1994 move_shape(
1995 &index.view(&doc),
1996 Shape::Block(block_id(10)),
1997 by(-1.0, 0.0),
1998 &mut builder,
1999 );
2000 let moved = fold(builder, &doc);
2001 assert_eq!(box_of(&moved, 10), rect(-1, 0, 8, 16));
2002
2003 let mut builder = CommitBuilder::new("Nudged a selection");
2004 move_group(
2005 &index.view(&doc),
2006 &[Shape::Block(block_id(10)), Shape::Block(block_id(11))],
2007 by(0.0, 1.0),
2008 &mut builder,
2009 );
2010 let moved = fold(builder, &doc);
2011 assert_eq!(box_of(&moved, 10), rect(0, 1, 8, 16));
2012 assert_eq!(box_of(&moved, 11), rect(20, 1, 8, 16));
2013
2014 let mut builder = CommitBuilder::new("Nudged a pin");
2015 nudge_pins(
2016 &index.view(&doc),
2017 &[pin_id(14)],
2018 SlotDelta::new(1),
2019 &mut builder,
2020 );
2021 let moved = fold(builder, &doc);
2022 assert_eq!(pin_of(&moved, 14).slot, slot(PinSide::East, 2));
2023 }
2024
2025 #[test]
2026 fn flip_pins_moves_every_pin_to_the_far_edge_and_freezes_how_it_faces() {
2027 let doc = scene();
2028 let mut index = DocIndex::default();
2029 assert!(
2030 !pin_of(&doc, 13).flip_lr,
2031 "precondition: the pins face the default way — opposite their own edge"
2032 );
2033
2034 let mut builder = CommitBuilder::new("Flipped a block");
2035 flip_pins(
2036 &index.view(&doc),
2037 FlipTarget::Block(block_id(11)),
2038 &mut builder,
2039 );
2040 let flipped = fold(builder, &doc);
2041
2042 assert_eq!(pin_of(&flipped, 13).slot, slot(PinSide::East, 0));
2043 assert_eq!(pin_of(&flipped, 14).slot, slot(PinSide::West, 1));
2044 assert!(
2045 pin_of(&flipped, 13).flip_lr && pin_of(&flipped, 14).flip_lr,
2046 "the body keeps pointing the way it did, which on the far edge reads as flipped"
2047 );
2048
2049 let mut builder = CommitBuilder::new("Flipped it back");
2050 flip_pins(
2051 &index.view(&flipped),
2052 FlipTarget::Block(block_id(11)),
2053 &mut builder,
2054 );
2055 let back = fold(builder, &flipped);
2056 for id in [13, 14] {
2057 assert_eq!(pin_of(&back, id).slot, pin_of(&doc, id).slot);
2058 assert_eq!(
2059 pin_of(&back, id).flip_lr,
2060 pin_of(&doc, id).flip_lr,
2061 "a double flip is the identity"
2062 );
2063 }
2064 }
2065
2066 #[test]
2067 fn flipping_a_port_turns_its_body_around_and_leaves_its_slot() {
2068 let doc = scene();
2069 let mut index = DocIndex::default();
2070 let mut builder = CommitBuilder::new("Flipped a port");
2071 flip_pins(
2072 &index.view(&doc),
2073 FlipTarget::Port(pin_id(13)),
2074 &mut builder,
2075 );
2076 let flipped = fold(builder, &doc);
2077
2078 assert!(pin_of(&flipped, 13).flip_lr);
2079 assert_eq!(
2080 pin_of(&flipped, 13).slot,
2081 slot(PinSide::West, 0),
2082 "a port stays on the slot it occupies as a pin"
2083 );
2084
2085 let mut builder = CommitBuilder::new("Flipped it back");
2086 flip_pins(
2087 &index.view(&flipped),
2088 FlipTarget::Port(pin_id(13)),
2089 &mut builder,
2090 );
2091 let back = fold(builder, &flipped);
2092 assert!(!pin_of(&back, 13).flip_lr);
2093 }
2094
2095 #[test]
2096 fn flip_vertical_mirrors_the_slots_about_the_blocks_capacity() {
2097 let doc = scene();
2098 let mut index = DocIndex::default();
2099 let capacity = slot_capacity(box_of(&doc, 11).size.h);
2100 assert_eq!(capacity, 4, "precondition: the block holds five slots");
2101
2102 let mut builder = CommitBuilder::new("Mirrored a block");
2103 flip_vertical(&index.view(&doc), block_id(11), &mut builder);
2104 let flipped = fold(builder, &doc);
2105
2106 assert_eq!(pin_of(&flipped, 13).slot, slot(PinSide::West, 4));
2107 assert_eq!(pin_of(&flipped, 14).slot, slot(PinSide::East, 3));
2108
2109 let mut builder = CommitBuilder::new("Mirrored it back");
2110 flip_vertical(&index.view(&flipped), block_id(11), &mut builder);
2111 let back = fold(builder, &flipped);
2112 for id in [13, 14] {
2113 assert_eq!(
2114 pin_of(&back, id).slot,
2115 pin_of(&doc, id).slot,
2116 "a double mirror is the identity"
2117 );
2118 }
2119 }
2120
2121 #[test]
2122 fn place_wire_label_writes_the_arc_length_once() {
2123 let doc = scene();
2124 let mut builder = CommitBuilder::new("Labelled a wire");
2125 builder.push(route_label_create(6, 16));
2126 let doc = fold(builder, &doc);
2127
2128 let mut builder = CommitBuilder::new("Slid the label");
2129 place_wire_label(&doc, route_label_id(6), FracVal::from(12.5), &mut builder);
2130 let doc = fold(builder, &doc);
2131 assert_eq!(
2132 doc.route_label(&route_label_id(6))
2133 .expect("the label exists")
2134 .pos,
2135 FracVal::from(12.5)
2136 );
2137
2138 let mut builder = CommitBuilder::new("Slid it nowhere");
2139 place_wire_label(&doc, route_label_id(6), FracVal::from(12.5), &mut builder);
2140 seals_to_nothing(builder);
2141
2142 let mut builder = CommitBuilder::new("Slid a ghost label");
2143 place_wire_label(&doc, route_label_id(99), FracVal::from(1.0), &mut builder);
2144 seals_to_nothing(builder);
2145 }
2146
2147 #[test]
2148 fn commit_route_edit_writes_the_promoted_corners_and_re_anchors_the_labels() {
2149 let doc = scene();
2150 let mut builder = CommitBuilder::new("Labelled a wire twice");
2151 builder.extend([route_label_create(6, 16), route_label_create(9, 16)]);
2152 let doc = fold(builder, &doc);
2153
2154 let promoted = vec![pinned(9, 6), corner(15, 6), corner(19, 6)];
2155 let mut builder = CommitBuilder::new("Edited a route");
2156 commit_route_edit(
2157 &doc,
2158 RouteEdit {
2159 route: route_id(16),
2160 waypoints: promoted.clone(),
2161 anchors: &[
2162 (route_label_id(6), FracVal::from(4.5)),
2163 (route_label_id(9), FracVal::default()),
2164 ],
2165 },
2166 &mut builder,
2167 );
2168 let commit = builder.seal().expect("the edit produced ops");
2169 assert_eq!(
2170 commit.ops().len(),
2171 2,
2172 "a label already at its distance takes no write of its own"
2173 );
2174 let doc = doc
2175 .try_apply(&commit)
2176 .expect("the fold accepts the gesture");
2177
2178 assert_eq!(corners_of(&doc, 16), promoted);
2179 assert_eq!(
2180 doc.route_label(&route_label_id(6))
2181 .expect("the label exists")
2182 .pos,
2183 FracVal::from(4.5)
2184 );
2185
2186 let mut builder = CommitBuilder::new("Edited it to the shape it has");
2187 commit_route_edit(
2188 &doc,
2189 RouteEdit {
2190 route: route_id(16),
2191 waypoints: promoted,
2192 anchors: &[],
2193 },
2194 &mut builder,
2195 );
2196 seals_to_nothing(builder);
2197 }
2198
2199 #[test]
2200 fn reroute_writes_the_re_solved_list_for_one_wire() {
2201 let doc = scene();
2202 let mut index = DocIndex::default();
2203 let solved = vec![corner(12, 2)];
2204 let mut builder = CommitBuilder::new("Rerouted a wire");
2205 reroute(
2206 &index.view(&doc),
2207 route_id(16),
2208 &[(route_id(16), solved.clone())],
2209 &mut builder,
2210 );
2211 let doc = fold(builder, &doc);
2212 assert_eq!(corners_of(&doc, 16), solved);
2213 assert_eq!(
2214 corners_of(&doc, 17).len(),
2215 3,
2216 "the wire that was not asked for is untouched"
2217 );
2218
2219 let mut builder = CommitBuilder::new("Rerouted it to what it has");
2220 reroute(
2221 &index.view(&doc),
2222 route_id(16),
2223 &[(route_id(16), solved)],
2224 &mut builder,
2225 );
2226 seals_to_nothing(builder);
2227 }
2228
2229 #[test]
2233 fn a_wire_the_solver_left_out_keeps_the_bare_clear() {
2234 let doc = scene();
2235 let mut index = DocIndex::default();
2236 assert!(
2237 !corners_of(&doc, 16).is_empty(),
2238 "precondition: the wire has corners to lose"
2239 );
2240
2241 let mut builder = CommitBuilder::new("Rerouted a wire");
2242 reroute(&index.view(&doc), route_id(16), &[], &mut builder);
2243 let doc = fold(builder, &doc);
2244
2245 assert!(corners_of(&doc, 16).is_empty());
2246 }
2247}