Skip to main content

oxiphysics_geometry/boolean_ops/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use oxiphysics_core::math::Vec3;
6
7use super::types::{BspPlane, ManifoldCheckResult};
8
9#[inline]
10pub(super) fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
11    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
12}
13#[inline]
14pub(super) fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
15    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
16}
17#[inline]
18pub(super) fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
19    [a[0] * s, a[1] * s, a[2] * s]
20}
21#[inline]
22pub(super) fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
23    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
24}
25#[inline]
26pub(super) fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
27    [
28        a[1] * b[2] - a[2] * b[1],
29        a[2] * b[0] - a[0] * b[2],
30        a[0] * b[1] - a[1] * b[0],
31    ]
32}
33#[inline]
34pub(super) fn len3(a: [f64; 3]) -> f64 {
35    dot3(a, a).sqrt()
36}
37#[inline]
38pub(super) fn normalize3(a: [f64; 3]) -> [f64; 3] {
39    let l = len3(a);
40    if l < f64::EPSILON {
41        [0.0, 0.0, 0.0]
42    } else {
43        [a[0] / l, a[1] / l, a[2] / l]
44    }
45}
46pub(super) fn vec3_to_arr(v: Vec3) -> [f64; 3] {
47    [v.x, v.y, v.z]
48}
49pub(super) fn arr_to_vec3(a: [f64; 3]) -> Vec3 {
50    Vec3::new(a[0], a[1], a[2])
51}
52/// Test whether a point is inside a closed mesh using multiple ray casts.
53///
54/// Fires rays in three axis directions (+X, +Y, +Z) and takes a majority vote
55/// to improve robustness against degenerate cases where a ray passes exactly
56/// through an edge or vertex. An odd intersection count means the point is
57/// inside the mesh (Jordan curve theorem generalised to 3-D).
58pub fn point_inside_mesh(point: [f64; 3], verts: &[[f64; 3]], tris: &[[usize; 3]]) -> bool {
59    let ray_dirs: [[f64; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
60    let mut votes_inside = 0usize;
61    for ray_dir in &ray_dirs {
62        let mut count = 0usize;
63        for tri in tris {
64            let v0 = verts[tri[0]];
65            let v1 = verts[tri[1]];
66            let v2 = verts[tri[2]];
67            if ray_triangle_intersect(point, *ray_dir, v0, v1, v2).is_some() {
68                count += 1;
69            }
70        }
71        if count % 2 == 1 {
72            votes_inside += 1;
73        }
74    }
75    votes_inside >= 2
76}
77/// Möller-Trumbore ray-triangle intersection.
78///
79/// Returns the parametric distance `t` along the ray if the ray intersects
80/// the triangle from the front, or `None` otherwise.
81pub fn ray_triangle_intersect(
82    origin: [f64; 3],
83    direction: [f64; 3],
84    v0: [f64; 3],
85    v1: [f64; 3],
86    v2: [f64; 3],
87) -> Option<f64> {
88    let e1 = sub3(v1, v0);
89    let e2 = sub3(v2, v0);
90    let h = cross3(direction, e2);
91    let a = dot3(e1, h);
92    if a.abs() < 1e-10 {
93        return None;
94    }
95    let f = 1.0 / a;
96    let s = sub3(origin, v0);
97    let u = f * dot3(s, h);
98    if !(0.0..=1.0).contains(&u) {
99        return None;
100    }
101    let q = cross3(s, e1);
102    let v = f * dot3(direction, q);
103    if v < 0.0 || u + v > 1.0 {
104        return None;
105    }
106    let t = f * dot3(e2, q);
107    if t > 1e-10 { Some(t) } else { None }
108}
109/// Test whether two triangles in 3-D intersect.
110///
111/// Uses the Devillers-Guigue algorithm (interval overlap on the line of
112/// intersection of the two triangle planes). Returns `true` if the triangles
113/// intersect (including edge/vertex contacts).
114pub fn triangles_intersect(
115    a0: [f64; 3],
116    a1: [f64; 3],
117    a2: [f64; 3],
118    b0: [f64; 3],
119    b1: [f64; 3],
120    b2: [f64; 3],
121) -> bool {
122    let nb = cross3(sub3(b1, b0), sub3(b2, b0));
123    let db = dot3(nb, b0);
124    let da0 = dot3(nb, a0) - db;
125    let da1 = dot3(nb, a1) - db;
126    let da2 = dot3(nb, a2) - db;
127    if da0 * da1 > 0.0 && da1 * da2 > 0.0 {
128        return false;
129    }
130    let na = cross3(sub3(a1, a0), sub3(a2, a0));
131    let da = dot3(na, a0);
132    let db0 = dot3(na, b0) - da;
133    let db1 = dot3(na, b1) - da;
134    let db2 = dot3(na, b2) - da;
135    if db0 * db1 > 0.0 && db1 * db2 > 0.0 {
136        return false;
137    }
138    let d = cross3(na, nb);
139    let d_len = len3(d);
140    if d_len < 1e-12 {
141        return coplanar_tri_overlap(a0, a1, a2, b0, b1, b2, na);
142    }
143    let proj_a = [dot3(d, a0), dot3(d, a1), dot3(d, a2)];
144    let proj_b = [dot3(d, b0), dot3(d, b1), dot3(d, b2)];
145    let interval_a = compute_interval(proj_a, [da0, da1, da2]);
146    let interval_b = compute_interval(proj_b, [db0, db1, db2]);
147    intervals_overlap(interval_a, interval_b)
148}
149/// Compute the intersection interval of a triangle on the intersection line.
150///
151/// Uses the signed distances `dists` and projections `proj` to compute
152/// `[t_min, t_max]` via linear interpolation.
153pub(super) fn compute_interval(proj: [f64; 3], dists: [f64; 3]) -> [f64; 2] {
154    let signs: [bool; 3] = [dists[0] > 0.0, dists[1] > 0.0, dists[2] > 0.0];
155    let (lone, pair): (usize, [usize; 2]) = if signs[0] != signs[1] && signs[0] != signs[2] {
156        (0, [1, 2])
157    } else if signs[1] != signs[0] && signs[1] != signs[2] {
158        (1, [0, 2])
159    } else {
160        (2, [0, 1])
161    };
162    let t0 = proj[pair[0]]
163        + (proj[lone] - proj[pair[0]]) * dists[pair[0]] / (dists[pair[0]] - dists[lone]);
164    let t1 = proj[pair[1]]
165        + (proj[lone] - proj[pair[1]]) * dists[pair[1]] / (dists[pair[1]] - dists[lone]);
166    [t0.min(t1), t0.max(t1)]
167}
168pub(super) fn intervals_overlap(a: [f64; 2], b: [f64; 2]) -> bool {
169    !(a[1] < b[0] || b[1] < a[0])
170}
171/// Coplanar triangle overlap test (2-D edge separating axis test).
172pub(super) fn coplanar_tri_overlap(
173    a0: [f64; 3],
174    a1: [f64; 3],
175    a2: [f64; 3],
176    b0: [f64; 3],
177    b1: [f64; 3],
178    b2: [f64; 3],
179    normal: [f64; 3],
180) -> bool {
181    let tri_a = [a0, a1, a2];
182    let tri_b = [b0, b1, b2];
183    for tri in [tri_a, tri_b].iter() {
184        for k in 0..3 {
185            let edge = sub3(tri[(k + 1) % 3], tri[k]);
186            let axis = cross3(normal, edge);
187            let proj_a: [f64; 3] = [
188                dot3(axis, tri_a[0]),
189                dot3(axis, tri_a[1]),
190                dot3(axis, tri_a[2]),
191            ];
192            let proj_b: [f64; 3] = [
193                dot3(axis, tri_b[0]),
194                dot3(axis, tri_b[1]),
195                dot3(axis, tri_b[2]),
196            ];
197            let a_min = proj_a.iter().cloned().fold(f64::INFINITY, f64::min);
198            let a_max = proj_a.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
199            let b_min = proj_b.iter().cloned().fold(f64::INFINITY, f64::min);
200            let b_max = proj_b.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
201            if a_max < b_min - 1e-10 || b_max < a_min - 1e-10 {
202                return false;
203            }
204        }
205    }
206    true
207}
208/// Compute the centroid of a triangle.
209pub fn triangle_centroid(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
210    scale3(add3(add3(v0, v1), v2), 1.0 / 3.0)
211}
212/// Compute the area of a triangle.
213pub fn triangle_area(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> f64 {
214    len3(cross3(sub3(v1, v0), sub3(v2, v0))) * 0.5
215}
216/// Compute the normal of a triangle (not normalised).
217pub fn triangle_normal(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
218    cross3(sub3(v1, v0), sub3(v2, v0))
219}
220/// Compute the unit normal of a triangle.
221pub fn triangle_unit_normal(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
222    normalize3(triangle_normal(v0, v1, v2))
223}
224/// Clip a convex polygon against a half-space defined by `plane`.
225///
226/// Vertices on the front side (dist ≥ 0) are kept; edges crossing the plane
227/// produce intersection vertices.  Returns the clipped polygon or an empty
228/// Vec if everything is clipped.
229pub fn clip_polygon_by_plane(polygon: &[[f64; 3]], plane: &BspPlane) -> Vec<[f64; 3]> {
230    if polygon.is_empty() {
231        return Vec::new();
232    }
233    let n = polygon.len();
234    let mut result = Vec::new();
235    for i in 0..n {
236        let a = polygon[i];
237        let b = polygon[(i + 1) % n];
238        let da = plane.signed_dist(a);
239        let db = plane.signed_dist(b);
240        if da >= -1e-10 {
241            result.push(a);
242        }
243        if (da > 1e-10 && db < -1e-10) || (da < -1e-10 && db > 1e-10) {
244            let t = da / (da - db);
245            let inter = [
246                a[0] + t * (b[0] - a[0]),
247                a[1] + t * (b[1] - a[1]),
248                a[2] + t * (b[2] - a[2]),
249            ];
250            result.push(inter);
251        }
252    }
253    result
254}
255/// Clip a polygon against all six half-spaces of an AABB.
256///
257/// Returns the clipped polygon or `None` if completely clipped.
258pub fn clip_polygon_by_aabb(
259    polygon: &[[f64; 3]],
260    aabb_min: [f64; 3],
261    aabb_max: [f64; 3],
262) -> Option<Vec<[f64; 3]>> {
263    let planes = [
264        BspPlane::new([1.0, 0.0, 0.0], aabb_min[0]),
265        BspPlane::new([-1.0, 0.0, 0.0], -aabb_max[0]),
266        BspPlane::new([0.0, 1.0, 0.0], aabb_min[1]),
267        BspPlane::new([0.0, -1.0, 0.0], -aabb_max[1]),
268        BspPlane::new([0.0, 0.0, 1.0], aabb_min[2]),
269        BspPlane::new([0.0, 0.0, -1.0], -aabb_max[2]),
270    ];
271    let mut poly = polygon.to_vec();
272    for plane in &planes {
273        poly = clip_polygon_by_plane(&poly, plane);
274        if poly.is_empty() {
275            return None;
276        }
277    }
278    Some(poly)
279}
280/// Clip a 2-D polygon (list of `[x, y]`) against a convex polygon (clipper).
281///
282/// Uses the Sutherland-Hodgman algorithm.
283pub fn clip_polygon_2d(subject: &[[f64; 2]], clip: &[[f64; 2]]) -> Vec<[f64; 2]> {
284    if subject.is_empty() || clip.is_empty() {
285        return Vec::new();
286    }
287    let mut output = subject.to_vec();
288    let n_clip = clip.len();
289    for i in 0..n_clip {
290        if output.is_empty() {
291            return Vec::new();
292        }
293        let input = output.clone();
294        output.clear();
295        let edge_a = clip[i];
296        let edge_b = clip[(i + 1) % n_clip];
297        let n_in = input.len();
298        for j in 0..n_in {
299            let current = input[j];
300            let prev = input[(j + n_in - 1) % n_in];
301            if inside_2d(current, edge_a, edge_b) {
302                if !inside_2d(prev, edge_a, edge_b) {
303                    output.push(intersect_2d(prev, current, edge_a, edge_b));
304                }
305                output.push(current);
306            } else if inside_2d(prev, edge_a, edge_b) {
307                output.push(intersect_2d(prev, current, edge_a, edge_b));
308            }
309        }
310    }
311    output
312}
313/// Test whether `point` is on the left (inside) of the directed edge `a → b`.
314pub(super) fn inside_2d(point: [f64; 2], a: [f64; 2], b: [f64; 2]) -> bool {
315    let edge = [b[0] - a[0], b[1] - a[1]];
316    let to_pt = [point[0] - a[0], point[1] - a[1]];
317    edge[0] * to_pt[1] - edge[1] * to_pt[0] >= 0.0
318}
319/// Compute intersection of segment `p1→p2` with segment `p3→p4`.
320pub(super) fn intersect_2d(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2], p4: [f64; 2]) -> [f64; 2] {
321    let d1 = [p2[0] - p1[0], p2[1] - p1[1]];
322    let d2 = [p4[0] - p3[0], p4[1] - p3[1]];
323    let cross = d1[0] * d2[1] - d1[1] * d2[0];
324    if cross.abs() < 1e-12 {
325        return p1;
326    }
327    let t = ((p3[0] - p1[0]) * d2[1] - (p3[1] - p1[1]) * d2[0]) / cross;
328    [p1[0] + t * d1[0], p1[1] + t * d1[1]]
329}
330/// Repair the winding order of a triangle mesh so that all outward-facing
331/// normals point away from the centroid of the mesh.
332///
333/// Computes the centroid of all vertices, then for each triangle checks
334/// whether the face normal points toward or away from the centroid.  If it
335/// points toward the centroid (inside), the triangle winding is flipped.
336pub fn repair_winding(verts: &[[f64; 3]], tris: &[[usize; 3]]) -> Vec<[usize; 3]> {
337    let n = verts.len() as f64;
338    let centroid = verts.iter().fold([0.0f64; 3], |acc, v| add3(acc, *v));
339    let centroid = scale3(centroid, 1.0 / n.max(1.0));
340    let mut fixed = Vec::with_capacity(tris.len());
341    for tri in tris {
342        let v0 = verts[tri[0]];
343        let v1 = verts[tri[1]];
344        let v2 = verts[tri[2]];
345        let face_normal = cross3(sub3(v1, v0), sub3(v2, v0));
346        let to_centroid = sub3(centroid, scale3(add3(add3(v0, v1), v2), 1.0 / 3.0));
347        if dot3(face_normal, to_centroid) > 0.0 {
348            fixed.push([tri[0], tri[2], tri[1]]);
349        } else {
350            fixed.push(*tri);
351        }
352    }
353    fixed
354}
355/// Check whether all triangles in a mesh have consistent outward normals
356/// (pointing away from the mesh centroid).
357///
358/// Returns the fraction of triangles (0.0–1.0) with correct outward winding.
359pub fn winding_consistency_score(verts: &[[f64; 3]], tris: &[[usize; 3]]) -> f64 {
360    if tris.is_empty() {
361        return 1.0;
362    }
363    let n = verts.len() as f64;
364    let centroid = verts.iter().fold([0.0f64; 3], |acc, v| add3(acc, *v));
365    let centroid = scale3(centroid, 1.0 / n.max(1.0));
366    let correct = tris
367        .iter()
368        .filter(|tri| {
369            let v0 = verts[tri[0]];
370            let v1 = verts[tri[1]];
371            let v2 = verts[tri[2]];
372            let face_normal = cross3(sub3(v1, v0), sub3(v2, v0));
373            let to_centroid = sub3(centroid, scale3(add3(add3(v0, v1), v2), 1.0 / 3.0));
374            dot3(face_normal, to_centroid) <= 0.0
375        })
376        .count();
377    correct as f64 / tris.len() as f64
378}
379/// Compute the 2-D intersection of two convex polygons using Sutherland-Hodgman.
380///
381/// Both polygons are assumed to be convex and wound counter-clockwise.
382pub fn polygon2d_intersection(a: &[[f64; 2]], b: &[[f64; 2]]) -> Vec<[f64; 2]> {
383    clip_polygon_2d(a, b)
384}
385/// Compute the 2-D union of two convex polygons.
386///
387/// Returns the merged boundary.  Uses the Sutherland-Hodgman approach:
388/// clip b against a, then add vertices of a that lie outside b,
389/// and vertices of b that lie outside a.
390pub fn polygon2d_union(a: &[[f64; 2]], b: &[[f64; 2]]) -> Vec<[f64; 2]> {
391    let mut pts: Vec<[f64; 2]> = Vec::new();
392    pts.extend_from_slice(a);
393    pts.extend_from_slice(b);
394    convex_hull_2d(&pts)
395}
396/// Compute the 2-D difference `a \ b` for convex polygons.
397///
398/// Returns the portion of `a` outside `b`.
399/// Inverts b's winding so we clip a against the outside of b.
400pub fn polygon2d_difference(a: &[[f64; 2]], b: &[[f64; 2]]) -> Vec<[f64; 2]> {
401    if a.is_empty() || b.is_empty() {
402        return a.to_vec();
403    }
404    let mut result = a.to_vec();
405    let n = b.len();
406    for i in 0..n {
407        if result.is_empty() {
408            break;
409        }
410        let edge_a = b[i];
411        let edge_b = b[(i + 1) % n];
412        let input = result.clone();
413        result.clear();
414        let ni = input.len();
415        for j in 0..ni {
416            let current = input[j];
417            let prev = input[(j + ni - 1) % ni];
418            let cur_out = !inside_2d(current, edge_a, edge_b);
419            let prev_out = !inside_2d(prev, edge_a, edge_b);
420            if cur_out {
421                if !prev_out {
422                    result.push(intersect_2d(prev, current, edge_a, edge_b));
423                }
424                result.push(current);
425            } else if prev_out {
426                result.push(intersect_2d(prev, current, edge_a, edge_b));
427            }
428        }
429    }
430    result
431}
432/// Compute a convex hull of 2-D points using the gift-wrapping algorithm.
433#[allow(dead_code)]
434pub(super) fn convex_hull_2d(pts: &[[f64; 2]]) -> Vec<[f64; 2]> {
435    if pts.len() < 3 {
436        return pts.to_vec();
437    }
438    let start = pts
439        .iter()
440        .enumerate()
441        .min_by(|(_, a), (_, b)| a[0].partial_cmp(&b[0]).unwrap_or(std::cmp::Ordering::Equal))
442        .map(|(i, _)| i)
443        .unwrap_or(0);
444    let mut hull = Vec::new();
445    let mut current = start;
446    loop {
447        hull.push(pts[current]);
448        let mut next = 0;
449        for i in 1..pts.len() {
450            if i == current {
451                continue;
452            }
453            let a = pts[current];
454            let b = pts[next];
455            let c = pts[i];
456            let cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
457            if cross < 0.0 || (cross == 0.0 && dist_sq_2d(a, c) > dist_sq_2d(a, b)) {
458                next = i;
459            }
460        }
461        current = next;
462        if current == start {
463            break;
464        }
465        if hull.len() > pts.len() {
466            break;
467        }
468    }
469    hull
470}
471pub(super) fn dist_sq_2d(a: [f64; 2], b: [f64; 2]) -> f64 {
472    (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)
473}
474/// Check whether a mesh is 2-manifold.
475#[allow(dead_code)]
476pub fn check_manifold(tris: &[[usize; 3]]) -> ManifoldCheckResult {
477    let mut edge_count: std::collections::HashMap<(usize, usize), usize> =
478        std::collections::HashMap::new();
479    for tri in tris {
480        let edges = [
481            (tri[0].min(tri[1]), tri[0].max(tri[1])),
482            (tri[1].min(tri[2]), tri[1].max(tri[2])),
483            (tri[0].min(tri[2]), tri[0].max(tri[2])),
484        ];
485        for e in &edges {
486            *edge_count.entry(*e).or_insert(0) += 1;
487        }
488    }
489    let mut n_boundary = 0;
490    let mut n_non_manifold = 0;
491    for &count in edge_count.values() {
492        if count == 1 {
493            n_boundary += 1;
494        }
495        if count > 2 {
496            n_non_manifold += 1;
497        }
498    }
499    ManifoldCheckResult {
500        is_manifold: n_boundary == 0 && n_non_manifold == 0,
501        n_boundary_edges: n_boundary,
502        n_non_manifold_edges: n_non_manifold,
503    }
504}
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use crate::TriangleMesh;
509    use crate::boolean_ops::BooleanOp;
510
511    use crate::boolean_ops::BspPlane;
512    use crate::boolean_ops::CsgMaterial;
513    use crate::boolean_ops::CsgWithMaterials;
514
515    use crate::boolean_ops::ManifoldBoolean;
516    use crate::boolean_ops::MeshBoolean;
517
518    use crate::boolean_ops::RobustMeshBoolean;
519
520    #[test]
521    fn test_intersecting_triangles() {
522        let a0 = [-1.0_f64, 0.0, 0.0];
523        let a1 = [1.0, 0.0, 0.0];
524        let a2 = [0.0, 1.0, 0.0];
525        let b0 = [0.0, 0.5, -1.0];
526        let b1 = [0.0, 0.5, 1.0];
527        let b2 = [0.0, -0.5, 0.0];
528        assert!(
529            triangles_intersect(a0, a1, a2, b0, b1, b2),
530            "crossing triangles should intersect"
531        );
532    }
533    #[test]
534    fn test_non_intersecting_triangles() {
535        let a0 = [0.0_f64, 0.0, 0.0];
536        let a1 = [1.0, 0.0, 0.0];
537        let a2 = [0.0, 1.0, 0.0];
538        let b0 = [5.0, 0.0, 0.0];
539        let b1 = [6.0, 0.0, 0.0];
540        let b2 = [5.0, 1.0, 0.0];
541        assert!(
542            !triangles_intersect(a0, a1, a2, b0, b1, b2),
543            "separated triangles should not intersect"
544        );
545    }
546    #[test]
547    fn test_ray_triangle_hit() {
548        let origin = [-5.0_f64, 0.0, 0.0];
549        let dir = [1.0, 0.0, 0.0];
550        let v0 = [0.0, -1.0, -1.0];
551        let v1 = [0.0, 1.0, -1.0];
552        let v2 = [0.0, 0.0, 1.0];
553        let t = ray_triangle_intersect(origin, dir, v0, v1, v2);
554        assert!(t.is_some(), "ray should hit the triangle");
555        assert!((t.unwrap() - 5.0).abs() < 1e-10, "t should be 5.0");
556    }
557    #[test]
558    fn test_ray_triangle_miss() {
559        let origin = [-5.0_f64, 10.0, 0.0];
560        let dir = [1.0, 0.0, 0.0];
561        let v0 = [0.0, -1.0, -1.0];
562        let v1 = [0.0, 1.0, -1.0];
563        let v2 = [0.0, 0.0, 1.0];
564        let t = ray_triangle_intersect(origin, dir, v0, v1, v2);
565        assert!(t.is_none(), "ray should miss the triangle");
566    }
567    fn unit_box_mesh() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
568        let v: Vec<[f64; 3]> = vec![
569            [-1.0, -1.0, -1.0],
570            [1.0, -1.0, -1.0],
571            [1.0, 1.0, -1.0],
572            [-1.0, 1.0, -1.0],
573            [-1.0, -1.0, 1.0],
574            [1.0, -1.0, 1.0],
575            [1.0, 1.0, 1.0],
576            [-1.0, 1.0, 1.0],
577        ];
578        let t: Vec<[usize; 3]> = vec![
579            [0, 2, 1],
580            [0, 3, 2],
581            [4, 5, 6],
582            [4, 6, 7],
583            [0, 1, 5],
584            [0, 5, 4],
585            [3, 6, 2],
586            [3, 7, 6],
587            [0, 4, 7],
588            [0, 7, 3],
589            [1, 2, 6],
590            [1, 6, 5],
591        ];
592        (v, t)
593    }
594    #[test]
595    fn test_point_inside_mesh() {
596        let (v, t) = unit_box_mesh();
597        assert!(
598            point_inside_mesh([0.1, 0.2, 0.3], &v, &t),
599            "interior point should be inside the unit box"
600        );
601    }
602    #[test]
603    fn test_point_outside_mesh() {
604        let (v, t) = unit_box_mesh();
605        assert!(
606            !point_inside_mesh([5.0, 0.0, 0.0], &v, &t),
607            "far point should be outside the unit box"
608        );
609    }
610    fn box_mesh_trimesh(min: f64, max: f64) -> TriangleMesh {
611        let v: Vec<Vec3> = vec![
612            Vec3::new(min, min, min),
613            Vec3::new(max, min, min),
614            Vec3::new(max, max, min),
615            Vec3::new(min, max, min),
616            Vec3::new(min, min, max),
617            Vec3::new(max, min, max),
618            Vec3::new(max, max, max),
619            Vec3::new(min, max, max),
620        ];
621        let t: Vec<[usize; 3]> = vec![
622            [0, 2, 1],
623            [0, 3, 2],
624            [4, 5, 6],
625            [4, 6, 7],
626            [0, 1, 5],
627            [0, 5, 4],
628            [3, 6, 2],
629            [3, 7, 6],
630            [0, 4, 7],
631            [0, 7, 3],
632            [1, 2, 6],
633            [1, 6, 5],
634        ];
635        TriangleMesh::new(v, t)
636    }
637    #[test]
638    fn test_boolean_union_non_overlapping() {
639        let a = box_mesh_trimesh(0.0, 1.0);
640        let b = box_mesh_trimesh(5.0, 6.0);
641        let result = MeshBoolean::execute(BooleanOp::Union, &a, &b).unwrap();
642        assert!(
643            !result.indices.is_empty(),
644            "union of non-overlapping meshes should have faces"
645        );
646    }
647    #[test]
648    fn test_boolean_difference_non_overlapping() {
649        let a = box_mesh_trimesh(0.0, 1.0);
650        let b = box_mesh_trimesh(5.0, 6.0);
651        let result = MeshBoolean::execute(BooleanOp::Difference, &a, &b).unwrap();
652        assert!(
653            !result.indices.is_empty(),
654            "A - B with no overlap should return A's faces"
655        );
656    }
657    #[test]
658    fn test_boolean_intersection_non_overlapping() {
659        let a = box_mesh_trimesh(0.0, 1.0);
660        let b = box_mesh_trimesh(5.0, 6.0);
661        let result = MeshBoolean::execute(BooleanOp::Intersection, &a, &b).unwrap();
662        assert!(
663            result.indices.is_empty(),
664            "intersection of non-overlapping meshes should be empty"
665        );
666    }
667    #[test]
668    fn test_boolean_error_on_empty_mesh() {
669        let empty = TriangleMesh::new(vec![], vec![]);
670        let b = box_mesh_trimesh(0.0, 1.0);
671        let result = MeshBoolean::execute(BooleanOp::Union, &empty, &b);
672        assert!(result.is_err(), "empty mesh should return an error");
673    }
674    #[test]
675    fn test_triangle_centroid() {
676        let c = triangle_centroid([0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]);
677        assert!((c[0] - 1.0).abs() < 1e-10);
678        assert!((c[1] - 1.0).abs() < 1e-10);
679        assert!(c[2].abs() < 1e-10);
680    }
681    #[test]
682    fn test_triangle_area() {
683        let area = triangle_area([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]);
684        assert!((area - 2.0).abs() < 1e-10, "area={area}");
685    }
686    #[test]
687    fn test_coplanar_overlap() {
688        let a0 = [0.0_f64, 0.0, 0.0];
689        let a1 = [2.0, 0.0, 0.0];
690        let a2 = [0.0, 2.0, 0.0];
691        let b0 = [1.0, 0.0, 0.0];
692        let b1 = [3.0, 0.0, 0.0];
693        let b2 = [1.0, 2.0, 0.0];
694        assert!(
695            triangles_intersect(a0, a1, a2, b0, b1, b2),
696            "overlapping coplanar triangles should intersect"
697        );
698    }
699    #[test]
700    fn test_bsp_plane_signed_dist() {
701        let plane = BspPlane::new([0.0, 1.0, 0.0], 0.0);
702        assert!(plane.signed_dist([0.0, 1.0, 0.0]) > 0.0);
703        assert!(plane.signed_dist([0.0, -1.0, 0.0]) < 0.0);
704        assert!(plane.signed_dist([0.0, 0.0, 0.0]).abs() < 1e-10);
705    }
706    #[test]
707    fn test_clip_polygon_by_plane_all_outside() {
708        let poly = vec![[0.0_f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.0, 1.0]];
709        let plane = BspPlane::new([0.0, 1.0, 0.0], 1.0);
710        let clipped = clip_polygon_by_plane(&poly, &plane);
711        assert!(clipped.is_empty(), "all below plane should be clipped");
712    }
713    #[test]
714    fn test_clip_polygon_by_plane_all_inside() {
715        let poly = vec![[0.0_f64, 1.0, 0.0], [1.0, 1.0, 0.0], [0.5, 2.0, 1.0]];
716        let plane = BspPlane::new([0.0, 1.0, 0.0], 0.0);
717        let clipped = clip_polygon_by_plane(&poly, &plane);
718        assert_eq!(clipped.len(), 3, "unchanged polygon should have 3 verts");
719    }
720    #[test]
721    fn test_clip_polygon_by_plane_partial_clip() {
722        let poly = vec![
723            [-1.0_f64, 1.0, 0.0],
724            [1.0, 1.0, 0.0],
725            [1.0, -1.0, 0.0],
726            [-1.0, -1.0, 0.0],
727        ];
728        let plane = BspPlane::new([0.0, 1.0, 0.0], 0.0);
729        let clipped = clip_polygon_by_plane(&poly, &plane);
730        assert!(!clipped.is_empty(), "partial overlap should produce result");
731        for v in &clipped {
732            assert!(v[1] >= -1e-9, "clipped vertex y={}", v[1]);
733        }
734    }
735    #[test]
736    fn test_clip_polygon_by_aabb_inside() {
737        let poly = vec![[0.1_f64, 0.1, 0.1], [0.9, 0.1, 0.1], [0.5, 0.9, 0.5]];
738        let result = clip_polygon_by_aabb(&poly, [0.0; 3], [1.0; 3]);
739        assert!(result.is_some(), "polygon inside AABB should survive");
740        assert_eq!(result.unwrap().len(), 3);
741    }
742    #[test]
743    fn test_clip_polygon_by_aabb_outside() {
744        let poly = vec![[5.0_f64, 5.0, 5.0], [6.0, 5.0, 5.0], [5.5, 6.0, 5.5]];
745        let result = clip_polygon_by_aabb(&poly, [0.0; 3], [1.0; 3]);
746        assert!(
747            result.is_none(),
748            "polygon entirely outside AABB should be clipped"
749        );
750    }
751    #[test]
752    fn test_clip_polygon_2d_square_vs_triangle() {
753        let subject = vec![[0.0_f64, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
754        let clip = vec![[-2.0, -2.0], [3.0, -2.0], [0.5, 3.0]];
755        let out = clip_polygon_2d(&subject, &clip);
756        assert!(
757            !out.is_empty(),
758            "square fully inside clip triangle should survive"
759        );
760    }
761    #[test]
762    fn test_clip_polygon_2d_no_overlap() {
763        let subject = vec![[0.0_f64, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
764        let clip = vec![[5.0, 5.0], [6.0, 5.0], [5.5, 6.0]];
765        let out = clip_polygon_2d(&subject, &clip);
766        assert!(
767            out.is_empty(),
768            "non-overlapping polygons should produce empty result"
769        );
770    }
771    #[test]
772    fn test_repair_winding_correct_winding_unchanged() {
773        let (verts, tris) = unit_box_mesh();
774        let score_before = winding_consistency_score(&verts, &tris);
775        let repaired = repair_winding(&verts, &tris);
776        let score_after = winding_consistency_score(&verts, &repaired);
777        assert!(
778            score_after >= score_before,
779            "repair should not worsen consistency"
780        );
781    }
782    #[test]
783    fn test_repair_winding_fixes_flipped_triangle() {
784        let (verts, mut tris) = unit_box_mesh();
785        tris[0] = [tris[0][0], tris[0][2], tris[0][1]];
786        let repaired = repair_winding(&verts, &tris);
787        let score = winding_consistency_score(&verts, &repaired);
788        assert!(
789            score > 0.9,
790            "most faces should have correct winding after repair, score={score}"
791        );
792    }
793    #[test]
794    fn test_winding_consistency_score_perfect() {
795        let (verts, tris) = unit_box_mesh();
796        let score = winding_consistency_score(&verts, &tris);
797        assert!(
798            score >= 0.5,
799            "consistent mesh should have score >= 0.5, got {score}"
800        );
801    }
802    #[test]
803    fn test_polygon2d_union_overlapping() {
804        let a: Vec<[f64; 2]> = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
805        let b: Vec<[f64; 2]> = vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]];
806        let result = polygon2d_union(&a, &b);
807        assert!(
808            !result.is_empty(),
809            "union of overlapping squares should be non-empty"
810        );
811    }
812    #[test]
813    fn test_polygon2d_intersection_overlapping() {
814        let a: Vec<[f64; 2]> = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
815        let b: Vec<[f64; 2]> = vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]];
816        let result = polygon2d_intersection(&a, &b);
817        assert!(
818            !result.is_empty(),
819            "intersection of overlapping squares should be non-empty"
820        );
821        assert_eq!(result.len(), 4);
822    }
823    #[test]
824    fn test_polygon2d_intersection_non_overlapping() {
825        let a: Vec<[f64; 2]> = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
826        let b: Vec<[f64; 2]> = vec![[5.0, 0.0], [6.0, 0.0], [6.0, 1.0], [5.0, 1.0]];
827        let result = polygon2d_intersection(&a, &b);
828        assert!(
829            result.is_empty(),
830            "non-overlapping squares have empty intersection"
831        );
832    }
833    #[test]
834    fn test_polygon2d_difference_basic() {
835        let a: Vec<[f64; 2]> = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
836        let b: Vec<[f64; 2]> = vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]];
837        let _result = polygon2d_difference(&a, &b);
838    }
839    #[test]
840    fn test_csg_material_union() {
841        let a = box_mesh_trimesh(0.0, 1.0);
842        let b = box_mesh_trimesh(5.0, 6.0);
843        let mat_a = CsgMaterial {
844            id: 1,
845            name: "metal".to_string(),
846            density: 7800.0,
847        };
848        let mat_b = CsgMaterial {
849            id: 2,
850            name: "wood".to_string(),
851            density: 600.0,
852        };
853        let result = CsgWithMaterials::execute(BooleanOp::Union, &a, mat_a, &b, mat_b);
854        assert!(result.is_ok());
855        let (mesh, _mats) = result.unwrap();
856        assert!(!mesh.indices.is_empty());
857    }
858    #[test]
859    fn test_csg_material_retains_ids() {
860        let a = box_mesh_trimesh(0.0, 1.0);
861        let b = box_mesh_trimesh(5.0, 6.0);
862        let mat_a = CsgMaterial {
863            id: 10,
864            name: "a".to_string(),
865            density: 1.0,
866        };
867        let mat_b = CsgMaterial {
868            id: 20,
869            name: "b".to_string(),
870            density: 2.0,
871        };
872        let (_, mats) =
873            CsgWithMaterials::execute(BooleanOp::Union, &a, mat_a.clone(), &b, mat_b.clone())
874                .unwrap();
875        let ids: Vec<u32> = mats.iter().map(|m| m.id).collect();
876        assert!(ids.contains(&10));
877        assert!(ids.contains(&20));
878    }
879    #[test]
880    fn test_robust_boolean_union_non_overlapping() {
881        let a = box_mesh_trimesh(0.0, 1.0);
882        let b = box_mesh_trimesh(5.0, 6.0);
883        let result = RobustMeshBoolean::execute(BooleanOp::Union, &a, &b, 1e-8);
884        assert!(result.is_ok());
885        let mesh = result.unwrap();
886        assert!(!mesh.indices.is_empty());
887    }
888    #[test]
889    fn test_robust_boolean_intersection_non_overlapping() {
890        let a = box_mesh_trimesh(0.0, 1.0);
891        let b = box_mesh_trimesh(5.0, 6.0);
892        let result = RobustMeshBoolean::execute(BooleanOp::Intersection, &a, &b, 1e-8);
893        assert!(result.is_ok());
894        assert!(result.unwrap().indices.is_empty());
895    }
896    #[test]
897    fn test_robust_boolean_difference() {
898        let a = box_mesh_trimesh(0.0, 2.0);
899        let b = box_mesh_trimesh(5.0, 6.0);
900        let result = RobustMeshBoolean::execute(BooleanOp::Difference, &a, &b, 1e-8);
901        assert!(result.is_ok());
902        assert!(!result.unwrap().indices.is_empty());
903    }
904    #[test]
905    fn test_robust_boolean_epsilon_tolerance() {
906        let a = box_mesh_trimesh(0.0, 1.0);
907        let b = box_mesh_trimesh(0.999_999_9, 2.0);
908        let result_large_eps = RobustMeshBoolean::execute(BooleanOp::Intersection, &a, &b, 1.0);
909        assert!(result_large_eps.is_ok());
910        let result_small_eps = RobustMeshBoolean::execute(BooleanOp::Union, &a, &b, 1e-12);
911        assert!(result_small_eps.is_ok());
912    }
913    #[test]
914    fn test_manifold_boolean_union() {
915        let a = box_mesh_trimesh(0.0, 1.0);
916        let b = box_mesh_trimesh(5.0, 6.0);
917        let result = ManifoldBoolean::execute(BooleanOp::Union, &a, &b);
918        assert!(result.is_ok());
919        let mesh = result.unwrap();
920        assert!(!mesh.indices.is_empty());
921    }
922    #[test]
923    fn test_manifold_boolean_validates_input() {
924        let empty = TriangleMesh::new(vec![], vec![]);
925        let b = box_mesh_trimesh(0.0, 1.0);
926        let result = ManifoldBoolean::execute(BooleanOp::Union, &empty, &b);
927        assert!(result.is_err(), "empty input should produce error");
928    }
929    #[test]
930    fn test_manifold_boolean_self_union_is_identity() {
931        let a = box_mesh_trimesh(0.0, 1.0);
932        let result = ManifoldBoolean::execute(BooleanOp::Union, &a, &a);
933        assert!(result.is_ok());
934        let _mesh = result.unwrap();
935    }
936}
937/// Split a polygon by a plane into front and back fragments.
938///
939/// Returns `(front_polygon, back_polygon)`.  Either may be empty if the
940/// polygon lies entirely on one side.
941#[allow(dead_code)]
942pub fn split_polygon_by_plane(
943    polygon: &[[f64; 3]],
944    plane: &BspPlane,
945) -> (Vec<[f64; 3]>, Vec<[f64; 3]>) {
946    let mut front: Vec<[f64; 3]> = Vec::new();
947    let mut back: Vec<[f64; 3]> = Vec::new();
948    let n = polygon.len();
949    if n == 0 {
950        return (front, back);
951    }
952    for i in 0..n {
953        let a = polygon[i];
954        let b = polygon[(i + 1) % n];
955        let da = plane.signed_dist(a);
956        let db = plane.signed_dist(b);
957        if da >= -1e-8 {
958            front.push(a);
959        }
960        if da <= 1e-8 {
961            back.push(a);
962        }
963        if (da > 1e-8 && db < -1e-8) || (da < -1e-8 && db > 1e-8) {
964            let t = da / (da - db);
965            let inter = [
966                a[0] + t * (b[0] - a[0]),
967                a[1] + t * (b[1] - a[1]),
968                a[2] + t * (b[2] - a[2]),
969            ];
970            front.push(inter);
971            back.push(inter);
972        }
973    }
974    (front, back)
975}
976/// Clip a subject polygon against a convex clip polygon using the
977/// Weiler-Atherton algorithm (2D version).
978///
979/// The clip polygon must be wound counter-clockwise.
980/// Returns the clipped polygon.
981#[allow(dead_code)]
982pub fn weiler_atherton_clip(subject: &[[f64; 2]], clip: &[[f64; 2]]) -> Vec<[f64; 2]> {
983    clip_polygon_2d(subject, clip)
984}
985/// Compute intersection points of two polygons.
986///
987/// Returns all edge-edge intersection points between the two polygon boundaries.
988#[allow(dead_code)]
989pub fn polygon_intersection_points(a: &[[f64; 2]], b: &[[f64; 2]]) -> Vec<[f64; 2]> {
990    let mut result = Vec::new();
991    let na = a.len();
992    let nb = b.len();
993    for i in 0..na {
994        let a0 = a[i];
995        let a1 = a[(i + 1) % na];
996        for j in 0..nb {
997            let b0 = b[j];
998            let b1 = b[(j + 1) % nb];
999            if let Some(pt) = segment_segment_intersect_2d(a0, a1, b0, b1) {
1000                result.push(pt);
1001            }
1002        }
1003    }
1004    result
1005}
1006/// Compute the intersection of two 2D line segments.
1007///
1008/// Returns the intersection point if segments properly intersect, or `None`.
1009#[allow(dead_code)]
1010pub fn segment_segment_intersect_2d(
1011    p1: [f64; 2],
1012    p2: [f64; 2],
1013    p3: [f64; 2],
1014    p4: [f64; 2],
1015) -> Option<[f64; 2]> {
1016    let d1 = [p2[0] - p1[0], p2[1] - p1[1]];
1017    let d2 = [p4[0] - p3[0], p4[1] - p3[1]];
1018    let denom = d1[0] * d2[1] - d1[1] * d2[0];
1019    if denom.abs() < 1e-12 {
1020        return None;
1021    }
1022    let t = ((p3[0] - p1[0]) * d2[1] - (p3[1] - p1[1]) * d2[0]) / denom;
1023    let u = ((p3[0] - p1[0]) * d1[1] - (p3[1] - p1[1]) * d1[0]) / denom;
1024    if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
1025        Some([p1[0] + t * d1[0], p1[1] + t * d1[1]])
1026    } else {
1027        None
1028    }
1029}
1030/// Offset a 2D polygon outward by `delta` (positive) or inward by `|delta|`
1031/// (negative).
1032///
1033/// Each edge is displaced by `delta` in its outward normal direction.
1034/// The offset polygon is computed by offsetting each edge and re-intersecting
1035/// adjacent pairs.  This is the Minkowski sum with a disk of radius `|delta|`
1036/// for convex polygons.
1037///
1038/// Returns the offset polygon or an empty vector if degenerate.
1039#[allow(dead_code)]
1040pub fn offset_polygon_2d(polygon: &[[f64; 2]], delta: f64) -> Vec<[f64; 2]> {
1041    let n = polygon.len();
1042    if n < 3 {
1043        return Vec::new();
1044    }
1045    let mut offset_lines: Vec<([f64; 2], [f64; 2])> = Vec::with_capacity(n);
1046    for i in 0..n {
1047        let a = polygon[i];
1048        let b = polygon[(i + 1) % n];
1049        let edge = [b[0] - a[0], b[1] - a[1]];
1050        let len = (edge[0] * edge[0] + edge[1] * edge[1]).sqrt();
1051        if len < 1e-12 {
1052            continue;
1053        }
1054        let normal = [edge[1] / len, -edge[0] / len];
1055        let oa = [a[0] + delta * normal[0], a[1] + delta * normal[1]];
1056        let ob = [b[0] + delta * normal[0], b[1] + delta * normal[1]];
1057        offset_lines.push((oa, ob));
1058    }
1059    if offset_lines.len() < 3 {
1060        return Vec::new();
1061    }
1062    let m = offset_lines.len();
1063    let mut result = Vec::with_capacity(m);
1064    for i in 0..m {
1065        let (a0, a1) = offset_lines[i];
1066        let (b0, b1) = offset_lines[(i + 1) % m];
1067        if let Some(pt) = line_line_intersect_2d(a0, a1, b0, b1) {
1068            result.push(pt);
1069        } else {
1070            result.push([(a1[0] + b0[0]) * 0.5, (a1[1] + b0[1]) * 0.5]);
1071        }
1072    }
1073    result
1074}
1075/// Compute the intersection of two infinite 2D lines (given as two points each).
1076///
1077/// Returns `None` if the lines are parallel.
1078#[allow(dead_code)]
1079pub fn line_line_intersect_2d(
1080    a0: [f64; 2],
1081    a1: [f64; 2],
1082    b0: [f64; 2],
1083    b1: [f64; 2],
1084) -> Option<[f64; 2]> {
1085    let da = [a1[0] - a0[0], a1[1] - a0[1]];
1086    let db = [b1[0] - b0[0], b1[1] - b0[1]];
1087    let denom = da[0] * db[1] - da[1] * db[0];
1088    if denom.abs() < 1e-12 {
1089        return None;
1090    }
1091    let t = ((b0[0] - a0[0]) * db[1] - (b0[1] - a0[1]) * db[0]) / denom;
1092    Some([a0[0] + t * da[0], a0[1] + t * da[1]])
1093}
1094/// Compute the solid angle subtended by a triangle at a point `p`.
1095///
1096/// This is used in the winding number computation.
1097/// Formula from Oosterom and Strackee (1983).
1098pub(super) fn solid_angle_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
1099    let ra = sub3(a, p);
1100    let rb = sub3(b, p);
1101    let rc = sub3(c, p);
1102    let la = len3(ra);
1103    let lb = len3(rb);
1104    let lc = len3(rc);
1105    if la < 1e-15 || lb < 1e-15 || lc < 1e-15 {
1106        return 0.0;
1107    }
1108    let numerator = dot3(ra, cross3(rb, rc));
1109    let denominator = la * lb * lc + dot3(ra, rb) * lc + dot3(rb, rc) * la + dot3(ra, rc) * lb;
1110    if denominator.abs() < 1e-15 {
1111        return 0.0;
1112    }
1113    2.0 * numerator.atan2(denominator)
1114}
1115/// Compute the winding number of a closed triangle mesh around point `p`.
1116///
1117/// The winding number is an integer for points strictly inside/outside:
1118/// - `0` → outside the mesh
1119/// - `±1` → inside the mesh
1120/// - other values indicate the point lies on the surface
1121///
1122/// This is more robust than ray casting for points near mesh boundaries.
1123#[allow(dead_code)]
1124pub fn winding_number_3d(p: [f64; 3], verts: &[[f64; 3]], tris: &[[usize; 3]]) -> f64 {
1125    let mut total = 0.0f64;
1126    for tri in tris {
1127        let a = verts[tri[0]];
1128        let b = verts[tri[1]];
1129        let c = verts[tri[2]];
1130        total += solid_angle_triangle(p, a, b, c);
1131    }
1132    total / (4.0 * std::f64::consts::PI)
1133}
1134/// Test whether a point is strictly inside a closed mesh using the winding number.
1135///
1136/// Returns `true` if the winding number is close to ±1 (i.e., the point is enclosed).
1137#[allow(dead_code)]
1138pub fn winding_number_inside(p: [f64; 3], verts: &[[f64; 3]], tris: &[[usize; 3]]) -> bool {
1139    let wn = winding_number_3d(p, verts, tris);
1140    wn.abs() > 0.5
1141}
1142/// Compute the signed area of a 2D polygon (positive for CCW winding).
1143///
1144/// Uses the shoelace (Gauss) formula.
1145#[allow(dead_code)]
1146pub fn polygon2d_signed_area(poly: &[[f64; 2]]) -> f64 {
1147    let n = poly.len();
1148    if n < 3 {
1149        return 0.0;
1150    }
1151    let mut area = 0.0f64;
1152    for i in 0..n {
1153        let a = poly[i];
1154        let b = poly[(i + 1) % n];
1155        area += a[0] * b[1] - b[0] * a[1];
1156    }
1157    area * 0.5
1158}
1159/// Compute the unsigned area of a 2D polygon.
1160#[allow(dead_code)]
1161pub fn polygon2d_area(poly: &[[f64; 2]]) -> f64 {
1162    polygon2d_signed_area(poly).abs()
1163}
1164/// Compute the centroid of a 2D polygon.
1165///
1166/// Uses the formula based on vertex cross products.
1167#[allow(dead_code)]
1168pub fn polygon2d_centroid(poly: &[[f64; 2]]) -> Option<[f64; 2]> {
1169    let n = poly.len();
1170    if n < 3 {
1171        return None;
1172    }
1173    let area = polygon2d_signed_area(poly);
1174    if area.abs() < 1e-24 {
1175        return None;
1176    }
1177    let mut cx = 0.0f64;
1178    let mut cy = 0.0f64;
1179    for i in 0..n {
1180        let a = poly[i];
1181        let b = poly[(i + 1) % n];
1182        let cross = a[0] * b[1] - b[0] * a[1];
1183        cx += (a[0] + b[0]) * cross;
1184        cy += (a[1] + b[1]) * cross;
1185    }
1186    Some([cx / (6.0 * area), cy / (6.0 * area)])
1187}
1188/// Test whether a 2D point is inside a (possibly non-convex) polygon.
1189///
1190/// Uses ray casting in the +X direction.
1191#[allow(dead_code)]
1192pub fn point_in_polygon_2d(p: [f64; 2], poly: &[[f64; 2]]) -> bool {
1193    let n = poly.len();
1194    if n < 3 {
1195        return false;
1196    }
1197    let mut inside = false;
1198    let mut j = n - 1;
1199    for i in 0..n {
1200        let vi = poly[i];
1201        let vj = poly[j];
1202        if ((vi[1] > p[1]) != (vj[1] > p[1]))
1203            && p[0] < (vj[0] - vi[0]) * (p[1] - vi[1]) / (vj[1] - vi[1]) + vi[0]
1204        {
1205            inside = !inside;
1206        }
1207        j = i;
1208    }
1209    inside
1210}
1211/// Compute the perimeter of a 2D polygon.
1212#[allow(dead_code)]
1213pub fn polygon2d_perimeter(poly: &[[f64; 2]]) -> f64 {
1214    let n = poly.len();
1215    if n < 2 {
1216        return 0.0;
1217    }
1218    let mut perimeter = 0.0f64;
1219    for i in 0..n {
1220        let a = poly[i];
1221        let b = poly[(i + 1) % n];
1222        let d = [a[0] - b[0], a[1] - b[1]];
1223        perimeter += (d[0] * d[0] + d[1] * d[1]).sqrt();
1224    }
1225    perimeter
1226}
1227/// Test whether a 2D polygon is convex.
1228///
1229/// Returns `true` if all cross products of consecutive edges have the same sign.
1230#[allow(dead_code)]
1231pub fn polygon2d_is_convex(poly: &[[f64; 2]]) -> bool {
1232    let n = poly.len();
1233    if n < 3 {
1234        return true;
1235    }
1236    let mut sign = 0.0f64;
1237    for i in 0..n {
1238        let a = poly[i];
1239        let b = poly[(i + 1) % n];
1240        let c = poly[(i + 2) % n];
1241        let cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
1242        if cross.abs() < 1e-12 {
1243            continue;
1244        }
1245        if sign == 0.0 {
1246            sign = cross.signum();
1247        } else if cross.signum() != sign {
1248            return false;
1249        }
1250    }
1251    true
1252}
1253#[cfg(test)]
1254mod tests_new_bool {
1255
1256    use crate::boolean_ops::BspNode;
1257    use crate::boolean_ops::BspPlane;
1258
1259    use crate::boolean_ops::HalfEdgeMesh;
1260
1261    use crate::boolean_ops::PlaneClass;
1262
1263    use crate::boolean_ops::line_line_intersect_2d;
1264    use crate::boolean_ops::offset_polygon_2d;
1265    use crate::boolean_ops::point_in_polygon_2d;
1266    use crate::boolean_ops::polygon_intersection_points;
1267    use crate::boolean_ops::polygon2d_area;
1268    use crate::boolean_ops::polygon2d_centroid;
1269    use crate::boolean_ops::polygon2d_is_convex;
1270    use crate::boolean_ops::polygon2d_perimeter;
1271    use crate::boolean_ops::polygon2d_signed_area;
1272    use crate::boolean_ops::segment_segment_intersect_2d;
1273    use crate::boolean_ops::split_polygon_by_plane;
1274    use crate::boolean_ops::weiler_atherton_clip;
1275    use crate::boolean_ops::winding_number_3d;
1276    use crate::boolean_ops::winding_number_inside;
1277    #[test]
1278    fn test_bsp_node_classify_point() {
1279        let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1280        let mut node = BspNode::new(plane);
1281        node.polygons
1282            .push(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]]);
1283        assert_eq!(node.classify_point([0.0, 0.0, 1.0]), PlaneClass::Front);
1284        assert_eq!(node.classify_point([0.0, 0.0, -1.0]), PlaneClass::Back);
1285        assert_eq!(node.classify_point([0.0, 0.0, 0.0]), PlaneClass::OnPlane);
1286    }
1287    #[test]
1288    fn test_bsp_node_insert_front_polygon() {
1289        let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1290        let mut node = BspNode::new(plane);
1291        let poly = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.5, 1.0, 1.0]];
1292        node.insert_polygon(poly);
1293        assert!(node.count_polygons() >= 1);
1294    }
1295    #[test]
1296    fn test_bsp_node_insert_back_polygon() {
1297        let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1298        let mut node = BspNode::new(plane);
1299        let poly = vec![[0.0, 0.0, -1.0], [1.0, 0.0, -1.0], [0.5, 1.0, -1.0]];
1300        node.insert_polygon(poly);
1301        assert!(node.count_polygons() >= 1);
1302    }
1303    #[test]
1304    fn test_bsp_node_coplanar_polygon() {
1305        let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1306        let mut node = BspNode::new(plane);
1307        let poly = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
1308        node.insert_polygon(poly);
1309        assert!(node.count_polygons() >= 1);
1310    }
1311    #[test]
1312    fn test_split_polygon_by_plane_clean_separation() {
1313        let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1314        let poly = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.5, 0.5, -1.0]];
1315        let (front, back) = split_polygon_by_plane(&poly, &plane);
1316        assert!(!front.is_empty());
1317        assert!(!back.is_empty());
1318        for v in &front {
1319            assert!(v[2] >= -1e-7, "front z={}", v[2]);
1320        }
1321        for v in &back {
1322            assert!(v[2] <= 1e-7, "back z={}", v[2]);
1323        }
1324    }
1325    #[test]
1326    fn test_split_polygon_all_front() {
1327        let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1328        let poly = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 2.0], [0.5, 1.0, 1.5]];
1329        let (front, back) = split_polygon_by_plane(&poly, &plane);
1330        assert!(!front.is_empty());
1331        assert!(back.is_empty() || back.iter().all(|v| v[2] <= 1e-7));
1332    }
1333    #[test]
1334    fn test_weiler_atherton_clip_contained() {
1335        let subject = vec![[0.2, 0.2], [0.8, 0.2], [0.8, 0.8], [0.2, 0.8]];
1336        let clip = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1337        let result = weiler_atherton_clip(&subject, &clip);
1338        assert!(!result.is_empty(), "fully contained polygon should survive");
1339    }
1340    #[test]
1341    fn test_weiler_atherton_clip_partial() {
1342        let subject = vec![[0.5, 0.0], [1.5, 0.0], [1.5, 1.0], [0.5, 1.0]];
1343        let clip = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1344        let result = weiler_atherton_clip(&subject, &clip);
1345        assert!(!result.is_empty(), "partial overlap should produce result");
1346    }
1347    #[test]
1348    fn test_segment_segment_intersect_basic() {
1349        let p = segment_segment_intersect_2d([0.0, 0.0], [1.0, 1.0], [0.0, 1.0], [1.0, 0.0]);
1350        assert!(p.is_some(), "crossing segments should intersect");
1351        let pt = p.unwrap();
1352        assert!((pt[0] - 0.5).abs() < 1e-10 && (pt[1] - 0.5).abs() < 1e-10);
1353    }
1354    #[test]
1355    fn test_segment_segment_no_intersect() {
1356        let p = segment_segment_intersect_2d([0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]);
1357        assert!(
1358            p.is_none(),
1359            "parallel non-overlapping segments should not intersect"
1360        );
1361    }
1362    #[test]
1363    fn test_segment_segment_parallel() {
1364        let p = segment_segment_intersect_2d([0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]);
1365        assert!(
1366            p.is_none(),
1367            "parallel horizontal segments should not intersect"
1368        );
1369    }
1370    #[test]
1371    fn test_polygon_intersection_points_two_squares() {
1372        let a = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
1373        let b = vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]];
1374        let pts = polygon_intersection_points(&a, &b);
1375        assert!(
1376            pts.len() >= 2,
1377            "two overlapping squares should have intersection points, got {}",
1378            pts.len()
1379        );
1380    }
1381    #[test]
1382    fn test_polygon_intersection_points_no_overlap() {
1383        let a = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1384        let b = vec![[5.0, 0.0], [6.0, 0.0], [6.0, 1.0], [5.0, 1.0]];
1385        let pts = polygon_intersection_points(&a, &b);
1386        assert!(
1387            pts.is_empty(),
1388            "non-overlapping polygons should have no intersection points"
1389        );
1390    }
1391    #[test]
1392    fn test_offset_polygon_2d_outward() {
1393        let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1394        let offset = offset_polygon_2d(&poly, 0.1);
1395        assert_eq!(offset.len(), 4, "square offset should have 4 vertices");
1396        let area_orig = polygon2d_area(&poly);
1397        let area_offset = polygon2d_area(&offset);
1398        assert!(
1399            area_offset > area_orig,
1400            "outward offset should increase area: {} vs {}",
1401            area_offset,
1402            area_orig
1403        );
1404    }
1405    #[test]
1406    fn test_offset_polygon_2d_inward() {
1407        let poly = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
1408        let offset = offset_polygon_2d(&poly, -0.1);
1409        assert!(
1410            !offset.is_empty(),
1411            "inward offset of large polygon should survive"
1412        );
1413    }
1414    #[test]
1415    fn test_offset_polygon_2d_zero() {
1416        let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1417        let offset = offset_polygon_2d(&poly, 0.0);
1418        let area_orig = polygon2d_area(&poly);
1419        let area_offset = polygon2d_area(&offset);
1420        assert!(
1421            (area_orig - area_offset).abs() < 0.01,
1422            "zero offset should not change area"
1423        );
1424    }
1425    #[test]
1426    fn test_line_line_intersect_crossing() {
1427        let pt = line_line_intersect_2d([0.0, 0.0], [2.0, 2.0], [0.0, 2.0], [2.0, 0.0]);
1428        assert!(pt.is_some());
1429        let p = pt.unwrap();
1430        assert!((p[0] - 1.0).abs() < 1e-10 && (p[1] - 1.0).abs() < 1e-10);
1431    }
1432    #[test]
1433    fn test_line_line_intersect_parallel() {
1434        let pt = line_line_intersect_2d([0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]);
1435        assert!(pt.is_none(), "parallel lines should not intersect");
1436    }
1437    #[test]
1438    fn test_half_edge_mesh_tetrahedron() {
1439        let verts = vec![
1440            [0.0, 0.0, 0.0],
1441            [1.0, 0.0, 0.0],
1442            [0.5, 1.0, 0.0],
1443            [0.5, 0.5, 1.0],
1444        ];
1445        let tris = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]];
1446        let mesh = HalfEdgeMesh::from_triangle_mesh(&verts, &tris);
1447        assert_eq!(mesh.n_faces(), 4);
1448        assert_eq!(mesh.n_vertices(), 4);
1449        assert_eq!(mesh.half_edges.len(), 12, "4 triangles * 3 half-edges = 12");
1450    }
1451    #[test]
1452    fn test_half_edge_mesh_face_vertices() {
1453        let verts = vec![
1454            [0.0, 0.0, 0.0],
1455            [1.0, 0.0, 0.0],
1456            [0.0, 1.0, 0.0],
1457            [0.0, 0.0, 1.0],
1458        ];
1459        let tris = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]];
1460        let mesh = HalfEdgeMesh::from_triangle_mesh(&verts, &tris);
1461        for fi in 0..mesh.n_faces() {
1462            let fv = mesh.face_vertices(fi);
1463            for &v in &fv {
1464                assert!(v < mesh.n_vertices(), "vertex index out of range: {v}");
1465            }
1466        }
1467    }
1468    #[test]
1469    fn test_half_edge_mesh_face_area_positive() {
1470        let verts = vec![
1471            [0.0, 0.0, 0.0],
1472            [1.0, 0.0, 0.0],
1473            [0.5, 1.0, 0.0],
1474            [0.5, 0.5, 1.0],
1475        ];
1476        let tris = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]];
1477        let mesh = HalfEdgeMesh::from_triangle_mesh(&verts, &tris);
1478        for fi in 0..mesh.n_faces() {
1479            let area = mesh.face_area(fi);
1480            assert!(area > 0.0, "face {fi} area should be positive, got {area}");
1481        }
1482    }
1483    #[test]
1484    fn test_winding_number_inside_unit_box() {
1485        let (verts, tris) = unit_box_mesh_raw();
1486        let inside = winding_number_inside([0.5, 0.5, 0.5], &verts, &tris);
1487        assert!(inside, "center of unit box should be inside");
1488    }
1489    #[test]
1490    fn test_winding_number_outside_unit_box() {
1491        let (verts, tris) = unit_box_mesh_raw();
1492        let inside = winding_number_inside([5.0, 5.0, 5.0], &verts, &tris);
1493        assert!(!inside, "far exterior point should not be inside");
1494    }
1495    #[test]
1496    fn test_winding_number_value_inside_is_one() {
1497        let (verts, tris) = unit_box_mesh_raw();
1498        let wn = winding_number_3d([0.5, 0.5, 0.5], &verts, &tris);
1499        assert!(
1500            (wn.abs() - 1.0).abs() < 0.1,
1501            "winding number inside should be ≈1, got {wn}"
1502        );
1503    }
1504    #[test]
1505    fn test_winding_number_value_outside_is_zero() {
1506        let (verts, tris) = unit_box_mesh_raw();
1507        let wn = winding_number_3d([5.0, 5.0, 5.0], &verts, &tris);
1508        assert!(
1509            wn.abs() < 0.1,
1510            "winding number outside should be ≈0, got {wn}"
1511        );
1512    }
1513    #[test]
1514    fn test_polygon2d_area_unit_square() {
1515        let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1516        let area = polygon2d_area(&poly);
1517        assert!(
1518            (area - 1.0).abs() < 1e-12,
1519            "unit square area should be 1.0, got {area}"
1520        );
1521    }
1522    #[test]
1523    fn test_polygon2d_signed_area_ccw_positive() {
1524        let poly = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
1525        let area = polygon2d_signed_area(&poly);
1526        assert!(
1527            area > 0.0,
1528            "CCW polygon should have positive signed area, got {area}"
1529        );
1530    }
1531    #[test]
1532    fn test_polygon2d_signed_area_cw_negative() {
1533        let poly = vec![[0.0, 0.0], [0.5, 1.0], [1.0, 0.0]];
1534        let area = polygon2d_signed_area(&poly);
1535        assert!(
1536            area < 0.0,
1537            "CW polygon should have negative signed area, got {area}"
1538        );
1539    }
1540    #[test]
1541    fn test_polygon2d_centroid_square() {
1542        let poly = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
1543        let c = polygon2d_centroid(&poly).expect("centroid");
1544        assert!(
1545            (c[0] - 1.0).abs() < 1e-10 && (c[1] - 1.0).abs() < 1e-10,
1546            "centroid of 2x2 square should be (1,1), got ({}, {})",
1547            c[0],
1548            c[1]
1549        );
1550    }
1551    #[test]
1552    fn test_point_in_polygon_2d_inside() {
1553        let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1554        assert!(
1555            point_in_polygon_2d([0.5, 0.5], &poly),
1556            "center should be inside"
1557        );
1558    }
1559    #[test]
1560    fn test_point_in_polygon_2d_outside() {
1561        let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1562        assert!(
1563            !point_in_polygon_2d([2.0, 2.0], &poly),
1564            "exterior point should be outside"
1565        );
1566    }
1567    #[test]
1568    fn test_polygon2d_perimeter_unit_square() {
1569        let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1570        let p = polygon2d_perimeter(&poly);
1571        assert!(
1572            (p - 4.0).abs() < 1e-12,
1573            "unit square perimeter should be 4, got {p}"
1574        );
1575    }
1576    #[test]
1577    fn test_polygon2d_is_convex_square() {
1578        let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1579        assert!(polygon2d_is_convex(&poly), "unit square should be convex");
1580    }
1581    #[test]
1582    fn test_polygon2d_is_not_convex_star() {
1583        let poly = vec![[0.0, 0.0], [2.0, 1.0], [1.0, 0.0], [2.0, -1.0]];
1584        let _ = polygon2d_is_convex(&poly);
1585    }
1586    #[test]
1587    fn test_polygon2d_perimeter_equilateral_triangle() {
1588        let side = 1.0_f64;
1589        let h = (3.0_f64.sqrt() / 2.0) * side;
1590        let poly = vec![[0.0, 0.0], [side, 0.0], [side / 2.0, h]];
1591        let p = polygon2d_perimeter(&poly);
1592        assert!(
1593            (p - 3.0 * side).abs() < 1e-10,
1594            "equilateral triangle perimeter should be 3, got {p}"
1595        );
1596    }
1597    /// Helper: build a unit box mesh as raw arrays.
1598    fn unit_box_mesh_raw() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
1599        let verts = vec![
1600            [0.0, 0.0, 0.0],
1601            [1.0, 0.0, 0.0],
1602            [1.0, 1.0, 0.0],
1603            [0.0, 1.0, 0.0],
1604            [0.0, 0.0, 1.0],
1605            [1.0, 0.0, 1.0],
1606            [1.0, 1.0, 1.0],
1607            [0.0, 1.0, 1.0],
1608        ];
1609        let tris = vec![
1610            [0, 3, 2],
1611            [0, 2, 1],
1612            [4, 5, 6],
1613            [4, 6, 7],
1614            [0, 1, 5],
1615            [0, 5, 4],
1616            [2, 3, 7],
1617            [2, 7, 6],
1618            [0, 4, 7],
1619            [0, 7, 3],
1620            [1, 2, 6],
1621            [1, 6, 5],
1622        ];
1623        (verts, tris)
1624    }
1625}