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
@@ -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