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