1use std::collections::{HashMap, HashSet};
10
11use blockworx_doc::{
12 commit::CommitBuilder,
13 document::IndexedDocument,
14 geometry::{GridPoint, GridVec, Waypoint},
15 id::{PinId, RouteId, RouteLabelId},
16 values::PinSide,
17};
18use blockworx_geom::{Pos2, Rect, Vec2, vec2};
19use blockworx_router::{
20 ClosedRouter, RouterNGBuilder, WIRE_COST, cost::COST_ZERO, point::Point, turtle::Mark,
21};
22
23use crate::{
24 edit::{
25 create::PathOrdinal,
26 geometry::{PinMove, RouteEnd, backtracking_ordinals, pruned_waypoints, trimmed_ordinals},
27 naming::InterfaceLock,
28 },
29 grid::{GRID_SIZE, grid_point, px_point, snap_to_grid},
30 path::BlockPath,
31 presentation::Presentation,
32 shape::{ShapeId, ShapeRef},
33 widget::{
34 auto_route::{
35 label_anchors, reanchored, recompute_route_crossings, reroute_preview_closed,
36 reroute_preview_excluding,
37 },
38 drawing::Drawing,
39 materialize::{
40 Endpoints, Obstacles, materialize_corners_direct, materialize_route,
41 promote_corners_to_waypoints,
42 },
43 movement::moved_set_and_delta,
44 waypoint_router::{add_route_cost, route_edges_blocked},
45 },
46};
47pub type Promoted = Vec<(RouteId, Vec<Waypoint>)>;
52
53pub(crate) fn solve_rider(
62 indexed: &IndexedDocument<'_>,
63 path: &BlockPath,
64 presentation: &Presentation,
65 sink: &mut CommitBuilder,
66) {
67 let mut scratch = presentation.scratch(indexed);
68 let mut discarded = crate::gesture::Gesture::idle();
69 let promoted = {
70 let mut drawing = Drawing::new(*indexed, path, &mut scratch, &mut discarded);
71 drawing.solve_routes(&[])
72 };
73 debug_assert!(
74 discarded.ops().is_empty(),
75 "the solve is a read; it must author nothing of its own"
76 );
77 for (route, waypoints) in promoted {
78 crate::edit::geometry::commit_route_edit(
79 indexed.doc,
80 crate::edit::geometry::RouteEdit {
81 route,
82 waypoints,
83 anchors: &[],
84 },
85 sink,
86 );
87 }
88}
89
90pub(super) fn obstacle_rect(rect: Rect) -> blockworx_router::block::Block {
94 blockworx_router::block::Block {
95 top_left: rect.left_top().into(),
96 bottom_right: rect.right_bottom().into(),
97 }
98}
99
100enum RoutePass<'a> {
104 Preview(PreviewSpec<'a>),
105 Commit,
106}
107
108struct PreviewSpec<'a> {
114 grid_delta: GridVec,
115 inside: &'a dyn Fn(PinId) -> bool,
116}
117
118type RelaidRoute = (
121 Vec<Waypoint>,
122 Vec<(RouteLabelId, blockworx_doc::geometry::FracVal)>,
123);
124
125pub struct PinSlotOverride {
128 pub pin: PinId,
129 pub side: PinSide,
130 pub offset: u32,
131}
132
133#[derive(Clone, Copy, Default)]
137struct GeometryOverrides<'a> {
138 rects: &'a [(ShapeId, Rect)],
139 pins: &'a [PinSlotOverride],
140}
141
142#[derive(Default)]
148struct PreviewExclusions {
149 excluded: HashSet<PathOrdinal>,
150 pruned: bool,
151}
152
153pub struct RouteEditSession {
161 slots: Vec<WaypointSlot>,
162 anchors: Vec<(RouteLabelId, Pos2)>,
163}
164
165enum WaypointSlot {
168 Existing(PathOrdinal),
171 InsertAt(usize),
176}
177
178fn keyed_corners<T>(
183 waypoints: &[Waypoint],
184 value: impl Fn(&Waypoint) -> T,
185) -> Vec<(Option<PathOrdinal>, T)> {
186 waypoints
187 .iter()
188 .enumerate()
189 .map(|(index, wp)| (Some(PathOrdinal::new(index)), value(wp)))
190 .collect()
191}
192
193impl RouteEditSession {
194 pub fn anchors(&self) -> &[(RouteLabelId, Pos2)] {
197 &self.anchors
198 }
199
200 pub fn corners(
204 &self,
205 waypoints: &[Waypoint],
206 cursors: &[Pos2],
207 ) -> Vec<(GridPoint, InterfaceLock)> {
208 let mut corners = keyed_corners(waypoints, |wp| {
211 let lock = if wp.locked {
212 InterfaceLock::Locked
213 } else {
214 InterfaceLock::Unlocked
215 };
216 (wp.pos, lock)
217 });
218 for (slot, &cursor) in self.slots.iter().zip(cursors) {
219 let pos = grid_point(cursor);
220 match slot {
221 WaypointSlot::Existing(ordinal) => {
222 if let Some(c) = corners.iter_mut().find(|(o, _)| *o == Some(*ordinal)) {
223 c.1 = (pos, InterfaceLock::Locked);
224 }
225 }
226 WaypointSlot::InsertAt(i) => {
231 let i = (*i).min(corners.len());
232 corners.insert(i, (None, (pos, InterfaceLock::Locked)));
233 }
234 }
235 }
236 corners.into_iter().map(|(_, corner)| corner).collect()
237 }
238}
239
240fn override_rect(shape: ShapeId, overrides: &[(ShapeId, Rect)]) -> Option<Rect> {
243 overrides
244 .iter()
245 .find(|(overridden, _)| *overridden == shape)
246 .map(|(_, rect)| *rect)
247}
248
249fn override_slot(pin: PinId, pins: &[PinSlotOverride]) -> Option<&PinSlotOverride> {
251 pins.iter().find(|o| o.pin == pin)
252}
253
254impl Drawing<'_> {
255 pub fn solve_routes(&mut self, ripup: &[RouteId]) -> Promoted {
260 self.route_update_closed(ripup, GeometryOverrides::default(), RoutePass::Commit)
261 }
262
263 pub fn suppose_drag(
269 &mut self,
270 _phase: &crate::tools::tool::Supposing,
271 drags: &[(ShapeId, Vec2)],
272 ) {
273 let overrides: Vec<(ShapeId, Rect)> = drags
275 .iter()
276 .filter_map(|&(shape, offset)| {
277 Some((shape, self.shape(shape)?.gui_rect().translate(offset)))
278 })
279 .collect();
280 self.suppose_shapes(&overrides);
281 if !drags.iter().any(|&(shape, _)| shape.affects_routing()) {
284 return;
285 }
286 let ids: Vec<ShapeId> = drags.iter().map(|&(shape, _)| shape).collect();
294 let delta = drags.first().map_or(Vec2::ZERO, |&(_, offset)| offset);
295 let (moved_rects, moved_pins, grid_delta) = moved_set_and_delta(&ids, delta);
296 let riding = self.riding_pins(&moved_rects, &moved_pins);
297 let inside = |pin: PinId| riding.contains(&pin);
298 let _ = self.route_update_closed(
299 &[],
300 GeometryOverrides {
301 rects: &overrides,
302 pins: &[],
303 },
304 RoutePass::Preview(PreviewSpec {
305 grid_delta,
306 inside: &inside,
307 }),
308 );
309 }
310
311 pub fn suppose_resize(
315 &mut self,
316 _phase: &crate::tools::tool::Supposing,
317 resizes: &[(ShapeId, Rect)],
318 ) {
319 self.suppose_shapes(resizes);
320 if !resizes.iter().any(|&(shape, _)| shape.affects_routing()) {
321 return;
322 }
323 let _ = self.route_update_closed(
324 &[],
325 GeometryOverrides {
326 rects: resizes,
327 pins: &[],
328 },
329 RoutePass::Preview(PreviewSpec {
330 grid_delta: GridVec::new(0, 0),
331 inside: &|_| false,
332 }),
333 );
334 }
335
336 pub fn suppose_pin_drag(
342 &mut self,
343 _phase: &crate::tools::tool::Supposing,
344 pin: PinId,
345 side: PinSide,
346 offset: u32,
347 ) {
348 let slots = [PinSlotOverride { pin, side, offset }];
349 let inside = move |other: PinId| other == pin;
350 let _ = self.route_update_closed(
351 &[],
352 GeometryOverrides {
353 rects: &[],
354 pins: &slots,
355 },
356 RoutePass::Preview(PreviewSpec {
357 grid_delta: GridVec::new(0, 0),
358 inside: &inside,
359 }),
360 );
361 }
362
363 pub fn suppose_pin_drags(&mut self, _phase: &crate::tools::tool::Supposing, moves: &[PinMove]) {
370 let slots: Vec<PinSlotOverride> = moves
371 .iter()
372 .map(|m| PinSlotOverride {
373 pin: m.pin,
374 side: m.to.side,
375 offset: m.to.offset,
376 })
377 .collect();
378 let _ = self.route_update_closed(
379 &[],
380 GeometryOverrides {
381 rects: &[],
382 pins: &slots,
383 },
384 RoutePass::Preview(PreviewSpec {
385 grid_delta: GridVec::new(0, 0),
386 inside: &|_| false,
387 }),
388 );
389 }
390
391 fn anchor_pos_overridden(&self, pin: PinId, hypo: GeometryOverrides<'_>) -> Option<Pos2> {
396 let sid = self.pin_shape(pin)?;
397 let shape = self.shape(sid)?;
398 let rect = override_rect(sid, hypo.rects).unwrap_or_else(|| shape.gui_rect());
399 match (override_slot(pin, hypo.pins), &shape) {
400 (Some(o), ShapeRef::Block(block)) => Some(block.pin_anchor_at(rect, o.side, o.offset)),
401 _ => shape.anchor_point_with_rect(rect, pin),
402 }
403 }
404
405 fn routing_shapes(&self) -> Vec<(ShapeId, ShapeRef<'_>)> {
410 self.blocks_layer().chain(self.ports_layer()).collect()
411 }
412
413 fn build_closed_router(
420 &self,
421 hypo: GeometryOverrides<'_>,
422 seed_points: &[Point],
423 ) -> ClosedRouter {
424 let mut builder = RouterNGBuilder::default();
425 for (sid, shape) in self.routing_shapes() {
426 let rect = override_rect(sid, hypo.rects).unwrap_or_else(|| shape.gui_rect());
430 builder.add_block(rect.left_top(), rect.right_bottom());
431 shape.with_pins(|pid, _pin| {
432 let slot = match &shape {
433 ShapeRef::Block(block) => override_slot(pid, hypo.pins)
434 .map(|o| block.pin_anchor_at(rect, o.side, o.offset)),
435 _ => None,
436 };
437 let Some(anchor) = slot.or_else(|| shape.anchor_point_with_rect(rect, pid)) else {
438 return;
439 };
440 let anchor_pos = if anchor.x >= rect.center().x {
441 anchor + vec2(GRID_SIZE, 0.0)
442 } else {
443 anchor - vec2(GRID_SIZE, 0.0)
444 };
445 builder.add_h_channel(anchor_pos, COST_ZERO);
446 });
447 }
448 for &p in seed_points {
449 builder.add_seed_point(p);
450 }
451 builder.build_closed()
452 }
453
454 fn obstacle_rects(&self, overrides: &[(ShapeId, Rect)]) -> Obstacles {
461 let rects = self
462 .routing_shapes()
463 .into_iter()
464 .map(|(sid, shape)| {
465 obstacle_rect(override_rect(sid, overrides).unwrap_or_else(|| shape.gui_rect()))
466 })
467 .collect();
468 Obstacles::new(rects)
469 }
470
471 fn preview_exclusions(
480 &self,
481 endpoints: &[(RouteId, Pos2, Pos2)],
482 spec: &PreviewSpec<'_>,
483 ) -> HashMap<RouteId, PreviewExclusions> {
484 endpoints
485 .iter()
486 .filter_map(|&(id, s, e)| {
487 let route = self.route(id)?;
488 let stored = route.waypoints.clone();
489 let (start_in, finish_in) = ((spec.inside)(route.from), (spec.inside)(route.to));
490 if start_in && finish_in {
491 return None;
492 }
493 let mut excluded: HashSet<PathOrdinal> = match (start_in, finish_in) {
494 (true, false) => trimmed_ordinals(&stored, RouteEnd::From),
495 (false, true) => trimmed_ordinals(&stored, RouteEnd::To),
496 _ => Vec::new(),
497 }
498 .into_iter()
499 .collect();
500 let working: Vec<Waypoint> = stored
501 .iter()
502 .enumerate()
503 .filter(|(index, _)| !excluded.contains(&PathOrdinal::new(*index)))
504 .map(|(_, wp)| *wp)
505 .collect();
506 let survivors: Vec<PathOrdinal> = (0..stored.len())
509 .map(PathOrdinal::new)
510 .filter(|o| !excluded.contains(o))
511 .collect();
512 let doomed = backtracking_ordinals(&working, grid_point(s), grid_point(e));
513 let pruned = !doomed.is_empty();
514 excluded.extend(
515 doomed
516 .into_iter()
517 .filter_map(|o| survivors.get(usize::from(o)).copied()),
518 );
519 (!excluded.is_empty()).then_some((id, PreviewExclusions { excluded, pruned }))
520 })
521 .collect()
522 }
523
524 #[tracing::instrument(level = "info", skip_all, fields(routes = self.scope_route_ids().len(), ripup = ripup.len()))]
543 fn route_update_closed(
544 &mut self,
545 ripup: &[RouteId],
546 hypo: GeometryOverrides<'_>,
547 pass: RoutePass<'_>,
548 ) -> Promoted {
549 let ids = self.scope_route_ids();
550 let endpoints: Vec<(RouteId, Pos2, Pos2)> = ids
551 .iter()
552 .filter_map(|&id| {
553 let route = self.route(id)?;
554 let s = self.anchor_pos_overridden(route.from, hypo)?;
555 let e = self.anchor_pos_overridden(route.to, hypo)?;
556 Some((id, s, e))
557 })
558 .collect();
559 let exclusions: HashMap<RouteId, PreviewExclusions> = match &pass {
563 RoutePass::Preview(spec) => self.preview_exclusions(&endpoints, spec),
564 RoutePass::Commit => HashMap::new(),
565 };
566 let previewed = matches!(pass, RoutePass::Preview(_));
567 let obstacles = matches!(pass, RoutePass::Commit).then(|| self.obstacle_rects(hypo.rects));
568 let mut seed_points: Vec<Point> = Vec::with_capacity(endpoints.len() * 2);
573 for &(_, s, e) in &endpoints {
574 seed_points.push(snap_to_grid(s).into());
575 seed_points.push(snap_to_grid(e).into());
576 }
577 for &id in &ids {
578 let Some(route) = self.route(id) else {
579 continue;
580 };
581 let offset = match &pass {
582 RoutePass::Preview(spec)
583 if (spec.inside)(route.from) && (spec.inside)(route.to) =>
584 {
585 Some(spec.grid_delta)
586 }
587 _ => None,
588 };
589 let excluded = exclusions.get(&id);
590 for (index, wp) in route.waypoints.iter().enumerate() {
591 if excluded.is_some_and(|x| x.excluded.contains(&PathOrdinal::new(index))) {
592 continue;
593 }
594 let pos = wp.pos;
595 let pos = match offset {
596 Some(delta) => pos + delta,
597 None => pos,
598 };
599 seed_points.push(pos.into());
600 }
601 }
602 let mut router = {
603 let _s = tracing::info_span!("closed_build", seeds = seed_points.len()).entered();
604 self.build_closed_router(hypo, &seed_points)
605 };
606 let route_loop = tracing::info_span!("closed_route_loop").entered();
609 let (indexed, presentation) = self.split();
610 let doc = indexed.doc;
611 let mut promoted: Promoted = Vec::new();
612 let mut resolved: Vec<RouteId> = Vec::new();
613 match pass {
614 RoutePass::Commit => {
615 let routes = &mut presentation.routes;
616 for (id, anchor_start, anchor_end) in endpoints {
617 let Some(route) = doc.route(&id) else {
618 continue;
619 };
620 let geometry = routes.entry(id).or_default();
621 let doomed = backtracking_ordinals(
625 &route.waypoints,
626 grid_point(anchor_start),
627 grid_point(anchor_end),
628 );
629 let pruned = !doomed.is_empty();
630 let stored = if pruned {
631 pruned_waypoints(&route.waypoints, &doomed)
632 } else {
633 route.waypoints.clone()
634 };
635 let unchanged = !pruned
636 && geometry.start_pos() == anchor_start
637 && geometry.end_pos() == anchor_end
638 && !route_edges_blocked(&router, geometry.iter_edges().map(|(_, e)| e))
639 && !ripup.contains(&id);
640 if unchanged {
641 promoted.push((id, promote_corners_to_waypoints(&stored, geometry)));
651 add_route_cost(
652 &mut router,
653 geometry.iter_edges().map(|(_, edge)| edge),
654 WIRE_COST,
655 );
656 } else if let Some(obstacles) = &obstacles {
657 if let Some(corners) = materialize_route(
662 &stored,
663 geometry,
664 Endpoints {
665 start: grid_point(anchor_start),
666 end: grid_point(anchor_end),
667 },
668 obstacles,
669 Some(&mut router),
670 ) {
671 promoted.push((id, corners));
672 }
673 }
674 }
675 }
676 RoutePass::Preview(spec) => {
677 let routes = &mut presentation.routes;
678 let no_exclusions = PreviewExclusions::default();
679 for (id, anchor_start, anchor_end) in endpoints {
680 let Some(route) = doc.route(&id) else {
681 continue;
682 };
683 let geometry = routes.entry(id).or_default();
684 let exclusion = exclusions.get(&id).unwrap_or(&no_exclusions);
685 let unchanged = !exclusion.pruned
686 && geometry.start_pos() == anchor_start
687 && geometry.end_pos() == anchor_end
688 && !route_edges_blocked(&router, geometry.iter_edges().map(|(_, e)| e))
689 && !ripup.contains(&id);
690 if unchanged {
691 add_route_cost(
692 &mut router,
693 geometry.iter_edges().map(|(_, edge)| edge),
694 WIRE_COST,
695 );
696 continue;
697 }
698 let ends = Endpoints {
699 start: grid_point(anchor_start),
700 end: grid_point(anchor_end),
701 };
702 let stored = route.waypoints.clone();
703 if (spec.inside)(route.from) && (spec.inside)(route.to) {
704 reroute_preview_closed(
705 &stored,
706 geometry,
707 ends,
708 spec.grid_delta,
709 &mut router,
710 );
711 } else {
712 reroute_preview_excluding(
713 &stored,
714 geometry,
715 ends,
716 &exclusion.excluded,
717 &mut router,
718 );
719 }
720 resolved.push(id);
724 }
725 }
726 }
727 drop(route_loop);
728 {
729 let _s = tracing::info_span!("closed_crossings").entered();
730 recompute_route_crossings(&ids, &mut self.presentation.routes);
731 }
732 if previewed {
733 self.presentation.routes_supposed();
734 self.suppose_routes(resolved);
735 }
736 promoted
737 }
738
739 pub fn materialize_routes(&mut self) {
747 let ids = self.scope_route_ids();
748 let endpoints: Vec<(RouteId, GridPoint, GridPoint)> = ids
749 .iter()
750 .filter_map(|&id| {
751 let route = self.route(id)?;
752 let s = self.anchor_pos_overridden(route.from, GeometryOverrides::default())?;
753 let e = self.anchor_pos_overridden(route.to, GeometryOverrides::default())?;
754 Some((id, grid_point(s), grid_point(e)))
755 })
756 .collect();
757 let obstacles = self.obstacle_rects(&[]);
758
759 let mut deferred: Vec<(RouteId, GridPoint, GridPoint)> = Vec::new();
762 {
763 let (indexed, presentation) = self.split();
764 let doc = indexed.doc;
765 let routes = &mut presentation.routes;
766 for &(id, s, e) in &endpoints {
767 let Some(route) = doc.route(&id) else {
768 continue;
769 };
770 let geometry = routes.entry(id).or_default();
771 let ends = Endpoints { start: s, end: e };
772 if materialize_route(&route.waypoints, geometry, ends, &obstacles, None).is_none() {
773 deferred.push((id, s, e));
774 }
775 }
776 }
777
778 if !deferred.is_empty() {
781 let deferred_ids: HashSet<RouteId> = deferred.iter().map(|&(id, ..)| id).collect();
782 let mut router = {
783 let mut seeds: Vec<Point> = Vec::new();
784 for &(_, s, e) in &endpoints {
785 seeds.push(snap_to_grid(px_point(s)).into());
786 seeds.push(snap_to_grid(px_point(e)).into());
787 }
788 for &id in &ids {
789 let Some(route) = self.route(id) else {
790 continue;
791 };
792 for wp in route.waypoints.clone() {
793 seeds.push(wp.pos.into());
794 }
795 }
796 let mut router = self.build_closed_router(GeometryOverrides::default(), &seeds);
797 for &id in &ids {
798 if !deferred_ids.contains(&id)
799 && let Some(geometry) = self.presentation.routes.get(&id)
800 {
801 add_route_cost(
802 &mut router,
803 geometry.iter_edges().map(|(_, e)| e),
804 WIRE_COST,
805 );
806 }
807 }
808 router
809 };
810 let (indexed, presentation) = self.split();
811 let doc = indexed.doc;
812 let routes = &mut presentation.routes;
813 for (id, s, e) in deferred {
814 let Some(route) = doc.route(&id) else {
815 continue;
816 };
817 let geometry = routes.entry(id).or_default();
818 let ends = Endpoints { start: s, end: e };
819 materialize_route(
820 &route.waypoints,
821 geometry,
822 ends,
823 &obstacles,
824 Some(&mut router),
825 );
826 }
827 }
828
829 recompute_route_crossings(&ids, &mut self.presentation.routes);
830 }
831
832 fn route_ends(&self, id: RouteId) -> Option<Endpoints> {
835 let route = self.route(id)?;
836 let s = self.anchor_pos_overridden(route.from, GeometryOverrides::default())?;
837 let e = self.anchor_pos_overridden(route.to, GeometryOverrides::default())?;
838 Some(Endpoints {
839 start: grid_point(s),
840 end: grid_point(e),
841 })
842 }
843
844 fn relay_route(
851 &mut self,
852 id: RouteId,
853 corners: &[GridPoint],
854 stored: &[Waypoint],
855 anchors: &[(RouteLabelId, Pos2)],
856 ) -> Option<RelaidRoute> {
857 let ends = self.route_ends(id)?;
858 let ids = self.scope_route_ids();
859 let geometry = self.presentation.routes.entry(id).or_default();
860 materialize_corners_direct(corners, geometry, ends);
861 let relayed = reanchored(geometry, anchors);
862 let promoted = promote_corners_to_waypoints(stored, geometry);
863 recompute_route_crossings(&ids, &mut self.presentation.routes);
864 self.presentation.routes_supposed();
868 Some((promoted, relayed))
869 }
870
871 pub fn route_edit_session(&self, id: RouteId, cursors: &[Pos2]) -> Option<RouteEditSession> {
880 let wire = self.auto_route(id)?;
881 let geometry = self.route_geometry(id)?;
882 let mut working = keyed_corners(&wire.route.waypoints, |wp| px_point(wp.pos));
887 let mut slots = Vec::with_capacity(cursors.len());
888 for &cursor in cursors {
889 let hit = working
890 .iter()
891 .find(|(_, pos)| pos.distance(cursor) <= GRID_SIZE * 0.5)
892 .copied();
893 match hit {
894 Some((Some(ordinal), _)) => slots.push(WaypointSlot::Existing(ordinal)),
895 Some((None, _)) => return None,
896 None => {
897 let d_new = geometry.distance_along(cursor);
898 let index = working
899 .iter()
900 .filter(|&&(_, pos)| geometry.distance_along(pos) < d_new)
901 .count();
902 let pos = px_point(grid_point(cursor));
903 working.insert(index, (None, pos));
904 slots.push(WaypointSlot::InsertAt(index));
905 }
906 }
907 }
908 Some(RouteEditSession {
909 slots,
910 anchors: label_anchors(&wire.labels, geometry),
911 })
912 }
913
914 pub fn preview_route_edit(
919 &mut self,
920 _phase: &crate::tools::tool::Supposing,
921 id: RouteId,
922 session: &RouteEditSession,
923 cursors: &[Pos2],
924 ) {
925 let Some(ends) = self.route_ends(id) else {
926 return;
927 };
928 let ids = self.scope_route_ids();
929 let Some(route) = self.route(id) else {
930 return;
931 };
932 let corners: Vec<GridPoint> = session
933 .corners(&route.waypoints, cursors)
934 .into_iter()
935 .map(|(pos, _)| pos)
936 .collect();
937 let routes = &mut self.presentation.routes;
938 let geometry = routes.entry(id).or_default();
939 materialize_corners_direct(&corners, geometry, ends);
940 recompute_route_crossings(&ids, routes);
941 self.presentation.routes_supposed();
942 }
943
944 pub fn commit_route_edit(&mut self, id: RouteId, session: &RouteEditSession, cursors: &[Pos2]) {
949 let Some(route) = self.route(id) else {
950 return;
951 };
952 let edited: Vec<Waypoint> = session
953 .corners(&route.waypoints, cursors)
954 .into_iter()
955 .map(|(pos, lock)| Waypoint {
956 pos,
957 locked: lock.is_locked(),
958 })
959 .collect();
960 let corners: Vec<GridPoint> = edited.iter().map(|wp| wp.pos).collect();
961 let anchors = session.anchors.clone();
962 let Some((waypoints, relayed)) = self.relay_route(id, &corners, &edited, &anchors) else {
963 return;
964 };
965 self.author("commit_route_edit", |indexed, sink| {
966 crate::edit::geometry::commit_route_edit(
967 indexed.doc,
968 crate::edit::geometry::RouteEdit {
969 route: id,
970 waypoints,
971 anchors: &relayed,
972 },
973 sink,
974 );
975 });
976 }
977
978 pub fn reroute(&mut self, id: RouteId) {
982 self.author("reroute", |indexed, sink| {
983 crate::edit::geometry::reroute(indexed, id, &[], sink);
984 });
985 self.rip_up(&[id]);
986 }
987
988 pub fn reroute_block(&mut self, block: blockworx_doc::id::BlockId) {
997 let on_block: HashSet<PinId> = self
998 .block_pins(crate::path::Scope::Block(block))
999 .into_iter()
1000 .map(|(id, _)| id)
1001 .collect();
1002 let ids: Vec<RouteId> = self
1003 .scope_route_ids()
1004 .into_iter()
1005 .filter(|&id| {
1006 self.route(id)
1007 .is_some_and(|r| on_block.contains(&r.from) || on_block.contains(&r.to))
1008 })
1009 .collect();
1010 for &id in &ids {
1011 self.author("reroute_block", |indexed, sink| {
1012 crate::edit::geometry::reroute(indexed, id, &[], sink);
1013 });
1014 }
1015 self.rip_up(&ids);
1016 }
1017
1018 fn rip_up(&mut self, ids: &[RouteId]) {
1023 for id in ids {
1024 self.presentation.routes.remove(id);
1025 }
1026 self.presentation.routes_supposed();
1027 }
1028
1029 pub fn scratch_closed_router(&self, extra_seeds: &[Point]) -> ClosedRouter {
1037 let ids = self.scope_route_ids();
1038 let mut seeds: Vec<Point> = Vec::new();
1039 for &id in &ids {
1040 let Some(route) = self.route(id) else {
1041 continue;
1042 };
1043 if let Some(s) = self.anchor_pos_overridden(route.from, GeometryOverrides::default()) {
1044 seeds.push(snap_to_grid(s).into());
1045 }
1046 if let Some(e) = self.anchor_pos_overridden(route.to, GeometryOverrides::default()) {
1047 seeds.push(snap_to_grid(e).into());
1048 }
1049 for wp in route.waypoints.clone() {
1050 seeds.push(wp.pos.into());
1051 }
1052 }
1053 seeds.extend_from_slice(extra_seeds);
1054 let mut router = self.build_closed_router(GeometryOverrides::default(), &seeds);
1055 for &id in &ids {
1056 if let Some(geometry) = self.presentation.routes.get(&id) {
1057 add_route_cost(
1058 &mut router,
1059 geometry.iter_edges().map(|(_, e)| e),
1060 WIRE_COST,
1061 );
1062 }
1063 }
1064 router
1065 }
1066
1067 pub fn debug_marks(&self) -> Vec<Mark> {
1068 self.scratch_closed_router(&[]).debug_marks()
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use blockworx_doc::{
1075 fixtures::{block_id, pin_id, route_id},
1076 geometry::{GridPoint, PinSlot, Waypoint},
1077 id::RouteId,
1078 values::PinSide,
1079 };
1080 use blockworx_geom::{Rect, pos2, vec2};
1081
1082 use crate::{
1083 edit::{
1084 geometry::{PinMove, RouteEnd, trimmed_approach},
1085 naming::InterfaceLock,
1086 },
1087 grid::{GRID_SIZE, grid_point, px_point},
1088 path::Scope,
1089 presentation::{RouteEdge, RouteGeometry},
1090 shape::{BaseShape, ShapeId, ShapeRef},
1091 widget::{
1092 auto_route::Wire,
1093 drawing::Drawing,
1094 test_fixtures::{
1095 self as fx, Scene, route_passes_through, two_blocks_with_a_routed_waypoint,
1096 },
1097 },
1098 };
1099
1100 #[test]
1107 fn a_two_corner_plan_replays_in_the_order_it_was_planned() {
1108 let mut scene = two_blocks_with_a_routed_waypoint();
1109 fx::materialize(&mut scene);
1110 let route = route_id(5);
1111 let drawing = scene.drawing();
1112 let geometry = drawing
1113 .route_geometry(route)
1114 .expect("the fixture's wire is materialized")
1115 .clone();
1116 let edges: Vec<_> = geometry.iter_edges().map(|(_, e)| e.clone()).collect();
1121 assert!(
1122 edges.len() >= 2,
1123 "precondition: the wire needs two edges to insert around",
1124 );
1125 let midpoint =
1126 |e: &RouteEdge| px_point(e.start) + (px_point(e.end) - px_point(e.start)) * 0.5;
1127 let (early, late) = (midpoint(&edges[0]), midpoint(&edges[edges.len() - 1]));
1128 let stored = drawing
1129 .auto_route(route)
1130 .expect("the wire is in scope")
1131 .route
1132 .waypoints
1133 .clone();
1134 assert_eq!(
1135 stored.len(),
1136 1,
1137 "precondition: one stored corner to insert around",
1138 );
1139
1140 let session = drawing
1141 .route_edit_session(route, &[early, late])
1142 .expect("two well-separated cursors plan");
1143 let corners = session.corners(&stored, &[early, late]);
1144
1145 assert_eq!(
1146 corners.len(),
1147 3,
1148 "one stored corner plus two insertions: {corners:?}",
1149 );
1150 let locked: Vec<usize> = corners
1151 .iter()
1152 .enumerate()
1153 .filter(|(_, (_, lock))| *lock == InterfaceLock::Locked)
1154 .map(|(i, _)| i)
1155 .collect();
1156 assert_eq!(
1157 locked.len(),
1158 2,
1159 "the two the user placed come out locked: {corners:?}",
1160 );
1161 let (first, second) = (corners[locked[0]].0, corners[locked[1]].0);
1164 assert!(
1165 geometry.distance_along(px_point(first)) < geometry.distance_along(px_point(second)),
1166 "the replay reordered the planned corners: {first:?} then {second:?}",
1167 );
1168 }
1169
1170 fn a_to_b_route(x_b: f32) -> Scene {
1173 Scene::new(vec![
1174 fx::block(1, 0.0),
1175 fx::block(2, x_b),
1176 fx::pin(3, 1, PinSide::East, 0),
1177 fx::pin(4, 2, PinSide::West, 0),
1178 fx::route(5, Scope::Root, 3, 4, &[]),
1179 ])
1180 }
1181
1182 fn route_edges(drawing: &Drawing<'_>, rid: RouteId) -> Vec<(GridPoint, GridPoint)> {
1183 drawing
1184 .route_geometry(rid)
1185 .unwrap()
1186 .iter_edges()
1187 .map(|(_, e)| (e.start, e.end))
1188 .collect()
1189 }
1190
1191 fn stored_corners(drawing: &Drawing<'_>, rid: RouteId) -> Vec<GridPoint> {
1192 drawing
1193 .auto_route(rid)
1194 .expect("the route is in this scope")
1195 .route
1196 .waypoints
1197 .iter()
1198 .map(|wp| wp.pos)
1199 .collect()
1200 }
1201
1202 #[test]
1203 fn a_drag_preview_moves_only_the_dragged_endpoint() {
1204 let mut scene = a_to_b_route(120.0);
1205 let rid = route_id(5);
1206 let mut drawing = scene.drawing();
1207 drawing.solve_routes(&[]);
1209 let start0 = drawing.route_geometry(rid).unwrap().start_pos();
1210 let end0 = drawing.route_geometry(rid).unwrap().end_pos();
1211
1212 let offset = vec2(2.0 * GRID_SIZE, GRID_SIZE);
1214 drawing.suppose_drag(
1215 &crate::tools::tool::Supposing::testing(),
1216 &[(ShapeId::Rect(block_id(1)), offset)],
1217 );
1218
1219 let g = drawing.route_geometry(rid).unwrap();
1220 assert_eq!(g.start_pos(), start0 + offset);
1221 assert_eq!(g.end_pos(), end0);
1222 }
1223
1224 #[test]
1228 fn place_route_label_stores_the_distance_it_is_given() {
1229 let mut scene = a_to_b_route(120.0);
1230 let rid = route_id(5);
1231
1232 let (on_wire, lid) = scene.commit(|drawing| {
1233 let geometry = drawing
1234 .route_geometry(rid)
1235 .expect("the route is materialized");
1236 let points = geometry.points();
1237 assert!(points.len() >= 2, "the route has at least one segment");
1238 let on_wire = points[0] + (points[1] - points[0]) * 0.5;
1239 let lid = drawing
1240 .add_route_label(rid, on_wire)
1241 .expect("the label lands on the materialized route");
1242 (on_wire, lid)
1243 });
1244 let _ = on_wire;
1245
1246 let stored = |scene: &mut Scene| -> blockworx_doc::geometry::FracVal {
1247 scene
1248 .drawing()
1249 .auto_route(rid)
1250 .expect("the route is in this scope")
1251 .labels
1252 .iter()
1253 .find(|(id, _)| *id == lid)
1254 .map(|&(_, dist)| dist)
1255 .expect("the label is on the wire")
1256 };
1257 let before = stored(&mut scene);
1258 let target = blockworx_doc::geometry::FracVal::from(f32::from(before) + GRID_SIZE);
1259 assert_ne!(before, target, "the test moves the label somewhere new");
1260
1261 scene.commit(|drawing| drawing.place_route_label(lid, target));
1262
1263 assert_eq!(stored(&mut scene), target);
1264 }
1265
1266 #[test]
1267 fn moving_a_distant_block_leaves_unrelated_routes_byte_identical() {
1268 let mut scene = a_to_b_route(120.0);
1270 let far = block_id(9);
1271 scene.apply(vec![fx::block(9, 600.0)]); let rid = route_id(5);
1273 let before = route_edges(&scene.drawing(), rid);
1274
1275 scene.commit(|drawing| {
1277 drawing.move_shapes(
1278 &[ShapeId::Rect(far)],
1279 vec2(2.0 * GRID_SIZE, 3.0 * GRID_SIZE),
1280 );
1281 });
1282
1283 assert_eq!(
1284 before,
1285 route_edges(&scene.drawing(), rid),
1286 "a distant block move must leave an unrelated route byte-identical"
1287 );
1288 }
1289
1290 #[test]
1291 fn editing_a_route_never_autoroutes_around_obstacles() {
1292 let mut scene = a_to_b_route(240.0);
1297 scene.apply(vec![fx::block_in(
1299 9,
1300 Scope::Root,
1301 Rect::from_min_max(pos2(120.0, 20.0), pos2(160.0, 60.0)),
1302 )]);
1303 let rid = route_id(5);
1304 let mut drawing = scene.drawing();
1305
1306 drawing
1307 .relay_route(rid, &[], &[], &[])
1308 .expect("the wire has resolvable endpoints");
1309
1310 assert_eq!(
1311 route_edges(&drawing, rid).len(),
1312 1,
1313 "the relayed wire stays a single straight leg"
1314 );
1315 assert!(
1316 route_passes_through(
1317 drawing.route_geometry(rid).unwrap(),
1318 GridPoint { x: 7, y: 2 }
1319 ),
1320 "the relay draws straight through the block — the editor never autoroutes"
1321 );
1322 }
1323
1324 #[test]
1325 fn drag_preview_reroutes_through_offset_waypoint() {
1326 let mut scene = two_blocks_with_a_routed_waypoint();
1330 let (a, b, rid) = (block_id(1), block_id(2), route_id(5));
1331 let mut drawing = scene.drawing();
1332 drawing.solve_routes(&[]);
1333 let stored_before = stored_corners(&drawing, rid);
1334 let (start0, end0) = {
1335 let g = drawing.route_geometry(rid).unwrap();
1336 (g.start_pos(), g.end_pos())
1337 };
1338
1339 let off = vec2(0.0, 4.0 * GRID_SIZE); drawing.suppose_drag(
1341 &crate::tools::tool::Supposing::testing(),
1342 &[(ShapeId::Rect(a), off), (ShapeId::Rect(b), off)],
1343 );
1344
1345 let g = drawing.route_geometry(rid).unwrap();
1347 assert_eq!(g.start_pos(), start0 + off);
1348 assert_eq!(g.end_pos(), end0 + off);
1349
1350 assert_eq!(
1353 stored_corners(&drawing, rid),
1354 stored_before,
1355 "stored waypoints stay put; the preview is uncommitted"
1356 );
1357 }
1358
1359 #[test]
1360 fn drag_preview_leaves_partially_selected_route() {
1361 let mut scene = two_blocks_with_a_routed_waypoint();
1365 let (a, rid) = (block_id(1), route_id(5));
1366 let mut drawing = scene.drawing();
1367 drawing.solve_routes(&[]);
1368 let stored_before = stored_corners(&drawing, rid);
1369 let end0 = drawing.route_geometry(rid).unwrap().end_pos();
1370
1371 let off = vec2(0.0, 4.0 * GRID_SIZE);
1372 drawing.suppose_drag(
1373 &crate::tools::tool::Supposing::testing(),
1374 &[(ShapeId::Rect(a), off)],
1375 );
1376
1377 assert_eq!(
1379 drawing.route_geometry(rid).unwrap().end_pos(),
1380 end0,
1381 "the un-dragged endpoint is fixed"
1382 );
1383
1384 let stored_after = stored_corners(&drawing, rid);
1388 assert!(
1389 stored_after.iter().all(|p| stored_before.contains(p)),
1390 "preview introduced new corners {stored_after:?} vs {stored_before:?}"
1391 );
1392 }
1393
1394 fn settled_bend() -> Scene {
1401 let tall = |n, x: f32| {
1402 fx::block_in(
1403 n,
1404 Scope::Root,
1405 Rect::from_min_max(pos2(x, 0.0), pos2(x + 40.0, 120.0)),
1406 )
1407 };
1408 let mut scene = Scene::new(vec![
1409 tall(1, 0.0),
1410 tall(2, 300.0),
1411 fx::pin(3, 1, PinSide::East, 0),
1412 fx::pin(4, 2, PinSide::West, 1),
1413 fx::route(5, Scope::Root, 3, 4, &[(10, 4)]),
1414 ]);
1415 fx::materialize(&mut scene);
1416 scene.commit(|drawing| drawing.reroute(route_id(5)));
1417 scene
1418 }
1419
1420 #[test]
1421 fn drag_preview_matches_commit() {
1422 let off = vec2(0.0, 4.0 * GRID_SIZE);
1425 let (a, b, rid) = (block_id(1), block_id(2), route_id(5));
1426
1427 let mut scene = settled_bend();
1429 let settled = scene.drawing().route_geometry(rid).unwrap().points();
1430 let previewed = {
1431 let mut preview = scene.drawing();
1432 preview.suppose_drag(
1433 &crate::tools::tool::Supposing::testing(),
1434 &[(ShapeId::Rect(a), off), (ShapeId::Rect(b), off)],
1435 );
1436 preview.route_geometry(rid).unwrap().points()
1437 };
1438 assert_ne!(previewed, settled, "the preview moved the wire");
1441
1442 let mut scene2 = settled_bend();
1444 scene2.commit(|commit| commit.move_shapes(&[ShapeId::Rect(a), ShapeId::Rect(b)], off));
1445 let committed = scene2.drawing().route_geometry(rid).unwrap().points();
1448
1449 assert_eq!(
1450 previewed, committed,
1451 "drag preview geometry must equal the committed geometry (no release snap)"
1452 );
1453 }
1454
1455 #[test]
1456 fn a_gesture_removes_a_backtracking_waypoint() {
1457 let far = (15, 1);
1463 let near = (5, 1);
1464 let mut scene = Scene::new(vec![
1465 fx::block(1, 0.0),
1466 fx::block(2, 300.0),
1467 fx::pin(3, 1, PinSide::East, 0),
1468 fx::pin(4, 2, PinSide::West, 0),
1469 fx::route(5, Scope::Root, 3, 4, &[far, near]),
1470 ]);
1471 let rid = route_id(5);
1472 scene.commit(|drawing| drawing.reroute(rid));
1476
1477 let drawing = scene.drawing();
1478 let geometry = drawing
1479 .route_geometry(rid)
1480 .expect("the route is materialized");
1481 let edges: Vec<_> = geometry.iter_edges().map(|(_, e)| e.clone()).collect();
1482 let doubles_back = edges.windows(2).any(|w| {
1483 let (ax, ay) = (w[0].end.x - w[0].start.x, w[0].end.y - w[0].start.y);
1484 let (bx, by) = (w[1].end.x - w[1].start.x, w[1].end.y - w[1].start.y);
1485 (ay == 0 && by == 0 && (ax > 0) != (bx > 0))
1486 || (ax == 0 && bx == 0 && (ay > 0) != (by > 0))
1487 });
1488 assert!(!doubles_back, "the wire doubles back after the gesture");
1489 assert!(
1490 !stored_corners(&drawing, rid).contains(&GridPoint { x: far.0, y: far.1 }),
1491 "the overshoot waypoint was dropped"
1492 );
1493 }
1494
1495 #[test]
1496 fn trim_approach_drops_up_to_two_unlocked_corners_from_the_endpoint() {
1497 let wp = |x, y, locked| Waypoint {
1498 pos: GridPoint { x, y },
1499 locked,
1500 };
1501 let corners = [wp(2, 0, false), wp(4, 0, false), wp(6, 0, false)];
1502 let positions =
1503 |kept: Vec<Waypoint>| -> Vec<GridPoint> { kept.into_iter().map(|w| w.pos).collect() };
1504
1505 assert_eq!(
1506 positions(trimmed_approach(&corners, RouteEnd::From)),
1507 vec![GridPoint { x: 6, y: 0 }],
1508 "the two leading corners nearest the start are dropped"
1509 );
1510 assert_eq!(
1511 positions(trimmed_approach(&corners, RouteEnd::To)),
1512 vec![GridPoint { x: 2, y: 0 }],
1513 "the two trailing corners nearest the finish are dropped"
1514 );
1515
1516 let locked_first = [wp(2, 0, true), wp(4, 0, false)];
1519 assert_eq!(
1520 positions(trimmed_approach(&locked_first, RouteEnd::From)),
1521 vec![GridPoint { x: 2, y: 0 }, GridPoint { x: 4, y: 0 }],
1522 "a leading locked corner blocks the trim entirely"
1523 );
1524 }
1525
1526 #[test]
1527 fn flipping_a_block_reroutes_and_trims_like_a_drag() {
1528 let mut scene = two_blocks_with_a_routed_waypoint();
1533 let (a, rid) = (block_id(1), route_id(5));
1534
1535 scene.commit(|d| {
1536 d.flip_shape_pins(ShapeId::Rect(a));
1537 d.trim_partial_route_approaches(&[ShapeId::Rect(a)]);
1538 });
1539
1540 let d = scene.drawing();
1541 let wire = d.auto_route(rid).expect("the route survives the flip");
1542 let geometry = d.route_geometry(rid).expect("the route is materialized");
1543 assert!(
1544 geometry.iter_edges().next().is_some(),
1545 "the flipped route has geometry"
1546 );
1547 assert!(
1548 waypoints_are_the_corners(&wire, geometry),
1549 "flip re-route left the waypoints in sync with the corners"
1550 );
1551 }
1552
1553 #[test]
1554 fn flipping_a_port_reroutes_without_moving_the_parent_pin() {
1555 use crate::shape::pin::{orientation, slot};
1556 let (port_id, rid) = (pin_id(3), route_id(5));
1558 let mut scene = Scene::new(vec![
1559 fx::pin_at(
1560 3,
1561 Scope::Root,
1562 "io",
1563 fx::slot(PinSide::East, 1),
1564 Rect::from_min_max(pos2(0.0, 45.0), pos2(40.0, 61.0)),
1565 ),
1566 fx::block(2, 120.0),
1567 fx::pin(4, 2, PinSide::West, 0),
1568 fx::route(5, Scope::Root, 3, 4, &[]),
1569 ]);
1570 let pin = scene
1571 .drawing()
1572 .held_pin(port_id)
1573 .expect("the port's pin")
1574 .clone();
1575 let (before_side, before_orientation) = (slot(&pin).side, orientation(&pin));
1576 assert_eq!(before_orientation, before_side.flip());
1578
1579 scene.commit(|drawing| drawing.flip_shape_pins(ShapeId::Port(port_id)));
1580
1581 let drawing = scene.drawing();
1582 let pin = drawing.held_pin(port_id).expect("the port's pin").clone();
1583 assert_eq!(
1586 slot(&pin).side,
1587 before_side,
1588 "flipping the port moved the parent pin"
1589 );
1590 assert_eq!(orientation(&pin), before_orientation.flip());
1591
1592 let port = drawing.shape(ShapeId::Port(port_id)).unwrap();
1594 let anchor = port
1595 .anchor_point_with_rect(port.gui_rect(), port_id)
1596 .unwrap();
1597 assert_eq!(drawing.route_geometry(rid).unwrap().start_pos(), anchor);
1598 }
1599
1600 fn waypoints_are_the_corners(wire: &Wire<'_>, geometry: &RouteGeometry) -> bool {
1605 let edges: Vec<_> = geometry.iter_edges().map(|(_, e)| e).collect();
1606 let corners: std::collections::HashSet<GridPoint> =
1607 edges.windows(2).map(|w| w[0].end).collect();
1608 let waypoints: std::collections::HashSet<GridPoint> =
1609 wire.route.waypoints.iter().map(|wp| wp.pos).collect();
1610 corners == waypoints
1611 }
1612
1613 #[test]
1614 fn dragging_a_block_keeps_route_waypoints_matching_the_corners() {
1615 let (b, rid) = (block_id(2), route_id(5));
1618 let mut scene = Scene::new(vec![
1619 fx::block(1, 0.0),
1620 fx::block_in(
1621 2,
1622 Scope::Root,
1623 Rect::from_min_max(pos2(300.0, 150.0), pos2(340.0, 190.0)),
1624 ),
1625 fx::pin(3, 1, PinSide::East, 0),
1626 fx::pin(4, 2, PinSide::West, 0),
1627 fx::route(5, Scope::Root, 3, 4, &[]),
1628 ]);
1629 scene.drawing().solve_routes(&[]);
1630
1631 let delta = vec2(0.0, 4.0 * GRID_SIZE);
1634 for frame in 1..=4 {
1635 scene.drawing().suppose_drag(
1636 &crate::tools::tool::Supposing::testing(),
1637 &[(ShapeId::Rect(b), vec2(0.0, frame as f32 * GRID_SIZE))],
1638 );
1639 }
1640 scene.commit(|d| {
1641 d.move_shape(ShapeId::Rect(b), delta);
1642 d.trim_partial_route_approaches(&[ShapeId::Rect(b)]);
1643 });
1644
1645 let d = scene.drawing();
1646 let wire = d.auto_route(rid).unwrap();
1647 let geometry = d.route_geometry(rid).expect("the route is materialized");
1648 assert!(
1649 waypoints_are_the_corners(&wire, geometry),
1650 "after a drag the route's waypoints must match its corners"
1651 );
1652 }
1653
1654 #[test]
1655 fn flipping_a_block_preserves_its_childrens_internal_routes() {
1656 let (c, pc, rid) = (block_id(1), pin_id(3), route_id(5));
1658 let mut scene = Scene::new(vec![
1659 fx::block(1, 0.0),
1660 fx::pin_at(
1661 3,
1662 Scope::Block(c),
1663 "io",
1664 fx::slot(PinSide::West, 1),
1665 Rect::from_min_max(pos2(0.0, 45.0), pos2(40.0, 61.0)),
1666 ),
1667 fx::block_in(
1668 2,
1669 Scope::Block(c),
1670 Rect::from_min_max(pos2(60.0, 0.0), pos2(100.0, 40.0)),
1671 ),
1672 fx::pin(4, 2, PinSide::East, 0),
1673 fx::route(5, Scope::Block(c), 3, 4, &[]),
1674 ]);
1675
1676 let anchor_before = {
1677 scene.path.push(c);
1678 let mut drawing = scene.drawing();
1679 drawing.solve_routes(&[]);
1680 let port = drawing.shape(ShapeId::Port(pc)).unwrap();
1681 port.anchor_point_with_rect(port.gui_rect(), pc).unwrap()
1682 };
1683
1684 scene.path.pop();
1687 scene.commit(|drawing| drawing.flip_shape_pins(ShapeId::Rect(c)));
1688
1689 scene.path.push(c);
1692 let mut drawing = scene.drawing();
1693 drawing.solve_routes(&[]);
1694 let port = drawing.shape(ShapeId::Port(pc)).unwrap();
1695 let anchor_after = port.anchor_point_with_rect(port.gui_rect(), pc).unwrap();
1696 assert_eq!(
1697 anchor_after, anchor_before,
1698 "flipping the block moved its interior port"
1699 );
1700 assert_eq!(
1701 drawing.route_geometry(rid).unwrap().start_pos(),
1702 anchor_after,
1703 "internal route lost its port endpoint"
1704 );
1705 }
1706
1707 #[test]
1708 fn drag_and_resize_preview_frames_never_touch_the_document() {
1709 let mut scene = two_blocks_with_a_routed_waypoint();
1712 let (a, rid) = (block_id(1), route_id(5));
1713 scene.drawing().solve_routes(&[]);
1714 assert!(
1715 !stored_corners(&scene.drawing(), rid).is_empty(),
1716 "precondition: the committed route must carry corners to trim"
1717 );
1718 let before = scene.doc.clone();
1719 let stamp = scene.doc.stamp();
1720
1721 {
1722 let mut drawing = scene.drawing();
1723 let start0 = drawing.route_geometry(rid).unwrap().start_pos();
1724 for frame in 1..=4 {
1725 let offset = vec2(GRID_SIZE * frame as f32, GRID_SIZE);
1726 drawing.suppose_drag(
1727 &crate::tools::tool::Supposing::testing(),
1728 &[(ShapeId::Rect(a), offset)],
1729 );
1730 }
1731 assert_ne!(
1732 drawing.route_geometry(rid).unwrap().start_pos(),
1733 start0,
1734 "precondition: the preview really re-solved the dragged route"
1735 );
1736 let grown = drawing.shape(ShapeId::Rect(a)).unwrap().gui_rect();
1737 drawing.suppose_resize(
1738 &crate::tools::tool::Supposing::testing(),
1739 &[(
1740 ShapeId::Rect(a),
1741 Rect::from_min_size(grown.min, grown.size() + vec2(GRID_SIZE, GRID_SIZE)),
1742 )],
1743 );
1744 }
1745
1746 assert_eq!(
1747 scene.doc.stamp(),
1748 stamp,
1749 "a preview frame must not produce a new document value"
1750 );
1751 assert_eq!(scene.doc.clone(), before);
1752 }
1753
1754 #[test]
1755 fn a_pin_drag_preview_tracks_the_hypothetical_slot_without_moving_the_pin() {
1756 let mut scene = a_to_b_route(120.0);
1757 let (a, pa, rid) = (block_id(1), pin_id(3), route_id(5));
1758 scene.drawing().solve_routes(&[]);
1759 let before = scene.doc.clone();
1760 let stamp = scene.doc.stamp();
1761
1762 let (start0, start, supposed) = {
1763 let mut drawing = scene.drawing();
1764 let start0 = drawing.route_geometry(rid).unwrap().start_pos();
1765 drawing.suppose_pin_drag(
1766 &crate::tools::tool::Supposing::testing(),
1767 pa,
1768 PinSide::East,
1769 3,
1770 );
1771 let start = drawing.route_geometry(rid).unwrap().start_pos();
1772 let supposed = match drawing.shape(ShapeId::Rect(a)).unwrap() {
1773 ShapeRef::Block(block) => block.pin_anchor_at(block.gui_rect(), PinSide::East, 3),
1774 _ => unreachable!("a is a block"),
1775 };
1776 (start0, start, supposed)
1777 };
1778
1779 assert_ne!(start, start0, "precondition: slot 3 moves the anchor");
1780 assert_eq!(
1781 grid_point(start),
1782 grid_point(supposed),
1783 "the previewed endpoint sits at the hypothetical slot"
1784 );
1785 assert_eq!(scene.doc.stamp(), stamp);
1786 assert_eq!(scene.doc.clone(), before);
1787
1788 {
1790 let mut drawing = scene.drawing();
1791 drawing.suppose_pin_drags(
1792 &crate::tools::tool::Supposing::testing(),
1793 &[PinMove {
1794 pin: pa,
1795 to: PinSlot {
1796 side: PinSide::East,
1797 offset: 2,
1798 },
1799 }],
1800 );
1801 }
1802 assert_eq!(scene.doc.stamp(), stamp);
1803 assert_eq!(scene.doc.clone(), before);
1804 }
1805}