Skip to main content

blockworx_editor/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 reconstruction 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::{BTreeMap, HashMap, HashSet};
10
11use blockworx_doc::{
12    commit::CommitBuilder,
13    document::IndexedDocument,
14    geometry::{GridPoint, GridVec, Waypoint},
15    id::{PinId, RouteId, RouteLabelId},
16    opcode::OpCodes,
17    values::PinSide,
18};
19use blockworx_geom::{Pos2, Rect, Vec2, vec2};
20use blockworx_router::{
21    Bounds, ClosedRouter, Resolution, RouterNGBuilder, WIRE_COST, cost::COST_ZERO, point::Point,
22    turtle::Mark,
23};
24
25use crate::{
26    edit::{
27        create::PathOrdinal,
28        geometry::{PinMove, RouteEnd, backtracking_ordinals, pruned_waypoints, trimmed_ordinals},
29        naming::InterfaceLock,
30    },
31    grid::{GRID_SIZE, grid_point, px_point, snap_to_grid},
32    path::BlockPath,
33    presentation::{Presentation, RouteGeometry},
34    shape::{ShapeId, ShapeRef},
35    widget::{
36        auto_route::{
37            label_anchors, reanchored, reroute_preview_closed, reroute_preview_excluding,
38        },
39        drawing::Drawing,
40        foreground::Foreground,
41        movement::moved_set_and_delta,
42        reconstruct::{
43            Endpoints, Obstacles, promote_corners_to_waypoints, reconstruct_corners_direct,
44            reconstruct_route, straighten_route,
45        },
46        waypoint_router::add_route_cost,
47    },
48};
49/// What one solve pass promoted: for each wire it touched, the corner list
50/// its geometry now implies. The gesture's funnel diffs these against the
51/// staged document and pushes the ones that moved — the solver itself never
52/// writes.
53pub type Promoted = Vec<(RouteId, Vec<Waypoint>)>;
54
55/// What the gesture wrote, as the rider has to read it: the ops staged so far,
56/// and the footprints their shapes have *left* — which the staged document no
57/// longer holds, so nothing downstream can derive them.
58#[derive(Clone, Copy)]
59pub struct Edited<'a> {
60    pub ops: &'a [OpCodes],
61    pub vacated: &'a [Rect],
62}
63
64/// How much of the scope one ride re-solves.
65#[derive(Clone, Copy)]
66enum Riding<'a> {
67    /// Every wire. The settling pass, which is not a gesture and has no
68    /// foreground to derive — it is solving the document for the first time.
69    WholeScope,
70    /// Only what the gesture wrote and what that disturbed.
71    Edited(Edited<'a>),
72}
73
74/// Fold what the gesture has written so far onto a scratch prediction,
75/// re-solve against that document, and author every corner list the solve
76/// promotes. Trims, re-solves and promotions therefore land in the same commit
77/// as the gesture that caused them, and the "every corner is a waypoint"
78/// invariant survives without the solver ever writing.
79fn ride(
80    indexed: &IndexedDocument<'_>,
81    path: &BlockPath,
82    presentation: &Presentation,
83    riding: Riding<'_>,
84    sink: &mut CommitBuilder,
85) -> Option<Rect> {
86    let _s = tracing::info_span!("ride").entered();
87    let mut scratch = {
88        let _s = tracing::info_span!("rider_scratch").entered();
89        presentation.scratch(indexed)
90    };
91    let mut discarded = crate::gesture::Gesture::idle();
92    let (promoted, disturbed) = {
93        let mut drawing = Drawing::new(*indexed, path, &mut scratch, &mut discarded);
94        match riding {
95            Riding::WholeScope => (drawing.solve_routes(&[]), None),
96            Riding::Edited(edited) => {
97                let mut foreground = Foreground::written(edited.ops, &drawing);
98                for &vacated in edited.vacated {
99                    foreground.also_disturbed_by(vacated, &drawing);
100                }
101                // Measured before the solve moves anything: the wires it is
102                // about to rewrite are where they are *now*, and the
103                // reconstruction after the commit has to reach both.
104                let was = foreground.extent(&drawing);
105                let promoted = drawing.solve_foreground(&foreground, &[]);
106                let now = foreground.extent(&drawing);
107                (promoted, Some(was.union(now)))
108            }
109        }
110    };
111    debug_assert!(
112        discarded.ops().is_empty(),
113        "the solve is a read; it must author nothing of its own"
114    );
115    for (route, waypoints) in promoted {
116        crate::edit::geometry::commit_route_edit(
117            indexed.doc,
118            crate::edit::geometry::RouteEdit {
119                route,
120                waypoints,
121                anchors: &[],
122            },
123            sink,
124        );
125    }
126    disturbed
127}
128
129/// The solve rider: re-solve the wires the gesture *disturbed*, and author what
130/// that promotes into the same commit.
131///
132/// Scoped to the foreground rather than the whole scope, because re-solving
133/// every wire means every wire may move — a cost proportional to the sheet, and
134/// geometry churn nobody asked for: one block moving one cell used to rewrite
135/// two thirds of a small sheet's corners.
136///
137/// A gesture that has written nothing has nothing to solve against.
138/// Returns the rectangle the gesture disturbed, for the reconstruction that
139/// follows the commit.
140pub(crate) fn solve_rider(
141    indexed: &IndexedDocument<'_>,
142    path: &BlockPath,
143    presentation: &Presentation,
144    edited: Edited<'_>,
145    sink: &mut CommitBuilder,
146) -> Option<Rect> {
147    ride(indexed, path, presentation, Riding::Edited(edited), sink)
148}
149
150/// Solve every wire in the scope, for the settling pass that gives a generated
151/// document its corners. Not a gesture: there is nothing to scope to, and the
152/// point is precisely to touch everything.
153pub(crate) fn settle_scope(
154    indexed: &IndexedDocument<'_>,
155    path: &BlockPath,
156    presentation: &Presentation,
157    sink: &mut CommitBuilder,
158) {
159    ride(indexed, path, presentation, Riding::WholeScope, sink);
160}
161
162/// A shape's world rect as the router reads it. The one conversion, so a
163/// preview's obstacle set and anything else that builds one cannot disagree
164/// about where a block's edges are.
165pub fn obstacle_rect(rect: Rect) -> blockworx_router::block::Block {
166    blockworx_router::block::Block {
167        top_left: rect.left_top().into(),
168        bottom_right: rect.right_bottom().into(),
169    }
170}
171
172/// What a lattice is built from: the hypothetical geometry it holds shapes
173/// at, and every route's endpoints and waypoints, which it must hold a node for.
174struct Seeding<'a> {
175    hypo: GeometryOverrides<'a>,
176    ids: &'a [RouteId],
177    endpoints: &'a [(RouteId, Pos2, Pos2)],
178    exclusions: &'a HashMap<RouteId, PreviewExclusions>,
179    offsets: &'a HashMap<RouteId, GridVec>,
180}
181
182/// A wire a preview frame reroutes: its ends, the waypoints it routes
183/// through, and — for one riding entirely inside a group drag — the offset
184/// those waypoints ride at.
185struct Rerouting {
186    ends: Endpoints,
187    stored: Vec<Waypoint>,
188    rigid: Option<GridVec>,
189}
190
191/// Seed points for a lattice covering `within`, and no others.
192///
193/// Gathered per lattice rather than once per pass: a commit that straightens
194/// every leg builds no lattice at all, and gathering 30,000 points for one that
195/// is never built was ~8 ms of every commit. Points outside the bounds are
196/// dropped here because the lattice would drop them anyway.
197fn seeds_within(drawing: &Drawing<'_>, seeding: &Seeding<'_>, within: Region) -> Vec<Point> {
198    let mut seed_points: Vec<Point> = Vec::with_capacity(seeding.endpoints.len() * 2);
199    let bounds = within.bounds();
200    let mut take = |p: Point| {
201        if bounds.holds(p) {
202            seed_points.push(p);
203        }
204    };
205    for &(_, s, e) in seeding.endpoints {
206        take(snap_to_grid(s).into());
207        take(snap_to_grid(e).into());
208    }
209    for &id in seeding.ids {
210        for pos in seeded_waypoints(drawing, seeding, id) {
211            take(pos.into());
212        }
213    }
214    seed_points
215}
216
217/// Where a lattice holds `id`'s waypoints this pass: offset with a group drag
218/// it rides entirely inside, and without the ones a preview trims or prunes.
219fn seeded_waypoints<'a>(
220    drawing: &'a Drawing<'_>,
221    seeding: &'a Seeding<'_>,
222    id: RouteId,
223) -> impl Iterator<Item = GridPoint> + 'a {
224    let excluded = seeding.exclusions.get(&id);
225    let delta = seeding.offsets.get(&id).copied();
226    drawing
227        .route(id)
228        .into_iter()
229        .flat_map(|route| route.waypoints.iter().enumerate())
230        .filter(move |(index, _)| {
231            !excluded.is_some_and(|x| x.excluded.contains(&PathOrdinal::new(*index)))
232        })
233        .map(move |(_, wp)| delta.map_or(wp.pos, |delta| wp.pos + delta))
234}
235
236/// Does a wire's geometry still run between these anchors without crossing a
237/// block? One that does needs no routing, whatever else moved.
238fn still_holds(geometry: &RouteGeometry, start: Pos2, end: Pos2, obstacles: &Obstacles) -> bool {
239    geometry.start_pos() == start
240        && geometry.end_pos() == end
241        && !geometry
242            .iter_edges()
243            .any(|(_, e)| obstacles.blocked(e.start, e.end))
244}
245
246/// The rectangle to heal: what the wires needing a lattice span, with room
247/// around them to get past whatever is refusing them.
248///
249/// The margin is the moat twice over — once because a block shapes the channels
250/// that far out, and once more so a detour has somewhere to go. A region too
251/// small does not route wrongly, it fails to route, and the leg falls back to
252/// an L; that is why the margin is generous and why the unresolved list is what
253/// makes this honest.
254fn healing_region(reach: impl IntoIterator<Item = GridPoint>) -> Rect {
255    let margin = 2.0 * blockworx_router::block::MOAT_REACH as f32 * GRID_SIZE;
256    let mut region: Option<Rect> = None;
257    for p in reach {
258        let at = px_point(p);
259        let here = Rect::from_min_max(at, at);
260        region = Some(region.map_or(here, |so_far| so_far.union(here)));
261    }
262    region.unwrap_or(Rect::ZERO).expand(margin)
263}
264
265/// Which of a scope's wires a reconstruction re-derives. The rest keep the
266/// geometry they have, which is still what the document implies — nothing
267/// moved near them.
268#[derive(Clone, Copy)]
269pub(crate) enum Reconstructing<'a> {
270    /// An open, an undo, a commit from elsewhere: nothing is known about what
271    /// changed, so everything is re-derived.
272    Everything,
273    /// What a commit disturbed, and the wires a preview left drawn where the
274    /// document does not put them — taken back in the same pass rather than
275    /// re-derived once by the take-back and again by the commit.
276    Within {
277        disturbed: Rect,
278        previewed: &'a std::collections::HashSet<RouteId>,
279    },
280    /// Exactly these — what a preview rewrote and the next borrow takes back.
281    These(&'a std::collections::HashSet<RouteId>),
282}
283
284/// How much of the routing plane a lattice covers. A local edit has local
285/// effects, so there is no reason a router built to move one block has to hold
286/// the whole diagram.
287#[derive(Clone, Copy, Debug)]
288pub(crate) enum Region {
289    WholeScope,
290    Within(Rect),
291}
292
293impl Region {
294    /// The lattice bounds this region names. The router owns what bounds mean —
295    /// a block straddling the edge is kept whole, a channel stops at the edge,
296    /// a seed outside seeds nothing — so nothing here re-decides it.
297    fn bounds(self) -> Bounds {
298        match self {
299            Region::WholeScope => Bounds::UNBOUNDED,
300            Region::Within(region) => Bounds::between(region.min, region.max),
301        }
302    }
303}
304
305/// Which of a scope's wires a pass may re-solve. The rest are not skipped —
306/// they contribute their occupancy — they are simply not re-decided.
307#[derive(Clone, Copy)]
308enum Solving<'a> {
309    Everything,
310    Only(&'a Foreground),
311}
312
313impl Solving<'_> {
314    fn may_solve(self, id: RouteId) -> bool {
315        match self {
316            Solving::Everything => true,
317            Solving::Only(foreground) => foreground.holds_route(id),
318        }
319    }
320}
321
322/// Whether a routing pass is the live preview or the committed result. A
323/// preview never mutates the document — it solves against hypothetical
324/// geometry into the derived store; only a commit rewrites waypoints.
325enum RoutePass<'a> {
326    Preview(PreviewSpec<'a>),
327    Commit,
328}
329
330/// What a preview frame previews without committing. `inside` classifies each
331/// route endpoint as riding the gesture: a route fully inside previews its
332/// waypoints offset rigidly by `grid_delta`; a straddling route (exactly one
333/// end riding) has its moved-side approach corners *computed as trimmed* — the
334/// commit applies the real trim on drop.
335struct PreviewSpec<'a> {
336    grid_delta: GridVec,
337    inside: &'a dyn Fn(PinId) -> bool,
338}
339
340/// What a route edit's commit pushes as ops: the corner list the relayed
341/// geometry promotes, and each captured label anchor re-projected onto it.
342type RelaidRoute = (
343    Vec<Waypoint>,
344    Vec<(RouteLabelId, blockworx_doc::geometry::FracVal)>,
345);
346
347/// A pin sitting at a hypothetical slot for one preview frame: routes anchored
348/// to it solve as if the pin were already there, without moving it.
349pub struct PinSlotOverride {
350    pub pin: PinId,
351    pub side: PinSide,
352    pub offset: u32,
353}
354
355/// The hypothetical geometry one solve runs against: dragged/resized shapes at
356/// previewed rects, dragged pins at previewed slots. Empty for a settled
357/// document (a commit, or a preview whose overrides ride in elsewhere).
358#[derive(Clone, Copy, Default)]
359struct GeometryOverrides<'a> {
360    rects: &'a [(ShapeId, Rect)],
361    pins: &'a [PinSlotOverride],
362}
363
364/// The corners one preview frame previews deleted without deleting them: the
365/// straddle trim plus any backtracking prune, computed against the frame's
366/// hypothetical endpoints. `pruned` mirrors the commit pass's "a prune
367/// happened, so the route must re-solve" trigger; a trim alone does not force
368/// one (matching the mutating flow, where trims never did).
369#[derive(Default)]
370struct PreviewExclusions {
371    excluded: HashSet<PathOrdinal>,
372    pruned: bool,
373}
374
375/// One route edit's working-corner plan, built on the drag's first frame
376/// ([`Drawing::route_edit_session`]) and applied only on release
377/// ([`Drawing::commit_route_edit`]): where each dragged corner enters the
378/// stored waypoint list, plus each label's world position at drag start —
379/// re-imposed on every relayout so labels stay put while the wire moves under
380/// them. Between build and commit the document is never written; the per-frame
381/// preview relays the hypothetical corner list into derived state.
382pub struct RouteEditSession {
383    slots: Vec<WaypointSlot>,
384    anchors: Vec<(RouteLabelId, Pos2)>,
385}
386
387/// Where one working corner lands in the stored waypoint list: a grabbed
388/// existing corner (moved in place) or a fresh insertion.
389enum WaypointSlot {
390    /// Reuse the corner holding this ordinal in the *stored* list, so a grab
391    /// survives insertions made by other slots of the same batch.
392    Existing(PathOrdinal),
393    /// Make a new corner at this index of the *working* list — which counts
394    /// the corners earlier slots of the same batch already inserted. So the
395    /// plan and its replay have to walk the slots in the same order, or a
396    /// second insertion lands somewhere the first never made room for.
397    InsertAt(usize),
398}
399
400/// The stored corners as a working list keyed by the ordinal each holds in
401/// the document. Shared by the plan and its replay rather than written twice:
402/// both count positions in this list, and a list built two ways is two
403/// different meanings for the same `InsertAt`.
404fn keyed_corners<T>(
405    waypoints: &[Waypoint],
406    value: impl Fn(&Waypoint) -> T,
407) -> Vec<(Option<PathOrdinal>, T)> {
408    waypoints
409        .iter()
410        .enumerate()
411        .map(|(index, wp)| (Some(PathOrdinal::new(index)), value(wp)))
412        .collect()
413}
414
415impl RouteEditSession {
416    /// Each label's captured drag-start world anchor, for the overlay that
417    /// draws labels pinned while the wire moves under them.
418    pub fn anchors(&self) -> &[(RouteLabelId, Pos2)] {
419        &self.anchors
420    }
421
422    /// The stored corner list with the working corners applied at `cursors` —
423    /// the hypothetical the preview relays through and the commit writes.
424    /// Working corners come out locked: the user placed them.
425    pub fn corners(
426        &self,
427        waypoints: &[Waypoint],
428        cursors: &[Pos2],
429    ) -> Vec<(GridPoint, InterfaceLock)> {
430        // Ordinals name positions in the *stored* list, so a grabbed corner is
431        // resolved before any insertion shifts it.
432        let mut corners = keyed_corners(waypoints, |wp| {
433            let lock = if wp.locked {
434                InterfaceLock::Locked
435            } else {
436                InterfaceLock::Unlocked
437            };
438            (wp.pos, lock)
439        });
440        for (slot, &cursor) in self.slots.iter().zip(cursors) {
441            let pos = grid_point(cursor);
442            match slot {
443                WaypointSlot::Existing(ordinal) => {
444                    if let Some(c) = corners.iter_mut().find(|(o, _)| *o == Some(*ordinal)) {
445                        c.1 = (pos, InterfaceLock::Locked);
446                    }
447                }
448                // Clamped because the stored list can have shrunk under the
449                // plan since it was made, and an index past the end must
450                // still place a corner rather than panic.
451                WaypointSlot::InsertAt(i) => {
452                    let i = (*i).min(corners.len());
453                    corners.insert(i, (None, (pos, InterfaceLock::Locked)));
454                }
455            }
456        }
457        corners.into_iter().map(|(_, corner)| corner).collect()
458    }
459}
460
461/// The previewed geometry for `shape` when it appears in `overrides` (a block or
462/// port being dragged or resized), or `None` to use its stored geometry.
463fn override_rect(shape: ShapeId, overrides: &[(ShapeId, Rect)]) -> Option<Rect> {
464    overrides
465        .iter()
466        .find(|(overridden, _)| *overridden == shape)
467        .map(|(_, rect)| *rect)
468}
469
470/// The previewed slot for `pin` when a drag previews one.
471fn override_slot(pin: PinId, pins: &[PinSlotOverride]) -> Option<&PinSlotOverride> {
472    pins.iter().find(|o| o.pin == pin)
473}
474
475impl Drawing<'_> {
476    /// Solve this scope's wires against the settled document and report the
477    /// corner lists the solve promotes. Read-only: the geometry lands in the
478    /// presentation layer, and the promoted lists are the gesture funnel's to
479    /// push as ops.
480    pub fn solve_routes(&mut self, ripup: &[RouteId]) -> Promoted {
481        self.route_update_closed(
482            ripup,
483            GeometryOverrides::default(),
484            RoutePass::Commit,
485            Solving::Everything,
486        )
487    }
488
489    /// Re-solve only `foreground`'s wires. Every other wire in the scope keeps
490    /// the geometry it has and contributes it as occupancy, so the foreground
491    /// routes *around* the rest and lands last — which is what makes the result
492    /// depend on the selection, deliberately
493    /// (`docs/foreground-router-playbook.md`).
494    pub fn solve_foreground(&mut self, foreground: &Foreground, ripup: &[RouteId]) -> Promoted {
495        self.route_update_closed(
496            ripup,
497            GeometryOverrides::default(),
498            RoutePass::Commit,
499            Solving::Only(foreground),
500        )
501    }
502
503    /// Suppose each `(shape, offset)` in `drags` displaced by its offset,
504    /// without moving the shapes themselves in the model: the previewed rects
505    /// the frame draws at, and — for the shapes that take part in routing — the
506    /// wires re-solved to track them. With grid-snapped offsets the geometry
507    /// matches what the real move on drop produces.
508    pub fn preview_drag(
509        &mut self,
510        _phase: &crate::widget::PreviewPhase,
511        drags: &[(ShapeId, Vec2)],
512    ) {
513        // A translation is just a rect override at the offset position.
514        let overrides: Vec<(ShapeId, Rect)> = drags
515            .iter()
516            .filter_map(|&(shape, offset)| {
517                Some((shape, self.shape(shape)?.gui_rect().translate(offset)))
518            })
519            .collect();
520        self.preview_shapes(&overrides);
521        // Annotations (areas, text, artwork) are not in the routing graph, so a
522        // drag of nothing else has no wires to re-solve.
523        if !drags.iter().any(|&(shape, _)| shape.affects_routing()) {
524            return;
525        }
526        // A group drag shares one offset across all shapes; derive the moved-set
527        // and grid-cell delta from that offset via the same helper the commit
528        // (`move_shapes`) uses, so a route fully inside the selection previews
529        // through waypoints offset by the SAME delta the commit will apply. The
530        // same sets classify straddling routes, whose stale approach corners the
531        // preview solve computes as trimmed — so the live preview shows the clean
532        // re-route the commit's real trim will produce (no release flash).
533        let ids: Vec<ShapeId> = drags.iter().map(|&(shape, _)| shape).collect();
534        let delta = drags.first().map_or(Vec2::ZERO, |&(_, offset)| offset);
535        let (moved_rects, moved_pins, grid_delta) = moved_set_and_delta(&ids, delta);
536        let riding = self.riding_pins(&moved_rects, &moved_pins);
537        let inside = |pin: PinId| riding.contains(&pin);
538        let foreground = self.preview_foreground(ids, &overrides);
539        let _ = self.route_update_closed(
540            &[],
541            GeometryOverrides {
542                rects: &overrides,
543                pins: &[],
544            },
545            RoutePass::Preview(PreviewSpec {
546                grid_delta,
547                inside: &inside,
548            }),
549            Solving::Only(&foreground),
550        );
551    }
552
553    /// Suppose each `(shape, rect)` in `resizes` at that previewed geometry,
554    /// without resizing the shapes in the model — the resize-aware counterpart
555    /// of [`Self::preview_drag`].
556    pub fn preview_resize(
557        &mut self,
558        _phase: &crate::widget::PreviewPhase,
559        resizes: &[(ShapeId, Rect)],
560    ) {
561        self.preview_shapes(resizes);
562        if !resizes.iter().any(|&(shape, _)| shape.affects_routing()) {
563            return;
564        }
565        let foreground = self.preview_foreground(resizes.iter().map(|&(shape, _)| shape), resizes);
566        let _ = self.route_update_closed(
567            &[],
568            GeometryOverrides {
569                rects: resizes,
570                pins: &[],
571            },
572            RoutePass::Preview(PreviewSpec {
573                grid_delta: GridVec::new(0, 0),
574                inside: &|_| false,
575            }),
576            Solving::Only(&foreground),
577        );
578    }
579
580    /// Re-route the routes touching `pin` as if it sat at (`side`, `offset`),
581    /// for the live drag preview. The hypothetical slot rides into the solve as
582    /// an override — anchor point and zero-cost channel both — so the pin itself
583    /// never moves; its routes' approach corners are computed as trimmed so the
584    /// preview matches the clean re-route the drop produces.
585    pub fn preview_pin_drag(
586        &mut self,
587        _phase: &crate::widget::PreviewPhase,
588        pin: PinId,
589        side: PinSide,
590        offset: u32,
591    ) {
592        let slots = [PinSlotOverride { pin, side, offset }];
593        let inside = move |other: PinId| other == pin;
594        let foreground = self.preview_foreground(self.pin_shape(pin), &[]);
595        let _ = self.route_update_closed(
596            &[],
597            GeometryOverrides {
598                rects: &[],
599                pins: &slots,
600            },
601            RoutePass::Preview(PreviewSpec {
602                grid_delta: GridVec::new(0, 0),
603                inside: &inside,
604            }),
605            Solving::Only(&foreground),
606        );
607    }
608
609    /// Re-route as if every listed pin were already at its previewed
610    /// `(side, slot)` — the group equivalent of
611    /// [`preview_pin_drag`](Self::preview_pin_drag), minus the approach trim
612    /// (the group flow never trimmed). Applies the whole move set as overrides
613    /// so the routes re-solve in a single pass while a multi-pin drag previews
614    /// them, without committing the relocation.
615    pub fn preview_pin_drags(&mut self, _phase: &crate::widget::PreviewPhase, moves: &[PinMove]) {
616        let slots: Vec<PinSlotOverride> = moves
617            .iter()
618            .map(|m| PinSlotOverride {
619                pin: m.pin,
620                side: m.to.side,
621                offset: m.to.offset,
622            })
623            .collect();
624        let foreground =
625            self.preview_foreground(moves.iter().filter_map(|m| self.pin_shape(m.pin)), &[]);
626        let _ = self.route_update_closed(
627            &[],
628            GeometryOverrides {
629                rects: &[],
630                pins: &slots,
631            },
632            RoutePass::Preview(PreviewSpec {
633                grid_delta: GridVec::new(0, 0),
634                inside: &|_| false,
635            }),
636            Solving::Only(&foreground),
637        );
638    }
639
640    /// What a preview may re-solve: the wires the moving shapes carry or
641    /// disturb, both where they are and where they are `heading_for`.
642    fn preview_foreground(
643        &self,
644        shapes: impl IntoIterator<Item = ShapeId>,
645        heading_for: &[(ShapeId, Rect)],
646    ) -> Foreground {
647        let mut foreground = Foreground::raising(shapes, self);
648        for &(_, rect) in heading_for {
649            foreground.also_disturbed_by(rect, self);
650        }
651        foreground
652    }
653
654    /// Where a route endpoint anchors, honoring a `hypo` entry for the shape
655    /// (previewed rect) or the pin (previewed slot) it lands on. Which shape
656    /// draws the pin is [`Drawing::pin_shape`]'s one answer, and the anchor
657    /// math is the shape layer's own — the same the emitters read.
658    fn anchor_pos_overridden(&self, pin: PinId, hypo: GeometryOverrides<'_>) -> Option<Pos2> {
659        let sid = self.pin_shape(pin)?;
660        let shape = self.shape(sid)?;
661        let rect = override_rect(sid, hypo.rects).unwrap_or_else(|| shape.gui_rect());
662        match (override_slot(pin, hypo.pins), &shape) {
663            (Some(o), ShapeRef::Block(block)) => Some(block.pin_anchor_at(rect, o.side, o.offset)),
664            _ => shape.anchor_point_with_rect(rect, pin),
665        }
666    }
667
668    /// The scope's routing shapes — its child blocks and its own boundary
669    /// ports, in draw order. Exactly what the router registers as obstacles
670    /// and what the obstacle index tests, so a leg judged clear by one is
671    /// judged clear by the other.
672    #[tracing::instrument(level = "info", skip_all)]
673    fn routing_shapes(&self) -> Vec<(ShapeId, ShapeRef<'_>)> {
674        self.blocks_layer().chain(self.ports_layer()).collect()
675    }
676
677    /// Build the closed routing graph for this scope: seed each child/port
678    /// obstacle rect and its pin channels (at previewed geometry for any shape
679    /// or pin in `hypo`), plus a full channel at every point in `seed_points`
680    /// (route endpoints and waypoints), then freeze the graph. The seed points
681    /// are exactly the ones every route needs, so the closed graph contains a
682    /// node for every route's endpoints and waypoints.
683    fn build_closed_router(
684        &self,
685        hypo: GeometryOverrides<'_>,
686        seed_points: &[Point],
687    ) -> ClosedRouter {
688        self.build_router_within(Region::WholeScope, hypo, seed_points)
689    }
690
691    /// The same build, confined to `within`. A region keeps every block that
692    /// *intersects* it — a block straddling the edge blocks as much as it does
693    /// anywhere — and every seed point inside it. What that leaves out is the
694    /// rest of the diagram, which is the point: the lattice costs what the
695    /// region holds rather than what the sheet holds.
696    fn build_router_within(
697        &self,
698        within: Region,
699        hypo: GeometryOverrides<'_>,
700        seed_points: &[Point],
701    ) -> ClosedRouter {
702        let mut builder = RouterNGBuilder::default().within(within.bounds());
703        for (sid, shape) in self.routing_shapes() {
704            // A shape being dragged/resized uses its previewed rect, so obstacles
705            // and pin channels are placed where it is heading — and a dragged
706            // pin's channel likewise sits at its previewed slot.
707            let rect = override_rect(sid, hypo.rects).unwrap_or_else(|| shape.gui_rect());
708            builder.add_block(rect.left_top(), rect.right_bottom());
709            shape.with_pins(|pid, _pin| {
710                let slot = match &shape {
711                    ShapeRef::Block(block) => override_slot(pid, hypo.pins)
712                        .map(|o| block.pin_anchor_at(rect, o.side, o.offset)),
713                    _ => None,
714                };
715                let Some(anchor) = slot.or_else(|| shape.anchor_point_with_rect(rect, pid)) else {
716                    return;
717                };
718                let anchor_pos = if anchor.x >= rect.center().x {
719                    anchor + vec2(GRID_SIZE, 0.0)
720                } else {
721                    anchor - vec2(GRID_SIZE, 0.0)
722                };
723                builder.add_h_channel(anchor_pos, COST_ZERO);
724            });
725        }
726        for &p in seed_points {
727            builder.add_seed_point(p);
728        }
729        builder.build_closed()
730    }
731
732    /// A lattice over `region`, with every wire not being `routed` laid in as
733    /// occupancy before anything routes — so the wires that do route land
734    /// last, around the rest.
735    fn healing_lattice(
736        &self,
737        within: Region,
738        seeding: &Seeding<'_>,
739        routed: &HashSet<RouteId>,
740    ) -> ClosedRouter {
741        let seed_points = seeds_within(self, seeding, within);
742        let mut lattice = {
743            let _s = tracing::info_span!(
744                "closed_build",
745                seeds = seed_points.len(),
746                region = ?within
747            )
748            .entered();
749            self.build_router_within(within, seeding.hypo, &seed_points)
750        };
751        for &id in seeding.ids {
752            if routed.contains(&id) {
753                continue;
754            }
755            if let Some(geometry) = self.presentation.routes.get(&id) {
756                add_route_cost(
757                    &mut lattice,
758                    geometry.iter_edges().map(|(_, edge)| edge),
759                    WIRE_COST,
760                );
761            }
762        }
763        lattice
764    }
765
766    /// Route `wires` through a lattice over `region`, everything else laid in
767    /// as occupancy first; then route the ones that fell back again, through a
768    /// lattice over the whole scope. A region too small to detour in is routine
769    /// and says nothing about the wire, so a wire is unresolved only when the
770    /// sheet itself holds no path — what it would have been before regions.
771    ///
772    /// `route` lays one wire in the lattice it is given and says whether it
773    /// found a path. One that finds none over the scope keeps its fallback L:
774    /// that is what it settles on.
775    fn heal<W>(
776        &mut self,
777        wires: Vec<(RouteId, W)>,
778        region: Rect,
779        seeding: &Seeding<'_>,
780        mut route: impl FnMut(RouteId, &W, &mut RouteGeometry, &mut ClosedRouter) -> Resolution,
781    ) {
782        let mut attempt = |drawing: &mut Self, within: Region, wires: Vec<(RouteId, W)>| {
783            let routed: HashSet<RouteId> = wires.iter().map(|(id, _)| *id).collect();
784            let mut lattice = drawing.healing_lattice(within, seeding, &routed);
785            let mut fell_back = Vec::new();
786            for (id, wire) in wires {
787                let geometry = drawing.presentation.routes.entry(id).or_default();
788                if route(id, &wire, geometry, &mut lattice) == Resolution::Fallback {
789                    fell_back.push((id, wire));
790                }
791            }
792            fell_back
793        };
794        let fell_back = attempt(self, Region::Within(region), wires);
795        if !fell_back.is_empty() {
796            let _s = tracing::info_span!("heal_whole_scope", wires = fell_back.len()).entered();
797            attempt(self, Region::WholeScope, fell_back);
798        }
799    }
800
801    /// The obstacle rectangles of this scope, at previewed geometry for any
802    /// shape in `overrides`. These are exactly the rects
803    /// [`Self::build_closed_router`] registers as router blocks, wrapped for the
804    /// cheap "does a straight leg cross a block?" test used by reconstruction,
805    /// so a leg judged clear here is the same one the router would leave
806    /// straight — without building the routing graph.
807    fn obstacle_rects(&self, overrides: &[(ShapeId, Rect)]) -> Obstacles {
808        let rects = self
809            .routing_shapes()
810            .into_iter()
811            .map(|(sid, shape)| {
812                obstacle_rect(override_rect(sid, overrides).unwrap_or_else(|| shape.gui_rect()))
813            })
814            .collect();
815        Obstacles::new(rects)
816    }
817
818    /// The waypoints each route must route *around* this preview frame: the
819    /// straddle trim (the moved side of a route with exactly one end riding the
820    /// gesture) plus the backtracking prune, both computed against the frame's
821    /// hypothetical endpoints — previewed, never applied. A route fully inside the
822    /// gesture keeps its skeleton byte-identical: it previews through rigidly
823    /// offset waypoints and, moving rigidly, can't introduce a reversal. Both
824    /// rules are the emitters' own ([`crate::edit::geometry`]), read from the
825    /// preview's side.
826    fn preview_exclusions(
827        &self,
828        endpoints: &[(RouteId, Pos2, Pos2)],
829        spec: &PreviewSpec<'_>,
830    ) -> HashMap<RouteId, PreviewExclusions> {
831        endpoints
832            .iter()
833            .filter_map(|&(id, s, e)| {
834                let route = self.route(id)?;
835                let stored = route.waypoints.clone();
836                let (start_in, finish_in) = ((spec.inside)(route.from), (spec.inside)(route.to));
837                if start_in && finish_in {
838                    return None;
839                }
840                let mut excluded: HashSet<PathOrdinal> = match (start_in, finish_in) {
841                    (true, false) => trimmed_ordinals(&stored, RouteEnd::From),
842                    (false, true) => trimmed_ordinals(&stored, RouteEnd::To),
843                    _ => Vec::new(),
844                }
845                .into_iter()
846                .collect();
847                let working: Vec<Waypoint> = stored
848                    .iter()
849                    .enumerate()
850                    .filter(|(index, _)| !excluded.contains(&PathOrdinal::new(*index)))
851                    .map(|(_, wp)| *wp)
852                    .collect();
853                // The prune's ordinals index `working`; map them back through the
854                // surviving positions so both sets name the stored list.
855                let survivors: Vec<PathOrdinal> = (0..stored.len())
856                    .map(PathOrdinal::new)
857                    .filter(|o| !excluded.contains(o))
858                    .collect();
859                let doomed = backtracking_ordinals(&working, grid_point(s), grid_point(e));
860                let pruned = !doomed.is_empty();
861                excluded.extend(
862                    doomed
863                        .into_iter()
864                        .filter_map(|o| survivors.get(usize::from(o)).copied()),
865                );
866                (!excluded.is_empty()).then_some((id, PreviewExclusions { excluded, pruned }))
867            })
868            .collect()
869    }
870
871    /// Closed-graph route solver used by every path except live `RouteTool`
872    /// drawing: load, interactive commits, AND the drag/resize preview. It builds
873    /// the routing graph ONCE with all obstacle, endpoint, and waypoint geometry
874    /// (honoring `overrides` for previewed shapes and offsetting waypoints of
875    /// fully-dragged routes) seeded up front, then routes every route against it,
876    /// applying `WIRE_COST` occupancy by mutating existing edge weights in place —
877    /// so the graph is never rebuilt per route (the O(routes²) cost the old
878    /// per-route rebuild paid; see TUNING.md Finding 1).
879    ///
880    /// `pass` distinguishes a permanent edit (a drop, delete, paste, nudge —
881    /// the geometry is final) from a transient drag/resize preview (recomputed
882    /// every frame from stable state). On commit a changed route is
883    /// re-reconstructed per leg — its straight legs are kept and only the legs that
884    /// are now blocked or non-colinear are routed — and its corners are promoted
885    /// to waypoints; the backtracking prune really removes reversal corners. A
886    /// preview never touches the document: it routes through the stable waypoint
887    /// skeleton minus its *previewed* trims and prunes (so per-frame recomputation
888    /// never piles up derived corners), taking only immutable borrows.
889    #[tracing::instrument(level = "info", skip_all, fields(routes = self.scope_route_ids().len(), ripup = ripup.len()))]
890    fn route_update_closed(
891        &mut self,
892        ripup: &[RouteId],
893        hypo: GeometryOverrides<'_>,
894        pass: RoutePass<'_>,
895        solving: Solving<'_>,
896    ) -> Promoted {
897        let ids = self.scope_route_ids();
898        // Anchors only for the wires this pass may re-solve. A wire it will not
899        // touch contributes its *geometry* as occupancy, which is already in the
900        // presentation — resolving where its pins are would answer a question
901        // nobody asks.
902        let endpoints: Vec<(RouteId, Pos2, Pos2)> = {
903            let _s = tracing::info_span!("closed_anchors").entered();
904            ids.iter()
905                .filter(|&&id| solving.may_solve(id))
906                .filter_map(|&id| {
907                    let route = self.route(id)?;
908                    let s = self.anchor_pos_overridden(route.from, hypo)?;
909                    let e = self.anchor_pos_overridden(route.to, hypo)?;
910                    Some((id, s, e))
911                })
912                .collect()
913        };
914        // Preview: work out each route's previewed exclusions BEFORE seeding, so
915        // the graph matches the steady state of the old mutating flow (where a
916        // trimmed or pruned corner really was gone by the next frame's build).
917        let exclusions: HashMap<RouteId, PreviewExclusions> = match &pass {
918            RoutePass::Preview(spec) => self.preview_exclusions(&endpoints, spec),
919            RoutePass::Commit => HashMap::new(),
920        };
921        // A route riding entirely inside a group drag seeds its waypoints at the
922        // drag's offset, because that is where `reroute_preview_closed` will
923        // route them. Worked out here so seeding needs neither the pass nor its
924        // spec.
925        let offsets: HashMap<RouteId, GridVec> = match &pass {
926            RoutePass::Preview(spec) => ids
927                .iter()
928                .filter_map(|&id| {
929                    let route = self.route(id)?;
930                    ((spec.inside)(route.from) && (spec.inside)(route.to))
931                        .then_some((id, spec.grid_delta))
932                })
933                .collect(),
934            RoutePass::Commit => HashMap::new(),
935        };
936        let previewed = matches!(pass, RoutePass::Preview(_));
937        let obstacles = self.obstacle_rects(hypo.rects);
938        let seeding = Seeding {
939            hypo,
940            ids: &ids,
941            endpoints: &endpoints,
942            exclusions: &exclusions,
943            offsets: &offsets,
944        };
945        let mut promoted: Promoted = Vec::new();
946        let mut resolved: Vec<RouteId> = Vec::new();
947        // An illegal L never holds — its legs run through blocks — so a pass
948        // that raised one routes it again.
949        let holding: HashSet<RouteId> = endpoints
950            .iter()
951            .filter(|&&(id, start, end)| {
952                self.route_geometry(id)
953                    .is_some_and(|geometry| still_holds(geometry, start, end, &obstacles))
954                    && !ripup.contains(&id)
955            })
956            .map(|&(id, ..)| id)
957            .collect();
958        match pass {
959            RoutePass::Commit => {
960                // Wires the straighten pass could not take verbatim. Only these
961                // need a lattice, and on settled geometry there are usually
962                // none: a nudge routes **zero** legs.
963                let mut deferred: Vec<(RouteId, (Endpoints, Vec<Waypoint>))> = Vec::new();
964                {
965                    let _s = tracing::info_span!("closed_route_loop").entered();
966                    let (indexed, presentation) = self.split();
967                    let doc = indexed.doc;
968                    for &(id, anchor_start, anchor_end) in &endpoints {
969                        let Some(route) = doc.route(&id) else {
970                            continue;
971                        };
972                        let geometry = presentation.routes.entry(id).or_default();
973                        // Drop any waypoint the wire doubles back on (a 180°
974                        // turn strict waypoint routing can leave behind), then
975                        // force a re-route so the reversal geometry is rebuilt
976                        // clean.
977                        let doomed = backtracking_ordinals(
978                            &route.waypoints,
979                            grid_point(anchor_start),
980                            grid_point(anchor_end),
981                        );
982                        let pruned = !doomed.is_empty();
983                        let stored = if pruned {
984                            pruned_waypoints(&route.waypoints, &doomed)
985                        } else {
986                            route.waypoints.clone()
987                        };
988                        if !pruned && holding.contains(&id) {
989                            // An "unchanged" route may still carry a stale
990                            // waypoint set from a live drag preview: the preview
991                            // rewrites the edges to track the moved shape but
992                            // leaves the waypoints behind, and its snapped
993                            // endpoints then match here so the route reads as
994                            // unchanged. Re-derive the waypoints from the
995                            // (correct) edges so the "every corner is a
996                            // waypoint" invariant holds and the saved geometry
997                            // matches what a fresh load rebuilds.
998                            promoted.push((id, promote_corners_to_waypoints(&stored, geometry)));
999                            continue;
1000                        }
1001                        // Keep every straight leg; a leg that is blocked or
1002                        // non-colinear needs the lattice, so set the wire aside
1003                        // rather than build one for a pass that may not use it.
1004                        let ends = Endpoints {
1005                            start: grid_point(anchor_start),
1006                            end: grid_point(anchor_end),
1007                        };
1008                        match straighten_route(&stored, geometry, ends, obstacles.reroute_blocked())
1009                        {
1010                            Some(corners) => promoted.push((id, corners)),
1011                            None => deferred.push((id, (ends, stored))),
1012                        }
1013                    }
1014                }
1015                if !deferred.is_empty() {
1016                    let _s =
1017                        tracing::info_span!("closed_route_deferred", deferred = deferred.len())
1018                            .entered();
1019                    let mut settled: BTreeMap<RouteId, Vec<Waypoint>> = BTreeMap::new();
1020                    let region = healing_region(deferred.iter().flat_map(|(_, (ends, stored))| {
1021                        [ends.start, ends.end]
1022                            .into_iter()
1023                            .chain(stored.iter().map(|wp| wp.pos))
1024                    }));
1025                    self.heal(
1026                        deferred,
1027                        region,
1028                        &seeding,
1029                        |id, (ends, stored), geometry, lattice| {
1030                            let laid = reconstruct_route(
1031                                stored,
1032                                geometry,
1033                                *ends,
1034                                obstacles.reroute_blocked(),
1035                                lattice,
1036                            );
1037                            // A retry replaces the attempt before it.
1038                            settled.insert(id, laid.corners);
1039                            laid.resolution
1040                        },
1041                    );
1042                    promoted.extend(settled);
1043                }
1044            }
1045            RoutePass::Preview(spec) => {
1046                // A wire whose geometry still holds keeps it and is occupancy;
1047                // the rest are re-routed through a lattice over the region they
1048                // span, never the sheet. One riding entirely inside a group
1049                // drag previews through its waypoints offset rigidly.
1050                let rerouting: Vec<(RouteId, Rerouting)> = endpoints
1051                    .iter()
1052                    .filter(|&&(id, ..)| {
1053                        let pruned = exclusions.get(&id).is_some_and(|x| x.pruned);
1054                        pruned || !holding.contains(&id)
1055                    })
1056                    .filter_map(|&(id, s, e)| {
1057                        let route = self.route(id)?;
1058                        let ends = Endpoints {
1059                            start: grid_point(s),
1060                            end: grid_point(e),
1061                        };
1062                        let rigid = ((spec.inside)(route.from) && (spec.inside)(route.to))
1063                            .then_some(spec.grid_delta);
1064                        let stored = route.waypoints.clone();
1065                        Some((
1066                            id,
1067                            Rerouting {
1068                                ends,
1069                                stored,
1070                                rigid,
1071                            },
1072                        ))
1073                    })
1074                    .collect();
1075                if !rerouting.is_empty() {
1076                    let region = healing_region(rerouting.iter().flat_map(|(id, wire)| {
1077                        [wire.ends.start, wire.ends.end]
1078                            .into_iter()
1079                            .chain(seeded_waypoints(self, &seeding, *id))
1080                    }));
1081                    let _s = tracing::info_span!("closed_route_loop", routes = rerouting.len())
1082                        .entered();
1083                    // Drawn somewhere the document does not put it, so the
1084                    // cull set and the take-back have to be told.
1085                    resolved.extend(rerouting.iter().map(|&(id, _)| id));
1086                    let no_exclusions = PreviewExclusions::default();
1087                    self.heal(
1088                        rerouting,
1089                        region,
1090                        &seeding,
1091                        |id, wire, geometry, lattice| match wire.rigid {
1092                            Some(delta) => reroute_preview_closed(
1093                                &wire.stored,
1094                                geometry,
1095                                wire.ends,
1096                                delta,
1097                                lattice,
1098                            ),
1099                            None => reroute_preview_excluding(
1100                                &wire.stored,
1101                                geometry,
1102                                wire.ends,
1103                                &exclusions.get(&id).unwrap_or(&no_exclusions).excluded,
1104                                lattice,
1105                            ),
1106                        },
1107                    );
1108                }
1109            }
1110        }
1111        if previewed {
1112            let scope = self.scope_path().clone();
1113            self.presentation
1114                .routes_previewed(resolved.iter().copied(), &scope);
1115            self.preview_routes(resolved);
1116        }
1117        promoted
1118    }
1119
1120    /// Is this wire one of the set being reconstructed? A wire with no
1121    /// geometry yet has never been reconstructed, so a rectangle always
1122    /// reaches it.
1123    fn route_reaches(&self, id: RouteId, reconstructing: Reconstructing<'_>) -> bool {
1124        match reconstructing {
1125            Reconstructing::Everything => true,
1126            Reconstructing::These(these) => these.contains(&id),
1127            Reconstructing::Within {
1128                disturbed: region,
1129                previewed,
1130            } => {
1131                if previewed.contains(&id) {
1132                    return true;
1133                }
1134                let Some(geometry) = self.route_geometry(id) else {
1135                    return true;
1136                };
1137                region.contains(geometry.start_pos())
1138                    || region.contains(geometry.end_pos())
1139                    || geometry.iter_edges().any(|(_, edge)| {
1140                        region.contains(px_point(edge.start)) || region.contains(px_point(edge.end))
1141                    })
1142            }
1143        }
1144    }
1145
1146    /// Reconstruct every route's edge geometry from its stored corner waypoints
1147    /// WITHOUT globally re-routing. Straight (axis-aligned, unobstructed) legs are
1148    /// drawn directly; only non-colinear or obstacle-crossing legs are routed, and
1149    /// then only for the routes that need it. A clean block (every leg straight) is
1150    /// rebuilt with zero pathfinding, so stored/hand-adjusted geometry is preserved
1151    /// exactly. This is the load-time reconstruction; the interactive mutation
1152    /// path reuses the same per-leg policy.
1153    pub fn reconstruct_routes(&mut self) {
1154        self.reconstruct_routes_reaching(Reconstructing::Everything);
1155    }
1156
1157    /// The same reconstruction, confined to the wires `reconstructing` names.
1158    ///
1159    /// A wire it does not reach is drawn from geometry that is still correct —
1160    /// nothing moved near it — so re-deriving it would spend the sheet's time
1161    /// to arrive back where it started. A commit that knows what it disturbed
1162    /// passes that rectangle; an open, an undo or a foreign commit knows
1163    /// nothing and passes [`Reconstructing::Everything`]; a preview's next
1164    /// borrow takes back [`Reconstructing::These`] wires it drew.
1165    pub(crate) fn reconstruct_routes_reaching(&mut self, reconstructing: Reconstructing<'_>) {
1166        let ids: Vec<RouteId> = self
1167            .scope_route_ids()
1168            .into_iter()
1169            .filter(|&id| self.route_reaches(id, reconstructing))
1170            .collect();
1171        if ids.is_empty() {
1172            return;
1173        }
1174        let endpoints: Vec<(RouteId, GridPoint, GridPoint)> = {
1175            let _s = tracing::info_span!("anchors", routes = ids.len()).entered();
1176            ids.iter()
1177                .filter_map(|&id| {
1178                    let route = self.route(id)?;
1179                    let s = self.anchor_pos_overridden(route.from, GeometryOverrides::default())?;
1180                    let e = self.anchor_pos_overridden(route.to, GeometryOverrides::default())?;
1181                    Some((id, grid_point(s), grid_point(e)))
1182                })
1183                .collect()
1184        };
1185        let obstacles = {
1186            let _s = tracing::info_span!("obstacles").entered();
1187            self.obstacle_rects(&[])
1188        };
1189
1190        // Phase 1: straighten every leg we can; defer routes that still need the
1191        // router (some leg blocked or non-colinear).
1192        let mut deferred: Vec<(RouteId, (Endpoints, Vec<Waypoint>))> = Vec::new();
1193        {
1194            let _s = tracing::info_span!("straighten", routes = endpoints.len()).entered();
1195            let (indexed, presentation) = self.split();
1196            let doc = indexed.doc;
1197            for &(id, s, e) in &endpoints {
1198                let Some(route) = doc.route(&id) else {
1199                    continue;
1200                };
1201                let geometry = presentation.routes.entry(id).or_default();
1202                let ends = Endpoints { start: s, end: e };
1203                if straighten_route(&route.waypoints, geometry, ends, obstacles.keep_blocked())
1204                    .is_none()
1205                {
1206                    deferred.push((id, (ends, route.waypoints.clone())));
1207                }
1208            }
1209        }
1210
1211        // Phase 2: only if something needs routing, heal it — through a lattice
1212        // over the rectangle it spans, as a commit does: a load that defers three
1213        // wires has no more reason to lattice the sheet than a commit that defers
1214        // three. A first settle defers everything, so its region is the sheet.
1215        if !deferred.is_empty() {
1216            let _s = tracing::info_span!("route_deferred", deferred = deferred.len()).entered();
1217            let anchors: Vec<(RouteId, Pos2, Pos2)> = endpoints
1218                .iter()
1219                .map(|&(id, s, e)| (id, px_point(s), px_point(e)))
1220                .collect();
1221            let (exclusions, offsets) = (HashMap::new(), HashMap::new());
1222            let seeding = Seeding {
1223                hypo: GeometryOverrides::default(),
1224                ids: &ids,
1225                endpoints: &anchors,
1226                exclusions: &exclusions,
1227                offsets: &offsets,
1228            };
1229            let region = healing_region(deferred.iter().flat_map(|(_, (ends, stored))| {
1230                [ends.start, ends.end]
1231                    .into_iter()
1232                    .chain(stored.iter().map(|wp| wp.pos))
1233            }));
1234            self.heal(
1235                deferred,
1236                region,
1237                &seeding,
1238                |_, (ends, stored), geometry, lattice| {
1239                    reconstruct_route(stored, geometry, *ends, obstacles.keep_blocked(), lattice)
1240                        .resolution
1241                },
1242            );
1243        }
1244    }
1245
1246    /// The route's resolved, snapped anchor endpoints against the settled
1247    /// document — the ends every route-edit relayout runs between.
1248    fn route_ends(&self, id: RouteId) -> Option<Endpoints> {
1249        let route = self.route(id)?;
1250        let s = self.anchor_pos_overridden(route.from, GeometryOverrides::default())?;
1251        let e = self.anchor_pos_overridden(route.to, GeometryOverrides::default())?;
1252        Some(Endpoints {
1253            start: grid_point(s),
1254            end: grid_point(e),
1255        })
1256    }
1257
1258    /// Re-lay a single route's edges directly from `corners`, WITHOUT the
1259    /// router — straight legs stay straight and a diagonal leg gets an
1260    /// L-bend, so the wire goes exactly where it was put and never
1261    /// autoroutes. Returns the corner list the relayed geometry promotes and
1262    /// each captured label anchor re-projected onto it: the pair the route
1263    /// edit's commit pushes as ops.
1264    fn relay_route(
1265        &mut self,
1266        id: RouteId,
1267        corners: &[GridPoint],
1268        stored: &[Waypoint],
1269        anchors: &[(RouteLabelId, Pos2)],
1270    ) -> Option<RelaidRoute> {
1271        let ends = self.route_ends(id)?;
1272        let geometry = self.presentation.routes.entry(id).or_default();
1273        reconstruct_corners_direct(corners, geometry, ends);
1274        let relayed = reanchored(geometry, anchors);
1275        let promoted = promote_corners_to_waypoints(stored, geometry);
1276        // The relay ignores obstacles on purpose, so what it left is not what
1277        // the document implies — a drag that ended where it started commits
1278        // nothing, and this is what takes its preview back.
1279        let scope = self.scope_path().clone();
1280        self.presentation.routes_previewed([id], &scope);
1281        Some((promoted, relayed))
1282    }
1283
1284    /// Plan a route edit: classify where each cursor's working corner lands on
1285    /// route `id` — a grabbed existing corner within half a cell is reused,
1286    /// anything else inserts in path order — and capture each label's world
1287    /// anchor. Read-only: [`Self::preview_route_edit`] renders the plan per
1288    /// drag frame and [`Self::commit_route_edit`] applies it on release, so
1289    /// between them the document is never written. Cursors must sit at least a
1290    /// cell apart (the edge drag's boundary seeds guarantee it); two cursors
1291    /// collapsing onto one working corner abort the plan.
1292    pub fn route_edit_session(&self, id: RouteId, cursors: &[Pos2]) -> Option<RouteEditSession> {
1293        let wire = self.auto_route(id)?;
1294        let geometry = self.route_geometry(id)?;
1295        // Planned against a working copy, so the second cursor sees the first
1296        // cursor's insertion — the same list `corners` replays the slots
1297        // against, built by the same function so the two cannot mean
1298        // different things by an index into it.
1299        let mut working = keyed_corners(&wire.route.waypoints, |wp| px_point(wp.pos));
1300        let mut slots = Vec::with_capacity(cursors.len());
1301        for &cursor in cursors {
1302            let hit = working
1303                .iter()
1304                .find(|(_, pos)| pos.distance(cursor) <= GRID_SIZE * 0.5)
1305                .copied();
1306            match hit {
1307                Some((Some(ordinal), _)) => slots.push(WaypointSlot::Existing(ordinal)),
1308                Some((None, _)) => return None,
1309                None => {
1310                    let d_new = geometry.distance_along(cursor);
1311                    let index = working
1312                        .iter()
1313                        .filter(|&&(_, pos)| geometry.distance_along(pos) < d_new)
1314                        .count();
1315                    let pos = px_point(grid_point(cursor));
1316                    working.insert(index, (None, pos));
1317                    slots.push(WaypointSlot::InsertAt(index));
1318                }
1319            }
1320        }
1321        Some(RouteEditSession {
1322            slots,
1323            anchors: label_anchors(&wire.labels, geometry),
1324        })
1325    }
1326
1327    /// Relay route `id` directly through the session's hypothetical corner
1328    /// list at `cursors` — the pure per-frame preview of a route edit. Writes
1329    /// derived geometry only; the authored route and its labels (the drag
1330    /// overlay draws them pinned at the session's anchors) are untouched.
1331    pub fn preview_route_edit(
1332        &mut self,
1333        _phase: &crate::widget::PreviewPhase,
1334        id: RouteId,
1335        session: &RouteEditSession,
1336        cursors: &[Pos2],
1337    ) {
1338        let Some(ends) = self.route_ends(id) else {
1339            return;
1340        };
1341        let Some(route) = self.route(id) else {
1342            return;
1343        };
1344        let corners: Vec<GridPoint> = session
1345            .corners(&route.waypoints, cursors)
1346            .into_iter()
1347            .map(|(pos, _)| pos)
1348            .collect();
1349        let routes = &mut self.presentation.routes;
1350        let geometry = routes.entry(id).or_default();
1351        reconstruct_corners_direct(&corners, geometry, ends);
1352        let scope = self.scope_path().clone();
1353        self.presentation.routes_previewed([id], &scope);
1354    }
1355
1356    /// Finalize a route edit — the drag's one document write. Relays the
1357    /// session's working corners (locked: the user placed them), promotes the
1358    /// resulting corners so the hand-placed geometry persists, and re-anchors
1359    /// each label to its captured drag-start position.
1360    pub fn commit_route_edit(&mut self, id: RouteId, session: &RouteEditSession, cursors: &[Pos2]) {
1361        let Some(route) = self.route(id) else {
1362            return;
1363        };
1364        let edited: Vec<Waypoint> = session
1365            .corners(&route.waypoints, cursors)
1366            .into_iter()
1367            .map(|(pos, lock)| Waypoint {
1368                pos,
1369                locked: lock.is_locked(),
1370            })
1371            .collect();
1372        let corners: Vec<GridPoint> = edited.iter().map(|wp| wp.pos).collect();
1373        let anchors = session.anchors.clone();
1374        let Some((waypoints, relayed)) = self.relay_route(id, &corners, &edited, &anchors) else {
1375            return;
1376        };
1377        self.author("commit_route_edit", |indexed, sink| {
1378            crate::edit::geometry::commit_route_edit(
1379                indexed.doc,
1380                crate::edit::geometry::RouteEdit {
1381                    route: id,
1382                    waypoints,
1383                    anchors: &relayed,
1384                },
1385                sink,
1386            );
1387        });
1388    }
1389
1390    /// Rip up a route entirely and autoroute it fresh: drop every waypoint so the
1391    /// endpoints re-route on Dijkstra cost alone, with no user-placed corners.
1392    /// The gesture's solve rider re-promotes the corners the router finds.
1393    pub fn reroute(&mut self, id: RouteId) {
1394        self.author("reroute", |indexed, sink| {
1395            crate::edit::geometry::reroute(indexed, id, &[], sink);
1396        });
1397        self.rip_up(&[id]);
1398    }
1399
1400    /// Rip up and autoroute every wire of THIS scope with an endpoint on
1401    /// `block` (either end): drop their user waypoints and let the rider
1402    /// re-solve them. The block-wide analogue of [`Self::reroute`].
1403    ///
1404    /// Scoped deliberately: a block's pins are its boundary ports, so the
1405    /// wires *inside* it end on the same ids. Those are drawn one level
1406    /// down, the rider does not re-solve them, and tearing up a path nobody
1407    /// re-lays would just lose it.
1408    pub fn reroute_block(&mut self, block: blockworx_doc::id::BlockId) {
1409        let on_block: HashSet<PinId> = self
1410            .block_pins(crate::path::Scope::Block(block))
1411            .into_iter()
1412            .map(|(id, _)| id)
1413            .collect();
1414        let ids: Vec<RouteId> = self
1415            .scope_route_ids()
1416            .into_iter()
1417            .filter(|&id| {
1418                self.route(id)
1419                    .is_some_and(|r| on_block.contains(&r.from) || on_block.contains(&r.to))
1420            })
1421            .collect();
1422        for &id in &ids {
1423            self.author("reroute_block", |indexed, sink| {
1424                crate::edit::geometry::reroute(indexed, id, &[], sink);
1425            });
1426        }
1427        self.rip_up(&ids);
1428    }
1429
1430    /// Forget what these wires were solved to, so the next pass re-solves
1431    /// them from nothing rather than keeping the path the user just tore up.
1432    /// The rip-up is the one gesture that must NOT be handed its own previous
1433    /// answer, and dropped geometry is how the solve is told so.
1434    fn rip_up(&mut self, ids: &[RouteId]) {
1435        for id in ids {
1436            self.presentation.routes.remove(id);
1437        }
1438        let scope = self.scope_path().clone();
1439        self.presentation
1440            .routes_previewed(ids.iter().copied(), &scope);
1441    }
1442
1443    /// Closed-router view of the current scope's geometry and existing-route
1444    /// occupancy, for tools that pathfind against it without mutating the
1445    /// document (`RouteTool`). The graph is built ONCE (obstacles + every existing
1446    /// route's endpoints and waypoints + `extra_seeds`), then existing routes are
1447    /// re-applied as `WIRE_COST` occupancy in place — no per-route rebuild.
1448    /// `extra_seeds` seeds the points the caller will route through (an in-progress
1449    /// route's endpoints/waypoints) so they exist as graph nodes.
1450    pub fn scratch_closed_router(&self, extra_seeds: &[Point]) -> ClosedRouter {
1451        let ids = self.scope_route_ids();
1452        let mut seeds: Vec<Point> = Vec::new();
1453        for &id in &ids {
1454            let Some(route) = self.route(id) else {
1455                continue;
1456            };
1457            if let Some(s) = self.anchor_pos_overridden(route.from, GeometryOverrides::default()) {
1458                seeds.push(snap_to_grid(s).into());
1459            }
1460            if let Some(e) = self.anchor_pos_overridden(route.to, GeometryOverrides::default()) {
1461                seeds.push(snap_to_grid(e).into());
1462            }
1463            for wp in route.waypoints.clone() {
1464                seeds.push(wp.pos.into());
1465            }
1466        }
1467        seeds.extend_from_slice(extra_seeds);
1468        let mut router = self.build_closed_router(GeometryOverrides::default(), &seeds);
1469        for &id in &ids {
1470            if let Some(geometry) = self.presentation.routes.get(&id) {
1471                add_route_cost(
1472                    &mut router,
1473                    geometry.iter_edges().map(|(_, e)| e),
1474                    WIRE_COST,
1475                );
1476            }
1477        }
1478        router
1479    }
1480
1481    /// Where a gesture on `foreground` routes, for the diagnostic overlay: the
1482    /// healing region around every wire it raises, and the lattice the router
1483    /// builds there with everything else laid in as occupancy.
1484    ///
1485    /// The largest bound such a gesture uses: a pass heals only the raised
1486    /// wires that need routing, and the region around those is inside this
1487    /// one. `None` when the foreground raises no wire of this scope.
1488    pub fn healing_bound(&self, foreground: &Foreground) -> Option<(Rect, Vec<Mark>)> {
1489        let ids = self.scope_route_ids();
1490        let raised: Vec<(RouteId, Endpoints)> = ids
1491            .iter()
1492            .filter(|&&id| foreground.holds_route(id))
1493            .filter_map(|&id| Some((id, self.route_ends(id)?)))
1494            .collect();
1495        if raised.is_empty() {
1496            return None;
1497        }
1498        let endpoints: Vec<(RouteId, Pos2, Pos2)> = raised
1499            .iter()
1500            .map(|&(id, ends)| (id, px_point(ends.start), px_point(ends.end)))
1501            .collect();
1502        let (exclusions, offsets) = (HashMap::new(), HashMap::new());
1503        let seeding = Seeding {
1504            hypo: GeometryOverrides::default(),
1505            ids: &ids,
1506            endpoints: &endpoints,
1507            exclusions: &exclusions,
1508            offsets: &offsets,
1509        };
1510        let region = healing_region(raised.iter().flat_map(|&(id, ends)| {
1511            [ends.start, ends.end]
1512                .into_iter()
1513                .chain(seeded_waypoints(self, &seeding, id))
1514        }));
1515        let routed: HashSet<RouteId> = raised.iter().map(|&(id, _)| id).collect();
1516        let lattice = self.healing_lattice(Region::Within(region), &seeding, &routed);
1517        Some((region, lattice.debug_marks()))
1518    }
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523    use blockworx_doc::{
1524        block_model::RouteUpdate,
1525        opcode::{Crud, OpCodes},
1526    };
1527    use blockworx_doc::{
1528        fixtures::{block_id, pin_id, route_id},
1529        geometry::{GridPoint, PinSlot, Waypoint},
1530        id::RouteId,
1531        values::PinSide,
1532    };
1533    use blockworx_geom::{Rect, pos2, vec2};
1534
1535    use super::{GeometryOverrides, Region, healing_region};
1536    use crate::{
1537        edit::{
1538            geometry::{PinMove, RouteEnd, trimmed_approach},
1539            naming::InterfaceLock,
1540        },
1541        grid::{GRID_SIZE, grid_point, px_point},
1542        path::Scope,
1543        presentation::{RouteEdge, RouteGeometry},
1544        shape::{BaseShape, ShapeId, ShapeRef},
1545        widget::{
1546            auto_route::Wire,
1547            drawing::Drawing,
1548            test_fixtures::{
1549                self as fx, Scene, route_passes_through, two_blocks_with_a_routed_waypoint,
1550            },
1551        },
1552    };
1553
1554    /// The plan and its replay have to agree about what an insertion index
1555    /// counts. Two cursors that both make new corners are the case where
1556    /// they can disagree: the second is planned against a list the first has
1557    /// already grown, so a replay that rebuilt the list differently — or
1558    /// applied the slots in another order — would put the second corner
1559    /// somewhere the preview never drew it.
1560    #[test]
1561    fn a_two_corner_plan_replays_in_the_order_it_was_planned() {
1562        let mut scene = two_blocks_with_a_routed_waypoint();
1563        fx::reconstruct(&mut scene);
1564        let route = route_id(5);
1565        let drawing = scene.drawing();
1566        let geometry = drawing
1567            .route_geometry(route)
1568            .expect("the fixture's wire is reconstructed")
1569            .clone();
1570        // Two points on the wire, a long way apart and neither on the stored
1571        // corner, so both slots are insertions rather than grabs. Taken as
1572        // edge midpoints, which are on the polyline by construction — a
1573        // guessed point would miss the wire and plan nothing.
1574        let edges: Vec<_> = geometry.iter_edges().map(|(_, e)| e.clone()).collect();
1575        assert!(
1576            edges.len() >= 2,
1577            "precondition: the wire needs two edges to insert around",
1578        );
1579        let midpoint =
1580            |e: &RouteEdge| px_point(e.start) + (px_point(e.end) - px_point(e.start)) * 0.5;
1581        let (early, late) = (midpoint(&edges[0]), midpoint(&edges[edges.len() - 1]));
1582        let stored = drawing
1583            .auto_route(route)
1584            .expect("the wire is in scope")
1585            .route
1586            .waypoints
1587            .clone();
1588        assert_eq!(
1589            stored.len(),
1590            1,
1591            "precondition: one stored corner to insert around",
1592        );
1593
1594        let session = drawing
1595            .route_edit_session(route, &[early, late])
1596            .expect("two well-separated cursors plan");
1597        let corners = session.corners(&stored, &[early, late]);
1598
1599        assert_eq!(
1600            corners.len(),
1601            3,
1602            "one stored corner plus two insertions: {corners:?}",
1603        );
1604        let locked: Vec<usize> = corners
1605            .iter()
1606            .enumerate()
1607            .filter(|(_, (_, lock))| *lock == InterfaceLock::Locked)
1608            .map(|(i, _)| i)
1609            .collect();
1610        assert_eq!(
1611            locked.len(),
1612            2,
1613            "the two the user placed come out locked: {corners:?}",
1614        );
1615        // Planned early-then-late, so the replay must keep that order: the
1616        // earlier cursor's corner sits before the later one's.
1617        let (first, second) = (corners[locked[0]].0, corners[locked[1]].0);
1618        assert!(
1619            geometry.distance_along(px_point(first)) < geometry.distance_along(px_point(second)),
1620            "the replay reordered the planned corners: {first:?} then {second:?}",
1621        );
1622    }
1623
1624    /// The walls of a ring around block `1`, by id, in whole cells. The west
1625    /// wall is far from the wire, which runs east.
1626    const RING: [(u32, (i32, i32, i32, i32)); 4] = [
1627        (10, (0, 0, 44, 4)),
1628        (11, (0, 52, 44, 4)),
1629        (12, (40, 4, 4, 48)),
1630        (WEST_WALL, (0, 4, 4, 48)),
1631    ];
1632    const WEST_WALL: u32 = 13;
1633
1634    /// Block `1` inside the `walls` of [`RING`] that are listed, wired east to
1635    /// block `2` outside it, whose top sits at `far_top` cells: pins `3`/`4`,
1636    /// route `5`.
1637    fn walled_in_wire(walls: &[u32], far_top: i32) -> Scene {
1638        let mut ops = vec![
1639            fx::block_in(1, Scope::Root, fx::cells(26, 26, 4, 4)),
1640            fx::block_in(2, Scope::Root, fx::cells(80, far_top, 4, 4)),
1641        ];
1642        for (n, (x, y, w, h)) in RING {
1643            if walls.contains(&n) {
1644                ops.push(fx::block_in(n, Scope::Root, fx::cells(x, y, w, h)));
1645            }
1646        }
1647        ops.extend([
1648            fx::pin(3, 1, PinSide::East, 0),
1649            fx::pin(4, 2, PinSide::West, 0),
1650            fx::route(5, Scope::Root, 3, 4, &[]),
1651        ]);
1652        let mut scene = Scene::new(ops);
1653        fx::reconstruct(&mut scene);
1654        scene
1655    }
1656
1657    /// The walls of [`RING`] `geometry` runs through.
1658    fn walls_crossed(geometry: &RouteGeometry, walls: &[u32]) -> Vec<u32> {
1659        RING.iter()
1660            .filter(|(n, _)| walls.contains(n))
1661            .filter(|&&(_, (x, y, w, h))| {
1662                geometry.iter_edges().any(|(_, e)| {
1663                    let (x0, x1) = (e.start.x.min(e.end.x), e.start.x.max(e.end.x));
1664                    let (y0, y1) = (e.start.y.min(e.end.y), e.start.y.max(e.end.y));
1665                    x0 < x + w && x < x1.max(x0 + 1) && y0 < y + h && y < y1.max(y0 + 1)
1666                })
1667            })
1668            .map(|&(n, _)| n)
1669            .collect()
1670    }
1671
1672    /// Every routing shape of the scene's scope, for asking where wires cross
1673    /// them.
1674    fn routing_shape_ids(drawing: &Drawing<'_>) -> Vec<ShapeId> {
1675        drawing
1676            .routing_shapes()
1677            .into_iter()
1678            .map(|(id, _)| id)
1679            .collect()
1680    }
1681
1682    const ALL_WALLS: [u32; 4] = [10, 11, 12, WEST_WALL];
1683    const EAST_WALL: u32 = 12;
1684
1685    /// A wire the whole sheet holds no path for settles on its fallback L: the
1686    /// gesture that raised it writes the L's corners, and the draw pass marks
1687    /// where the L runs through a block — not the wire's color, which an
1688    /// accent can share.
1689    #[test]
1690    fn an_unroutable_wire_settles_on_its_l_and_is_marked_where_it_crosses() {
1691        let rid = route_id(5);
1692        let mut scene = walled_in_wire(&ALL_WALLS, 40);
1693        assert!(
1694            stored_corners(&scene.drawing(), rid).is_empty(),
1695            "precondition: the wire starts with no corners"
1696        );
1697
1698        scene.commit(|drawing| {
1699            drawing.move_shape(ShapeId::Rect(block_id(2)), vec2(0.0, GRID_SIZE));
1700        });
1701
1702        let corners = stored_corners(&scene.drawing(), rid);
1703        assert_eq!(corners.len(), 1, "the L's one bend is written: {corners:?}");
1704        let geometry = fx::geometry(&scene, rid);
1705        assert_eq!(walls_crossed(geometry, &ALL_WALLS), vec![EAST_WALL]);
1706        let drawing = scene.drawing();
1707        let marks = drawing
1708            .standing_conflicts(&routing_shape_ids(&drawing))
1709            .crossings;
1710        let (x, y, w, h) = RING[2].1;
1711        let east = fx::cells(x, y, w, h);
1712        assert!(!marks.is_empty(), "the illegal stretch is marked");
1713        assert!(
1714            marks.iter().all(|mark| mark.intersects(east)),
1715            "every mark sits on the wall the L runs through: {marks:?}"
1716        );
1717    }
1718
1719    /// An illegal wire costs nothing while nothing touches it: a commit that
1720    /// never raised it leaves it exactly as it is, even one that opens a path
1721    /// for it — and the commit that does raise it routes it.
1722    #[test]
1723    fn an_illegal_wire_is_rerouted_only_by_a_commit_that_raises_it() {
1724        let rid = route_id(5);
1725        let west = ShapeId::Rect(block_id(WEST_WALL));
1726        let mut scene = walled_in_wire(&ALL_WALLS, 40);
1727        scene.commit(|drawing| {
1728            drawing.move_shape(ShapeId::Rect(block_id(2)), vec2(0.0, GRID_SIZE));
1729        });
1730        let settled = route_edges(&scene.drawing(), rid);
1731        {
1732            let drawing = scene.drawing();
1733            let raised = crate::widget::foreground::Foreground::raising([west], &drawing);
1734            assert!(
1735                !raised.holds_route(rid),
1736                "precondition: deleting the west wall does not raise the wire"
1737            );
1738        }
1739
1740        scene.commit(|drawing| drawing.delete(crate::shape::Deletable::Shape(west)));
1741        assert!(
1742            !scene
1743                .committed()
1744                .iter()
1745                .any(|op| matches!(op, OpCodes::Route(id, _) if *id == rid)),
1746            "the far delete wrote the wire"
1747        );
1748        assert_eq!(route_edges(&scene.drawing(), rid), settled);
1749
1750        let east = ShapeId::Rect(block_id(EAST_WALL));
1751        scene.commit(|drawing| drawing.delete(crate::shape::Deletable::Shape(east)));
1752        let standing = [10, 11];
1753        assert_eq!(
1754            walls_crossed(fx::geometry(&scene, rid), &standing),
1755            Vec::<u32>::new()
1756        );
1757        let drawing = scene.drawing();
1758        assert!(
1759            drawing
1760                .standing_conflicts(&routing_shape_ids(&drawing))
1761                .crossings
1762                .is_empty(),
1763            "the rerouted wire is legal"
1764        );
1765    }
1766
1767    /// Wires the router laid are never marked: on a settled grid, ports and
1768    /// all, no wire runs through a routing shape.
1769    #[test]
1770    fn a_settled_grid_has_no_wire_conflicts() {
1771        let mut scene = fx::scale_scene(6);
1772        let drawing = scene.drawing();
1773        assert!(
1774            !drawing.scope_route_ids().is_empty(),
1775            "precondition: the grid has wires"
1776        );
1777        assert_eq!(
1778            drawing
1779                .standing_conflicts(&routing_shape_ids(&drawing))
1780                .crossings,
1781            Vec::<Rect>::new()
1782        );
1783    }
1784
1785    /// The diagnostic's bound holds every wire the selection raises, and the
1786    /// lattice it shows is the region's, not the sheet's: nothing drawn falls
1787    /// outside the bound.
1788    #[test]
1789    fn the_diagnostic_shows_the_bounded_lattice_a_selection_heals_in() {
1790        let mut scene = fx::scale_scene(6);
1791        let drawing = scene.drawing();
1792        let block = drawing
1793            .child_blocks()
1794            .first()
1795            .map(|&(id, _)| ShapeId::Rect(id))
1796            .expect("the grid has blocks");
1797        let foreground = crate::widget::foreground::Foreground::raising([block], &drawing);
1798        let (bound, marks) = drawing
1799            .healing_bound(&foreground)
1800            .expect("a block with wires raises some");
1801        let raised: Vec<RouteId> = foreground.routes().collect();
1802        assert!(!raised.is_empty(), "precondition: the block raises wires");
1803        for &id in &raised {
1804            let ends = drawing.route_ends(id).expect("a raised wire has ends");
1805            assert!(
1806                bound.contains(px_point(ends.start)) && bound.contains(px_point(ends.end)),
1807                "{id:?} lies outside the bound"
1808            );
1809        }
1810        let sheet = drawing
1811            .child_blocks()
1812            .iter()
1813            .filter_map(|&(id, _)| drawing.shape(ShapeId::Rect(id)))
1814            .map(|shape| shape.gui_rect())
1815            .reduce(|a, b| a.union(b))
1816            .expect("the grid has blocks");
1817        assert!(
1818            !bound.contains_rect(sheet),
1819            "precondition: the bound is smaller than the sheet"
1820        );
1821        let slack = bound.expand(GRID_SIZE);
1822        let outside: Vec<_> = marks
1823            .iter()
1824            .filter_map(|mark| match *mark {
1825                blockworx_router::turtle::Mark::Line { from, to } => {
1826                    (!slack.contains(from) || !slack.contains(to)).then_some(from)
1827                }
1828                blockworx_router::turtle::Mark::Circle { center, .. }
1829                | blockworx_router::turtle::Mark::Label { pos: center, .. } => {
1830                    (!slack.contains(center)).then_some(center)
1831                }
1832            })
1833            .take(4)
1834            .collect();
1835        assert!(!marks.is_empty(), "precondition: the lattice has marks");
1836        assert!(
1837            outside.is_empty(),
1838            "the lattice reaches past its bound: {outside:?}"
1839        );
1840    }
1841
1842    /// A region lattice is bounded by the router, not merely built from the
1843    /// blocks near the region: a lane escaping it runs where the region dropped
1844    /// every block, and a wire routed along it passes through them as though
1845    /// they were not there.
1846    #[test]
1847    fn a_region_lattice_holds_nothing_outside_its_region() {
1848        let mut scene = walled_in_wire(&ALL_WALLS, 26);
1849        let drawing = scene.drawing();
1850        let region = Region::Within(fx::cells(18, 16, 74, 24));
1851        let lattice = drawing.build_router_within(region, GeometryOverrides::default(), &[]);
1852        let nodes = lattice.fingerprint();
1853        assert!(
1854            !nodes.points().is_empty(),
1855            "precondition: the region holds a lattice"
1856        );
1857        let escaped: Vec<_> = nodes
1858            .points()
1859            .iter()
1860            .filter(|&&p| !region.bounds().holds(p))
1861            .take(4)
1862            .collect();
1863        assert!(escaped.is_empty(), "lanes escaped the region: {escaped:?}");
1864    }
1865
1866    /// A wire whose only way round lies beyond its healing region is not left
1867    /// on its L: the bounded lattice fails, and the whole scope is tried.
1868    #[test]
1869    fn a_region_too_small_to_detour_in_grows_to_the_scope() {
1870        let rid = route_id(5);
1871        let standing = [10, 11, EAST_WALL];
1872        let mut scene = walled_in_wire(&standing, 26);
1873        let geometry = fx::geometry(&scene, rid);
1874        let region = healing_region([geometry.start_pos, geometry.end_pos]);
1875        let (x, y, w, h) = RING[2].1;
1876        let east = fx::cells(x, y, w, h);
1877        assert!(
1878            east.min.y <= region.min.y && region.max.y <= east.max.y,
1879            "precondition: the east wall cuts the region in two, so it holds no path"
1880        );
1881        assert!(
1882            !region.intersects(fx::cells(0, 4, 4, 48)),
1883            "precondition: the gap the path needs lies outside the region"
1884        );
1885        assert_eq!(
1886            walls_crossed(geometry, &standing),
1887            vec![EAST_WALL],
1888            "precondition: the document's straight wire runs through the east wall"
1889        );
1890
1891        scene.commit(|drawing| {
1892            drawing.move_shape(ShapeId::Rect(block_id(2)), vec2(GRID_SIZE, 0.0));
1893        });
1894        assert_eq!(
1895            walls_crossed(fx::geometry(&scene, rid), &standing),
1896            Vec::<u32>::new()
1897        );
1898    }
1899
1900    /// Two blocks joined by one straight wire, B at `x_b`. Blocks `1`/`2`,
1901    /// pins `3`/`4`, route `5`.
1902    fn a_to_b_route(x_b: f32) -> Scene {
1903        Scene::new(vec![
1904            fx::block(1, 0.0),
1905            fx::block(2, x_b),
1906            fx::pin(3, 1, PinSide::East, 0),
1907            fx::pin(4, 2, PinSide::West, 0),
1908            fx::route(5, Scope::Root, 3, 4, &[]),
1909        ])
1910    }
1911
1912    fn route_edges(drawing: &Drawing<'_>, rid: RouteId) -> Vec<(GridPoint, GridPoint)> {
1913        drawing
1914            .route_geometry(rid)
1915            .unwrap()
1916            .iter_edges()
1917            .map(|(_, e)| (e.start, e.end))
1918            .collect()
1919    }
1920
1921    fn stored_corners(drawing: &Drawing<'_>, rid: RouteId) -> Vec<GridPoint> {
1922        drawing
1923            .auto_route(rid)
1924            .expect("the route is in this scope")
1925            .route
1926            .waypoints
1927            .iter()
1928            .map(|wp| wp.pos)
1929            .collect()
1930    }
1931
1932    #[test]
1933    fn a_drag_preview_moves_only_the_dragged_endpoint() {
1934        let mut scene = a_to_b_route(120.0);
1935        let rid = route_id(5);
1936        let mut drawing = scene.drawing();
1937        // Baseline routing against the stored positions.
1938        drawing.solve_routes(&[]);
1939        let start0 = drawing.route_geometry(rid).unwrap().start_pos();
1940        let end0 = drawing.route_geometry(rid).unwrap().end_pos();
1941
1942        // Drag block `a` by a grid-aligned offset; only its endpoint follows.
1943        let offset = vec2(2.0 * GRID_SIZE, GRID_SIZE);
1944        drawing.preview_drag(
1945            &crate::widget::PreviewPhase::testing(),
1946            &[(ShapeId::Rect(block_id(1)), offset)],
1947        );
1948
1949        let g = drawing.route_geometry(rid).unwrap();
1950        assert_eq!(g.start_pos(), start0 + offset);
1951        assert_eq!(g.end_pos(), end0);
1952    }
1953
1954    /// `add_route_label` and `place_route_label` are both wire-label writes
1955    /// (`edit::create::wire_label`, `edit::geometry::place_wire_label`), so
1956    /// each is a gesture of its own here.
1957    #[test]
1958    fn place_route_label_stores_the_distance_it_is_given() {
1959        let mut scene = a_to_b_route(120.0);
1960        let rid = route_id(5);
1961
1962        let (on_wire, lid) = scene.commit(|drawing| {
1963            let geometry = drawing
1964                .route_geometry(rid)
1965                .expect("the route is reconstructed");
1966            let points = geometry.points();
1967            assert!(points.len() >= 2, "the route has at least one segment");
1968            let on_wire = points[0] + (points[1] - points[0]) * 0.5;
1969            let lid = drawing
1970                .add_route_label(rid, on_wire)
1971                .expect("the label lands on the reconstructed route");
1972            (on_wire, lid)
1973        });
1974        let _ = on_wire;
1975
1976        let stored = |scene: &mut Scene| -> blockworx_doc::geometry::FracVal {
1977            scene
1978                .drawing()
1979                .auto_route(rid)
1980                .expect("the route is in this scope")
1981                .labels
1982                .iter()
1983                .find(|(id, _)| *id == lid)
1984                .map(|&(_, dist)| dist)
1985                .expect("the label is on the wire")
1986        };
1987        let before = stored(&mut scene);
1988        let target = blockworx_doc::geometry::FracVal::from(f32::from(before) + GRID_SIZE);
1989        assert_ne!(before, target, "the test moves the label somewhere new");
1990
1991        scene.commit(|drawing| drawing.place_route_label(lid, target));
1992
1993        assert_eq!(stored(&mut scene), target);
1994    }
1995
1996    #[test]
1997    fn moving_a_distant_block_leaves_unrelated_routes_byte_identical() {
1998        // Goal A: a purely local edit must not perturb the rest of the network.
1999        let mut scene = a_to_b_route(120.0);
2000        let far = block_id(9);
2001        scene.apply(vec![fx::block(9, 600.0)]); // nowhere near the route
2002        let rid = route_id(5);
2003        let before = route_edges(&scene.drawing(), rid);
2004
2005        // Commit a move of the distant block.
2006        scene.commit(|drawing| {
2007            drawing.move_shapes(
2008                &[ShapeId::Rect(far)],
2009                vec2(2.0 * GRID_SIZE, 3.0 * GRID_SIZE),
2010            );
2011        });
2012
2013        assert_eq!(
2014            before,
2015            route_edges(&scene.drawing(), rid),
2016            "a distant block move must leave an unrelated route byte-identical"
2017        );
2018    }
2019
2020    #[test]
2021    fn editing_a_route_never_autoroutes_around_obstacles() {
2022        // Requirement: moving a route segment must not trigger an autoroute. The
2023        // editor relay ([`Drawing::relay_route`], which the route edit's commit
2024        // runs) redraws the wire straight THROUGH a block sitting on it — a
2025        // fresh autoroute would instead detour around it.
2026        let mut scene = a_to_b_route(240.0);
2027        // A block straddling the wire's row (grid (8,1)-(10,4)).
2028        scene.apply(vec![fx::block_in(
2029            9,
2030            Scope::Root,
2031            Rect::from_min_max(pos2(120.0, 20.0), pos2(160.0, 60.0)),
2032        )]);
2033        let rid = route_id(5);
2034        let mut drawing = scene.drawing();
2035
2036        drawing
2037            .relay_route(rid, &[], &[], &[])
2038            .expect("the wire has resolvable endpoints");
2039
2040        assert_eq!(
2041            route_edges(&drawing, rid).len(),
2042            1,
2043            "the relayed wire stays a single straight leg"
2044        );
2045        assert!(
2046            route_passes_through(
2047                drawing.route_geometry(rid).unwrap(),
2048                GridPoint { x: 7, y: 2 }
2049            ),
2050            "the relay draws straight through the block — the editor never autoroutes"
2051        );
2052    }
2053
2054    #[test]
2055    fn drag_preview_reroutes_through_offset_waypoint() {
2056        // Dragging BOTH endpoints of a route with a waypoint must preview the
2057        // wire routed through the waypoint OFFSET by the drag delta, while the
2058        // STORED waypoint stays put (the drag is uncommitted).
2059        let mut scene = two_blocks_with_a_routed_waypoint();
2060        let (a, b, rid) = (block_id(1), block_id(2), route_id(5));
2061        let mut drawing = scene.drawing();
2062        drawing.solve_routes(&[]);
2063        let stored_before = stored_corners(&drawing, rid);
2064        let (start0, end0) = {
2065            let g = drawing.route_geometry(rid).unwrap();
2066            (g.start_pos(), g.end_pos())
2067        };
2068
2069        let off = vec2(0.0, 4.0 * GRID_SIZE); // grid_delta (0, 4)
2070        drawing.preview_drag(
2071            &crate::widget::PreviewPhase::testing(),
2072            &[(ShapeId::Rect(a), off), (ShapeId::Rect(b), off)],
2073        );
2074
2075        // Both endpoints follow the drag: the whole route previews rigidly shifted.
2076        let g = drawing.route_geometry(rid).unwrap();
2077        assert_eq!(g.start_pos(), start0 + off);
2078        assert_eq!(g.end_pos(), end0 + off);
2079
2080        // The STORED waypoints are untouched by the preview (the drag is
2081        // uncommitted): the shift lives only in the previewed edges.
2082        assert_eq!(
2083            stored_corners(&drawing, rid),
2084            stored_before,
2085            "stored waypoints stay put; the preview is uncommitted"
2086        );
2087    }
2088
2089    #[test]
2090    fn drag_preview_leaves_partially_selected_route() {
2091        // Dragging only ONE endpoint: the route straddles the selection boundary,
2092        // so only the dragged endpoint follows and the stored corners stay put
2093        // (the preview is uncommitted).
2094        let mut scene = two_blocks_with_a_routed_waypoint();
2095        let (a, rid) = (block_id(1), route_id(5));
2096        let mut drawing = scene.drawing();
2097        drawing.solve_routes(&[]);
2098        let stored_before = stored_corners(&drawing, rid);
2099        let end0 = drawing.route_geometry(rid).unwrap().end_pos();
2100
2101        let off = vec2(0.0, 4.0 * GRID_SIZE);
2102        drawing.preview_drag(
2103            &crate::widget::PreviewPhase::testing(),
2104            &[(ShapeId::Rect(a), off)],
2105        );
2106
2107        // The stationary endpoint stays; only the dragged one moves.
2108        assert_eq!(
2109            drawing.route_geometry(rid).unwrap().end_pos(),
2110            end0,
2111            "the un-dragged endpoint is fixed"
2112        );
2113
2114        // A partial-selection preview re-solves the moved leg from the stable
2115        // corner skeleton, so it may normalize (e.g. drop a deduped revisit) but
2116        // must never COMMIT new corners — every surviving corner was already there.
2117        let stored_after = stored_corners(&drawing, rid);
2118        assert!(
2119            stored_after.iter().all(|p| stored_before.contains(p)),
2120            "preview introduced new corners {stored_after:?} vs {stored_before:?}"
2121        );
2122    }
2123
2124    /// Two blocks whose pins sit on different slots, so the wire between them
2125    /// really bends, standing on the corners the SOLVER itself lays down: the
2126    /// throwaway corner is ripped up, which gives the settling gesture an op to
2127    /// author and its rider a reason to promote the solved list back. A claim
2128    /// about what a drag does to a wire's shape only means something about a
2129    /// shape the solve already agrees with.
2130    fn settled_bend() -> Scene {
2131        let tall = |n, x: f32| {
2132            fx::block_in(
2133                n,
2134                Scope::Root,
2135                Rect::from_min_max(pos2(x, 0.0), pos2(x + 40.0, 120.0)),
2136            )
2137        };
2138        let mut scene = Scene::new(vec![
2139            tall(1, 0.0),
2140            tall(2, 300.0),
2141            fx::pin(3, 1, PinSide::East, 0),
2142            fx::pin(4, 2, PinSide::West, 1),
2143            fx::route(5, Scope::Root, 3, 4, &[(10, 4)]),
2144        ]);
2145        fx::reconstruct(&mut scene);
2146        scene.commit(|drawing| drawing.reroute(route_id(5)));
2147        scene
2148    }
2149
2150    #[test]
2151    fn drag_preview_matches_commit() {
2152        // The whole point: the previewed geometry must equal the committed
2153        // geometry, so releasing the drag causes no visible snap.
2154        let off = vec2(0.0, 4.0 * GRID_SIZE);
2155        let (a, b, rid) = (block_id(1), block_id(2), route_id(5));
2156
2157        // Preview path.
2158        let mut scene = settled_bend();
2159        let settled = scene.drawing().route_geometry(rid).unwrap().points();
2160        let previewed = {
2161            let mut preview = scene.drawing();
2162            preview.preview_drag(
2163                &crate::widget::PreviewPhase::testing(),
2164                &[(ShapeId::Rect(a), off), (ShapeId::Rect(b), off)],
2165            );
2166            preview.route_geometry(rid).unwrap().points()
2167        };
2168        // Precondition: the drag previewed a move at all, so agreeing with the
2169        // commit is agreement about something.
2170        assert_ne!(previewed, settled, "the preview moved the wire");
2171
2172        // Commit path (fresh scene, same drag), from the same settled solve.
2173        let mut scene2 = settled_bend();
2174        scene2.commit(|commit| commit.move_shapes(&[ShapeId::Rect(a), ShapeId::Rect(b)], off));
2175        // What the user sees on release: the geometry the landed commit
2176        // re-derives, which is the rider's promoted corners reconstructed.
2177        let committed = scene2.drawing().route_geometry(rid).unwrap().points();
2178
2179        assert_eq!(
2180            previewed, committed,
2181            "drag preview geometry must equal the committed geometry (no release snap)"
2182        );
2183    }
2184
2185    #[test]
2186    fn a_gesture_removes_a_backtracking_waypoint() {
2187        // Two waypoints planted in the open corridor between the blocks force the
2188        // wire east, then straight back west, then east again — a 180° reversal
2189        // strict waypoint routing would honour. The gesture's solve rider must
2190        // drop the overshoot so no edge doubles back. The blocks sit far apart
2191        // so the corridor is wide enough for the two interior waypoints.
2192        let far = (15, 1);
2193        let near = (5, 1);
2194        let mut scene = Scene::new(vec![
2195            fx::block(1, 0.0),
2196            fx::block(2, 300.0),
2197            fx::pin(3, 1, PinSide::East, 0),
2198            fx::pin(4, 2, PinSide::West, 0),
2199            fx::route(5, Scope::Root, 3, 4, &[far, near]),
2200        ]);
2201        let rid = route_id(5);
2202        // Any gesture at all: the rider re-solves the scope it touched, and
2203        // the reversal is what the re-solve prunes. A rip-up is the smallest
2204        // one that names this route.
2205        scene.commit(|drawing| drawing.reroute(rid));
2206
2207        let drawing = scene.drawing();
2208        let geometry = drawing
2209            .route_geometry(rid)
2210            .expect("the route is reconstructed");
2211        let edges: Vec<_> = geometry.iter_edges().map(|(_, e)| e.clone()).collect();
2212        let doubles_back = edges.windows(2).any(|w| {
2213            let (ax, ay) = (w[0].end.x - w[0].start.x, w[0].end.y - w[0].start.y);
2214            let (bx, by) = (w[1].end.x - w[1].start.x, w[1].end.y - w[1].start.y);
2215            (ay == 0 && by == 0 && (ax > 0) != (bx > 0))
2216                || (ax == 0 && bx == 0 && (ay > 0) != (by > 0))
2217        });
2218        assert!(!doubles_back, "the wire doubles back after the gesture");
2219        assert!(
2220            !stored_corners(&drawing, rid).contains(&GridPoint { x: far.0, y: far.1 }),
2221            "the overshoot waypoint was dropped"
2222        );
2223    }
2224
2225    #[test]
2226    fn trim_approach_drops_up_to_two_unlocked_corners_from_the_endpoint() {
2227        let wp = |x, y, locked| Waypoint {
2228            pos: GridPoint { x, y },
2229            locked,
2230        };
2231        let corners = [wp(2, 0, false), wp(4, 0, false), wp(6, 0, false)];
2232        let positions =
2233            |kept: Vec<Waypoint>| -> Vec<GridPoint> { kept.into_iter().map(|w| w.pos).collect() };
2234
2235        assert_eq!(
2236            positions(trimmed_approach(&corners, RouteEnd::From)),
2237            vec![GridPoint { x: 6, y: 0 }],
2238            "the two leading corners nearest the start are dropped"
2239        );
2240        assert_eq!(
2241            positions(trimmed_approach(&corners, RouteEnd::To)),
2242            vec![GridPoint { x: 2, y: 0 }],
2243            "the two trailing corners nearest the finish are dropped"
2244        );
2245
2246        // A locked corner ends the scan, preserving the user's explicit bend and
2247        // everything past it.
2248        let locked_first = [wp(2, 0, true), wp(4, 0, false)];
2249        assert_eq!(
2250            positions(trimmed_approach(&locked_first, RouteEnd::From)),
2251            vec![GridPoint { x: 2, y: 0 }, GridPoint { x: 4, y: 0 }],
2252            "a leading locked corner blocks the trim entirely"
2253        );
2254    }
2255
2256    /// A drop trims every wire straddling the moved block in one pass — each
2257    /// from its moved end — and leaves a wire with both ends on the block as
2258    /// it was: its corners ride with the block, so none went stale.
2259    #[test]
2260    fn a_moved_blocks_straddling_wires_are_all_trimmed_together() {
2261        let mut scene = Scene::new(vec![
2262            fx::block(1, 0.0),
2263            fx::block(2, 300.0),
2264            fx::block(6, 600.0),
2265            fx::pin(3, 1, PinSide::East, 0),
2266            fx::pin(7, 1, PinSide::East, 1),
2267            fx::pin(8, 1, PinSide::West, 0),
2268            fx::pin(4, 2, PinSide::West, 0),
2269            fx::pin(9, 6, PinSide::West, 0),
2270            fx::route(5, Scope::Root, 3, 4, &[(8, 1), (8, 3), (14, 3)]),
2271            fx::route(10, Scope::Root, 9, 7, &[(30, 5), (8, 5), (8, 2)]),
2272            fx::route(11, Scope::Root, 8, 3, &[(-2, 1), (-2, -3), (5, -3)]),
2273        ]);
2274        scene.begin("drop");
2275        scene
2276            .drawing()
2277            .trim_partial_route_approaches(&[ShapeId::Rect(block_id(1))]);
2278
2279        let written: Vec<(RouteId, Vec<GridPoint>)> = scene
2280            .gesture
2281            .ops()
2282            .iter()
2283            .filter_map(|op| match op {
2284                OpCodes::Route(id, Crud::Update(RouteUpdate::Waypoints(waypoints))) => {
2285                    Some((*id, waypoints.iter().map(|wp| wp.pos).collect()))
2286                }
2287                _ => None,
2288            })
2289            .collect();
2290        let at = |x, y| GridPoint { x, y };
2291        assert_eq!(
2292            written,
2293            vec![
2294                (route_id(5), vec![at(14, 3)]),
2295                (route_id(10), vec![at(30, 5)]),
2296            ],
2297            "each straddler loses the corners at its moved end; the loop keeps its own"
2298        );
2299    }
2300
2301    #[test]
2302    fn flipping_a_block_reroutes_and_trims_like_a_drag() {
2303        // Flip L/R moves a block's pins to new sides. Like a drag, the connected
2304        // route drops its stale approach corner and re-routes, leaving a
2305        // well-formed wire (waypoints in sync with corners). Mirrors the
2306        // FlipShapePins handler: flip, trim approaches, re-route.
2307        let mut scene = two_blocks_with_a_routed_waypoint();
2308        let (a, rid) = (block_id(1), route_id(5));
2309
2310        scene.commit(|d| {
2311            d.flip_shape_pins(ShapeId::Rect(a));
2312            d.trim_partial_route_approaches(&[ShapeId::Rect(a)]);
2313        });
2314
2315        let d = scene.drawing();
2316        let wire = d.auto_route(rid).expect("the route survives the flip");
2317        let geometry = d.route_geometry(rid).expect("the route is reconstructed");
2318        assert!(
2319            geometry.iter_edges().next().is_some(),
2320            "the flipped route has geometry"
2321        );
2322        assert!(
2323            waypoints_are_the_corners(&wire, geometry),
2324            "flip re-route left the waypoints in sync with the corners"
2325        );
2326    }
2327
2328    #[test]
2329    fn flipping_a_port_reroutes_without_moving_the_parent_pin() {
2330        use crate::shape::pin::{orientation, slot};
2331        // A boundary port on the document root, wired to a child block's pin.
2332        let (port_id, rid) = (pin_id(3), route_id(5));
2333        let mut scene = Scene::new(vec![
2334            fx::pin_at(
2335                3,
2336                Scope::Root,
2337                "io",
2338                fx::slot(PinSide::East, 1),
2339                Rect::from_min_max(pos2(0.0, 45.0), pos2(40.0, 61.0)),
2340            ),
2341            fx::block(2, 120.0),
2342            fx::pin(4, 2, PinSide::West, 0),
2343            fx::route(5, Scope::Root, 3, 4, &[]),
2344        ]);
2345        let pin = scene
2346            .drawing()
2347            .held_pin(port_id)
2348            .expect("the port's pin")
2349            .clone();
2350        let (before_side, before_orientation) = (slot(&pin).side, orientation(&pin));
2351        // A fresh port faces opposite the edge its pin occupies.
2352        assert_eq!(before_orientation, before_side.flip());
2353
2354        scene.commit(|drawing| drawing.flip_shape_pins(ShapeId::Port(port_id)));
2355
2356        let drawing = scene.drawing();
2357        let pin = drawing.held_pin(port_id).expect("the port's pin").clone();
2358        // The pin's edge side (what the parent block draws) is untouched; only the
2359        // port's facing changed.
2360        assert_eq!(
2361            slot(&pin).side,
2362            before_side,
2363            "flipping the port moved the parent pin"
2364        );
2365        assert_eq!(orientation(&pin), before_orientation.flip());
2366
2367        // The route followed the port to its new facing — still connected.
2368        let port = drawing.shape(ShapeId::Port(port_id)).unwrap();
2369        let anchor = port
2370            .anchor_point_with_rect(port.gui_rect(), port_id)
2371            .unwrap();
2372        assert_eq!(drawing.route_geometry(rid).unwrap().start_pos(), anchor);
2373    }
2374
2375    /// The route's stored waypoints must be exactly its edge corners (the interior
2376    /// vertices where the polyline bends) — the "every corner is a waypoint"
2377    /// invariant. A mismatch is a stale waypoint set: it renders wrong when the
2378    /// route is selected and changes on the next load (which re-derives it).
2379    fn waypoints_are_the_corners(wire: &Wire<'_>, geometry: &RouteGeometry) -> bool {
2380        let edges: Vec<_> = geometry.iter_edges().map(|(_, e)| e).collect();
2381        let corners: std::collections::HashSet<GridPoint> =
2382            edges.windows(2).map(|w| w[0].end).collect();
2383        let waypoints: std::collections::HashSet<GridPoint> =
2384            wire.route.waypoints.iter().map(|wp| wp.pos).collect();
2385        corners == waypoints
2386    }
2387
2388    #[test]
2389    fn dragging_a_block_keeps_route_waypoints_matching_the_corners() {
2390        // B sits on a different row than A, so the wire bends — it has real
2391        // corners that must stay in sync with the stored waypoints.
2392        let (b, rid) = (block_id(2), route_id(5));
2393        let mut scene = Scene::new(vec![
2394            fx::block(1, 0.0),
2395            fx::block_in(
2396                2,
2397                Scope::Root,
2398                Rect::from_min_max(pos2(300.0, 150.0), pos2(340.0, 190.0)),
2399            ),
2400            fx::pin(3, 1, PinSide::East, 0),
2401            fx::pin(4, 2, PinSide::West, 0),
2402            fx::route(5, Scope::Root, 3, 4, &[]),
2403        ]);
2404        scene.drawing().solve_routes(&[]);
2405
2406        // Drag B: several live-preview frames, then the on-drop commit — exactly
2407        // the sequence MoveBlock produces.
2408        let delta = vec2(0.0, 4.0 * GRID_SIZE);
2409        for frame in 1..=4 {
2410            scene.drawing().preview_drag(
2411                &crate::widget::PreviewPhase::testing(),
2412                &[(ShapeId::Rect(b), vec2(0.0, frame as f32 * GRID_SIZE))],
2413            );
2414        }
2415        scene.commit(|d| {
2416            d.move_shape(ShapeId::Rect(b), delta);
2417            d.trim_partial_route_approaches(&[ShapeId::Rect(b)]);
2418        });
2419
2420        let d = scene.drawing();
2421        let wire = d.auto_route(rid).unwrap();
2422        let geometry = d.route_geometry(rid).expect("the route is reconstructed");
2423        assert!(
2424            waypoints_are_the_corners(&wire, geometry),
2425            "after a drag the route's waypoints must match its corners"
2426        );
2427    }
2428
2429    #[test]
2430    fn flipping_a_block_preserves_its_childrens_internal_routes() {
2431        // C's boundary port wired to a grandchild's pin — a route living inside C.
2432        let (c, pc, rid) = (block_id(1), pin_id(3), route_id(5));
2433        let mut scene = Scene::new(vec![
2434            fx::block(1, 0.0),
2435            fx::pin_at(
2436                3,
2437                Scope::Block(c),
2438                "io",
2439                fx::slot(PinSide::West, 1),
2440                Rect::from_min_max(pos2(0.0, 45.0), pos2(40.0, 61.0)),
2441            ),
2442            fx::block_in(
2443                2,
2444                Scope::Block(c),
2445                Rect::from_min_max(pos2(60.0, 0.0), pos2(100.0, 40.0)),
2446            ),
2447            fx::pin(4, 2, PinSide::East, 0),
2448            fx::route(5, Scope::Block(c), 3, 4, &[]),
2449        ]);
2450
2451        let anchor_before = {
2452            scene.path.push(c);
2453            let mut drawing = scene.drawing();
2454            drawing.solve_routes(&[]);
2455            let port = drawing.shape(ShapeId::Port(pc)).unwrap();
2456            port.anchor_point_with_rect(port.gui_rect(), pc).unwrap()
2457        };
2458
2459        // Mirror C at the top level: its edge pins flip side, but each pin's port
2460        // orientation is frozen.
2461        scene.path.pop();
2462        scene.commit(|drawing| drawing.flip_shape_pins(ShapeId::Rect(c)));
2463
2464        // Inside C, the boundary port hasn't moved, so its internal route keeps
2465        // its endpoint.
2466        scene.path.push(c);
2467        let mut drawing = scene.drawing();
2468        drawing.solve_routes(&[]);
2469        let port = drawing.shape(ShapeId::Port(pc)).unwrap();
2470        let anchor_after = port.anchor_point_with_rect(port.gui_rect(), pc).unwrap();
2471        assert_eq!(
2472            anchor_after, anchor_before,
2473            "flipping the block moved its interior port"
2474        );
2475        assert_eq!(
2476            drawing.route_geometry(rid).unwrap().start_pos(),
2477            anchor_after,
2478            "internal route lost its port endpoint"
2479        );
2480    }
2481
2482    /// A commit that lands after a preview takes the preview back in its own
2483    /// pass, even when what it disturbed is nowhere near the previewed wire.
2484    #[test]
2485    fn a_commit_takes_back_a_preview_it_never_disturbed() {
2486        let mut scene = Scene::new(vec![
2487            fx::block(1, 0.0),
2488            fx::block(2, 120.0),
2489            fx::block(6, 3000.0),
2490            fx::pin(3, 1, PinSide::East, 0),
2491            fx::pin(4, 2, PinSide::West, 0),
2492            fx::route(5, Scope::Root, 3, 4, &[]),
2493        ]);
2494        let rid = route_id(5);
2495        let settled = route_edges(&scene.drawing(), rid);
2496        {
2497            let mut drawing = scene.drawing();
2498            drawing.preview_drag(
2499                &crate::widget::PreviewPhase::testing(),
2500                &[(ShapeId::Rect(block_id(2)), vec2(0.0, 4.0 * GRID_SIZE))],
2501            );
2502            assert_ne!(
2503                route_edges(&drawing, rid),
2504                settled,
2505                "precondition: the preview drew the wire somewhere else"
2506            );
2507        }
2508        scene.commit(|drawing| {
2509            drawing.move_shape(ShapeId::Rect(block_id(6)), vec2(GRID_SIZE, 0.0));
2510        });
2511        assert!(
2512            !scene.committed().is_empty(),
2513            "precondition: the far move committed"
2514        );
2515        assert_eq!(
2516            route_edges(&scene.drawing(), rid),
2517            settled,
2518            "the preview outlived the commit"
2519        );
2520    }
2521
2522    #[test]
2523    fn drag_and_resize_preview_frames_never_touch_the_document() {
2524        // A routed waypoint makes the straddle trim and backtracking prune
2525        // non-trivial: the preview must *preview* corners away, not delete them.
2526        let mut scene = two_blocks_with_a_routed_waypoint();
2527        let (a, rid) = (block_id(1), route_id(5));
2528        scene.drawing().solve_routes(&[]);
2529        assert!(
2530            !stored_corners(&scene.drawing(), rid).is_empty(),
2531            "precondition: the committed route must carry corners to trim"
2532        );
2533        let before = scene.doc.clone();
2534        let stamp = scene.doc.stamp();
2535
2536        {
2537            let mut drawing = scene.drawing();
2538            let start0 = drawing.route_geometry(rid).unwrap().start_pos();
2539            for frame in 1..=4 {
2540                let offset = vec2(GRID_SIZE * frame as f32, GRID_SIZE);
2541                drawing.preview_drag(
2542                    &crate::widget::PreviewPhase::testing(),
2543                    &[(ShapeId::Rect(a), offset)],
2544                );
2545            }
2546            assert_ne!(
2547                drawing.route_geometry(rid).unwrap().start_pos(),
2548                start0,
2549                "precondition: the preview really re-solved the dragged route"
2550            );
2551            let grown = drawing.shape(ShapeId::Rect(a)).unwrap().gui_rect();
2552            drawing.preview_resize(
2553                &crate::widget::PreviewPhase::testing(),
2554                &[(
2555                    ShapeId::Rect(a),
2556                    Rect::from_min_size(grown.min, grown.size() + vec2(GRID_SIZE, GRID_SIZE)),
2557                )],
2558            );
2559        }
2560
2561        assert_eq!(
2562            scene.doc.stamp(),
2563            stamp,
2564            "a preview frame must not produce a new document value"
2565        );
2566        assert_eq!(scene.doc.clone(), before);
2567    }
2568
2569    #[test]
2570    fn a_pin_drag_preview_tracks_the_hypothetical_slot_without_moving_the_pin() {
2571        let mut scene = a_to_b_route(120.0);
2572        let (a, pa, rid) = (block_id(1), pin_id(3), route_id(5));
2573        scene.drawing().solve_routes(&[]);
2574        let before = scene.doc.clone();
2575        let stamp = scene.doc.stamp();
2576
2577        let (start0, start, previewed) = {
2578            let mut drawing = scene.drawing();
2579            let start0 = drawing.route_geometry(rid).unwrap().start_pos();
2580            drawing.preview_pin_drag(
2581                &crate::widget::PreviewPhase::testing(),
2582                pa,
2583                PinSide::East,
2584                3,
2585            );
2586            let start = drawing.route_geometry(rid).unwrap().start_pos();
2587            let previewed = match drawing.shape(ShapeId::Rect(a)).unwrap() {
2588                ShapeRef::Block(block) => block.pin_anchor_at(block.gui_rect(), PinSide::East, 3),
2589                _ => unreachable!("a is a block"),
2590            };
2591            (start0, start, previewed)
2592        };
2593
2594        assert_ne!(start, start0, "precondition: slot 3 moves the anchor");
2595        assert_eq!(
2596            grid_point(start),
2597            grid_point(previewed),
2598            "the previewed endpoint sits at the hypothetical slot"
2599        );
2600        assert_eq!(scene.doc.stamp(), stamp);
2601        assert_eq!(scene.doc.clone(), before);
2602
2603        // The group flow runs the same pure pass.
2604        {
2605            let mut drawing = scene.drawing();
2606            drawing.preview_pin_drags(
2607                &crate::widget::PreviewPhase::testing(),
2608                &[PinMove {
2609                    pin: pa,
2610                    to: PinSlot {
2611                        side: PinSide::East,
2612                        offset: 2,
2613                    },
2614                }],
2615            );
2616        }
2617        assert_eq!(scene.doc.stamp(), stamp);
2618        assert_eq!(scene.doc.clone(), before);
2619    }
2620}
2621
2622#[cfg(test)]
2623mod region_cost {
2624    use super::*;
2625    use crate::widget::test_fixtures::{cells, scale_scene};
2626    use std::time::Instant;
2627
2628    /// What a lattice costs when it covers the region an edit touches rather
2629    /// than the whole diagram.
2630    ///
2631    /// `cargo test --release -p blockworx-editor region_cost -- --ignored --nocapture`
2632    #[test]
2633    #[ignore = "a timing, not a check"]
2634    fn a_region_lattice_costs_what_the_region_holds() {
2635        let mut scene = scale_scene(50);
2636        let drawing = scene.drawing();
2637
2638        let mut seeds: Vec<Point> = Vec::new();
2639        for id in drawing.scope_route_ids() {
2640            let Some(route) = drawing.route(id) else {
2641                continue;
2642            };
2643            for end in [route.from, route.to] {
2644                if let Some(at) = drawing.anchor_pos_overridden(end, GeometryOverrides::default()) {
2645                    seeds.push(snap_to_grid(at).into());
2646                }
2647            }
2648            for wp in &route.waypoints {
2649                seeds.push(wp.pos.into());
2650            }
2651        }
2652
2653        let whole = Instant::now();
2654        let full =
2655            drawing.build_router_within(Region::WholeScope, GeometryOverrides::default(), &seeds);
2656        let whole = whole.elapsed();
2657
2658        // The grid is on a 16-cell pitch with 8-cell blocks, so this holds a
2659        // 3×3 neighbourhood with room for their moats.
2660        let region = cells(-8, -8, 56, 56);
2661        let local = Instant::now();
2662        let bounded = drawing.build_router_within(
2663            Region::Within(region),
2664            GeometryOverrides::default(),
2665            &seeds,
2666        );
2667        let local = local.elapsed();
2668
2669        let raised = Instant::now();
2670        let foreground = crate::widget::foreground::Foreground::raising(
2671            drawing
2672                .child_blocks()
2673                .first()
2674                .map(|&(id, _)| crate::shape::ShapeId::Rect(id)),
2675            &drawing,
2676        );
2677        let raised = raised.elapsed();
2678
2679        eprintln!(
2680            "==== whole sheet: {whole:?}, {} nodes",
2681            full.fingerprint().nodes()
2682        );
2683        eprintln!(
2684            "==== raising one block: {raised:?}, {} of {} wires raised",
2685            foreground.routes().count(),
2686            drawing.scope_route_ids().len()
2687        );
2688        eprintln!(
2689            "==== 3×3 region:  {local:?}, {} nodes",
2690            bounded.fingerprint().nodes()
2691        );
2692    }
2693}