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/line",
"math/polygon_tangents",
"math/tangents",
"math/bitangents",
"math/tunnel",
"router/draw",
"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.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),
)
}

View File

@ -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)]

View File

@ -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<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
head: &Head,
into: FixedDotIndex,
width: f64,
) -> Result<Line, NoTangents> {
) -> Result<Line, NoBitangents> {
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<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
};
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<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
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<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]))
}
@ -58,13 +58,13 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
around: DotIndex,
sense: RotationSense,
width: f64,
) -> Result<Line, NoTangents> {
) -> Result<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);
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<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
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<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]))
}
@ -101,13 +101,13 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
around: BendIndex,
sense: RotationSense,
width: f64,
) -> Result<Line, NoTangents> {
) -> Result<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);
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(

View File

@ -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<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 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<LineInGeneralForm, ()> {
})
}
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<LineInGeneralForm> {
let mut tgs: Vec<LineInGeneralForm> = [
_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<Vec<(Point, Point)>, 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<RotationSense>,
circle2: Circle,
maybe_sense2: Option<RotationSense>,
) -> Result<impl Iterator<Item = Line>, NoTangents> {
Ok(tangent_point_pairs(circle1, circle2)?
) -> Result<impl Iterator<Item = Line>, 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<RotationSense>,
circle2: Circle,
maybe_sense2: Option<RotationSense>,
) -> Result<Line, NoTangents> {
Ok(
tangent_segments(circle1, maybe_sense1, circle2, maybe_sense2)?
.next()
.unwrap(),
)
) -> Result<Line, NoBitangents> {
Ok(bitangents(circle1, maybe_sense1, circle2, maybe_sense2)?
.next()
.ok_or(NoBitangents(circle1, circle2))?)
}

View File

@ -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::*;

View File

@ -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),