diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index af2553fad..fea24019d 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -895,6 +895,32 @@ impl Scene { .segments .extend(segments); } + // Finite extents of the wire geometry. Rays and infinite + // construction lines are clipped to this box below: tessellating + // them raw yields a single unit stub at the base point, which can + // never close a face (and would overwrite the wire segments with + // it). Expanded by the weld tolerance so clipped endpoints meet + // the segments that produced the box. + let mut bounds: Option<([f64; 2], [f64; 2])> = None; + for source in sources.values() { + for segment in &source.segments { + for point in [segment.start, segment.end] { + bounds = Some(match bounds { + None => ([point[0], point[1]], [point[0], point[1]]), + Some((min, max)) => ( + [min[0].min(point[0]), min[1].min(point[1])], + [max[0].max(point[0]), max[1].max(point[1])], + ), + }); + } + } + } + let bounds = bounds.map(|(min, max)| { + ( + [min[0] - tolerance, min[1] - tolerance], + [max[0] + tolerance, max[1] + tolerance], + ) + }); for (&handle, source) in &mut sources { let curves = self .document @@ -908,18 +934,46 @@ impl Scene { let live_segments: Vec = curves .iter() .flat_map(|curve| { - curve - .tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE) - .windows(2) - .filter_map(|pair| { - let start = pair[0]; - let end = pair[1]; + match curve { + Curve::Ray(ray) => bounds + .and_then(|(min, max)| { + clip_unbounded_to_bounds( + ray.origin, + ray.direction, + true, + min, + max, + tolerance, + ) + }) + .into_iter() + .collect::>(), + Curve::XLine(line) => bounds + .and_then(|(min, max)| { + clip_unbounded_to_bounds( + line.base, + line.direction, + false, + min, + max, + tolerance, + ) + }) + .into_iter() + .collect::>(), + _ => curve + .tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE) + .windows(2) + .filter_map(|pair| { + let start = pair[0]; + let end = pair[1]; - (start.iter().chain(&end).all(|value| value.is_finite()) - && start != end) - .then_some(Line { start, end }) - }) - .collect::>() + (start.iter().chain(&end).all(|value| value.is_finite()) + && start != end) + .then_some(Line { start, end }) + }) + .collect::>(), + } }) .collect(); @@ -942,6 +996,56 @@ impl Scene { } } +/// Clip an unbounded curve (ray when `forward_only`, infinite line +/// otherwise) to a finite bounding box, so it can participate in face +/// detection. Returns `None` when the direction is degenerate, when the +/// curve misses the box, or when the clipped piece is shorter than +/// `tolerance` and could never close a face on its own. +fn clip_unbounded_to_bounds( + base: [f64; 2], + direction: [f64; 2], + forward_only: bool, + min: [f64; 2], + max: [f64; 2], + tolerance: f64, +) -> Option { + let (dx, dy) = (direction[0], direction[1]); + if dx == 0.0 && dy == 0.0 { + return None; + } + // Liang-Barsky: narrow the parameter interval per axis. + let mut entered = f64::NEG_INFINITY; + let mut exited = f64::INFINITY; + for axis in 0..2 { + let (base, delta, low, high) = (base[axis], direction[axis], min[axis], max[axis]); + if delta.abs() <= f64::EPSILON { + if base < low || base > high { + return None; + } + } else { + let (mut near, mut far) = ((low - base) / delta, (high - base) / delta); + if near > far { + std::mem::swap(&mut near, &mut far); + } + entered = entered.max(near); + exited = exited.min(far); + if entered > exited { + return None; + } + } + } + if forward_only { + entered = entered.max(0.0); + } + if !(entered <= exited) || !entered.is_finite() || !exited.is_finite() { + return None; + } + let start = [base[0] + entered * dx, base[1] + entered * dy]; + let end = [base[0] + exited * dx, base[1] + exited * dy]; + let length = (end[0] - start[0]).hypot(end[1] - start[1]); + (length > tolerance).then_some(Line { start, end }) +} + fn hatch_path_geometry( path: &acadrust::entities::BoundaryPath, ) -> (Vec<[f64; 2]>, Vec) { @@ -1028,3 +1132,90 @@ pub(crate) fn boundary_faces( .collect(); bounded_faces(&segments, Tolerance::new(tolerance)) } + +#[cfg(test)] +mod boundary_xline_tests { + use super::*; + + fn rect_edge( + scene: &mut Scene, + start: [f64; 3], + end: [f64; 3], + ) -> acadrust::Handle { + scene.add_entity(EntityType::Line( + acadrust::entities::Line::from_points( + acadrust::types::Vector3::new(start[0], start[1], start[2]), + acadrust::types::Vector3::new(end[0], end[1], end[2]), + ), + )) + } + + #[test] + fn xline_splits_rectangle_into_two_faces() { + let mut scene = Scene::new(); + rect_edge(&mut scene, [0.0, 0.0, 0.0], [10.0, 0.0, 0.0]); + rect_edge(&mut scene, [10.0, 0.0, 0.0], [10.0, 10.0, 0.0]); + rect_edge(&mut scene, [10.0, 10.0, 0.0], [0.0, 10.0, 0.0]); + rect_edge(&mut scene, [0.0, 10.0, 0.0], [0.0, 0.0, 0.0]); + // Infinite vertical construction line through x=5. + scene.add_entity(EntityType::XLine(acadrust::entities::XLine::new( + acadrust::types::Vector3::new(5.0, 0.0, 0.0), + acadrust::types::Vector3::new(0.0, 1.0, 0.0), + ))); + + let sources = + scene.boundary_sources_on_plane(crate::command::WorkingPlane::default(), 1.0e-6); + let faces = boundary_faces(&sources, 1.0e-6); + assert_eq!( + faces.len(), + 2, + "vertical xline must split the 10x10 rectangle into two faces" + ); + } + + #[test] + fn ray_splits_rectangle_into_two_faces() { + let mut scene = Scene::new(); + rect_edge(&mut scene, [0.0, 0.0, 0.0], [10.0, 0.0, 0.0]); + rect_edge(&mut scene, [10.0, 0.0, 0.0], [10.0, 10.0, 0.0]); + rect_edge(&mut scene, [10.0, 10.0, 0.0], [0.0, 10.0, 0.0]); + rect_edge(&mut scene, [0.0, 10.0, 0.0], [0.0, 0.0, 0.0]); + // Ray starting below the rectangle, pointing up through x=5. + scene.add_entity(EntityType::Ray(acadrust::entities::Ray::new( + acadrust::types::Vector3::new(5.0, -5.0, 0.0), + acadrust::types::Vector3::new(0.0, 1.0, 0.0), + ))); + + let sources = + scene.boundary_sources_on_plane(crate::command::WorkingPlane::default(), 1.0e-6); + let faces = boundary_faces(&sources, 1.0e-6); + assert_eq!( + faces.len(), + 2, + "upward ray must split the 10x10 rectangle into two faces" + ); + } + + #[test] + fn xline_missing_the_geometry_contributes_no_face() { + let mut scene = Scene::new(); + rect_edge(&mut scene, [0.0, 0.0, 0.0], [10.0, 0.0, 0.0]); + rect_edge(&mut scene, [10.0, 0.0, 0.0], [10.0, 10.0, 0.0]); + rect_edge(&mut scene, [10.0, 10.0, 0.0], [0.0, 10.0, 0.0]); + rect_edge(&mut scene, [0.0, 10.0, 0.0], [0.0, 0.0, 0.0]); + // Far-away vertical construction line: never touches the rectangle. + scene.add_entity(EntityType::XLine(acadrust::entities::XLine::new( + acadrust::types::Vector3::new(50.0, 0.0, 0.0), + acadrust::types::Vector3::new(0.0, 1.0, 0.0), + ))); + + let sources = + scene.boundary_sources_on_plane(crate::command::WorkingPlane::default(), 1.0e-6); + let faces = boundary_faces(&sources, 1.0e-6); + assert_eq!( + faces.len(), + 1, + "a distant xline must leave the single rectangle face alone" + ); + } +}