1use ahash::HashSet;
6use blockworx_geom::Vec2;
7
8use blockworx_doc::{
9 geometry::{GridRect, GridVec},
10 id::{BlockId, PinId},
11};
12
13use crate::{
14 edit::geometry::{self as edit_geometry, Moving, Shape},
15 grid::{GRID_SIZE, artwork_rect, px_rect, snap_to_grid},
16 shape::ShapeId,
17 widget::drawing::Drawing,
18};
19
20impl Drawing<'_> {
21 pub fn conflicts(&self, shapes: &[ShapeId], delta: Vec2, moving: Moving) -> Vec<GridRect> {
28 let members: Vec<Shape> = shapes.iter().copied().map(Shape::from).collect();
29 edit_geometry::move_conflicts(&self.indexed(), &members, delta, moving)
30 }
31
32 pub fn constrain_move(&self, shape: ShapeId, delta: Vec2) -> Vec2 {
36 match shape {
37 ShapeId::Icon(rid) => match (self.icon(rid), self.block(rid)) {
38 (Some(icon), Some(block)) => {
39 let box_now = artwork_rect(icon.rect);
40 let contained = crate::shape::block::contain_rect(
41 box_now.translate(delta),
42 px_rect(block.rect),
43 );
44 contained.min - box_now.min
45 }
46 _ => delta,
47 },
48 _ => delta,
49 }
50 }
51
52 pub fn move_shape(&mut self, id: ShapeId, delta: Vec2) {
56 let _s = tracing::info_span!("emit_move").entered();
57 self.author("move_shape", |indexed, sink| {
58 edit_geometry::move_shape(indexed, Shape::from(id), delta, sink);
59 });
60 }
61
62 pub(super) fn riding_pins(
67 &self,
68 moved_blocks: &HashSet<BlockId>,
69 moved_ports: &HashSet<PinId>,
70 ) -> HashSet<PinId> {
71 let mut pins: HashSet<PinId> = moved_ports.clone();
72 for &block in moved_blocks {
73 pins.extend(
74 self.block_pins(crate::path::Scope::Block(block))
75 .into_iter()
76 .map(|(id, _)| id),
77 );
78 }
79 pins
80 }
81
82 #[tracing::instrument(level = "info", skip_all)]
91 pub fn trim_partial_route_approaches(&mut self, moved: &[ShapeId]) {
92 let (moved_rects, moved_pins, _) = moved_set_and_delta(moved, Vec2::ZERO);
93 let riding = self.riding_pins(&moved_rects, &moved_pins);
94 self.trim_approaches(&riding);
95 }
96
97 pub fn trim_anchor_approach(&mut self, pin: PinId) {
100 self.trim_approaches(&HashSet::from_iter([pin]));
101 }
102
103 fn trim_approaches(&mut self, riding: &HashSet<PinId>) {
108 let scopes: HashSet<_> = [self.current_scope().wire_id()].into_iter().collect();
109 self.author("trim_approaches", |indexed, sink| {
110 edit_geometry::push_route_riders(indexed, riding, &scopes, GridVec::ZERO, sink);
111 });
112 }
113
114 pub fn move_shapes(&mut self, ids: &[ShapeId], delta: Vec2) {
121 let members: Vec<Shape> = ids.iter().copied().map(Shape::from).collect();
122 self.author("move_shapes", |indexed, sink| {
123 edit_geometry::move_group(indexed, &members, delta, sink);
124 });
125 }
126}
127
128pub(super) fn moved_set_and_delta(
135 ids: &[ShapeId],
136 delta: Vec2,
137) -> (HashSet<BlockId>, HashSet<PinId>, GridVec) {
138 let moved_rects: HashSet<BlockId> = ids
139 .iter()
140 .filter_map(|id| match id {
141 ShapeId::Rect(rid) => Some(*rid),
142 _ => None,
143 })
144 .collect();
145 let moved_pins: HashSet<PinId> = ids
146 .iter()
147 .filter_map(|id| match id {
148 ShapeId::Port(pid) => Some(*pid),
149 _ => None,
150 })
151 .collect();
152 let snapped = snap_to_grid(delta.to_pos2()).to_vec2();
153 let grid_delta = GridVec::new(
154 (snapped.x / GRID_SIZE).round() as i32,
155 (snapped.y / GRID_SIZE).round() as i32,
156 );
157 (moved_rects, moved_pins, grid_delta)
158}
159
160#[cfg(test)]
161mod tests {
162 use blockworx_doc::{
163 fixtures::{block_id, image_id, route_id},
164 geometry::GridPoint,
165 id::BlockId,
166 values::PinSide,
167 };
168 use blockworx_geom::{Pos2, Rect, pos2, vec2};
169
170 use crate::{
171 grid::{GRID_SIZE, artwork_rect},
172 path::Scope,
173 shape::ShapeId,
174 widget::{
175 drawing::Drawing,
176 test_fixtures::{self as fx, Scene, two_blocks_with_a_routed_waypoint},
177 },
178 };
179
180 fn corners(drawing: &Drawing<'_>, rid: blockworx_doc::id::RouteId) -> Vec<GridPoint> {
181 drawing
182 .auto_route(rid)
183 .expect("the route is in this scope")
184 .route
185 .waypoints
186 .iter()
187 .map(|wp| wp.pos)
188 .collect()
189 }
190
191 fn icon_min(scene: &mut Scene, id: BlockId) -> Pos2 {
192 artwork_rect(
193 scene
194 .drawing()
195 .icon(id)
196 .expect("the block carries an icon")
197 .rect,
198 )
199 .min
200 }
201
202 fn image_rect(scene: &mut Scene, id: blockworx_doc::id::ImageId) -> Rect {
203 artwork_rect(
204 scene
205 .drawing()
206 .image(id)
207 .expect("the image is in this scope")
208 .rect,
209 )
210 }
211
212 fn image_min(scene: &mut Scene, id: blockworx_doc::id::ImageId) -> Pos2 {
213 image_rect(scene, id).min
214 }
215
216 fn block_rect(drawing: &Drawing<'_>, id: BlockId) -> Rect {
217 drawing
218 .shape(ShapeId::Rect(id))
219 .expect("the block is in this scope")
220 .gui_rect()
221 }
222
223 fn settled_row() -> Scene {
228 let tall = |n, x: f32| {
229 fx::block_in(
230 n,
231 Scope::Root,
232 Rect::from_min_max(pos2(x, 0.0), pos2(x + 40.0, 120.0)),
233 )
234 };
235 let mut scene = Scene::new(vec![
236 tall(1, 0.0),
237 tall(2, 300.0),
238 tall(3, 600.0),
239 fx::pin(4, 1, PinSide::East, 0),
240 fx::pin(5, 2, PinSide::West, 1),
241 fx::pin(6, 2, PinSide::East, 0),
242 fx::pin(9, 3, PinSide::West, 1),
243 fx::route(7, Scope::Root, 4, 5, &[(10, 4)]),
247 fx::route(8, Scope::Root, 6, 9, &[(30, 4)]),
248 ]);
249 fx::reconstruct(&mut scene);
250 scene.commit(|drawing| {
251 drawing.reroute(route_id(7));
252 drawing.reroute(route_id(8));
253 });
254 scene
255 }
256
257 fn shifted(corners: &[GridPoint], dy: i32) -> Vec<GridPoint> {
258 corners
259 .iter()
260 .map(|p| GridPoint {
261 x: p.x,
262 y: p.y + dy,
263 })
264 .collect()
265 }
266
267 #[test]
268 fn move_shapes_moves_fully_selected_route_waypoints() {
269 let mut scene = settled_row();
272 let before = corners(&scene.drawing(), route_id(7));
273 assert!(!before.is_empty(), "the settled wire has a shape to carry");
274
275 scene.commit(|drawing| {
276 drawing.move_shapes(
277 &[ShapeId::Rect(block_id(1)), ShapeId::Rect(block_id(2))],
278 vec2(0.0, 4.0 * GRID_SIZE),
279 );
280 });
281
282 assert_eq!(
283 corners(&scene.drawing(), route_id(7)),
284 shifted(&before, 4),
285 "every corner shifted by the (0, 4) grid delta the blocks moved"
286 );
287 }
288
289 #[test]
290 fn move_shapes_group_translates_internal_routes_and_trims_boundary_ones() {
291 let (a, b) = (block_id(1), block_id(2));
296 let (internal_id, boundary_id) = (route_id(7), route_id(8));
297 let mut scene = settled_row();
298 let internal_before = corners(&scene.drawing(), internal_id);
299 let boundary_before = corners(&scene.drawing(), boundary_id);
300 assert!(!internal_before.is_empty() && !boundary_before.is_empty());
301
302 scene.commit(|drawing| {
303 drawing.move_shapes(
304 &[ShapeId::Rect(a), ShapeId::Rect(b)],
305 vec2(0.0, 4.0 * GRID_SIZE),
306 );
307 });
308
309 let drawing = scene.drawing();
310 assert_eq!(
311 corners(&drawing, internal_id),
312 shifted(&internal_before, 4),
313 "the fully-selected route travelled rigidly with its endpoints"
314 );
315 assert_ne!(
316 corners(&drawing, boundary_id),
317 shifted(&boundary_before, 4),
318 "the straddling route re-approached instead of translating"
319 );
320 }
321
322 #[test]
323 fn move_shapes_trims_a_partially_selected_route_approach() {
324 let mut scene = two_blocks_with_a_routed_waypoint();
331
332 scene.commit(|drawing| {
333 drawing.move_shapes(&[ShapeId::Rect(block_id(1))], vec2(GRID_SIZE, 0.0));
334 });
335
336 let after = corners(&scene.drawing(), route_id(5));
337 assert!(
338 after.is_empty(),
339 "the stale approach waypoint was trimmed; got {after:?}"
340 );
341 }
342
343 #[test]
344 fn move_shapes_moves_an_image_freely() {
345 let sid = image_id(1);
346 let mut scene = Scene::new(vec![
347 fx::asset().1,
348 fx::image(
349 1,
350 Scope::Root,
351 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 40.0)),
352 ),
353 ]);
354 let before = image_min(&mut scene, sid);
355
356 let delta = vec2(GRID_SIZE, 2.0 * GRID_SIZE);
357 scene.commit(|drawing| drawing.move_shapes(&[ShapeId::Image(sid)], delta));
358
359 assert_eq!(
360 image_min(&mut scene, sid),
361 before + delta,
362 "the image moves freely (no grid snap)"
363 );
364 }
365
366 #[test]
367 fn a_background_image_does_not_move_with_a_block() {
368 let (a, inside) = (block_id(1), image_id(2));
371 let mut scene = Scene::new(vec![
372 fx::block(1, 0.0), fx::asset().1,
374 fx::image(
375 2,
376 Scope::Root,
377 Rect::from_min_max(pos2(15.0, 15.0), pos2(25.0, 25.0)),
378 ),
379 ]);
380 let before = image_min(&mut scene, inside);
381 let block_before = block_rect(&scene.drawing(), a);
382
383 scene.commit(|drawing| drawing.move_shape(ShapeId::Rect(a), vec2(4.0 * GRID_SIZE, 0.0)));
384
385 assert_ne!(block_rect(&scene.drawing(), a), block_before);
388 assert_eq!(
389 image_min(&mut scene, inside),
390 before,
391 "a background image is not dragged by a moving block"
392 );
393 }
394
395 #[test]
396 fn an_icon_moves_with_its_block() {
397 let a = block_id(1);
399 let mut scene = Scene::new(vec![
400 fx::block(1, 0.0), fx::asset().1,
402 fx::icon(1, Rect::from_min_max(pos2(8.0, 8.0), pos2(32.0, 32.0))),
403 ]);
404 let before = icon_min(&mut scene, a);
405
406 scene.commit(|drawing| drawing.move_shape(ShapeId::Rect(a), vec2(4.0 * GRID_SIZE, 0.0)));
407
408 assert_eq!(
409 icon_min(&mut scene, a),
410 before + vec2(4.0 * GRID_SIZE, 0.0),
411 "the icon travels with its block"
412 );
413 }
414
415 #[test]
416 fn move_icon_is_clamped_inside_its_block() {
417 let a = block_id(1);
418 let mut scene = Scene::new(vec![
419 fx::block(1, 0.0), fx::asset().1,
421 fx::icon(1, Rect::from_min_max(pos2(8.0, 8.0), pos2(32.0, 32.0))),
422 ]);
423 let before = icon_min(&mut scene, a);
424 scene.commit(|drawing| drawing.move_shape(ShapeId::Icon(a), vec2(1000.0, 1000.0)));
426
427 let drawing = scene.drawing();
428 let icon = artwork_rect(drawing.icon(a).unwrap().rect);
429 assert_ne!(icon.min, before);
432 assert!(
433 block_rect(&drawing, a).contains_rect(icon),
434 "the icon stays within its block"
435 );
436 }
437
438 #[test]
439 fn an_icon_co_selected_with_its_block_is_not_moved_twice() {
440 let a = block_id(1);
444 let mut scene = Scene::new(vec![
445 fx::block(1, 0.0), fx::asset().1,
447 fx::icon(1, Rect::from_min_max(pos2(8.0, 8.0), pos2(32.0, 32.0))),
448 ]);
449 let before = icon_min(&mut scene, a);
450
451 scene.commit(|drawing| {
452 drawing.move_shapes(
453 &[ShapeId::Rect(a), ShapeId::Icon(a)],
454 vec2(4.0 * GRID_SIZE, 0.0),
455 );
456 });
457
458 assert_eq!(
459 icon_min(&mut scene, a),
460 before + vec2(4.0 * GRID_SIZE, 0.0),
461 "the icon shifts by one block-move, not two"
462 );
463 }
464
465 #[test]
466 fn an_image_never_blocks_a_block_move() {
467 let a = block_id(1);
470 let mut scene = Scene::new(vec![
471 fx::block(1, 0.0), fx::asset().1,
473 fx::image(
475 2,
476 Scope::Root,
477 Rect::from_min_max(pos2(40.0, 0.0), pos2(80.0, 40.0)),
478 ),
479 ]);
480 let before = block_rect(&scene.drawing(), a);
481 assert!(
483 image_rect(&mut scene, image_id(2))
484 .intersects(before.translate(vec2(2.0 * GRID_SIZE, 0.0)))
485 );
486
487 scene.commit(|drawing| drawing.move_shape(ShapeId::Rect(a), vec2(2.0 * GRID_SIZE, 0.0)));
488
489 assert_ne!(
490 block_rect(&scene.drawing(), a),
491 before,
492 "the block moved despite the overlapping image"
493 );
494 }
495
496 #[test]
497 fn an_already_overlapping_group_can_be_moved() {
498 let (copy_a, copy_b) = (block_id(3), block_id(4));
502 let mut scene = Scene::new(vec![
503 fx::block(1, 0.0),
504 fx::block(2, 120.0),
505 fx::block(3, 30.0), fx::block(4, 150.0), ]);
508 let pasted = [ShapeId::Rect(copy_a), ShapeId::Rect(copy_b)];
509 let before: Vec<Rect> = {
510 let drawing = scene.drawing();
511 pasted
512 .iter()
513 .map(|&id| drawing.shape(id).unwrap().gui_rect())
514 .collect()
515 };
516 assert!(before[0].intersects(block_rect(&scene.drawing(), block_id(1))));
519
520 scene.commit(|drawing| drawing.move_shapes(&pasted, vec2(GRID_SIZE, 0.0)));
521
522 let drawing = scene.drawing();
523 for (&id, b) in pasted.iter().zip(before) {
524 assert_eq!(
525 drawing.shape(id).unwrap().gui_rect(),
526 b.translate(vec2(GRID_SIZE, 0.0)),
527 "each pasted block shifts one grid cell; the move is not a no-op"
528 );
529 }
530 }
531
532 #[test]
533 fn move_shapes_still_blocks_a_move_into_a_clear_neighbor() {
534 let a = block_id(1);
537 let mut scene = Scene::new(vec![
538 fx::block(1, 0.0), fx::block(2, 120.0), ]);
541 let before = block_rect(&scene.drawing(), a);
542 assert!(!before.intersects(block_rect(&scene.drawing(), block_id(2))));
544 assert!(
545 before
546 .translate(vec2(120.0, 0.0))
547 .intersects(block_rect(&scene.drawing(), block_id(2)))
548 );
549
550 scene.commit(|drawing| drawing.move_shapes(&[ShapeId::Rect(a)], vec2(120.0, 0.0)));
552
553 assert_eq!(
554 block_rect(&scene.drawing(), a),
555 before,
556 "the move into a clear neighbor is rejected"
557 );
558 }
559
560 #[test]
561 fn move_shapes_allows_a_nudge_that_does_not_worsen_overlap() {
562 let a = block_id(1);
565 let mut scene = Scene::new(vec![
566 fx::block(1, 0.0), fx::block(2, 10.0), ]);
569 let before = block_rect(&scene.drawing(), a);
570 assert!(before.intersects(block_rect(&scene.drawing(), block_id(2))));
572
573 scene.commit(|drawing| drawing.move_shapes(&[ShapeId::Rect(a)], vec2(GRID_SIZE, 0.0)));
574
575 assert_eq!(
576 block_rect(&scene.drawing(), a),
577 before.translate(vec2(GRID_SIZE, 0.0)),
578 "an already-overlapping member is still allowed to move"
579 );
580 }
581}