1use 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#[derive(Debug, Default, Clone, PartialEq, Eq)]
37pub struct Foreground {
38 shapes: BTreeSet<ShapeId>,
39 routes: BTreeSet<RouteId>,
40}
41
42impl Foreground {
43 pub fn raising(shapes: impl IntoIterator<Item = ShapeId>, drawing: &Drawing<'_>) -> Self {
49 Self::derive(shapes.into_iter().collect(), BTreeSet::new(), drawing)
50 }
51
52 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 pub fn also_disturbed_by(&mut self, rect: Rect, drawing: &Drawing<'_>) {
100 self.raise_wires_crossing(rect, drawing);
101 }
102
103 #[must_use]
105 pub fn holds_route(&self, id: RouteId) -> bool {
106 self.routes.contains(&id)
107 }
108
109 #[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 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 #[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 pub fn shapes(&self) -> impl Iterator<Item = ShapeId> + '_ {
186 self.shapes.iter().copied()
187 }
188
189 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 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
239fn 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 fn block_id_of(scene: &mut Scene, n: usize) -> BlockId {
266 scene.drawing().child_blocks()[n - 1].0
267 }
268
269 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 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 #[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 #[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 #[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 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 #[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 #[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}