1use std::collections::BTreeMap;
2
3use blockworx_doc::geometry::Waypoint;
4use blockworx_doc::id::{BlockId, PinId};
5use blockworx_geom::Pos2;
6use blockworx_router::{ClosedRouter, point::Point};
7
8use crate::render::render_path_with_chamfered_corners;
9use crate::theme::Style;
10use crate::{
11 edit::create::PathOrdinal,
12 grid::{PORT_RADIUS, grid_point, px_point, snap_to_grid},
13 shape::PinLocation,
14 theme::Role,
15 tools::{
16 SelectTool,
17 names::ToolName,
18 new_pin::{
19 NEW_PIN_ACTIVATION_RANGE, active_new_pin_target, draw_route_new_pin_targets,
20 new_pin_targets,
21 },
22 rename_pin::{Field, RenamePin},
23 route_start,
24 tool::{Action, ToolTrait},
25 },
26 widget::{
27 drawing::Drawing,
28 waypoint_router::{
29 FixedLegs, RouteRequest, SelfCost, TaggedPoint, route_fixed_legs, route_to_head,
30 },
31 },
32};
33use blockworx_paint::{Canvas, Cursor, Event, Interaction};
34fn waypoint_nodes(waypoints: &[Waypoint]) -> (Vec<PathOrdinal>, BTreeMap<PathOrdinal, Point>) {
37 let positions: BTreeMap<PathOrdinal, Point> = waypoints
38 .iter()
39 .enumerate()
40 .map(|(index, wp)| (PathOrdinal::new(index), waypoint_point(wp)))
41 .collect();
42 (positions.keys().copied().collect(), positions)
43}
44
45fn waypoint_point(wp: &Waypoint) -> Point {
46 Point::from(wp.pos)
47}
48
49fn route_in_progress(
55 data: &Drawing,
56 start: Pos2,
57 waypoints: &[Waypoint],
58 end: Pos2,
59) -> Vec<TaggedPoint> {
60 let mut extra: Vec<Point> = Vec::with_capacity(waypoints.len() + 2);
61 extra.push(start.into());
62 extra.push(end.into());
63 extra.extend(waypoints.iter().map(waypoint_point));
64 let mut router = data.scratch_closed_router(&extra);
65 let (wp_ids, wp_positions) = waypoint_nodes(waypoints);
66 RouteRequest {
67 start: start.into(),
68 end: end.into(),
69 wp_ids: &wp_ids,
70 wp_positions: &wp_positions,
71 self_cost: SelfCost::Apply,
72 }
73 .route(&mut router)
74}
75
76#[derive(Default)]
77enum RouteToolState {
78 #[default]
79 Idle,
80 PinHeadHovered {
81 anchor: PinId,
82 },
83 InProgress {
84 start: PinId,
85 waypoints: Vec<Waypoint>,
86 head: Pos2,
87 },
88 Proposed {
89 start: PinId,
90 waypoints: Vec<Waypoint>,
91 finish: FinishTarget,
92 },
93}
94
95#[derive(Clone, Copy)]
99enum FinishTarget {
100 Anchor(PinId),
101 NewPin {
102 block: BlockId,
103 loc: PinLocation,
104 center: Pos2,
105 },
106}
107
108struct PreviewRequest<'a> {
111 start: PinId,
112 start_pos: Pos2,
113 waypoints: &'a [Waypoint],
114 end: Pos2,
115}
116
117struct PreviewCache {
122 start: PinId,
123 waypoint_count: usize,
124 router: ClosedRouter,
125 fixed: FixedLegs,
126}
127
128#[derive(Default)]
129pub struct RouteTool {
130 state: RouteToolState,
131 preview_path: Vec<TaggedPoint>,
132 preview_cache: Option<Box<PreviewCache>>,
138 return_to_select: bool,
142 edit_start_on_commit: bool,
146 pending_name_edit: Option<PinId>,
149}
150
151impl ToolTrait for RouteTool {
152 fn name(&self) -> ToolName {
153 ToolName::Route
154 }
155
156 fn widget<C: Canvas>(
157 &mut self,
158 data: &mut Drawing,
159 interaction: &Interaction,
160 painter: &mut Style<'_, C>,
161 ) -> Option<Action> {
162 let state = std::mem::take(&mut self.state);
165
166 crate::widget::display::widget(data, interaction, painter);
167 painter.set_cursor(Cursor::Crosshair);
168
169 let was_routing = matches!(
170 state,
171 RouteToolState::InProgress { .. } | RouteToolState::Proposed { .. }
172 );
173 let cancelled = was_routing && (interaction.escape_pressed || interaction.delete_pressed);
175
176 self.state = if cancelled {
177 self.preview_path.clear();
178 self.edit_start_on_commit = false;
180 RouteToolState::Idle
181 } else {
182 match state {
183 RouteToolState::Idle => match interaction.event {
184 Some(Event::DragStarted { pos } | Event::Clicked { pos }) => {
185 if let Some(anchor) = data.anchor_at_pos(pos) {
186 RouteToolState::InProgress {
187 start: anchor,
188 waypoints: Vec::new(),
189 head: pos,
190 }
191 } else {
192 RouteToolState::Idle
193 }
194 }
195 Some(Event::HoverAt(pos)) => {
196 if let Some(anchor) = data.anchor_at_pos(pos) {
197 RouteToolState::PinHeadHovered { anchor }
198 } else {
199 RouteToolState::Idle
200 }
201 }
202 _ => RouteToolState::Idle,
203 },
204
205 RouteToolState::PinHeadHovered { anchor } => match interaction.event {
206 Some(Event::DragStarted { pos } | Event::Clicked { pos }) => {
207 RouteToolState::InProgress {
208 start: anchor,
209 waypoints: Vec::new(),
210 head: pos,
211 }
212 }
213 Some(Event::HoverAt(pos)) => {
214 if let Some(new_anchor) = data.anchor_at_pos(pos) {
215 RouteToolState::PinHeadHovered { anchor: new_anchor }
216 } else {
217 RouteToolState::Idle
218 }
219 }
220 _ => RouteToolState::PinHeadHovered { anchor },
221 },
222
223 RouteToolState::InProgress {
224 start,
225 mut waypoints,
226 mut head,
227 } => match interaction.event {
228 Some(Event::Clicked { pos }) => {
229 waypoints.push(Waypoint {
230 pos: grid_point(pos),
231 locked: true,
232 });
233 RouteToolState::InProgress {
234 start,
235 waypoints,
236 head,
237 }
238 }
239 Some(Event::HoverAt(pos) | Event::Dragging { pos, .. }) => {
243 if let Some(finish) = Self::finish_at(data, start, pos) {
244 RouteToolState::Proposed {
245 start,
246 waypoints,
247 finish,
248 }
249 } else {
250 head = pos;
251 RouteToolState::InProgress {
252 start,
253 waypoints,
254 head,
255 }
256 }
257 }
258 Some(Event::DragStopped { pos }) => {
262 if let Some(finish) = Self::finish_at(data, start, pos) {
263 self.commit(data, start, finish, waypoints)
264 } else {
265 head = pos;
266 RouteToolState::InProgress {
267 start,
268 waypoints,
269 head,
270 }
271 }
272 }
273 _ => RouteToolState::InProgress {
274 start,
275 waypoints,
276 head,
277 },
278 },
279
280 RouteToolState::Proposed {
281 start,
282 waypoints,
283 mut finish,
284 } => match interaction.event {
285 Some(Event::Clicked { .. }) => self.commit(data, start, finish, waypoints),
286 Some(Event::DragStopped { pos }) => match Self::finish_at(data, start, pos) {
289 Some(finish) => self.commit(data, start, finish, waypoints),
290 None => RouteToolState::InProgress {
291 start,
292 waypoints,
293 head: pos,
294 },
295 },
296 Some(Event::HoverAt(pos) | Event::Dragging { pos, .. }) => {
297 match Self::finish_at(data, start, pos) {
298 Some(new_finish) => {
299 finish = new_finish;
300 RouteToolState::Proposed {
301 start,
302 waypoints,
303 finish,
304 }
305 }
306 None => RouteToolState::InProgress {
307 start,
308 waypoints,
309 head: pos,
310 },
311 }
312 }
313 _ => RouteToolState::Proposed {
314 start,
315 waypoints,
316 finish,
317 },
318 },
319 }
320 };
321
322 self.update_preview(data);
323 self.render(data, painter);
324 let committed = was_routing && !cancelled && matches!(self.state, RouteToolState::Idle);
329 if committed {
330 painter.request_repaint();
331 }
332 if let Some(anchor) = self.pending_name_edit.take()
335 && let Some(tool) = RenamePin::new_with_anchor(data, anchor, Field::Name)
336 {
337 return Some(Action::SwitchTool(tool.into()));
338 }
339 if self.return_to_select && was_routing && matches!(self.state, RouteToolState::Idle) {
342 return Some(Action::SwitchTool(SelectTool.into()));
343 }
344 None
345 }
346}
347
348impl RouteTool {
349 pub fn routing_from(start: PinId, head: Pos2) -> Self {
354 Self {
355 state: RouteToolState::InProgress {
356 start,
357 waypoints: Vec::new(),
358 head,
359 },
360 preview_path: Vec::new(),
361 preview_cache: None,
362 return_to_select: false,
363 edit_start_on_commit: false,
364 pending_name_edit: None,
365 }
366 }
367
368 pub fn routing_from_new_pin(start: PinId, head: Pos2) -> Self {
372 Self {
373 edit_start_on_commit: true,
374 ..Self::routing_from(start, head)
375 }
376 }
377
378 pub fn routing_from_select(start: PinId, head: Pos2) -> Self {
382 Self {
383 return_to_select: true,
384 ..Self::routing_from(start, head)
385 }
386 }
387
388 fn commit(
395 &mut self,
396 data: &mut Drawing,
397 start: PinId,
398 finish: FinishTarget,
399 waypoints: Vec<Waypoint>,
400 ) -> RouteToolState {
401 let Some(start_pos) = data.anchor(start) else {
402 return RouteToolState::Proposed {
403 start,
404 waypoints,
405 finish,
406 };
407 };
408 let Some(end_pos) = Self::finish_pos(data, &finish) else {
409 return RouteToolState::Proposed {
410 start,
411 waypoints,
412 finish,
413 };
414 };
415 if start_pos == end_pos {
416 return RouteToolState::Proposed {
417 start,
418 waypoints,
419 finish,
420 };
421 }
422 let destination = match finish {
426 FinishTarget::Anchor(anchor) => crate::edit::create::RouteEnd::Pin(anchor),
427 FinishTarget::NewPin { block, loc, .. } => {
428 let Some(owner) = data.unlocked_scope(block) else {
433 return RouteToolState::Proposed {
434 start,
435 waypoints,
436 finish,
437 };
438 };
439 crate::edit::create::RouteEnd::Fresh(crate::edit::create::NewPin {
440 id: data.mint(),
441 owner,
442 slot: blockworx_doc::geometry::PinSlot {
443 side: loc.side,
444 offset: crate::grid::pin_slot(loc.offset),
445 },
446 })
447 }
448 };
449 let path = route_in_progress(
450 data,
451 snap_to_grid(start_pos),
452 &waypoints,
453 snap_to_grid(end_pos),
454 );
455 let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
459 let corners =
460 crate::widget::materialize::promote_corners_to_waypoints(&waypoints, &mut geometry);
461 data.add_route(start, destination, corners);
462 self.preview_path.clear();
463 if self.edit_start_on_commit {
466 self.pending_name_edit = Some(start);
467 }
468 RouteToolState::Idle
469 }
470
471 fn finish_pos(data: &Drawing, finish: &FinishTarget) -> Option<Pos2> {
475 match finish {
476 FinishTarget::Anchor(anchor) => data.anchor(*anchor),
477 FinishTarget::NewPin { center, .. } => Some(*center),
478 }
479 }
480
481 fn finish_at(data: &Drawing, start: PinId, pos: Pos2) -> Option<FinishTarget> {
485 match data.anchor_at_pos(pos) {
486 Some(anchor) if anchor != start => Some(FinishTarget::Anchor(anchor)),
487 Some(_) => None,
488 None => Self::new_pin_target_at(data, pos),
489 }
490 }
491
492 fn nearest_block_targets(
496 data: &Drawing,
497 pos: Pos2,
498 ) -> Option<(BlockId, Vec<(PinLocation, Pos2)>)> {
499 data.current_blocks()
500 .filter(|(_, block)| !block.locked)
503 .filter_map(|(rid, _)| {
504 let targets = new_pin_targets(&data.block_shape(rid)?);
505 let nearest = targets
506 .iter()
507 .map(|(_, p)| p.distance(pos))
508 .min_by(f32::total_cmp)?;
509 (nearest < NEW_PIN_ACTIVATION_RANGE.get()).then_some((rid, targets, nearest))
510 })
511 .min_by(|a, b| a.2.total_cmp(&b.2))
512 .map(|(rid, targets, _)| (rid, targets))
513 }
514
515 fn new_pin_target_at(data: &Drawing, pos: Pos2) -> Option<FinishTarget> {
520 let (block, targets) = Self::nearest_block_targets(data, pos)?;
521 let idx = active_new_pin_target(&targets, pos)?;
522 let (loc, center) = targets[idx];
523 (center.distance(pos) < crate::tools::new_pin::new_pin_grab().get())
524 .then_some(FinishTarget::NewPin { block, loc, center })
525 }
526
527 fn update_preview(&mut self, data: &mut Drawing) {
528 let req: Option<(PinId, Vec<Waypoint>, Pos2)> = match &self.state {
532 RouteToolState::InProgress {
533 start,
534 waypoints,
535 head,
536 } => Some((*start, waypoints.clone(), *head)),
537 RouteToolState::Proposed {
538 start,
539 waypoints,
540 finish,
541 } => Self::finish_pos(data, finish).map(|end| (*start, waypoints.clone(), end)),
542 _ => None,
543 };
544 self.preview_path = if let Some((start, waypoints, end)) = req {
545 if let Some(start_pos) = data.anchor(start) {
546 self.preview_route(
547 data,
548 &PreviewRequest {
549 start,
550 start_pos: snap_to_grid(start_pos),
551 waypoints: &waypoints,
552 end: snap_to_grid(end),
553 },
554 )
555 } else {
556 self.preview_cache = None;
557 Vec::new()
558 }
559 } else {
560 self.preview_cache = None;
563 Vec::new()
564 };
565 }
566
567 fn preview_route(&mut self, data: &Drawing, gesture: &PreviewRequest<'_>) -> Vec<TaggedPoint> {
575 let PreviewRequest {
576 start,
577 start_pos,
578 waypoints,
579 end,
580 } = *gesture;
581 let stale = match &self.preview_cache {
582 Some(c) => c.start != start || c.waypoint_count != waypoints.len(),
583 None => true,
584 };
585 if stale {
586 let mut seeds: Vec<Point> = Vec::with_capacity(waypoints.len() + 1);
590 seeds.push(start_pos.into());
591 seeds.extend(waypoints.iter().map(waypoint_point));
592 let mut router = data.scratch_closed_router(&seeds);
593 let (wp_ids, wp_positions) = waypoint_nodes(waypoints);
594 let fixed = route_fixed_legs(&mut router, &wp_positions, start_pos.into(), &wp_ids);
597 self.preview_cache = Some(Box::new(PreviewCache {
598 start,
599 waypoint_count: waypoints.len(),
600 router,
601 fixed,
602 }));
603 }
604 let Some(cache) = self.preview_cache.as_mut() else {
605 return Vec::new();
606 };
607 route_to_head(&mut cache.router, &cache.fixed, end.into())
608 }
609
610 fn draw_new_pin_markers<C: Canvas>(data: &Drawing, painter: &mut Style<'_, C>) {
613 let pointer = Self::world_pointer(painter);
614 let Some(pos) = pointer else { return };
615 if let Some((rid, targets)) = Self::nearest_block_targets(data, pos) {
616 draw_route_new_pin_targets(rid, &targets, pointer, painter);
617 }
618 }
619
620 fn render<C: Canvas>(&self, data: &Drawing, painter: &mut Style<'_, C>) {
621 if matches!(
622 self.state,
623 RouteToolState::InProgress { .. } | RouteToolState::Proposed { .. }
624 ) {
625 Self::draw_new_pin_markers(data, painter);
626 }
627 match &self.state {
628 RouteToolState::Idle | RouteToolState::PinHeadHovered { .. } => {
630 Self::draw_hover_anchor_target(data, painter);
631 }
632 RouteToolState::InProgress {
633 start, waypoints, ..
634 } => {
635 let pts: Vec<Pos2> = self.preview_path.iter().map(|p| p.pos.into()).collect();
636 render_path_with_chamfered_corners(&pts)
637 .render(painter, (0.5, Role::RouteInProgress));
638 for wp in waypoints {
639 painter.circle_filled(px_point(wp.pos), PORT_RADIUS, Role::RouteInProgress);
640 }
641 Self::draw_end_target(data, *start, painter);
642 }
643 RouteToolState::Proposed {
644 start, waypoints, ..
645 } => {
646 let pts: Vec<Pos2> = self.preview_path.iter().map(|p| p.pos.into()).collect();
647 render_path_with_chamfered_corners(&pts)
648 .render(painter, (1.5, Role::RouteInProgress));
649 if let Some(pos) = data.anchor(*start) {
652 painter.circle(
653 pos,
654 PORT_RADIUS,
655 Role::RouteProposedEndpoint,
656 (0.5, Role::RouteProposedEndpoint),
657 );
658 }
659 for wp in waypoints {
660 painter.circle_filled(px_point(wp.pos), PORT_RADIUS, Role::RouteInProgress);
661 }
662 Self::draw_end_target(data, *start, painter);
663 }
664 }
665 }
666
667 fn draw_end_target<C: Canvas>(data: &Drawing, start: PinId, painter: &mut Style<'_, C>) {
670 let Some(pointer) = Self::world_pointer(painter) else {
671 return;
672 };
673 route_start::draw_nearest_anchor_target(data, pointer, Some(start), painter);
674 }
675
676 fn draw_hover_anchor_target<C: Canvas>(data: &Drawing, painter: &mut Style<'_, C>) {
679 let Some(pointer) = Self::world_pointer(painter) else {
680 return;
681 };
682 route_start::draw_nearest_anchor_target(data, pointer, None, painter);
683 }
684
685 fn world_pointer<C: Canvas>(painter: &Style<'_, C>) -> Option<Pos2> {
686 painter.pointer_world()
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use crate::path::Scope;
696 use crate::widget::test_fixtures::{self as fx, Scene};
697 use blockworx_doc::fixtures::block_id;
698 use blockworx_geom::{Rect, pos2};
699
700 #[test]
701 fn no_new_pin_targets_offered_on_a_locked_block() {
702 let rid = block_id(1);
703 let mut scene = Scene::new(vec![fx::block_in(
704 1,
705 Scope::Root,
706 Rect::from_min_max(pos2(0.0, 0.0), pos2(300.0, 300.0)),
707 )]);
708
709 let near = {
712 let drawing = scene.drawing();
713 let targets = new_pin_targets(&drawing.block_shape(rid).expect("the block"));
714 targets[0].1
715 };
716
717 {
718 let drawing = scene.drawing();
719 assert!(
720 RouteTool::nearest_block_targets(&drawing, near).is_some(),
721 "an unlocked block offers new-pin targets"
722 );
723 }
724
725 scene.apply(vec![fx::locked(1)]);
726 let drawing = scene.drawing();
727 assert!(
728 RouteTool::nearest_block_targets(&drawing, near).is_none(),
729 "a locked block offers no new-pin targets"
730 );
731 }
732}