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}
432pub(super) fn convex_hull_2d(pts: &[[f64; 2]]) -> Vec<[f64; 2]> {
434 if pts.len() < 3 {
435 return pts.to_vec();
436 }
437 let start = pts
438 .iter()
439 .enumerate()
440 .min_by(|(_, a), (_, b)| a[0].partial_cmp(&b[0]).unwrap_or(std::cmp::Ordering::Equal))
441 .map(|(i, _)| i)
442 .unwrap_or(0);
443 let mut hull = Vec::new();
444 let mut current = start;
445 loop {
446 hull.push(pts[current]);
447 let mut next = 0;
448 for i in 1..pts.len() {
449 if i == current {
450 continue;
451 }
452 let a = pts[current];
453 let b = pts[next];
454 let c = pts[i];
455 let cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
456 if cross < 0.0 || (cross == 0.0 && dist_sq_2d(a, c) > dist_sq_2d(a, b)) {
457 next = i;
458 }
459 }
460 current = next;
461 if current == start {
462 break;
463 }
464 if hull.len() > pts.len() {
465 break;
466 }
467 }
468 hull
469}
470pub(super) fn dist_sq_2d(a: [f64; 2], b: [f64; 2]) -> f64 {
471 (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)
472}
473pub fn check_manifold(tris: &[[usize; 3]]) -> ManifoldCheckResult {
475 let mut edge_count: std::collections::HashMap<(usize, usize), usize> =
476 std::collections::HashMap::new();
477 for tri in tris {
478 let edges = [
479 (tri[0].min(tri[1]), tri[0].max(tri[1])),
480 (tri[1].min(tri[2]), tri[1].max(tri[2])),
481 (tri[0].min(tri[2]), tri[0].max(tri[2])),
482 ];
483 for e in &edges {
484 *edge_count.entry(*e).or_insert(0) += 1;
485 }
486 }
487 let mut n_boundary = 0;
488 let mut n_non_manifold = 0;
489 for &count in edge_count.values() {
490 if count == 1 {
491 n_boundary += 1;
492 }
493 if count > 2 {
494 n_non_manifold += 1;
495 }
496 }
497 ManifoldCheckResult {
498 is_manifold: n_boundary == 0 && n_non_manifold == 0,
499 n_boundary_edges: n_boundary,
500 n_non_manifold_edges: n_non_manifold,
501 }
502}
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use crate::TriangleMesh;
507 use crate::boolean_ops::BooleanOp;
508
509 use crate::boolean_ops::BspPlane;
510 use crate::boolean_ops::CsgMaterial;
511 use crate::boolean_ops::CsgWithMaterials;
512
513 use crate::boolean_ops::ManifoldBoolean;
514 use crate::boolean_ops::MeshBoolean;
515
516 use crate::boolean_ops::RobustMeshBoolean;
517
518 #[test]
519 fn test_intersecting_triangles() {
520 let a0 = [-1.0_f64, 0.0, 0.0];
521 let a1 = [1.0, 0.0, 0.0];
522 let a2 = [0.0, 1.0, 0.0];
523 let b0 = [0.0, 0.5, -1.0];
524 let b1 = [0.0, 0.5, 1.0];
525 let b2 = [0.0, -0.5, 0.0];
526 assert!(
527 triangles_intersect(a0, a1, a2, b0, b1, b2),
528 "crossing triangles should intersect"
529 );
530 }
531 #[test]
532 fn test_non_intersecting_triangles() {
533 let a0 = [0.0_f64, 0.0, 0.0];
534 let a1 = [1.0, 0.0, 0.0];
535 let a2 = [0.0, 1.0, 0.0];
536 let b0 = [5.0, 0.0, 0.0];
537 let b1 = [6.0, 0.0, 0.0];
538 let b2 = [5.0, 1.0, 0.0];
539 assert!(
540 !triangles_intersect(a0, a1, a2, b0, b1, b2),
541 "separated triangles should not intersect"
542 );
543 }
544 #[test]
545 fn test_ray_triangle_hit() {
546 let origin = [-5.0_f64, 0.0, 0.0];
547 let dir = [1.0, 0.0, 0.0];
548 let v0 = [0.0, -1.0, -1.0];
549 let v1 = [0.0, 1.0, -1.0];
550 let v2 = [0.0, 0.0, 1.0];
551 let t = ray_triangle_intersect(origin, dir, v0, v1, v2);
552 assert!(t.is_some(), "ray should hit the triangle");
553 assert!((t.unwrap() - 5.0).abs() < 1e-10, "t should be 5.0");
554 }
555 #[test]
556 fn test_ray_triangle_miss() {
557 let origin = [-5.0_f64, 10.0, 0.0];
558 let dir = [1.0, 0.0, 0.0];
559 let v0 = [0.0, -1.0, -1.0];
560 let v1 = [0.0, 1.0, -1.0];
561 let v2 = [0.0, 0.0, 1.0];
562 let t = ray_triangle_intersect(origin, dir, v0, v1, v2);
563 assert!(t.is_none(), "ray should miss the triangle");
564 }
565 fn unit_box_mesh() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
566 let v: Vec<[f64; 3]> = vec![
567 [-1.0, -1.0, -1.0],
568 [1.0, -1.0, -1.0],
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 ];
576 let t: Vec<[usize; 3]> = vec![
577 [0, 2, 1],
578 [0, 3, 2],
579 [4, 5, 6],
580 [4, 6, 7],
581 [0, 1, 5],
582 [0, 5, 4],
583 [3, 6, 2],
584 [3, 7, 6],
585 [0, 4, 7],
586 [0, 7, 3],
587 [1, 2, 6],
588 [1, 6, 5],
589 ];
590 (v, t)
591 }
592 #[test]
593 fn test_point_inside_mesh() {
594 let (v, t) = unit_box_mesh();
595 assert!(
596 point_inside_mesh([0.1, 0.2, 0.3], &v, &t),
597 "interior point should be inside the unit box"
598 );
599 }
600 #[test]
601 fn test_point_outside_mesh() {
602 let (v, t) = unit_box_mesh();
603 assert!(
604 !point_inside_mesh([5.0, 0.0, 0.0], &v, &t),
605 "far point should be outside the unit box"
606 );
607 }
608 fn box_mesh_trimesh(min: f64, max: f64) -> TriangleMesh {
609 let v: Vec<Vec3> = vec![
610 Vec3::new(min, min, min),
611 Vec3::new(max, min, min),
612 Vec3::new(max, max, min),
613 Vec3::new(min, max, min),
614 Vec3::new(min, min, max),
615 Vec3::new(max, min, max),
616 Vec3::new(max, max, max),
617 Vec3::new(min, max, max),
618 ];
619 let t: Vec<[usize; 3]> = vec![
620 [0, 2, 1],
621 [0, 3, 2],
622 [4, 5, 6],
623 [4, 6, 7],
624 [0, 1, 5],
625 [0, 5, 4],
626 [3, 6, 2],
627 [3, 7, 6],
628 [0, 4, 7],
629 [0, 7, 3],
630 [1, 2, 6],
631 [1, 6, 5],
632 ];
633 TriangleMesh::new(v, t)
634 }
635 #[test]
636 fn test_boolean_union_non_overlapping() {
637 let a = box_mesh_trimesh(0.0, 1.0);
638 let b = box_mesh_trimesh(5.0, 6.0);
639 let result = MeshBoolean::execute(BooleanOp::Union, &a, &b).unwrap();
640 assert!(
641 !result.indices.is_empty(),
642 "union of non-overlapping meshes should have faces"
643 );
644 }
645 #[test]
646 fn test_boolean_difference_non_overlapping() {
647 let a = box_mesh_trimesh(0.0, 1.0);
648 let b = box_mesh_trimesh(5.0, 6.0);
649 let result = MeshBoolean::execute(BooleanOp::Difference, &a, &b).unwrap();
650 assert!(
651 !result.indices.is_empty(),
652 "A - B with no overlap should return A's faces"
653 );
654 }
655 #[test]
656 fn test_boolean_intersection_non_overlapping() {
657 let a = box_mesh_trimesh(0.0, 1.0);
658 let b = box_mesh_trimesh(5.0, 6.0);
659 let result = MeshBoolean::execute(BooleanOp::Intersection, &a, &b).unwrap();
660 assert!(
661 result.indices.is_empty(),
662 "intersection of non-overlapping meshes should be empty"
663 );
664 }
665 #[test]
666 fn test_boolean_error_on_empty_mesh() {
667 let empty = TriangleMesh::new(vec![], vec![]);
668 let b = box_mesh_trimesh(0.0, 1.0);
669 let result = MeshBoolean::execute(BooleanOp::Union, &empty, &b);
670 assert!(result.is_err(), "empty mesh should return an error");
671 }
672 #[test]
673 fn test_triangle_centroid() {
674 let c = triangle_centroid([0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]);
675 assert!((c[0] - 1.0).abs() < 1e-10);
676 assert!((c[1] - 1.0).abs() < 1e-10);
677 assert!(c[2].abs() < 1e-10);
678 }
679 #[test]
680 fn test_triangle_area() {
681 let area = triangle_area([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]);
682 assert!((area - 2.0).abs() < 1e-10, "area={area}");
683 }
684 #[test]
685 fn test_coplanar_overlap() {
686 let a0 = [0.0_f64, 0.0, 0.0];
687 let a1 = [2.0, 0.0, 0.0];
688 let a2 = [0.0, 2.0, 0.0];
689 let b0 = [1.0, 0.0, 0.0];
690 let b1 = [3.0, 0.0, 0.0];
691 let b2 = [1.0, 2.0, 0.0];
692 assert!(
693 triangles_intersect(a0, a1, a2, b0, b1, b2),
694 "overlapping coplanar triangles should intersect"
695 );
696 }
697 #[test]
698 fn test_bsp_plane_signed_dist() {
699 let plane = BspPlane::new([0.0, 1.0, 0.0], 0.0);
700 assert!(plane.signed_dist([0.0, 1.0, 0.0]) > 0.0);
701 assert!(plane.signed_dist([0.0, -1.0, 0.0]) < 0.0);
702 assert!(plane.signed_dist([0.0, 0.0, 0.0]).abs() < 1e-10);
703 }
704 #[test]
705 fn test_clip_polygon_by_plane_all_outside() {
706 let poly = vec![[0.0_f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.0, 1.0]];
707 let plane = BspPlane::new([0.0, 1.0, 0.0], 1.0);
708 let clipped = clip_polygon_by_plane(&poly, &plane);
709 assert!(clipped.is_empty(), "all below plane should be clipped");
710 }
711 #[test]
712 fn test_clip_polygon_by_plane_all_inside() {
713 let poly = vec![[0.0_f64, 1.0, 0.0], [1.0, 1.0, 0.0], [0.5, 2.0, 1.0]];
714 let plane = BspPlane::new([0.0, 1.0, 0.0], 0.0);
715 let clipped = clip_polygon_by_plane(&poly, &plane);
716 assert_eq!(clipped.len(), 3, "unchanged polygon should have 3 verts");
717 }
718 #[test]
719 fn test_clip_polygon_by_plane_partial_clip() {
720 let poly = vec![
721 [-1.0_f64, 1.0, 0.0],
722 [1.0, 1.0, 0.0],
723 [1.0, -1.0, 0.0],
724 [-1.0, -1.0, 0.0],
725 ];
726 let plane = BspPlane::new([0.0, 1.0, 0.0], 0.0);
727 let clipped = clip_polygon_by_plane(&poly, &plane);
728 assert!(!clipped.is_empty(), "partial overlap should produce result");
729 for v in &clipped {
730 assert!(v[1] >= -1e-9, "clipped vertex y={}", v[1]);
731 }
732 }
733 #[test]
734 fn test_clip_polygon_by_aabb_inside() {
735 let poly = vec![[0.1_f64, 0.1, 0.1], [0.9, 0.1, 0.1], [0.5, 0.9, 0.5]];
736 let result = clip_polygon_by_aabb(&poly, [0.0; 3], [1.0; 3]);
737 assert!(result.is_some(), "polygon inside AABB should survive");
738 assert_eq!(result.unwrap().len(), 3);
739 }
740 #[test]
741 fn test_clip_polygon_by_aabb_outside() {
742 let poly = vec![[5.0_f64, 5.0, 5.0], [6.0, 5.0, 5.0], [5.5, 6.0, 5.5]];
743 let result = clip_polygon_by_aabb(&poly, [0.0; 3], [1.0; 3]);
744 assert!(
745 result.is_none(),
746 "polygon entirely outside AABB should be clipped"
747 );
748 }
749 #[test]
750 fn test_clip_polygon_2d_square_vs_triangle() {
751 let subject = vec![[0.0_f64, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
752 let clip = vec![[-2.0, -2.0], [3.0, -2.0], [0.5, 3.0]];
753 let out = clip_polygon_2d(&subject, &clip);
754 assert!(
755 !out.is_empty(),
756 "square fully inside clip triangle should survive"
757 );
758 }
759 #[test]
760 fn test_clip_polygon_2d_no_overlap() {
761 let subject = vec![[0.0_f64, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
762 let clip = vec![[5.0, 5.0], [6.0, 5.0], [5.5, 6.0]];
763 let out = clip_polygon_2d(&subject, &clip);
764 assert!(
765 out.is_empty(),
766 "non-overlapping polygons should produce empty result"
767 );
768 }
769 #[test]
770 fn test_repair_winding_correct_winding_unchanged() {
771 let (verts, tris) = unit_box_mesh();
772 let score_before = winding_consistency_score(&verts, &tris);
773 let repaired = repair_winding(&verts, &tris);
774 let score_after = winding_consistency_score(&verts, &repaired);
775 assert!(
776 score_after >= score_before,
777 "repair should not worsen consistency"
778 );
779 }
780 #[test]
781 fn test_repair_winding_fixes_flipped_triangle() {
782 let (verts, mut tris) = unit_box_mesh();
783 tris[0] = [tris[0][0], tris[0][2], tris[0][1]];
784 let repaired = repair_winding(&verts, &tris);
785 let score = winding_consistency_score(&verts, &repaired);
786 assert!(
787 score > 0.9,
788 "most faces should have correct winding after repair, score={score}"
789 );
790 }
791 #[test]
792 fn test_winding_consistency_score_perfect() {
793 let (verts, tris) = unit_box_mesh();
794 let score = winding_consistency_score(&verts, &tris);
795 assert!(
796 score >= 0.5,
797 "consistent mesh should have score >= 0.5, got {score}"
798 );
799 }
800 #[test]
801 fn test_polygon2d_union_overlapping() {
802 let a: Vec<[f64; 2]> = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
803 let b: Vec<[f64; 2]> = vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]];
804 let result = polygon2d_union(&a, &b);
805 assert!(
806 !result.is_empty(),
807 "union of overlapping squares should be non-empty"
808 );
809 }
810 #[test]
811 fn test_polygon2d_intersection_overlapping() {
812 let a: Vec<[f64; 2]> = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
813 let b: Vec<[f64; 2]> = vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]];
814 let result = polygon2d_intersection(&a, &b);
815 assert!(
816 !result.is_empty(),
817 "intersection of overlapping squares should be non-empty"
818 );
819 assert_eq!(result.len(), 4);
820 }
821 #[test]
822 fn test_polygon2d_intersection_non_overlapping() {
823 let a: Vec<[f64; 2]> = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
824 let b: Vec<[f64; 2]> = vec![[5.0, 0.0], [6.0, 0.0], [6.0, 1.0], [5.0, 1.0]];
825 let result = polygon2d_intersection(&a, &b);
826 assert!(
827 result.is_empty(),
828 "non-overlapping squares have empty intersection"
829 );
830 }
831 #[test]
832 fn test_polygon2d_difference_basic() {
833 let a: Vec<[f64; 2]> = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
834 let b: Vec<[f64; 2]> = vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]];
835 let _result = polygon2d_difference(&a, &b);
836 }
837 #[test]
838 fn test_csg_material_union() {
839 let a = box_mesh_trimesh(0.0, 1.0);
840 let b = box_mesh_trimesh(5.0, 6.0);
841 let mat_a = CsgMaterial {
842 id: 1,
843 name: "metal".to_string(),
844 density: 7800.0,
845 };
846 let mat_b = CsgMaterial {
847 id: 2,
848 name: "wood".to_string(),
849 density: 600.0,
850 };
851 let result = CsgWithMaterials::execute(BooleanOp::Union, &a, mat_a, &b, mat_b);
852 assert!(result.is_ok());
853 let (mesh, _mats) = result.unwrap();
854 assert!(!mesh.indices.is_empty());
855 }
856 #[test]
857 fn test_csg_material_retains_ids() {
858 let a = box_mesh_trimesh(0.0, 1.0);
859 let b = box_mesh_trimesh(5.0, 6.0);
860 let mat_a = CsgMaterial {
861 id: 10,
862 name: "a".to_string(),
863 density: 1.0,
864 };
865 let mat_b = CsgMaterial {
866 id: 20,
867 name: "b".to_string(),
868 density: 2.0,
869 };
870 let (_, mats) =
871 CsgWithMaterials::execute(BooleanOp::Union, &a, mat_a.clone(), &b, mat_b.clone())
872 .unwrap();
873 let ids: Vec<u32> = mats.iter().map(|m| m.id).collect();
874 assert!(ids.contains(&10));
875 assert!(ids.contains(&20));
876 }
877 #[test]
878 fn test_robust_boolean_union_non_overlapping() {
879 let a = box_mesh_trimesh(0.0, 1.0);
880 let b = box_mesh_trimesh(5.0, 6.0);
881 let result = RobustMeshBoolean::execute(BooleanOp::Union, &a, &b, 1e-8);
882 assert!(result.is_ok());
883 let mesh = result.unwrap();
884 assert!(!mesh.indices.is_empty());
885 }
886 #[test]
887 fn test_robust_boolean_intersection_non_overlapping() {
888 let a = box_mesh_trimesh(0.0, 1.0);
889 let b = box_mesh_trimesh(5.0, 6.0);
890 let result = RobustMeshBoolean::execute(BooleanOp::Intersection, &a, &b, 1e-8);
891 assert!(result.is_ok());
892 assert!(result.unwrap().indices.is_empty());
893 }
894 #[test]
895 fn test_robust_boolean_difference() {
896 let a = box_mesh_trimesh(0.0, 2.0);
897 let b = box_mesh_trimesh(5.0, 6.0);
898 let result = RobustMeshBoolean::execute(BooleanOp::Difference, &a, &b, 1e-8);
899 assert!(result.is_ok());
900 assert!(!result.unwrap().indices.is_empty());
901 }
902 #[test]
903 fn test_robust_boolean_epsilon_tolerance() {
904 let a = box_mesh_trimesh(0.0, 1.0);
905 let b = box_mesh_trimesh(0.999_999_9, 2.0);
906 let result_large_eps = RobustMeshBoolean::execute(BooleanOp::Intersection, &a, &b, 1.0);
907 assert!(result_large_eps.is_ok());
908 let result_small_eps = RobustMeshBoolean::execute(BooleanOp::Union, &a, &b, 1e-12);
909 assert!(result_small_eps.is_ok());
910 }
911 #[test]
912 fn test_manifold_boolean_union() {
913 let a = box_mesh_trimesh(0.0, 1.0);
914 let b = box_mesh_trimesh(5.0, 6.0);
915 let result = ManifoldBoolean::execute(BooleanOp::Union, &a, &b);
916 assert!(result.is_ok());
917 let mesh = result.unwrap();
918 assert!(!mesh.indices.is_empty());
919 }
920 #[test]
921 fn test_manifold_boolean_validates_input() {
922 let empty = TriangleMesh::new(vec![], vec![]);
923 let b = box_mesh_trimesh(0.0, 1.0);
924 let result = ManifoldBoolean::execute(BooleanOp::Union, &empty, &b);
925 assert!(result.is_err(), "empty input should produce error");
926 }
927 #[test]
928 fn test_manifold_boolean_self_union_is_identity() {
929 let a = box_mesh_trimesh(0.0, 1.0);
930 let result = ManifoldBoolean::execute(BooleanOp::Union, &a, &a);
931 assert!(result.is_ok());
932 let _mesh = result.unwrap();
933 }
934}
935pub fn split_polygon_by_plane(
940 polygon: &[[f64; 3]],
941 plane: &BspPlane,
942) -> (Vec<[f64; 3]>, Vec<[f64; 3]>) {
943 let mut front: Vec<[f64; 3]> = Vec::new();
944 let mut back: Vec<[f64; 3]> = Vec::new();
945 let n = polygon.len();
946 if n == 0 {
947 return (front, back);
948 }
949 for i in 0..n {
950 let a = polygon[i];
951 let b = polygon[(i + 1) % n];
952 let da = plane.signed_dist(a);
953 let db = plane.signed_dist(b);
954 if da >= -1e-8 {
955 front.push(a);
956 }
957 if da <= 1e-8 {
958 back.push(a);
959 }
960 if (da > 1e-8 && db < -1e-8) || (da < -1e-8 && db > 1e-8) {
961 let t = da / (da - db);
962 let inter = [
963 a[0] + t * (b[0] - a[0]),
964 a[1] + t * (b[1] - a[1]),
965 a[2] + t * (b[2] - a[2]),
966 ];
967 front.push(inter);
968 back.push(inter);
969 }
970 }
971 (front, back)
972}
973pub fn weiler_atherton_clip(subject: &[[f64; 2]], clip: &[[f64; 2]]) -> Vec<[f64; 2]> {
979 clip_polygon_2d(subject, clip)
980}
981pub fn polygon_intersection_points(a: &[[f64; 2]], b: &[[f64; 2]]) -> Vec<[f64; 2]> {
985 let mut result = Vec::new();
986 let na = a.len();
987 let nb = b.len();
988 for i in 0..na {
989 let a0 = a[i];
990 let a1 = a[(i + 1) % na];
991 for j in 0..nb {
992 let b0 = b[j];
993 let b1 = b[(j + 1) % nb];
994 if let Some(pt) = segment_segment_intersect_2d(a0, a1, b0, b1) {
995 result.push(pt);
996 }
997 }
998 }
999 result
1000}
1001pub fn segment_segment_intersect_2d(
1005 p1: [f64; 2],
1006 p2: [f64; 2],
1007 p3: [f64; 2],
1008 p4: [f64; 2],
1009) -> Option<[f64; 2]> {
1010 let d1 = [p2[0] - p1[0], p2[1] - p1[1]];
1011 let d2 = [p4[0] - p3[0], p4[1] - p3[1]];
1012 let denom = d1[0] * d2[1] - d1[1] * d2[0];
1013 if denom.abs() < 1e-12 {
1014 return None;
1015 }
1016 let t = ((p3[0] - p1[0]) * d2[1] - (p3[1] - p1[1]) * d2[0]) / denom;
1017 let u = ((p3[0] - p1[0]) * d1[1] - (p3[1] - p1[1]) * d1[0]) / denom;
1018 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
1019 Some([p1[0] + t * d1[0], p1[1] + t * d1[1]])
1020 } else {
1021 None
1022 }
1023}
1024pub fn offset_polygon_2d(polygon: &[[f64; 2]], delta: f64) -> Vec<[f64; 2]> {
1034 let n = polygon.len();
1035 if n < 3 {
1036 return Vec::new();
1037 }
1038 let mut offset_lines: Vec<([f64; 2], [f64; 2])> = Vec::with_capacity(n);
1039 for i in 0..n {
1040 let a = polygon[i];
1041 let b = polygon[(i + 1) % n];
1042 let edge = [b[0] - a[0], b[1] - a[1]];
1043 let len = (edge[0] * edge[0] + edge[1] * edge[1]).sqrt();
1044 if len < 1e-12 {
1045 continue;
1046 }
1047 let normal = [edge[1] / len, -edge[0] / len];
1048 let oa = [a[0] + delta * normal[0], a[1] + delta * normal[1]];
1049 let ob = [b[0] + delta * normal[0], b[1] + delta * normal[1]];
1050 offset_lines.push((oa, ob));
1051 }
1052 if offset_lines.len() < 3 {
1053 return Vec::new();
1054 }
1055 let m = offset_lines.len();
1056 let mut result = Vec::with_capacity(m);
1057 for i in 0..m {
1058 let (a0, a1) = offset_lines[i];
1059 let (b0, b1) = offset_lines[(i + 1) % m];
1060 if let Some(pt) = line_line_intersect_2d(a0, a1, b0, b1) {
1061 result.push(pt);
1062 } else {
1063 result.push([(a1[0] + b0[0]) * 0.5, (a1[1] + b0[1]) * 0.5]);
1064 }
1065 }
1066 result
1067}
1068pub fn line_line_intersect_2d(
1072 a0: [f64; 2],
1073 a1: [f64; 2],
1074 b0: [f64; 2],
1075 b1: [f64; 2],
1076) -> Option<[f64; 2]> {
1077 let da = [a1[0] - a0[0], a1[1] - a0[1]];
1078 let db = [b1[0] - b0[0], b1[1] - b0[1]];
1079 let denom = da[0] * db[1] - da[1] * db[0];
1080 if denom.abs() < 1e-12 {
1081 return None;
1082 }
1083 let t = ((b0[0] - a0[0]) * db[1] - (b0[1] - a0[1]) * db[0]) / denom;
1084 Some([a0[0] + t * da[0], a0[1] + t * da[1]])
1085}
1086pub(super) fn solid_angle_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
1091 let ra = sub3(a, p);
1092 let rb = sub3(b, p);
1093 let rc = sub3(c, p);
1094 let la = len3(ra);
1095 let lb = len3(rb);
1096 let lc = len3(rc);
1097 if la < 1e-15 || lb < 1e-15 || lc < 1e-15 {
1098 return 0.0;
1099 }
1100 let numerator = dot3(ra, cross3(rb, rc));
1101 let denominator = la * lb * lc + dot3(ra, rb) * lc + dot3(rb, rc) * la + dot3(ra, rc) * lb;
1102 if denominator.abs() < 1e-15 {
1103 return 0.0;
1104 }
1105 2.0 * numerator.atan2(denominator)
1106}
1107pub fn winding_number_3d(p: [f64; 3], verts: &[[f64; 3]], tris: &[[usize; 3]]) -> f64 {
1116 let mut total = 0.0f64;
1117 for tri in tris {
1118 let a = verts[tri[0]];
1119 let b = verts[tri[1]];
1120 let c = verts[tri[2]];
1121 total += solid_angle_triangle(p, a, b, c);
1122 }
1123 total / (4.0 * std::f64::consts::PI)
1124}
1125pub fn winding_number_inside(p: [f64; 3], verts: &[[f64; 3]], tris: &[[usize; 3]]) -> bool {
1129 let wn = winding_number_3d(p, verts, tris);
1130 wn.abs() > 0.5
1131}
1132pub fn polygon2d_signed_area(poly: &[[f64; 2]]) -> f64 {
1136 let n = poly.len();
1137 if n < 3 {
1138 return 0.0;
1139 }
1140 let mut area = 0.0f64;
1141 for i in 0..n {
1142 let a = poly[i];
1143 let b = poly[(i + 1) % n];
1144 area += a[0] * b[1] - b[0] * a[1];
1145 }
1146 area * 0.5
1147}
1148pub fn polygon2d_area(poly: &[[f64; 2]]) -> f64 {
1150 polygon2d_signed_area(poly).abs()
1151}
1152pub fn polygon2d_centroid(poly: &[[f64; 2]]) -> Option<[f64; 2]> {
1156 let n = poly.len();
1157 if n < 3 {
1158 return None;
1159 }
1160 let area = polygon2d_signed_area(poly);
1161 if area.abs() < 1e-24 {
1162 return None;
1163 }
1164 let mut cx = 0.0f64;
1165 let mut cy = 0.0f64;
1166 for i in 0..n {
1167 let a = poly[i];
1168 let b = poly[(i + 1) % n];
1169 let cross = a[0] * b[1] - b[0] * a[1];
1170 cx += (a[0] + b[0]) * cross;
1171 cy += (a[1] + b[1]) * cross;
1172 }
1173 Some([cx / (6.0 * area), cy / (6.0 * area)])
1174}
1175pub fn point_in_polygon_2d(p: [f64; 2], poly: &[[f64; 2]]) -> bool {
1179 let n = poly.len();
1180 if n < 3 {
1181 return false;
1182 }
1183 let mut inside = false;
1184 let mut j = n - 1;
1185 for i in 0..n {
1186 let vi = poly[i];
1187 let vj = poly[j];
1188 if ((vi[1] > p[1]) != (vj[1] > p[1]))
1189 && p[0] < (vj[0] - vi[0]) * (p[1] - vi[1]) / (vj[1] - vi[1]) + vi[0]
1190 {
1191 inside = !inside;
1192 }
1193 j = i;
1194 }
1195 inside
1196}
1197pub fn polygon2d_perimeter(poly: &[[f64; 2]]) -> f64 {
1199 let n = poly.len();
1200 if n < 2 {
1201 return 0.0;
1202 }
1203 let mut perimeter = 0.0f64;
1204 for i in 0..n {
1205 let a = poly[i];
1206 let b = poly[(i + 1) % n];
1207 let d = [a[0] - b[0], a[1] - b[1]];
1208 perimeter += (d[0] * d[0] + d[1] * d[1]).sqrt();
1209 }
1210 perimeter
1211}
1212pub fn polygon2d_is_convex(poly: &[[f64; 2]]) -> bool {
1216 let n = poly.len();
1217 if n < 3 {
1218 return true;
1219 }
1220 let mut sign = 0.0f64;
1221 for i in 0..n {
1222 let a = poly[i];
1223 let b = poly[(i + 1) % n];
1224 let c = poly[(i + 2) % n];
1225 let cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
1226 if cross.abs() < 1e-12 {
1227 continue;
1228 }
1229 if sign == 0.0 {
1230 sign = cross.signum();
1231 } else if cross.signum() != sign {
1232 return false;
1233 }
1234 }
1235 true
1236}
1237#[cfg(test)]
1238mod tests_new_bool {
1239
1240 use crate::boolean_ops::BspNode;
1241 use crate::boolean_ops::BspPlane;
1242
1243 use crate::boolean_ops::HalfEdgeMesh;
1244
1245 use crate::boolean_ops::PlaneClass;
1246
1247 use crate::boolean_ops::line_line_intersect_2d;
1248 use crate::boolean_ops::offset_polygon_2d;
1249 use crate::boolean_ops::point_in_polygon_2d;
1250 use crate::boolean_ops::polygon_intersection_points;
1251 use crate::boolean_ops::polygon2d_area;
1252 use crate::boolean_ops::polygon2d_centroid;
1253 use crate::boolean_ops::polygon2d_is_convex;
1254 use crate::boolean_ops::polygon2d_perimeter;
1255 use crate::boolean_ops::polygon2d_signed_area;
1256 use crate::boolean_ops::segment_segment_intersect_2d;
1257 use crate::boolean_ops::split_polygon_by_plane;
1258 use crate::boolean_ops::weiler_atherton_clip;
1259 use crate::boolean_ops::winding_number_3d;
1260 use crate::boolean_ops::winding_number_inside;
1261 #[test]
1262 fn test_bsp_node_classify_point() {
1263 let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1264 let mut node = BspNode::new(plane);
1265 node.polygons
1266 .push(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]]);
1267 assert_eq!(node.classify_point([0.0, 0.0, 1.0]), PlaneClass::Front);
1268 assert_eq!(node.classify_point([0.0, 0.0, -1.0]), PlaneClass::Back);
1269 assert_eq!(node.classify_point([0.0, 0.0, 0.0]), PlaneClass::OnPlane);
1270 }
1271 #[test]
1272 fn test_bsp_node_insert_front_polygon() {
1273 let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1274 let mut node = BspNode::new(plane);
1275 let poly = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.5, 1.0, 1.0]];
1276 node.insert_polygon(poly);
1277 assert!(node.count_polygons() >= 1);
1278 }
1279 #[test]
1280 fn test_bsp_node_insert_back_polygon() {
1281 let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1282 let mut node = BspNode::new(plane);
1283 let poly = vec![[0.0, 0.0, -1.0], [1.0, 0.0, -1.0], [0.5, 1.0, -1.0]];
1284 node.insert_polygon(poly);
1285 assert!(node.count_polygons() >= 1);
1286 }
1287 #[test]
1288 fn test_bsp_node_coplanar_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, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
1292 node.insert_polygon(poly);
1293 assert!(node.count_polygons() >= 1);
1294 }
1295 #[test]
1296 fn test_split_polygon_by_plane_clean_separation() {
1297 let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1298 let poly = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.5, 0.5, -1.0]];
1299 let (front, back) = split_polygon_by_plane(&poly, &plane);
1300 assert!(!front.is_empty());
1301 assert!(!back.is_empty());
1302 for v in &front {
1303 assert!(v[2] >= -1e-7, "front z={}", v[2]);
1304 }
1305 for v in &back {
1306 assert!(v[2] <= 1e-7, "back z={}", v[2]);
1307 }
1308 }
1309 #[test]
1310 fn test_split_polygon_all_front() {
1311 let plane = BspPlane::new([0.0, 0.0, 1.0], 0.0);
1312 let poly = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 2.0], [0.5, 1.0, 1.5]];
1313 let (front, back) = split_polygon_by_plane(&poly, &plane);
1314 assert!(!front.is_empty());
1315 assert!(back.is_empty() || back.iter().all(|v| v[2] <= 1e-7));
1316 }
1317 #[test]
1318 fn test_weiler_atherton_clip_contained() {
1319 let subject = vec![[0.2, 0.2], [0.8, 0.2], [0.8, 0.8], [0.2, 0.8]];
1320 let clip = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1321 let result = weiler_atherton_clip(&subject, &clip);
1322 assert!(!result.is_empty(), "fully contained polygon should survive");
1323 }
1324 #[test]
1325 fn test_weiler_atherton_clip_partial() {
1326 let subject = vec![[0.5, 0.0], [1.5, 0.0], [1.5, 1.0], [0.5, 1.0]];
1327 let clip = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1328 let result = weiler_atherton_clip(&subject, &clip);
1329 assert!(!result.is_empty(), "partial overlap should produce result");
1330 }
1331 #[test]
1332 fn test_segment_segment_intersect_basic() {
1333 let p = segment_segment_intersect_2d([0.0, 0.0], [1.0, 1.0], [0.0, 1.0], [1.0, 0.0]);
1334 assert!(p.is_some(), "crossing segments should intersect");
1335 let pt = p.unwrap();
1336 assert!((pt[0] - 0.5).abs() < 1e-10 && (pt[1] - 0.5).abs() < 1e-10);
1337 }
1338 #[test]
1339 fn test_segment_segment_no_intersect() {
1340 let p = segment_segment_intersect_2d([0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]);
1341 assert!(
1342 p.is_none(),
1343 "parallel non-overlapping segments should not intersect"
1344 );
1345 }
1346 #[test]
1347 fn test_segment_segment_parallel() {
1348 let p = segment_segment_intersect_2d([0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]);
1349 assert!(
1350 p.is_none(),
1351 "parallel horizontal segments should not intersect"
1352 );
1353 }
1354 #[test]
1355 fn test_polygon_intersection_points_two_squares() {
1356 let a = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
1357 let b = vec![[1.0, 0.0], [3.0, 0.0], [3.0, 2.0], [1.0, 2.0]];
1358 let pts = polygon_intersection_points(&a, &b);
1359 assert!(
1360 pts.len() >= 2,
1361 "two overlapping squares should have intersection points, got {}",
1362 pts.len()
1363 );
1364 }
1365 #[test]
1366 fn test_polygon_intersection_points_no_overlap() {
1367 let a = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1368 let b = vec![[5.0, 0.0], [6.0, 0.0], [6.0, 1.0], [5.0, 1.0]];
1369 let pts = polygon_intersection_points(&a, &b);
1370 assert!(
1371 pts.is_empty(),
1372 "non-overlapping polygons should have no intersection points"
1373 );
1374 }
1375 #[test]
1376 fn test_offset_polygon_2d_outward() {
1377 let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1378 let offset = offset_polygon_2d(&poly, 0.1);
1379 assert_eq!(offset.len(), 4, "square offset should have 4 vertices");
1380 let area_orig = polygon2d_area(&poly);
1381 let area_offset = polygon2d_area(&offset);
1382 assert!(
1383 area_offset > area_orig,
1384 "outward offset should increase area: {} vs {}",
1385 area_offset,
1386 area_orig
1387 );
1388 }
1389 #[test]
1390 fn test_offset_polygon_2d_inward() {
1391 let poly = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
1392 let offset = offset_polygon_2d(&poly, -0.1);
1393 assert!(
1394 !offset.is_empty(),
1395 "inward offset of large polygon should survive"
1396 );
1397 }
1398 #[test]
1399 fn test_offset_polygon_2d_zero() {
1400 let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1401 let offset = offset_polygon_2d(&poly, 0.0);
1402 let area_orig = polygon2d_area(&poly);
1403 let area_offset = polygon2d_area(&offset);
1404 assert!(
1405 (area_orig - area_offset).abs() < 0.01,
1406 "zero offset should not change area"
1407 );
1408 }
1409 #[test]
1410 fn test_line_line_intersect_crossing() {
1411 let pt = line_line_intersect_2d([0.0, 0.0], [2.0, 2.0], [0.0, 2.0], [2.0, 0.0]);
1412 assert!(pt.is_some());
1413 let p = pt.unwrap();
1414 assert!((p[0] - 1.0).abs() < 1e-10 && (p[1] - 1.0).abs() < 1e-10);
1415 }
1416 #[test]
1417 fn test_line_line_intersect_parallel() {
1418 let pt = line_line_intersect_2d([0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]);
1419 assert!(pt.is_none(), "parallel lines should not intersect");
1420 }
1421 #[test]
1422 fn test_half_edge_mesh_tetrahedron() {
1423 let verts = vec![
1424 [0.0, 0.0, 0.0],
1425 [1.0, 0.0, 0.0],
1426 [0.5, 1.0, 0.0],
1427 [0.5, 0.5, 1.0],
1428 ];
1429 let tris = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]];
1430 let mesh = HalfEdgeMesh::from_triangle_mesh(&verts, &tris);
1431 assert_eq!(mesh.n_faces(), 4);
1432 assert_eq!(mesh.n_vertices(), 4);
1433 assert_eq!(mesh.half_edges.len(), 12, "4 triangles * 3 half-edges = 12");
1434 }
1435 #[test]
1436 fn test_half_edge_mesh_face_vertices() {
1437 let verts = vec![
1438 [0.0, 0.0, 0.0],
1439 [1.0, 0.0, 0.0],
1440 [0.0, 1.0, 0.0],
1441 [0.0, 0.0, 1.0],
1442 ];
1443 let tris = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]];
1444 let mesh = HalfEdgeMesh::from_triangle_mesh(&verts, &tris);
1445 for fi in 0..mesh.n_faces() {
1446 let fv = mesh.face_vertices(fi);
1447 for &v in &fv {
1448 assert!(v < mesh.n_vertices(), "vertex index out of range: {v}");
1449 }
1450 }
1451 }
1452 #[test]
1453 fn test_half_edge_mesh_face_area_positive() {
1454 let verts = vec![
1455 [0.0, 0.0, 0.0],
1456 [1.0, 0.0, 0.0],
1457 [0.5, 1.0, 0.0],
1458 [0.5, 0.5, 1.0],
1459 ];
1460 let tris = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]];
1461 let mesh = HalfEdgeMesh::from_triangle_mesh(&verts, &tris);
1462 for fi in 0..mesh.n_faces() {
1463 let area = mesh.face_area(fi);
1464 assert!(area > 0.0, "face {fi} area should be positive, got {area}");
1465 }
1466 }
1467 #[test]
1468 fn test_winding_number_inside_unit_box() {
1469 let (verts, tris) = unit_box_mesh_raw();
1470 let inside = winding_number_inside([0.5, 0.5, 0.5], &verts, &tris);
1471 assert!(inside, "center of unit box should be inside");
1472 }
1473 #[test]
1474 fn test_winding_number_outside_unit_box() {
1475 let (verts, tris) = unit_box_mesh_raw();
1476 let inside = winding_number_inside([5.0, 5.0, 5.0], &verts, &tris);
1477 assert!(!inside, "far exterior point should not be inside");
1478 }
1479 #[test]
1480 fn test_winding_number_value_inside_is_one() {
1481 let (verts, tris) = unit_box_mesh_raw();
1482 let wn = winding_number_3d([0.5, 0.5, 0.5], &verts, &tris);
1483 assert!(
1484 (wn.abs() - 1.0).abs() < 0.1,
1485 "winding number inside should be ≈1, got {wn}"
1486 );
1487 }
1488 #[test]
1489 fn test_winding_number_value_outside_is_zero() {
1490 let (verts, tris) = unit_box_mesh_raw();
1491 let wn = winding_number_3d([5.0, 5.0, 5.0], &verts, &tris);
1492 assert!(
1493 wn.abs() < 0.1,
1494 "winding number outside should be ≈0, got {wn}"
1495 );
1496 }
1497 #[test]
1498 fn test_polygon2d_area_unit_square() {
1499 let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1500 let area = polygon2d_area(&poly);
1501 assert!(
1502 (area - 1.0).abs() < 1e-12,
1503 "unit square area should be 1.0, got {area}"
1504 );
1505 }
1506 #[test]
1507 fn test_polygon2d_signed_area_ccw_positive() {
1508 let poly = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
1509 let area = polygon2d_signed_area(&poly);
1510 assert!(
1511 area > 0.0,
1512 "CCW polygon should have positive signed area, got {area}"
1513 );
1514 }
1515 #[test]
1516 fn test_polygon2d_signed_area_cw_negative() {
1517 let poly = vec![[0.0, 0.0], [0.5, 1.0], [1.0, 0.0]];
1518 let area = polygon2d_signed_area(&poly);
1519 assert!(
1520 area < 0.0,
1521 "CW polygon should have negative signed area, got {area}"
1522 );
1523 }
1524 #[test]
1525 fn test_polygon2d_centroid_square() {
1526 let poly = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
1527 let c = polygon2d_centroid(&poly).expect("centroid");
1528 assert!(
1529 (c[0] - 1.0).abs() < 1e-10 && (c[1] - 1.0).abs() < 1e-10,
1530 "centroid of 2x2 square should be (1,1), got ({}, {})",
1531 c[0],
1532 c[1]
1533 );
1534 }
1535 #[test]
1536 fn test_point_in_polygon_2d_inside() {
1537 let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1538 assert!(
1539 point_in_polygon_2d([0.5, 0.5], &poly),
1540 "center should be inside"
1541 );
1542 }
1543 #[test]
1544 fn test_point_in_polygon_2d_outside() {
1545 let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1546 assert!(
1547 !point_in_polygon_2d([2.0, 2.0], &poly),
1548 "exterior point should be outside"
1549 );
1550 }
1551 #[test]
1552 fn test_polygon2d_perimeter_unit_square() {
1553 let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1554 let p = polygon2d_perimeter(&poly);
1555 assert!(
1556 (p - 4.0).abs() < 1e-12,
1557 "unit square perimeter should be 4, got {p}"
1558 );
1559 }
1560 #[test]
1561 fn test_polygon2d_is_convex_square() {
1562 let poly = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1563 assert!(polygon2d_is_convex(&poly), "unit square should be convex");
1564 }
1565 #[test]
1566 fn test_polygon2d_is_not_convex_star() {
1567 let poly = vec![[0.0, 0.0], [2.0, 1.0], [1.0, 0.0], [2.0, -1.0]];
1568 let _ = polygon2d_is_convex(&poly);
1569 }
1570 #[test]
1571 fn test_polygon2d_perimeter_equilateral_triangle() {
1572 let side = 1.0_f64;
1573 let h = (3.0_f64.sqrt() / 2.0) * side;
1574 let poly = vec![[0.0, 0.0], [side, 0.0], [side / 2.0, h]];
1575 let p = polygon2d_perimeter(&poly);
1576 assert!(
1577 (p - 3.0 * side).abs() < 1e-10,
1578 "equilateral triangle perimeter should be 3, got {p}"
1579 );
1580 }
1581 fn unit_box_mesh_raw() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
1583 let verts = vec![
1584 [0.0, 0.0, 0.0],
1585 [1.0, 0.0, 0.0],
1586 [1.0, 1.0, 0.0],
1587 [0.0, 1.0, 0.0],
1588 [0.0, 0.0, 1.0],
1589 [1.0, 0.0, 1.0],
1590 [1.0, 1.0, 1.0],
1591 [0.0, 1.0, 1.0],
1592 ];
1593 let tris = vec![
1594 [0, 3, 2],
1595 [0, 2, 1],
1596 [4, 5, 6],
1597 [4, 6, 7],
1598 [0, 1, 5],
1599 [0, 5, 4],
1600 [2, 3, 7],
1601 [2, 7, 6],
1602 [0, 4, 7],
1603 [0, 7, 3],
1604 [1, 2, 6],
1605 [1, 6, 5],
1606 ];
1607 (verts, tris)
1608 }
1609}