forked from 1b-agent/opencadstudio-upstream
Merge pull request #1714 from yztink/fix/arc-tangent-line
fix(draw): support line tangency in arc construction and resolve trim contact precision
This commit is contained in:
commit
3cc3ead3a6
3 files changed
+955
-19
No files matched your search
@@ -564,6 +564,354 @@ fn target_to_circle2d_pure(target: TangentObject, plane: WorkingPlane) -> Option
|
||||
}
|
||||
}
|
||||
|
||||
fn target_to_line2d(target: TangentObject, plane: WorkingPlane) -> Option<(DVec2, DVec2)> {
|
||||
match target {
|
||||
TangentObject::Line { p1, p2 } => {
|
||||
let loc1 = plane.to_local(p1);
|
||||
let loc2 = plane.to_local(p2);
|
||||
Some((DVec2::new(loc1.x, loc1.y), DVec2::new(loc2.x, loc2.y)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Solves PPC (Point, Point, Curve) where Curve is an infinite 2D Line passing through `l_p1` and `l_p2`.
|
||||
/// Closed-form quadratic solution along the perpendicular bisector of `p1` and `p2`.
|
||||
pub fn solve_line_ppc(
|
||||
l_p1: DVec2,
|
||||
l_p2: DVec2,
|
||||
p1: DVec2,
|
||||
p2: DVec2,
|
||||
hint: DVec2,
|
||||
) -> Option<DVec2> {
|
||||
let line_vec = l_p2 - l_p1;
|
||||
let line_len = line_vec.length();
|
||||
if line_len < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let v_line = line_vec / line_len;
|
||||
let n_line = DVec2::new(-v_line.y, v_line.x);
|
||||
|
||||
let d_vec = p2 - p1;
|
||||
let d_len = d_vec.length();
|
||||
if d_len < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let u_seg = d_vec / d_len;
|
||||
let w_bisector = DVec2::new(-u_seg.y, u_seg.x);
|
||||
let m = (p1 + p2) * 0.5;
|
||||
let r0 = d_len * 0.5;
|
||||
let r0_sq = r0 * r0;
|
||||
|
||||
let h0 = (m - l_p1).dot(n_line);
|
||||
let k = w_bisector.dot(n_line);
|
||||
|
||||
let a = k * k - 1.0;
|
||||
let b = 2.0 * h0 * k;
|
||||
let c = h0 * h0 - r0_sq;
|
||||
|
||||
let mut s_candidates = Vec::with_capacity(2);
|
||||
if a.abs() < 1e-9 {
|
||||
if b.abs() > 1e-9 {
|
||||
s_candidates.push(-c / b);
|
||||
}
|
||||
} else {
|
||||
let disc = 4.0 * (h0 * h0 - r0_sq * (1.0 - k * k));
|
||||
if disc >= -1e-9 {
|
||||
let sqrt_disc = disc.max(0.0).sqrt();
|
||||
s_candidates.push((-b + sqrt_disc) / (2.0 * a));
|
||||
s_candidates.push((-b - sqrt_disc) / (2.0 * a));
|
||||
}
|
||||
}
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
for s in s_candidates {
|
||||
let center = m + w_bisector * s;
|
||||
let r = (r0_sq + s * s).sqrt();
|
||||
if r < 1e-4 || r > 1e8 {
|
||||
continue;
|
||||
}
|
||||
let t_pt = center - n_line * ((center - l_p1).dot(n_line));
|
||||
// Ensure t_pt, p1, p2 are non-collinear
|
||||
let cross = (p2.x - p1.x) * (t_pt.y - p1.y) - (p2.y - p1.y) * (t_pt.x - p1.x);
|
||||
let norm_cross = cross.abs() / (d_len * (t_pt - p1).length().max(1.0));
|
||||
if norm_cross < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
candidates.push((t_pt, r));
|
||||
}
|
||||
|
||||
candidates
|
||||
.into_iter()
|
||||
.min_by(|(pt_a, r_a), (pt_b, r_b)| {
|
||||
let dist_a = (*pt_a - hint).length_squared() + if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
||||
let dist_b = (*pt_b - hint).length_squared() + if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
||||
dist_a.total_cmp(&dist_b)
|
||||
})
|
||||
.map(|(pt, _)| pt)
|
||||
}
|
||||
|
||||
/// Solves CCP (Curve, Curve, Point) where both curves are Lines.
|
||||
pub fn solve_line_line_ccp(
|
||||
l1_p1: DVec2,
|
||||
l1_p2: DVec2,
|
||||
l2_p1: DVec2,
|
||||
l2_p2: DVec2,
|
||||
p: DVec2,
|
||||
hint1: DVec2,
|
||||
hint2: DVec2,
|
||||
) -> Option<(DVec2, DVec2)> {
|
||||
let d1 = l1_p2 - l1_p1;
|
||||
let d2 = l2_p2 - l2_p1;
|
||||
let l1_len = d1.length();
|
||||
let l2_len = d2.length();
|
||||
if l1_len < 1e-9 || l2_len < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let v1 = d1 / l1_len;
|
||||
let v2 = d2 / l2_len;
|
||||
let n1 = DVec2::new(-v1.y, v1.x);
|
||||
let n2 = DVec2::new(-v2.y, v2.x);
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
let cross12 = v1.x * v2.y - v1.y * v2.x;
|
||||
|
||||
if cross12.abs() < 1e-6 {
|
||||
// Parallel lines
|
||||
let dist_lines = (l2_p1 - l1_p1).dot(n1).abs();
|
||||
let r = dist_lines * 0.5;
|
||||
if r > 1e-4 {
|
||||
let mid_line_p = l1_p1 + n1 * ((l2_p1 - l1_p1).dot(n1) * 0.5);
|
||||
let w = mid_line_p - p;
|
||||
let a = 1.0;
|
||||
let b = 2.0 * w.dot(v1);
|
||||
let c = w.length_squared() - r * r;
|
||||
let disc = b * b - 4.0 * a * c;
|
||||
if disc >= 0.0 {
|
||||
let sqrt_d = disc.sqrt();
|
||||
for t in [(-b + sqrt_d) / (2.0 * a), (-b - sqrt_d) / (2.0 * a)] {
|
||||
let center = mid_line_p + v1 * t;
|
||||
let t1 = center - n1 * ((center - l1_p1).dot(n1));
|
||||
let t2 = center - n2 * ((center - l2_p1).dot(n2));
|
||||
candidates.push((t1, t2, r));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Intersecting lines: find intersection point V
|
||||
let delta = l2_p1 - l1_p1;
|
||||
let t_intersect = (delta.x * v2.y - delta.y * v2.x) / cross12;
|
||||
let v_intersect = l1_p1 + v1 * t_intersect;
|
||||
|
||||
let b1 = (v1 + v2).normalize_or_zero();
|
||||
let b2 = (v1 - v2).normalize_or_zero();
|
||||
|
||||
for bisector in [b1, b2] {
|
||||
if bisector.length_squared() < 0.5 {
|
||||
continue;
|
||||
}
|
||||
let sigma = (bisector.x * v1.y - bisector.y * v1.x).abs();
|
||||
if sigma < 1e-6 || (1.0 - sigma * sigma).abs() < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
let cos2 = 1.0 - sigma * sigma;
|
||||
let w = v_intersect - p;
|
||||
let a = cos2;
|
||||
let b = 2.0 * w.dot(bisector);
|
||||
let c = w.length_squared();
|
||||
let disc = b * b - 4.0 * a * c;
|
||||
if disc >= 0.0 {
|
||||
let sqrt_d = disc.sqrt();
|
||||
for t in [(-b + sqrt_d) / (2.0 * a), (-b - sqrt_d) / (2.0 * a)] {
|
||||
let center = v_intersect + bisector * t;
|
||||
let r = t.abs() * sigma;
|
||||
if r < 1e-4 || r > 1e8 {
|
||||
continue;
|
||||
}
|
||||
let t1 = center - n1 * ((center - l1_p1).dot(n1));
|
||||
let t2 = center - n2 * ((center - l2_p1).dot(n2));
|
||||
candidates.push((t1, t2, r));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
.into_iter()
|
||||
.min_by(|(t1_a, t2_a, r_a), (t1_b, t2_b, r_b)| {
|
||||
let dist_a = (*t1_a - hint1).length_squared() + (*t2_a - hint2).length_squared() + if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
||||
let dist_b = (*t1_b - hint1).length_squared() + (*t2_b - hint2).length_squared() + if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
||||
dist_a.total_cmp(&dist_b)
|
||||
})
|
||||
.map(|(t1, t2, _)| (t1, t2))
|
||||
}
|
||||
|
||||
/// Solves CCP (Curve, Curve, Point) where Curve 1 is a Circle and Curve 2 is a Line.
|
||||
pub fn solve_circle_line_ccp(
|
||||
c1: Circle2D,
|
||||
l_p1: DVec2,
|
||||
l_p2: DVec2,
|
||||
p: DVec2,
|
||||
hint_c: DVec2,
|
||||
hint_l: DVec2,
|
||||
) -> Option<(DVec2, DVec2)> {
|
||||
let line_vec = l_p2 - l_p1;
|
||||
let line_len = line_vec.length();
|
||||
if line_len < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let v_line = line_vec / line_len;
|
||||
let n_line = DVec2::new(-v_line.y, v_line.x);
|
||||
|
||||
let n_steps = 144;
|
||||
let dt = TAU / (n_steps as f64);
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
let eval_t = |theta: f64| -> Option<(f64, DVec2, f64, DVec2)> {
|
||||
let n_hat = DVec2::new(theta.cos(), theta.sin());
|
||||
let pt = c1.center + n_hat * c1.radius;
|
||||
let w = pt - p;
|
||||
let w_dot_n = w.dot(n_hat);
|
||||
if w_dot_n.abs() < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let lambda = -w.length_squared() / (2.0 * w_dot_n);
|
||||
let r = lambda.abs();
|
||||
if r < 1e-4 || r > 1e8 {
|
||||
return None;
|
||||
}
|
||||
let center = pt + n_hat * lambda;
|
||||
let d_line = (center - l_p1).dot(n_line).abs();
|
||||
let res = d_line - r;
|
||||
Some((res, pt, r, center))
|
||||
};
|
||||
|
||||
for i in 0..n_steps {
|
||||
let t_a = i as f64 * dt;
|
||||
let t_b = (i + 1) as f64 * dt;
|
||||
|
||||
let Some((res_a, _, _, _)) = eval_t(t_a) else { continue; };
|
||||
let Some((res_b, _, _, _)) = eval_t(t_b) else { continue; };
|
||||
|
||||
if res_a.signum() != res_b.signum() {
|
||||
let mut lo = t_a;
|
||||
let mut hi = t_b;
|
||||
let mut best_t = (lo + hi) * 0.5;
|
||||
for _ in 0..16 {
|
||||
let mid = (lo + hi) * 0.5;
|
||||
let Some((res_m, _, _, _)) = eval_t(mid) else { break; };
|
||||
best_t = mid;
|
||||
if res_m.abs() < 1e-8 {
|
||||
break;
|
||||
}
|
||||
if res_m.signum() == res_a.signum() {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
if let Some((res, pt_c, r, center)) = eval_t(best_t) {
|
||||
if res.abs() < 1e-3 {
|
||||
let pt_l = center - n_line * ((center - l_p1).dot(n_line));
|
||||
candidates.push((pt_c, pt_l, r));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
.into_iter()
|
||||
.min_by(|(c_a, l_a, r_a), (c_b, l_b, r_b)| {
|
||||
let dist_a = (*c_a - hint_c).length_squared() + (*l_a - hint_l).length_squared() + if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
||||
let dist_b = (*c_b - hint_c).length_squared() + (*l_b - hint_l).length_squared() + if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
||||
dist_a.total_cmp(&dist_b)
|
||||
})
|
||||
.map(|(pt_c, pt_l, _)| (pt_c, pt_l))
|
||||
}
|
||||
|
||||
/// Solves CCP (Curve, Curve, Point) where Curve 1 is an Ellipse and Curve 2 is a Line.
|
||||
pub fn solve_ellipse_line_ccp(
|
||||
ell: &Ellipse2D,
|
||||
l_p1: DVec2,
|
||||
l_p2: DVec2,
|
||||
p: DVec2,
|
||||
hint_e: DVec2,
|
||||
hint_l: DVec2,
|
||||
) -> Option<(DVec2, DVec2)> {
|
||||
let line_vec = l_p2 - l_p1;
|
||||
let line_len = line_vec.length();
|
||||
if line_len < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let v_line = line_vec / line_len;
|
||||
let n_line = DVec2::new(-v_line.y, v_line.x);
|
||||
|
||||
let n_steps = 144;
|
||||
let dt = TAU / (n_steps as f64);
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
let eval_t = |t: f64| -> Option<(f64, DVec2, f64, DVec2)> {
|
||||
let pt = ell.point_at(t);
|
||||
let n_hat = ell.normal_at(t);
|
||||
let w = pt - p;
|
||||
let w_dot_n = w.dot(n_hat);
|
||||
if w_dot_n.abs() < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let lambda = -w.length_squared() / (2.0 * w_dot_n);
|
||||
let r = lambda.abs();
|
||||
if r < 1e-4 || r > 1e8 {
|
||||
return None;
|
||||
}
|
||||
let center = pt + n_hat * lambda;
|
||||
let d_line = (center - l_p1).dot(n_line).abs();
|
||||
let res = d_line - r;
|
||||
Some((res, pt, r, center))
|
||||
};
|
||||
|
||||
for i in 0..n_steps {
|
||||
let t_a = i as f64 * dt;
|
||||
let t_b = (i + 1) as f64 * dt;
|
||||
|
||||
let Some((res_a, _, _, _)) = eval_t(t_a) else { continue; };
|
||||
let Some((res_b, _, _, _)) = eval_t(t_b) else { continue; };
|
||||
|
||||
if res_a.signum() != res_b.signum() {
|
||||
let mut lo = t_a;
|
||||
let mut hi = t_b;
|
||||
let mut best_t = (lo + hi) * 0.5;
|
||||
for _ in 0..16 {
|
||||
let mid = (lo + hi) * 0.5;
|
||||
let Some((res_m, _, _, _)) = eval_t(mid) else { break; };
|
||||
best_t = mid;
|
||||
if res_m.abs() < 1e-8 {
|
||||
break;
|
||||
}
|
||||
if res_m.signum() == res_a.signum() {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
if let Some((res, pt_e, r, center)) = eval_t(best_t) {
|
||||
if res.abs() < 1e-3 {
|
||||
let pt_l = center - n_line * ((center - l_p1).dot(n_line));
|
||||
candidates.push((pt_e, pt_l, r));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
.into_iter()
|
||||
.min_by(|(e_a, l_a, r_a), (e_b, l_b, r_b)| {
|
||||
let dist_a = (*e_a - hint_e).length_squared() + (*l_a - hint_l).length_squared() + if *r_a > 50000.0 { 1e8 } else { 0.0 };
|
||||
let dist_b = (*e_b - hint_e).length_squared() + (*l_b - hint_l).length_squared() + if *r_b > 50000.0 { 1e8 } else { 0.0 };
|
||||
dist_a.total_cmp(&dist_b)
|
||||
})
|
||||
.map(|(pt_e, pt_l, _)| (pt_e, pt_l))
|
||||
}
|
||||
|
||||
/// Solves PPC (Point, Point, Curve) where Curve is an Ellipse.
|
||||
/// Scans the full perimeter t ∈ [0, 2π) to find all valid tangent circles passing through p1 and p2,
|
||||
/// and selects the one closest to `hint`.
|
||||
@@ -882,11 +1230,55 @@ fn solve_tangent_ccp(
|
||||
let hint1 = DVec2::new(plane.to_local(h1).x, plane.to_local(h1).y);
|
||||
let hint2 = DVec2::new(plane.to_local(h2).x, plane.to_local(h2).y);
|
||||
|
||||
let l1 = target_to_line2d(t1, plane);
|
||||
let l2 = target_to_line2d(t2, plane);
|
||||
let ell1 = target_to_ellipse2d(t1, plane);
|
||||
let ell2 = target_to_ellipse2d(t2, plane);
|
||||
let c1 = target_to_circle2d_pure(t1, plane);
|
||||
let c2 = target_to_circle2d_pure(t2, plane);
|
||||
|
||||
// Line + Line
|
||||
if let (Some((l1_a, l1_b)), Some((l2_a, l2_b))) = (l1, l2) {
|
||||
let (w1, w2) = solve_line_line_ccp(l1_a, l1_b, l2_a, l2_b, p, hint1, hint2)?;
|
||||
return Some((
|
||||
plane.to_world(DVec3::new(w1.x, w1.y, z)),
|
||||
plane.to_world(DVec3::new(w2.x, w2.y, z)),
|
||||
));
|
||||
}
|
||||
|
||||
// Line + Circle / Circle + Line
|
||||
if let (Some((l1_a, l1_b)), Some(c2)) = (l1, c2) {
|
||||
let (w2, w1) = solve_circle_line_ccp(c2, l1_a, l1_b, p, hint2, hint1)?;
|
||||
return Some((
|
||||
plane.to_world(DVec3::new(w1.x, w1.y, z)),
|
||||
plane.to_world(DVec3::new(w2.x, w2.y, z)),
|
||||
));
|
||||
}
|
||||
if let (Some(c1), Some((l2_a, l2_b))) = (c1, l2) {
|
||||
let (w1, w2) = solve_circle_line_ccp(c1, l2_a, l2_b, p, hint1, hint2)?;
|
||||
return Some((
|
||||
plane.to_world(DVec3::new(w1.x, w1.y, z)),
|
||||
plane.to_world(DVec3::new(w2.x, w2.y, z)),
|
||||
));
|
||||
}
|
||||
|
||||
// Line + Ellipse / Ellipse + Line
|
||||
if let (Some((l1_a, l1_b)), Some(ell2)) = (l1, ell2) {
|
||||
let (w2, w1) = solve_ellipse_line_ccp(&ell2, l1_a, l1_b, p, hint2, hint1)?;
|
||||
return Some((
|
||||
plane.to_world(DVec3::new(w1.x, w1.y, z)),
|
||||
plane.to_world(DVec3::new(w2.x, w2.y, z)),
|
||||
));
|
||||
}
|
||||
if let (Some(ell1), Some((l2_a, l2_b))) = (ell1, l2) {
|
||||
let (w1, w2) = solve_ellipse_line_ccp(&ell1, l2_a, l2_b, p, hint1, hint2)?;
|
||||
return Some((
|
||||
plane.to_world(DVec3::new(w1.x, w1.y, z)),
|
||||
plane.to_world(DVec3::new(w2.x, w2.y, z)),
|
||||
));
|
||||
}
|
||||
|
||||
// Ellipse + Circle / Circle + Ellipse
|
||||
if let (Some(ell1), Some(c2)) = (ell1, c2) {
|
||||
let (w1, w2) = solve_ellipse_circle_ccp(&ell1, c2, p, hint1, hint2)?;
|
||||
return Some((
|
||||
@@ -903,6 +1295,7 @@ fn solve_tangent_ccp(
|
||||
));
|
||||
}
|
||||
|
||||
// Ellipse + Ellipse
|
||||
if let (Some(ell1), Some(ell2)) = (ell1, ell2) {
|
||||
let (w1, w2) = solve_ellipse_ellipse_ccp(&ell1, &ell2, p, hint1, hint2)?;
|
||||
return Some((
|
||||
@@ -911,6 +1304,7 @@ fn solve_tangent_ccp(
|
||||
));
|
||||
}
|
||||
|
||||
// Circle + Circle
|
||||
if let (Some(c1), Some(c2)) = (c1, c2) {
|
||||
let sols = apollonius::solve_ccp(c1, c2, p);
|
||||
if let Some(best) = apollonius::best_ccp(&sols, hint1, hint2) {
|
||||
@@ -936,6 +1330,13 @@ fn solve_tangent_ppc(
|
||||
let pt2 = DVec2::new(plane.to_local(p2).x, plane.to_local(p2).y);
|
||||
let hint1 = DVec2::new(plane.to_local(h1).x, plane.to_local(h1).y);
|
||||
|
||||
if let Some((l1, l2)) = target_to_line2d(t1, plane) {
|
||||
if let Some(w1_2d) = solve_line_ppc(l1, l2, pt1, pt2, hint1) {
|
||||
return Some(plane.to_world(DVec3::new(w1_2d.x, w1_2d.y, z)));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(ell) = target_to_ellipse2d(t1, plane) {
|
||||
if let Some(w1_2d) = solve_ellipse_ppc(&ell, pt1, pt2, hint1) {
|
||||
return Some(plane.to_world(DVec3::new(w1_2d.x, w1_2d.y, z)));
|
||||
@@ -3718,4 +4119,192 @@ mod acad_flow_tests {
|
||||
"resolved point must lie on ellipse"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_3p_fluid_tangent_start_from_line() {
|
||||
let mut cmd = Arc3PCommand::new();
|
||||
let line = TangentObject::Line {
|
||||
p1: DVec3::new(-10.0, 0.0, 0.0),
|
||||
p2: DVec3::new(10.0, 0.0, 0.0),
|
||||
};
|
||||
|
||||
// Pick 1: Tangent on line y = 0
|
||||
let r1 = cmd.on_point_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
||||
assert!(matches!(r1, Some(CmdResult::NeedPoint)));
|
||||
|
||||
// Pick 2: Pass-through point
|
||||
let r2 = cmd.on_point_with_tangent(DVec3::new(0.0, 2.0, 0.0), None);
|
||||
assert!(matches!(r2, Some(CmdResult::NeedPoint)));
|
||||
|
||||
// Live preview at (2, 2, 0)
|
||||
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
||||
assert!(!preview.is_empty(), "Live preview must generate arc wire");
|
||||
assert!(
|
||||
preview[0].tangent_geoms.iter().any(|g| matches!(g, TangentGeom::Arc { .. })),
|
||||
"Preview must produce an analytical arc geometry"
|
||||
);
|
||||
|
||||
// Pick 3: Second pass-through point
|
||||
let r3 = cmd.on_point_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
||||
let arc = committed_arc(r3.expect("command completed"));
|
||||
let center = DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
||||
|
||||
// Expected center: (1.0, 1.25, 0.0), radius: 1.25
|
||||
assert!((center.x - 1.0).abs() < 1e-3);
|
||||
assert!((center.y - 1.25).abs() < 1e-3);
|
||||
assert!((arc.radius - 1.25).abs() < 1e-3);
|
||||
|
||||
// Tangent to y = 0 line
|
||||
assert!((center.y.abs() - arc.radius).abs() < 1e-3);
|
||||
// Passes through (0, 2) and (2, 2)
|
||||
assert!(((center - DVec3::new(0.0, 2.0, 0.0)).length() - arc.radius).abs() < 1e-3);
|
||||
assert!(((center - DVec3::new(2.0, 2.0, 0.0)).length() - arc.radius).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_3p_fluid_tangent_end_at_line() {
|
||||
let mut cmd = Arc3PCommand::new();
|
||||
let line = TangentObject::Line {
|
||||
p1: DVec3::new(-10.0, 0.0, 0.0),
|
||||
p2: DVec3::new(10.0, 0.0, 0.0),
|
||||
};
|
||||
|
||||
// Pick 1: Point 1
|
||||
cmd.on_point_with_tangent(DVec3::new(0.0, 2.0, 0.0), None);
|
||||
// Pick 2: Point 2
|
||||
cmd.on_point_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
||||
|
||||
// Preview with tangent snap on line
|
||||
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
||||
assert!(!preview.is_empty(), "Live preview must generate arc wire");
|
||||
assert!(
|
||||
preview[0].tangent_geoms.iter().any(|g| matches!(g, TangentGeom::Arc { .. })),
|
||||
"Preview must produce an analytical arc geometry"
|
||||
);
|
||||
|
||||
// Pick 3: Tangent on line
|
||||
let r3 = cmd.on_point_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
||||
let arc = committed_arc(r3.expect("command completed"));
|
||||
let center = DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
||||
|
||||
assert!((center.x - 1.0).abs() < 1e-3);
|
||||
assert!((center.y - 1.25).abs() < 1e-3);
|
||||
assert!((arc.radius - 1.25).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_command_start_from_tangent_line() {
|
||||
let mut cmd = ArcCommand::new();
|
||||
let line = TangentObject::Line {
|
||||
p1: DVec3::new(-10.0, 0.0, 0.0),
|
||||
p2: DVec3::new(10.0, 0.0, 0.0),
|
||||
};
|
||||
|
||||
// Pick 1: Tangent snap to line
|
||||
let r1 = cmd.on_point_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
||||
assert!(matches!(r1, Some(CmdResult::NeedPoint)));
|
||||
|
||||
// Pick 2: Point
|
||||
let r2 = cmd.on_point_with_tangent(DVec3::new(0.0, 2.0, 0.0), None);
|
||||
assert!(matches!(r2, Some(CmdResult::NeedPoint)));
|
||||
|
||||
// Move to Pick 3: Live preview should show curved arc wire, NOT fallback straight line
|
||||
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
||||
assert!(!preview.is_empty(), "Live preview must exist");
|
||||
assert!(
|
||||
preview[0].tangent_geoms.iter().any(|g| matches!(g, TangentGeom::Arc { .. })),
|
||||
"Preview must produce an arc, not a straight fallback line"
|
||||
);
|
||||
|
||||
// Pick 3: Point -> Must commit CadArc!
|
||||
let r3 = cmd.on_point_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
||||
let arc = committed_arc(r3.expect("arc command must commit"));
|
||||
let center = DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
||||
assert!((center.y.abs() - arc.radius).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_command_end_at_tangent_line() {
|
||||
let mut cmd = ArcCommand::new();
|
||||
let line = TangentObject::Line {
|
||||
p1: DVec3::new(-10.0, 0.0, 0.0),
|
||||
p2: DVec3::new(10.0, 0.0, 0.0),
|
||||
};
|
||||
|
||||
// Pick 1: Point
|
||||
cmd.on_point_with_tangent(DVec3::new(0.0, 2.0, 0.0), None);
|
||||
// Pick 2: Point
|
||||
cmd.on_point_with_tangent(DVec3::new(2.0, 2.0, 0.0), None);
|
||||
|
||||
// Preview with tangent on line
|
||||
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
||||
assert!(!preview.is_empty());
|
||||
assert!(
|
||||
preview[0].tangent_geoms.iter().any(|g| matches!(g, TangentGeom::Arc { .. })),
|
||||
"Preview must produce an arc, not a straight fallback line"
|
||||
);
|
||||
|
||||
// Pick 3: Tangent on line -> commits CadArc!
|
||||
let r3 = cmd.on_point_with_tangent(DVec3::new(1.0, 0.0, 0.0), Some(line));
|
||||
let arc = committed_arc(r3.expect("arc command must commit"));
|
||||
let center = DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
||||
assert!((center.y.abs() - arc.radius).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_3p_fluid_tangent_two_lines_and_point() {
|
||||
let mut cmd = Arc3PCommand::new();
|
||||
let line1 = TangentObject::Line {
|
||||
p1: DVec3::new(-10.0, 0.0, 0.0),
|
||||
p2: DVec3::new(10.0, 0.0, 0.0),
|
||||
};
|
||||
let line2 = TangentObject::Line {
|
||||
p1: DVec3::new(0.0, -10.0, 0.0),
|
||||
p2: DVec3::new(0.0, 10.0, 0.0),
|
||||
};
|
||||
|
||||
// Pick 1: Tangent on Line 1 (y = 0)
|
||||
cmd.on_point_with_tangent(DVec3::new(2.0, 0.0, 0.0), Some(line1));
|
||||
// Pick 2: Point in first quadrant
|
||||
cmd.on_point_with_tangent(DVec3::new(1.0 - (0.5_f64).sqrt(), 1.0 - (0.5_f64).sqrt(), 0.0), None);
|
||||
// Pick 3: Tangent on Line 2 (x = 0)
|
||||
let r3 = cmd.on_point_with_tangent(DVec3::new(0.0, 2.0, 0.0), Some(line2));
|
||||
let arc = committed_arc(r3.expect("two-line arc command must commit"));
|
||||
let center = DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
||||
|
||||
// Center should be equidistant to both lines (x = y = radius)
|
||||
assert!((center.x.abs() - arc.radius).abs() < 1e-3);
|
||||
assert!((center.y.abs() - arc.radius).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_3p_fluid_tangent_line_and_circle() {
|
||||
let mut cmd = Arc3PCommand::new();
|
||||
let line = TangentObject::Line {
|
||||
p1: DVec3::new(-20.0, 0.0, 0.0),
|
||||
p2: DVec3::new(20.0, 0.0, 0.0),
|
||||
};
|
||||
let circle = TangentObject::Circle {
|
||||
center: DVec3::new(0.0, 10.0, 0.0),
|
||||
radius: 2.0,
|
||||
};
|
||||
|
||||
// Pick 1: Tangent on line
|
||||
cmd.on_point_with_tangent(DVec3::new(0.0, 0.0, 0.0), Some(line));
|
||||
// Pick 2: Middle point
|
||||
cmd.on_point_with_tangent(DVec3::new(3.0, 4.0, 0.0), None);
|
||||
// Pick 3: Tangent on circle
|
||||
let r3 = cmd.on_point_with_tangent(DVec3::new(0.0, 8.0, 0.0), Some(circle));
|
||||
let arc = committed_arc(r3.expect("line and circle arc must commit"));
|
||||
let center = DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
||||
|
||||
// Distance from center to line must equal radius
|
||||
assert!((center.y.abs() - arc.radius).abs() < 1e-3);
|
||||
// Distance from center to circle must equal radius +- circle.radius
|
||||
let dist_to_circle = center.distance(DVec3::new(0.0, 10.0, 0.0));
|
||||
assert!(
|
||||
(dist_to_circle - (arc.radius + 2.0)).abs() < 1e-3
|
||||
|| (dist_to_circle - (arc.radius - 2.0).abs()).abs() < 1e-3
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4923,6 +4923,357 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_line_tangent_to_arc() {
|
||||
use codec::entities::{Arc as ArcEnt, Line as LineEnt};
|
||||
let mut l1 = LineEnt::new();
|
||||
l1.common.handle = Handle::new(1);
|
||||
l1.start = Vector3::new(-10.0, 0.0, 0.0);
|
||||
l1.end = Vector3::new(10.0, 0.0, 0.0);
|
||||
|
||||
let mut a = ArcEnt::new();
|
||||
a.common.handle = Handle::new(2);
|
||||
a.center = Vector3::new(0.0, 5.0, 0.0);
|
||||
a.radius = 5.0;
|
||||
a.start_angle = -std::f64::consts::FRAC_PI_2;
|
||||
a.end_angle = std::f64::consts::PI;
|
||||
|
||||
let all = vec![EntityType::Line(l1.clone()), EntityType::Arc(a.clone())];
|
||||
let geos = build_geos(&all);
|
||||
let curve_line = entity_curve_xy(&EntityType::Line(l1.clone())).unwrap();
|
||||
let ts_line = cut_params(&curve_line, Handle::new(1), &geos);
|
||||
assert!(!ts_line.is_empty(), "line must have cut point where tangent arc touches it: ts={:?}", ts_line);
|
||||
|
||||
let curve_arc = entity_curve_xy(&EntityType::Arc(a.clone())).unwrap();
|
||||
let ts_arc = cut_params(&curve_arc, Handle::new(2), &geos);
|
||||
println!("ts_arc: {:?}", ts_arc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_arc_tangent_to_two_lines() {
|
||||
use codec::entities::{Arc as CadArc, Line as LineEnt};
|
||||
use crate::command::{TangentObject, WorkingPlane};
|
||||
use crate::modules::draw::draw::arc::{solve_arc_3_inputs, ArcPick};
|
||||
|
||||
let mut l1 = LineEnt::new();
|
||||
l1.common.handle = Handle::new(1);
|
||||
l1.start = Vector3::new(-10.0, 0.0, 0.0);
|
||||
l1.end = Vector3::new(10.0, 0.0, 0.0);
|
||||
|
||||
let mut l2 = LineEnt::new();
|
||||
l2.common.handle = Handle::new(2);
|
||||
l2.start = Vector3::new(0.0, -10.0, 0.0);
|
||||
l2.end = Vector3::new(0.0, 10.0, 0.0);
|
||||
|
||||
let t1 = TangentObject::Line {
|
||||
p1: DVec3::new(-10.0, 0.0, 0.0),
|
||||
p2: DVec3::new(10.0, 0.0, 0.0),
|
||||
};
|
||||
let t2 = TangentObject::Line {
|
||||
p1: DVec3::new(0.0, -10.0, 0.0),
|
||||
p2: DVec3::new(0.0, 10.0, 0.0),
|
||||
};
|
||||
|
||||
// Middle point: (1 - sqrt(0.5), 1 - sqrt(0.5)) -> radius ~ 1
|
||||
let mid = DVec3::new(1.0 - (0.5_f64).sqrt(), 1.0 - (0.5_f64).sqrt(), 0.0);
|
||||
let sol = solve_arc_3_inputs(
|
||||
ArcPick::Tangent { target: t1, hit: DVec3::new(2.0, 0.0, 0.0) },
|
||||
ArcPick::Point(mid),
|
||||
ArcPick::Tangent { target: t2, hit: DVec3::new(0.0, 2.0, 0.0) },
|
||||
WorkingPlane::default(),
|
||||
);
|
||||
|
||||
let (center, radius, sa, ea) = sol.expect("must solve arc tangent to two lines");
|
||||
let mut arc = CadArc::new();
|
||||
arc.common.handle = Handle::new(3);
|
||||
arc.center = Vector3::new(center.x, center.y, center.z);
|
||||
arc.radius = radius;
|
||||
arc.start_angle = sa;
|
||||
arc.end_angle = ea;
|
||||
|
||||
let all = vec![
|
||||
EntityType::Line(l1.clone()),
|
||||
EntityType::Line(l2.clone()),
|
||||
EntityType::Arc(arc.clone()),
|
||||
];
|
||||
let geos = build_geos(&all);
|
||||
|
||||
let curve_l1 = entity_curve_xy(&EntityType::Line(l1.clone())).unwrap();
|
||||
let ts_l1 = cut_params(&curve_l1, Handle::new(1), &geos);
|
||||
println!("ts_l1: {:?}", ts_l1);
|
||||
|
||||
let curve_l2 = entity_curve_xy(&EntityType::Line(l2.clone())).unwrap();
|
||||
let ts_l2 = cut_params(&curve_l2, Handle::new(2), &geos);
|
||||
println!("ts_l2: {:?}", ts_l2);
|
||||
|
||||
let curve_arc = entity_curve_xy(&EntityType::Arc(arc.clone())).unwrap();
|
||||
let ts_arc = cut_params(&curve_arc, Handle::new(3), &geos);
|
||||
println!("ts_arc: {:?}", ts_arc);
|
||||
|
||||
assert!(!ts_l1.is_empty(), "Line 1 must be cut: ts_l1={:?}", ts_l1);
|
||||
assert!(!ts_l2.is_empty(), "Line 2 must be cut: ts_l2={:?}", ts_l2);
|
||||
|
||||
// Test trimming line 1 arm between (1, 0) and (10, 0)
|
||||
let trim_l1 = pick_trim_at(&all, &geos, Handle::new(1), 5.0, 0.0);
|
||||
println!("trim_l1: {:?}", trim_l1);
|
||||
assert!(trim_l1.is_some(), "trimming line 1 arm must succeed");
|
||||
|
||||
// Test trimming line 2 arm between (0, 1) and (0, 10)
|
||||
let trim_l2 = pick_trim_at(&all, &geos, Handle::new(2), 0.0, 5.0);
|
||||
println!("trim_l2: {:?}", trim_l2);
|
||||
assert!(trim_l2.is_some(), "trimming line 2 arm must succeed");
|
||||
|
||||
// Test trimming the arc
|
||||
let trim_arc = pick_trim_at(&all, &geos, Handle::new(3), mid.x, mid.y);
|
||||
println!("trim_arc: {:?}", trim_arc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_arc_tangent_angled_lines() {
|
||||
use codec::entities::{Arc as CadArc, Line as LineEnt};
|
||||
use crate::command::{TangentObject, WorkingPlane};
|
||||
use crate::modules::draw::draw::arc::{solve_arc_3_inputs, ArcPick};
|
||||
|
||||
// Two lines meeting at (0, 0): one along (cos(20 deg), sin(20 deg)), other along (cos(80 deg), sin(80 deg))
|
||||
let a1 = 20.0_f64.to_radians();
|
||||
let a2 = 80.0_f64.to_radians();
|
||||
let dir1 = DVec3::new(a1.cos(), a1.sin(), 0.0);
|
||||
let dir2 = DVec3::new(a2.cos(), a2.sin(), 0.0);
|
||||
|
||||
let mut l1 = LineEnt::new();
|
||||
l1.common.handle = Handle::new(1);
|
||||
l1.start = Vector3::new(-10.0 * dir1.x, -10.0 * dir1.y, 0.0);
|
||||
l1.end = Vector3::new(50.0 * dir1.x, 50.0 * dir1.y, 0.0);
|
||||
|
||||
let mut l2 = LineEnt::new();
|
||||
l2.common.handle = Handle::new(2);
|
||||
l2.start = Vector3::new(-10.0 * dir2.x, -10.0 * dir2.y, 0.0);
|
||||
l2.end = Vector3::new(50.0 * dir2.x, 50.0 * dir2.y, 0.0);
|
||||
|
||||
let t1 = TangentObject::Line {
|
||||
p1: DVec3::new(l1.start.x, l1.start.y, 0.0),
|
||||
p2: DVec3::new(l1.end.x, l1.end.y, 0.0),
|
||||
};
|
||||
let t2 = TangentObject::Line {
|
||||
p1: DVec3::new(l2.start.x, l2.start.y, 0.0),
|
||||
p2: DVec3::new(l2.end.x, l2.end.y, 0.0),
|
||||
};
|
||||
|
||||
// Midpoint between points at distance 20 on the bisector
|
||||
let bisector_angle = (a1 + a2) * 0.5;
|
||||
let mid = DVec3::new(15.0 * bisector_angle.cos(), 15.0 * bisector_angle.sin(), 0.0);
|
||||
|
||||
let sol = solve_arc_3_inputs(
|
||||
ArcPick::Tangent { target: t1, hit: DVec3::new(20.0 * dir1.x, 20.0 * dir1.y, 0.0) },
|
||||
ArcPick::Point(mid),
|
||||
ArcPick::Tangent { target: t2, hit: DVec3::new(20.0 * dir2.x, 20.0 * dir2.y, 0.0) },
|
||||
WorkingPlane::default(),
|
||||
);
|
||||
|
||||
let (center, radius, sa, ea) = sol.expect("must solve arc tangent to two angled lines");
|
||||
let mut arc = CadArc::new();
|
||||
arc.common.handle = Handle::new(3);
|
||||
arc.center = Vector3::new(center.x, center.y, center.z);
|
||||
arc.radius = radius;
|
||||
arc.start_angle = sa;
|
||||
arc.end_angle = ea;
|
||||
|
||||
let all = vec![
|
||||
EntityType::Line(l1.clone()),
|
||||
EntityType::Line(l2.clone()),
|
||||
EntityType::Arc(arc.clone()),
|
||||
];
|
||||
let geos = build_geos(&all);
|
||||
|
||||
let curve_l1 = entity_curve_xy(&EntityType::Line(l1.clone())).unwrap();
|
||||
let ts_l1 = cut_params(&curve_l1, Handle::new(1), &geos);
|
||||
println!("angled ts_l1: {:?}", ts_l1);
|
||||
|
||||
let curve_l2 = entity_curve_xy(&EntityType::Line(l2.clone())).unwrap();
|
||||
let ts_l2 = cut_params(&curve_l2, Handle::new(2), &geos);
|
||||
println!("angled ts_l2: {:?}", ts_l2);
|
||||
|
||||
// We expect l1 to have at least 2 cuts: the intersection with l2 at (0,0) and the tangent point with arc!
|
||||
assert!(ts_l1.len() >= 2, "Line 1 must have intersection and tangent cut: got {:?}", ts_l1);
|
||||
assert!(ts_l2.len() >= 2, "Line 2 must have intersection and tangent cut: got {:?}", ts_l2);
|
||||
|
||||
// Trimming l1 arm past the tangent point
|
||||
let click_l1 = 40.0 * dir1;
|
||||
let trim_l1 = pick_trim_at(&all, &geos, Handle::new(1), click_l1.x, click_l1.y);
|
||||
println!("angled trim_l1: {:?}", trim_l1);
|
||||
assert!(trim_l1.is_some(), "trimming line 1 must succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kernel_intersect_tangency() {
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
let line = Curve::Line(KernelLine { start: [-10.0, 0.0], end: [10.0, 0.0] });
|
||||
|
||||
// Case 1: Exact tangency at interior of arc
|
||||
let arc_exact = Curve::Arc(KernelArc {
|
||||
centre: [0.0, 5.0],
|
||||
radius: 5.0,
|
||||
start_angle: -FRAC_PI_2 - 0.2,
|
||||
end_angle: -FRAC_PI_2 + 0.2,
|
||||
});
|
||||
let hits_exact = kernel_intersect(&line, &arc_exact, KernelTolerance::new(CUT_TOLERANCE));
|
||||
println!("hits_exact: {:?}", hits_exact);
|
||||
|
||||
// Case 2: Distance slightly greater than R by 1e-12 (within tolerance 1e-7)
|
||||
let arc_eps = Curve::Arc(KernelArc {
|
||||
centre: [0.0, 5.0 + 1e-12],
|
||||
radius: 5.0,
|
||||
start_angle: -FRAC_PI_2 - 0.2,
|
||||
end_angle: -FRAC_PI_2 + 0.2,
|
||||
});
|
||||
let hits_eps = kernel_intersect(&line, &arc_eps, KernelTolerance::new(CUT_TOLERANCE));
|
||||
println!("hits_eps: {:?}", hits_eps);
|
||||
|
||||
// Case 3: Distance slightly greater than R by 1e-9
|
||||
let arc_eps9 = Curve::Arc(KernelArc {
|
||||
centre: [0.0, 5.0 + 1e-9],
|
||||
radius: 5.0,
|
||||
start_angle: -FRAC_PI_2 - 0.2,
|
||||
end_angle: -FRAC_PI_2 + 0.2,
|
||||
});
|
||||
let hits_eps9 = kernel_intersect(&line, &arc_eps9, KernelTolerance::new(CUT_TOLERANCE));
|
||||
println!("hits_eps9: {:?}", hits_eps9);
|
||||
|
||||
// Case 4: Tangency at arc start endpoint
|
||||
let arc_start = Curve::Arc(KernelArc {
|
||||
centre: [0.0, 5.0],
|
||||
radius: 5.0,
|
||||
start_angle: -FRAC_PI_2,
|
||||
end_angle: 0.0,
|
||||
});
|
||||
let hits_start = kernel_intersect(&line, &arc_start, KernelTolerance::new(CUT_TOLERANCE));
|
||||
println!("hits_start (-PI/2): {:?}", hits_start);
|
||||
|
||||
// Case 5: Tangency at arc start endpoint with normalized angle 3*PI/2
|
||||
let arc_norm = Curve::Arc(KernelArc {
|
||||
centre: [0.0, 5.0],
|
||||
radius: 5.0,
|
||||
start_angle: 3.0 * FRAC_PI_2,
|
||||
end_angle: 0.0, // wait! In CCW, from 3*PI/2 to 0 is span PI/2, or 2*PI?
|
||||
});
|
||||
let hits_norm = kernel_intersect(&line, &arc_norm, KernelTolerance::new(CUT_TOLERANCE));
|
||||
println!("hits_norm (3*PI/2): {:?}", hits_norm);
|
||||
|
||||
// Case 6: Tangency at arc end endpoint
|
||||
let arc_end = Curve::Arc(KernelArc {
|
||||
centre: [0.0, 5.0],
|
||||
radius: 5.0,
|
||||
start_angle: std::f64::consts::PI,
|
||||
end_angle: -FRAC_PI_2,
|
||||
});
|
||||
let hits_end = kernel_intersect(&line, &arc_end, KernelTolerance::new(CUT_TOLERANCE));
|
||||
println!("hits_end (-PI/2): {:?}", hits_end);
|
||||
|
||||
// Case 7: Large coordinates (e.g. 5000, 5005)
|
||||
let line_large = Curve::Line(KernelLine { start: [4990.0, 5000.0], end: [5010.0, 5000.0] });
|
||||
let arc_large = Curve::Arc(KernelArc {
|
||||
centre: [5000.0, 5005.0],
|
||||
radius: 5.0,
|
||||
start_angle: -FRAC_PI_2,
|
||||
end_angle: 0.0,
|
||||
});
|
||||
let hits_large = kernel_intersect(&line_large, &arc_large, KernelTolerance::new(CUT_TOLERANCE));
|
||||
println!("hits_large: {:?}", hits_large);
|
||||
|
||||
// Test with cut_params:
|
||||
let geos_large = vec![Geo::Arc {
|
||||
handle: Handle::new(2),
|
||||
cx: 5000.0,
|
||||
cy: 5005.0,
|
||||
r: 5.0,
|
||||
a0: -FRAC_PI_2,
|
||||
a1: 0.0,
|
||||
}];
|
||||
let cuts_large = cut_params(&line_large, Handle::new(1), &geos_large);
|
||||
let _ = cuts_large;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arc_tangent_line_precision_sweep() {
|
||||
use codec::entities::{Arc as CadArc, Line as LineEnt};
|
||||
use crate::command::{TangentObject, WorkingPlane};
|
||||
use crate::modules::draw::draw::arc::{solve_arc_3_inputs, ArcPick};
|
||||
|
||||
let mut max_dist = 0.0_f64;
|
||||
let mut failed_cuts = 0;
|
||||
|
||||
for i in 1..36 {
|
||||
let angle_deg = i as f64 * 5.0; // 5 to 175 degrees
|
||||
let a2 = angle_deg.to_radians();
|
||||
|
||||
let dir1 = DVec3::new(1.0, 0.0, 0.0);
|
||||
let dir2 = DVec3::new(a2.cos(), a2.sin(), 0.0);
|
||||
|
||||
let mut l1 = LineEnt::new();
|
||||
l1.common.handle = Handle::new(1);
|
||||
l1.start = Vector3::new(-50.0, 0.0, 0.0);
|
||||
l1.end = Vector3::new(150.0, 0.0, 0.0);
|
||||
|
||||
let mut l2 = LineEnt::new();
|
||||
l2.common.handle = Handle::new(2);
|
||||
l2.start = Vector3::new(-50.0 * dir2.x, -50.0 * dir2.y, 0.0);
|
||||
l2.end = Vector3::new(150.0 * dir2.x, 150.0 * dir2.y, 0.0);
|
||||
|
||||
let t1 = TangentObject::Line {
|
||||
p1: DVec3::new(l1.start.x, l1.start.y, 0.0),
|
||||
p2: DVec3::new(l1.end.x, l1.end.y, 0.0),
|
||||
};
|
||||
let t2 = TangentObject::Line {
|
||||
p1: DVec3::new(l2.start.x, l2.start.y, 0.0),
|
||||
p2: DVec3::new(l2.end.x, l2.end.y, 0.0),
|
||||
};
|
||||
|
||||
let bisector = (dir1 + dir2).normalize();
|
||||
let mid = bisector * 30.0;
|
||||
|
||||
let sol = solve_arc_3_inputs(
|
||||
ArcPick::Tangent { target: t1, hit: DVec3::new(50.0, 0.0, 0.0) },
|
||||
ArcPick::Point(mid),
|
||||
ArcPick::Tangent { target: t2, hit: dir2 * 50.0 },
|
||||
WorkingPlane::default(),
|
||||
);
|
||||
|
||||
if let Some((center, radius, sa, ea)) = sol {
|
||||
let mut arc = CadArc::new();
|
||||
arc.common.handle = Handle::new(3);
|
||||
arc.center = Vector3::new(center.x, center.y, center.z);
|
||||
arc.radius = radius;
|
||||
arc.start_angle = sa;
|
||||
arc.end_angle = ea;
|
||||
|
||||
let all = vec![
|
||||
EntityType::Line(l1.clone()),
|
||||
EntityType::Line(l2.clone()),
|
||||
EntityType::Arc(arc.clone()),
|
||||
];
|
||||
let geos = build_geos(&all);
|
||||
|
||||
let curve_l1 = entity_curve_xy(&EntityType::Line(l1.clone())).unwrap();
|
||||
let ts_l1 = cut_params(&curve_l1, Handle::new(1), &geos);
|
||||
|
||||
let curve_l2 = entity_curve_xy(&EntityType::Line(l2.clone())).unwrap();
|
||||
let ts_l2 = cut_params(&curve_l2, Handle::new(2), &geos);
|
||||
|
||||
if ts_l1.len() < 2 || ts_l2.len() < 2 {
|
||||
failed_cuts += 1;
|
||||
println!("FAILED at angle {angle_deg} deg: ts_l1={:?}, ts_l2={:?}", ts_l1, ts_l2);
|
||||
}
|
||||
|
||||
// Check distance of arc endpoints to line
|
||||
let ep1 = [center.x + radius * sa.cos(), center.y + radius * sa.sin()];
|
||||
let d1 = (ep1[1] - 0.0).abs(); // distance to l1 (y=0)
|
||||
if d1 > max_dist { max_dist = d1; }
|
||||
}
|
||||
}
|
||||
println!("Sweep results: failed_cuts={failed_cuts}, max_dist={max_dist:e}");
|
||||
assert_eq!(failed_cuts, 0, "No cuts should fail across angle sweep");
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in new issue
Block a user