Skip to main content

blockworx_editor/widget/
materialize.rs

1//! Deriving a route's edge geometry from its ordered corner list.
2//!
3//! Under the explicit-path model a route is fully described by its ordered
4//! corner sequence `[start_pos, waypoint…, end_pos]` — waypoint insertion order
5//! *is* the polyline order. These helpers extract the canonical corner set from
6//! a raw polyline and read a route's ordered corner points; the straight-or-route
7//! materialization (which turns those corners back into edges, invoking the
8//! router only for blocked/non-colinear legs) builds on them.
9
10use std::collections::{BTreeMap, HashSet};
11
12use blockworx_doc::geometry::{GridPoint, Waypoint};
13use blockworx_router::block::{Block as ObstacleRect, ROUTE_GUTTER};
14use blockworx_router::{ClosedRouter, Direction, WIRE_COST, direction_between};
15
16use crate::presentation::RouteEdge;
17use crate::presentation::RouteGeometry;
18use crate::presentation::store::{EdgeId, IdMap, IdMapExt};
19use crate::widget::waypoint_router::add_route_cost;
20
21/// A route's authored corners in the presentation layer's grid vocabulary —
22/// the one conversion the materialization pass makes, so the leg walk below
23/// speaks a single coordinate type.
24fn corner_positions(waypoints: &[Waypoint]) -> Vec<GridPoint> {
25    waypoints.iter().map(|wp| wp.pos).collect()
26}
27
28/// The obstacle rectangles of a block, for the cheap "does a straight wire cross
29/// a block?" test that decides whether a leg can stay straight. Mirrors exactly
30/// the rects `build_closed_router_for_block` feeds to the router as blocks, so a
31/// straight leg judged clear here is the same one the router would leave alone —
32/// but without paying to build the routing graph (the point of the fast load path).
33///
34/// Rects are indexed by the rows and columns they span, so `blocked`/`accessible`
35/// only test the rects that could possibly hit the query line — not all of them.
36/// A linear scan here is quadratic across a whole document's routes (each leg
37/// scans every block), which showed up as ~20s of load overhead at scale.
38pub struct Obstacles {
39    rects: Vec<ObstacleRect>,
40    by_row: BTreeMap<i32, Vec<usize>>,
41    by_col: BTreeMap<i32, Vec<usize>>,
42}
43
44impl Obstacles {
45    pub fn new(rects: Vec<ObstacleRect>) -> Self {
46        let mut by_row: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
47        let mut by_col: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
48        for (i, r) in rects.iter().enumerate() {
49            for y in r.top_left.y.raw()..=r.bottom_right.y.raw() {
50                by_row.entry(y).or_default().push(i);
51            }
52            for x in r.top_left.x.raw()..=r.bottom_right.x.raw() {
53                by_col.entry(x).or_default().push(i);
54            }
55        }
56        Self {
57            rects,
58            by_row,
59            by_col,
60        }
61    }
62
63    /// Whether the axis-aligned wire `a → b` crosses any obstacle. Same result as
64    /// [`ClosedRouter::is_wire_blocked`], but only the rects spanning the wire's
65    /// row (horizontal wire) or column (vertical wire) are tested.
66    pub fn blocked(&self, a: GridPoint, b: GridPoint) -> bool {
67        let candidates = if a.y == b.y {
68            self.by_row.get(&a.y)
69        } else if a.x == b.x {
70            self.by_col.get(&a.x)
71        } else {
72            None
73        };
74        candidates
75            .into_iter()
76            .flatten()
77            .any(|&i| self.rects[i].intersects_edge(a, b))
78    }
79
80    /// Whether the axis-aligned wire `a → b` hugs any obstacle (runs alongside an
81    /// edge within the routing gutter).
82    ///
83    /// A hugging wire runs *beside* an edge rather than over it, so the index is
84    /// queried a gutter wide either side of the wire: a rect with an edge within
85    /// `ROUTE_GUTTER` of row `y` spans one of the rows `y-gutter ..= y+gutter`,
86    /// and so is in one of their buckets. A rect spanning several of them is
87    /// tested more than once, which costs less than the dedup would.
88    pub fn hugs(&self, a: GridPoint, b: GridPoint) -> bool {
89        let (index, at) = if a.y == b.y {
90            (&self.by_row, a.y)
91        } else if a.x == b.x {
92            (&self.by_col, a.x)
93        } else {
94            return false;
95        };
96        index
97            .range(at - ROUTE_GUTTER..=at + ROUTE_GUTTER)
98            .flat_map(|(_, bucket)| bucket)
99            .any(|&i| self.rects[i].hugs_wire(a, b, ROUTE_GUTTER))
100    }
101
102    /// Whether `p` lies outside every obstacle (a waypoint inside one is dropped).
103    pub fn accessible(&self, p: GridPoint) -> bool {
104        !self
105            .by_row
106            .get(&p.y)
107            .into_iter()
108            .flatten()
109            .any(|&i| self.rects[i].contains(p.into()))
110    }
111}
112
113fn axis_aligned(a: GridPoint, b: GridPoint) -> bool {
114    a.x == b.x || a.y == b.y
115}
116
117/// A route's resolved (snapped) endpoints for one solve or
118/// materialization pass.
119#[derive(Clone, Copy)]
120pub struct Endpoints {
121    pub start: GridPoint,
122    pub end: GridPoint,
123}
124
125/// Rebuild the edge geometry of a wire whose authored corners are `stored`,
126/// keeping every straight (axis-aligned, unblocked) leg as a single edge and
127/// routing ONLY the legs that are non-colinear or blocked. `start`/`end` are the
128/// resolved grid endpoints. Straight legs never touch the router, so an
129/// unaffected route (all legs straight) is reconstructed byte-identically with
130/// zero pathfinding.
131///
132/// When a leg needs routing, `router` must be `Some` (already built and occupied
133/// by the other routes); the leg's new bends are spliced into the corner list so
134/// the "every corner is a waypoint" invariant is restored. If a leg needs routing
135/// and `router` is `None`, no geometry is written and `None` is returned so the
136/// caller can build the router and retry. Interior waypoints that fall inside an
137/// obstacle are dropped (only on the routing path, where `router` is `Some`).
138///
139/// Returns the corner list the materialization promotes — the polyline the
140/// gesture's commit stores, which the caller pushes as an op. Nothing here
141/// writes the document.
142pub fn materialize_route(
143    stored: &[Waypoint],
144    geometry: &mut RouteGeometry,
145    ends: Endpoints,
146    obstacles: &Obstacles,
147    mut router: Option<&mut ClosedRouter>,
148) -> Option<Vec<Waypoint>> {
149    // Ordered corners: start, each waypoint (drop inaccessible ones when routing),
150    // then end. Waypoint order is path order.
151    let mut corners: Vec<GridPoint> = Vec::with_capacity(stored.len() + 2);
152    corners.push(ends.start);
153    for pos in corner_positions(stored) {
154        if router.is_some() && !obstacles.accessible(pos) {
155            continue;
156        }
157        corners.push(pos);
158    }
159    corners.push(ends.end);
160
161    // Walk the legs, emitting the resulting polyline. Straight legs contribute a
162    // single hop; routed legs contribute their solved sub-path.
163    let mut polyline: Vec<GridPoint> = vec![corners[0]];
164    let mut incoming: Option<Direction> = None;
165    for pair in corners.windows(2) {
166        let (a, b) = (pair[0], pair[1]);
167        if a == b {
168            continue;
169        }
170        if axis_aligned(a, b) && !obstacles.blocked(a, b) && !obstacles.hugs(a, b) {
171            polyline.push(b);
172            incoming = direction_between(a.into(), b.into());
173            continue;
174        }
175        let r = router.as_mut()?;
176        let (subpath, outgoing) = r.route_leg(a.into(), b.into(), incoming);
177        if subpath.len() >= 2 {
178            polyline.extend(subpath[1..].iter().map(|&p| GridPoint::from(p)));
179        } else {
180            polyline.push(b);
181        }
182        incoming = outgoing;
183    }
184
185    let locked = locked_corners(stored);
186    let path = corners_of(&polyline);
187    let promoted = write_geometry(geometry, ends, &path, &locked);
188
189    if let Some(r) = router.as_mut() {
190        add_route_cost(r, geometry.iter_edges().map(|(_, e)| e), WIRE_COST);
191    }
192    Some(promoted)
193}
194
195/// Rebuild a wire's edges from its corner list (start + corners + end) as
196/// axis-aligned segments WITHOUT the router: a straight leg is one edge; a
197/// diagonal leg (its two endpoints share no row or column) becomes an L via a
198/// single bend. It never consults obstacles and never promotes bends to
199/// waypoints, so dragging a segment or corner repositions the wire directly and
200/// never triggers an autoroute — the segment goes exactly where the user puts
201/// it. The corner list is a read, so per-frame recomputation stays
202/// accumulation-free and the route editor's drag keeps owning it.
203///
204/// A *connector* leg — the first leg (from the start anchor to its corner) and
205/// the last (from the last corner to the end anchor) — is bent so the segment
206/// touching the pin runs along the pin's horizontal E/W stub: the first leg goes
207/// horizontal-first (leaving the pin), the last goes vertical-first (so its final
208/// segment enters the pin horizontally). This keeps the ends one cell clear of the
209/// block instead of hugging its gutter. Interior legs keep an incoming-axis bend so
210/// they can't double back.
211pub fn materialize_corners_direct(
212    waypoints: &[GridPoint],
213    geometry: &mut RouteGeometry,
214    ends: Endpoints,
215) {
216    let mut corners: Vec<GridPoint> = Vec::with_capacity(waypoints.len() + 2);
217    corners.push(ends.start);
218    corners.extend_from_slice(waypoints);
219    corners.push(ends.end);
220
221    let last_leg = corners.len().saturating_sub(2);
222    let has_waypoints = corners.len() > 2;
223    let mut polyline: Vec<GridPoint> = vec![corners[0]];
224    let mut incoming: Option<Direction> = None;
225    for (i, pair) in corners.windows(2).enumerate() {
226        let (a, b) = (pair[0], pair[1]);
227        if a == b {
228            continue;
229        }
230        if axis_aligned(a, b) {
231            polyline.push(b);
232        } else {
233            let horizontal_first = if has_waypoints && i == 0 {
234                true // first connector: leave the start pin along its horizontal stub
235            } else if has_waypoints && i == last_leg {
236                false // last connector: enter the end pin along its horizontal stub
237            } else {
238                !matches!(incoming, Some(Direction::North | Direction::South))
239            };
240            let bend = if horizontal_first {
241                GridPoint { x: b.x, y: a.y }
242            } else {
243                GridPoint { x: a.x, y: b.y }
244            };
245            polyline.push(bend);
246            polyline.push(b);
247        }
248        incoming = direction_between(polyline[polyline.len() - 2].into(), b.into());
249    }
250
251    let path = corners_of(&polyline);
252    let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
253    for w in path.windows(2) {
254        edges.insert_value(RouteEdge {
255            start: w[0],
256            end: w[1],
257        });
258    }
259    geometry.edges = edges;
260    geometry.start_pos = ends.start;
261    geometry.end_pos = ends.end;
262}
263
264/// Overwrite the solved geometry from the canonical corner polyline `path`
265/// (`[start, corners…, end]`) and return the waypoint list that polyline
266/// promotes: interior corners in path order, `locked` iff they coincide with
267/// a previously locked corner. The list is the caller's to push as an op —
268/// the solver writes geometry, never the document.
269fn write_geometry(
270    geometry: &mut RouteGeometry,
271    ends: Endpoints,
272    path: &[GridPoint],
273    locked: &HashSet<GridPoint>,
274) -> Vec<Waypoint> {
275    geometry.edges = edges_of(path);
276    geometry.start_pos = ends.start;
277    geometry.end_pos = ends.end;
278    interior_waypoints(path, locked)
279}
280
281/// The polyline's segments as presentation edges.
282fn edges_of(path: &[GridPoint]) -> IdMap<EdgeId, RouteEdge> {
283    let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
284    for w in path.windows(2) {
285        edges.insert_value(RouteEdge {
286            start: w[0],
287            end: w[1],
288        });
289    }
290    edges
291}
292
293/// The corners a canonical polyline `[start, corners…, end]` implies as
294/// waypoints, in path order, `locked` iff one already sat there.
295fn interior_waypoints(path: &[GridPoint], locked: &HashSet<GridPoint>) -> Vec<Waypoint> {
296    if path.len() <= 2 {
297        return Vec::new();
298    }
299    path[1..path.len() - 1]
300        .iter()
301        .map(|&pos| Waypoint {
302            pos,
303            locked: locked.contains(&pos),
304        })
305        .collect()
306}
307
308/// Where the user pinned a corner, so a rebuild can keep it pinned.
309fn locked_corners(waypoints: &[Waypoint]) -> HashSet<GridPoint> {
310    waypoints
311        .iter()
312        .filter(|wp| wp.locked)
313        .map(|wp| wp.pos)
314        .collect()
315}
316
317/// Reduce a polyline to its corners: drop adjacent duplicate points and any
318/// point collinear with its neighbours (a redundant interior point on a straight
319/// run). Keeps the first and last points and only the true direction-change
320/// vertices — the canonical corner set the model stores as waypoints. Idempotent.
321pub fn corners_of(points: &[GridPoint]) -> Vec<GridPoint> {
322    let mut out: Vec<GridPoint> = Vec::with_capacity(points.len());
323    for &p in points {
324        if out.last() == Some(&p) {
325            continue;
326        }
327        if let [.., a, b] = out[..]
328            && collinear(a, b, p)
329        {
330            out.pop();
331        }
332        out.push(p);
333    }
334    out
335}
336
337/// Every corner of the wire's polyline as a waypoint, in path order — the
338/// invariant the explicit-path model relies on. Derives the list from the
339/// route's current edge geometry: each interior bend becomes a waypoint,
340/// preserving the `locked` flag of any existing waypoint already at that position
341/// (so user-pinned corners stay pinned) and marking the rest structural
342/// (`locked: false`). The endpoints are never waypoints. The returned list is
343/// what the gesture's commit stores; the geometry is rebuilt in place.
344pub fn promote_corners_to_waypoints(
345    stored: &[Waypoint],
346    geometry: &mut RouteGeometry,
347) -> Vec<Waypoint> {
348    let corners = geometry_corners(geometry);
349    let locked = locked_corners(stored);
350
351    // Rebuild the edges from the canonical corners as well, not just the
352    // waypoints. `corners_of` collapses a 180° reversal (an overshoot the wire
353    // doubles straight back on), which strict waypoint routing and a literal
354    // route edit can leave in `edges`. Rebuilding only the waypoints would let
355    // that spike linger in the geometry — showing a malformed wire that "fixes
356    // itself" only after a reload rebuilds edges from the clean waypoints — so
357    // keep the two in sync here. Real 90° bends are direction changes, not
358    // collinear, so obstacle-avoidance geometry is preserved untouched.
359    geometry.edges = edges_of(&corners);
360    interior_waypoints(&corners, &locked)
361}
362
363/// The canonical corner polyline of solved geometry, endpoints included.
364pub fn geometry_corners(geometry: &RouteGeometry) -> Vec<GridPoint> {
365    let mut polyline = vec![geometry.start_pos];
366    polyline.extend(geometry.iter_edges().map(|(_, e)| e.end));
367    polyline.push(geometry.end_pos);
368    corners_of(&polyline)
369}
370
371/// Whether `b` lies on the straight line through `a` and `c` (zero cross
372/// product). For the axis-aligned corners we deal with, this means `a`, `b`, `c`
373/// share a row or column.
374fn collinear(a: GridPoint, b: GridPoint, c: GridPoint) -> bool {
375    (b.x - a.x) as i64 * (c.y - a.y) as i64 == (b.y - a.y) as i64 * (c.x - a.x) as i64
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    use crate::edit::geometry::{backtracking_ordinals, pruned_waypoints};
383
384    fn g(x: i32, y: i32) -> GridPoint {
385        GridPoint { x, y }
386    }
387
388    fn wp(x: i32, y: i32) -> Waypoint {
389        Waypoint {
390            pos: GridPoint { x, y },
391            locked: false,
392        }
393    }
394
395    /// The authored corners of a wire as a materialization pass reads them —
396    /// the post-load state: corner waypoints present, edges empty.
397    fn loaded_route(waypoints: &[(i32, i32)]) -> Vec<Waypoint> {
398        waypoints.iter().map(|&(x, y)| wp(x, y)).collect()
399    }
400
401    fn edges_double_back(geometry: &RouteGeometry) -> bool {
402        let edges: Vec<_> = geometry
403            .iter_edges()
404            .map(|(_, e)| (e.start, e.end))
405            .collect();
406        edges.windows(2).any(|w| {
407            let (ax, ay) = (w[0].1.x - w[0].0.x, w[0].1.y - w[0].0.y);
408            let (bx, by) = (w[1].1.x - w[1].0.x, w[1].1.y - w[1].0.y);
409            (ay == 0 && by == 0 && (ax > 0) != (bx > 0))
410                || (ax == 0 && bx == 0 && (ay > 0) != (by > 0))
411        })
412    }
413
414    #[test]
415    fn promote_erases_a_backtracking_spike_from_the_edges() {
416        use crate::edit::create::PathOrdinal;
417        use crate::widget::segmentkind::SegmentKind;
418        use crate::widget::waypoint_router::TaggedPoint;
419        use blockworx_router::point::Point;
420
421        let tp = |segment, x, y| TaggedPoint {
422            segment,
423            pos: Point::from(g(x, y)),
424        };
425        // Overshoot: run east to the waypoint at x=10, then straight back west to
426        // x=5 — a 180° reversal split across the two waypoint legs, so the solve
427        // keeps both edges (they never merge across a leg boundary).
428        let w = PathOrdinal::new(0);
429        let path = [
430            tp(SegmentKind::StartToWaypoint(w), 0, 0),
431            tp(SegmentKind::StartToWaypoint(w), 10, 0),
432            tp(SegmentKind::WaypointToEnd(w), 10, 0),
433            tp(SegmentKind::WaypointToEnd(w), 5, 0),
434        ];
435        let stored = loaded_route(&[(10, 0)]);
436        let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
437        assert!(
438            edges_double_back(&geometry),
439            "setup: the solve kept the reversal spike in the edges"
440        );
441
442        let promoted = promote_corners_to_waypoints(&stored, &mut geometry);
443        assert!(
444            !edges_double_back(&geometry),
445            "promote collapsed the spike in the edges"
446        );
447        assert!(promoted.is_empty(), "and dropped the overshoot waypoint");
448    }
449
450    #[test]
451    fn corners_of_drops_collinear_interior_points() {
452        // A run east then a run south: only the bend at (2,0) survives.
453        let pts = [g(0, 0), g(1, 0), g(2, 0), g(2, 1), g(2, 2)];
454        assert_eq!(corners_of(&pts), vec![g(0, 0), g(2, 0), g(2, 2)]);
455    }
456
457    #[test]
458    fn corners_of_drops_adjacent_duplicates() {
459        let pts = [g(0, 0), g(0, 0), g(3, 0), g(3, 0)];
460        assert_eq!(corners_of(&pts), vec![g(0, 0), g(3, 0)]);
461    }
462
463    #[test]
464    fn corners_of_is_idempotent() {
465        let pts = [g(0, 0), g(1, 0), g(2, 0), g(2, 3), g(5, 3), g(5, 3)];
466        let once = corners_of(&pts);
467        assert_eq!(corners_of(&once), once);
468    }
469
470    #[test]
471    fn promote_makes_every_bend_a_waypoint() {
472        use crate::edit::create::PathOrdinal;
473        use crate::widget::segmentkind::SegmentKind;
474        use crate::widget::waypoint_router::TaggedPoint;
475        use blockworx_router::point::Point;
476
477        let tp = |x: i32, y: i32| TaggedPoint {
478            segment: SegmentKind::StartToEnd,
479            pos: Point::from(GridPoint { x, y }),
480        };
481        // Z-shape: east, south, east — two bends at (3,0) and (3,3).
482        let path = [tp(0, 0), tp(3, 0), tp(3, 3), tp(6, 3)];
483        let stored = loaded_route(&[]);
484        let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
485        assert!(stored.is_empty());
486        assert_ne!(
487            PathOrdinal::new(0),
488            PathOrdinal::new(1),
489            "ordinals name distinct corners"
490        );
491
492        let promoted = promote_corners_to_waypoints(&stored, &mut geometry);
493
494        assert_eq!(corner_positions(&promoted), vec![g(3, 0), g(3, 3)]);
495        assert!(promoted.iter().all(|wp| !wp.locked));
496    }
497
498    #[test]
499    fn straight_legs_materialize_without_a_router() {
500        let stored = loaded_route(&[(3, 0)]);
501        let mut geometry = RouteGeometry::default();
502        // (0,0) →H (3,0) →V (3,4): both legs axis-aligned and clear.
503        let done = materialize_route(
504            &stored,
505            &mut geometry,
506            Endpoints {
507                start: g(0, 0),
508                end: g(3, 4),
509            },
510            &Obstacles::new(vec![]),
511            None,
512        );
513        assert!(done.is_some(), "a straight route needs no router");
514        let edges: Vec<(GridPoint, GridPoint)> = geometry
515            .iter_edges()
516            .map(|(_, e)| (e.start, e.end))
517            .collect();
518        assert_eq!(edges, vec![(g(0, 0), g(3, 0)), (g(3, 0), g(3, 4))]);
519    }
520
521    #[test]
522    fn a_blocked_leg_defers_when_no_router() {
523        let stored = loaded_route(&[]);
524        let mut geometry = RouteGeometry::default();
525        // A block straddling the straight run (0,0)→(10,0).
526        let obstacles = Obstacles::new(vec![ObstacleRect {
527            top_left: g(4, -2).into(),
528            bottom_right: g(6, 2).into(),
529        }]);
530        let done = materialize_route(
531            &stored,
532            &mut geometry,
533            Endpoints {
534                start: g(0, 0),
535                end: g(10, 0),
536            },
537            &obstacles,
538            None,
539        );
540        assert!(
541            done.is_none(),
542            "a blocked straight leg must defer to the router"
543        );
544    }
545
546    #[test]
547    fn direct_relay_draws_straight_legs_and_leaves_its_corners() {
548        let corners = [g(0, 5)];
549        let mut geometry = RouteGeometry::default();
550        materialize_corners_direct(
551            &corners,
552            &mut geometry,
553            Endpoints {
554                start: g(0, 0),
555                end: g(5, 5),
556            },
557        );
558        let edges: Vec<(GridPoint, GridPoint)> = geometry
559            .iter_edges()
560            .map(|(_, e)| (e.start, e.end))
561            .collect();
562        assert_eq!(edges, vec![(g(0, 0), g(0, 5)), (g(0, 5), g(5, 5))]);
563        // The direct relay rebuilds edges only; the corner list it was handed
564        // is a read, so the drag still owns it.
565        assert_eq!(corners, [g(0, 5)]);
566    }
567
568    #[test]
569    fn direct_relay_l_bends_a_diagonal_leg_without_a_router() {
570        let mut geometry = RouteGeometry::default();
571        materialize_corners_direct(
572            &[g(3, 4)],
573            &mut geometry,
574            Endpoints {
575                start: g(0, 0),
576                end: g(3, 8),
577            },
578        );
579        assert!(
580            geometry
581                .iter_edges()
582                .all(|(_, e)| e.start.x == e.end.x || e.start.y == e.end.y),
583            "every relayed edge is axis-aligned — an L, never a diagonal"
584        );
585        assert!(
586            geometry.iter_edges().count() >= 2,
587            "the diagonal leg produced a bend"
588        );
589    }
590
591    #[test]
592    fn a_non_colinear_leg_defers_when_no_router() {
593        // Waypoint diagonal from the start: the first leg is not axis-aligned.
594        let stored = loaded_route(&[(3, 4)]);
595        let mut geometry = RouteGeometry::default();
596        let done = materialize_route(
597            &stored,
598            &mut geometry,
599            Endpoints {
600                start: g(0, 0),
601                end: g(3, 8),
602            },
603            &Obstacles::new(vec![]),
604            None,
605        );
606        assert!(
607            done.is_none(),
608            "a non-colinear leg must defer to the router"
609        );
610    }
611
612    /// The backtracking prune the commit pass applies and the preview
613    /// supposes: one policy, [`crate::edit::geometry::backtracking_ordinals`],
614    /// read here through the corner list it prunes.
615    fn pruned(
616        waypoints: &[(i32, i32)],
617        start: GridPoint,
618        end: GridPoint,
619    ) -> (Vec<GridPoint>, bool) {
620        let stored: Vec<Waypoint> = waypoints.iter().map(|&(x, y)| wp(x, y)).collect();
621        let doomed = backtracking_ordinals(&stored, start, end);
622        (
623            corner_positions(&pruned_waypoints(&stored, &doomed)),
624            !doomed.is_empty(),
625        )
626    }
627
628    #[test]
629    fn removes_a_waypoint_the_wire_doubles_back_on() {
630        // start (0,0) → wp (10,0) → end (5,0): the wire runs east to x=10, then
631        // west to x=5 — a 180° reversal at the waypoint, which must be dropped.
632        let (kept, removed) = pruned(&[(10, 0)], g(0, 0), g(5, 0));
633        assert!(removed);
634        assert!(kept.is_empty());
635    }
636
637    #[test]
638    fn keeps_a_real_corner_and_a_straight_through_pin() {
639        // A 90° corner is not a reversal.
640        let (kept, removed) = pruned(&[(10, 0)], g(0, 0), g(10, 5));
641        assert!(!removed);
642        assert_eq!(kept, vec![g(10, 0)]);
643        // A waypoint between its neighbours on a straight run (0°) pins the wire's
644        // row; it is not a reversal, so it survives.
645        let (kept, removed) = pruned(&[(5, 0)], g(0, 0), g(10, 0));
646        assert!(!removed);
647        assert_eq!(kept.len(), 1);
648    }
649
650    #[test]
651    fn cascades_after_a_removal_exposes_a_new_reversal() {
652        // start (0,0) → (10,0) → (3,0) → end (5,0). Dropping the (10,0) overshoot
653        // leaves (0,0) → (3,0) → (5,0), a straight run, so (3,0) stays.
654        let (kept, removed) = pruned(&[(10, 0), (3, 0)], g(0, 0), g(5, 0));
655        assert!(removed);
656        assert_eq!(kept, vec![g(3, 0)]);
657    }
658}