1use 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}
52pub 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}
77pub 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}
109pub 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}
149pub(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}
171pub(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}
208pub 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}
212pub 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}
216pub fn triangle_normal(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
218 cross3(sub3(v1, v0), sub3(v2, v0))
219}
220pub fn triangle_unit_normal(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
222 normalize3(triangle_normal(v0, v1, v2))
223}
224pub 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}
255pub 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}
280pub 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}
313pub(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}
319pub(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}
330pub 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}
355pub 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}
379pub fn polygon2d_intersection(a: &[[f64; 2]], b: &[[f64; 2]]) -> Vec<[f64; 2]> {
383 clip_polygon_2d(a, b)
384}
385pub 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}
396pub 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#[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#[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#[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#[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#[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#[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#[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#[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}
1094pub(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#[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#[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#[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#[allow(dead_code)]
1161pub fn polygon2d_area(poly: &[[f64; 2]]) -> f64 {
1162 polygon2d_signed_area(poly).abs()
1163}
1164#[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#[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#[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#[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 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}