1use std::collections::{BTreeMap, HashMap, HashSet};
10
11use blockworx_doc::{
12 commit::CommitBuilder,
13 document::IndexedDocument,
14 geometry::{GridPoint, GridVec, Waypoint},
15 id::{PinId, RouteId, RouteLabelId},
16 opcode::OpCodes,
17 values::PinSide,
18};
19use blockworx_geom::{Pos2, Rect, Vec2, vec2};
20use blockworx_router::{
21 Bounds, ClosedRouter, Resolution, RouterNGBuilder, WIRE_COST, cost::COST_ZERO, point::Point,
22 turtle::Mark,
23};
24
25use crate::{
26 edit::{
27 create::PathOrdinal,
28 geometry::{PinMove, RouteEnd, backtracking_ordinals, pruned_waypoints, trimmed_ordinals},
29 naming::InterfaceLock,
30 },
31 grid::{GRID_SIZE, grid_point, px_point, snap_to_grid},
32 path::BlockPath,
33 presentation::{Presentation, RouteGeometry},
34 shape::{ShapeId, ShapeRef},
35 widget::{
36 auto_route::{
37 label_anchors, reanchored, reroute_preview_closed, reroute_preview_excluding,
38 },
39 drawing::Drawing,
40 foreground::Foreground,
41 movement::moved_set_and_delta,
42 reconstruct::{
43 Endpoints, Obstacles, promote_corners_to_waypoints, reconstruct_corners_direct,
44 reconstruct_route, straighten_route,
45 },
46 waypoint_router::add_route_cost,
47 },
48};
49pub type Promoted = Vec<(RouteId, Vec<Waypoint>)>;
54
55#[derive(Clone, Copy)]
59pub struct Edited<'a> {
60 pub ops: &'a [OpCodes],
61 pub vacated: &'a [Rect],
62}
63
64#[derive(Clone, Copy)]
66enum Riding<'a> {
67 WholeScope,
70 Edited(Edited<'a>),
72}
73
74fn ride(
80 indexed: &IndexedDocument<'_>,
81 path: &BlockPath,
82 presentation: &Presentation,
83 riding: Riding<'_>,
84 sink: &mut CommitBuilder,
85) -> Option<Rect> {
86 let _s = tracing::info_span!("ride").entered();
87 let mut scratch = {
88 let _s = tracing::info_span!("rider_scratch").entered();
89 presentation.scratch(indexed)
90 };
91 let mut discarded = crate::gesture::Gesture::idle();
92 let (promoted, disturbed) = {
93 let mut drawing = Drawing::new(*indexed, path, &mut scratch, &mut discarded);
94 match riding {
95 Riding::WholeScope => (drawing.solve_routes(&[]), None),
96 Riding::Edited(edited) => {
97 let mut foreground = Foreground::written(edited.ops, &drawing);
98 for &vacated in edited.vacated {
99 foreground.also_disturbed_by(vacated, &drawing);
100 }
101 let was = foreground.extent(&drawing);
105 let promoted = drawing.solve_foreground(&foreground, &[]);
106 let now = foreground.extent(&drawing);
107 (promoted, Some(was.union(now)))
108 }
109 }
110 };
111 debug_assert!(
112 discarded.ops().is_empty(),
113 "the solve is a read; it must author nothing of its own"
114 );
115 for (route, waypoints) in promoted {
116 crate::edit::geometry::commit_route_edit(
117 indexed.doc,
118 crate::edit::geometry::RouteEdit {
119 route,
120 waypoints,
121 anchors: &[],
122 },
123 sink,
124 );
125 }
126 disturbed
127}
128
129pub(crate) fn solve_rider(
141 indexed: &IndexedDocument<'_>,
142 path: &BlockPath,
143 presentation: &Presentation,
144 edited: Edited<'_>,
145 sink: &mut CommitBuilder,
146) -> Option<Rect> {
147 ride(indexed, path, presentation, Riding::Edited(edited), sink)
148}
149
150pub(crate) fn settle_scope(
154 indexed: &IndexedDocument<'_>,
155 path: &BlockPath,
156 presentation: &Presentation,
157 sink: &mut CommitBuilder,
158) {
159 ride(indexed, path, presentation, Riding::WholeScope, sink);
160}
161
162pub fn obstacle_rect(rect: Rect) -> blockworx_router::block::Block {
166 blockworx_router::block::Block {
167 top_left: rect.left_top().into(),
168 bottom_right: rect.right_bottom().into(),
169 }
170}
171
172struct Seeding<'a> {
175 hypo: GeometryOverrides<'a>,
176 ids: &'a [RouteId],
177 endpoints: &'a [(RouteId, Pos2, Pos2)],
178 exclusions: &'a HashMap<RouteId, PreviewExclusions>,
179 offsets: &'a HashMap<RouteId, GridVec>,
180}
181
182struct Rerouting {
186 ends: Endpoints,
187 stored: Vec<Waypoint>,
188 rigid: Option<GridVec>,
189}
190
191fn seeds_within(drawing: &Drawing<'_>, seeding: &Seeding<'_>, within: Region) -> Vec<Point> {
198 let mut seed_points: Vec<Point> = Vec::with_capacity(seeding.endpoints.len() * 2);
199 let bounds = within.bounds();
200 let mut take = |p: Point| {
201 if bounds.holds(p) {
202 seed_points.push(p);
203 }
204 };
205 for &(_, s, e) in seeding.endpoints {
206 take(snap_to_grid(s).into());
207 take(snap_to_grid(e).into());
208 }
209 for &id in seeding.ids {
210 for pos in seeded_waypoints(drawing, seeding, id) {
211 take(pos.into());
212 }
213 }
214 seed_points
215}
216
217fn seeded_waypoints<'a>(
220 drawing: &'a Drawing<'_>,
221 seeding: &'a Seeding<'_>,
222 id: RouteId,
223) -> impl Iterator<Item = GridPoint> + 'a {
224 let excluded = seeding.exclusions.get(&id);
225 let delta = seeding.offsets.get(&id).copied();
226 drawing
227 .route(id)
228 .into_iter()
229 .flat_map(|route| route.waypoints.iter().enumerate())
230 .filter(move |(index, _)| {
231 !excluded.is_some_and(|x| x.excluded.contains(&PathOrdinal::new(*index)))
232 })
233 .map(move |(_, wp)| delta.map_or(wp.pos, |delta| wp.pos + delta))
234}
235
236fn still_holds(geometry: &RouteGeometry, start: Pos2, end: Pos2, obstacles: &Obstacles) -> bool {
239 geometry.start_pos() == start
240 && geometry.end_pos() == end
241 && !geometry
242 .iter_edges()
243 .any(|(_, e)| obstacles.blocked(e.start, e.end))
244}
245
246fn healing_region(reach: impl IntoIterator<Item = GridPoint>) -> Rect {
255 let margin = 2.0 * blockworx_router::block::MOAT_REACH as f32 * GRID_SIZE;
256 let mut region: Option<Rect> = None;
257 for p in reach {
258 let at = px_point(p);
259 let here = Rect::from_min_max(at, at);
260 region = Some(region.map_or(here, |so_far| so_far.union(here)));
261 }
262 region.unwrap_or(Rect::ZERO).expand(margin)
263}
264
265#[derive(Clone, Copy)]
269pub(crate) enum Reconstructing<'a> {
270 Everything,
273 Within {
277 disturbed: Rect,
278 previewed: &'a std::collections::HashSet<RouteId>,
279 },
280 These(&'a std::collections::HashSet<RouteId>),
282}
283
284#[derive(Clone, Copy, Debug)]
288pub(crate) enum Region {
289 WholeScope,
290 Within(Rect),
291}
292
293impl Region {
294 fn bounds(self) -> Bounds {
298 match self {
299 Region::WholeScope => Bounds::UNBOUNDED,
300 Region::Within(region) => Bounds::between(region.min, region.max),
301 }
302 }
303}
304
305#[derive(Clone, Copy)]
308enum Solving<'a> {
309 Everything,
310 Only(&'a Foreground),
311}
312
313impl Solving<'_> {
314 fn may_solve(self, id: RouteId) -> bool {
315 match self {
316 Solving::Everything => true,
317 Solving::Only(foreground) => foreground.holds_route(id),
318 }
319 }
320}
321
322enum RoutePass<'a> {
326 Preview(PreviewSpec<'a>),
327 Commit,
328}
329
330struct PreviewSpec<'a> {
336 grid_delta: GridVec,
337 inside: &'a dyn Fn(PinId) -> bool,
338}
339
340type RelaidRoute = (
343 Vec<Waypoint>,
344 Vec<(RouteLabelId, blockworx_doc::geometry::FracVal)>,
345);
346
347pub struct PinSlotOverride {
350 pub pin: PinId,
351 pub side: PinSide,
352 pub offset: u32,
353}
354
355#[derive(Clone, Copy, Default)]
359struct GeometryOverrides<'a> {
360 rects: &'a [(ShapeId, Rect)],
361 pins: &'a [PinSlotOverride],
362}
363
364#[derive(Default)]
370struct PreviewExclusions {
371 excluded: HashSet<PathOrdinal>,
372 pruned: bool,
373}
374
375pub struct RouteEditSession {
383 slots: Vec<WaypointSlot>,
384 anchors: Vec<(RouteLabelId, Pos2)>,
385}
386
387enum WaypointSlot {
390 Existing(PathOrdinal),
393 InsertAt(usize),
398}
399
400fn keyed_corners<T>(
405 waypoints: &[Waypoint],
406 value: impl Fn(&Waypoint) -> T,
407) -> Vec<(Option<PathOrdinal>, T)> {
408 waypoints
409 .iter()
410 .enumerate()
411 .map(|(index, wp)| (Some(PathOrdinal::new(index)), value(wp)))
412 .collect()
413}
414
415impl RouteEditSession {
416 pub fn anchors(&self) -> &[(RouteLabelId, Pos2)] {
419 &self.anchors
420 }
421
422 pub fn corners(
426 &self,
427 waypoints: &[Waypoint],
428 cursors: &[Pos2],
429 ) -> Vec<(GridPoint, InterfaceLock)> {
430 let mut corners = keyed_corners(waypoints, |wp| {
433 let lock = if wp.locked {
434 InterfaceLock::Locked
435 } else {
436 InterfaceLock::Unlocked
437 };
438 (wp.pos, lock)
439 });
440 for (slot, &cursor) in self.slots.iter().zip(cursors) {
441 let pos = grid_point(cursor);
442 match slot {
443 WaypointSlot::Existing(ordinal) => {
444 if let Some(c) = corners.iter_mut().find(|(o, _)| *o == Some(*ordinal)) {
445 c.1 = (pos, InterfaceLock::Locked);
446 }
447 }
448 WaypointSlot::InsertAt(i) => {
452 let i = (*i).min(corners.len());
453 corners.insert(i, (None, (pos, InterfaceLock::Locked)));
454 }
455 }
456 }
457 corners.into_iter().map(|(_, corner)| corner).collect()
458 }
459}
460
461fn override_rect(shape: ShapeId, overrides: &[(ShapeId, Rect)]) -> Option<Rect> {
464 overrides
465 .iter()
466 .find(|(overridden, _)| *overridden == shape)
467 .map(|(_, rect)| *rect)
468}
469
470fn override_slot(pin: PinId, pins: &[PinSlotOverride]) -> Option<&PinSlotOverride> {
472 pins.iter().find(|o| o.pin == pin)
473}
474
475impl Drawing<'_> {
476 pub fn solve_routes(&mut self, ripup: &[RouteId]) -> Promoted {
481 self.route_update_closed(
482 ripup,
483 GeometryOverrides::default(),
484 RoutePass::Commit,
485 Solving::Everything,
486 )
487 }
488
489 pub fn solve_foreground(&mut self, foreground: &Foreground, ripup: &[RouteId]) -> Promoted {
495 self.route_update_closed(
496 ripup,
497 GeometryOverrides::default(),
498 RoutePass::Commit,
499 Solving::Only(foreground),
500 )
501 }
502
503 pub fn preview_drag(
509 &mut self,
510 _phase: &crate::widget::PreviewPhase,
511 drags: &[(ShapeId, Vec2)],
512 ) {
513 let overrides: Vec<(ShapeId, Rect)> = drags
515 .iter()
516 .filter_map(|&(shape, offset)| {
517 Some((shape, self.shape(shape)?.gui_rect().translate(offset)))
518 })
519 .collect();
520 self.preview_shapes(&overrides);
521 if !drags.iter().any(|&(shape, _)| shape.affects_routing()) {
524 return;
525 }
526 let ids: Vec<ShapeId> = drags.iter().map(|&(shape, _)| shape).collect();
534 let delta = drags.first().map_or(Vec2::ZERO, |&(_, offset)| offset);
535 let (moved_rects, moved_pins, grid_delta) = moved_set_and_delta(&ids, delta);
536 let riding = self.riding_pins(&moved_rects, &moved_pins);
537 let inside = |pin: PinId| riding.contains(&pin);
538 let foreground = self.preview_foreground(ids, &overrides);
539 let _ = self.route_update_closed(
540 &[],
541 GeometryOverrides {
542 rects: &overrides,
543 pins: &[],
544 },
545 RoutePass::Preview(PreviewSpec {
546 grid_delta,
547 inside: &inside,
548 }),
549 Solving::Only(&foreground),
550 );
551 }
552
553 pub fn preview_resize(
557 &mut self,
558 _phase: &crate::widget::PreviewPhase,
559 resizes: &[(ShapeId, Rect)],
560 ) {
561 self.preview_shapes(resizes);
562 if !resizes.iter().any(|&(shape, _)| shape.affects_routing()) {
563 return;
564 }
565 let foreground = self.preview_foreground(resizes.iter().map(|&(shape, _)| shape), resizes);
566 let _ = self.route_update_closed(
567 &[],
568 GeometryOverrides {
569 rects: resizes,
570 pins: &[],
571 },
572 RoutePass::Preview(PreviewSpec {
573 grid_delta: GridVec::new(0, 0),
574 inside: &|_| false,
575 }),
576 Solving::Only(&foreground),
577 );
578 }
579
580 pub fn preview_pin_drag(
586 &mut self,
587 _phase: &crate::widget::PreviewPhase,
588 pin: PinId,
589 side: PinSide,
590 offset: u32,
591 ) {
592 let slots = [PinSlotOverride { pin, side, offset }];
593 let inside = move |other: PinId| other == pin;
594 let foreground = self.preview_foreground(self.pin_shape(pin), &[]);
595 let _ = self.route_update_closed(
596 &[],
597 GeometryOverrides {
598 rects: &[],
599 pins: &slots,
600 },
601 RoutePass::Preview(PreviewSpec {
602 grid_delta: GridVec::new(0, 0),
603 inside: &inside,
604 }),
605 Solving::Only(&foreground),
606 );
607 }
608
609 pub fn preview_pin_drags(&mut self, _phase: &crate::widget::PreviewPhase, moves: &[PinMove]) {
616 let slots: Vec<PinSlotOverride> = moves
617 .iter()
618 .map(|m| PinSlotOverride {
619 pin: m.pin,
620 side: m.to.side,
621 offset: m.to.offset,
622 })
623 .collect();
624 let foreground =
625 self.preview_foreground(moves.iter().filter_map(|m| self.pin_shape(m.pin)), &[]);
626 let _ = self.route_update_closed(
627 &[],
628 GeometryOverrides {
629 rects: &[],
630 pins: &slots,
631 },
632 RoutePass::Preview(PreviewSpec {
633 grid_delta: GridVec::new(0, 0),
634 inside: &|_| false,
635 }),
636 Solving::Only(&foreground),
637 );
638 }
639
640 fn preview_foreground(
643 &self,
644 shapes: impl IntoIterator<Item = ShapeId>,
645 heading_for: &[(ShapeId, Rect)],
646 ) -> Foreground {
647 let mut foreground = Foreground::raising(shapes, self);
648 for &(_, rect) in heading_for {
649 foreground.also_disturbed_by(rect, self);
650 }
651 foreground
652 }
653
654 fn anchor_pos_overridden(&self, pin: PinId, hypo: GeometryOverrides<'_>) -> Option<Pos2> {
659 let sid = self.pin_shape(pin)?;
660 let shape = self.shape(sid)?;
661 let rect = override_rect(sid, hypo.rects).unwrap_or_else(|| shape.gui_rect());
662 match (override_slot(pin, hypo.pins), &shape) {
663 (Some(o), ShapeRef::Block(block)) => Some(block.pin_anchor_at(rect, o.side, o.offset)),
664 _ => shape.anchor_point_with_rect(rect, pin),
665 }
666 }
667
668 #[tracing::instrument(level = "info", skip_all)]
673 fn routing_shapes(&self) -> Vec<(ShapeId, ShapeRef<'_>)> {
674 self.blocks_layer().chain(self.ports_layer()).collect()
675 }
676
677 fn build_closed_router(
684 &self,
685 hypo: GeometryOverrides<'_>,
686 seed_points: &[Point],
687 ) -> ClosedRouter {
688 self.build_router_within(Region::WholeScope, hypo, seed_points)
689 }
690
691 fn build_router_within(
697 &self,
698 within: Region,
699 hypo: GeometryOverrides<'_>,
700 seed_points: &[Point],
701 ) -> ClosedRouter {
702 let mut builder = RouterNGBuilder::default().within(within.bounds());
703 for (sid, shape) in self.routing_shapes() {
704 let rect = override_rect(sid, hypo.rects).unwrap_or_else(|| shape.gui_rect());
708 builder.add_block(rect.left_top(), rect.right_bottom());
709 shape.with_pins(|pid, _pin| {
710 let slot = match &shape {
711 ShapeRef::Block(block) => override_slot(pid, hypo.pins)
712 .map(|o| block.pin_anchor_at(rect, o.side, o.offset)),
713 _ => None,
714 };
715 let Some(anchor) = slot.or_else(|| shape.anchor_point_with_rect(rect, pid)) else {
716 return;
717 };
718 let anchor_pos = if anchor.x >= rect.center().x {
719 anchor + vec2(GRID_SIZE, 0.0)
720 } else {
721 anchor - vec2(GRID_SIZE, 0.0)
722 };
723 builder.add_h_channel(anchor_pos, COST_ZERO);
724 });
725 }
726 for &p in seed_points {
727 builder.add_seed_point(p);
728 }
729 builder.build_closed()
730 }
731
732 fn healing_lattice(
736 &self,
737 within: Region,
738 seeding: &Seeding<'_>,
739 routed: &HashSet<RouteId>,
740 ) -> ClosedRouter {
741 let seed_points = seeds_within(self, seeding, within);
742 let mut lattice = {
743 let _s = tracing::info_span!(
744 "closed_build",
745 seeds = seed_points.len(),
746 region = ?within
747 )
748 .entered();
749 self.build_router_within(within, seeding.hypo, &seed_points)
750 };
751 for &id in seeding.ids {
752 if routed.contains(&id) {
753 continue;
754 }
755 if let Some(geometry) = self.presentation.routes.get(&id) {
756 add_route_cost(
757 &mut lattice,
758 geometry.iter_edges().map(|(_, edge)| edge),
759 WIRE_COST,
760 );
761 }
762 }
763 lattice
764 }
765
766 fn heal<W>(
776 &mut self,
777 wires: Vec<(RouteId, W)>,
778 region: Rect,
779 seeding: &Seeding<'_>,
780 mut route: impl FnMut(RouteId, &W, &mut RouteGeometry, &mut ClosedRouter) -> Resolution,
781 ) {
782 let mut attempt = |drawing: &mut Self, within: Region, wires: Vec<(RouteId, W)>| {
783 let routed: HashSet<RouteId> = wires.iter().map(|(id, _)| *id).collect();
784 let mut lattice = drawing.healing_lattice(within, seeding, &routed);
785 let mut fell_back = Vec::new();
786 for (id, wire) in wires {
787 let geometry = drawing.presentation.routes.entry(id).or_default();
788 if route(id, &wire, geometry, &mut lattice) == Resolution::Fallback {
789 fell_back.push((id, wire));
790 }
791 }
792 fell_back
793 };
794 let fell_back = attempt(self, Region::Within(region), wires);
795 if !fell_back.is_empty() {
796 let _s = tracing::info_span!("heal_whole_scope", wires = fell_back.len()).entered();
797 attempt(self, Region::WholeScope, fell_back);
798 }
799 }
800
801 fn obstacle_rects(&self, overrides: &[(ShapeId, Rect)]) -> Obstacles {
808 let rects = self
809 .routing_shapes()
810 .into_iter()
811 .map(|(sid, shape)| {
812 obstacle_rect(override_rect(sid, overrides).unwrap_or_else(|| shape.gui_rect()))
813 })
814 .collect();
815 Obstacles::new(rects)
816 }
817
818 fn preview_exclusions(
827 &self,
828 endpoints: &[(RouteId, Pos2, Pos2)],
829 spec: &PreviewSpec<'_>,
830 ) -> HashMap<RouteId, PreviewExclusions> {
831 endpoints
832 .iter()
833 .filter_map(|&(id, s, e)| {
834 let route = self.route(id)?;
835 let stored = route.waypoints.clone();
836 let (start_in, finish_in) = ((spec.inside)(route.from), (spec.inside)(route.to));
837 if start_in && finish_in {
838 return None;
839 }
840 let mut excluded: HashSet<PathOrdinal> = match (start_in, finish_in) {
841 (true, false) => trimmed_ordinals(&stored, RouteEnd::From),
842 (false, true) => trimmed_ordinals(&stored, RouteEnd::To),
843 _ => Vec::new(),
844 }
845 .into_iter()
846 .collect();
847 let working: Vec<Waypoint> = stored
848 .iter()
849 .enumerate()
850 .filter(|(index, _)| !excluded.contains(&PathOrdinal::new(*index)))
851 .map(|(_, wp)| *wp)
852 .collect();
853 let survivors: Vec<PathOrdinal> = (0..stored.len())
856 .map(PathOrdinal::new)
857 .filter(|o| !excluded.contains(o))
858 .collect();
859 let doomed = backtracking_ordinals(&working, grid_point(s), grid_point(e));
860 let pruned = !doomed.is_empty();
861 excluded.extend(
862 doomed
863 .into_iter()
864 .filter_map(|o| survivors.get(usize::from(o)).copied()),
865 );
866 (!excluded.is_empty()).then_some((id, PreviewExclusions { excluded, pruned }))
867 })
868 .collect()
869 }
870
871 #[tracing::instrument(level = "info", skip_all, fields(routes = self.scope_route_ids().len(), ripup = ripup.len()))]
890 fn route_update_closed(
891 &mut self,
892 ripup: &[RouteId],
893 hypo: GeometryOverrides<'_>,
894 pass: RoutePass<'_>,
895 solving: Solving<'_>,
896 ) -> Promoted {
897 let ids = self.scope_route_ids();
898 let endpoints: Vec<(RouteId, Pos2, Pos2)> = {
903 let _s = tracing::info_span!("closed_anchors").entered();
904 ids.iter()
905 .filter(|&&id| solving.may_solve(id))
906 .filter_map(|&id| {
907 let route = self.route(id)?;
908 let s = self.anchor_pos_overridden(route.from, hypo)?;
909 let e = self.anchor_pos_overridden(route.to, hypo)?;
910 Some((id, s, e))
911 })
912 .collect()
913 };
914 let exclusions: HashMap<RouteId, PreviewExclusions> = match &pass {
918 RoutePass::Preview(spec) => self.preview_exclusions(&endpoints, spec),
919 RoutePass::Commit => HashMap::new(),
920 };
921 let offsets: HashMap<RouteId, GridVec> = match &pass {
926 RoutePass::Preview(spec) => ids
927 .iter()
928 .filter_map(|&id| {
929 let route = self.route(id)?;
930 ((spec.inside)(route.from) && (spec.inside)(route.to))
931 .then_some((id, spec.grid_delta))
932 })
933 .collect(),
934 RoutePass::Commit => HashMap::new(),
935 };
936 let previewed = matches!(pass, RoutePass::Preview(_));
937 let obstacles = self.obstacle_rects(hypo.rects);
938 let seeding = Seeding {
939 hypo,
940 ids: &ids,
941 endpoints: &endpoints,
942 exclusions: &exclusions,
943 offsets: &offsets,
944 };
945 let mut promoted: Promoted = Vec::new();
946 let mut resolved: Vec<RouteId> = Vec::new();
947 let holding: HashSet<RouteId> = endpoints
950 .iter()
951 .filter(|&&(id, start, end)| {
952 self.route_geometry(id)
953 .is_some_and(|geometry| still_holds(geometry, start, end, &obstacles))
954 && !ripup.contains(&id)
955 })
956 .map(|&(id, ..)| id)
957 .collect();
958 match pass {
959 RoutePass::Commit => {
960 let mut deferred: Vec<(RouteId, (Endpoints, Vec<Waypoint>))> = Vec::new();
964 {
965 let _s = tracing::info_span!("closed_route_loop").entered();
966 let (indexed, presentation) = self.split();
967 let doc = indexed.doc;
968 for &(id, anchor_start, anchor_end) in &endpoints {
969 let Some(route) = doc.route(&id) else {
970 continue;
971 };
972 let geometry = presentation.routes.entry(id).or_default();
973 let doomed = backtracking_ordinals(
978 &route.waypoints,
979 grid_point(anchor_start),
980 grid_point(anchor_end),
981 );
982 let pruned = !doomed.is_empty();
983 let stored = if pruned {
984 pruned_waypoints(&route.waypoints, &doomed)
985 } else {
986 route.waypoints.clone()
987 };
988 if !pruned && holding.contains(&id) {
989 promoted.push((id, promote_corners_to_waypoints(&stored, geometry)));
999 continue;
1000 }
1001 let ends = Endpoints {
1005 start: grid_point(anchor_start),
1006 end: grid_point(anchor_end),
1007 };
1008 match straighten_route(&stored, geometry, ends, obstacles.reroute_blocked())
1009 {
1010 Some(corners) => promoted.push((id, corners)),
1011 None => deferred.push((id, (ends, stored))),
1012 }
1013 }
1014 }
1015 if !deferred.is_empty() {
1016 let _s =
1017 tracing::info_span!("closed_route_deferred", deferred = deferred.len())
1018 .entered();
1019 let mut settled: BTreeMap<RouteId, Vec<Waypoint>> = BTreeMap::new();
1020 let region = healing_region(deferred.iter().flat_map(|(_, (ends, stored))| {
1021 [ends.start, ends.end]
1022 .into_iter()
1023 .chain(stored.iter().map(|wp| wp.pos))
1024 }));
1025 self.heal(
1026 deferred,
1027 region,
1028 &seeding,
1029 |id, (ends, stored), geometry, lattice| {
1030 let laid = reconstruct_route(
1031 stored,
1032 geometry,
1033 *ends,
1034 obstacles.reroute_blocked(),
1035 lattice,
1036 );
1037 settled.insert(id, laid.corners);
1039 laid.resolution
1040 },
1041 );
1042 promoted.extend(settled);
1043 }
1044 }
1045 RoutePass::Preview(spec) => {
1046 let rerouting: Vec<(RouteId, Rerouting)> = endpoints
1051 .iter()
1052 .filter(|&&(id, ..)| {
1053 let pruned = exclusions.get(&id).is_some_and(|x| x.pruned);
1054 pruned || !holding.contains(&id)
1055 })
1056 .filter_map(|&(id, s, e)| {
1057 let route = self.route(id)?;
1058 let ends = Endpoints {
1059 start: grid_point(s),
1060 end: grid_point(e),
1061 };
1062 let rigid = ((spec.inside)(route.from) && (spec.inside)(route.to))
1063 .then_some(spec.grid_delta);
1064 let stored = route.waypoints.clone();
1065 Some((
1066 id,
1067 Rerouting {
1068 ends,
1069 stored,
1070 rigid,
1071 },
1072 ))
1073 })
1074 .collect();
1075 if !rerouting.is_empty() {
1076 let region = healing_region(rerouting.iter().flat_map(|(id, wire)| {
1077 [wire.ends.start, wire.ends.end]
1078 .into_iter()
1079 .chain(seeded_waypoints(self, &seeding, *id))
1080 }));
1081 let _s = tracing::info_span!("closed_route_loop", routes = rerouting.len())
1082 .entered();
1083 resolved.extend(rerouting.iter().map(|&(id, _)| id));
1086 let no_exclusions = PreviewExclusions::default();
1087 self.heal(
1088 rerouting,
1089 region,
1090 &seeding,
1091 |id, wire, geometry, lattice| match wire.rigid {
1092 Some(delta) => reroute_preview_closed(
1093 &wire.stored,
1094 geometry,
1095 wire.ends,
1096 delta,
1097 lattice,
1098 ),
1099 None => reroute_preview_excluding(
1100 &wire.stored,
1101 geometry,
1102 wire.ends,
1103 &exclusions.get(&id).unwrap_or(&no_exclusions).excluded,
1104 lattice,
1105 ),
1106 },
1107 );
1108 }
1109 }
1110 }
1111 if previewed {
1112 let scope = self.scope_path().clone();
1113 self.presentation
1114 .routes_previewed(resolved.iter().copied(), &scope);
1115 self.preview_routes(resolved);
1116 }
1117 promoted
1118 }
1119
1120 fn route_reaches(&self, id: RouteId, reconstructing: Reconstructing<'_>) -> bool {
1124 match reconstructing {
1125 Reconstructing::Everything => true,
1126 Reconstructing::These(these) => these.contains(&id),
1127 Reconstructing::Within {
1128 disturbed: region,
1129 previewed,
1130 } => {
1131 if previewed.contains(&id) {
1132 return true;
1133 }
1134 let Some(geometry) = self.route_geometry(id) else {
1135 return true;
1136 };
1137 region.contains(geometry.start_pos())
1138 || region.contains(geometry.end_pos())
1139 || geometry.iter_edges().any(|(_, edge)| {
1140 region.contains(px_point(edge.start)) || region.contains(px_point(edge.end))
1141 })
1142 }
1143 }
1144 }
1145
1146 pub fn reconstruct_routes(&mut self) {
1154 self.reconstruct_routes_reaching(Reconstructing::Everything);
1155 }
1156
1157 pub(crate) fn reconstruct_routes_reaching(&mut self, reconstructing: Reconstructing<'_>) {
1166 let ids: Vec<RouteId> = self
1167 .scope_route_ids()
1168 .into_iter()
1169 .filter(|&id| self.route_reaches(id, reconstructing))
1170 .collect();
1171 if ids.is_empty() {
1172 return;
1173 }
1174 let endpoints: Vec<(RouteId, GridPoint, GridPoint)> = {
1175 let _s = tracing::info_span!("anchors", routes = ids.len()).entered();
1176 ids.iter()
1177 .filter_map(|&id| {
1178 let route = self.route(id)?;
1179 let s = self.anchor_pos_overridden(route.from, GeometryOverrides::default())?;
1180 let e = self.anchor_pos_overridden(route.to, GeometryOverrides::default())?;
1181 Some((id, grid_point(s), grid_point(e)))
1182 })
1183 .collect()
1184 };
1185 let obstacles = {
1186 let _s = tracing::info_span!("obstacles").entered();
1187 self.obstacle_rects(&[])
1188 };
1189
1190 let mut deferred: Vec<(RouteId, (Endpoints, Vec<Waypoint>))> = Vec::new();
1193 {
1194 let _s = tracing::info_span!("straighten", routes = endpoints.len()).entered();
1195 let (indexed, presentation) = self.split();
1196 let doc = indexed.doc;
1197 for &(id, s, e) in &endpoints {
1198 let Some(route) = doc.route(&id) else {
1199 continue;
1200 };
1201 let geometry = presentation.routes.entry(id).or_default();
1202 let ends = Endpoints { start: s, end: e };
1203 if straighten_route(&route.waypoints, geometry, ends, obstacles.keep_blocked())
1204 .is_none()
1205 {
1206 deferred.push((id, (ends, route.waypoints.clone())));
1207 }
1208 }
1209 }
1210
1211 if !deferred.is_empty() {
1216 let _s = tracing::info_span!("route_deferred", deferred = deferred.len()).entered();
1217 let anchors: Vec<(RouteId, Pos2, Pos2)> = endpoints
1218 .iter()
1219 .map(|&(id, s, e)| (id, px_point(s), px_point(e)))
1220 .collect();
1221 let (exclusions, offsets) = (HashMap::new(), HashMap::new());
1222 let seeding = Seeding {
1223 hypo: GeometryOverrides::default(),
1224 ids: &ids,
1225 endpoints: &anchors,
1226 exclusions: &exclusions,
1227 offsets: &offsets,
1228 };
1229 let region = healing_region(deferred.iter().flat_map(|(_, (ends, stored))| {
1230 [ends.start, ends.end]
1231 .into_iter()
1232 .chain(stored.iter().map(|wp| wp.pos))
1233 }));
1234 self.heal(
1235 deferred,
1236 region,
1237 &seeding,
1238 |_, (ends, stored), geometry, lattice| {
1239 reconstruct_route(stored, geometry, *ends, obstacles.keep_blocked(), lattice)
1240 .resolution
1241 },
1242 );
1243 }
1244 }
1245
1246 fn route_ends(&self, id: RouteId) -> Option<Endpoints> {
1249 let route = self.route(id)?;
1250 let s = self.anchor_pos_overridden(route.from, GeometryOverrides::default())?;
1251 let e = self.anchor_pos_overridden(route.to, GeometryOverrides::default())?;
1252 Some(Endpoints {
1253 start: grid_point(s),
1254 end: grid_point(e),
1255 })
1256 }
1257
1258 fn relay_route(
1265 &mut self,
1266 id: RouteId,
1267 corners: &[GridPoint],
1268 stored: &[Waypoint],
1269 anchors: &[(RouteLabelId, Pos2)],
1270 ) -> Option<RelaidRoute> {
1271 let ends = self.route_ends(id)?;
1272 let geometry = self.presentation.routes.entry(id).or_default();
1273 reconstruct_corners_direct(corners, geometry, ends);
1274 let relayed = reanchored(geometry, anchors);
1275 let promoted = promote_corners_to_waypoints(stored, geometry);
1276 let scope = self.scope_path().clone();
1280 self.presentation.routes_previewed([id], &scope);
1281 Some((promoted, relayed))
1282 }
1283
1284 pub fn route_edit_session(&self, id: RouteId, cursors: &[Pos2]) -> Option<RouteEditSession> {
1293 let wire = self.auto_route(id)?;
1294 let geometry = self.route_geometry(id)?;
1295 let mut working = keyed_corners(&wire.route.waypoints, |wp| px_point(wp.pos));
1300 let mut slots = Vec::with_capacity(cursors.len());
1301 for &cursor in cursors {
1302 let hit = working
1303 .iter()
1304 .find(|(_, pos)| pos.distance(cursor) <= GRID_SIZE * 0.5)
1305 .copied();
1306 match hit {
1307 Some((Some(ordinal), _)) => slots.push(WaypointSlot::Existing(ordinal)),
1308 Some((None, _)) => return None,
1309 None => {
1310 let d_new = geometry.distance_along(cursor);
1311 let index = working
1312 .iter()
1313 .filter(|&&(_, pos)| geometry.distance_along(pos) < d_new)
1314 .count();
1315 let pos = px_point(grid_point(cursor));
1316 working.insert(index, (None, pos));
1317 slots.push(WaypointSlot::InsertAt(index));
1318 }
1319 }
1320 }
1321 Some(RouteEditSession {
1322 slots,
1323 anchors: label_anchors(&wire.labels, geometry),
1324 })
1325 }
1326
1327 pub fn preview_route_edit(
1332 &mut self,
1333 _phase: &crate::widget::PreviewPhase,
1334 id: RouteId,
1335 session: &RouteEditSession,
1336 cursors: &[Pos2],
1337 ) {
1338 let Some(ends) = self.route_ends(id) else {
1339 return;
1340 };
1341 let Some(route) = self.route(id) else {
1342 return;
1343 };
1344 let corners: Vec<GridPoint> = session
1345 .corners(&route.waypoints, cursors)
1346 .into_iter()
1347 .map(|(pos, _)| pos)
1348 .collect();
1349 let routes = &mut self.presentation.routes;
1350 let geometry = routes.entry(id).or_default();
1351 reconstruct_corners_direct(&corners, geometry, ends);
1352 let scope = self.scope_path().clone();
1353 self.presentation.routes_previewed([id], &scope);
1354 }
1355
1356 pub fn commit_route_edit(&mut self, id: RouteId, session: &RouteEditSession, cursors: &[Pos2]) {
1361 let Some(route) = self.route(id) else {
1362 return;
1363 };
1364 let edited: Vec<Waypoint> = session
1365 .corners(&route.waypoints, cursors)
1366 .into_iter()
1367 .map(|(pos, lock)| Waypoint {
1368 pos,
1369 locked: lock.is_locked(),
1370 })
1371 .collect();
1372 let corners: Vec<GridPoint> = edited.iter().map(|wp| wp.pos).collect();
1373 let anchors = session.anchors.clone();
1374 let Some((waypoints, relayed)) = self.relay_route(id, &corners, &edited, &anchors) else {
1375 return;
1376 };
1377 self.author("commit_route_edit", |indexed, sink| {
1378 crate::edit::geometry::commit_route_edit(
1379 indexed.doc,
1380 crate::edit::geometry::RouteEdit {
1381 route: id,
1382 waypoints,
1383 anchors: &relayed,
1384 },
1385 sink,
1386 );
1387 });
1388 }
1389
1390 pub fn reroute(&mut self, id: RouteId) {
1394 self.author("reroute", |indexed, sink| {
1395 crate::edit::geometry::reroute(indexed, id, &[], sink);
1396 });
1397 self.rip_up(&[id]);
1398 }
1399
1400 pub fn reroute_block(&mut self, block: blockworx_doc::id::BlockId) {
1409 let on_block: HashSet<PinId> = self
1410 .block_pins(crate::path::Scope::Block(block))
1411 .into_iter()
1412 .map(|(id, _)| id)
1413 .collect();
1414 let ids: Vec<RouteId> = self
1415 .scope_route_ids()
1416 .into_iter()
1417 .filter(|&id| {
1418 self.route(id)
1419 .is_some_and(|r| on_block.contains(&r.from) || on_block.contains(&r.to))
1420 })
1421 .collect();
1422 for &id in &ids {
1423 self.author("reroute_block", |indexed, sink| {
1424 crate::edit::geometry::reroute(indexed, id, &[], sink);
1425 });
1426 }
1427 self.rip_up(&ids);
1428 }
1429
1430 fn rip_up(&mut self, ids: &[RouteId]) {
1435 for id in ids {
1436 self.presentation.routes.remove(id);
1437 }
1438 let scope = self.scope_path().clone();
1439 self.presentation
1440 .routes_previewed(ids.iter().copied(), &scope);
1441 }
1442
1443 pub fn scratch_closed_router(&self, extra_seeds: &[Point]) -> ClosedRouter {
1451 let ids = self.scope_route_ids();
1452 let mut seeds: Vec<Point> = Vec::new();
1453 for &id in &ids {
1454 let Some(route) = self.route(id) else {
1455 continue;
1456 };
1457 if let Some(s) = self.anchor_pos_overridden(route.from, GeometryOverrides::default()) {
1458 seeds.push(snap_to_grid(s).into());
1459 }
1460 if let Some(e) = self.anchor_pos_overridden(route.to, GeometryOverrides::default()) {
1461 seeds.push(snap_to_grid(e).into());
1462 }
1463 for wp in route.waypoints.clone() {
1464 seeds.push(wp.pos.into());
1465 }
1466 }
1467 seeds.extend_from_slice(extra_seeds);
1468 let mut router = self.build_closed_router(GeometryOverrides::default(), &seeds);
1469 for &id in &ids {
1470 if let Some(geometry) = self.presentation.routes.get(&id) {
1471 add_route_cost(
1472 &mut router,
1473 geometry.iter_edges().map(|(_, e)| e),
1474 WIRE_COST,
1475 );
1476 }
1477 }
1478 router
1479 }
1480
1481 pub fn healing_bound(&self, foreground: &Foreground) -> Option<(Rect, Vec<Mark>)> {
1489 let ids = self.scope_route_ids();
1490 let raised: Vec<(RouteId, Endpoints)> = ids
1491 .iter()
1492 .filter(|&&id| foreground.holds_route(id))
1493 .filter_map(|&id| Some((id, self.route_ends(id)?)))
1494 .collect();
1495 if raised.is_empty() {
1496 return None;
1497 }
1498 let endpoints: Vec<(RouteId, Pos2, Pos2)> = raised
1499 .iter()
1500 .map(|&(id, ends)| (id, px_point(ends.start), px_point(ends.end)))
1501 .collect();
1502 let (exclusions, offsets) = (HashMap::new(), HashMap::new());
1503 let seeding = Seeding {
1504 hypo: GeometryOverrides::default(),
1505 ids: &ids,
1506 endpoints: &endpoints,
1507 exclusions: &exclusions,
1508 offsets: &offsets,
1509 };
1510 let region = healing_region(raised.iter().flat_map(|&(id, ends)| {
1511 [ends.start, ends.end]
1512 .into_iter()
1513 .chain(seeded_waypoints(self, &seeding, id))
1514 }));
1515 let routed: HashSet<RouteId> = raised.iter().map(|&(id, _)| id).collect();
1516 let lattice = self.healing_lattice(Region::Within(region), &seeding, &routed);
1517 Some((region, lattice.debug_marks()))
1518 }
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523 use blockworx_doc::{
1524 block_model::RouteUpdate,
1525 opcode::{Crud, OpCodes},
1526 };
1527 use blockworx_doc::{
1528 fixtures::{block_id, pin_id, route_id},
1529 geometry::{GridPoint, PinSlot, Waypoint},
1530 id::RouteId,
1531 values::PinSide,
1532 };
1533 use blockworx_geom::{Rect, pos2, vec2};
1534
1535 use super::{GeometryOverrides, Region, healing_region};
1536 use crate::{
1537 edit::{
1538 geometry::{PinMove, RouteEnd, trimmed_approach},
1539 naming::InterfaceLock,
1540 },
1541 grid::{GRID_SIZE, grid_point, px_point},
1542 path::Scope,
1543 presentation::{RouteEdge, RouteGeometry},
1544 shape::{BaseShape, ShapeId, ShapeRef},
1545 widget::{
1546 auto_route::Wire,
1547 drawing::Drawing,
1548 test_fixtures::{
1549 self as fx, Scene, route_passes_through, two_blocks_with_a_routed_waypoint,
1550 },
1551 },
1552 };
1553
1554 #[test]
1561 fn a_two_corner_plan_replays_in_the_order_it_was_planned() {
1562 let mut scene = two_blocks_with_a_routed_waypoint();
1563 fx::reconstruct(&mut scene);
1564 let route = route_id(5);
1565 let drawing = scene.drawing();
1566 let geometry = drawing
1567 .route_geometry(route)
1568 .expect("the fixture's wire is reconstructed")
1569 .clone();
1570 let edges: Vec<_> = geometry.iter_edges().map(|(_, e)| e.clone()).collect();
1575 assert!(
1576 edges.len() >= 2,
1577 "precondition: the wire needs two edges to insert around",
1578 );
1579 let midpoint =
1580 |e: &RouteEdge| px_point(e.start) + (px_point(e.end) - px_point(e.start)) * 0.5;
1581 let (early, late) = (midpoint(&edges[0]), midpoint(&edges[edges.len() - 1]));
1582 let stored = drawing
1583 .auto_route(route)
1584 .expect("the wire is in scope")
1585 .route
1586 .waypoints
1587 .clone();
1588 assert_eq!(
1589 stored.len(),
1590 1,
1591 "precondition: one stored corner to insert around",
1592 );
1593
1594 let session = drawing
1595 .route_edit_session(route, &[early, late])
1596 .expect("two well-separated cursors plan");
1597 let corners = session.corners(&stored, &[early, late]);
1598
1599 assert_eq!(
1600 corners.len(),
1601 3,
1602 "one stored corner plus two insertions: {corners:?}",
1603 );
1604 let locked: Vec<usize> = corners
1605 .iter()
1606 .enumerate()
1607 .filter(|(_, (_, lock))| *lock == InterfaceLock::Locked)
1608 .map(|(i, _)| i)
1609 .collect();
1610 assert_eq!(
1611 locked.len(),
1612 2,
1613 "the two the user placed come out locked: {corners:?}",
1614 );
1615 let (first, second) = (corners[locked[0]].0, corners[locked[1]].0);
1618 assert!(
1619 geometry.distance_along(px_point(first)) < geometry.distance_along(px_point(second)),
1620 "the replay reordered the planned corners: {first:?} then {second:?}",
1621 );
1622 }
1623
1624 const RING: [(u32, (i32, i32, i32, i32)); 4] = [
1627 (10, (0, 0, 44, 4)),
1628 (11, (0, 52, 44, 4)),
1629 (12, (40, 4, 4, 48)),
1630 (WEST_WALL, (0, 4, 4, 48)),
1631 ];
1632 const WEST_WALL: u32 = 13;
1633
1634 fn walled_in_wire(walls: &[u32], far_top: i32) -> Scene {
1638 let mut ops = vec![
1639 fx::block_in(1, Scope::Root, fx::cells(26, 26, 4, 4)),
1640 fx::block_in(2, Scope::Root, fx::cells(80, far_top, 4, 4)),
1641 ];
1642 for (n, (x, y, w, h)) in RING {
1643 if walls.contains(&n) {
1644 ops.push(fx::block_in(n, Scope::Root, fx::cells(x, y, w, h)));
1645 }
1646 }
1647 ops.extend([
1648 fx::pin(3, 1, PinSide::East, 0),
1649 fx::pin(4, 2, PinSide::West, 0),
1650 fx::route(5, Scope::Root, 3, 4, &[]),
1651 ]);
1652 let mut scene = Scene::new(ops);
1653 fx::reconstruct(&mut scene);
1654 scene
1655 }
1656
1657 fn walls_crossed(geometry: &RouteGeometry, walls: &[u32]) -> Vec<u32> {
1659 RING.iter()
1660 .filter(|(n, _)| walls.contains(n))
1661 .filter(|&&(_, (x, y, w, h))| {
1662 geometry.iter_edges().any(|(_, e)| {
1663 let (x0, x1) = (e.start.x.min(e.end.x), e.start.x.max(e.end.x));
1664 let (y0, y1) = (e.start.y.min(e.end.y), e.start.y.max(e.end.y));
1665 x0 < x + w && x < x1.max(x0 + 1) && y0 < y + h && y < y1.max(y0 + 1)
1666 })
1667 })
1668 .map(|&(n, _)| n)
1669 .collect()
1670 }
1671
1672 fn routing_shape_ids(drawing: &Drawing<'_>) -> Vec<ShapeId> {
1675 drawing
1676 .routing_shapes()
1677 .into_iter()
1678 .map(|(id, _)| id)
1679 .collect()
1680 }
1681
1682 const ALL_WALLS: [u32; 4] = [10, 11, 12, WEST_WALL];
1683 const EAST_WALL: u32 = 12;
1684
1685 #[test]
1690 fn an_unroutable_wire_settles_on_its_l_and_is_marked_where_it_crosses() {
1691 let rid = route_id(5);
1692 let mut scene = walled_in_wire(&ALL_WALLS, 40);
1693 assert!(
1694 stored_corners(&scene.drawing(), rid).is_empty(),
1695 "precondition: the wire starts with no corners"
1696 );
1697
1698 scene.commit(|drawing| {
1699 drawing.move_shape(ShapeId::Rect(block_id(2)), vec2(0.0, GRID_SIZE));
1700 });
1701
1702 let corners = stored_corners(&scene.drawing(), rid);
1703 assert_eq!(corners.len(), 1, "the L's one bend is written: {corners:?}");
1704 let geometry = fx::geometry(&scene, rid);
1705 assert_eq!(walls_crossed(geometry, &ALL_WALLS), vec![EAST_WALL]);
1706 let drawing = scene.drawing();
1707 let marks = drawing
1708 .standing_conflicts(&routing_shape_ids(&drawing))
1709 .crossings;
1710 let (x, y, w, h) = RING[2].1;
1711 let east = fx::cells(x, y, w, h);
1712 assert!(!marks.is_empty(), "the illegal stretch is marked");
1713 assert!(
1714 marks.iter().all(|mark| mark.intersects(east)),
1715 "every mark sits on the wall the L runs through: {marks:?}"
1716 );
1717 }
1718
1719 #[test]
1723 fn an_illegal_wire_is_rerouted_only_by_a_commit_that_raises_it() {
1724 let rid = route_id(5);
1725 let west = ShapeId::Rect(block_id(WEST_WALL));
1726 let mut scene = walled_in_wire(&ALL_WALLS, 40);
1727 scene.commit(|drawing| {
1728 drawing.move_shape(ShapeId::Rect(block_id(2)), vec2(0.0, GRID_SIZE));
1729 });
1730 let settled = route_edges(&scene.drawing(), rid);
1731 {
1732 let drawing = scene.drawing();
1733 let raised = crate::widget::foreground::Foreground::raising([west], &drawing);
1734 assert!(
1735 !raised.holds_route(rid),
1736 "precondition: deleting the west wall does not raise the wire"
1737 );
1738 }
1739
1740 scene.commit(|drawing| drawing.delete(crate::shape::Deletable::Shape(west)));
1741 assert!(
1742 !scene
1743 .committed()
1744 .iter()
1745 .any(|op| matches!(op, OpCodes::Route(id, _) if *id == rid)),
1746 "the far delete wrote the wire"
1747 );
1748 assert_eq!(route_edges(&scene.drawing(), rid), settled);
1749
1750 let east = ShapeId::Rect(block_id(EAST_WALL));
1751 scene.commit(|drawing| drawing.delete(crate::shape::Deletable::Shape(east)));
1752 let standing = [10, 11];
1753 assert_eq!(
1754 walls_crossed(fx::geometry(&scene, rid), &standing),
1755 Vec::<u32>::new()
1756 );
1757 let drawing = scene.drawing();
1758 assert!(
1759 drawing
1760 .standing_conflicts(&routing_shape_ids(&drawing))
1761 .crossings
1762 .is_empty(),
1763 "the rerouted wire is legal"
1764 );
1765 }
1766
1767 #[test]
1770 fn a_settled_grid_has_no_wire_conflicts() {
1771 let mut scene = fx::scale_scene(6);
1772 let drawing = scene.drawing();
1773 assert!(
1774 !drawing.scope_route_ids().is_empty(),
1775 "precondition: the grid has wires"
1776 );
1777 assert_eq!(
1778 drawing
1779 .standing_conflicts(&routing_shape_ids(&drawing))
1780 .crossings,
1781 Vec::<Rect>::new()
1782 );
1783 }
1784
1785 #[test]
1789 fn the_diagnostic_shows_the_bounded_lattice_a_selection_heals_in() {
1790 let mut scene = fx::scale_scene(6);
1791 let drawing = scene.drawing();
1792 let block = drawing
1793 .child_blocks()
1794 .first()
1795 .map(|&(id, _)| ShapeId::Rect(id))
1796 .expect("the grid has blocks");
1797 let foreground = crate::widget::foreground::Foreground::raising([block], &drawing);
1798 let (bound, marks) = drawing
1799 .healing_bound(&foreground)
1800 .expect("a block with wires raises some");
1801 let raised: Vec<RouteId> = foreground.routes().collect();
1802 assert!(!raised.is_empty(), "precondition: the block raises wires");
1803 for &id in &raised {
1804 let ends = drawing.route_ends(id).expect("a raised wire has ends");
1805 assert!(
1806 bound.contains(px_point(ends.start)) && bound.contains(px_point(ends.end)),
1807 "{id:?} lies outside the bound"
1808 );
1809 }
1810 let sheet = drawing
1811 .child_blocks()
1812 .iter()
1813 .filter_map(|&(id, _)| drawing.shape(ShapeId::Rect(id)))
1814 .map(|shape| shape.gui_rect())
1815 .reduce(|a, b| a.union(b))
1816 .expect("the grid has blocks");
1817 assert!(
1818 !bound.contains_rect(sheet),
1819 "precondition: the bound is smaller than the sheet"
1820 );
1821 let slack = bound.expand(GRID_SIZE);
1822 let outside: Vec<_> = marks
1823 .iter()
1824 .filter_map(|mark| match *mark {
1825 blockworx_router::turtle::Mark::Line { from, to } => {
1826 (!slack.contains(from) || !slack.contains(to)).then_some(from)
1827 }
1828 blockworx_router::turtle::Mark::Circle { center, .. }
1829 | blockworx_router::turtle::Mark::Label { pos: center, .. } => {
1830 (!slack.contains(center)).then_some(center)
1831 }
1832 })
1833 .take(4)
1834 .collect();
1835 assert!(!marks.is_empty(), "precondition: the lattice has marks");
1836 assert!(
1837 outside.is_empty(),
1838 "the lattice reaches past its bound: {outside:?}"
1839 );
1840 }
1841
1842 #[test]
1847 fn a_region_lattice_holds_nothing_outside_its_region() {
1848 let mut scene = walled_in_wire(&ALL_WALLS, 26);
1849 let drawing = scene.drawing();
1850 let region = Region::Within(fx::cells(18, 16, 74, 24));
1851 let lattice = drawing.build_router_within(region, GeometryOverrides::default(), &[]);
1852 let nodes = lattice.fingerprint();
1853 assert!(
1854 !nodes.points().is_empty(),
1855 "precondition: the region holds a lattice"
1856 );
1857 let escaped: Vec<_> = nodes
1858 .points()
1859 .iter()
1860 .filter(|&&p| !region.bounds().holds(p))
1861 .take(4)
1862 .collect();
1863 assert!(escaped.is_empty(), "lanes escaped the region: {escaped:?}");
1864 }
1865
1866 #[test]
1869 fn a_region_too_small_to_detour_in_grows_to_the_scope() {
1870 let rid = route_id(5);
1871 let standing = [10, 11, EAST_WALL];
1872 let mut scene = walled_in_wire(&standing, 26);
1873 let geometry = fx::geometry(&scene, rid);
1874 let region = healing_region([geometry.start_pos, geometry.end_pos]);
1875 let (x, y, w, h) = RING[2].1;
1876 let east = fx::cells(x, y, w, h);
1877 assert!(
1878 east.min.y <= region.min.y && region.max.y <= east.max.y,
1879 "precondition: the east wall cuts the region in two, so it holds no path"
1880 );
1881 assert!(
1882 !region.intersects(fx::cells(0, 4, 4, 48)),
1883 "precondition: the gap the path needs lies outside the region"
1884 );
1885 assert_eq!(
1886 walls_crossed(geometry, &standing),
1887 vec![EAST_WALL],
1888 "precondition: the document's straight wire runs through the east wall"
1889 );
1890
1891 scene.commit(|drawing| {
1892 drawing.move_shape(ShapeId::Rect(block_id(2)), vec2(GRID_SIZE, 0.0));
1893 });
1894 assert_eq!(
1895 walls_crossed(fx::geometry(&scene, rid), &standing),
1896 Vec::<u32>::new()
1897 );
1898 }
1899
1900 fn a_to_b_route(x_b: f32) -> Scene {
1903 Scene::new(vec![
1904 fx::block(1, 0.0),
1905 fx::block(2, x_b),
1906 fx::pin(3, 1, PinSide::East, 0),
1907 fx::pin(4, 2, PinSide::West, 0),
1908 fx::route(5, Scope::Root, 3, 4, &[]),
1909 ])
1910 }
1911
1912 fn route_edges(drawing: &Drawing<'_>, rid: RouteId) -> Vec<(GridPoint, GridPoint)> {
1913 drawing
1914 .route_geometry(rid)
1915 .unwrap()
1916 .iter_edges()
1917 .map(|(_, e)| (e.start, e.end))
1918 .collect()
1919 }
1920
1921 fn stored_corners(drawing: &Drawing<'_>, rid: RouteId) -> Vec<GridPoint> {
1922 drawing
1923 .auto_route(rid)
1924 .expect("the route is in this scope")
1925 .route
1926 .waypoints
1927 .iter()
1928 .map(|wp| wp.pos)
1929 .collect()
1930 }
1931
1932 #[test]
1933 fn a_drag_preview_moves_only_the_dragged_endpoint() {
1934 let mut scene = a_to_b_route(120.0);
1935 let rid = route_id(5);
1936 let mut drawing = scene.drawing();
1937 drawing.solve_routes(&[]);
1939 let start0 = drawing.route_geometry(rid).unwrap().start_pos();
1940 let end0 = drawing.route_geometry(rid).unwrap().end_pos();
1941
1942 let offset = vec2(2.0 * GRID_SIZE, GRID_SIZE);
1944 drawing.preview_drag(
1945 &crate::widget::PreviewPhase::testing(),
1946 &[(ShapeId::Rect(block_id(1)), offset)],
1947 );
1948
1949 let g = drawing.route_geometry(rid).unwrap();
1950 assert_eq!(g.start_pos(), start0 + offset);
1951 assert_eq!(g.end_pos(), end0);
1952 }
1953
1954 #[test]
1958 fn place_route_label_stores_the_distance_it_is_given() {
1959 let mut scene = a_to_b_route(120.0);
1960 let rid = route_id(5);
1961
1962 let (on_wire, lid) = scene.commit(|drawing| {
1963 let geometry = drawing
1964 .route_geometry(rid)
1965 .expect("the route is reconstructed");
1966 let points = geometry.points();
1967 assert!(points.len() >= 2, "the route has at least one segment");
1968 let on_wire = points[0] + (points[1] - points[0]) * 0.5;
1969 let lid = drawing
1970 .add_route_label(rid, on_wire)
1971 .expect("the label lands on the reconstructed route");
1972 (on_wire, lid)
1973 });
1974 let _ = on_wire;
1975
1976 let stored = |scene: &mut Scene| -> blockworx_doc::geometry::FracVal {
1977 scene
1978 .drawing()
1979 .auto_route(rid)
1980 .expect("the route is in this scope")
1981 .labels
1982 .iter()
1983 .find(|(id, _)| *id == lid)
1984 .map(|&(_, dist)| dist)
1985 .expect("the label is on the wire")
1986 };
1987 let before = stored(&mut scene);
1988 let target = blockworx_doc::geometry::FracVal::from(f32::from(before) + GRID_SIZE);
1989 assert_ne!(before, target, "the test moves the label somewhere new");
1990
1991 scene.commit(|drawing| drawing.place_route_label(lid, target));
1992
1993 assert_eq!(stored(&mut scene), target);
1994 }
1995
1996 #[test]
1997 fn moving_a_distant_block_leaves_unrelated_routes_byte_identical() {
1998 let mut scene = a_to_b_route(120.0);
2000 let far = block_id(9);
2001 scene.apply(vec![fx::block(9, 600.0)]); let rid = route_id(5);
2003 let before = route_edges(&scene.drawing(), rid);
2004
2005 scene.commit(|drawing| {
2007 drawing.move_shapes(
2008 &[ShapeId::Rect(far)],
2009 vec2(2.0 * GRID_SIZE, 3.0 * GRID_SIZE),
2010 );
2011 });
2012
2013 assert_eq!(
2014 before,
2015 route_edges(&scene.drawing(), rid),
2016 "a distant block move must leave an unrelated route byte-identical"
2017 );
2018 }
2019
2020 #[test]
2021 fn editing_a_route_never_autoroutes_around_obstacles() {
2022 let mut scene = a_to_b_route(240.0);
2027 scene.apply(vec![fx::block_in(
2029 9,
2030 Scope::Root,
2031 Rect::from_min_max(pos2(120.0, 20.0), pos2(160.0, 60.0)),
2032 )]);
2033 let rid = route_id(5);
2034 let mut drawing = scene.drawing();
2035
2036 drawing
2037 .relay_route(rid, &[], &[], &[])
2038 .expect("the wire has resolvable endpoints");
2039
2040 assert_eq!(
2041 route_edges(&drawing, rid).len(),
2042 1,
2043 "the relayed wire stays a single straight leg"
2044 );
2045 assert!(
2046 route_passes_through(
2047 drawing.route_geometry(rid).unwrap(),
2048 GridPoint { x: 7, y: 2 }
2049 ),
2050 "the relay draws straight through the block — the editor never autoroutes"
2051 );
2052 }
2053
2054 #[test]
2055 fn drag_preview_reroutes_through_offset_waypoint() {
2056 let mut scene = two_blocks_with_a_routed_waypoint();
2060 let (a, b, rid) = (block_id(1), block_id(2), route_id(5));
2061 let mut drawing = scene.drawing();
2062 drawing.solve_routes(&[]);
2063 let stored_before = stored_corners(&drawing, rid);
2064 let (start0, end0) = {
2065 let g = drawing.route_geometry(rid).unwrap();
2066 (g.start_pos(), g.end_pos())
2067 };
2068
2069 let off = vec2(0.0, 4.0 * GRID_SIZE); drawing.preview_drag(
2071 &crate::widget::PreviewPhase::testing(),
2072 &[(ShapeId::Rect(a), off), (ShapeId::Rect(b), off)],
2073 );
2074
2075 let g = drawing.route_geometry(rid).unwrap();
2077 assert_eq!(g.start_pos(), start0 + off);
2078 assert_eq!(g.end_pos(), end0 + off);
2079
2080 assert_eq!(
2083 stored_corners(&drawing, rid),
2084 stored_before,
2085 "stored waypoints stay put; the preview is uncommitted"
2086 );
2087 }
2088
2089 #[test]
2090 fn drag_preview_leaves_partially_selected_route() {
2091 let mut scene = two_blocks_with_a_routed_waypoint();
2095 let (a, rid) = (block_id(1), route_id(5));
2096 let mut drawing = scene.drawing();
2097 drawing.solve_routes(&[]);
2098 let stored_before = stored_corners(&drawing, rid);
2099 let end0 = drawing.route_geometry(rid).unwrap().end_pos();
2100
2101 let off = vec2(0.0, 4.0 * GRID_SIZE);
2102 drawing.preview_drag(
2103 &crate::widget::PreviewPhase::testing(),
2104 &[(ShapeId::Rect(a), off)],
2105 );
2106
2107 assert_eq!(
2109 drawing.route_geometry(rid).unwrap().end_pos(),
2110 end0,
2111 "the un-dragged endpoint is fixed"
2112 );
2113
2114 let stored_after = stored_corners(&drawing, rid);
2118 assert!(
2119 stored_after.iter().all(|p| stored_before.contains(p)),
2120 "preview introduced new corners {stored_after:?} vs {stored_before:?}"
2121 );
2122 }
2123
2124 fn settled_bend() -> Scene {
2131 let tall = |n, x: f32| {
2132 fx::block_in(
2133 n,
2134 Scope::Root,
2135 Rect::from_min_max(pos2(x, 0.0), pos2(x + 40.0, 120.0)),
2136 )
2137 };
2138 let mut scene = Scene::new(vec![
2139 tall(1, 0.0),
2140 tall(2, 300.0),
2141 fx::pin(3, 1, PinSide::East, 0),
2142 fx::pin(4, 2, PinSide::West, 1),
2143 fx::route(5, Scope::Root, 3, 4, &[(10, 4)]),
2144 ]);
2145 fx::reconstruct(&mut scene);
2146 scene.commit(|drawing| drawing.reroute(route_id(5)));
2147 scene
2148 }
2149
2150 #[test]
2151 fn drag_preview_matches_commit() {
2152 let off = vec2(0.0, 4.0 * GRID_SIZE);
2155 let (a, b, rid) = (block_id(1), block_id(2), route_id(5));
2156
2157 let mut scene = settled_bend();
2159 let settled = scene.drawing().route_geometry(rid).unwrap().points();
2160 let previewed = {
2161 let mut preview = scene.drawing();
2162 preview.preview_drag(
2163 &crate::widget::PreviewPhase::testing(),
2164 &[(ShapeId::Rect(a), off), (ShapeId::Rect(b), off)],
2165 );
2166 preview.route_geometry(rid).unwrap().points()
2167 };
2168 assert_ne!(previewed, settled, "the preview moved the wire");
2171
2172 let mut scene2 = settled_bend();
2174 scene2.commit(|commit| commit.move_shapes(&[ShapeId::Rect(a), ShapeId::Rect(b)], off));
2175 let committed = scene2.drawing().route_geometry(rid).unwrap().points();
2178
2179 assert_eq!(
2180 previewed, committed,
2181 "drag preview geometry must equal the committed geometry (no release snap)"
2182 );
2183 }
2184
2185 #[test]
2186 fn a_gesture_removes_a_backtracking_waypoint() {
2187 let far = (15, 1);
2193 let near = (5, 1);
2194 let mut scene = Scene::new(vec![
2195 fx::block(1, 0.0),
2196 fx::block(2, 300.0),
2197 fx::pin(3, 1, PinSide::East, 0),
2198 fx::pin(4, 2, PinSide::West, 0),
2199 fx::route(5, Scope::Root, 3, 4, &[far, near]),
2200 ]);
2201 let rid = route_id(5);
2202 scene.commit(|drawing| drawing.reroute(rid));
2206
2207 let drawing = scene.drawing();
2208 let geometry = drawing
2209 .route_geometry(rid)
2210 .expect("the route is reconstructed");
2211 let edges: Vec<_> = geometry.iter_edges().map(|(_, e)| e.clone()).collect();
2212 let doubles_back = edges.windows(2).any(|w| {
2213 let (ax, ay) = (w[0].end.x - w[0].start.x, w[0].end.y - w[0].start.y);
2214 let (bx, by) = (w[1].end.x - w[1].start.x, w[1].end.y - w[1].start.y);
2215 (ay == 0 && by == 0 && (ax > 0) != (bx > 0))
2216 || (ax == 0 && bx == 0 && (ay > 0) != (by > 0))
2217 });
2218 assert!(!doubles_back, "the wire doubles back after the gesture");
2219 assert!(
2220 !stored_corners(&drawing, rid).contains(&GridPoint { x: far.0, y: far.1 }),
2221 "the overshoot waypoint was dropped"
2222 );
2223 }
2224
2225 #[test]
2226 fn trim_approach_drops_up_to_two_unlocked_corners_from_the_endpoint() {
2227 let wp = |x, y, locked| Waypoint {
2228 pos: GridPoint { x, y },
2229 locked,
2230 };
2231 let corners = [wp(2, 0, false), wp(4, 0, false), wp(6, 0, false)];
2232 let positions =
2233 |kept: Vec<Waypoint>| -> Vec<GridPoint> { kept.into_iter().map(|w| w.pos).collect() };
2234
2235 assert_eq!(
2236 positions(trimmed_approach(&corners, RouteEnd::From)),
2237 vec![GridPoint { x: 6, y: 0 }],
2238 "the two leading corners nearest the start are dropped"
2239 );
2240 assert_eq!(
2241 positions(trimmed_approach(&corners, RouteEnd::To)),
2242 vec![GridPoint { x: 2, y: 0 }],
2243 "the two trailing corners nearest the finish are dropped"
2244 );
2245
2246 let locked_first = [wp(2, 0, true), wp(4, 0, false)];
2249 assert_eq!(
2250 positions(trimmed_approach(&locked_first, RouteEnd::From)),
2251 vec![GridPoint { x: 2, y: 0 }, GridPoint { x: 4, y: 0 }],
2252 "a leading locked corner blocks the trim entirely"
2253 );
2254 }
2255
2256 #[test]
2260 fn a_moved_blocks_straddling_wires_are_all_trimmed_together() {
2261 let mut scene = Scene::new(vec![
2262 fx::block(1, 0.0),
2263 fx::block(2, 300.0),
2264 fx::block(6, 600.0),
2265 fx::pin(3, 1, PinSide::East, 0),
2266 fx::pin(7, 1, PinSide::East, 1),
2267 fx::pin(8, 1, PinSide::West, 0),
2268 fx::pin(4, 2, PinSide::West, 0),
2269 fx::pin(9, 6, PinSide::West, 0),
2270 fx::route(5, Scope::Root, 3, 4, &[(8, 1), (8, 3), (14, 3)]),
2271 fx::route(10, Scope::Root, 9, 7, &[(30, 5), (8, 5), (8, 2)]),
2272 fx::route(11, Scope::Root, 8, 3, &[(-2, 1), (-2, -3), (5, -3)]),
2273 ]);
2274 scene.begin("drop");
2275 scene
2276 .drawing()
2277 .trim_partial_route_approaches(&[ShapeId::Rect(block_id(1))]);
2278
2279 let written: Vec<(RouteId, Vec<GridPoint>)> = scene
2280 .gesture
2281 .ops()
2282 .iter()
2283 .filter_map(|op| match op {
2284 OpCodes::Route(id, Crud::Update(RouteUpdate::Waypoints(waypoints))) => {
2285 Some((*id, waypoints.iter().map(|wp| wp.pos).collect()))
2286 }
2287 _ => None,
2288 })
2289 .collect();
2290 let at = |x, y| GridPoint { x, y };
2291 assert_eq!(
2292 written,
2293 vec![
2294 (route_id(5), vec![at(14, 3)]),
2295 (route_id(10), vec![at(30, 5)]),
2296 ],
2297 "each straddler loses the corners at its moved end; the loop keeps its own"
2298 );
2299 }
2300
2301 #[test]
2302 fn flipping_a_block_reroutes_and_trims_like_a_drag() {
2303 let mut scene = two_blocks_with_a_routed_waypoint();
2308 let (a, rid) = (block_id(1), route_id(5));
2309
2310 scene.commit(|d| {
2311 d.flip_shape_pins(ShapeId::Rect(a));
2312 d.trim_partial_route_approaches(&[ShapeId::Rect(a)]);
2313 });
2314
2315 let d = scene.drawing();
2316 let wire = d.auto_route(rid).expect("the route survives the flip");
2317 let geometry = d.route_geometry(rid).expect("the route is reconstructed");
2318 assert!(
2319 geometry.iter_edges().next().is_some(),
2320 "the flipped route has geometry"
2321 );
2322 assert!(
2323 waypoints_are_the_corners(&wire, geometry),
2324 "flip re-route left the waypoints in sync with the corners"
2325 );
2326 }
2327
2328 #[test]
2329 fn flipping_a_port_reroutes_without_moving_the_parent_pin() {
2330 use crate::shape::pin::{orientation, slot};
2331 let (port_id, rid) = (pin_id(3), route_id(5));
2333 let mut scene = Scene::new(vec![
2334 fx::pin_at(
2335 3,
2336 Scope::Root,
2337 "io",
2338 fx::slot(PinSide::East, 1),
2339 Rect::from_min_max(pos2(0.0, 45.0), pos2(40.0, 61.0)),
2340 ),
2341 fx::block(2, 120.0),
2342 fx::pin(4, 2, PinSide::West, 0),
2343 fx::route(5, Scope::Root, 3, 4, &[]),
2344 ]);
2345 let pin = scene
2346 .drawing()
2347 .held_pin(port_id)
2348 .expect("the port's pin")
2349 .clone();
2350 let (before_side, before_orientation) = (slot(&pin).side, orientation(&pin));
2351 assert_eq!(before_orientation, before_side.flip());
2353
2354 scene.commit(|drawing| drawing.flip_shape_pins(ShapeId::Port(port_id)));
2355
2356 let drawing = scene.drawing();
2357 let pin = drawing.held_pin(port_id).expect("the port's pin").clone();
2358 assert_eq!(
2361 slot(&pin).side,
2362 before_side,
2363 "flipping the port moved the parent pin"
2364 );
2365 assert_eq!(orientation(&pin), before_orientation.flip());
2366
2367 let port = drawing.shape(ShapeId::Port(port_id)).unwrap();
2369 let anchor = port
2370 .anchor_point_with_rect(port.gui_rect(), port_id)
2371 .unwrap();
2372 assert_eq!(drawing.route_geometry(rid).unwrap().start_pos(), anchor);
2373 }
2374
2375 fn waypoints_are_the_corners(wire: &Wire<'_>, geometry: &RouteGeometry) -> bool {
2380 let edges: Vec<_> = geometry.iter_edges().map(|(_, e)| e).collect();
2381 let corners: std::collections::HashSet<GridPoint> =
2382 edges.windows(2).map(|w| w[0].end).collect();
2383 let waypoints: std::collections::HashSet<GridPoint> =
2384 wire.route.waypoints.iter().map(|wp| wp.pos).collect();
2385 corners == waypoints
2386 }
2387
2388 #[test]
2389 fn dragging_a_block_keeps_route_waypoints_matching_the_corners() {
2390 let (b, rid) = (block_id(2), route_id(5));
2393 let mut scene = Scene::new(vec![
2394 fx::block(1, 0.0),
2395 fx::block_in(
2396 2,
2397 Scope::Root,
2398 Rect::from_min_max(pos2(300.0, 150.0), pos2(340.0, 190.0)),
2399 ),
2400 fx::pin(3, 1, PinSide::East, 0),
2401 fx::pin(4, 2, PinSide::West, 0),
2402 fx::route(5, Scope::Root, 3, 4, &[]),
2403 ]);
2404 scene.drawing().solve_routes(&[]);
2405
2406 let delta = vec2(0.0, 4.0 * GRID_SIZE);
2409 for frame in 1..=4 {
2410 scene.drawing().preview_drag(
2411 &crate::widget::PreviewPhase::testing(),
2412 &[(ShapeId::Rect(b), vec2(0.0, frame as f32 * GRID_SIZE))],
2413 );
2414 }
2415 scene.commit(|d| {
2416 d.move_shape(ShapeId::Rect(b), delta);
2417 d.trim_partial_route_approaches(&[ShapeId::Rect(b)]);
2418 });
2419
2420 let d = scene.drawing();
2421 let wire = d.auto_route(rid).unwrap();
2422 let geometry = d.route_geometry(rid).expect("the route is reconstructed");
2423 assert!(
2424 waypoints_are_the_corners(&wire, geometry),
2425 "after a drag the route's waypoints must match its corners"
2426 );
2427 }
2428
2429 #[test]
2430 fn flipping_a_block_preserves_its_childrens_internal_routes() {
2431 let (c, pc, rid) = (block_id(1), pin_id(3), route_id(5));
2433 let mut scene = Scene::new(vec![
2434 fx::block(1, 0.0),
2435 fx::pin_at(
2436 3,
2437 Scope::Block(c),
2438 "io",
2439 fx::slot(PinSide::West, 1),
2440 Rect::from_min_max(pos2(0.0, 45.0), pos2(40.0, 61.0)),
2441 ),
2442 fx::block_in(
2443 2,
2444 Scope::Block(c),
2445 Rect::from_min_max(pos2(60.0, 0.0), pos2(100.0, 40.0)),
2446 ),
2447 fx::pin(4, 2, PinSide::East, 0),
2448 fx::route(5, Scope::Block(c), 3, 4, &[]),
2449 ]);
2450
2451 let anchor_before = {
2452 scene.path.push(c);
2453 let mut drawing = scene.drawing();
2454 drawing.solve_routes(&[]);
2455 let port = drawing.shape(ShapeId::Port(pc)).unwrap();
2456 port.anchor_point_with_rect(port.gui_rect(), pc).unwrap()
2457 };
2458
2459 scene.path.pop();
2462 scene.commit(|drawing| drawing.flip_shape_pins(ShapeId::Rect(c)));
2463
2464 scene.path.push(c);
2467 let mut drawing = scene.drawing();
2468 drawing.solve_routes(&[]);
2469 let port = drawing.shape(ShapeId::Port(pc)).unwrap();
2470 let anchor_after = port.anchor_point_with_rect(port.gui_rect(), pc).unwrap();
2471 assert_eq!(
2472 anchor_after, anchor_before,
2473 "flipping the block moved its interior port"
2474 );
2475 assert_eq!(
2476 drawing.route_geometry(rid).unwrap().start_pos(),
2477 anchor_after,
2478 "internal route lost its port endpoint"
2479 );
2480 }
2481
2482 #[test]
2485 fn a_commit_takes_back_a_preview_it_never_disturbed() {
2486 let mut scene = Scene::new(vec![
2487 fx::block(1, 0.0),
2488 fx::block(2, 120.0),
2489 fx::block(6, 3000.0),
2490 fx::pin(3, 1, PinSide::East, 0),
2491 fx::pin(4, 2, PinSide::West, 0),
2492 fx::route(5, Scope::Root, 3, 4, &[]),
2493 ]);
2494 let rid = route_id(5);
2495 let settled = route_edges(&scene.drawing(), rid);
2496 {
2497 let mut drawing = scene.drawing();
2498 drawing.preview_drag(
2499 &crate::widget::PreviewPhase::testing(),
2500 &[(ShapeId::Rect(block_id(2)), vec2(0.0, 4.0 * GRID_SIZE))],
2501 );
2502 assert_ne!(
2503 route_edges(&drawing, rid),
2504 settled,
2505 "precondition: the preview drew the wire somewhere else"
2506 );
2507 }
2508 scene.commit(|drawing| {
2509 drawing.move_shape(ShapeId::Rect(block_id(6)), vec2(GRID_SIZE, 0.0));
2510 });
2511 assert!(
2512 !scene.committed().is_empty(),
2513 "precondition: the far move committed"
2514 );
2515 assert_eq!(
2516 route_edges(&scene.drawing(), rid),
2517 settled,
2518 "the preview outlived the commit"
2519 );
2520 }
2521
2522 #[test]
2523 fn drag_and_resize_preview_frames_never_touch_the_document() {
2524 let mut scene = two_blocks_with_a_routed_waypoint();
2527 let (a, rid) = (block_id(1), route_id(5));
2528 scene.drawing().solve_routes(&[]);
2529 assert!(
2530 !stored_corners(&scene.drawing(), rid).is_empty(),
2531 "precondition: the committed route must carry corners to trim"
2532 );
2533 let before = scene.doc.clone();
2534 let stamp = scene.doc.stamp();
2535
2536 {
2537 let mut drawing = scene.drawing();
2538 let start0 = drawing.route_geometry(rid).unwrap().start_pos();
2539 for frame in 1..=4 {
2540 let offset = vec2(GRID_SIZE * frame as f32, GRID_SIZE);
2541 drawing.preview_drag(
2542 &crate::widget::PreviewPhase::testing(),
2543 &[(ShapeId::Rect(a), offset)],
2544 );
2545 }
2546 assert_ne!(
2547 drawing.route_geometry(rid).unwrap().start_pos(),
2548 start0,
2549 "precondition: the preview really re-solved the dragged route"
2550 );
2551 let grown = drawing.shape(ShapeId::Rect(a)).unwrap().gui_rect();
2552 drawing.preview_resize(
2553 &crate::widget::PreviewPhase::testing(),
2554 &[(
2555 ShapeId::Rect(a),
2556 Rect::from_min_size(grown.min, grown.size() + vec2(GRID_SIZE, GRID_SIZE)),
2557 )],
2558 );
2559 }
2560
2561 assert_eq!(
2562 scene.doc.stamp(),
2563 stamp,
2564 "a preview frame must not produce a new document value"
2565 );
2566 assert_eq!(scene.doc.clone(), before);
2567 }
2568
2569 #[test]
2570 fn a_pin_drag_preview_tracks_the_hypothetical_slot_without_moving_the_pin() {
2571 let mut scene = a_to_b_route(120.0);
2572 let (a, pa, rid) = (block_id(1), pin_id(3), route_id(5));
2573 scene.drawing().solve_routes(&[]);
2574 let before = scene.doc.clone();
2575 let stamp = scene.doc.stamp();
2576
2577 let (start0, start, previewed) = {
2578 let mut drawing = scene.drawing();
2579 let start0 = drawing.route_geometry(rid).unwrap().start_pos();
2580 drawing.preview_pin_drag(
2581 &crate::widget::PreviewPhase::testing(),
2582 pa,
2583 PinSide::East,
2584 3,
2585 );
2586 let start = drawing.route_geometry(rid).unwrap().start_pos();
2587 let previewed = match drawing.shape(ShapeId::Rect(a)).unwrap() {
2588 ShapeRef::Block(block) => block.pin_anchor_at(block.gui_rect(), PinSide::East, 3),
2589 _ => unreachable!("a is a block"),
2590 };
2591 (start0, start, previewed)
2592 };
2593
2594 assert_ne!(start, start0, "precondition: slot 3 moves the anchor");
2595 assert_eq!(
2596 grid_point(start),
2597 grid_point(previewed),
2598 "the previewed endpoint sits at the hypothetical slot"
2599 );
2600 assert_eq!(scene.doc.stamp(), stamp);
2601 assert_eq!(scene.doc.clone(), before);
2602
2603 {
2605 let mut drawing = scene.drawing();
2606 drawing.preview_pin_drags(
2607 &crate::widget::PreviewPhase::testing(),
2608 &[PinMove {
2609 pin: pa,
2610 to: PinSlot {
2611 side: PinSide::East,
2612 offset: 2,
2613 },
2614 }],
2615 );
2616 }
2617 assert_eq!(scene.doc.stamp(), stamp);
2618 assert_eq!(scene.doc.clone(), before);
2619 }
2620}
2621
2622#[cfg(test)]
2623mod region_cost {
2624 use super::*;
2625 use crate::widget::test_fixtures::{cells, scale_scene};
2626 use std::time::Instant;
2627
2628 #[test]
2633 #[ignore = "a timing, not a check"]
2634 fn a_region_lattice_costs_what_the_region_holds() {
2635 let mut scene = scale_scene(50);
2636 let drawing = scene.drawing();
2637
2638 let mut seeds: Vec<Point> = Vec::new();
2639 for id in drawing.scope_route_ids() {
2640 let Some(route) = drawing.route(id) else {
2641 continue;
2642 };
2643 for end in [route.from, route.to] {
2644 if let Some(at) = drawing.anchor_pos_overridden(end, GeometryOverrides::default()) {
2645 seeds.push(snap_to_grid(at).into());
2646 }
2647 }
2648 for wp in &route.waypoints {
2649 seeds.push(wp.pos.into());
2650 }
2651 }
2652
2653 let whole = Instant::now();
2654 let full =
2655 drawing.build_router_within(Region::WholeScope, GeometryOverrides::default(), &seeds);
2656 let whole = whole.elapsed();
2657
2658 let region = cells(-8, -8, 56, 56);
2661 let local = Instant::now();
2662 let bounded = drawing.build_router_within(
2663 Region::Within(region),
2664 GeometryOverrides::default(),
2665 &seeds,
2666 );
2667 let local = local.elapsed();
2668
2669 let raised = Instant::now();
2670 let foreground = crate::widget::foreground::Foreground::raising(
2671 drawing
2672 .child_blocks()
2673 .first()
2674 .map(|&(id, _)| crate::shape::ShapeId::Rect(id)),
2675 &drawing,
2676 );
2677 let raised = raised.elapsed();
2678
2679 eprintln!(
2680 "==== whole sheet: {whole:?}, {} nodes",
2681 full.fingerprint().nodes()
2682 );
2683 eprintln!(
2684 "==== raising one block: {raised:?}, {} of {} wires raised",
2685 foreground.routes().count(),
2686 drawing.scope_route_ids().len()
2687 );
2688 eprintln!(
2689 "==== 3×3 region: {local:?}, {} nodes",
2690 bounded.fingerprint().nodes()
2691 );
2692 }
2693}