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:
Hakan Seven committed 2026-10-11 23:42:48 +03:00
commit 3cc3ead3a6
3 files changed
+955 -19

No files matched your search

+351
View File
@@ -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");
}
}
// ══════════════════════════════════════════════════════════════════════════