fix(math/bitangents): Calculate bitangents even for intersecting circles

This fixes the bug where the router was failing to draw around SMD pads.
This commit is contained in:
Mikolaj Wielgus 2025-08-16 14:15:26 +02:00
parent 5fd4926fb6
commit 3738bacf6f
7 changed files with 68 additions and 65 deletions

View File

@ -63,7 +63,7 @@ allowed_scopes = [
"math/cyclic_search", "math/cyclic_search",
"math/line", "math/line",
"math/polygon_tangents", "math/polygon_tangents",
"math/tangents", "math/bitangents",
"math/tunnel", "math/tunnel",
"router/draw", "router/draw",
"router/navcord", "router/navcord",

View File

@ -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.source().0),
Self::node_guide_circle(board, navmesh, navcord, edge.target().0), Self::node_guide_circle(board, navmesh, navcord, edge.target().0),
) { ) {
if let Ok(tangents) = if let Ok(bitangents) =
math::tangent_segments(from_circle, None, to_circle, None) math::bitangents(from_circle, None, to_circle, None)
{ {
for tangent in tangents { for bitangent in bitangents {
self.painter.paint_line_segment( self.painter.paint_line_segment(
tangent.start_point(), bitangent.start_point(),
tangent.end_point(), bitangent.end_point(),
egui::Stroke::new(1.0, egui::Color32::WHITE), egui::Stroke::new(1.0, egui::Color32::WHITE),
) )
} }

View File

@ -39,7 +39,7 @@ use crate::{
GetLayer, GetOffset, GetSetPos, GetWidth, GetLayer, GetOffset, GetSetPos, GetWidth,
}, },
graph::{GenericIndex, GetPetgraphIndex, MakeRef}, graph::{GenericIndex, GetPetgraphIndex, MakeRef},
math::{NoTangents, RotationSense}, math::{NoBitangents, RotationSense},
}; };
use super::gear::{GetOuterGears, WalkOutwards}; use super::gear::{GetOuterGears, WalkOutwards};
@ -47,7 +47,7 @@ use super::gear::{GetOuterGears, WalkOutwards};
#[derive(Clone, Copy, Error)] #[derive(Clone, Copy, Error)]
pub enum DrawingException { pub enum DrawingException {
#[error(transparent)] #[error(transparent)]
NoTangents(#[from] NoTangents), NoTangents(#[from] NoBitangents),
#[error(transparent)] #[error(transparent)]
Infringement(#[from] Infringement), Infringement(#[from] Infringement),
#[error(transparent)] #[error(transparent)]

View File

@ -6,7 +6,7 @@ use geo::Line;
use crate::{ use crate::{
geometry::{primitive::PrimitiveShape, shape::AccessShape, GetWidth}, geometry::{primitive::PrimitiveShape, shape::AccessShape, GetWidth},
math::{self, Circle, NoTangents, RotationSense}, math::{self, Circle, NoBitangents, RotationSense},
}; };
use super::{ use super::{
@ -25,7 +25,7 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
head: &Head, head: &Head,
into: FixedDotIndex, into: FixedDotIndex,
width: f64, width: f64,
) -> Result<Line, NoTangents> { ) -> Result<Line, NoBitangents> {
let from_circle = self.head_circle(head, width); let from_circle = self.head_circle(head, width);
let to_circle = Circle { let to_circle = Circle {
pos: self.primitive(into).weight().0.circle.pos, pos: self.primitive(into).weight().0.circle.pos,
@ -33,7 +33,7 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
}; };
let from_sense = self.head_sense(head); 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( pub fn guides_for_head_around_dot(
@ -41,14 +41,14 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
head: &Head, head: &Head,
around: DotIndex, around: DotIndex,
width: f64, width: f64,
) -> Result<(Line, Line), NoTangents> { ) -> Result<(Line, Line), NoBitangents> {
let from_circle = self.head_circle(head, width); let from_circle = self.head_circle(head, width);
let to_circle = let to_circle =
self.dot_circle(around, width, self.conditions(head.face().into()).as_ref()); self.dot_circle(around, width, self.conditions(head.face().into()).as_ref());
let from_sense = self.head_sense(head); let from_sense = self.head_sense(head);
let tangents: Vec<Line> = let tangents: Vec<Line> =
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])) Ok((tangents[0], tangents[1]))
} }
@ -58,13 +58,13 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
around: DotIndex, around: DotIndex,
sense: RotationSense, sense: RotationSense,
width: f64, width: f64,
) -> Result<Line, NoTangents> { ) -> Result<Line, NoBitangents> {
let from_circle = self.head_circle(head, width); let from_circle = self.head_circle(head, width);
let to_circle = let to_circle =
self.dot_circle(around, width, self.conditions(head.face().into()).as_ref()); self.dot_circle(around, width, self.conditions(head.face().into()).as_ref());
let from_sense = self.head_sense(head); 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( pub fn offset_for_guide_for_head_around_dot(
@ -84,14 +84,14 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
head: &Head, head: &Head,
around: BendIndex, around: BendIndex,
width: f64, width: f64,
) -> Result<(Line, Line), NoTangents> { ) -> Result<(Line, Line), NoBitangents> {
let from_circle = self.head_circle(head, width); let from_circle = self.head_circle(head, width);
let to_circle = let to_circle =
self.bend_circle(around, width, self.conditions(head.face().into()).as_ref()); self.bend_circle(around, width, self.conditions(head.face().into()).as_ref());
let from_sense = self.head_sense(head); let from_sense = self.head_sense(head);
let tangents: Vec<Line> = let tangents: Vec<Line> =
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])) Ok((tangents[0], tangents[1]))
} }
@ -101,13 +101,13 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
around: BendIndex, around: BendIndex,
sense: RotationSense, sense: RotationSense,
width: f64, width: f64,
) -> Result<Line, NoTangents> { ) -> Result<Line, NoBitangents> {
let from_circle = self.head_circle(head, width); let from_circle = self.head_circle(head, width);
let to_circle = let to_circle =
self.bend_circle(around, width, self.conditions(head.face().into()).as_ref()); self.bend_circle(around, width, self.conditions(head.face().into()).as_ref());
let from_sense = self.head_sense(head); 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( pub fn offset_for_guide_for_head_around_bend(

View File

@ -10,9 +10,16 @@ use super::{seq_perp_dot_product, LineInGeneralForm, RotationSense};
#[derive(Error, Debug, Clone, Copy, PartialEq)] #[derive(Error, Debug, Clone, Copy, PartialEq)]
#[error("no tangents for {0:?} and {1:?}")] // TODO add real error message #[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<LineInGeneralForm, ()> {
// 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<LineInGeneralForm, ()> {
let epsilon = 1e-9; let epsilon = 1e-9;
let dr = r2 - r1; let dr = r2 - r1;
let norm = center.x() * center.x() + center.y() * center.y(); let norm = center.x() * center.x() + center.y() * center.y();
@ -31,22 +38,25 @@ fn _tangent(center: Point, r1: f64, r2: f64) -> Result<LineInGeneralForm, ()> {
}) })
} }
fn _tangents(circle1: Circle, circle2: Circle) -> Result<[LineInGeneralForm; 4], ()> { fn _bitangents(circle1: Circle, circle2: Circle) -> Vec<LineInGeneralForm> {
let mut tgs: [LineInGeneralForm; 4] = [ let mut tgs: Vec<LineInGeneralForm> = [
_tangent((circle2 - circle1).pos, -circle1.r, -circle2.r)?, _bitangent((circle2 - circle1).pos, -circle1.r, -circle2.r),
_tangent((circle2 - circle1).pos, -circle1.r, circle2.r)?, _bitangent((circle2 - circle1).pos, -circle1.r, circle2.r),
_tangent((circle2 - circle1).pos, circle1.r, -circle2.r)?, _bitangent((circle2 - circle1).pos, circle1.r, -circle2.r),
_tangent((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() { for tg in tgs.iter_mut() {
tg.c -= tg.a * circle1.pos.x() + tg.b * circle1.pos.y(); 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.b * (line.b * pt.x() - line.a * pt.y()) - line.a * line.c)
/ (line.a * line.a + line.b * line.b), / (line.a * line.a + line.b * line.b),
@ -56,39 +66,34 @@ fn cast_point_to_canonical_line(pt: Point, line: LineInGeneralForm) -> Point {
.into() .into()
} }
fn tangent_point_pairs( fn bitangent_point_pairs(
circle1: Circle, circle1: Circle,
circle2: Circle, circle2: Circle,
) -> Result<[(Point, Point); 4], NoTangents> { ) -> Result<Vec<(Point, Point)>, NoBitangents> {
let tgs = _tangents(circle1, circle2).map_err(|_| NoTangents(circle1, circle2))?; 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([ if bitangents.is_empty() {
( return Err(NoBitangents(circle1, circle2));
cast_point_to_canonical_line(circle1.pos, tgs[0]), }
cast_point_to_canonical_line(circle2.pos, tgs[0]),
), Ok(bitangents)
(
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]),
),
])
} }
pub fn tangent_segments( pub fn bitangents(
circle1: Circle, circle1: Circle,
maybe_sense1: Option<RotationSense>, maybe_sense1: Option<RotationSense>,
circle2: Circle, circle2: Circle,
maybe_sense2: Option<RotationSense>, maybe_sense2: Option<RotationSense>,
) -> Result<impl Iterator<Item = Line>, NoTangents> { ) -> Result<impl Iterator<Item = Line>, NoBitangents> {
Ok(tangent_point_pairs(circle1, circle2)? Ok(bitangent_point_pairs(circle1, circle2)?
.into_iter() .into_iter()
.filter_map(move |tangent_point_pair| { .filter_map(move |tangent_point_pair| {
if let Some(sense1) = maybe_sense1 { if let Some(sense1) = maybe_sense1 {
@ -117,15 +122,13 @@ pub fn tangent_segments(
})) }))
} }
pub fn tangent_segment( pub fn bitangent(
circle1: Circle, circle1: Circle,
maybe_sense1: Option<RotationSense>, maybe_sense1: Option<RotationSense>,
circle2: Circle, circle2: Circle,
maybe_sense2: Option<RotationSense>, maybe_sense2: Option<RotationSense>,
) -> Result<Line, NoTangents> { ) -> Result<Line, NoBitangents> {
Ok( Ok(bitangents(circle1, maybe_sense1, circle2, maybe_sense2)?
tangent_segments(circle1, maybe_sense1, circle2, maybe_sense2)? .next()
.next() .ok_or(NoBitangents(circle1, circle2))?)
.unwrap(),
)
} }

View File

@ -15,8 +15,8 @@ pub use line::*;
mod polygon_tangents; mod polygon_tangents;
pub use polygon_tangents::*; pub use polygon_tangents::*;
mod tangents; mod bitangents;
pub use tangents::*; pub use bitangents::*;
mod tunnel; mod tunnel;
pub use tunnel::*; pub use tunnel::*;

View File

@ -23,13 +23,13 @@ use crate::{
}, },
geometry::{GetLayer, GetSetPos}, geometry::{GetLayer, GetSetPos},
layout::{Layout, LayoutEdit}, layout::{Layout, LayoutEdit},
math::{Circle, NoTangents, RotationSense}, math::{Circle, NoBitangents, RotationSense},
}; };
#[derive(Error, Debug, Clone, Copy)] #[derive(Error, Debug, Clone, Copy)]
pub enum DrawException { pub enum DrawException {
#[error(transparent)] #[error(transparent)]
NoTangents(#[from] NoTangents), NoTangents(#[from] NoBitangents),
// TODO add real error messages + these should eventually use Display // TODO add real error messages + these should eventually use Display
#[error("cannot finish in {0:?}")] #[error("cannot finish in {0:?}")]
CannotFinishIn(FixedDotIndex, #[source] DrawingException), CannotFinishIn(FixedDotIndex, #[source] DrawingException),