use arael::vect::vect2d;
pub fn fit_earc_tangent(
p1: vect2d, t1: vect2d, p2: vect2d, t2: vect2d, bulge: f64,
) -> Option<(vect2d, f64, f64, f64, f64, f64, bool)> {
let chord_dx = p2.x - p1.x;
let chord_dy = p2.y - p1.y;
let chord_len = (chord_dx * chord_dx + chord_dy * chord_dy).sqrt();
if chord_len < 1e-12 { return None; }
let half_chord = chord_len / 2.0;
let chord_mid = vect2d::new((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0);
let l1 = (t1.y, -t1.x, -(t1.y * p1.x - t1.x * p1.y));
let l2 = (t2.y, -t2.x, -(t2.y * p2.x - t2.x * p2.y));
let m = (chord_dy, -chord_dx, -(chord_dy * p1.x - chord_dx * p1.y));
let m_len = (m.0 * m.0 + m.1 * m.1).sqrt();
if m_len < 1e-12 { return None; }
let m = (m.0 / m_len, m.1 / m_len, m.2 / m_len);
let chord_ux = chord_dx / chord_len;
let chord_uy = chord_dy / chord_len;
let n1 = vect2d::new(-t1.y, t1.x);
let n2 = vect2d::new(-t2.y, t2.x);
let denom = n1.x * n2.y - n1.y * n2.x;
let bulge_sign = if denom.abs() > 1e-12 {
let s = (chord_dx * n2.y - chord_dy * n2.x) / denom;
let center_guess = vect2d::new(p1.x + s * n1.x, p1.y + s * n1.y);
let center_side = m.0 * center_guess.x + m.1 * center_guess.y + m.2;
if center_side >= 0.0 { -1.0 } else { 1.0 } } else {
1.0
};
let perp_x = -chord_uy;
let perp_y = chord_ux;
let l1v = [l1.0, l1.1, l1.2];
let l2v = [l2.0, l2.1, l2.2];
let mv = [m.0, m.1, m.2];
let target_mid = vect2d::new(
chord_mid.x + perp_x * bulge_sign * bulge * half_chord,
chord_mid.y + perp_y * bulge_sign * bulge * half_chord,
);
let l1_val = l1.0 * target_mid.x + l1.1 * target_mid.y + l1.2;
let l2_val = l2.0 * target_mid.x + l2.1 * target_mid.y + l2.2;
let m_val = m.0 * target_mid.x + m.1 * target_mid.y + m.2;
if m_val.abs() < 1e-12 { return None; }
let lambda = -(l1_val * l2_val) / (m_val * m_val);
let mut q = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
q[i][j] = (l1v[i] * l2v[j] + l2v[i] * l1v[j]) / 2.0 + lambda * mv[i] * mv[j];
}
}
let a = q[0][0];
let b = q[0][1]; let c = q[1][1];
let d = q[0][2]; let e = q[1][2]; let f = q[2][2];
let det2 = a * c - b * b;
if det2 <= 1e-12 {
return None; }
let cx = (b * e - c * d) / det2;
let cy = (b * d - a * e) / det2;
let center = vect2d::new(cx, cy);
let trace = a + c;
let disc = ((a - c) * (a - c) + 4.0 * b * b).sqrt();
let ev1 = (trace + disc) / 2.0;
let ev2 = (trace - disc) / 2.0;
if ev1.abs() < 1e-12 || ev2.abs() < 1e-12 { return None; }
let f_prime = f + d * cx + e * cy; let rx_sq = -f_prime / ev1;
let ry_sq = -f_prime / ev2;
if rx_sq <= 0.0 || ry_sq <= 0.0 { return None; }
let rx = rx_sq.sqrt();
let ry = ry_sq.sqrt();
let rot = if b.abs() > 1e-12 {
(ev1 - a).atan2(b)
} else if a <= c {
0.0
} else {
std::f64::consts::FRAC_PI_2
};
let cr = rot.cos();
let sr = rot.sin();
let lx1 = (p1.x - cx) * cr + (p1.y - cy) * sr;
let ly1 = -(p1.x - cx) * sr + (p1.y - cy) * cr;
let lx2 = (p2.x - cx) * cr + (p2.y - cy) * sr;
let ly2 = -(p2.x - cx) * sr + (p2.y - cy) * cr;
let sa = (ly1 / ry).atan2(lx1 / rx);
let ea = (ly2 / ry).atan2(lx2 / rx);
let arc_tan_x = -rx * sa.sin() * cr - ry * sa.cos() * sr;
let arc_tan_y = -rx * sa.sin() * sr + ry * sa.cos() * cr;
let dot = arc_tan_x * t1.x + arc_tan_y * t1.y;
let ccw = dot > 0.0;
Some((center, rx, ry, rot, sa, ea, ccw))
}
#[cfg(test)]
mod tests {
use super::*;
fn arc_point(center: vect2d, rx: f64, ry: f64, rot: f64, t: f64) -> vect2d {
let cr = rot.cos(); let sr = rot.sin();
let ct = t.cos(); let st = t.sin();
vect2d::new(
center.x + rx * ct * cr - ry * st * sr,
center.y + rx * ct * sr + ry * st * cr,
)
}
fn arc_tangent(rx: f64, ry: f64, rot: f64, t: f64) -> vect2d {
let cr = rot.cos(); let sr = rot.sin();
let ct = t.cos(); let st = t.sin();
vect2d::new(
-rx * st * cr - ry * ct * sr,
-rx * st * sr + ry * ct * cr,
)
}
#[test]
fn test_fit_semicircle_bulge1() {
let result = fit_earc_tangent(
vect2d::new(0.0, 0.0), vect2d::new(0.0, 1.0),
vect2d::new(10.0, 0.0), vect2d::new(0.0, -1.0),
1.0,
);
let (center, rx, ry, rot, sa, ea, _ccw) = result.expect("should converge");
eprintln!("b=1: center=({:.4},{:.4}) rx={:.4} ry={:.4} rot={:.4}", center.x, center.y, rx, ry, rot);
let sp = arc_point(center, rx, ry, rot, sa);
let ep = arc_point(center, rx, ry, rot, ea);
assert!((sp.x - 0.0).abs() < 0.01, "start x: {:.4}", sp.x);
assert!((sp.y - 0.0).abs() < 0.01, "start y: {:.4}", sp.y);
assert!((ep.x - 10.0).abs() < 0.01, "end x: {:.4}", ep.x);
assert!((ep.y - 0.0).abs() < 0.01, "end y: {:.4}", ep.y);
assert!((rx - ry).abs() < 0.01, "should be circular: rx={:.4} ry={:.4}", rx, ry);
}
#[test]
fn test_fit_small_bulge() {
let result = fit_earc_tangent(
vect2d::new(0.0, 0.0), vect2d::new(1.0, 0.0),
vect2d::new(5.0, 5.0), vect2d::new(0.0, 1.0),
0.3,
);
let (center, rx, ry, rot, sa, ea, _ccw) = result.expect("should converge");
eprintln!("b=0.3: center=({:.4},{:.4}) rx={:.4} ry={:.4} rot={:.4}", center.x, center.y, rx, ry, rot);
let sp = arc_point(center, rx, ry, rot, sa);
let ep = arc_point(center, rx, ry, rot, ea);
assert!((sp.x - 0.0).abs() < 0.01, "start x: {:.4}", sp.x);
assert!((sp.y - 0.0).abs() < 0.01, "start y: {:.4}", sp.y);
assert!((ep.x - 5.0).abs() < 0.01, "end x: {:.4}", ep.x);
assert!((ep.y - 5.0).abs() < 0.01, "end y: {:.4}", ep.y);
}
#[test]
fn test_fit_tangent_directions() {
let result = fit_earc_tangent(
vect2d::new(0.0, 0.0), vect2d::new(1.0, 0.0),
vect2d::new(5.0, 5.0), vect2d::new(0.0, 1.0),
0.5,
);
let (_center, rx, ry, rot, sa, ea, _ccw) = result.expect("should converge");
let ts = arc_tangent(rx, ry, rot, sa);
let ts_len = (ts.x * ts.x + ts.y * ts.y).sqrt();
let cross_s = (ts.x * 0.0 - ts.y * 1.0).abs() / ts_len;
assert!(cross_s < 0.01, "start tangent should be horizontal, cross={:.4}", cross_s);
let te = arc_tangent(rx, ry, rot, ea);
let te_len = (te.x * te.x + te.y * te.y).sqrt();
let cross_e = (te.x * 1.0 - te.y * 0.0).abs() / te_len;
assert!(cross_e < 0.01, "end tangent should be vertical, cross={:.4}", cross_e);
}
#[test]
fn test_fit_exact_endpoints() {
let result = fit_earc_tangent(
vect2d::new(1.0, 0.0), vect2d::new(1.0, 0.0),
vect2d::new(3.0, 1.0), vect2d::new(0.0, 1.0),
0.2,
);
let (center, rx, ry, rot, sa, ea, _ccw) = result.expect("should converge");
let sp = arc_point(center, rx, ry, rot, sa);
let ep = arc_point(center, rx, ry, rot, ea);
assert!((sp.x - 1.0).abs() < 0.001, "start x: {:.6}", sp.x);
assert!((sp.y - 0.0).abs() < 0.001, "start y: {:.6}", sp.y);
assert!((ep.x - 3.0).abs() < 0.001, "end x: {:.6}", ep.x);
assert!((ep.y - 1.0).abs() < 0.001, "end y: {:.6}", ep.y);
}
#[test]
fn test_fit_degenerate_zero_chord() {
let result = fit_earc_tangent(
vect2d::new(0.0, 0.0), vect2d::new(1.0, 0.0),
vect2d::new(0.0, 0.0), vect2d::new(0.0, 1.0),
0.5,
);
assert!(result.is_none(), "zero chord should be degenerate");
}
}