1use ahash::HashSet;
6use blockworx_geom::Vec2;
7
8use blockworx_doc::{
9 geometry::GridVec,
10 id::{BlockId, PinId},
11};
12
13use crate::{
14 edit::geometry::{self as edit_geometry, RouteEdit, RouteEnd, 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 constrain_move(&self, shape: ShapeId, delta: Vec2) -> Vec2 {
25 match shape {
26 ShapeId::Icon(rid) => match (self.icon(rid), self.block(rid)) {
27 (Some(icon), Some(block)) => {
28 let box_now = artwork_rect(icon.rect);
29 let contained = crate::shape::block::contain_rect(
30 box_now.translate(delta),
31 px_rect(block.rect),
32 );
33 contained.min - box_now.min
34 }
35 _ => delta,
36 },
37 _ => delta,
38 }
39 }
40
41 pub fn move_shape(&mut self, id: ShapeId, delta: Vec2) {
45 self.author("move_shape", |indexed, sink| {
46 edit_geometry::move_shape(indexed, Shape::from(id), delta, sink);
47 });
48 }
49
50 pub(super) fn riding_pins(
55 &self,
56 moved_blocks: &HashSet<BlockId>,
57 moved_ports: &HashSet<PinId>,
58 ) -> HashSet<PinId> {
59 let mut pins: HashSet<PinId> = moved_ports.clone();
60 for &block in moved_blocks {
61 pins.extend(
62 self.block_pins(crate::path::Scope::Block(block))
63 .into_iter()
64 .map(|(id, _)| id),
65 );
66 }
67 pins
68 }
69
70 fn straddling_routes(
75 &self,
76 riding: &HashSet<PinId>,
77 ) -> Vec<(blockworx_doc::id::RouteId, Option<RouteEnd>)> {
78 self.scope_route_ids()
79 .into_iter()
80 .filter_map(|id| {
81 let route = self.route(id)?;
82 let end = match (riding.contains(&route.from), riding.contains(&route.to)) {
83 (true, false) => Some(RouteEnd::From),
84 (false, true) => Some(RouteEnd::To),
85 (true, true) => None,
86 (false, false) => return None,
87 };
88 Some((id, end))
89 })
90 .collect()
91 }
92
93 pub fn trim_partial_route_approaches(&mut self, moved: &[ShapeId]) {
102 let (moved_rects, moved_pins, _) = moved_set_and_delta(moved, Vec2::ZERO);
103 let riding = self.riding_pins(&moved_rects, &moved_pins);
104 self.trim_approaches(&riding);
105 }
106
107 pub fn trim_anchor_approach(&mut self, pin: PinId) {
110 self.trim_approaches(&HashSet::from_iter([pin]));
111 }
112
113 fn trim_approaches(&mut self, riding: &HashSet<PinId>) {
114 let straddling = self.straddling_routes(riding);
115 for (id, end) in straddling {
116 let Some(end) = end else { continue };
117 let Some(route) = self.route(id) else {
118 continue;
119 };
120 let trimmed = edit_geometry::trimmed_approach(&route.waypoints, end);
121 self.author("trim_partial_route_approaches", |indexed, sink| {
122 edit_geometry::commit_route_edit(
123 indexed.doc,
124 RouteEdit {
125 route: id,
126 waypoints: trimmed,
127 anchors: &[],
128 },
129 sink,
130 );
131 });
132 }
133 }
134
135 pub fn move_shapes(&mut self, ids: &[ShapeId], delta: Vec2) {
142 let members: Vec<Shape> = ids.iter().copied().map(Shape::from).collect();
143 self.author("move_shapes", |indexed, sink| {
144 edit_geometry::move_group(indexed, &members, delta, sink);
145 });
146 }
147}
148
149pub(super) fn moved_set_and_delta(
156 ids: &[ShapeId],
157 delta: Vec2,
158) -> (HashSet<BlockId>, HashSet<PinId>, GridVec) {
159 let moved_rects: HashSet<BlockId> = ids
160 .iter()
161 .filter_map(|id| match id {
162 ShapeId::Rect(rid) => Some(*rid),
163 _ => None,
164 })
165 .collect();
166 let moved_pins: HashSet<PinId> = ids
167 .iter()
168 .filter_map(|id| match id {
169 ShapeId::Port(pid) => Some(*pid),
170 _ => None,
171 })
172 .collect();
173 let snapped = snap_to_grid(delta.to_pos2()).to_vec2();
174 let grid_delta = GridVec::new(
175 (snapped.x / GRID_SIZE).round() as i32,
176 (snapped.y / GRID_SIZE).round() as i32,
177 );
178 (moved_rects, moved_pins, grid_delta)
179}
180
181#[cfg(test)]
182mod tests {
183 use blockworx_doc::{
184 fixtures::{block_id, image_id, route_id},
185 geometry::GridPoint,
186 id::BlockId,
187 values::PinSide,
188 };
189 use blockworx_geom::{Pos2, Rect, pos2, vec2};
190
191 use crate::{
192 grid::{GRID_SIZE, artwork_rect},
193 path::Scope,
194 shape::ShapeId,
195 widget::{
196 drawing::Drawing,
197 test_fixtures::{self as fx, Scene, two_blocks_with_a_routed_waypoint},
198 },
199 };
200
201 fn corners(drawing: &Drawing<'_>, rid: blockworx_doc::id::RouteId) -> Vec<GridPoint> {
202 drawing
203 .auto_route(rid)
204 .expect("the route is in this scope")
205 .route
206 .waypoints
207 .iter()
208 .map(|wp| wp.pos)
209 .collect()
210 }
211
212 fn icon_min(scene: &mut Scene, id: BlockId) -> Pos2 {
213 artwork_rect(
214 scene
215 .drawing()
216 .icon(id)
217 .expect("the block carries an icon")
218 .rect,
219 )
220 .min
221 }
222
223 fn image_rect(scene: &mut Scene, id: blockworx_doc::id::ImageId) -> Rect {
224 artwork_rect(
225 scene
226 .drawing()
227 .image(id)
228 .expect("the image is in this scope")
229 .rect,
230 )
231 }
232
233 fn image_min(scene: &mut Scene, id: blockworx_doc::id::ImageId) -> Pos2 {
234 image_rect(scene, id).min
235 }
236
237 fn block_rect(drawing: &Drawing<'_>, id: BlockId) -> Rect {
238 drawing
239 .shape(ShapeId::Rect(id))
240 .expect("the block is in this scope")
241 .gui_rect()
242 }
243
244 fn settled_row() -> Scene {
249 let tall = |n, x: f32| {
250 fx::block_in(
251 n,
252 Scope::Root,
253 Rect::from_min_max(pos2(x, 0.0), pos2(x + 40.0, 120.0)),
254 )
255 };
256 let mut scene = Scene::new(vec![
257 tall(1, 0.0),
258 tall(2, 300.0),
259 tall(3, 600.0),
260 fx::pin(4, 1, PinSide::East, 0),
261 fx::pin(5, 2, PinSide::West, 1),
262 fx::pin(6, 2, PinSide::East, 0),
263 fx::pin(9, 3, PinSide::West, 1),
264 fx::route(7, Scope::Root, 4, 5, &[(10, 4)]),
268 fx::route(8, Scope::Root, 6, 9, &[(30, 4)]),
269 ]);
270 fx::materialize(&mut scene);
271 scene.commit(|drawing| {
272 drawing.reroute(route_id(7));
273 drawing.reroute(route_id(8));
274 });
275 scene
276 }
277
278 fn shifted(corners: &[GridPoint], dy: i32) -> Vec<GridPoint> {
279 corners
280 .iter()
281 .map(|p| GridPoint {
282 x: p.x,
283 y: p.y + dy,
284 })
285 .collect()
286 }
287
288 #[test]
289 fn move_shapes_moves_fully_selected_route_waypoints() {
290 let mut scene = settled_row();
293 let before = corners(&scene.drawing(), route_id(7));
294 assert!(!before.is_empty(), "the settled wire has a shape to carry");
295
296 scene.commit(|drawing| {
297 drawing.move_shapes(
298 &[ShapeId::Rect(block_id(1)), ShapeId::Rect(block_id(2))],
299 vec2(0.0, 4.0 * GRID_SIZE),
300 );
301 });
302
303 assert_eq!(
304 corners(&scene.drawing(), route_id(7)),
305 shifted(&before, 4),
306 "every corner shifted by the (0, 4) grid delta the blocks moved"
307 );
308 }
309
310 #[test]
311 fn move_shapes_group_translates_internal_routes_and_trims_boundary_ones() {
312 let (a, b) = (block_id(1), block_id(2));
317 let (internal_id, boundary_id) = (route_id(7), route_id(8));
318 let mut scene = settled_row();
319 let internal_before = corners(&scene.drawing(), internal_id);
320 let boundary_before = corners(&scene.drawing(), boundary_id);
321 assert!(!internal_before.is_empty() && !boundary_before.is_empty());
322
323 scene.commit(|drawing| {
324 drawing.move_shapes(
325 &[ShapeId::Rect(a), ShapeId::Rect(b)],
326 vec2(0.0, 4.0 * GRID_SIZE),
327 );
328 });
329
330 let drawing = scene.drawing();
331 assert_eq!(
332 corners(&drawing, internal_id),
333 shifted(&internal_before, 4),
334 "the fully-selected route travelled rigidly with its endpoints"
335 );
336 assert_ne!(
337 corners(&drawing, boundary_id),
338 shifted(&boundary_before, 4),
339 "the straddling route re-approached instead of translating"
340 );
341 }
342
343 #[test]
344 fn move_shapes_trims_a_partially_selected_route_approach() {
345 let mut scene = two_blocks_with_a_routed_waypoint();
352
353 scene.commit(|drawing| {
354 drawing.move_shapes(&[ShapeId::Rect(block_id(1))], vec2(GRID_SIZE, 0.0));
355 });
356
357 let after = corners(&scene.drawing(), route_id(5));
358 assert!(
359 after.is_empty(),
360 "the stale approach waypoint was trimmed; got {after:?}"
361 );
362 }
363
364 #[test]
365 fn move_shapes_moves_an_image_freely() {
366 let sid = image_id(1);
367 let mut scene = Scene::new(vec![
368 fx::asset().1,
369 fx::image(
370 1,
371 Scope::Root,
372 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 40.0)),
373 ),
374 ]);
375 let before = image_min(&mut scene, sid);
376
377 let delta = vec2(GRID_SIZE, 2.0 * GRID_SIZE);
378 scene.commit(|drawing| drawing.move_shapes(&[ShapeId::Image(sid)], delta));
379
380 assert_eq!(
381 image_min(&mut scene, sid),
382 before + delta,
383 "the image moves freely (no grid snap)"
384 );
385 }
386
387 #[test]
388 fn a_background_image_does_not_move_with_a_block() {
389 let (a, inside) = (block_id(1), image_id(2));
392 let mut scene = Scene::new(vec![
393 fx::block(1, 0.0), fx::asset().1,
395 fx::image(
396 2,
397 Scope::Root,
398 Rect::from_min_max(pos2(15.0, 15.0), pos2(25.0, 25.0)),
399 ),
400 ]);
401 let before = image_min(&mut scene, inside);
402 let block_before = block_rect(&scene.drawing(), a);
403
404 scene.commit(|drawing| drawing.move_shape(ShapeId::Rect(a), vec2(4.0 * GRID_SIZE, 0.0)));
405
406 assert_ne!(block_rect(&scene.drawing(), a), block_before);
409 assert_eq!(
410 image_min(&mut scene, inside),
411 before,
412 "a background image is not dragged by a moving block"
413 );
414 }
415
416 #[test]
417 fn an_icon_moves_with_its_block() {
418 let a = block_id(1);
420 let mut scene = Scene::new(vec![
421 fx::block(1, 0.0), fx::asset().1,
423 fx::icon(1, Rect::from_min_max(pos2(8.0, 8.0), pos2(32.0, 32.0))),
424 ]);
425 let before = icon_min(&mut scene, a);
426
427 scene.commit(|drawing| drawing.move_shape(ShapeId::Rect(a), vec2(4.0 * GRID_SIZE, 0.0)));
428
429 assert_eq!(
430 icon_min(&mut scene, a),
431 before + vec2(4.0 * GRID_SIZE, 0.0),
432 "the icon travels with its block"
433 );
434 }
435
436 #[test]
437 fn move_icon_is_clamped_inside_its_block() {
438 let a = block_id(1);
439 let mut scene = Scene::new(vec![
440 fx::block(1, 0.0), fx::asset().1,
442 fx::icon(1, Rect::from_min_max(pos2(8.0, 8.0), pos2(32.0, 32.0))),
443 ]);
444 let before = icon_min(&mut scene, a);
445 scene.commit(|drawing| drawing.move_shape(ShapeId::Icon(a), vec2(1000.0, 1000.0)));
447
448 let drawing = scene.drawing();
449 let icon = artwork_rect(drawing.icon(a).unwrap().rect);
450 assert_ne!(icon.min, before);
453 assert!(
454 block_rect(&drawing, a).contains_rect(icon),
455 "the icon stays within its block"
456 );
457 }
458
459 #[test]
460 fn an_icon_co_selected_with_its_block_is_not_moved_twice() {
461 let a = block_id(1);
465 let mut scene = Scene::new(vec![
466 fx::block(1, 0.0), fx::asset().1,
468 fx::icon(1, Rect::from_min_max(pos2(8.0, 8.0), pos2(32.0, 32.0))),
469 ]);
470 let before = icon_min(&mut scene, a);
471
472 scene.commit(|drawing| {
473 drawing.move_shapes(
474 &[ShapeId::Rect(a), ShapeId::Icon(a)],
475 vec2(4.0 * GRID_SIZE, 0.0),
476 );
477 });
478
479 assert_eq!(
480 icon_min(&mut scene, a),
481 before + vec2(4.0 * GRID_SIZE, 0.0),
482 "the icon shifts by one block-move, not two"
483 );
484 }
485
486 #[test]
487 fn an_image_never_blocks_a_block_move() {
488 let a = block_id(1);
491 let mut scene = Scene::new(vec![
492 fx::block(1, 0.0), fx::asset().1,
494 fx::image(
496 2,
497 Scope::Root,
498 Rect::from_min_max(pos2(40.0, 0.0), pos2(80.0, 40.0)),
499 ),
500 ]);
501 let before = block_rect(&scene.drawing(), a);
502 assert!(
504 image_rect(&mut scene, image_id(2))
505 .intersects(before.translate(vec2(2.0 * GRID_SIZE, 0.0)))
506 );
507
508 scene.commit(|drawing| drawing.move_shape(ShapeId::Rect(a), vec2(2.0 * GRID_SIZE, 0.0)));
509
510 assert_ne!(
511 block_rect(&scene.drawing(), a),
512 before,
513 "the block moved despite the overlapping image"
514 );
515 }
516
517 #[test]
518 fn an_already_overlapping_group_can_be_moved() {
519 let (copy_a, copy_b) = (block_id(3), block_id(4));
523 let mut scene = Scene::new(vec![
524 fx::block(1, 0.0),
525 fx::block(2, 120.0),
526 fx::block(3, 30.0), fx::block(4, 150.0), ]);
529 let pasted = [ShapeId::Rect(copy_a), ShapeId::Rect(copy_b)];
530 let before: Vec<Rect> = {
531 let drawing = scene.drawing();
532 pasted
533 .iter()
534 .map(|&id| drawing.shape(id).unwrap().gui_rect())
535 .collect()
536 };
537 assert!(before[0].intersects(block_rect(&scene.drawing(), block_id(1))));
540
541 scene.commit(|drawing| drawing.move_shapes(&pasted, vec2(GRID_SIZE, 0.0)));
542
543 let drawing = scene.drawing();
544 for (&id, b) in pasted.iter().zip(before) {
545 assert_eq!(
546 drawing.shape(id).unwrap().gui_rect(),
547 b.translate(vec2(GRID_SIZE, 0.0)),
548 "each pasted block shifts one grid cell; the move is not a no-op"
549 );
550 }
551 }
552
553 #[test]
554 fn move_shapes_still_blocks_a_move_into_a_clear_neighbor() {
555 let a = block_id(1);
558 let mut scene = Scene::new(vec![
559 fx::block(1, 0.0), fx::block(2, 120.0), ]);
562 let before = block_rect(&scene.drawing(), a);
563 assert!(!before.intersects(block_rect(&scene.drawing(), block_id(2))));
565 assert!(
566 before
567 .translate(vec2(120.0, 0.0))
568 .intersects(block_rect(&scene.drawing(), block_id(2)))
569 );
570
571 scene.commit(|drawing| drawing.move_shapes(&[ShapeId::Rect(a)], vec2(120.0, 0.0)));
573
574 assert_eq!(
575 block_rect(&scene.drawing(), a),
576 before,
577 "the move into a clear neighbor is rejected"
578 );
579 }
580
581 #[test]
582 fn move_shapes_allows_a_nudge_that_does_not_worsen_overlap() {
583 let a = block_id(1);
586 let mut scene = Scene::new(vec![
587 fx::block(1, 0.0), fx::block(2, 10.0), ]);
590 let before = block_rect(&scene.drawing(), a);
591 assert!(before.intersects(block_rect(&scene.drawing(), block_id(2))));
593
594 scene.commit(|drawing| drawing.move_shapes(&[ShapeId::Rect(a)], vec2(GRID_SIZE, 0.0)));
595
596 assert_eq!(
597 block_rect(&scene.drawing(), a),
598 before.translate(vec2(GRID_SIZE, 0.0)),
599 "an already-overlapping member is still allowed to move"
600 );
601 }
602}