From 3738bacf6f85ad34910107c5cacbfbfdaa733c5b Mon Sep 17 00:00:00 2001 From: Mikolaj Wielgus Date: Sat, 16 Aug 2025 14:15:26 +0200 Subject: [PATCH] fix(math/bitangents): Calculate bitangents even for intersecting circles This fixes the bug where the router was failing to draw around SMD pads. --- committed.toml | 2 +- crates/topola-egui/src/displayer.rs | 10 +-- src/drawing/drawing.rs | 4 +- src/drawing/guide.rs | 22 +++---- src/math/{tangents.rs => bitangents.rs} | 87 +++++++++++++------------ src/math/mod.rs | 4 +- src/router/draw.rs | 4 +- 7 files changed, 68 insertions(+), 65 deletions(-) rename src/math/{tangents.rs => bitangents.rs} (59%) diff --git a/committed.toml b/committed.toml index 5ec9641..82712d6 100644 --- a/committed.toml +++ b/committed.toml @@ -63,7 +63,7 @@ allowed_scopes = [ "math/cyclic_search", "math/line", "math/polygon_tangents", - "math/tangents", + "math/bitangents", "math/tunnel", "router/draw", "router/navcord", diff --git a/crates/topola-egui/src/displayer.rs b/crates/topola-egui/src/displayer.rs index 26452c7..ca472bf 100644 --- a/crates/topola-egui/src/displayer.rs +++ b/crates/topola-egui/src/displayer.rs @@ -235,13 +235,13 @@ impl<'a> Displayer<'a> { Self::node_guide_circle(board, navmesh, navcord, edge.source().0), Self::node_guide_circle(board, navmesh, navcord, edge.target().0), ) { - if let Ok(tangents) = - math::tangent_segments(from_circle, None, to_circle, None) + if let Ok(bitangents) = + math::bitangents(from_circle, None, to_circle, None) { - for tangent in tangents { + for bitangent in bitangents { self.painter.paint_line_segment( - tangent.start_point(), - tangent.end_point(), + bitangent.start_point(), + bitangent.end_point(), egui::Stroke::new(1.0, egui::Color32::WHITE), ) } diff --git a/src/drawing/drawing.rs b/src/drawing/drawing.rs index ce942c0..6b4f9e9 100644 --- a/src/drawing/drawing.rs +++ b/src/drawing/drawing.rs @@ -39,7 +39,7 @@ use crate::{ GetLayer, GetOffset, GetSetPos, GetWidth, }, graph::{GenericIndex, GetPetgraphIndex, MakeRef}, - math::{NoTangents, RotationSense}, + math::{NoBitangents, RotationSense}, }; use super::gear::{GetOuterGears, WalkOutwards}; @@ -47,7 +47,7 @@ use super::gear::{GetOuterGears, WalkOutwards}; #[derive(Clone, Copy, Error)] pub enum DrawingException { #[error(transparent)] - NoTangents(#[from] NoTangents), + NoTangents(#[from] NoBitangents), #[error(transparent)] Infringement(#[from] Infringement), #[error(transparent)] diff --git a/src/drawing/guide.rs b/src/drawing/guide.rs index 0b916ec..47f06d8 100644 --- a/src/drawing/guide.rs +++ b/src/drawing/guide.rs @@ -6,7 +6,7 @@ use geo::Line; use crate::{ geometry::{primitive::PrimitiveShape, shape::AccessShape, GetWidth}, - math::{self, Circle, NoTangents, RotationSense}, + math::{self, Circle, NoBitangents, RotationSense}, }; use super::{ @@ -25,7 +25,7 @@ impl Drawing { head: &Head, into: FixedDotIndex, width: f64, - ) -> Result { + ) -> Result { let from_circle = self.head_circle(head, width); let to_circle = Circle { pos: self.primitive(into).weight().0.circle.pos, @@ -33,7 +33,7 @@ impl Drawing { }; let from_sense = self.head_sense(head); - math::tangent_segment(from_circle, from_sense, to_circle, None) + math::bitangent(from_circle, from_sense, to_circle, None) } pub fn guides_for_head_around_dot( @@ -41,14 +41,14 @@ impl Drawing { head: &Head, around: DotIndex, width: f64, - ) -> Result<(Line, Line), NoTangents> { + ) -> Result<(Line, Line), NoBitangents> { let from_circle = self.head_circle(head, width); let to_circle = self.dot_circle(around, width, self.conditions(head.face().into()).as_ref()); let from_sense = self.head_sense(head); let tangents: Vec = - math::tangent_segments(from_circle, from_sense, to_circle, None)?.collect(); + math::bitangents(from_circle, from_sense, to_circle, None)?.collect(); Ok((tangents[0], tangents[1])) } @@ -58,13 +58,13 @@ impl Drawing { around: DotIndex, sense: RotationSense, width: f64, - ) -> Result { + ) -> Result { let from_circle = self.head_circle(head, width); let to_circle = self.dot_circle(around, width, self.conditions(head.face().into()).as_ref()); let from_sense = self.head_sense(head); - math::tangent_segment(from_circle, from_sense, to_circle, Some(sense)) + math::bitangent(from_circle, from_sense, to_circle, Some(sense)) } pub fn offset_for_guide_for_head_around_dot( @@ -84,14 +84,14 @@ impl Drawing { head: &Head, around: BendIndex, width: f64, - ) -> Result<(Line, Line), NoTangents> { + ) -> Result<(Line, Line), NoBitangents> { let from_circle = self.head_circle(head, width); let to_circle = self.bend_circle(around, width, self.conditions(head.face().into()).as_ref()); let from_sense = self.head_sense(head); let tangents: Vec = - math::tangent_segments(from_circle, from_sense, to_circle, None)?.collect(); + math::bitangents(from_circle, from_sense, to_circle, None)?.collect(); Ok((tangents[0], tangents[1])) } @@ -101,13 +101,13 @@ impl Drawing { around: BendIndex, sense: RotationSense, width: f64, - ) -> Result { + ) -> Result { let from_circle = self.head_circle(head, width); let to_circle = self.bend_circle(around, width, self.conditions(head.face().into()).as_ref()); let from_sense = self.head_sense(head); - math::tangent_segment(from_circle, from_sense, to_circle, Some(sense)) + math::bitangent(from_circle, from_sense, to_circle, Some(sense)) } pub fn offset_for_guide_for_head_around_bend( diff --git a/src/math/tangents.rs b/src/math/bitangents.rs similarity index 59% rename from src/math/tangents.rs rename to src/math/bitangents.rs index c6d2509..271a2d4 100644 --- a/src/math/tangents.rs +++ b/src/math/bitangents.rs @@ -10,9 +10,16 @@ use super::{seq_perp_dot_product, LineInGeneralForm, RotationSense}; #[derive(Error, Debug, Clone, Copy, PartialEq)] #[error("no tangents for {0:?} and {1:?}")] // TODO add real error message -pub struct NoTangents(pub Circle, pub Circle); +pub struct NoBitangents(pub Circle, pub Circle); + +fn _bitangent(center: Point, r1: f64, r2: f64) -> Result { + // Taken from https://cp-algorithms.com/geometry/tangents-to-two-circles.html + // with small changes. + + if approx::relative_eq!(center.x(), 0.0) && approx::relative_eq!(center.y(), 0.0) { + return Err(()); + } -fn _tangent(center: Point, r1: f64, r2: f64) -> Result { let epsilon = 1e-9; let dr = r2 - r1; let norm = center.x() * center.x() + center.y() * center.y(); @@ -31,22 +38,25 @@ fn _tangent(center: Point, r1: f64, r2: f64) -> Result { }) } -fn _tangents(circle1: Circle, circle2: Circle) -> Result<[LineInGeneralForm; 4], ()> { - let mut tgs: [LineInGeneralForm; 4] = [ - _tangent((circle2 - circle1).pos, -circle1.r, -circle2.r)?, - _tangent((circle2 - circle1).pos, -circle1.r, circle2.r)?, - _tangent((circle2 - circle1).pos, circle1.r, -circle2.r)?, - _tangent((circle2 - circle1).pos, circle1.r, circle2.r)?, - ]; +fn _bitangents(circle1: Circle, circle2: Circle) -> Vec { + let mut tgs: Vec = [ + _bitangent((circle2 - circle1).pos, -circle1.r, -circle2.r), + _bitangent((circle2 - circle1).pos, -circle1.r, circle2.r), + _bitangent((circle2 - circle1).pos, circle1.r, -circle2.r), + _bitangent((circle2 - circle1).pos, circle1.r, circle2.r), + ] + .into_iter() + .flatten() + .collect(); for tg in tgs.iter_mut() { tg.c -= tg.a * circle1.pos.x() + tg.b * circle1.pos.y(); } - Ok(tgs) + tgs } -fn cast_point_to_canonical_line(pt: Point, line: LineInGeneralForm) -> Point { +fn cast_point_to_line(pt: Point, line: LineInGeneralForm) -> Point { ( (line.b * (line.b * pt.x() - line.a * pt.y()) - line.a * line.c) / (line.a * line.a + line.b * line.b), @@ -56,39 +66,34 @@ fn cast_point_to_canonical_line(pt: Point, line: LineInGeneralForm) -> Point { .into() } -fn tangent_point_pairs( +fn bitangent_point_pairs( circle1: Circle, circle2: Circle, -) -> Result<[(Point, Point); 4], NoTangents> { - let tgs = _tangents(circle1, circle2).map_err(|_| NoTangents(circle1, circle2))?; +) -> Result, NoBitangents> { + let bitangents: Vec<(Point, Point)> = _bitangents(circle1, circle2) + .into_iter() + .map(|tg| { + ( + cast_point_to_line(circle1.pos, tg), + cast_point_to_line(circle2.pos, tg), + ) + }) + .collect(); - Ok([ - ( - cast_point_to_canonical_line(circle1.pos, tgs[0]), - cast_point_to_canonical_line(circle2.pos, tgs[0]), - ), - ( - cast_point_to_canonical_line(circle1.pos, tgs[1]), - cast_point_to_canonical_line(circle2.pos, tgs[1]), - ), - ( - cast_point_to_canonical_line(circle1.pos, tgs[2]), - cast_point_to_canonical_line(circle2.pos, tgs[2]), - ), - ( - cast_point_to_canonical_line(circle1.pos, tgs[3]), - cast_point_to_canonical_line(circle2.pos, tgs[3]), - ), - ]) + if bitangents.is_empty() { + return Err(NoBitangents(circle1, circle2)); + } + + Ok(bitangents) } -pub fn tangent_segments( +pub fn bitangents( circle1: Circle, maybe_sense1: Option, circle2: Circle, maybe_sense2: Option, -) -> Result, NoTangents> { - Ok(tangent_point_pairs(circle1, circle2)? +) -> Result, NoBitangents> { + Ok(bitangent_point_pairs(circle1, circle2)? .into_iter() .filter_map(move |tangent_point_pair| { if let Some(sense1) = maybe_sense1 { @@ -117,15 +122,13 @@ pub fn tangent_segments( })) } -pub fn tangent_segment( +pub fn bitangent( circle1: Circle, maybe_sense1: Option, circle2: Circle, maybe_sense2: Option, -) -> Result { - Ok( - tangent_segments(circle1, maybe_sense1, circle2, maybe_sense2)? - .next() - .unwrap(), - ) +) -> Result { + Ok(bitangents(circle1, maybe_sense1, circle2, maybe_sense2)? + .next() + .ok_or(NoBitangents(circle1, circle2))?) } diff --git a/src/math/mod.rs b/src/math/mod.rs index 72e7ab2..00fe1ee 100644 --- a/src/math/mod.rs +++ b/src/math/mod.rs @@ -15,8 +15,8 @@ pub use line::*; mod polygon_tangents; pub use polygon_tangents::*; -mod tangents; -pub use tangents::*; +mod bitangents; +pub use bitangents::*; mod tunnel; pub use tunnel::*; diff --git a/src/router/draw.rs b/src/router/draw.rs index 58faa69..2055375 100644 --- a/src/router/draw.rs +++ b/src/router/draw.rs @@ -23,13 +23,13 @@ use crate::{ }, geometry::{GetLayer, GetSetPos}, layout::{Layout, LayoutEdit}, - math::{Circle, NoTangents, RotationSense}, + math::{Circle, NoBitangents, RotationSense}, }; #[derive(Error, Debug, Clone, Copy)] pub enum DrawException { #[error(transparent)] - NoTangents(#[from] NoTangents), + NoTangents(#[from] NoBitangents), // TODO add real error messages + these should eventually use Display #[error("cannot finish in {0:?}")] CannotFinishIn(FixedDotIndex, #[source] DrawingException),