1#![forbid(unsafe_code)]
2
3use axiolid_core::{Frame2, Point2, Scalar, Tolerance, Vec2};
45use axiolid_curve::Circle2;
46use axiolid_linear::Line2;
47
48#[derive(Debug, Clone, Copy, PartialEq)]
52pub enum Tangent {
53 Point(Point2),
55 Line(Line2),
57 Circle(Circle2),
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
63#[non_exhaustive]
64pub enum TangencyError {
65 NonFinite,
67 InvalidRadius {
69 index: usize,
71 radius: Scalar,
73 },
74 ZeroDirection {
77 index: usize,
79 },
80 Degenerate,
85}
86
87impl core::fmt::Display for TangencyError {
88 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
89 match self {
90 Self::NonFinite => f.write_str("a coordinate, direction or radius is not finite"),
91 Self::InvalidRadius { index, radius } => {
92 write!(f, "constraint {index} has non-positive radius {radius}")
93 }
94 Self::ZeroDirection { index } => {
95 write!(f, "constraint {index} is a line with no direction")
96 }
97 Self::Degenerate => f.write_str("no sign choice pins down a family of tangent circles"),
98 }
99 }
100}
101
102impl core::error::Error for TangencyError {}
103
104#[derive(Debug, Clone, Copy)]
107struct Equation {
108 a: Scalar,
109 b: Scalar,
110 c: Scalar,
111 d: Scalar,
112 f: Scalar,
113}
114
115fn equation(constraint: &Tangent, sign: Scalar) -> Equation {
116 match *constraint {
117 Tangent::Point(p) => Equation {
118 a: 1.0,
119 b: -2.0 * p.x,
120 c: -2.0 * p.y,
121 d: 0.0,
122 f: p.x * p.x + p.y * p.y,
123 },
124 Tangent::Circle(circle) => {
125 let o = circle.frame.origin;
126 let rho = circle.radius;
127 Equation {
128 a: 1.0,
129 b: -2.0 * o.x,
130 c: -2.0 * o.y,
131 d: -2.0 * sign * rho,
132 f: o.x * o.x + o.y * o.y - rho * rho,
133 }
134 }
135 Tangent::Line(line) => {
136 let n = line.direction.perp().normalize();
137 Equation {
138 a: 0.0,
139 b: n.x,
140 c: n.y,
141 d: -sign,
142 f: -(n.dot(line.origin)),
143 }
144 }
145 }
146}
147
148fn sub(lhs: &Equation, rhs: &Equation) -> Equation {
149 Equation {
150 a: lhs.a - rhs.a,
151 b: lhs.b - rhs.b,
152 c: lhs.c - rhs.c,
153 d: lhs.d - rhs.d,
154 f: lhs.f - rhs.f,
155 }
156}
157
158pub fn tangent_circles(
177 constraints: [Tangent; 3],
178 tolerance: Tolerance,
179) -> Result<Vec<Circle2>, TangencyError> {
180 validate(&constraints, tolerance)?;
181 let linear = tolerance.linear();
182
183 let mut candidates: Vec<(Point2, Scalar)> = Vec::new();
184 let mut any_solvable = false;
185 for bits in 0u8..8 {
186 let signs = [
187 if bits & 1 == 0 { -1.0 } else { 1.0 },
188 if bits & 2 == 0 { -1.0 } else { 1.0 },
189 if bits & 4 == 0 { -1.0 } else { 1.0 },
190 ];
191 let eqs = [
192 equation(&constraints[0], signs[0]),
193 equation(&constraints[1], signs[1]),
194 equation(&constraints[2], signs[2]),
195 ];
196 if let Some(roots) = solve_system(&eqs) {
197 any_solvable = true;
198 for (cx, cy, r) in roots {
199 if r.is_finite() && cx.is_finite() && cy.is_finite() && r > linear {
200 candidates.push((Point2::new(cx, cy), r));
201 }
202 }
203 }
204 }
205 if !any_solvable {
206 return Err(TangencyError::Degenerate);
207 }
208
209 candidates.sort_by(|a, b| {
210 a.0.x
211 .total_cmp(&b.0.x)
212 .then_with(|| a.0.y.total_cmp(&b.0.y))
213 .then_with(|| a.1.total_cmp(&b.1))
214 });
215 let mut deduped: Vec<(Point2, Scalar)> = Vec::new();
216 for (centre, radius) in candidates {
217 let same = deduped
218 .iter()
219 .any(|&(c, r)| (c - centre).length() <= linear && (r - radius).abs() <= linear);
220 if !same {
221 deduped.push((centre, radius));
222 }
223 }
224
225 Ok(deduped
226 .into_iter()
227 .map(|(centre, radius)| Circle2 {
228 frame: Frame2 {
229 origin: centre,
230 x: Vec2::X,
231 y: Vec2::Y,
232 },
233 radius,
234 })
235 .collect())
236}
237
238fn validate(constraints: &[Tangent; 3], tolerance: Tolerance) -> Result<(), TangencyError> {
239 let linear = tolerance.linear();
240 for (index, constraint) in constraints.iter().enumerate() {
241 match *constraint {
242 Tangent::Point(p) => {
243 if !p.is_finite() {
244 return Err(TangencyError::NonFinite);
245 }
246 }
247 Tangent::Line(line) => {
248 if !line.origin.is_finite() || !line.direction.is_finite() {
249 return Err(TangencyError::NonFinite);
250 }
251 if line.direction.length() <= linear {
252 return Err(TangencyError::ZeroDirection { index });
253 }
254 }
255 Tangent::Circle(circle) => {
256 if !circle.frame.origin.is_finite() || !circle.radius.is_finite() {
257 return Err(TangencyError::NonFinite);
258 }
259 if circle.radius <= 0.0 {
260 return Err(TangencyError::InvalidRadius {
261 index,
262 radius: circle.radius,
263 });
264 }
265 }
266 }
267 }
268 Ok(())
269}
270
271fn solve_system(eqs: &[Equation; 3]) -> Option<Vec<(Scalar, Scalar, Scalar)>> {
275 let quad: Vec<usize> = (0..3).filter(|&i| eqs[i].a != 0.0).collect();
276 if quad.is_empty() {
277 return solve_linear3(eqs).map(|root| vec![root]);
278 }
279 let base = quad[0];
280 let mut lines: Vec<Equation> = Vec::with_capacity(2);
281 for i in 0..3 {
282 if i == base {
283 continue;
284 }
285 lines.push(if eqs[i].a != 0.0 {
286 sub(&eqs[base], &eqs[i])
287 } else {
288 eqs[i]
289 });
290 }
291 let (m1, n1, m2, n2) = solve_cxy_in_r(&lines[0], &lines[1])?;
292
293 let e = &eqs[base];
294 let a_coef = m1 * m1 + m2 * m2 - 1.0;
295 let b_coef = 2.0 * (m1 * n1 + m2 * n2) + e.b * m1 + e.c * m2 + e.d;
296 let c_coef = n1 * n1 + n2 * n2 + e.b * n1 + e.c * n2 + e.f;
297 Some(
298 solve_quadratic(a_coef, b_coef, c_coef)
299 .into_iter()
300 .map(|r| (m1 * r + n1, m2 * r + n2, r))
301 .collect(),
302 )
303}
304
305fn solve_cxy_in_r(l0: &Equation, l1: &Equation) -> Option<(Scalar, Scalar, Scalar, Scalar)> {
309 let det = l0.b * l1.c - l1.b * l0.c;
310 let scale = (l0.b.hypot(l0.c) * l1.b.hypot(l1.c)).max(1.0);
311 if det.abs() <= Scalar::EPSILON * 1e8 * scale {
312 return None;
313 }
314 let m1 = (l1.d * l0.c - l0.d * l1.c) / det;
315 let n1 = (l1.f * l0.c - l0.f * l1.c) / det;
316 let m2 = (l1.b * l0.d - l0.b * l1.d) / det;
317 let n2 = (l1.b * l0.f - l0.b * l1.f) / det;
318 Some((m1, n1, m2, n2))
319}
320
321fn solve_linear3(eqs: &[Equation; 3]) -> Option<(Scalar, Scalar, Scalar)> {
325 let rows = [
326 [eqs[0].b, eqs[0].c, eqs[0].d],
327 [eqs[1].b, eqs[1].c, eqs[1].d],
328 [eqs[2].b, eqs[2].c, eqs[2].d],
329 ];
330 let rhs = [-eqs[0].f, -eqs[1].f, -eqs[2].f];
331 let det = det3(rows);
332 let scale = rows
333 .iter()
334 .map(|row| row[0].hypot(row[1]).hypot(row[2]))
335 .fold(1.0, Scalar::max);
336 if det.abs() <= Scalar::EPSILON * 1e8 * scale * scale * scale {
337 return None;
338 }
339 let cx = det3(replace_col(rows, 0, rhs)) / det;
340 let cy = det3(replace_col(rows, 1, rhs)) / det;
341 let r = det3(replace_col(rows, 2, rhs)) / det;
342 Some((cx, cy, r))
343}
344
345fn det3(m: [[Scalar; 3]; 3]) -> Scalar {
346 m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
347 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
348 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
349}
350
351fn replace_col(mut m: [[Scalar; 3]; 3], col: usize, values: [Scalar; 3]) -> [[Scalar; 3]; 3] {
352 for row in 0..3 {
353 m[row][col] = values[row];
354 }
355 m
356}
357
358fn solve_quadratic(a: Scalar, b: Scalar, c: Scalar) -> Vec<Scalar> {
361 let scale = a.abs().max(b.abs()).max(c.abs()).max(1.0);
362 if a.abs() <= Scalar::EPSILON * 1e8 * scale {
363 if b.abs() <= Scalar::EPSILON * 1e8 * scale {
364 return Vec::new();
365 }
366 return vec![-c / b];
367 }
368 let discriminant = b * b - 4.0 * a * c;
369 if discriminant < 0.0 {
370 return Vec::new();
371 }
372 if discriminant <= Scalar::EPSILON * 1e8 * scale * scale {
373 return vec![-b / (2.0 * a)];
374 }
375 let sqrt_d = discriminant.sqrt();
376 let q = if b >= 0.0 {
379 -0.5 * (b + sqrt_d)
380 } else {
381 -0.5 * (b - sqrt_d)
382 };
383 if q.abs() <= Scalar::EPSILON * 1e8 * scale {
384 return vec![-b / (2.0 * a)];
385 }
386 let mut roots = vec![q / a, c / q];
387 roots.sort_by(Scalar::total_cmp);
388 roots
389}