// SPDX-FileCopyrightText: 2025 Topola contributors // // SPDX-License-Identifier: MIT use core::ops::ControlFlow; use geo::{LineString, Point}; use rstar::AABB; use crate::{ autorouter::invoker::Invoker, board::AccessMesadata, drawing::dot::FixedDotIndex, geometry::shape::AccessShape as _, stepper::{Abort, OnEvent, Step}, }; use super::{ activity::{ActivityContext, InteractiveEvent, InteractiveEventKind}, interaction::InteractionError, }; /// An interactive collector for a route #[derive(Debug)] pub struct RoutePlan { pub active_layer: usize, pub start_pin: Option<(FixedDotIndex, Point)>, pub end_pin: Option<(FixedDotIndex, Point)>, pub lines: LineString, } fn try_select_pin( context: &ActivityContext, ) -> Option<(usize, FixedDotIndex, Point)> { let active_layer = context.interactive_input.active_layer?; let pos = context.interactive_input.pointer_pos; let board = context.invoker.autorouter().board(); let layout = board.layout(); let bbox = AABB::from_point([pos.x(), pos.y()]); // gather relevant resolved selectors let mut pin = None; let mut apex_node = None; for &geom in layout .drawing() .rtree() .locate_in_envelope_intersecting(&AABB::<[f64; 3]>::from_corners( [pos.x(), pos.y(), active_layer as f64 - f64::EPSILON], [pos.x(), pos.y(), active_layer as f64 + f64::EPSILON], )) { let node = geom.data; if layout.node_shape(node).intersects_with_bbox(&bbox) { if let Some(pin_name) = board.node_pinname(&node) { // insert and check that we selected exactly one pin / node log::debug!("node {:?} -> pin {:?}", node, pin_name); if let Some(old_pin) = pin { if pin_name != old_pin { log::debug!( "rejected pin due to ambiguity: [{:?}, {:?}]", old_pin, pin_name ); return None; } continue; } pin = Some(pin_name); let (idx, pos) = board.pin_apex(pin_name, active_layer).unwrap(); if let Some((idx2, _)) = apex_node { if idx != idx2 { // node index is ambiguous log::debug!("rejected pin due to ambiguity: [{:?}, {:?}]", idx2, idx); return None; } } else { apex_node = Some((idx, pos)); } } } } let ret = apex_node.map(|(idx, pos)| (active_layer, idx, pos)); log::debug!("result {:?}", ret); ret } impl RoutePlan { pub fn new(active_layer: usize) -> Self { log::debug!("initialized RoutePlan"); Self { active_layer, start_pin: None, end_pin: None, lines: LineString(Vec::new()), } } pub fn last_pos(&self) -> Option { self.lines.0.last().map(|i| Point(*i)) } } impl Step, String> for RoutePlan { type Error = InteractionError; fn step( &mut self, _context: &mut ActivityContext, ) -> Result, InteractionError> { Ok(ControlFlow::Continue(())) } } impl Abort> for RoutePlan { fn abort(&mut self, _context: &mut Invoker) { self.start_pin = None; self.end_pin = None; self.lines.0.clear(); } } impl OnEvent, InteractiveEvent> for RoutePlan { type Output = Result<(), InteractionError>; fn on_event( &mut self, context: &mut ActivityContext, event: InteractiveEvent, ) -> Result<(), InteractionError> { match event.kind { InteractiveEventKind::PointerPrimaryButtonClicked if self.end_pin.is_none() => { if let Some((layer, idx, pos)) = try_select_pin(context) { // make sure double-click or such doesn't corrupt state if let Some(start_pin) = self.start_pin { if self.active_layer == layer && idx != start_pin.0 { log::debug!("clicked on end pin: {:?}", idx); self.lines.0.push(pos.0); self.end_pin = Some((idx, pos)); } } else { log::debug!("clicked on start pin: {:?}", idx); self.active_layer = layer; self.start_pin = Some((idx, pos)); self.lines.0.push(pos.0); } } else if let Some(last_pos) = self.last_pos() { let pos = context.interactive_input.pointer_pos; if last_pos != pos { self.lines.0.push(pos.0); } } } InteractiveEventKind::PointerSecondaryButtonClicked => { if self.end_pin.is_some() { log::debug!("un-clicked end pin"); self.end_pin = None; self.lines.0.pop(); } if !self.lines.0.is_empty() { self.lines.0.pop(); if self.lines.0.is_empty() { log::debug!("un-clicked start pin"); self.start_pin = None; } } } _ => {} } Ok(()) } }