forked from 1b-agent/opencadstudio-upstream
fix(boundary): clip rays/XLINEs to finite extents for face detection
Infinite construction lines tessellated to a single unit stub at the base point, which can never close a face (and overwrote the real wire segments), so XLINEs were silently ignored by BOUNDARY. Clip unbounded curves to the finite extents via Liang-Barsky; lines missing the geometry contribute nothing. Same shared helper fixes RAYs. Tests: - xline_splits_rectangle_into_two_faces: 10x10 rect split in two - ray_splits_rectangle_into_two_faces: upward ray splits in two - xline_missing_the_geometry_contributes_no_face: distant xline is a no-op
This commit is contained in:
1 parent
4432f633e7
commit
960d51d955
1 file changed
+202
-11
+202
-11
@@ -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<Line> = 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::<Vec<_>>(),
|
||||
Curve::XLine(line) => bounds
|
||||
.and_then(|(min, max)| {
|
||||
clip_unbounded_to_bounds(
|
||||
line.base,
|
||||
line.direction,
|
||||
false,
|
||||
min,
|
||||
max,
|
||||
tolerance,
|
||||
)
|
||||
})
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
_ => 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::<Vec<_>>()
|
||||
(start.iter().chain(&end).all(|value| value.is_finite())
|
||||
&& start != end)
|
||||
.then_some(Line { start, end })
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
})
|
||||
.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<Line> {
|
||||
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<f64>) {
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user