Skip to main content

blockworx/widget/
routing.rs

1//! Route (re)solving for a level: the closed-graph solver every commit and
2//! every drag/resize preview goes through, the per-leg materialization that
3//! rebuilds a route's geometry from its stored corners, and the rip-up/reroute
4//! entry points.
5//!
6//! Split out of [`Drawing`]'s core so the routing policy — when a route is left
7//! alone, straightened, or re-solved — reads as one thing.
8
9use 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};
47/// What one solve pass promoted: for each wire it touched, the corner list
48/// its geometry now implies. The gesture's funnel diffs these against the
49/// staged document and pushes the ones that moved — the solver itself never
50/// writes.
51pub type Promoted = Vec<(RouteId, Vec<Waypoint>)>;
52
53/// The solve rider (`docs/flag-day-playbook.md`, 12·3): fold what the
54/// gesture has written so far onto a scratch prediction, re-solve `path`'s
55/// scope against *that* document, and author every corner list the solve
56/// promotes. Trims, re-solves and promotions therefore land in the same
57/// commit as the gesture that caused them, and the "every corner is a
58/// waypoint" invariant survives without the solver ever writing.
59///
60/// A gesture that has written nothing has nothing to solve against.
61pub(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
90/// A shape's world rect as the router reads it. The one conversion, so a
91/// preview's obstacle set and anything else that builds one cannot disagree
92/// about where a block's edges are.
93pub(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
100/// Whether a routing pass is the live preview or the committed result. A
101/// preview never mutates the document — it solves against hypothetical
102/// geometry into the derived store; only a commit rewrites waypoints.
103enum RoutePass<'a> {
104    Preview(PreviewSpec<'a>),
105    Commit,
106}
107
108/// What a preview frame supposes without committing. `inside` classifies each
109/// route endpoint as riding the gesture: a route fully inside previews its
110/// waypoints offset rigidly by `grid_delta`; a straddling route (exactly one
111/// end riding) has its moved-side approach corners *computed as trimmed* — the
112/// commit applies the real trim on drop.
113struct PreviewSpec<'a> {
114    grid_delta: GridVec,
115    inside: &'a dyn Fn(PinId) -> bool,
116}
117
118/// What a route edit's commit pushes as ops: the corner list the relayed
119/// geometry promotes, and each captured label anchor re-projected onto it.
120type RelaidRoute = (
121    Vec<Waypoint>,
122    Vec<(RouteLabelId, blockworx_doc::geometry::FracVal)>,
123);
124
125/// A pin sitting at a hypothetical slot for one preview frame: routes anchored
126/// to it solve as if the pin were already there, without moving it.
127pub struct PinSlotOverride {
128    pub pin: PinId,
129    pub side: PinSide,
130    pub offset: u32,
131}
132
133/// The hypothetical geometry one solve runs against: dragged/resized shapes at
134/// previewed rects, dragged pins at previewed slots. Empty for a settled
135/// document (a commit, or a preview whose overrides ride in elsewhere).
136#[derive(Clone, Copy, Default)]
137struct GeometryOverrides<'a> {
138    rects: &'a [(ShapeId, Rect)],
139    pins: &'a [PinSlotOverride],
140}
141
142/// The corners one preview frame supposes deleted without deleting them: the
143/// straddle trim plus any backtracking prune, computed against the frame's
144/// hypothetical endpoints. `pruned` mirrors the commit pass's "a prune
145/// happened, so the route must re-solve" trigger; a trim alone does not force
146/// one (matching the mutating flow, where trims never did).
147#[derive(Default)]
148struct PreviewExclusions {
149    excluded: HashSet<PathOrdinal>,
150    pruned: bool,
151}
152
153/// One route edit's working-corner plan, built on the drag's first frame
154/// ([`Drawing::route_edit_session`]) and applied only on release
155/// ([`Drawing::commit_route_edit`]): where each dragged corner enters the
156/// stored waypoint list, plus each label's world position at drag start —
157/// re-imposed on every relayout so labels stay put while the wire moves under
158/// them. Between build and commit the document is never written; the per-frame
159/// preview relays the hypothetical corner list into derived state.
160pub struct RouteEditSession {
161    slots: Vec<WaypointSlot>,
162    anchors: Vec<(RouteLabelId, Pos2)>,
163}
164
165/// Where one working corner lands in the stored waypoint list: a grabbed
166/// existing corner (moved in place) or a fresh insertion.
167enum WaypointSlot {
168    /// Reuse the corner holding this ordinal in the *stored* list, so a grab
169    /// survives insertions made by other slots of the same batch.
170    Existing(PathOrdinal),
171    /// Make a new corner at this index of the *working* list — which counts
172    /// the corners earlier slots of the same batch already inserted. So the
173    /// plan and its replay have to walk the slots in the same order, or a
174    /// second insertion lands somewhere the first never made room for.
175    InsertAt(usize),
176}
177
178/// The stored corners as a working list keyed by the ordinal each holds in
179/// the document. Shared by the plan and its replay rather than written twice:
180/// both count positions in this list, and a list built two ways is two
181/// different meanings for the same `InsertAt`.
182fn 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    /// Each label's captured drag-start world anchor, for the overlay that
195    /// draws labels pinned while the wire moves under them.
196    pub fn anchors(&self) -> &[(RouteLabelId, Pos2)] {
197        &self.anchors
198    }
199
200    /// The stored corner list with the working corners applied at `cursors` —
201    /// the hypothetical the preview relays through and the commit writes.
202    /// Working corners come out locked: the user placed them.
203    pub fn corners(
204        &self,
205        waypoints: &[Waypoint],
206        cursors: &[Pos2],
207    ) -> Vec<(GridPoint, InterfaceLock)> {
208        // Ordinals name positions in the *stored* list, so a grabbed corner is
209        // resolved before any insertion shifts it.
210        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                // Clamped because the stored list can have shrunk under the
227                // plan since it was made: a collaborator's commit lands
228                // mid-drag, and an index past the end must still place a
229                // corner rather than panic.
230                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
240/// The previewed geometry for `shape` when it appears in `overrides` (a block or
241/// port being dragged or resized), or `None` to use its stored geometry.
242fn override_rect(shape: ShapeId, overrides: &[(ShapeId, Rect)]) -> Option<Rect> {
243    overrides
244        .iter()
245        .find(|(overridden, _)| *overridden == shape)
246        .map(|(_, rect)| *rect)
247}
248
249/// The previewed slot for `pin` when a drag supposes one.
250fn override_slot(pin: PinId, pins: &[PinSlotOverride]) -> Option<&PinSlotOverride> {
251    pins.iter().find(|o| o.pin == pin)
252}
253
254impl Drawing<'_> {
255    /// Solve this scope's wires against the settled document and report the
256    /// corner lists the solve promotes. Read-only: the geometry lands in the
257    /// presentation layer, and the promoted lists are the gesture funnel's to
258    /// push as ops.
259    pub fn solve_routes(&mut self, ripup: &[RouteId]) -> Promoted {
260        self.route_update_closed(ripup, GeometryOverrides::default(), RoutePass::Commit)
261    }
262
263    /// Suppose each `(shape, offset)` in `drags` displaced by its offset,
264    /// without moving the shapes themselves in the model: the previewed rects
265    /// the frame draws at, and — for the shapes that take part in routing — the
266    /// wires re-solved to track them. With grid-snapped offsets the geometry
267    /// matches what the real move on drop produces.
268    pub fn suppose_drag(
269        &mut self,
270        _phase: &crate::tools::tool::Supposing,
271        drags: &[(ShapeId, Vec2)],
272    ) {
273        // A translation is just a rect override at the offset position.
274        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        // Annotations (areas, text, artwork) are not in the routing graph, so a
282        // drag of nothing else has no wires to re-solve.
283        if !drags.iter().any(|&(shape, _)| shape.affects_routing()) {
284            return;
285        }
286        // A group drag shares one offset across all shapes; derive the moved-set
287        // and grid-cell delta from that offset via the same helper the commit
288        // (`move_shapes`) uses, so a route fully inside the selection previews
289        // through waypoints offset by the SAME delta the commit will apply. The
290        // same sets classify straddling routes, whose stale approach corners the
291        // preview solve computes as trimmed — so the live preview shows the clean
292        // re-route the commit's real trim will produce (no release flash).
293        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    /// Suppose each `(shape, rect)` in `resizes` at that previewed geometry,
312    /// without resizing the shapes in the model — the resize-aware counterpart
313    /// of [`Self::suppose_drag`].
314    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    /// Re-route the routes touching `pin` as if it sat at (`side`, `offset`),
337    /// for the live drag preview. The hypothetical slot rides into the solve as
338    /// an override — anchor point and zero-cost channel both — so the pin itself
339    /// never moves; its routes' approach corners are computed as trimmed so the
340    /// preview matches the clean re-route the drop produces.
341    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    /// Re-route as if every listed pin were already at its previewed
364    /// `(side, slot)` — the group equivalent of
365    /// [`suppose_pin_drag`](Self::suppose_pin_drag), minus the approach trim
366    /// (the group flow never trimmed). Applies the whole move set as overrides
367    /// so the routes re-solve in a single pass while a multi-pin drag previews
368    /// them, without committing the relocation.
369    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    /// Where a route endpoint anchors, honoring a `hypo` entry for the shape
392    /// (previewed rect) or the pin (previewed slot) it lands on. Which shape
393    /// draws the pin is [`Drawing::pin_shape`]'s one answer, and the anchor
394    /// math is the shape layer's own — the same the emitters read.
395    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    /// The scope's routing shapes — its child blocks and its own boundary
406    /// ports, in draw order. Exactly what the router registers as obstacles
407    /// and what the obstacle index tests, so a leg judged clear by one is
408    /// judged clear by the other.
409    fn routing_shapes(&self) -> Vec<(ShapeId, ShapeRef<'_>)> {
410        self.blocks_layer().chain(self.ports_layer()).collect()
411    }
412
413    /// Build the closed routing graph for this scope: seed each child/port
414    /// obstacle rect and its pin channels (at previewed geometry for any shape
415    /// or pin in `hypo`), plus a full channel at every point in `seed_points`
416    /// (route endpoints and waypoints), then freeze the graph. The seed points
417    /// are exactly the ones every route needs, so the closed graph contains a
418    /// node for every route's endpoints and waypoints.
419    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            // A shape being dragged/resized uses its previewed rect, so obstacles
427            // and pin channels are placed where it is heading — and a dragged
428            // pin's channel likewise sits at its previewed slot.
429            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    /// The obstacle rectangles of this scope, at previewed geometry for any
455    /// shape in `overrides`. These are exactly the rects
456    /// [`Self::build_closed_router`] registers as router blocks, wrapped for the
457    /// cheap "does a straight leg cross a block?" test used by materialization,
458    /// so a leg judged clear here is the same one the router would leave
459    /// straight — without building the routing graph.
460    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    /// The waypoints each route must route *around* this preview frame: the
472    /// straddle trim (the moved side of a route with exactly one end riding the
473    /// gesture) plus the backtracking prune, both computed against the frame's
474    /// hypothetical endpoints — supposed, never applied. A route fully inside the
475    /// gesture keeps its skeleton byte-identical: it previews through rigidly
476    /// offset waypoints and, moving rigidly, can't introduce a reversal. Both
477    /// rules are the emitters' own ([`crate::edit::geometry`]), read from the
478    /// preview's side.
479    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                // The prune's ordinals index `working`; map them back through the
507                // surviving positions so both sets name the stored list.
508                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    /// Closed-graph route solver used by every path except live `RouteTool`
525    /// drawing: load, interactive commits, AND the drag/resize preview. It builds
526    /// the routing graph ONCE with all obstacle, endpoint, and waypoint geometry
527    /// (honoring `overrides` for previewed shapes and offsetting waypoints of
528    /// fully-dragged routes) seeded up front, then routes every route against it,
529    /// applying `WIRE_COST` occupancy by mutating existing edge weights in place —
530    /// so the graph is never rebuilt per route (the O(routes²) cost the old
531    /// per-route rebuild paid; see TUNING.md Finding 1).
532    ///
533    /// `pass` distinguishes a permanent edit (a drop, delete, paste, nudge —
534    /// the geometry is final) from a transient drag/resize preview (recomputed
535    /// every frame from stable state). On commit a changed route is
536    /// re-materialized per leg — its straight legs are kept and only the legs that
537    /// are now blocked or non-colinear are routed — and its corners are promoted
538    /// to waypoints; the backtracking prune really removes reversal corners. A
539    /// preview never touches the document: it routes through the stable waypoint
540    /// skeleton minus its *supposed* trims and prunes (so per-frame recomputation
541    /// never piles up derived corners), taking only immutable borrows.
542    #[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        // Preview: work out each route's supposed exclusions BEFORE seeding, so
560        // the graph matches the steady state of the old mutating flow (where a
561        // trimmed or pruned corner really was gone by the next frame's build).
562        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        // Freeze the geometry up front: seed a full channel at every route
569        // endpoint (snapped, as the router will use them) and every waypoint. A
570        // route fully inside a group drag previews through offset waypoints, so
571        // seed its waypoints at the SAME offset that `reroute_preview_closed` uses.
572        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        // Route in draw order so occupancy accumulates identically to the old
607        // path and crossing hops resolve the same way.
608        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                    // Drop any waypoint the wire doubles back on (a 180° turn
622                    // strict waypoint routing can leave behind), then force a
623                    // re-route so the reversal geometry is rebuilt clean.
624                    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                        // An "unchanged" route may still carry a stale waypoint
642                        // set from a live drag preview: the preview rewrites the
643                        // edges to track the moved shape but leaves the waypoints
644                        // behind, and its snapped endpoints then match here so the
645                        // route reads as unchanged. Re-derive the waypoints from
646                        // the (correct) edges so the "every corner is a waypoint"
647                        // invariant holds and the saved geometry matches what a
648                        // fresh load rebuilds. Cheap and idempotent for
649                        // genuinely-unchanged routes.
650                        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                        // Keep every straight leg, route only the blocked or
658                        // non-colinear ones, and promote the resulting corners to
659                        // waypoints. `add_route_cost` is applied inside so later
660                        // routes still spread off this one.
661                        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                    // Drawn somewhere the document does not put it, so the
721                    // cull set has to be told; a wire whose geometry still
722                    // holds (the `unchanged` skip above) does not.
723                    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    /// Reconstruct every route's edge geometry from its stored corner waypoints
740    /// WITHOUT globally re-routing. Straight (axis-aligned, unobstructed) legs are
741    /// drawn directly; only non-colinear or obstacle-crossing legs are routed, and
742    /// then only for the routes that need it. A clean block (every leg straight) is
743    /// rebuilt with zero pathfinding, so stored/hand-adjusted geometry is preserved
744    /// exactly. This is the load-time materialization; the interactive mutation
745    /// path reuses the same per-leg policy.
746    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        // Phase 1: straighten every leg we can; defer routes that still need the
760        // router (some leg blocked or non-colinear).
761        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        // Phase 2: only if something needs routing, build the graph once, seed the
779        // straight routes' occupancy, then route the deferred legs against it.
780        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    /// The route's resolved, snapped anchor endpoints against the settled
833    /// document — the ends every route-edit relayout runs between.
834    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    /// Re-lay a single route's edges directly from `corners`, WITHOUT the
845    /// router — straight legs stay straight and a diagonal leg gets an
846    /// L-bend, so the wire goes exactly where it was put and never
847    /// autoroutes. Returns the corner list the relayed geometry promotes and
848    /// each captured label anchor re-projected onto it: the pair the route
849    /// edit's commit pushes as ops.
850    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        // The relay ignores obstacles on purpose, so what it left is not what
865        // the document implies — a drag that ended where it started commits
866        // nothing, and this is what takes its supposition back.
867        self.presentation.routes_supposed();
868        Some((promoted, relayed))
869    }
870
871    /// Plan a route edit: classify where each cursor's working corner lands on
872    /// route `id` — a grabbed existing corner within half a cell is reused,
873    /// anything else inserts in path order — and capture each label's world
874    /// anchor. Read-only: [`Self::preview_route_edit`] renders the plan per
875    /// drag frame and [`Self::commit_route_edit`] applies it on release, so
876    /// between them the document is never written. Cursors must sit at least a
877    /// cell apart (the edge drag's boundary seeds guarantee it); two cursors
878    /// collapsing onto one working corner abort the plan.
879    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        // Planned against a working copy, so the second cursor sees the first
883        // cursor's insertion — the same list `corners` replays the slots
884        // against, built by the same function so the two cannot mean
885        // different things by an index into it.
886        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    /// Relay route `id` directly through the session's hypothetical corner
915    /// list at `cursors` — the pure per-frame preview of a route edit. Writes
916    /// derived geometry only; the authored route and its labels (the drag
917    /// overlay draws them pinned at the session's anchors) are untouched.
918    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    /// Finalize a route edit — the drag's one document write. Relays the
945    /// session's working corners (locked: the user placed them), promotes the
946    /// resulting corners so the hand-placed geometry persists, and re-anchors
947    /// each label to its captured drag-start position.
948    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    /// Rip up a route entirely and autoroute it fresh: drop every waypoint so the
979    /// endpoints re-route on Dijkstra cost alone, with no user-placed corners.
980    /// The gesture's solve rider re-promotes the corners the router finds.
981    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    /// Rip up and autoroute every wire of THIS scope with an endpoint on
989    /// `block` (either end): drop their user waypoints and let the rider
990    /// re-solve them. The block-wide analogue of [`Self::reroute`].
991    ///
992    /// Scoped deliberately: a block's pins are its boundary ports, so the
993    /// wires *inside* it end on the same ids. Those are drawn one level
994    /// down, the rider does not re-solve them, and tearing up a path nobody
995    /// re-lays would just lose it.
996    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    /// Forget what these wires were solved to, so the next pass re-solves
1019    /// them from nothing rather than keeping the path the user just tore up.
1020    /// The rip-up is the one gesture that must NOT be handed its own previous
1021    /// answer, and dropped geometry is how the solve is told so.
1022    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    /// Closed-router view of the current scope's geometry and existing-route
1030    /// occupancy, for tools that pathfind against it without mutating the
1031    /// document (`RouteTool`). The graph is built ONCE (obstacles + every existing
1032    /// route's endpoints and waypoints + `extra_seeds`), then existing routes are
1033    /// re-applied as `WIRE_COST` occupancy in place — no per-route rebuild.
1034    /// `extra_seeds` seeds the points the caller will route through (an in-progress
1035    /// route's endpoints/waypoints) so they exist as graph nodes.
1036    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    /// The plan and its replay have to agree about what an insertion index
1101    /// counts. Two cursors that both make new corners are the case where
1102    /// they can disagree: the second is planned against a list the first has
1103    /// already grown, so a replay that rebuilt the list differently — or
1104    /// applied the slots in another order — would put the second corner
1105    /// somewhere the preview never drew it.
1106    #[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        // Two points on the wire, a long way apart and neither on the stored
1117        // corner, so both slots are insertions rather than grabs. Taken as
1118        // edge midpoints, which are on the polyline by construction — a
1119        // guessed point would miss the wire and plan nothing.
1120        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        // Planned early-then-late, so the replay must keep that order: the
1162        // earlier cursor's corner sits before the later one's.
1163        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    /// Two blocks joined by one straight wire, B at `x_b`. Blocks `1`/`2`,
1171    /// pins `3`/`4`, route `5`.
1172    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        // Baseline routing against the stored positions.
1208        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        // Drag block `a` by a grid-aligned offset; only its endpoint follows.
1213        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    /// `add_route_label` and `place_route_label` are both wire-label writes
1225    /// (`edit::create::wire_label`, `edit::geometry::place_wire_label`), so
1226    /// each is a gesture of its own here.
1227    #[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        // Goal A: a purely local edit must not perturb the rest of the network.
1269        let mut scene = a_to_b_route(120.0);
1270        let far = block_id(9);
1271        scene.apply(vec![fx::block(9, 600.0)]); // nowhere near the route
1272        let rid = route_id(5);
1273        let before = route_edges(&scene.drawing(), rid);
1274
1275        // Commit a move of the distant block.
1276        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        // Requirement: moving a route segment must not trigger an autoroute. The
1293        // editor relay ([`Drawing::relay_route`], which the route edit's commit
1294        // runs) redraws the wire straight THROUGH a block sitting on it — a
1295        // fresh autoroute would instead detour around it.
1296        let mut scene = a_to_b_route(240.0);
1297        // A block straddling the wire's row (grid (8,1)-(10,4)).
1298        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        // Dragging BOTH endpoints of a route with a waypoint must preview the
1327        // wire routed through the waypoint OFFSET by the drag delta, while the
1328        // STORED waypoint stays put (the drag is uncommitted).
1329        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); // grid_delta (0, 4)
1340        drawing.suppose_drag(
1341            &crate::tools::tool::Supposing::testing(),
1342            &[(ShapeId::Rect(a), off), (ShapeId::Rect(b), off)],
1343        );
1344
1345        // Both endpoints follow the drag: the whole route previews rigidly shifted.
1346        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        // The STORED waypoints are untouched by the preview (the drag is
1351        // uncommitted): the shift lives only in the previewed edges.
1352        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        // Dragging only ONE endpoint: the route straddles the selection boundary,
1362        // so only the dragged endpoint follows and the stored corners stay put
1363        // (the preview is uncommitted).
1364        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        // The stationary endpoint stays; only the dragged one moves.
1378        assert_eq!(
1379            drawing.route_geometry(rid).unwrap().end_pos(),
1380            end0,
1381            "the un-dragged endpoint is fixed"
1382        );
1383
1384        // A partial-selection preview re-solves the moved leg from the stable
1385        // corner skeleton, so it may normalize (e.g. drop a deduped revisit) but
1386        // must never COMMIT new corners — every surviving corner was already there.
1387        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    /// Two blocks whose pins sit on different slots, so the wire between them
1395    /// really bends, standing on the corners the SOLVER itself lays down: the
1396    /// throwaway corner is ripped up, which gives the settling gesture an op to
1397    /// author and its rider a reason to promote the solved list back. A claim
1398    /// about what a drag does to a wire's shape only means something about a
1399    /// shape the solve already agrees with.
1400    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        // The whole point: the previewed geometry must equal the committed
1423        // geometry, so releasing the drag causes no visible snap.
1424        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        // Preview path.
1428        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        // Precondition: the drag previewed a move at all, so agreeing with the
1439        // commit is agreement about something.
1440        assert_ne!(previewed, settled, "the preview moved the wire");
1441
1442        // Commit path (fresh scene, same drag), from the same settled solve.
1443        let mut scene2 = settled_bend();
1444        scene2.commit(|commit| commit.move_shapes(&[ShapeId::Rect(a), ShapeId::Rect(b)], off));
1445        // What the user sees on release: the geometry the landed commit
1446        // re-derives, which is the rider's promoted corners materialized.
1447        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        // Two waypoints planted in the open corridor between the blocks force the
1458        // wire east, then straight back west, then east again — a 180° reversal
1459        // strict waypoint routing would honour. The gesture's solve rider must
1460        // drop the overshoot so no edge doubles back. The blocks sit far apart
1461        // so the corridor is wide enough for the two interior waypoints.
1462        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        // Any gesture at all: the rider re-solves the scope it touched, and
1473        // the reversal is what the re-solve prunes. A rip-up is the smallest
1474        // one that names this route.
1475        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        // A locked corner ends the scan, preserving the user's explicit bend and
1517        // everything past it.
1518        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        // Flip L/R moves a block's pins to new sides. Like a drag, the connected
1529        // route drops its stale approach corner and re-routes, leaving a
1530        // well-formed wire (waypoints in sync with corners). Mirrors the
1531        // FlipShapePins handler: flip, trim approaches, re-route.
1532        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        // A boundary port on the document root, wired to a child block's pin.
1557        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        // A fresh port faces opposite the edge its pin occupies.
1577        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        // The pin's edge side (what the parent block draws) is untouched; only the
1584        // port's facing changed.
1585        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        // The route followed the port to its new facing — still connected.
1593        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    /// The route's stored waypoints must be exactly its edge corners (the interior
1601    /// vertices where the polyline bends) — the "every corner is a waypoint"
1602    /// invariant. A mismatch is a stale waypoint set: it renders wrong when the
1603    /// route is selected and changes on the next load (which re-derives it).
1604    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        // B sits on a different row than A, so the wire bends — it has real
1616        // corners that must stay in sync with the stored waypoints.
1617        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        // Drag B: several live-preview frames, then the on-drop commit — exactly
1632        // the sequence MoveBlock produces.
1633        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        // C's boundary port wired to a grandchild's pin — a route living inside C.
1657        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        // Mirror C at the top level: its edge pins flip side, but each pin's port
1685        // orientation is frozen.
1686        scene.path.pop();
1687        scene.commit(|drawing| drawing.flip_shape_pins(ShapeId::Rect(c)));
1688
1689        // Inside C, the boundary port hasn't moved, so its internal route keeps
1690        // its endpoint.
1691        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        // A routed waypoint makes the straddle trim and backtracking prune
1710        // non-trivial: the preview must *suppose* corners away, not delete them.
1711        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        // The group flow runs the same pure pass.
1789        {
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}