Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions crates/algo/src/builder/classify_2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ pub fn sample_interior_point(loop_pts: &[Point2]) -> Point2 {
let cy = loop_pts.iter().map(|p| p.y()).sum::<f64>() / n;
let centroid = Point2::new(cx, cy);

// 2. If centroid is inside, use it.
if point_in_polygon_2d(centroid, loop_pts) {
// 2. If centroid is inside, use it. A centroid that lands on the
// boundary itself (e.g. the reflex corner of an L-shaped loop) passes
// the even-odd test unpredictably and is not a safe interior sample.
if point_in_polygon_2d(centroid, loop_pts)
&& distance_to_polygon_boundary(centroid, loop_pts) > boundary_eps(loop_pts)
{
return centroid;
}

Expand Down Expand Up @@ -72,6 +76,40 @@ pub fn sample_interior_point(loop_pts: &[Point2]) -> Point2 {
centroid
}

fn boundary_eps(loop_pts: &[Point2]) -> f64 {
let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for p in loop_pts {
min_x = min_x.min(p.x());
min_y = min_y.min(p.y());
max_x = max_x.max(p.x());
max_y = max_y.max(p.y());
}
let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
(diag * 1e-6).max(1e-12)
}

fn distance_to_polygon_boundary(p: Point2, loop_pts: &[Point2]) -> f64 {
let mut best = f64::INFINITY;
let n = loop_pts.len();
for i in 0..n {
let a = loop_pts[i];
let b = loop_pts[(i + 1) % n];
let ab = Vec2::new(b.x() - a.x(), b.y() - a.y());
let ap = Vec2::new(p.x() - a.x(), p.y() - a.y());
let len_sq = ab.x() * ab.x() + ab.y() * ab.y();
let t = if len_sq < 1e-30 {
0.0
} else {
((ap.x() * ab.x() + ap.y() * ab.y()) / len_sq).clamp(0.0, 1.0)
};
let dx = p.x() - (a.x() + ab.x() * t);
let dy = p.y() - (a.y() + ab.y() * t);
best = best.min((dx * dx + dy * dy).sqrt());
}
best
}

/// Test whether a 2D point is inside a closed polygon using ray-casting
/// (even-odd rule).
pub fn point_in_polygon_2d(p: Point2, polygon: &[Point2]) -> bool {
Expand Down
56 changes: 50 additions & 6 deletions crates/algo/src/builder/same_domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,9 @@ fn planar_faces_overlap(topo: &Topology, sub_faces: &[SubFace], i: usize, j: usi
return false;
};

let wire_points = |face: &brepkit_topology::face::Face| -> Vec<brepkit_math::vec::Point3> {
let wire_points = |wire_id: brepkit_topology::wire::WireId| -> Vec<brepkit_math::vec::Point3> {
let mut pts = Vec::new();
let Ok(wire) = topo.wire(face.outer_wire()) else {
let Ok(wire) = topo.wire(wire_id) else {
return pts;
};
for oe in wire.edges() {
Expand All @@ -432,8 +432,8 @@ fn planar_faces_overlap(topo: &Topology, sub_faces: &[SubFace], i: usize, j: usi
pts
};

let pts_i = wire_points(face_i);
let pts_j = wire_points(face_j);
let pts_i = wire_points(face_i.outer_wire());
let pts_j = wire_points(face_j.outer_wire());
if pts_i.len() < 3 || pts_j.len() < 3 {
return false;
}
Expand All @@ -459,13 +459,57 @@ fn planar_faces_overlap(topo: &Topology, sub_faces: &[SubFace], i: usize, j: usi
.all(|&v| super::classify_2d::point_in_polygon_2d(v, poly))
};

// A point landing inside one of the container's inner wires sits in a
// hole, not on the face — e.g. a frame face whose hole exactly hosts
// the candidate. Containment through a hole is not overlap.
let in_hole = |p: brepkit_math::vec::Point2, face: &brepkit_topology::face::Face| -> bool {
face.inner_wires().iter().any(|&wid| {
let pts = wire_points(wid);
if pts.len() < 3 {
return false;
}
let poly: Vec<_> = pts.iter().map(|&q| frame.project(q)).collect();
super::classify_2d::point_in_polygon_2d(p, &poly)
})
Comment thread
greptile-apps[bot] marked this conversation as resolved.
};

// A single interior sample can miss the hole for a non-convex candidate
// straddling a hole boundary: the sample may land on solid material while
// the candidate's footprint actually sits entirely over the container's
// holes. As an additional (not replacement) suppressor, also reject when
// EVERY sampled point of the candidate that lies inside the container's
// outer boundary falls inside one of the container's holes. This keeps
// the common case (interior sample alone) identical and only fires extra
// for footprints fully over holes.
let footprint_in_holes = |sample: brepkit_math::vec::Point2,
verts: &[brepkit_math::vec::Point2],
outer: &[brepkit_math::vec::Point2],
face: &brepkit_topology::face::Face|
-> bool {
if face.inner_wires().is_empty() {
return false;
}
std::iter::once(sample)
.chain(verts.iter().copied())
.filter(|&p| super::classify_2d::point_in_polygon_2d(p, outer))
.all(|p| in_hole(p, face))
};

// i fully contained in j: every vertex of i (plus its interior sample)
// is inside j's polygon.
if super::classify_2d::point_in_polygon_2d(p_i_2d, &poly_j) && all_inside(&poly_i, &poly_j) {
if super::classify_2d::point_in_polygon_2d(p_i_2d, &poly_j)
&& all_inside(&poly_i, &poly_j)
&& !in_hole(p_i_2d, face_j)
&& !footprint_in_holes(p_i_2d, &poly_i, &poly_j, face_j)
{
return true;
}
// j fully contained in i.
if super::classify_2d::point_in_polygon_2d(p_j_2d, &poly_i) && all_inside(&poly_j, &poly_i) {
if super::classify_2d::point_in_polygon_2d(p_j_2d, &poly_i)
&& all_inside(&poly_j, &poly_i)
&& !in_hole(p_j_2d, face_i)
&& !footprint_in_holes(p_j_2d, &poly_j, &poly_i, face_i)
{
return true;
}
false
Expand Down
11 changes: 6 additions & 5 deletions crates/algo/src/classifier/ray_cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,18 +333,19 @@ mod tests {
let e37 = edge(3, 7);

let fwd = |eid| OrientedEdge::new(eid, true);
let rev = |eid| OrientedEdge::new(eid, false);
let w_bot =
topo.add_wire(Wire::new(vec![fwd(e01), fwd(e12), fwd(e23), fwd(e30)], true).unwrap());
topo.add_wire(Wire::new(vec![rev(e01), rev(e30), rev(e23), rev(e12)], true).unwrap());
let w_top =
topo.add_wire(Wire::new(vec![fwd(e45), fwd(e56), fwd(e67), fwd(e74)], true).unwrap());
let w_front =
topo.add_wire(Wire::new(vec![fwd(e01), fwd(e15), fwd(e45), fwd(e04)], true).unwrap());
topo.add_wire(Wire::new(vec![fwd(e01), fwd(e15), rev(e45), rev(e04)], true).unwrap());
let w_back =
topo.add_wire(Wire::new(vec![fwd(e23), fwd(e37), fwd(e67), fwd(e26)], true).unwrap());
topo.add_wire(Wire::new(vec![fwd(e23), fwd(e37), rev(e67), rev(e26)], true).unwrap());
let w_left =
topo.add_wire(Wire::new(vec![fwd(e30), fwd(e04), fwd(e74), fwd(e37)], true).unwrap());
topo.add_wire(Wire::new(vec![fwd(e30), fwd(e04), rev(e74), rev(e37)], true).unwrap());
let w_right =
topo.add_wire(Wire::new(vec![fwd(e12), fwd(e26), fwd(e56), fwd(e15)], true).unwrap());
topo.add_wire(Wire::new(vec![fwd(e12), fwd(e26), rev(e56), rev(e15)], true).unwrap());

let mk_face =
|w, n: Vec3, d: f64| Face::new(w, vec![], FaceSurface::Plane { normal: n, d });
Expand Down
11 changes: 6 additions & 5 deletions crates/algo/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,18 +201,19 @@ mod tests {
let e26 = edge(2, 6);
let e37 = edge(3, 7);
let fwd = |eid| OrientedEdge::new(eid, true);
let rev = |eid| OrientedEdge::new(eid, false);
let w_bot =
topo.add_wire(Wire::new(vec![fwd(e01), fwd(e12), fwd(e23), fwd(e30)], true).unwrap());
topo.add_wire(Wire::new(vec![rev(e01), rev(e30), rev(e23), rev(e12)], true).unwrap());
let w_top =
topo.add_wire(Wire::new(vec![fwd(e45), fwd(e56), fwd(e67), fwd(e74)], true).unwrap());
let w_front =
topo.add_wire(Wire::new(vec![fwd(e01), fwd(e15), fwd(e45), fwd(e04)], true).unwrap());
topo.add_wire(Wire::new(vec![fwd(e01), fwd(e15), rev(e45), rev(e04)], true).unwrap());
let w_back =
topo.add_wire(Wire::new(vec![fwd(e23), fwd(e37), fwd(e67), fwd(e26)], true).unwrap());
topo.add_wire(Wire::new(vec![fwd(e23), fwd(e37), rev(e67), rev(e26)], true).unwrap());
let w_left =
topo.add_wire(Wire::new(vec![fwd(e30), fwd(e04), fwd(e74), fwd(e37)], true).unwrap());
topo.add_wire(Wire::new(vec![fwd(e30), fwd(e04), rev(e74), rev(e37)], true).unwrap());
let w_right =
topo.add_wire(Wire::new(vec![fwd(e12), fwd(e26), fwd(e56), fwd(e15)], true).unwrap());
topo.add_wire(Wire::new(vec![fwd(e12), fwd(e26), rev(e56), rev(e15)], true).unwrap());
let f_bot = topo.add_face(Face::new(
w_bot,
vec![],
Expand Down
Loading
Loading