forked from 1b-agent/opencadstudio-upstream
fix(circle): TTT with concentric circles (#318)
The TTT solver's circle-pair difference equation loses its cx/cy terms when the two circles are CONCENTRIC (a ring + a line through it, the issue's drawing): lx = ly = 0, the equation only pins r, and the 2x2 center solve hit a zero determinant and returned no candidates. The degenerate branch now recovers the solution directly: r comes from the pinning equation (for a ring exactly (R_outer - R_inner) / 2), and the centers from intersecting the remaining equation — a line in cx/cy once r is known — with the first circle's distance constraint |C - P| = |r + eps*cr|. Concentric pairs inside three-circle picks ride the same branch. Candidate selection is unchanged, so the circle nearest the three pick points wins. Closes #318 Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
9b28170d4b
commit
ae83b82c96
1 file changed
+75
-1
@@ -749,7 +749,52 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64)
|
||||
let e1 = &lin_eqs[1];
|
||||
let det = e0.lx * e1.ly - e1.lx * e0.ly;
|
||||
if det.abs() < 1e-9 {
|
||||
return vec![];
|
||||
// Degenerate 2x2 — the CONCENTRIC circle pair case (#318): the
|
||||
// pair-difference equation loses its cx/cy terms (lx = ly = 0) and
|
||||
// pins r directly, so the solve above has nothing to invert. Recover
|
||||
// the centers instead: r from the pinning equation, then intersect
|
||||
// the other equation (a line in cx/cy once r is known) with the
|
||||
// first circle's distance constraint |C - P| = |r + eps·cr|.
|
||||
let (pin, other) = if e0.lx.hypot(e0.ly) < 1e-9 {
|
||||
(e0, e1)
|
||||
} else {
|
||||
(e1, e0)
|
||||
};
|
||||
if pin.lx.hypot(pin.ly) >= 1e-9
|
||||
|| pin.lr.abs() < 1e-9
|
||||
|| other.lx.hypot(other.ly) < 1e-9
|
||||
{
|
||||
return vec![];
|
||||
}
|
||||
let r = pin.k / pin.lr;
|
||||
if r <= 1e-4 {
|
||||
return vec![];
|
||||
}
|
||||
let Some(&ci) = circle_idx.first() else {
|
||||
return vec![];
|
||||
};
|
||||
let TangentObject::Circle {
|
||||
center: cp,
|
||||
radius: cr,
|
||||
} = objs[ci]
|
||||
else {
|
||||
return vec![];
|
||||
};
|
||||
let rho = (r + eps[ci] * cr).abs();
|
||||
// The center line: other.lx·cx + other.ly·cy = m.
|
||||
let m = other.k - other.lr * r;
|
||||
let l2 = other.lx * other.lx + other.ly * other.ly;
|
||||
let l = l2.sqrt();
|
||||
let bx = other.lx * m / l2;
|
||||
let by = other.ly * m / l2;
|
||||
let dx = -other.ly / l;
|
||||
let dy = other.lx / l;
|
||||
let p = bx - cp.x;
|
||||
let q = by - cp.y;
|
||||
return solve_quadratic(1.0, 2.0 * (p * dx + q * dy), p * p + q * q - rho * rho)
|
||||
.into_iter()
|
||||
.map(|t| (DVec3::new(bx + dx * t, by + dy * t, 0.0), r))
|
||||
.collect();
|
||||
}
|
||||
|
||||
// cx = a_cx + b_cx*r, cy = a_cy + b_cy*r
|
||||
@@ -994,3 +1039,32 @@ inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_CD"] }
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE"] }); // CircleCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTR"] }); // CircleTTRCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTT"] }); // CircleTTTCommand
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// #318: concentric ring + a line through the center — the degenerate
|
||||
/// circle-pair equation must still yield the annulus circle.
|
||||
#[test]
|
||||
fn ttt_concentric_ring_with_line() {
|
||||
let inner = TangentObject::Circle { center: DVec3::ZERO, radius: 3.5 };
|
||||
let outer = TangentObject::Circle { center: DVec3::ZERO, radius: 5.0 };
|
||||
let dir = DVec3::new(0.35, 1.0, 0.0).normalize();
|
||||
let line = TangentObject::Line { p1: DVec3::ZERO, p2: dir * 10.0 };
|
||||
for order in [
|
||||
[inner, outer, line],
|
||||
[line, inner, outer],
|
||||
[outer, line, inner],
|
||||
] {
|
||||
let cands = ttt_candidates(order[0], order[1], order[2]);
|
||||
let ok = cands.iter().any(|&(c, r)| {
|
||||
let d = (c.x * c.x + c.y * c.y).sqrt();
|
||||
(r - 0.75).abs() < 1e-6
|
||||
&& (d - 4.25).abs() < 1e-6
|
||||
&& (c.x * dir.y - c.y * dir.x).abs() - 0.75 < 1e-6
|
||||
});
|
||||
assert!(ok, "no annulus candidate, got {cands:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user