Skip to main content

blockworx_editor/widget/
foreground.rs

1//! The foreground: the geometry one gesture may write.
2//!
3//! Selection raises. What the user has selected — and everything whose wires
4//! that selection can disturb — is the *foreground*; everything else is the
5//! *background*, whose routing lattice can then be built once and cached
6//! across the whole gesture rather than rebuilt per mutation
7//! (`docs/foreground-router-playbook.md`).
8//!
9//! The set is derived from the selection rather than declared, so a caller
10//! cannot forget a wire — and it is deliberately a *superset* of what the
11//! gesture will actually touch, because the cache's whole claim rests on
12//! nothing being written outside it.
13
14use std::collections::BTreeSet;
15
16use blockworx_doc::{id::EntityRef, opcode::OpCodes};
17use blockworx_geom::Rect;
18use blockworx_router::block::MOAT_REACH;
19
20use blockworx_doc::id::RouteId;
21
22use crate::{
23    shape::ShapeId,
24    widget::{drawing::Drawing, routing::obstacle_rect},
25};
26
27/// What a gesture may write, raised out of the background.
28///
29/// The invariant the background cache depends on:
30///
31/// > **The foreground is a superset of everything the gesture can write.**
32///
33/// If it holds, the background survives the gesture. If it does not, the
34/// gesture invalidates its own cache partway through — correct, but with the
35/// saving lost, which is why [`Self::escapees`] exists to prove it in tests.
36#[derive(Debug, Default, Clone, PartialEq, Eq)]
37pub struct Foreground {
38    shapes: BTreeSet<ShapeId>,
39    routes: BTreeSet<RouteId>,
40}
41
42impl Foreground {
43    /// Raise `shapes` and everything their movement can disturb: the wires
44    /// anchored to them, and the wires their current footprints already block
45    /// or hug. A gesture that *moves* a shape adds its destination with
46    /// [`Self::also_disturbed_by`] — the footprint it is heading for disturbs
47    /// wires the one it is leaving never touched.
48    pub fn raising(shapes: impl IntoIterator<Item = ShapeId>, drawing: &Drawing<'_>) -> Self {
49        Self::derive(shapes.into_iter().collect(), BTreeSet::new(), drawing)
50    }
51
52    /// What a gesture's own ops touched, raised. Where [`Self::raising`] asks
53    /// what the user *selected*, this asks what the gesture actually *wrote* —
54    /// a subset, and the one the solve is scoped to, so that what a pass may
55    /// re-solve is derived from the same ops it is riding on.
56    pub fn written(ops: &[OpCodes], drawing: &Drawing<'_>) -> Self {
57        let mut shapes = BTreeSet::new();
58        let mut routes = BTreeSet::new();
59        for target in ops.iter().map(OpCodes::target) {
60            match target {
61                EntityRef::Block(id) => {
62                    shapes.insert(ShapeId::Rect(id));
63                }
64                EntityRef::Pin(id) => {
65                    shapes.extend(drawing.pin_shape(id));
66                }
67                EntityRef::Route(id) => {
68                    routes.insert(id);
69                }
70                EntityRef::RouteLabel(_)
71                | EntityRef::Text(_)
72                | EntityRef::Area(_)
73                | EntityRef::Image(_)
74                | EntityRef::Document
75                | EntityRef::Asset(_) => {}
76            }
77        }
78        Self::derive(shapes, routes, drawing)
79    }
80
81    #[tracing::instrument(name = "foreground", level = "info", skip_all)]
82    fn derive(shapes: BTreeSet<ShapeId>, routes: BTreeSet<RouteId>, drawing: &Drawing<'_>) -> Self {
83        let mut raised = Self { shapes, routes };
84        let anchored: Vec<RouteId> = drawing
85            .scope_route_ids()
86            .into_iter()
87            .filter(|&id| raised.anchored_to_a_raised_shape(id, drawing))
88            .collect();
89        raised.routes.extend(anchored);
90        for rect in raised.footprints(drawing) {
91            raised.raise_wires_crossing(rect, drawing);
92        }
93        raised
94    }
95
96    /// Also raise the wires `rect` blocks or hugs — the footprint a move is
97    /// heading for, which is not in the document yet and so cannot be derived
98    /// from it.
99    pub fn also_disturbed_by(&mut self, rect: Rect, drawing: &Drawing<'_>) {
100        self.raise_wires_crossing(rect, drawing);
101    }
102
103    /// Is this wire in the foreground — may this pass re-solve it?
104    #[must_use]
105    pub fn holds_route(&self, id: RouteId) -> bool {
106        self.routes.contains(&id)
107    }
108
109    /// Is `entity` in the foreground?
110    #[must_use]
111    pub fn holds(&self, entity: EntityRef, drawing: &Drawing<'_>) -> bool {
112        match entity {
113            EntityRef::Block(id) => self.shapes.contains(&ShapeId::Rect(id)),
114            EntityRef::Route(id) => self.routes.contains(&id),
115            EntityRef::Pin(id) => drawing
116                .pin_shape(id)
117                .is_some_and(|shape| self.shapes.contains(&shape)),
118            EntityRef::RouteLabel(id) => drawing
119                .indexed()
120                .doc
121                .route_label(&id)
122                .is_some_and(|label| self.routes.contains(&label.owner)),
123            EntityRef::Text(id) => self.shapes.contains(&ShapeId::Text(id)),
124            EntityRef::Area(id) => self.shapes.contains(&ShapeId::Area(id)),
125            EntityRef::Image(id) => self.shapes.contains(&ShapeId::Image(id)),
126            EntityRef::Document | EntityRef::Asset(_) => false,
127        }
128    }
129
130    /// The ops in `commit` that write routing geometry this foreground does
131    /// not hold — each one a reason the background cache would have to be
132    /// dropped. Empty is the invariant holding.
133    ///
134    /// Deliberately conservative about *which* ops count: any op on a block,
135    /// pin or route is treated as moving the lattice, though a rename plainly
136    /// does not. A cheap over-report costs a rebuild; an under-report would
137    /// solve against occupancy that has silently moved.
138    pub fn escapees<'a>(
139        &'a self,
140        ops: &'a [OpCodes],
141        drawing: &'a Drawing<'_>,
142    ) -> impl Iterator<Item = EntityRef> + 'a {
143        ops.iter()
144            .map(OpCodes::target)
145            .filter(|&target| moves_the_lattice(target) && !self.holds(target, drawing))
146    }
147
148    /// The rectangle this foreground occupies: every shape it raised and every
149    /// wire, grown by the moat a block shapes its channels within.
150    ///
151    /// What a commit *disturbed*, in other words — which is what the
152    /// reconstruction after it needs, since a wire outside this rectangle is
153    /// drawn from geometry nothing moved near.
154    #[must_use]
155    pub fn extent(&self, drawing: &Drawing<'_>) -> Rect {
156        let margin = crate::grid::GRID_SIZE * MOAT_REACH as f32;
157        let mut extent: Option<Rect> = None;
158        let mut widen = |rect: Rect| {
159            extent = Some(extent.map_or(rect, |so_far: Rect| so_far.union(rect)));
160        };
161        for id in self.shapes() {
162            if let Some(shape) = drawing.shape(id) {
163                widen(shape.gui_rect());
164            }
165        }
166        for id in self.routes() {
167            if let Some(geometry) = drawing.route_geometry(id) {
168                widen(Rect::from_min_max(
169                    geometry.start_pos().min(geometry.end_pos()),
170                    geometry.start_pos().max(geometry.end_pos()),
171                ));
172                for (_, edge) in geometry.iter_edges() {
173                    let (a, b) = (
174                        crate::grid::px_point(edge.start),
175                        crate::grid::px_point(edge.end),
176                    );
177                    widen(Rect::from_min_max(a.min(b), a.max(b)));
178                }
179            }
180        }
181        extent.unwrap_or(Rect::ZERO).expand(margin)
182    }
183
184    /// The shapes raised, for the caller building the background without them.
185    pub fn shapes(&self) -> impl Iterator<Item = ShapeId> + '_ {
186        self.shapes.iter().copied()
187    }
188
189    /// The wires raised, for the same reason.
190    pub fn routes(&self) -> impl Iterator<Item = RouteId> + '_ {
191        self.routes.iter().copied()
192    }
193
194    fn anchored_to_a_raised_shape(&self, id: RouteId, drawing: &Drawing<'_>) -> bool {
195        let Some(route) = drawing.route(id) else {
196            return false;
197        };
198        [route.from, route.to]
199            .into_iter()
200            .filter_map(|pin| drawing.pin_shape(pin))
201            .any(|shape| self.shapes.contains(&shape))
202    }
203
204    fn footprints(&self, drawing: &Drawing<'_>) -> Vec<Rect> {
205        self.shapes
206            .iter()
207            .filter_map(|&id| Some(drawing.shape(id)?.gui_rect()))
208            .collect()
209    }
210
211    /// Every wire `rect` reaches: the ones it crosses, and the ones running
212    /// within its moat.
213    ///
214    /// The moat rather than the route gutter, because a block does not only
215    /// obstruct wires — it *shapes the channels* around itself, trimming those
216    /// it spans and seeding five lanes of its own out to [`MOAT_REACH`]. A wire
217    /// running in one of those channels is drawn where it is *because* of this
218    /// block, so moving the block can change it even though the wire never
219    /// touches it.
220    fn raise_wires_crossing(&mut self, rect: Rect, drawing: &Drawing<'_>) {
221        let obstacle = obstacle_rect(rect);
222        let crossed: Vec<RouteId> = drawing
223            .scope_route_ids()
224            .into_iter()
225            .filter(|id| !self.routes.contains(id))
226            .filter(|&id| {
227                drawing.route_geometry(id).is_some_and(|geometry| {
228                    geometry.iter_edges().any(|(_, edge)| {
229                        obstacle.intersects_edge(edge.start, edge.end)
230                            || obstacle.hugs_wire(edge.start, edge.end, MOAT_REACH)
231                    })
232                })
233            })
234            .collect();
235        self.routes.extend(crossed);
236    }
237}
238
239/// Whether an op on this entity can move the routing lattice. Texts, areas and
240/// images are not routing obstacles — `routing_shapes` is the blocks layer and
241/// the ports layer — so no edit to one can change where a wire runs.
242fn moves_the_lattice(target: EntityRef) -> bool {
243    matches!(
244        target,
245        EntityRef::Block(_) | EntityRef::Pin(_) | EntityRef::Route(_)
246    )
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::grid::GRID_SIZE;
253    use crate::grid::px_point;
254    use crate::path::Scope;
255    use crate::widget::test_fixtures::{
256        Scene, block_in, cells, pin, reconstruct, route, scale_scene,
257    };
258    use blockworx_doc::id::BlockId;
259    use blockworx_doc::values::PinSide;
260    use blockworx_geom::Rect;
261    use blockworx_geom::vec2;
262
263    /// The nth block the scene created, by creation order — the hand-built
264    /// scenes above name their blocks by the number they passed.
265    fn block_id_of(scene: &mut Scene, n: usize) -> BlockId {
266        scene.drawing().child_blocks()[n - 1].0
267    }
268
269    /// The scale fixture titles its blocks `block_<row>_<col>`, which is how a
270    /// test names one without reaching into the generator's private tables.
271    fn block_titled(scene: &mut Scene, title: &str) -> BlockId {
272        scene
273            .drawing()
274            .child_blocks()
275            .into_iter()
276            .find(|(_, block)| block.title.name == title)
277            .map_or_else(|| panic!("no block titled {title}"), |(id, _)| id)
278    }
279
280    /// The wires the *document* says land on `block` — read through pin
281    /// ownership rather than through `pin_shape`, so the expectation does not
282    /// come from the same place the implementation does.
283    fn wires_landing_on(scene: &mut Scene, block: BlockId) -> Vec<RouteId> {
284        let drawing = scene.drawing();
285        let doc = drawing.indexed().doc;
286        drawing
287            .scope_route_ids()
288            .into_iter()
289            .filter(|&id| {
290                drawing.route(id).is_some_and(|route| {
291                    [route.from, route.to]
292                        .iter()
293                        .filter_map(|p| doc.pin(p))
294                        .any(|pin| pin.owner == block)
295                })
296            })
297            .collect()
298    }
299
300    /// Every wire anchored to a raised block comes up with it.
301    #[test]
302    fn raising_a_block_raises_the_wires_anchored_to_it() {
303        let mut scene = scale_scene(3);
304        let block = block_titled(&mut scene, "block_1_1");
305        let anchored = wires_landing_on(&mut scene, block);
306        assert!(
307            !anchored.is_empty(),
308            "the fixture wires nothing to this block; the test would prove nothing"
309        );
310
311        let raised = Foreground::raising([ShapeId::Rect(block)], &scene.drawing());
312        let raised_routes: Vec<RouteId> = raised.routes().collect();
313        for id in anchored {
314            assert!(raised_routes.contains(&id), "{id:?} was left behind");
315        }
316    }
317
318    /// And it is a *subset*: raising one block of nine must not raise every
319    /// wire. Without this the superset test below could pass trivially.
320    #[test]
321    fn raising_a_block_leaves_the_rest_in_the_background() {
322        let mut scene = scale_scene(3);
323        let block = block_titled(&mut scene, "block_1_1");
324        let all = scene.drawing().scope_route_ids().len();
325        assert!(all > 0, "no wires to leave behind");
326
327        let raised = Foreground::raising([ShapeId::Rect(block)], &scene.drawing());
328        assert!(
329            raised.routes().count() < all,
330            "raised all {all} wires — nothing was left in the background"
331        );
332    }
333
334    /// A block with no pins, moved onto a wire it has nothing to do with,
335    /// must raise that wire — it is the bystander case, and the only reason
336    /// the footprint clause takes the destination as well as the origin.
337    #[test]
338    fn a_destination_raises_a_wire_the_mover_has_nothing_to_do_with() {
339        let mut scene = Scene::new(vec![
340            block_in(1, Scope::Root, cells(0, 0, 4, 4)),
341            block_in(2, Scope::Root, cells(24, 0, 4, 4)),
342            pin(1, 1, PinSide::East, 0),
343            pin(2, 2, PinSide::West, 0),
344            route(1, Scope::Root, 1, 2, &[]),
345            // The bystander: no pins, nowhere near the wire.
346            block_in(3, Scope::Root, cells(10, 20, 4, 4)),
347        ]);
348        reconstruct(&mut scene);
349
350        let wire = scene.drawing().scope_route_ids()[0];
351        let over_the_wire = {
352            let drawing = scene.drawing();
353            let geometry = drawing.route_geometry(wire).expect("the wire is solved");
354            let (_, edge) = geometry.iter_edges().next().expect("the wire has an edge");
355            let mid = px_point(edge.start).lerp(px_point(edge.end), 0.5);
356            Rect::from_center_size(mid, vec2(4.0 * GRID_SIZE, 4.0 * GRID_SIZE))
357        };
358
359        let mut raised = Foreground::raising(
360            [ShapeId::Rect(block_id_of(&mut scene, 3))],
361            &scene.drawing(),
362        );
363        assert!(
364            raised.routes().next().is_none(),
365            "the bystander already holds the wire at its home; the test would prove nothing"
366        );
367
368        raised.also_disturbed_by(over_the_wire, &scene.drawing());
369        assert!(
370            raised.routes().any(|id| id == wire),
371            "moving onto the wire did not raise it"
372        );
373    }
374
375    /// **The superset rule**, proved through the real gesture path: every op a
376    /// move seals — the rider's promotions included — targets geometry the
377    /// foreground already held. It is what lets a background router be built
378    /// once and survive the gesture.
379    ///
380    /// This test was first written inverted, asserting the escapees were *not*
381    /// empty: with the rider re-solving the whole scope, moving one block one
382    /// cell rewrote 20 of this sheet's 30 wires.
383    #[test]
384    fn a_move_writes_nothing_outside_the_foreground() {
385        let mut scene = scale_scene(3);
386        let block = block_titled(&mut scene, "block_1_1");
387        let delta = vec2(GRID_SIZE, 0.0);
388
389        let mut raised = Foreground::raising([ShapeId::Rect(block)], &scene.drawing());
390        let heading_for = scene
391            .drawing()
392            .shape(ShapeId::Rect(block))
393            .expect("the block")
394            .gui_rect()
395            .translate(delta);
396        raised.also_disturbed_by(heading_for, &scene.drawing());
397
398        scene.commit(|drawing| drawing.move_shape(ShapeId::Rect(block), delta));
399        assert!(
400            !scene.committed().is_empty(),
401            "the move sealed nothing; the test would prove nothing"
402        );
403
404        let committed = scene.committed().to_vec();
405        let drawing = scene.drawing();
406        let escaped: Vec<EntityRef> = raised.escapees(&committed, &drawing).collect();
407        assert!(
408            escaped.is_empty(),
409            "wrote outside the foreground: {escaped:?}"
410        );
411    }
412
413    /// And the churn is gone with it: a one-cell move rewrites a handful of
414    /// wires, not most of the sheet.
415    #[test]
416    fn a_one_cell_move_rewrites_only_the_wires_it_disturbs() {
417        let mut scene = scale_scene(3);
418        let block = block_titled(&mut scene, "block_1_1");
419        let all = scene.drawing().scope_route_ids().len();
420
421        scene.commit(|drawing| {
422            drawing.move_shape(ShapeId::Rect(block), vec2(GRID_SIZE, 0.0));
423        });
424
425        let rewritten = scene
426            .committed()
427            .iter()
428            .filter(|op| matches!(op.target(), EntityRef::Route(_)))
429            .count();
430        assert!(
431            rewritten * 2 < all,
432            "rewrote {rewritten} of {all} wires — the solve is not scoped"
433        );
434    }
435}