1#[derive(Copy, Clone, Debug, Default, PartialEq)]
32pub struct Point2D {
33 pub x: f64,
34 pub y: f64,
35}
36
37impl Point2D {
38 #[inline]
39 pub const fn new(x: f64, y: f64) -> Self {
40 Self { x, y }
41 }
42}
43
44#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
49pub enum WindingRule {
50 #[default]
51 Positive = 0,
52 NonZero = 1,
53 EvenOdd = 2,
54}
55
56impl WindingRule {
57 #[inline]
58 pub const fn code(self) -> u32 {
59 self as u32
60 }
61
62 #[inline]
63 pub fn apply(self, w: f64) -> bool {
64 match self {
65 WindingRule::Positive => w >= 0.5,
66 WindingRule::NonZero => w.abs() >= 0.5,
67 WindingRule::EvenOdd => (w.round() as i64).abs() % 2 == 1,
68 }
69 }
70}
71
72#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
74pub enum BooleanOp {
75 Union = 0,
77 Intersect = 1,
79 Difference = 2,
81 Xor = 3,
83}
84
85impl BooleanOp {
86 #[inline]
87 pub const fn code(self) -> u32 {
88 self as u32
89 }
90}
91
92#[derive(Copy, Clone, Debug, PartialEq)]
94pub struct KernelOptions {
95 pub snap_tolerance: Option<f64>,
99 pub winding_rule: WindingRule,
102 pub simplify_collinear: bool,
105}
106
107impl Default for KernelOptions {
108 #[inline]
109 fn default() -> Self {
110 Self {
111 snap_tolerance: None,
112 winding_rule: WindingRule::Positive,
113 simplify_collinear: true,
114 }
115 }
116}
117
118#[derive(Copy, Clone, Debug, Default, PartialEq)]
120pub struct Point3D {
121 pub x: f64,
122 pub y: f64,
123 pub z: f64,
124}
125
126impl Point3D {
127 #[inline]
128 pub const fn new(x: f64, y: f64, z: f64) -> Self {
129 Self { x, y, z }
130 }
131
132 #[inline]
133 pub fn sub(self, other: Self) -> Self {
134 Self {
135 x: self.x - other.x,
136 y: self.y - other.y,
137 z: self.z - other.z,
138 }
139 }
140
141 #[inline]
142 pub fn add(self, other: Self) -> Self {
143 Self {
144 x: self.x + other.x,
145 y: self.y + other.y,
146 z: self.z + other.z,
147 }
148 }
149
150 #[inline]
151 pub fn scale(self, s: f64) -> Self {
152 Self {
153 x: self.x * s,
154 y: self.y * s,
155 z: self.z * s,
156 }
157 }
158
159 #[inline]
160 pub fn dot(self, other: Self) -> f64 {
161 self.x * other.x + self.y * other.y + self.z * other.z
162 }
163
164 #[inline]
165 pub fn cross(self, other: Self) -> Self {
166 Self {
167 x: self.y * other.z - self.z * other.y,
168 y: self.z * other.x - self.x * other.z,
169 z: self.x * other.y - self.y * other.x,
170 }
171 }
172
173 #[inline]
174 pub fn norm_sq(self) -> f64 {
175 self.dot(self)
176 }
177
178 #[inline]
179 pub fn norm(self) -> f64 {
180 self.norm_sq().sqrt()
181 }
182
183 #[inline]
184 pub fn normalized(self) -> Option<Self> {
185 let len = self.norm();
186 if len <= 1e-15 {
187 None
188 } else {
189 Some(self.scale(1.0 / len))
190 }
191 }
192}
193
194impl From<[f64; 3]> for Point3D {
195 #[inline]
196 fn from(arr: [f64; 3]) -> Self {
197 Self {
198 x: arr[0],
199 y: arr[1],
200 z: arr[2],
201 }
202 }
203}
204
205impl From<Point3D> for [f64; 3] {
206 #[inline]
207 fn from(p: Point3D) -> Self {
208 [p.x, p.y, p.z]
209 }
210}
211
212#[derive(Clone, Debug, Default, PartialEq)]
215pub struct Face3D {
216 pub outer: Vec<Point3D>,
217 pub holes: Vec<Vec<Point3D>>,
218 pub normal: Point3D,
219}
220
221impl Face3D {
222 pub fn from_outer<P: Into<Point3D> + Copy>(outer_pts: &[P]) -> Self {
225 let outer: Vec<Point3D> = outer_pts.iter().copied().map(Into::into).collect();
226 let normal = compute_newell_normal(&outer).unwrap_or(Point3D::new(0.0, 0.0, 1.0));
227 Self {
228 outer,
229 holes: Vec::new(),
230 normal,
231 }
232 }
233
234 pub fn new(outer: Vec<Point3D>, holes: Vec<Vec<Point3D>>) -> Self {
236 let normal = compute_newell_normal(&outer).unwrap_or(Point3D::new(0.0, 0.0, 1.0));
237 Self {
238 outer,
239 holes,
240 normal,
241 }
242 }
243
244 pub fn area(&self) -> f64 {
246 let mut total = loop_area_along_normal(&self.outer, self.normal);
247 for hole in &self.holes {
248 total += loop_area_along_normal(hole, self.normal);
249 }
250 total.abs()
251 }
252}
253
254#[derive(Clone, Debug, Default, PartialEq)]
256pub struct Solid3D {
257 pub faces: Vec<Face3D>,
258}
259
260impl Solid3D {
261 pub fn new(faces: Vec<Face3D>) -> Self {
262 Self { faces }
263 }
264
265 pub fn from_box(min: [f64; 3], max: [f64; 3]) -> Self {
267 let (x0, y0, z0) = (min[0], min[1], min[2]);
268 let (x1, y1, z1) = (max[0], max[1], max[2]);
269 let p000 = Point3D::new(x0, y0, z0);
270 let p100 = Point3D::new(x1, y0, z0);
271 let p110 = Point3D::new(x1, y1, z0);
272 let p010 = Point3D::new(x0, y1, z0);
273 let p001 = Point3D::new(x0, y0, z1);
274 let p101 = Point3D::new(x1, y0, z1);
275 let p111 = Point3D::new(x1, y1, z1);
276 let p011 = Point3D::new(x0, y1, z1);
277
278 let faces = vec![
279 Face3D::from_outer(&[p000, p010, p110, p100]),
281 Face3D::from_outer(&[p001, p101, p111, p011]),
283 Face3D::from_outer(&[p000, p100, p101, p001]),
285 Face3D::from_outer(&[p110, p010, p011, p111]),
287 Face3D::from_outer(&[p010, p000, p001, p011]),
289 Face3D::from_outer(&[p100, p110, p111, p101]),
291 ];
292 Self { faces }
293 }
294
295 pub fn from_extruded_polygon(
297 outer_xy: &[[f64; 2]],
298 holes_xy: &[Vec<[f64; 2]>],
299 z_min: f64,
300 z_max: f64,
301 ) -> Self {
302 let mut faces = Vec::new();
303 if outer_xy.len() < 3 || z_max <= z_min {
304 return Self { faces };
305 }
306
307 let mut outer_ccw: Vec<[f64; 2]> = outer_xy.to_vec();
309 if signed_area_2d_arr(&outer_ccw) < 0.0 {
310 outer_ccw.reverse();
311 }
312
313 let mut holes_cw: Vec<Vec<[f64; 2]>> = Vec::with_capacity(holes_xy.len());
314 for h in holes_xy {
315 if h.len() < 3 {
316 continue;
317 }
318 let mut h_cw = h.clone();
319 if signed_area_2d_arr(&h_cw) > 0.0 {
320 h_cw.reverse();
321 }
322 holes_cw.push(h_cw);
323 }
324
325 let top_outer: Vec<Point3D> = outer_ccw
327 .iter()
328 .map(|p| Point3D::new(p[0], p[1], z_max))
329 .collect();
330 let top_holes: Vec<Vec<Point3D>> = holes_cw
331 .iter()
332 .map(|h| h.iter().map(|p| Point3D::new(p[0], p[1], z_max)).collect())
333 .collect();
334 faces.push(Face3D {
335 outer: top_outer,
336 holes: top_holes,
337 normal: Point3D::new(0.0, 0.0, 1.0),
338 });
339
340 let bot_outer: Vec<Point3D> = outer_ccw
342 .iter()
343 .rev()
344 .map(|p| Point3D::new(p[0], p[1], z_min))
345 .collect();
346 let bot_holes: Vec<Vec<Point3D>> = holes_cw
347 .iter()
348 .map(|h| {
349 h.iter()
350 .rev()
351 .map(|p| Point3D::new(p[0], p[1], z_min))
352 .collect()
353 })
354 .collect();
355 faces.push(Face3D {
356 outer: bot_outer,
357 holes: bot_holes,
358 normal: Point3D::new(0.0, 0.0, -1.0),
359 });
360
361 append_extrusion_walls(&outer_ccw, z_min, z_max, &mut faces);
363 for h_cw in &holes_cw {
365 append_extrusion_walls(h_cw, z_min, z_max, &mut faces);
366 }
367
368 Self { faces }
369 }
370
371 pub fn volume(&self) -> f64 {
374 let mut vol6 = 0.0;
375 for face in &self.faces {
376 if let Some(&p0) = face.outer.first() {
377 let n = face.outer.len();
378 for i in 1..n.saturating_sub(1) {
379 vol6 += p0.dot(face.outer[i].cross(face.outer[i + 1]));
380 }
381 for hole in &face.holes {
382 let hn = hole.len();
383 for i in 0..hn {
384 vol6 += p0.dot(hole[i].cross(hole[(i + 1) % hn]));
385 }
386 }
387 }
388 }
389 vol6 / 6.0
390 }
391
392 pub fn surface_area(&self) -> f64 {
394 self.faces.iter().map(Face3D::area).sum()
395 }
396}
397
398fn append_extrusion_walls(ring: &[[f64; 2]], z_min: f64, z_max: f64, faces: &mut Vec<Face3D>) {
399 let n = ring.len();
400 for i in 0..n {
401 let a = ring[i];
402 let b = ring[(i + 1) % n];
403 if (b[0] - a[0]).hypot(b[1] - a[1]) <= 1e-12 {
404 continue;
405 }
406 let p0 = Point3D::new(a[0], a[1], z_min);
407 let p1 = Point3D::new(b[0], b[1], z_min);
408 let p2 = Point3D::new(b[0], b[1], z_max);
409 let p3 = Point3D::new(a[0], a[1], z_max);
410 faces.push(Face3D::from_outer(&[p0, p1, p2, p3]));
411 }
412}
413
414#[derive(Clone, Debug, Default, PartialEq)]
416pub struct BRepResult3D {
417 pub solid: Solid3D,
418 pub total_volume: f64,
419 pub total_surface_area: f64,
420 pub vertex_count: usize,
422 pub edge_count: usize,
424 pub unmatched_half_edge_count: usize,
426}
427
428#[inline]
429fn signed_area_2d_arr(pts: &[[f64; 2]]) -> f64 {
430 let n = pts.len();
431 if n < 3 {
432 return 0.0;
433 }
434 let mut sum = 0.0;
435 for i in 0..n {
436 let a = pts[i];
437 let b = pts[(i + 1) % n];
438 sum += a[0] * b[1] - b[0] * a[1];
439 }
440 0.5 * sum
441}
442
443#[inline]
444fn signed_area_2d_pts(pts: &[Point2D]) -> f64 {
445 let n = pts.len();
446 if n < 3 {
447 return 0.0;
448 }
449 let mut sum = 0.0;
450 for i in 0..n {
451 let a = pts[i];
452 let b = pts[(i + 1) % n];
453 sum += a.x * b.y - b.x * a.y;
454 }
455 0.5 * sum
456}
457
458pub fn compute_newell_normal(pts: &[Point3D]) -> Option<Point3D> {
460 let n = pts.len();
461 if n < 3 {
462 return None;
463 }
464 let mut nx = 0.0;
465 let mut ny = 0.0;
466 let mut nz = 0.0;
467 for i in 0..n {
468 let cur = pts[i];
469 let nxt = pts[(i + 1) % n];
470 nx += (cur.y - nxt.y) * (cur.z + nxt.z);
471 ny += (cur.z - nxt.z) * (cur.x + nxt.x);
472 nz += (cur.x - nxt.x) * (cur.y + nxt.y);
473 }
474 Point3D::new(nx, ny, nz).normalized()
475}
476
477fn loop_area_along_normal(pts: &[Point3D], normal: Point3D) -> f64 {
478 let n = pts.len();
479 if n < 3 {
480 return 0.0;
481 }
482 let p0 = pts[0];
483 let mut sum = Point3D::default();
484 for i in 1..(n - 1) {
485 let u = pts[i].sub(p0);
486 let v = pts[i + 1].sub(p0);
487 sum = sum.add(u.cross(v));
488 }
489 0.5 * sum.dot(normal)
490}
491
492fn canonicalize_plane(n: Point3D, d: f64) -> (Point3D, f64, i32) {
495 let ax = n.x.abs();
496 let ay = n.y.abs();
497 let az = n.z.abs();
498 let sign = if az >= ax && az >= ay {
499 if n.z >= 0.0 {
500 1
501 } else {
502 -1
503 }
504 } else if ay >= ax {
505 if n.y >= 0.0 {
506 1
507 } else {
508 -1
509 }
510 } else if n.x >= 0.0 {
511 1
512 } else {
513 -1
514 };
515
516 if sign == 1 {
517 (n, d, 1)
518 } else {
519 (n.scale(-1.0), -d, -1)
520 }
521}
522
523fn build_plane_frame(n: Point3D) -> (Point3D, Point3D) {
525 let helper = if n.x.abs() < 0.8 {
526 Point3D::new(1.0, 0.0, 0.0)
527 } else {
528 Point3D::new(0.0, 1.0, 0.0)
529 };
530 let u = n.cross(helper).normalized().unwrap();
531 let v = n.cross(u).normalized().unwrap();
532 (u, v)
533}
534
535struct CanonicalPlane {
536 origin: Point3D,
537 u_axis: Point3D,
538 v_axis: Point3D,
539 normal: Point3D,
540 d: f64,
541 min_pt: Point3D,
542 max_pt: Point3D,
543}
544
545impl CanonicalPlane {
546 #[inline]
547 fn project_2d(&self, p: Point3D) -> Point2D {
548 let rel = p.sub(self.origin);
549 Point2D {
550 x: rel.dot(self.u_axis),
551 y: rel.dot(self.v_axis),
552 }
553 }
554
555 #[inline]
556 fn unproject_3d(&self, u: f64, v: f64) -> Point3D {
557 self.origin
558 .add(self.u_axis.scale(u))
559 .add(self.v_axis.scale(v))
560 }
561}
562
563struct NormalizedInputFace {
564 body_index: usize,
565 plane_id: usize,
566 orient_sign: i32,
567 area_2d: f64,
568 loops_2d: Vec<Vec<Point2D>>,
569 loops_3d: Vec<Vec<Point3D>>,
570 min_pt: Point3D,
571 max_pt: Point3D,
572}
573
574#[derive(Clone, Copy, Debug)]
575struct IdxPoint2D {
576 x: f64,
577 y: f64,
578 id: u32,
579}
580
581impl IdxPoint2D {
582 #[inline]
583 fn pt(self) -> Point2D {
584 Point2D {
585 x: self.x,
586 y: self.y,
587 }
588 }
589}
590
591fn clean_indexed_ring_2d(pts: &[IdxPoint2D]) -> Vec<IdxPoint2D> {
592 let mut out: Vec<IdxPoint2D> = Vec::with_capacity(pts.len());
593 for &p in pts {
594 if let Some(&last) = out.last() {
595 if p.id == last.id || (p.x - last.x).hypot(p.y - last.y) <= 1e-11 {
596 continue;
597 }
598 }
599 out.push(p);
600 }
601 while out.len() >= 2 {
602 let a = out[0];
603 let b = *out.last().unwrap();
604 if a.id == b.id || (a.x - b.x).hypot(a.y - b.y) <= 1e-11 {
605 out.pop();
606 } else {
607 break;
608 }
609 }
610
611 let mut changed = true;
615 while changed && out.len() >= 3 {
616 changed = false;
617 let n = out.len();
618 let mut next_ring: Vec<IdxPoint2D> = Vec::with_capacity(n);
619 for i in 0..n {
620 let prev = if let Some(&last) = next_ring.last() {
621 last
622 } else {
623 out[(i + n - 1) % n]
624 };
625 let curr = out[i];
626 let next = out[(i + 1) % n];
627 if curr.id == prev.id || (curr.x - prev.x).hypot(curr.y - prev.y) <= 1e-11 {
628 changed = true;
629 continue;
630 }
631 if prev.id == next.id || (prev.x - next.x).hypot(prev.y - next.y) <= 1e-11 {
632 changed = true;
633 continue;
634 }
635 next_ring.push(curr);
636 }
637 out = next_ring;
638 }
639 out
640}
641
642#[inline]
643fn signed_area_2d_idx_pts(ring: &[IdxPoint2D]) -> f64 {
644 let n = ring.len();
645 if n < 3 {
646 return 0.0;
647 }
648 let mut s = 0.0;
649 for i in 0..n {
650 let a = ring[i];
651 let b = ring[(i + 1) % n];
652 s += a.x * b.y - b.x * a.y;
653 }
654 0.5 * s
655}
656
657#[inline]
659fn point_in_vertex_cone_2d(prev: Point2D, v: Point2D, next: Point2D, m: Point2D) -> bool {
660 let turn = cross_2d(prev, v, next);
661 let c_prev = cross_2d(prev, v, m);
662 let c_next = cross_2d(v, next, m);
663 if turn >= -1e-12 {
664 c_prev >= -1e-11 && c_next >= -1e-11
666 } else {
667 c_prev >= -1e-11 || c_next >= -1e-11
669 }
670}
671
672fn decompose_pinched_indexed_ring_2d(ring: &[IdxPoint2D]) -> Vec<Vec<IdxPoint2D>> {
675 if ring.len() < 3 {
676 return Vec::new();
677 }
678 let mut stack: Vec<IdxPoint2D> = Vec::with_capacity(ring.len());
679 let mut sub_loops: Vec<Vec<IdxPoint2D>> = Vec::new();
680
681 for &pt in ring {
682 if let Some(pos) = stack
683 .iter()
684 .position(|p| p.id == pt.id || (p.x - pt.x).hypot(p.y - pt.y) <= 1e-11)
685 {
686 if stack.len() - pos >= 3 {
687 let sub = clean_indexed_ring_2d(&stack[pos..]);
688 if sub.len() >= 3 {
689 sub_loops.push(sub);
690 }
691 }
692 stack.truncate(pos + 1);
693 } else {
694 stack.push(pt);
695 }
696 }
697
698 if stack.len() >= 3 {
699 let sub = clean_indexed_ring_2d(&stack);
700 if sub.len() >= 3 {
701 sub_loops.push(sub);
702 }
703 }
704
705 sub_loops
706}
707
708fn triangulate_indexed_polygon_2d(
711 outer: &[IdxPoint2D],
712 holes: &[Vec<IdxPoint2D>],
713) -> Vec<[u32; 3]> {
714 let ring = clean_indexed_ring_2d(outer);
715 if ring.len() < 3 {
716 return Vec::new();
717 }
718
719 let mut simple_shells = decompose_pinched_indexed_ring_2d(&ring);
720 if simple_shells.is_empty() {
721 return Vec::new();
722 }
723
724 if !holes.is_empty() {
725 let mut sorted_holes: Vec<Vec<IdxPoint2D>> = Vec::new();
726 for h in holes {
727 let ch = clean_indexed_ring_2d(h);
728 if ch.len() < 3 {
729 continue;
730 }
731 sorted_holes.push(ch);
732 }
733
734 sorted_holes.sort_by(|a, b| {
735 let max_xa = a.iter().fold(f64::NEG_INFINITY, |acc, p| acc.max(p.x));
736 let max_xb = b.iter().fold(f64::NEG_INFINITY, |acc, p| acc.max(p.x));
737 max_xb
738 .partial_cmp(&max_xa)
739 .unwrap_or(core::cmp::Ordering::Equal)
740 });
741
742 for hole in sorted_holes {
743 let mut target_idx = 0usize;
744 if simple_shells.len() > 1 {
745 let test_pt = hole[0].pt();
746 let mut best_area = f64::INFINITY;
747 for (s_idx, sh) in simple_shells.iter().enumerate() {
748 let sh_pts: Vec<Point2D> = sh.iter().map(|p| p.pt()).collect();
749 let s_area = signed_area_2d_pts(&sh_pts).abs();
750 if loops_winding_2d(test_pt, core::slice::from_ref(&sh_pts)) != 0
751 && s_area < best_area
752 {
753 best_area = s_area;
754 target_idx = s_idx;
755 }
756 }
757 }
758 splice_indexed_hole_into_outer(&mut simple_shells[target_idx], &hole);
759 }
760 }
761
762 let mut all_tris = Vec::new();
763 for sh in &simple_shells {
764 all_tris.extend(ear_clip_indexed_ring(sh));
765 }
766 all_tris
767}
768
769fn triangulate_polygon_2d(outer: &[Point2D], holes: &[Vec<Point2D>]) -> Vec<[Point2D; 3]> {
771 let mut all_pts: Vec<Point2D> = Vec::new();
772 let mut idx_outer: Vec<IdxPoint2D> = outer
773 .iter()
774 .map(|&p| {
775 let id = all_pts.len() as u32;
776 all_pts.push(p);
777 IdxPoint2D { x: p.x, y: p.y, id }
778 })
779 .collect();
780 if signed_area_2d_idx_pts(&idx_outer) < 0.0 {
781 idx_outer.reverse();
782 }
783 let idx_holes: Vec<Vec<IdxPoint2D>> = holes
784 .iter()
785 .map(|h| {
786 let mut ih: Vec<IdxPoint2D> = h
787 .iter()
788 .map(|&p| {
789 let id = all_pts.len() as u32;
790 all_pts.push(p);
791 IdxPoint2D { x: p.x, y: p.y, id }
792 })
793 .collect();
794 if signed_area_2d_idx_pts(&ih) > 0.0 {
795 ih.reverse();
796 }
797 ih
798 })
799 .collect();
800
801 triangulate_indexed_polygon_2d(&idx_outer, &idx_holes)
802 .into_iter()
803 .map(|[a, b, c]| [all_pts[a as usize], all_pts[b as usize], all_pts[c as usize]])
804 .collect()
805}
806
807fn splice_indexed_hole_into_outer(outer: &mut Vec<IdxPoint2D>, hole: &[IdxPoint2D]) {
808 if hole.is_empty() || outer.len() < 3 {
809 return;
810 }
811
812 for (hi, &hp) in hole.iter().enumerate() {
816 let h_next = hole[(hi + 1) % hole.len()].pt();
817 let mut best_oi = None;
818 for (oi, &op) in outer.iter().enumerate() {
819 if hp.id == op.id || (hp.x - op.x).hypot(hp.y - op.y) <= 1e-11 {
820 best_oi = Some(oi);
821 let prev = outer[(oi + outer.len() - 1) % outer.len()].pt();
822 let next = outer[(oi + 1) % outer.len()].pt();
823 if point_in_vertex_cone_2d(prev, op.pt(), next, h_next) {
824 break;
825 }
826 }
827 }
828 if let Some(oi) = best_oi {
829 let mut combined = Vec::with_capacity(outer.len() + hole.len() + 1);
830 combined.extend_from_slice(&outer[..=oi]);
831 for k in 1..=hole.len() {
832 combined.push(hole[(hi + k) % hole.len()]);
833 }
834 if oi + 1 < outer.len() {
835 combined.extend_from_slice(&outer[(oi + 1)..]);
836 }
837 *outer = combined;
838 return;
839 }
840 }
841
842 let mut hm_idx = 0usize;
843 for i in 1..hole.len() {
844 if hole[i].x > hole[hm_idx].x
845 || ((hole[i].x - hole[hm_idx].x).abs() <= 1e-12 && hole[i].y < hole[hm_idx].y)
846 {
847 hm_idx = i;
848 }
849 }
850 let m = hole[hm_idx];
851
852 let n = outer.len();
854 let mut best_t = f64::INFINITY;
855 let mut cand_idx = 0usize;
856
857 for i in 0..n {
858 let a = outer[i];
859 let b = outer[(i + 1) % n];
860 if !(a.y <= m.y && b.y > m.y) && !(a.y < m.y && b.y >= m.y) {
861 continue;
862 }
863 let dy = b.y - a.y;
864 if dy <= 1e-14 {
865 continue;
866 }
867 let s = (m.y - a.y) / dy;
868 let x_int = a.x + s * (b.x - a.x);
869 if x_int >= m.x - 1e-11 {
870 let dist = (x_int - m.x).max(0.0);
871 if dist < best_t {
872 best_t = dist;
873 if s <= 1e-11 {
874 cand_idx = i;
875 } else if s >= 1.0 - 1e-11 {
876 cand_idx = (i + 1) % n;
877 } else if a.x > b.x + 1e-11 {
878 cand_idx = i;
879 } else {
880 cand_idx = (i + 1) % n;
881 }
882 }
883 }
884 }
885
886 if !best_t.is_finite() {
887 let mut best_d_sq = f64::INFINITY;
888 let mut best_pair = (hm_idx, 0usize);
889 for (hi, &hp) in hole.iter().enumerate() {
890 for (oi, &op) in outer.iter().enumerate() {
891 let d_sq = (hp.x - op.x) * (hp.x - op.x) + (hp.y - op.y) * (hp.y - op.y);
892 if d_sq < best_d_sq {
893 best_d_sq = d_sq;
894 best_pair = (hi, oi);
895 }
896 }
897 }
898 let (hi, oi) = best_pair;
899 let mut combined = Vec::with_capacity(outer.len() + hole.len() + 2);
900 combined.extend_from_slice(&outer[..=oi]);
901 for k in 0..hole.len() {
902 combined.push(hole[(hi + k) % hole.len()]);
903 }
904 combined.push(hole[hi]);
905 combined.push(outer[oi]);
906 if oi + 1 < outer.len() {
907 combined.extend_from_slice(&outer[(oi + 1)..]);
908 }
909 *outer = combined;
910 return;
911 }
912
913 let p = outer[cand_idx];
914 let ix = m.x + best_t;
915 let mut bridge_pt = p;
916
917 if (p.y - m.y).abs() > 1e-11 && best_t.is_finite() {
918 let i_pt = Point2D { x: ix, y: m.y };
919 let (tri_a, tri_b, tri_c) = if p.y > m.y {
920 (m.pt(), i_pt, p.pt())
921 } else {
922 (m.pt(), p.pt(), i_pt)
923 };
924 let mut best_angle = f64::INFINITY;
925 let mut best_dist_sq = f64::INFINITY;
926
927 for i in 0..n {
928 let prev = outer[(i + n - 1) % n].pt();
929 let v = outer[i];
930 let next = outer[(i + 1) % n].pt();
931 if cross_2d(prev, v.pt(), next) >= 1e-11 {
932 continue;
933 }
934 if (v.x - m.x).hypot(v.y - m.y) <= 1e-11 || (v.x - p.x).hypot(v.y - p.y) <= 1e-11 {
935 continue;
936 }
937 if point_in_triangle_2d(v.pt(), tri_a, tri_b, tri_c) {
938 let dx = v.x - m.x;
939 let dy = (v.y - m.y).abs();
940 let ang = dy.atan2(dx);
941 let d_sq = dx * dx + dy * dy;
942 if ang < best_angle - 1e-11
943 || ((ang - best_angle).abs() <= 1e-11 && d_sq < best_dist_sq)
944 {
945 best_angle = ang;
946 best_dist_sq = d_sq;
947 bridge_pt = v;
948 }
949 }
950 }
951 }
952
953 let mut bridge_idx = cand_idx;
954 for i in 0..n {
955 let v = outer[i];
956 if v.id == bridge_pt.id || (v.x - bridge_pt.x).hypot(v.y - bridge_pt.y) <= 1e-11 {
957 bridge_idx = i;
958 let prev = outer[(i + n - 1) % n].pt();
959 let next = outer[(i + 1) % n].pt();
960 if point_in_vertex_cone_2d(prev, v.pt(), next, m.pt()) {
961 break;
962 }
963 }
964 }
965
966 let mut combined = Vec::with_capacity(outer.len() + hole.len() + 2);
967 combined.extend_from_slice(&outer[..=bridge_idx]);
968 for k in 0..hole.len() {
969 combined.push(hole[(hm_idx + k) % hole.len()]);
970 }
971 combined.push(hole[hm_idx]);
972 combined.push(outer[bridge_idx]);
973 if bridge_idx + 1 < outer.len() {
974 combined.extend_from_slice(&outer[(bridge_idx + 1)..]);
975 }
976 *outer = combined;
977}
978
979#[inline]
980fn cross_2d(a: Point2D, b: Point2D, c: Point2D) -> f64 {
981 (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
982}
983
984#[inline]
985fn point_in_triangle_2d(p: Point2D, a: Point2D, b: Point2D, c: Point2D) -> bool {
986 let c1 = cross_2d(a, b, p);
987 let c2 = cross_2d(b, c, p);
988 let c3 = cross_2d(c, a, p);
989 c1 >= -1e-11 && c2 >= -1e-11 && c3 >= -1e-11
990}
991
992fn ear_clip_indexed_ring(ring: &[IdxPoint2D]) -> Vec<[u32; 3]> {
993 let mut idxs: Vec<usize> = (0..ring.len()).collect();
994 let mut tris = Vec::with_capacity(ring.len().saturating_sub(2));
995 let mut guard = ring.len() * ring.len() * 2 + 32;
996
997 while idxs.len() > 3 && guard > 0 {
998 guard -= 1;
999
1000 let mut stripped = false;
1003 let m = idxs.len();
1004 for i in 0..m {
1005 let prev = ring[idxs[(i + m - 1) % m]];
1006 let curr = ring[idxs[i]];
1007 let next = ring[idxs[(i + 1) % m]];
1008 if curr.id == prev.id
1009 || (curr.x - prev.x).hypot(curr.y - prev.y) <= 1e-11
1010 || prev.id == next.id
1011 || (prev.x - next.x).hypot(prev.y - next.y) <= 1e-11
1012 {
1013 idxs.remove(i);
1014 stripped = true;
1015 break;
1016 }
1017 }
1018 if stripped {
1019 continue;
1020 }
1021
1022 let m = idxs.len();
1023 if m <= 3 {
1024 break;
1025 }
1026 let mut best_ear: Option<(usize, f64)> = None;
1027
1028 for pass in 0..2 {
1029 let tol = if pass == 0 { 1e-11 } else { 1e-7 };
1030 for i in 0..m {
1031 let prev = ring[idxs[(i + m - 1) % m]];
1032 let curr = ring[idxs[i]];
1033 let next = ring[idxs[(i + 1) % m]];
1034
1035 let cross = cross_2d(prev.pt(), curr.pt(), next.pt());
1036 if cross <= 1e-12 {
1037 continue;
1038 }
1039 let lu = (curr.x - prev.x).hypot(curr.y - prev.y);
1040 let lv = (next.x - curr.x).hypot(next.y - curr.y);
1041 let sin_theta = if lu * lv > 1e-20 {
1042 cross / (lu * lv)
1043 } else {
1044 0.0
1045 };
1046
1047 let prev_prev = ring[idxs[(i + m - 2) % m]];
1048 let next_next = ring[idxs[(i + 2) % m]];
1049
1050 if m > 3 {
1052 let l_pp = (prev_prev.x - prev.x).hypot(prev_prev.y - prev.y);
1053 let l_nn = (next_next.x - next.x).hypot(next_next.y - next.y);
1054 let l_pn = (next.x - prev.x).hypot(next.y - prev.y);
1055 if prev_prev.id != next.id && l_pp * l_pn > 1e-20 {
1056 let c_p = cross_2d(prev_prev.pt(), prev.pt(), next.pt()).abs();
1057 let d_p = (prev_prev.x - prev.x) * (next.x - prev.x)
1058 + (prev_prev.y - prev.y) * (next.y - prev.y);
1059 if c_p <= 1e-8 * l_pp * l_pn && d_p > 0.0 {
1060 continue;
1061 }
1062 }
1063 if next_next.id != prev.id && l_nn * l_pn > 1e-20 {
1064 let c_n = cross_2d(prev.pt(), next.pt(), next_next.pt()).abs();
1065 let d_n = (prev.x - next.x) * (next_next.x - next.x)
1066 + (prev.y - next.y) * (next_next.y - next.y);
1067 if c_n <= 1e-8 * l_nn * l_pn && d_n > 0.0 {
1068 continue;
1069 }
1070 }
1071 }
1072
1073 if pass == 0
1074 && (!point_in_vertex_cone_2d(
1075 prev_prev.pt(),
1076 prev.pt(),
1077 curr.pt(),
1078 next.pt(),
1079 ) || !point_in_vertex_cone_2d(
1080 curr.pt(),
1081 next.pt(),
1082 next_next.pt(),
1083 prev.pt(),
1084 ))
1085 {
1086 continue;
1087 }
1088
1089 let mut any_inside = false;
1090 for j in 0..m {
1091 if j == (i + m - 1) % m || j == i || j == (i + 1) % m {
1092 continue;
1093 }
1094 let pt = ring[idxs[j]];
1095 if pt.id == prev.id
1096 || pt.id == curr.id
1097 || pt.id == next.id
1098 || (pt.x - prev.x).hypot(pt.y - prev.y) <= tol
1099 || (pt.x - curr.x).hypot(pt.y - curr.y) <= tol
1100 || (pt.x - next.x).hypot(pt.y - next.y) <= tol
1101 {
1102 continue;
1103 }
1104 let pj = ring[idxs[(j + m - 1) % m]].pt();
1105 let nj = ring[idxs[(j + 1) % m]].pt();
1106 if cross_2d(pj, pt.pt(), nj) > tol {
1107 continue;
1108 }
1109 if point_in_triangle_2d(pt.pt(), prev.pt(), curr.pt(), next.pt()) {
1110 any_inside = true;
1111 break;
1112 }
1113 }
1114
1115 if !any_inside {
1116 if best_ear.map_or(true, |(_, best_s)| sin_theta > best_s) {
1117 best_ear = Some((i, sin_theta));
1118 }
1119 }
1120 }
1121 if let Some((best_i, _)) = best_ear {
1122 let prev = ring[idxs[(best_i + m - 1) % m]];
1123 let curr = ring[idxs[best_i]];
1124 let next = ring[idxs[(best_i + 1) % m]];
1125 if prev.id != curr.id && curr.id != next.id && next.id != prev.id {
1126 tris.push([prev.id, curr.id, next.id]);
1127 }
1128 idxs.remove(best_i);
1129 break;
1130 }
1131 }
1132
1133 if best_ear.is_none() {
1134 let mut best_i = 0usize;
1137 let mut best_inside_count = usize::MAX;
1138 let mut best_sin = f64::NEG_INFINITY;
1139 for i in 0..m {
1140 let prev = ring[idxs[(i + m - 1) % m]];
1141 let curr = ring[idxs[i]];
1142 let next = ring[idxs[(i + 1) % m]];
1143 let c = cross_2d(prev.pt(), curr.pt(), next.pt());
1144 if c <= -1e-9 {
1145 continue;
1146 }
1147 let lu = (curr.x - prev.x).hypot(curr.y - prev.y);
1148 let lv = (next.x - curr.x).hypot(next.y - curr.y);
1149 let sin_t = if lu * lv > 1e-20 { c / (lu * lv) } else { 0.0 };
1150 let mut inside_count = 0usize;
1151 for j in 0..m {
1152 if j == (i + m - 1) % m || j == i || j == (i + 1) % m {
1153 continue;
1154 }
1155 let pt = ring[idxs[j]];
1156 if point_in_triangle_2d(pt.pt(), prev.pt(), curr.pt(), next.pt()) {
1157 inside_count += 1;
1158 }
1159 }
1160 if inside_count < best_inside_count
1161 || (inside_count == best_inside_count && sin_t > best_sin)
1162 {
1163 best_inside_count = inside_count;
1164 best_sin = sin_t;
1165 best_i = i;
1166 }
1167 }
1168 let prev = ring[idxs[(best_i + m - 1) % m]];
1169 let curr = ring[idxs[best_i]];
1170 let next = ring[idxs[(best_i + 1) % m]];
1171 if prev.id != curr.id && curr.id != next.id && next.id != prev.id {
1172 tris.push([prev.id, curr.id, next.id]);
1173 }
1174 idxs.remove(best_i);
1175 }
1176 }
1177
1178 if idxs.len() == 3 {
1179 let a = ring[idxs[0]];
1180 let b = ring[idxs[1]];
1181 let c = ring[idxs[2]];
1182 if a.id != b.id && b.id != c.id && c.id != a.id {
1183 tris.push([a.id, b.id, c.id]);
1184 }
1185 }
1186
1187 tris
1188}
1189
1190#[inline]
1191fn quad_choose_diagonal_02(p0: Point3D, p1: Point3D, p2: Point3D, p3: Point3D, normal: Point3D) -> bool {
1192 let c0 = p1.sub(p0).cross(p2.sub(p0)).dot(normal);
1193 let c2 = p2.sub(p0).cross(p3.sub(p0)).dot(normal);
1194 let c1 = p2.sub(p1).cross(p3.sub(p1)).dot(normal);
1195 let c3 = p3.sub(p1).cross(p0.sub(p1)).dot(normal);
1196 c0.min(c2) >= c1.min(c3)
1197}
1198
1199fn triangulate_face_vids_3d(
1202 outer_vids: &[u32],
1203 holes_vids: &[Vec<u32>],
1204 outer_3d: &[Point3D],
1205 holes_3d: &[Vec<Point3D>],
1206 normal: Point3D,
1207) -> Vec<[u32; 3]> {
1208 if outer_vids.len() < 3 {
1209 return Vec::new();
1210 }
1211 if outer_vids.len() == 3 && holes_vids.is_empty() {
1212 return vec![[outer_vids[0], outer_vids[1], outer_vids[2]]];
1213 }
1214 if outer_vids.len() == 4
1215 && holes_vids.is_empty()
1216 && outer_vids[0] != outer_vids[1]
1217 && outer_vids[1] != outer_vids[2]
1218 && outer_vids[2] != outer_vids[3]
1219 && outer_vids[3] != outer_vids[0]
1220 && outer_vids[0] != outer_vids[2]
1221 && outer_vids[1] != outer_vids[3]
1222 {
1223 if quad_choose_diagonal_02(outer_3d[0], outer_3d[1], outer_3d[2], outer_3d[3], normal) {
1224 return vec![
1225 [outer_vids[0], outer_vids[1], outer_vids[2]],
1226 [outer_vids[0], outer_vids[2], outer_vids[3]],
1227 ];
1228 } else {
1229 return vec![
1230 [outer_vids[0], outer_vids[1], outer_vids[3]],
1231 [outer_vids[1], outer_vids[2], outer_vids[3]],
1232 ];
1233 }
1234 }
1235 let origin = outer_3d[0];
1236 let (u_axis, v_axis) = build_plane_frame(normal);
1237 let project = |p: Point3D, id: u32| -> IdxPoint2D {
1238 let rel = p.sub(origin);
1239 IdxPoint2D {
1240 x: rel.dot(u_axis),
1241 y: rel.dot(v_axis),
1242 id,
1243 }
1244 };
1245
1246 let outer_2d: Vec<IdxPoint2D> = outer_3d
1247 .iter()
1248 .zip(outer_vids.iter())
1249 .map(|(&p, &vid)| project(p, vid))
1250 .collect();
1251 let holes_2d: Vec<Vec<IdxPoint2D>> = holes_3d
1252 .iter()
1253 .zip(holes_vids.iter())
1254 .map(|(h_pts, h_vids)| {
1255 h_pts
1256 .iter()
1257 .zip(h_vids.iter())
1258 .map(|(&p, &vid)| project(p, vid))
1259 .collect()
1260 })
1261 .collect();
1262
1263 triangulate_indexed_polygon_2d(&outer_2d, &holes_2d)
1264}
1265
1266pub fn triangulate_face_3d(face: &Face3D) -> Vec<[Point3D; 3]> {
1268 if face.outer.len() < 3 {
1269 return Vec::new();
1270 }
1271 if face.outer.len() == 3 && face.holes.is_empty() {
1272 return vec![[face.outer[0], face.outer[1], face.outer[2]]];
1273 }
1274 if face.outer.len() == 4 && face.holes.is_empty() {
1275 let p0 = face.outer[0];
1276 let p1 = face.outer[1];
1277 let p2 = face.outer[2];
1278 let p3 = face.outer[3];
1279 if quad_choose_diagonal_02(p0, p1, p2, p3, face.normal) {
1280 return vec![[p0, p1, p2], [p0, p2, p3]];
1281 } else {
1282 return vec![[p0, p1, p3], [p1, p2, p3]];
1283 }
1284 }
1285
1286 let mut orig_3d: Vec<Point3D> = Vec::with_capacity(face.outer.len() + face.holes.len() * 4);
1287 let mut find_or_push_pt = |p: Point3D| -> u32 {
1288 for (i, &q) in orig_3d.iter().enumerate() {
1289 if p.sub(q).norm_sq() <= 1e-22 {
1290 return i as u32;
1291 }
1292 }
1293 let id = orig_3d.len() as u32;
1294 orig_3d.push(p);
1295 id
1296 };
1297
1298 let outer_vids: Vec<u32> = face.outer.iter().map(|&p| find_or_push_pt(p)).collect();
1299 let holes_vids: Vec<Vec<u32>> = face
1300 .holes
1301 .iter()
1302 .map(|h| h.iter().map(|&p| find_or_push_pt(p)).collect())
1303 .collect();
1304
1305 triangulate_face_vids_3d(&outer_vids, &holes_vids, &face.outer, &face.holes, face.normal)
1306 .into_iter()
1307 .map(|[a, b, c]| [orig_3d[a as usize], orig_3d[b as usize], orig_3d[c as usize]])
1308 .collect()
1309}
1310
1311#[inline]
1313fn triangle_solid_angle_turns(p: Point3D, a: Point3D, b: Point3D, c: Point3D) -> f64 {
1314 let ra = a.sub(p);
1315 let rb = b.sub(p);
1316 let rc = c.sub(p);
1317 let la = ra.norm();
1318 let lb = rb.norm();
1319 let lc = rc.norm();
1320 if la <= 1e-14 || lb <= 1e-14 || lc <= 1e-14 {
1321 return 0.0;
1322 }
1323 let num = ra.dot(rb.cross(rc));
1324 let den = la * lb * lc + ra.dot(rb) * lc + rb.dot(rc) * la + rc.dot(ra) * lb;
1325 let half_omega = num.atan2(den);
1326 half_omega / (2.0 * core::f64::consts::PI)
1327}
1328
1329fn face_solid_angle_turns(p: Point3D, nf: &NormalizedInputFace, pl: &CanonicalPlane) -> f64 {
1334 let h = pl.normal.dot(p) - pl.d;
1335 if h.abs() <= 1e-12 {
1337 return 0.0;
1338 }
1339 let mut half_omega_sum = 0.0;
1340 if nf.loops_3d.len() == 1 && nf.loops_3d[0].len() <= 4 {
1341 let ring = &nf.loops_3d[0];
1342 let n = ring.len();
1343 if n >= 3 {
1344 let ra = ring[0].sub(p);
1345 let la = ra.norm();
1346 let mut rb = ring[1].sub(p);
1347 let mut lb = rb.norm();
1348 for i in 1..(n - 1) {
1349 let rc = ring[i + 1].sub(p);
1350 let lc = rc.norm();
1351 if la > 1e-14 && lb > 1e-14 && lc > 1e-14 {
1352 let num = ra.dot(rb.cross(rc));
1353 let den = la * lb * lc + ra.dot(rb) * lc + rb.dot(rc) * la + rc.dot(ra) * lb;
1354 half_omega_sum += num.atan2(den);
1355 }
1356 rb = rc;
1357 lb = lc;
1358 }
1359 }
1360 } else {
1361 let p_perp = p.sub(pl.normal.scale(h));
1362 let ra = p_perp.sub(p);
1363 let la = h.abs();
1364 for ring in &nf.loops_3d {
1365 let n = ring.len();
1366 if n < 3 {
1367 continue;
1368 }
1369 let mut rb = ring[0].sub(p);
1370 let mut lb = rb.norm();
1371 for i in 0..n {
1372 let rc = ring[(i + 1) % n].sub(p);
1373 let lc = rc.norm();
1374 if lb > 1e-14 && lc > 1e-14 {
1375 let num = ra.dot(rb.cross(rc));
1376 let den = la * lb * lc + ra.dot(rb) * lc + rb.dot(rc) * la + rc.dot(ra) * lb;
1377 half_omega_sum += num.atan2(den);
1378 }
1379 rb = rc;
1380 lb = lc;
1381 }
1382 }
1383 }
1384 let turns = half_omega_sum * (1.0 / (2.0 * core::f64::consts::PI));
1385 if nf.orient_sign >= 0 {
1386 turns
1387 } else {
1388 -turns
1389 }
1390}
1391
1392pub fn solid_winding_number(p: Point3D, solid: &Solid3D) -> f64 {
1394 let mut turns = 0.0;
1395 for face in &solid.faces {
1396 if face.outer.len() < 3 {
1397 continue;
1398 }
1399 let d = face.normal.dot(face.outer[0]);
1400 let h = face.normal.dot(p) - d;
1401 if h.abs() <= 1e-12 {
1402 continue;
1403 }
1404 let p_perp = p.sub(face.normal.scale(h));
1405 let n = face.outer.len();
1406 for i in 0..n {
1407 turns += triangle_solid_angle_turns(p, p_perp, face.outer[i], face.outer[(i + 1) % n]);
1408 }
1409 for hole in &face.holes {
1410 let hn = hole.len();
1411 for i in 0..hn {
1412 turns += triangle_solid_angle_turns(p, p_perp, hole[i], hole[(i + 1) % hn]);
1413 }
1414 }
1415 }
1416 turns
1417}
1418
1419#[inline]
1420fn aabb_overlap_3d(min_a: Point3D, max_a: Point3D, min_b: Point3D, max_b: Point3D, tol: f64) -> bool {
1421 !(max_a.x + tol < min_b.x
1422 || min_a.x - tol > max_b.x
1423 || max_a.y + tol < min_b.y
1424 || min_a.y - tol > max_b.y
1425 || max_a.z + tol < min_b.z
1426 || min_a.z - tol > max_b.z)
1427}
1428
1429#[inline]
1430fn plane_straddles_aabb(n: Point3D, d: f64, min_p: Point3D, max_p: Point3D, tol: f64) -> bool {
1431 let px_min = if n.x >= 0.0 { min_p.x } else { max_p.x };
1432 let py_min = if n.y >= 0.0 { min_p.y } else { max_p.y };
1433 let pz_min = if n.z >= 0.0 { min_p.z } else { max_p.z };
1434 let d_min = n.x * px_min + n.y * py_min + n.z * pz_min - d;
1435 if d_min > tol {
1436 return false;
1437 }
1438 let px_max = if n.x >= 0.0 { max_p.x } else { min_p.x };
1439 let py_max = if n.y >= 0.0 { max_p.y } else { min_p.y };
1440 let pz_max = if n.z >= 0.0 { max_p.z } else { min_p.z };
1441 let d_max = n.x * px_max + n.y * py_max + n.z * pz_max - d;
1442 d_max >= -tol
1443}
1444
1445fn loops_winding_2d(pt: Point2D, loops: &[Vec<Point2D>]) -> i32 {
1447 let mut w = 0i32;
1448 for loop_pts in loops {
1449 let n = loop_pts.len();
1450 for i in 0..n {
1451 let a = loop_pts[i];
1452 let b = loop_pts[(i + 1) % n];
1453 if a.y <= pt.y {
1454 if b.y > pt.y && cross_2d(a, b, pt) > 0.0 {
1455 w += 1;
1456 }
1457 } else if b.y <= pt.y && cross_2d(a, b, pt) < 0.0 {
1458 w -= 1;
1459 }
1460 }
1461 }
1462 w
1463}
1464
1465#[derive(Clone, Debug)]
1470struct Vert3D {
1471 pos: Point3D,
1472 rep: Point3D,
1473 radius: f64,
1474 weight: u32,
1475 parent: u32,
1476 planes: Vec<usize>,
1477 dirty_proj: bool,
1478 alive: bool,
1479}
1480
1481#[derive(Clone, Debug)]
1482struct Edge3D {
1483 v0: u32,
1484 v1: u32,
1485 planes: Vec<usize>,
1486 face_windings: Vec<(usize, i32)>,
1488 alive: bool,
1489}
1490
1491#[inline]
1492fn merge_plane_tags(dst: &mut Vec<usize>, src: &[usize]) -> bool {
1493 let mut changed = false;
1494 for &p in src {
1495 if !dst.contains(&p) {
1496 dst.push(p);
1497 changed = true;
1498 }
1499 }
1500 changed
1501}
1502
1503#[inline]
1504fn merge_face_windings(dst: &mut Vec<(usize, i32)>, src: &[(usize, i32)], sign: i32) {
1505 for &(nf_idx, d) in src {
1506 let sd = d * sign;
1507 if sd == 0 {
1508 continue;
1509 }
1510 if let Some(pos) = dst.iter().position(|&(id, _)| id == nf_idx) {
1511 dst[pos].1 += sd;
1512 if dst[pos].1 == 0 {
1513 dst.swap_remove(pos);
1514 }
1515 } else {
1516 dst.push((nf_idx, sd));
1517 }
1518 }
1519}
1520
1521#[inline]
1522fn resolve_vert_3d(verts: &mut [Vert3D], mut v: u32) -> u32 {
1523 while verts[v as usize].parent != v {
1524 let p = verts[v as usize].parent;
1525 verts[v as usize].parent = verts[p as usize].parent;
1526 v = p;
1527 }
1528 v
1529}
1530
1531fn find_or_create_vert_3d(
1532 verts: &mut Vec<Vert3D>,
1533 p: Point3D,
1534 r0: f64,
1535 r_init: f64,
1536 plane_tags: &[usize],
1537) -> u32 {
1538 let mut best_v = None;
1539 let mut best_d_sq = f64::INFINITY;
1540 for i in 0..verts.len() {
1541 if !verts[i].alive {
1542 continue;
1543 }
1544 let d_sq = verts[i].pos.sub(p).norm_sq();
1545 let rad = verts[i].radius.max(r0);
1546 if d_sq <= rad * rad && d_sq < best_d_sq {
1547 best_d_sq = d_sq;
1548 best_v = Some(i as u32);
1549 }
1550 }
1551 if let Some(vid) = best_v {
1552 let root = resolve_vert_3d(verts, vid);
1553 if merge_plane_tags(&mut verts[root as usize].planes, plane_tags) {
1554 verts[root as usize].dirty_proj = true;
1555 }
1556 return root;
1557 }
1558 let id = verts.len() as u32;
1559 let mut planes = Vec::with_capacity(4);
1560 merge_plane_tags(&mut planes, plane_tags);
1561 verts.push(Vert3D {
1562 pos: p,
1563 rep: p,
1564 radius: r_init,
1565 weight: 1,
1566 parent: id,
1567 planes,
1568 dirty_proj: true,
1569 alive: true,
1570 });
1571 id
1572}
1573
1574fn find_or_create_vert_3d_bucketed(
1575 verts: &mut Vec<Vert3D>,
1576 vert_xy_buckets: &mut [Vec<u32>; 256],
1577 vert_adj: &mut Vec<Vec<usize>>,
1578 p: Point3D,
1579 r0: f64,
1580 r_init: f64,
1581 plane_tags: &[usize],
1582 x_low: f64,
1583 y_low: f64,
1584 x_scale: f64,
1585 y_scale: f64,
1586) -> u32 {
1587 let bx0 = (((p.x - r0 - x_low) * x_scale).floor() as isize).clamp(0, 15) as usize;
1588 let bx1 = (((p.x + r0 - x_low) * x_scale).floor() as isize).clamp(0, 15) as usize;
1589 let by0 = (((p.y - r0 - y_low) * y_scale).floor() as isize).clamp(0, 15) as usize;
1590 let by1 = (((p.y + r0 - y_low) * y_scale).floor() as isize).clamp(0, 15) as usize;
1591 let mut best_v = None;
1592 let mut best_d_sq = f64::INFINITY;
1593 for bx in bx0..=bx1 {
1594 let row = bx << 4;
1595 for by in by0..=by1 {
1596 for &cand in &vert_xy_buckets[row | by] {
1597 let i = cand as usize;
1598 if !verts[i].alive {
1599 continue;
1600 }
1601 let d_sq = verts[i].pos.sub(p).norm_sq();
1602 let rad = verts[i].radius.max(r0);
1603 if d_sq <= rad * rad && (d_sq < best_d_sq || (d_sq == best_d_sq && Some(cand) < best_v)) {
1604 best_d_sq = d_sq;
1605 best_v = Some(cand);
1606 }
1607 }
1608 }
1609 }
1610 if let Some(vid) = best_v {
1611 let root = resolve_vert_3d(verts, vid);
1612 if merge_plane_tags(&mut verts[root as usize].planes, plane_tags) {
1613 verts[root as usize].dirty_proj = true;
1614 }
1615 return root;
1616 }
1617 let id = verts.len() as u32;
1618 let mut planes = Vec::with_capacity(4);
1619 merge_plane_tags(&mut planes, plane_tags);
1620 verts.push(Vert3D {
1621 pos: p,
1622 rep: p,
1623 radius: r_init,
1624 weight: 1,
1625 parent: id,
1626 planes,
1627 dirty_proj: true,
1628 alive: true,
1629 });
1630 let bx = (((p.x - x_low) * x_scale).floor() as isize).clamp(0, 15) as usize;
1631 let by = (((p.y - y_low) * y_scale).floor() as isize).clamp(0, 15) as usize;
1632 vert_xy_buckets[(bx << 4) | by].push(id);
1633 vert_adj.push(Vec::new());
1634 id
1635}
1636
1637fn inject_edge_3d_adj(
1638 edges: &mut Vec<Edge3D>,
1639 vert_adj: &mut [Vec<usize>],
1640 v0: u32,
1641 v1: u32,
1642 plane_tags: &[usize],
1643 face_windings: &[(usize, i32)],
1644) {
1645 if v0 == v1 {
1646 return;
1647 }
1648 let (a, b, sign) = if v0 < v1 {
1649 (v0, v1, 1)
1650 } else {
1651 (v1, v0, -1)
1652 };
1653 for &e_id in &vert_adj[a as usize] {
1654 let e = &mut edges[e_id];
1655 if e.alive && e.v0 == a && e.v1 == b {
1656 merge_plane_tags(&mut e.planes, plane_tags);
1657 merge_face_windings(&mut e.face_windings, face_windings, sign);
1658 return;
1659 }
1660 }
1661 let mut planes = Vec::with_capacity(4);
1662 merge_plane_tags(&mut planes, plane_tags);
1663 let mut fw = Vec::with_capacity(face_windings.len());
1664 merge_face_windings(&mut fw, face_windings, sign);
1665 let e_id = edges.len();
1666 edges.push(Edge3D {
1667 v0: a,
1668 v1: b,
1669 planes,
1670 face_windings: fw,
1671 alive: true,
1672 });
1673 vert_adj[a as usize].push(e_id);
1674}
1675
1676fn point_in_or_near_face_2d(
1679 pt: Point2D,
1680 loops_2d: &[Vec<Point2D>],
1681 winding_rule: WindingRule,
1682 tol: f64,
1683) -> bool {
1684 let w = loops_winding_2d(pt, loops_2d);
1685 if w != 0 && winding_rule.apply(w as f64) {
1686 return true;
1687 }
1688 let tol_sq = tol * tol;
1689 for ring in loops_2d {
1690 let n = ring.len();
1691 for i in 0..n {
1692 let a = ring[i];
1693 let b = ring[(i + 1) % n];
1694 let dx = b.x - a.x;
1695 let dy = b.y - a.y;
1696 let l_sq = dx * dx + dy * dy;
1697 let t = if l_sq > 1e-24 {
1698 (((pt.x - a.x) * dx + (pt.y - a.y) * dy) / l_sq).clamp(0.0, 1.0)
1699 } else {
1700 0.0
1701 };
1702 let px = a.x + t * dx;
1703 let py = a.y + t * dy;
1704 let d_sq = (pt.x - px) * (pt.x - px) + (pt.y - py) * (pt.y - py);
1705 if d_sq <= tol_sq {
1706 return true;
1707 }
1708 }
1709 }
1710 false
1711}
1712
1713fn inject_face_plane_chords_3d(
1717 other_nf: &NormalizedInputFace,
1718 other_pl: &CanonicalPlane,
1719 target_pl: &CanonicalPlane,
1720 target_pl_id: usize,
1721 target_plane_faces: &[usize],
1722 norm_faces: &[NormalizedInputFace],
1723 r0: f64,
1724 r_init: f64,
1725 winding_rule: WindingRule,
1726 verts: &mut Vec<Vert3D>,
1727 edges: &mut Vec<Edge3D>,
1728 vert_xy_buckets: &mut [Vec<u32>; 256],
1729 vert_adj: &mut Vec<Vec<usize>>,
1730 x_low: f64,
1731 y_low: f64,
1732 x_scale: f64,
1733 y_scale: f64,
1734) -> bool {
1735 let s0 = target_pl.normal.dot(other_pl.origin) - target_pl.d;
1736 let alpha = target_pl.normal.dot(other_pl.u_axis);
1737 let beta = target_pl.normal.dot(other_pl.v_axis);
1738 let grad_norm = alpha.hypot(beta);
1739 if grad_norm <= 1e-11 {
1740 return false;
1741 }
1742 let inv_grad = 1.0 / grad_norm;
1743 let h0 = s0 * inv_grad;
1744 let nx = alpha * inv_grad;
1745 let ny = beta * inv_grad;
1746 let tx = -ny;
1747 let ty = nx;
1748 let snap_tol = 1e-11;
1749 let both_planes = [other_nf.plane_id, target_pl_id];
1750 let near_tol = r0.max(1e-7);
1751
1752 let mut line_pts: Vec<(f64, Point2D)> = Vec::with_capacity(16);
1753
1754 for loop_pts in &other_nf.loops_2d {
1755 let n = loop_pts.len();
1756 for i in 0..n {
1757 let a = loop_pts[i];
1758 let b = loop_pts[(i + 1) % n];
1759 let mut ha = h0 + nx * a.x + ny * a.y;
1760 let mut hb = h0 + nx * b.x + ny * b.y;
1761 if ha.abs() <= snap_tol {
1762 ha = 0.0;
1763 }
1764 if hb.abs() <= snap_tol {
1765 hb = 0.0;
1766 }
1767
1768 if ha == 0.0 && hb == 0.0 {
1769 let ta = tx * a.x + ty * a.y;
1770 let tb = tx * b.x + ty * b.y;
1771 line_pts.push((ta, a));
1772 line_pts.push((tb, b));
1773 } else if ha == 0.0 {
1774 line_pts.push((tx * a.x + ty * a.y, a));
1775 } else if hb == 0.0 {
1776 line_pts.push((tx * b.x + ty * b.y, b));
1777 } else if (ha < 0.0 && hb > 0.0) || (ha > 0.0 && hb < 0.0) {
1778 let s = ha / (ha - hb);
1779 let p = Point2D {
1780 x: a.x + s * (b.x - a.x),
1781 y: a.y + s * (b.y - a.y),
1782 };
1783 line_pts.push((tx * p.x + ty * p.y, p));
1784 }
1785 }
1786 }
1787
1788 if line_pts.len() < 2 {
1789 return false;
1790 }
1791
1792 let mut t_other_min = f64::INFINITY;
1793 let mut t_other_max = f64::NEG_INFINITY;
1794 for &(t, _) in &line_pts {
1795 t_other_min = t_other_min.min(t);
1796 t_other_max = t_other_max.max(t);
1797 }
1798 if t_other_max - t_other_min <= snap_tol {
1799 return false;
1800 }
1801
1802 let mut any_tf_near = false;
1804 let mut t_tf_min = f64::INFINITY;
1805 let mut t_tf_max = f64::NEG_INFINITY;
1806 let t_clip_min = t_other_min - near_tol;
1807 let t_clip_max = t_other_max + near_tol;
1808 for &tf_idx in target_plane_faces {
1809 let tf = &norm_faces[tf_idx];
1810 if !aabb_overlap_3d(other_nf.min_pt, other_nf.max_pt, tf.min_pt, tf.max_pt, r0) {
1811 continue;
1812 }
1813 any_tf_near = true;
1814 for loop_3d in &tf.loops_3d {
1815 let n = loop_3d.len();
1816 for i in 0..n {
1817 let a3 = loop_3d[i];
1818 let b3 = loop_3d[(i + 1) % n];
1819 let mut ha = other_pl.normal.dot(a3) - other_pl.d;
1820 let mut hb = other_pl.normal.dot(b3) - other_pl.d;
1821 if ha.abs() <= snap_tol {
1822 ha = 0.0;
1823 }
1824 if hb.abs() <= snap_tol {
1825 hb = 0.0;
1826 }
1827 if ha == 0.0 && hb == 0.0 {
1828 let pa = other_pl.project_2d(a3);
1829 let pb = other_pl.project_2d(b3);
1830 let ta = tx * pa.x + ty * pa.y;
1831 let tb = tx * pb.x + ty * pb.y;
1832 t_tf_min = t_tf_min.min(ta).min(tb);
1833 t_tf_max = t_tf_max.max(ta).max(tb);
1834 if ta >= t_clip_min && ta <= t_clip_max {
1835 line_pts.push((ta, pa));
1836 }
1837 if tb >= t_clip_min && tb <= t_clip_max {
1838 line_pts.push((tb, pb));
1839 }
1840 } else if ha == 0.0 {
1841 let pa = other_pl.project_2d(a3);
1842 let ta = tx * pa.x + ty * pa.y;
1843 t_tf_min = t_tf_min.min(ta);
1844 t_tf_max = t_tf_max.max(ta);
1845 if ta >= t_clip_min && ta <= t_clip_max {
1846 line_pts.push((ta, pa));
1847 }
1848 } else if hb == 0.0 {
1849 let pb = other_pl.project_2d(b3);
1850 let tb = tx * pb.x + ty * pb.y;
1851 t_tf_min = t_tf_min.min(tb);
1852 t_tf_max = t_tf_max.max(tb);
1853 if tb >= t_clip_min && tb <= t_clip_max {
1854 line_pts.push((tb, pb));
1855 }
1856 } else if (ha < 0.0 && hb > 0.0) || (ha > 0.0 && hb < 0.0) {
1857 let s = ha / (ha - hb);
1858 let p3 = a3.add(b3.sub(a3).scale(s));
1859 let p2 = other_pl.project_2d(p3);
1860 let tp = tx * p2.x + ty * p2.y;
1861 t_tf_min = t_tf_min.min(tp);
1862 t_tf_max = t_tf_max.max(tp);
1863 if tp >= t_clip_min && tp <= t_clip_max {
1864 line_pts.push((tp, p2));
1865 }
1866 }
1867 }
1868 }
1869 }
1870 if !any_tf_near {
1871 return false;
1872 }
1873 if r0 <= 1e-4 && t_tf_min <= t_tf_max {
1874 let overlap_min = t_other_min.max(t_tf_min - near_tol);
1875 let overlap_max = t_other_max.min(t_tf_max + near_tol);
1876 if overlap_max - overlap_min <= snap_tol {
1877 return false;
1878 }
1879 }
1880
1881 line_pts.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(core::cmp::Ordering::Equal));
1882
1883 let mut unique_pts: Vec<(f64, Point2D)> = Vec::with_capacity(line_pts.len());
1884 for item in line_pts {
1885 if let Some(last) = unique_pts.last_mut() {
1886 if (item.0 - last.0).abs() <= snap_tol {
1887 last.1 = Point2D {
1888 x: 0.5 * (last.1.x + item.1.x),
1889 y: 0.5 * (last.1.y + item.1.y),
1890 };
1891 last.0 = tx * last.1.x + ty * last.1.y;
1892 continue;
1893 }
1894 }
1895 unique_pts.push(item);
1896 }
1897
1898 if unique_pts.len() < 2 {
1899 return false;
1900 }
1901
1902 let mut active_start: Option<Point2D> = None;
1903 let mut active_end: Point2D = unique_pts[0].1;
1904 let mut injected_any = false;
1905
1906 for k in 0..(unique_pts.len() - 1) {
1907 let p0 = unique_pts[k].1;
1908 let p1 = unique_pts[k + 1].1;
1909 let mid = Point2D {
1910 x: 0.5 * (p0.x + p1.x),
1911 y: 0.5 * (p0.y + p1.y),
1912 };
1913 let in_other = point_in_or_near_face_2d(mid, &other_nf.loops_2d, winding_rule, 1e-7);
1914 let mid_3d = other_pl.unproject_3d(mid.x, mid.y);
1915 let mid_target_2d = target_pl.project_2d(mid_3d);
1916 let in_target = target_plane_faces.iter().any(|&tf_idx| {
1917 let tf = &norm_faces[tf_idx];
1918 aabb_overlap_3d(other_nf.min_pt, other_nf.max_pt, tf.min_pt, tf.max_pt, r0)
1919 && point_in_or_near_face_2d(mid_target_2d, &tf.loops_2d, winding_rule, near_tol)
1920 });
1921 let inside = in_other && in_target;
1922
1923 if inside {
1924 if active_start.is_none() {
1925 active_start = Some(p0);
1926 }
1927 active_end = p1;
1928 } else if let Some(start_pt) = active_start.take() {
1929 let p0_3d = other_pl.unproject_3d(start_pt.x, start_pt.y);
1930 let p1_3d = other_pl.unproject_3d(active_end.x, active_end.y);
1931 let v0 = find_or_create_vert_3d_bucketed(
1932 verts,
1933 vert_xy_buckets,
1934 vert_adj,
1935 p0_3d,
1936 r0,
1937 r_init,
1938 &both_planes,
1939 x_low,
1940 y_low,
1941 x_scale,
1942 y_scale,
1943 );
1944 let v1 = find_or_create_vert_3d_bucketed(
1945 verts,
1946 vert_xy_buckets,
1947 vert_adj,
1948 p1_3d,
1949 r0,
1950 r_init,
1951 &both_planes,
1952 x_low,
1953 y_low,
1954 x_scale,
1955 y_scale,
1956 );
1957 inject_edge_3d_adj(edges, vert_adj, v0, v1, &both_planes, &[]);
1958 injected_any = true;
1959 }
1960 }
1961
1962 if let Some(start_pt) = active_start {
1963 let p0_3d = other_pl.unproject_3d(start_pt.x, start_pt.y);
1964 let p1_3d = other_pl.unproject_3d(active_end.x, active_end.y);
1965 let v0 = find_or_create_vert_3d_bucketed(
1966 verts,
1967 vert_xy_buckets,
1968 vert_adj,
1969 p0_3d,
1970 r0,
1971 r_init,
1972 &both_planes,
1973 x_low,
1974 y_low,
1975 x_scale,
1976 y_scale,
1977 );
1978 let v1 = find_or_create_vert_3d_bucketed(
1979 verts,
1980 vert_xy_buckets,
1981 vert_adj,
1982 p1_3d,
1983 r0,
1984 r_init,
1985 &both_planes,
1986 x_low,
1987 y_low,
1988 x_scale,
1989 y_scale,
1990 );
1991 inject_edge_3d_adj(edges, vert_adj, v0, v1, &both_planes, &[]);
1992 injected_any = true;
1993 }
1994 injected_any
1995}
1996
1997fn merge_two_verts_3d(verts: &mut [Vert3D], u: u32, v: u32, r_max: f64) -> bool {
1998 let ru = resolve_vert_3d(verts, u) as usize;
1999 let rv = resolve_vert_3d(verts, v) as usize;
2000 if ru == rv || !verts[ru].alive || !verts[rv].alive {
2001 return false;
2002 }
2003 let pu = verts[ru].pos;
2004 let pv = verts[rv].pos;
2005 let d = pu.sub(pv).norm();
2006 let rad_u = verts[ru].radius;
2007 let rad_v = verts[rv].radius;
2008 let wu = verts[ru].weight as f64;
2009 let wv = verts[rv].weight as f64;
2010 let w_tot = wu + wv;
2011 let new_pos = pu.scale(wu / w_tot).add(pv.scale(wv / w_tot));
2012 let new_rad = r_max.min(rad_u.max(rad_v).max(0.5 * (d + rad_u + rad_v)));
2013
2014 let (keep, kill) = if ru < rv { (ru, rv) } else { (rv, ru) };
2015 let kill_planes = core::mem::take(&mut verts[kill].planes);
2016 verts[keep].pos = new_pos;
2017 verts[keep].rep = new_pos;
2018 verts[keep].radius = new_rad;
2019 verts[keep].weight = verts[ru].weight.saturating_add(verts[rv].weight);
2020 verts[keep].dirty_proj = true;
2021 merge_plane_tags(&mut verts[keep].planes, &kill_planes);
2022 verts[kill].alive = false;
2023 verts[kill].parent = keep as u32;
2024 true
2025}
2026
2027fn solve_3x3(a: [[f64; 3]; 3], b: [f64; 3]) -> Option<Point3D> {
2029 let det = a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
2030 - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
2031 + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
2032 if det.abs() <= 1e-18 {
2033 return None;
2034 }
2035 let inv_det = 1.0 / det;
2036 let dx = (b[0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
2037 - a[0][1] * (b[1] * a[2][2] - a[1][2] * b[2])
2038 + a[0][2] * (b[1] * a[2][1] - a[1][1] * b[2]))
2039 * inv_det;
2040 let dy = (a[0][0] * (b[1] * a[2][2] - a[1][2] * b[2])
2041 - b[0] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
2042 + a[0][2] * (a[1][0] * b[2] - b[1] * a[2][0]))
2043 * inv_det;
2044 let dz = (a[0][0] * (a[1][1] * b[2] - b[1] * a[2][1])
2045 - a[0][1] * (a[1][0] * b[2] - b[1] * a[2][0])
2046 + b[0] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]))
2047 * inv_det;
2048 Some(Point3D::new(dx, dy, dz))
2049}
2050
2051fn closest_approach_segments_3d(
2054 p0: Point3D,
2055 p1: Point3D,
2056 q0: Point3D,
2057 q1: Point3D,
2058) -> (f64, f64, f64, Point3D) {
2059 let u = p1.sub(p0);
2060 let v = q1.sub(q0);
2061 let w = p0.sub(q0);
2062 let a = u.dot(u);
2063 let b = u.dot(v);
2064 let c = v.dot(v);
2065 let d = u.dot(w);
2066 let e = v.dot(w);
2067 let denom = a * c - b * b;
2068
2069 if denom <= 1e-16 * a * c || a <= 1e-22 || c <= 1e-22 {
2070 return (0.0, 0.0, f64::INFINITY, p0);
2071 }
2072
2073 let s = ((b * e - c * d) / denom).clamp(0.0, 1.0);
2074 let t = ((a * e - b * d) / denom).clamp(0.0, 1.0);
2075 let pa = p0.add(u.scale(s));
2076 let qb = q0.add(v.scale(t));
2077 let dist_sq = pa.sub(qb).norm_sq();
2078 let mid = pa.add(qb).scale(0.5);
2079 (s, t, dist_sq, mid)
2080}
2081
2082fn project_vertices_to_carrier_planes_3d(
2085 verts: &mut [Vert3D],
2086 planes: &[CanonicalPlane],
2087 r_max: f64,
2088) {
2089 let base_lambda = 1e-9;
2090 for v in verts.iter_mut() {
2091 if !v.alive || !v.dirty_proj {
2092 continue;
2093 }
2094 v.dirty_proj = false;
2095 if v.planes.is_empty() {
2096 v.rep = v.pos;
2097 continue;
2098 }
2099 let mut ata = [[0.0; 3]; 3];
2100 let mut rhs = [0.0; 3];
2101 let mut max_res = 0.0_f64;
2102 for &p_id in &v.planes {
2103 let pl = &planes[p_id];
2104 let n = pl.normal;
2105 let res = pl.d - n.dot(v.pos);
2106 max_res = max_res.max(res.abs());
2107 let n_arr = [n.x, n.y, n.z];
2108 for r in 0..3 {
2109 rhs[r] += res * n_arr[r];
2110 for c in 0..3 {
2111 ata[r][c] += n_arr[r] * n_arr[c];
2112 }
2113 }
2114 }
2115 if max_res <= 1e-13 {
2116 v.rep = v.pos;
2117 continue;
2118 }
2119 let cap = v.radius.max(0.75 * r_max);
2120 let solve_with_lambda = |lam: f64| -> Option<Point3D> {
2121 let mut m = ata;
2122 m[0][0] += lam;
2123 m[1][1] += lam;
2124 m[2][2] += lam;
2125 solve_3x3(m, rhs)
2126 };
2127
2128 if let Some(delta0) = solve_with_lambda(base_lambda) {
2129 if delta0.norm() <= cap {
2130 v.rep = v.pos.add(delta0);
2131 } else {
2132 let k_scale = (v.planes.len() as f64).max(1.0);
2136 let mut lo = 1e-6 * k_scale;
2137 let mut hi = 100.0 * k_scale;
2138 let mut best = delta0.scale(cap / delta0.norm());
2139 for _ in 0..12 {
2140 let mid = (lo * hi).sqrt();
2141 if let Some(d_mid) = solve_with_lambda(mid) {
2142 let len = d_mid.norm();
2143 if len <= cap {
2144 best = d_mid;
2145 hi = mid;
2146 } else {
2147 lo = mid;
2148 }
2149 } else {
2150 lo = mid;
2151 }
2152 }
2153 v.rep = v.pos.add(best);
2154 }
2155 } else {
2156 v.rep = v.pos;
2157 }
2158 }
2159}
2160
2161fn consolidate_3d_complex(
2163 verts: &mut Vec<Vert3D>,
2164 edges: &mut Vec<Edge3D>,
2165 planes: &[CanonicalPlane],
2166 r0: f64,
2167 r_init: f64,
2168 r_max: f64,
2169 vert_active: &mut Vec<bool>,
2170 vert_xy_buckets: &mut [Vec<u32>; 256],
2171 vert_adj: &mut Vec<Vec<usize>>,
2172 x_low: f64,
2173 y_low: f64,
2174 x_scale: f64,
2175 y_scale: f64,
2176) {
2177 let mut edge_splits: Vec<Vec<(f64, u32)>> = Vec::new();
2178 let mut sorted_v_step1: Vec<(f64, f64, usize)> = Vec::new();
2179 let mut sorted_e_step3: Vec<(u32, u32, usize)> = Vec::new();
2180 let mut sorted_v_step4: Vec<(f64, usize)> = Vec::new();
2181 let mut sorted_e_step5: Vec<(f64, usize)> = Vec::new();
2182
2183 for iter in 0..32 {
2184 let mut changed = false;
2185 vert_active.resize(verts.len(), true);
2186 if vert_adj.len() < verts.len() {
2187 vert_adj.resize_with(verts.len(), Vec::new);
2188 }
2189
2190 project_vertices_to_carrier_planes_3d(verts, planes, r_max);
2192 let rep_collapse_tol = (1e-3 * r0).max(1e-7);
2193 let rep_collapse_sq = rep_collapse_tol * rep_collapse_tol;
2194
2195 let mut next_active = vec![false; verts.len()];
2196 let mark_neighborhood_active =
2197 |u: usize, next_active: &mut [bool], vert_adj: &[Vec<usize>], edges: &[Edge3D]| {
2198 if u < next_active.len() {
2199 next_active[u] = true;
2200 }
2201 if u < vert_adj.len() {
2202 for &eid in &vert_adj[u] {
2203 let e = &edges[eid];
2204 if e.alive {
2205 let va = e.v0 as usize;
2206 let vb = e.v1 as usize;
2207 if va < next_active.len() {
2208 next_active[va] = true;
2209 }
2210 if vb < next_active.len() {
2211 next_active[vb] = true;
2212 }
2213 }
2214 }
2215 }
2216 };
2217
2218 let nv = verts.len();
2220 let max_search_r = r_max.max(r0).max(rep_collapse_tol);
2221 let search_pos_rx = 2.0 * r_max + max_search_r;
2222 sorted_v_step1.clear();
2223 for idx in 0..nv {
2224 if verts[idx].alive && vert_active[idx] {
2225 sorted_v_step1.push((verts[idx].pos.x, 0.0, idx));
2226 }
2227 }
2228 sorted_v_step1.sort_unstable_by(|a, b| {
2229 a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal)
2230 });
2231
2232 let mut cand_js: Vec<usize> = Vec::with_capacity(16);
2233 for i in 0..nv {
2234 if !verts[i].alive || !vert_active[i] {
2235 continue;
2236 }
2237 let min_px = verts[i].pos.x - search_pos_rx;
2238 let max_px = verts[i].pos.x + search_pos_rx;
2239 let start = sorted_v_step1.partition_point(|&(px, _, _)| px < min_px);
2240 cand_js.clear();
2241 for &(px, _, j) in &sorted_v_step1[start..] {
2242 if px > max_px {
2243 break;
2244 }
2245 if j > i && verts[j].alive {
2246 cand_js.push(j);
2247 }
2248 }
2249 cand_js.sort_unstable();
2250
2251 let pi = verts[i].pos;
2252 let ri = verts[i].rep;
2253 let rad_i = verts[i].radius;
2254 for &j in &cand_js {
2255 if !verts[j].alive {
2256 continue;
2257 }
2258 let rj = verts[j].rep;
2259 let pj = verts[j].pos;
2260 if (ri.x - rj.x).abs() > max_search_r && (pi.x - pj.x).abs() > max_search_r {
2261 continue;
2262 }
2263 if (ri.y - rj.y).abs() > max_search_r && (pi.y - pj.y).abs() > max_search_r {
2264 continue;
2265 }
2266 if (ri.z - rj.z).abs() > max_search_r && (pi.z - pj.z).abs() > max_search_r {
2267 continue;
2268 }
2269 let max_r = (rad_i + verts[j].radius).min(r_max);
2270 let pos_close = (pi.x - pj.x).abs() <= max_r
2271 && (pi.y - pj.y).abs() <= max_r
2272 && (pi.z - pj.z).abs() <= max_r
2273 && pi.sub(pj).norm_sq() <= max_r * max_r;
2274 let rep_coincident = (ri.x - rj.x).abs() <= rep_collapse_tol
2275 && (ri.y - rj.y).abs() <= rep_collapse_tol
2276 && (ri.z - rj.z).abs() <= rep_collapse_tol
2277 && ri.sub(rj).norm_sq() <= rep_collapse_sq;
2278 let rep_close = (ri.x - rj.x).abs() <= r0
2279 && (ri.y - rj.y).abs() <= r0
2280 && (ri.z - rj.z).abs() <= r0
2281 && ri.sub(rj).norm_sq() <= r0 * r0
2282 && pi.sub(pj).norm_sq() <= 4.0 * r_max * r_max;
2283 if pos_close || rep_coincident || rep_close {
2284 if merge_two_verts_3d(verts, i as u32, j as u32, r_max) {
2285 changed = true;
2286 let root = resolve_vert_3d(verts, i as u32) as usize;
2287 mark_neighborhood_active(i, &mut next_active, vert_adj, edges);
2288 mark_neighborhood_active(j, &mut next_active, vert_adj, edges);
2289 mark_neighborhood_active(root, &mut next_active, vert_adj, edges);
2290 }
2291 }
2292 }
2293 }
2294
2295 let ne = edges.len();
2297 for e_idx in 0..ne {
2298 if !edges[e_idx].alive {
2299 continue;
2300 }
2301 let u0 = edges[e_idx].v0 as usize;
2302 let v0 = edges[e_idx].v1 as usize;
2303 if !vert_active[u0] && !vert_active[v0] {
2304 continue;
2305 }
2306 let u = resolve_vert_3d(verts, edges[e_idx].v0);
2307 let v = resolve_vert_3d(verts, edges[e_idx].v1);
2308 if u == v {
2309 edges[e_idx].alive = false;
2310 changed = true;
2311 mark_neighborhood_active(u as usize, &mut next_active, vert_adj, edges);
2312 continue;
2313 }
2314 let (a, b, flipped) = if u < v { (u, v, false) } else { (v, u, true) };
2315 if edges[e_idx].v0 != a || edges[e_idx].v1 != b {
2316 edges[e_idx].v0 = a;
2317 edges[e_idx].v1 = b;
2318 if !vert_adj[a as usize].contains(&e_idx) {
2319 vert_adj[a as usize].push(e_idx);
2320 }
2321 }
2322 if flipped {
2323 for fw in &mut edges[e_idx].face_windings {
2324 fw.1 = -fw.1;
2325 }
2326 }
2327
2328 let pa = verts[a as usize].pos;
2329 let pb = verts[b as usize].pos;
2330 let ra = verts[a as usize].rep;
2331 let rb = verts[b as usize].rep;
2332 let contract_r = (verts[a as usize].radius + verts[b as usize].radius).min(r_max);
2333 if pa.sub(pb).norm_sq() <= contract_r * contract_r
2334 || ra.sub(rb).norm_sq() <= rep_collapse_sq
2335 {
2336 merge_two_verts_3d(verts, a, b, r_max);
2337 let root = resolve_vert_3d(verts, a);
2338 let e_planes = core::mem::take(&mut edges[e_idx].planes);
2339 if merge_plane_tags(&mut verts[root as usize].planes, &e_planes) {
2340 verts[root as usize].dirty_proj = true;
2341 }
2342 edges[e_idx].alive = false;
2343 changed = true;
2344 mark_neighborhood_active(a as usize, &mut next_active, vert_adj, edges);
2345 mark_neighborhood_active(b as usize, &mut next_active, vert_adj, edges);
2346 mark_neighborhood_active(root as usize, &mut next_active, vert_adj, edges);
2347 continue;
2348 }
2349
2350 if merge_plane_tags(&mut verts[a as usize].planes, &edges[e_idx].planes) {
2352 verts[a as usize].dirty_proj = true;
2353 mark_neighborhood_active(a as usize, &mut next_active, vert_adj, edges);
2354 }
2355 if merge_plane_tags(&mut verts[b as usize].planes, &edges[e_idx].planes) {
2356 verts[b as usize].dirty_proj = true;
2357 mark_neighborhood_active(b as usize, &mut next_active, vert_adj, edges);
2358 }
2359 }
2360
2361 sorted_e_step3.clear();
2363 for i in 0..edges.len() {
2364 if !edges[i].alive {
2365 continue;
2366 }
2367 let u0 = edges[i].v0 as usize;
2368 let v0 = edges[i].v1 as usize;
2369 if !vert_active[u0] && !vert_active[v0] {
2370 continue;
2371 }
2372 let u = resolve_vert_3d(verts, edges[i].v0);
2373 let v = resolve_vert_3d(verts, edges[i].v1);
2374 if u == v {
2375 edges[i].alive = false;
2376 changed = true;
2377 mark_neighborhood_active(u as usize, &mut next_active, vert_adj, edges);
2378 continue;
2379 }
2380 let (a, b, flip_i) = if u < v { (u, v, false) } else { (v, u, true) };
2381 if edges[i].v0 != a {
2382 if !vert_adj[a as usize].contains(&i) {
2383 vert_adj[a as usize].push(i);
2384 }
2385 }
2386 edges[i].v0 = a;
2387 edges[i].v1 = b;
2388 if flip_i {
2389 for fw in &mut edges[i].face_windings {
2390 fw.1 = -fw.1;
2391 }
2392 }
2393 sorted_e_step3.push((a, b, i));
2394 }
2395 sorted_e_step3.sort_unstable();
2396 let mut idx_e = 0;
2397 while idx_e < sorted_e_step3.len() {
2398 let (a, b, i) = sorted_e_step3[idx_e];
2399 idx_e += 1;
2400 while idx_e < sorted_e_step3.len()
2401 && sorted_e_step3[idx_e].0 == a
2402 && sorted_e_step3[idx_e].1 == b
2403 {
2404 let j = sorted_e_step3[idx_e].2;
2405 let j_planes = core::mem::take(&mut edges[j].planes);
2406 let j_fw = core::mem::take(&mut edges[j].face_windings);
2407 merge_plane_tags(&mut edges[i].planes, &j_planes);
2408 merge_face_windings(&mut edges[i].face_windings, &j_fw, 1);
2409 edges[j].alive = false;
2410 changed = true;
2411 mark_neighborhood_active(a as usize, &mut next_active, vert_adj, edges);
2412 mark_neighborhood_active(b as usize, &mut next_active, vert_adj, edges);
2413 idx_e += 1;
2414 }
2415 }
2416
2417 project_vertices_to_carrier_planes_3d(verts, planes, r_max);
2419
2420 edge_splits.clear();
2421 edge_splits.resize(edges.len(), Vec::new());
2422
2423 let cur_nv = verts.len();
2425 let cur_ne = edges.len();
2426 sorted_v_step4.clear();
2427 for w in 0..cur_nv {
2428 if verts[w].alive && vert_active[w] {
2429 sorted_v_step4.push((verts[w].rep.x, w));
2430 }
2431 }
2432 sorted_v_step4.sort_unstable_by(|a, b| {
2433 a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal)
2434 });
2435
2436 for e_idx in 0..cur_ne {
2437 if !edges[e_idx].alive {
2438 continue;
2439 }
2440 let u = edges[e_idx].v0 as usize;
2441 let v = edges[e_idx].v1 as usize;
2442 if !vert_active[u] && !vert_active[v] {
2443 continue;
2444 }
2445 let pu = verts[u].rep;
2446 let pv = verts[v].rep;
2447 let ab = pv.sub(pu);
2448 let len_sq = ab.norm_sq();
2449 if len_sq <= 1e-20 {
2450 continue;
2451 }
2452 let min_x = pu.x.min(pv.x) - r_max;
2453 let max_x = pu.x.max(pv.x) + r_max;
2454 let min_y = pu.y.min(pv.y) - r_max;
2455 let max_y = pu.y.max(pv.y) + r_max;
2456 let min_z = pu.z.min(pv.z) - r_max;
2457 let max_z = pu.z.max(pv.z) + r_max;
2458
2459 let start_idx = sorted_v_step4.partition_point(|&(vx, _)| vx < min_x);
2460 for &(vx, w) in &sorted_v_step4[start_idx..] {
2461 if vx > max_x {
2462 break;
2463 }
2464 if w == u || w == v {
2465 continue;
2466 }
2467 let pw = verts[w].rep;
2468 if pw.y < min_y || pw.y > max_y || pw.z < min_z || pw.z > max_z {
2469 continue;
2470 }
2471
2472 let tol = verts[w].radius.max(r0);
2473 let tol_sq = tol * tol;
2474 if pw.sub(pu).norm_sq() <= tol_sq || pw.sub(pv).norm_sq() <= tol_sq {
2475 continue;
2476 }
2477
2478 let t = pw.sub(pu).dot(ab) / len_sq;
2479 let mut split_t = None;
2480 if t > 1e-5 && t < 1.0 - 1e-5 {
2481 let proj = pu.add(ab.scale(t));
2482 let d_sq = pw.sub(proj).norm_sq();
2483 if d_sq <= tol_sq {
2484 split_t = Some(t);
2485 }
2486 }
2487
2488 if split_t.is_none() {
2490 for &p_id in &edges[e_idx].planes {
2491 if !verts[w].planes.contains(&p_id) {
2492 continue;
2493 }
2494 let pl = &planes[p_id];
2495 let a2 = pl.project_2d(pu);
2496 let b2 = pl.project_2d(pv);
2497 let w2 = pl.project_2d(pw);
2498 let da_sq = (w2.x - a2.x) * (w2.x - a2.x) + (w2.y - a2.y) * (w2.y - a2.y);
2499 let db_sq = (w2.x - b2.x) * (w2.x - b2.x) + (w2.y - b2.y) * (w2.y - b2.y);
2500 if da_sq <= tol_sq || db_sq <= tol_sq {
2501 continue;
2502 }
2503 let dx = b2.x - a2.x;
2504 let dy = b2.y - a2.y;
2505 let l2 = dx * dx + dy * dy;
2506 if l2 <= 1e-20 {
2507 continue;
2508 }
2509 let t2 = ((w2.x - a2.x) * dx + (w2.y - a2.y) * dy) / l2;
2510 if t2 > 1e-5 && t2 < 1.0 - 1e-5 {
2511 let px = a2.x + t2 * dx;
2512 let py = a2.y + t2 * dy;
2513 let d2_sq = (w2.x - px) * (w2.x - px) + (w2.y - py) * (w2.y - py);
2514 if d2_sq <= tol_sq {
2515 split_t = Some(t2);
2516 break;
2517 }
2518 }
2519 }
2520 }
2521
2522 if let Some(st) = split_t {
2523 if merge_plane_tags(&mut verts[w].planes, &edges[e_idx].planes) {
2524 verts[w].dirty_proj = true;
2525 }
2526 edge_splits[e_idx].push((st, w as u32));
2527 }
2528 }
2529 }
2530
2531 let mut edge_aabb: Vec<[f64; 6]> = Vec::with_capacity(cur_ne);
2533 sorted_e_step5.clear();
2534 for e_idx in 0..cur_ne {
2535 let u = edges[e_idx].v0 as usize;
2536 let v = edges[e_idx].v1 as usize;
2537 if !edges[e_idx].alive || (!vert_active[u] && !vert_active[v]) {
2538 edge_aabb.push([0.0; 6]);
2539 continue;
2540 }
2541 let p0 = verts[u].rep;
2542 let p1 = verts[v].rep;
2543 let min_x = p0.x.min(p1.x) - r0;
2544 edge_aabb.push([
2545 min_x,
2546 p0.x.max(p1.x) + r0,
2547 p0.y.min(p1.y) - r0,
2548 p0.y.max(p1.y) + r0,
2549 p0.z.min(p1.z) - r0,
2550 p0.z.max(p1.z) + r0,
2551 ]);
2552 sorted_e_step5.push((min_x, e_idx));
2553 }
2554 sorted_e_step5.sort_unstable_by(|a, b| {
2555 a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal)
2556 });
2557
2558 for idx_a in 0..sorted_e_step5.len() {
2559 let e1 = sorted_e_step5[idx_a].1;
2560 let max1_x_bound = edge_aabb[e1][1];
2561
2562 for idx_b in (idx_a + 1)..sorted_e_step5.len() {
2563 if sorted_e_step5[idx_b].0 > max1_x_bound {
2564 break;
2565 }
2566 let e2 = sorted_e_step5[idx_b].1;
2567 let (i, j) = if e1 < e2 { (e1, e2) } else { (e2, e1) };
2568
2569 let [min1_x, max1_x, min1_y, max1_y, min1_z, max1_z] = edge_aabb[i];
2570 let [min2_x, max2_x, min2_y, max2_y, min2_z, max2_z] = edge_aabb[j];
2571 if max2_x < min1_x
2572 || min2_x > max1_x
2573 || max2_y < min1_y
2574 || min2_y > max1_y
2575 || max2_z < min1_z
2576 || min2_z > max1_z
2577 {
2578 continue;
2579 }
2580 let u1 = edges[i].v0 as usize;
2581 let v1 = edges[i].v1 as usize;
2582 let u2 = edges[j].v0 as usize;
2583 let v2 = edges[j].v1 as usize;
2584 if u1 == u2 || u1 == v2 || v1 == u2 || v1 == v2 {
2585 continue;
2586 }
2587 let p0 = verts[u1].rep;
2588 let p1 = verts[v1].rep;
2589 let q0 = verts[u2].rep;
2590 let q1 = verts[v2].rep;
2591
2592 let mut found_cross: Option<(f64, f64, Point3D)> = None;
2593
2594 let (s3, t3, d3_sq, mid3) = closest_approach_segments_3d(p0, p1, q0, q1);
2596 if s3 > 1e-5 && s3 < 1.0 - 1e-5 && t3 > 1e-5 && t3 < 1.0 - 1e-5 && d3_sq <= r0 * r0
2597 {
2598 found_cross = Some((s3, t3, mid3));
2599 }
2600
2601 if found_cross.is_none() {
2603 for &p_id in &edges[i].planes {
2604 if !edges[j].planes.contains(&p_id) {
2605 continue;
2606 }
2607 let pl = &planes[p_id];
2608 let a2 = pl.project_2d(p0);
2609 let b2 = pl.project_2d(p1);
2610 let c2 = pl.project_2d(q0);
2611 let d2 = pl.project_2d(q1);
2612 let dx1 = b2.x - a2.x;
2613 let dy1 = b2.y - a2.y;
2614 let dx2 = d2.x - c2.x;
2615 let dy2 = d2.y - c2.y;
2616 let denom = dx1 * dy2 - dy1 * dx2;
2617 if denom.abs() > 1e-14 {
2618 let s2 = ((c2.x - a2.x) * dy2 - (c2.y - a2.y) * dx2) / denom;
2619 let t2 = ((c2.x - a2.x) * dy1 - (c2.y - a2.y) * dx1) / denom;
2620 if s2 > 1e-5 && s2 < 1.0 - 1e-5 && t2 > 1e-5 && t2 < 1.0 - 1e-5 {
2621 let ix = a2.x + s2 * dx1;
2622 let iy = a2.y + s2 * dy1;
2623 found_cross = Some((s2, t2, pl.unproject_3d(ix, iy)));
2624 break;
2625 }
2626 }
2627 }
2628 }
2629
2630 if let Some((s, t, mid)) = found_cross {
2631 let mut union_planes = edges[i].planes.clone();
2632 merge_plane_tags(&mut union_planes, &edges[j].planes);
2633 let v_int = find_or_create_vert_3d_bucketed(
2634 verts,
2635 vert_xy_buckets,
2636 vert_adj,
2637 mid,
2638 r0,
2639 r_init,
2640 &union_planes,
2641 x_low,
2642 y_low,
2643 x_scale,
2644 y_scale,
2645 );
2646 let r_int = resolve_vert_3d(verts, v_int) as usize;
2647 if r_int >= next_active.len() {
2648 next_active.resize(r_int + 1, false);
2649 }
2650 next_active[r_int] = true;
2651 if r_int != u1 && r_int != v1 {
2652 edge_splits[i].push((s, r_int as u32));
2653 }
2654 if r_int != u2 && r_int != v2 {
2655 edge_splits[j].push((t, r_int as u32));
2656 }
2657 }
2658 }
2659 }
2660
2661 for e_idx in 0..cur_ne {
2663 if !edges[e_idx].alive || edge_splits[e_idx].is_empty() {
2664 continue;
2665 }
2666 let raw_u = edges[e_idx].v0;
2667 let raw_v = edges[e_idx].v1;
2668 let u = resolve_vert_3d(verts, raw_u);
2669 let v = resolve_vert_3d(verts, raw_v);
2670 let e_planes = edges[e_idx].planes.clone();
2671 let mut e_fw = edges[e_idx].face_windings.clone();
2672 if raw_u > raw_v {
2673 for fw in &mut e_fw {
2674 fw.1 = -fw.1;
2675 }
2676 }
2677
2678 let splits = &mut edge_splits[e_idx];
2679 splits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
2680
2681 let mut chain: Vec<u32> = Vec::with_capacity(splits.len() + 2);
2682 chain.push(u);
2683 for &(_, sv) in splits.iter() {
2684 let rsv = resolve_vert_3d(verts, sv);
2685 if rsv != v && !chain.contains(&rsv) {
2686 chain.push(rsv);
2687 }
2688 }
2689 if chain.last() != Some(&v) {
2690 chain.push(v);
2691 }
2692
2693 if chain.len() > 2 {
2694 edges[e_idx].alive = false;
2695 changed = true;
2696 for &cv in &chain {
2697 let c_idx = cv as usize;
2698 if c_idx >= next_active.len() {
2699 next_active.resize(c_idx + 1, false);
2700 }
2701 mark_neighborhood_active(c_idx, &mut next_active, vert_adj, edges);
2702 }
2703 for k in 0..(chain.len() - 1) {
2704 inject_edge_3d_adj(edges, vert_adj, chain[k], chain[k + 1], &e_planes, &e_fw);
2705 }
2706 }
2707 }
2708
2709 if !changed {
2710 break;
2711 }
2712 if iter >= 0 {
2713 next_active.resize(verts.len(), true);
2714 let seed = next_active.clone();
2716 for e in edges.iter() {
2717 if !e.alive {
2718 continue;
2719 }
2720 let u = e.v0 as usize;
2721 let v = e.v1 as usize;
2722 if seed[u] || seed[v] {
2723 next_active[u] = true;
2724 next_active[v] = true;
2725 }
2726 }
2727 *vert_active = next_active;
2728 }
2729 }
2730
2731 project_vertices_to_carrier_planes_3d(verts, planes, r_max);
2733}
2734
2735#[derive(Clone, Debug)]
2736struct FacePatch3D {
2737 plane_id: usize,
2738 merged_planes: Vec<usize>,
2739 directed_edges: Vec<(usize, i32)>,
2741 outer_vids: Vec<u32>,
2742 holes_vids: Vec<Vec<u32>>,
2743 sample_2d: Point2D,
2744 area_2d: f64,
2745 delta_w: Vec<i32>,
2746 alive: bool,
2747}
2748
2749struct DisjointSet {
2750 parent: Vec<usize>,
2751}
2752
2753impl DisjointSet {
2754 fn new(n: usize) -> Self {
2755 Self {
2756 parent: (0..n).collect(),
2757 }
2758 }
2759
2760 fn find(&mut self, mut i: usize) -> usize {
2761 while self.parent[i] != i {
2762 let p = self.parent[i];
2763 self.parent[i] = self.parent[p];
2764 i = p;
2765 }
2766 i
2767 }
2768
2769 fn union(&mut self, a: usize, b: usize) {
2770 let ra = self.find(a);
2771 let rb = self.find(b);
2772 if ra != rb {
2773 if ra < rb {
2774 self.parent[rb] = ra;
2775 } else {
2776 self.parent[ra] = rb;
2777 }
2778 }
2779 }
2780}
2781
2782fn consolidated_face_winding_2d(
2785 pt: Point2D,
2786 nf_idx: usize,
2787 plane_edges: &[usize],
2788 edges: &[Edge3D],
2789 verts: &[Vert3D],
2790 pl: &CanonicalPlane,
2791) -> i32 {
2792 let mut total_angle = 0.0;
2793 let mut found_any = false;
2794 for &e_id in plane_edges {
2795 let e = &edges[e_id];
2796 if !e.alive {
2797 continue;
2798 }
2799 let mut dw = 0i32;
2800 for &(id, d) in &e.face_windings {
2801 if id == nf_idx {
2802 dw = d;
2803 break;
2804 }
2805 }
2806 if dw == 0 {
2807 continue;
2808 }
2809 found_any = true;
2810 let a2 = pl.project_2d(verts[e.v0 as usize].rep);
2811 let b2 = pl.project_2d(verts[e.v1 as usize].rep);
2812 let ax = a2.x - pt.x;
2813 let ay = a2.y - pt.y;
2814 let bx = b2.x - pt.x;
2815 let by = b2.y - pt.y;
2816 let d_theta = (ax * by - ay * bx).atan2(ax * bx + ay * by);
2817 total_angle += (dw as f64) * d_theta;
2818 }
2819 if !found_any {
2820 return 0;
2821 }
2822 (total_angle / (2.0 * core::f64::consts::PI)).round() as i32
2823}
2824
2825fn compute_patch_interior_sample_2d(
2827 outer_vids: &[u32],
2828 holes_vids: &[Vec<u32>],
2829 verts: &[Vert3D],
2830 pl: &CanonicalPlane,
2831) -> Point2D {
2832 let n = outer_vids.len();
2833 if holes_vids.is_empty() {
2834 if n == 3 {
2835 let a = pl.project_2d(verts[outer_vids[0] as usize].rep);
2836 let b = pl.project_2d(verts[outer_vids[1] as usize].rep);
2837 let c = pl.project_2d(verts[outer_vids[2] as usize].rep);
2838 let la = (b.x - c.x).hypot(b.y - c.y);
2839 let lb = (c.x - a.x).hypot(c.y - a.y);
2840 let lc = (a.x - b.x).hypot(a.y - b.y);
2841 let perim = la + lb + lc;
2842 if perim > 1e-12 {
2843 return Point2D {
2844 x: (la * a.x + lb * b.x + lc * c.x) / perim,
2845 y: (la * a.y + lb * b.y + lc * c.y) / perim,
2846 };
2847 }
2848 return Point2D {
2849 x: (a.x + b.x + c.x) * (1.0 / 3.0),
2850 y: (a.y + b.y + c.y) * (1.0 / 3.0),
2851 };
2852 } else if n == 4 {
2853 let p0 = pl.project_2d(verts[outer_vids[0] as usize].rep);
2854 let p1 = pl.project_2d(verts[outer_vids[1] as usize].rep);
2855 let p2 = pl.project_2d(verts[outer_vids[2] as usize].rep);
2856 let p3 = pl.project_2d(verts[outer_vids[3] as usize].rep);
2857 let a012 = cross_2d(p0, p1, p2);
2858 let a023 = cross_2d(p0, p2, p3);
2859 let a013 = cross_2d(p0, p1, p3);
2860 let a123 = cross_2d(p1, p2, p3);
2861 let tri = if a012 > 1e-11 && a023 > 1e-11 {
2862 if a012 >= a023 {
2863 Some((p0, p1, p2))
2864 } else {
2865 Some((p0, p2, p3))
2866 }
2867 } else if a013 > 1e-11 && a123 > 1e-11 {
2868 if a013 >= a123 {
2869 Some((p0, p1, p3))
2870 } else {
2871 Some((p1, p2, p3))
2872 }
2873 } else {
2874 None
2875 };
2876 if let Some((a, b, c)) = tri {
2877 let la = (b.x - c.x).hypot(b.y - c.y);
2878 let lb = (c.x - a.x).hypot(c.y - a.y);
2879 let lc = (a.x - b.x).hypot(a.y - b.y);
2880 let perim = la + lb + lc;
2881 if perim > 1e-12 {
2882 return Point2D {
2883 x: (la * a.x + lb * b.x + lc * c.x) / perim,
2884 y: (la * a.y + lb * b.y + lc * c.y) / perim,
2885 };
2886 }
2887 return Point2D {
2888 x: (a.x + b.x + c.x) * (1.0 / 3.0),
2889 y: (a.y + b.y + c.y) * (1.0 / 3.0),
2890 };
2891 }
2892 }
2893 }
2894
2895 let outer_2d: Vec<Point2D> = outer_vids
2896 .iter()
2897 .map(|&v| pl.project_2d(verts[v as usize].rep))
2898 .collect();
2899 if holes_vids.is_empty() && n >= 5 {
2900 for i in 0..n {
2901 let a = outer_2d[(i + n - 1) % n];
2902 let b = outer_2d[i];
2903 let c = outer_2d[(i + 1) % n];
2904 if cross_2d(a, b, c) <= 1e-11 {
2905 continue;
2906 }
2907 let mut ear_ok = true;
2908 for j in 0..n {
2909 if j == i || j == (i + n - 1) % n || j == (i + 1) % n {
2910 continue;
2911 }
2912 if point_in_triangle_2d(outer_2d[j], a, b, c) {
2913 ear_ok = false;
2914 break;
2915 }
2916 }
2917 if ear_ok {
2918 let la = (b.x - c.x).hypot(b.y - c.y);
2919 let lb = (c.x - a.x).hypot(c.y - a.y);
2920 let lc = (a.x - b.x).hypot(a.y - b.y);
2921 let perim = la + lb + lc;
2922 if perim > 1e-12 {
2923 return Point2D {
2924 x: (la * a.x + lb * b.x + lc * c.x) / perim,
2925 y: (la * a.y + lb * b.y + lc * c.y) / perim,
2926 };
2927 }
2928 return Point2D {
2929 x: (a.x + b.x + c.x) * (1.0 / 3.0),
2930 y: (a.y + b.y + c.y) * (1.0 / 3.0),
2931 };
2932 }
2933 }
2934 }
2935 let holes_2d: Vec<Vec<Point2D>> = holes_vids
2936 .iter()
2937 .map(|h| {
2938 h.iter()
2939 .map(|&v| pl.project_2d(verts[v as usize].rep))
2940 .collect()
2941 })
2942 .collect();
2943
2944 let tris = triangulate_polygon_2d(&outer_2d, &holes_2d);
2945 let mut best_area = -1.0f64;
2946 let mut best_pt = outer_2d.first().copied().unwrap_or(Point2D { x: 0.0, y: 0.0 });
2947 for [a, b, c] in tris {
2948 let area = cross_2d(a, b, c).abs();
2949 if area > best_area {
2950 best_area = area;
2951 let la = (b.x - c.x).hypot(b.y - c.y);
2952 let lb = (c.x - a.x).hypot(c.y - a.y);
2953 let lc = (a.x - b.x).hypot(a.y - b.y);
2954 let perim = la + lb + lc;
2955 if perim > 1e-12 {
2956 best_pt = Point2D {
2957 x: (la * a.x + lb * b.x + lc * c.x) / perim,
2958 y: (la * a.y + lb * b.y + lc * c.y) / perim,
2959 };
2960 } else {
2961 best_pt = Point2D {
2962 x: (a.x + b.x + c.x) / 3.0,
2963 y: (a.y + b.y + c.y) / 3.0,
2964 };
2965 }
2966 }
2967 }
2968 best_pt
2969}
2970
2971fn extract_plane_face_patches_3d(
2973 p_id: usize,
2974 pl: &CanonicalPlane,
2975 plane_edges: &[usize],
2976 on_plane_faces: &[usize],
2977 norm_faces: &[NormalizedInputFace],
2978 verts: &[Vert3D],
2979 edges: &[Edge3D],
2980 body_count: usize,
2981 winding_rule: WindingRule,
2982 out_patches: &mut Vec<FacePatch3D>,
2983) {
2984 if plane_edges.len() < 3 {
2985 return;
2986 }
2987
2988 let mut local_vids: Vec<u32> = Vec::with_capacity(plane_edges.len() * 2);
2989 for &e_id in plane_edges {
2990 local_vids.push(edges[e_id].v0);
2991 local_vids.push(edges[e_id].v1);
2992 }
2993 local_vids.sort_unstable();
2994 local_vids.dedup();
2995 let num_lv = local_vids.len();
2996 if num_lv < 3 {
2997 return;
2998 }
2999 let to_lv = |vid: u32| -> usize { local_vids.binary_search(&vid).unwrap() };
3000
3001 let mut active_pe: Vec<bool> = vec![true; plane_edges.len()];
3003 let mut deg: Vec<u32> = vec![0; num_lv];
3004 loop {
3005 deg.fill(0);
3006 for (idx, &e_id) in plane_edges.iter().enumerate() {
3007 if active_pe[idx] {
3008 deg[to_lv(edges[e_id].v0)] += 1;
3009 deg[to_lv(edges[e_id].v1)] += 1;
3010 }
3011 }
3012 let mut pruned = false;
3013 for (idx, &e_id) in plane_edges.iter().enumerate() {
3014 if active_pe[idx]
3015 && (deg[to_lv(edges[e_id].v0)] < 2 || deg[to_lv(edges[e_id].v1)] < 2)
3016 {
3017 active_pe[idx] = false;
3018 pruned = true;
3019 }
3020 }
3021 if !pruned {
3022 break;
3023 }
3024 }
3025
3026 let surviving_edges: Vec<usize> = plane_edges
3027 .iter()
3028 .enumerate()
3029 .filter_map(|(i, &e_id)| if active_pe[i] { Some(e_id) } else { None })
3030 .collect();
3031 if surviving_edges.len() < 3 {
3032 return;
3033 }
3034
3035 let num_se = surviving_edges.len();
3037 let num_he = 2 * num_se;
3038 let mut he_orig: Vec<u32> = Vec::with_capacity(num_he);
3039 let mut he_orig_lv: Vec<usize> = Vec::with_capacity(num_he);
3040 let mut he_dest_lv: Vec<usize> = Vec::with_capacity(num_he);
3041 let mut he_edge: Vec<usize> = Vec::with_capacity(num_he);
3042 let mut he_sign: Vec<i32> = Vec::with_capacity(num_he);
3043
3044 let mut vert_out: Vec<Vec<usize>> = vec![Vec::new(); num_lv];
3045 for (k, &e_id) in surviving_edges.iter().enumerate() {
3046 let u = edges[e_id].v0;
3047 let v = edges[e_id].v1;
3048 let lu = to_lv(u);
3049 let lv = to_lv(v);
3050 let h0 = 2 * k;
3051 let h1 = 2 * k + 1;
3052 he_orig.push(u);
3053 he_orig_lv.push(lu);
3054 he_dest_lv.push(lv);
3055 he_edge.push(e_id);
3056 he_sign.push(1);
3057 vert_out[lu].push(h0);
3058
3059 he_orig.push(v);
3060 he_orig_lv.push(lv);
3061 he_dest_lv.push(lu);
3062 he_edge.push(e_id);
3063 he_sign.push(-1);
3064 vert_out[lv].push(h1);
3065 }
3066
3067 let v_2d: Vec<Point2D> = local_vids
3068 .iter()
3069 .map(|&vid| pl.project_2d(verts[vid as usize].rep))
3070 .collect();
3071
3072 let mut he_next: Vec<usize> = vec![0; num_he];
3074 for lv_id in 0..num_lv {
3075 let spokes = &mut vert_out[lv_id];
3076 if spokes.len() <= 2 {
3077 continue;
3078 }
3079 let pv = v_2d[lv_id];
3080 spokes.sort_by(|&ha, &hb| {
3081 let da = v_2d[he_dest_lv[ha]];
3082 let db = v_2d[he_dest_lv[hb]];
3083 let ang_a = (da.y - pv.y).atan2(da.x - pv.x);
3084 let ang_b = (db.y - pv.y).atan2(db.x - pv.x);
3085 ang_a
3086 .partial_cmp(&ang_b)
3087 .unwrap_or(core::cmp::Ordering::Equal)
3088 });
3089 }
3090
3091 for h in 0..num_he {
3093 let twin = h ^ 1;
3094 let target_lv = he_dest_lv[h];
3095 let spokes = &vert_out[target_lv];
3096 let pos = spokes.iter().position(|&sh| sh == twin).unwrap();
3097 let prev_idx = (pos + spokes.len() - 1) % spokes.len();
3098 he_next[h] = spokes[prev_idx];
3099 }
3100
3101 let mut visited = vec![false; num_he];
3103 let mut outer_cycles: Vec<(Vec<u32>, Vec<(usize, i32)>, f64)> = Vec::new();
3104 let mut hole_cycles: Vec<(Vec<u32>, Vec<(usize, i32)>, f64)> = Vec::new();
3105
3106 for h_start in 0..num_he {
3107 if visited[h_start] {
3108 continue;
3109 }
3110 let mut raw_he_seq: Vec<usize> = Vec::new();
3111 let mut cur = h_start;
3112 while !visited[cur] {
3113 visited[cur] = true;
3114 raw_he_seq.push(cur);
3115 cur = he_next[cur];
3116 }
3117 if cur != h_start || raw_he_seq.len() < 3 {
3118 continue;
3119 }
3120
3121 let mut v_stack: Vec<u32> = Vec::with_capacity(raw_he_seq.len());
3123 let mut e_stack: Vec<(usize, i32)> = Vec::with_capacity(raw_he_seq.len());
3124 let mut simple_loops: Vec<(Vec<u32>, Vec<(usize, i32)>)> = Vec::new();
3125
3126 for &h in &raw_he_seq {
3127 let u = he_orig[h];
3128 let e_pair = (he_edge[h], he_sign[h]);
3129 if let Some(pos) = v_stack.iter().position(|&sv| sv == u) {
3130 let sub_v = v_stack[pos..].to_vec();
3131 let sub_e = e_stack[pos..].to_vec();
3132 if sub_v.len() >= 3 {
3133 simple_loops.push((sub_v, sub_e));
3134 }
3135 v_stack.truncate(pos);
3136 e_stack.truncate(pos);
3137 }
3138 v_stack.push(u);
3139 e_stack.push(e_pair);
3140 }
3141 if v_stack.len() >= 3 {
3142 simple_loops.push((v_stack, e_stack));
3143 }
3144
3145 for (mut sv, mut se) in simple_loops {
3146 let mut chg = true;
3148 while chg && sv.len() >= 3 {
3149 chg = false;
3150 let n = sv.len();
3151 for i in 0..n {
3152 let j = (i + 1) % n;
3153 if se[i].0 == se[j].0 && se[i].1 == -se[j].1 {
3154 if i < j {
3155 sv.remove(j);
3156 se.remove(j);
3157 sv.remove(i);
3158 se.remove(i);
3159 } else {
3160 sv.remove(i);
3161 se.remove(i);
3162 sv.remove(j);
3163 se.remove(j);
3164 }
3165 chg = true;
3166 break;
3167 }
3168 }
3169 }
3170 if sv.len() < 3 {
3171 continue;
3172 }
3173 let pts_2d: Vec<Point2D> = sv.iter().map(|&vid| v_2d[to_lv(vid)]).collect();
3174 let area = signed_area_2d_pts(&pts_2d);
3175 if area > 1e-11 {
3176 outer_cycles.push((sv, se, area));
3177 } else if area < -1e-11 {
3178 hole_cycles.push((sv, se, area.abs()));
3179 }
3180 }
3181 }
3182
3183 if outer_cycles.is_empty() {
3184 return;
3185 }
3186
3187 let mut patch_holes_vids: Vec<Vec<Vec<u32>>> = vec![Vec::new(); outer_cycles.len()];
3189 let mut patch_holes_edges: Vec<Vec<(usize, i32)>> = vec![Vec::new(); outer_cycles.len()];
3190 let outer_2d_loops: Vec<Vec<Point2D>> = outer_cycles
3191 .iter()
3192 .map(|(ov, _, _)| ov.iter().map(|&vid| v_2d[to_lv(vid)]).collect())
3193 .collect();
3194
3195 for (hv, he, h_area) in hole_cycles {
3196 let test_pt = hv
3197 .iter()
3198 .map(|&vid| v_2d[to_lv(vid)])
3199 .find(|hp| {
3200 outer_2d_loops.iter().all(|sh| {
3201 sh.iter()
3202 .all(|sp| (sp.x - hp.x).hypot(sp.y - hp.y) > 1e-8)
3203 })
3204 })
3205 .unwrap_or_else(|| {
3206 let sum_x: f64 = hv.iter().map(|&vid| v_2d[to_lv(vid)].x).sum();
3207 let sum_y: f64 = hv.iter().map(|&vid| v_2d[to_lv(vid)].y).sum();
3208 Point2D {
3209 x: sum_x / (hv.len() as f64),
3210 y: sum_y / (hv.len() as f64),
3211 }
3212 });
3213
3214 let mut best_idx = None;
3215 let mut best_area = f64::INFINITY;
3216 for (o_idx, (_, _, o_area)) in outer_cycles.iter().enumerate() {
3217 if *o_area <= h_area + 1e-11 {
3218 continue;
3219 }
3220 if loops_winding_2d(test_pt, core::slice::from_ref(&outer_2d_loops[o_idx])) != 0
3221 && *o_area < best_area
3222 {
3223 best_area = *o_area;
3224 best_idx = Some(o_idx);
3225 }
3226 }
3227 if let Some(o_idx) = best_idx {
3228 patch_holes_vids[o_idx].push(hv);
3229 patch_holes_edges[o_idx].extend(he);
3230 }
3231 }
3232
3233 for (o_idx, (ov, mut oe, o_area)) in outer_cycles.into_iter().enumerate() {
3234 let hv = core::mem::take(&mut patch_holes_vids[o_idx]);
3235 oe.extend(core::mem::take(&mut patch_holes_edges[o_idx]));
3236
3237 let mut net_area = o_area;
3238 for h_ring in &hv {
3239 let h_pts: Vec<Point2D> = h_ring.iter().map(|&vid| v_2d[to_lv(vid)]).collect();
3240 net_area -= signed_area_2d_pts(&h_pts).abs();
3241 }
3242 if net_area <= 1e-11 {
3243 continue;
3244 }
3245
3246 let sample_2d = compute_patch_interior_sample_2d(&ov, &hv, verts, pl);
3247
3248 let mut delta_w = vec![0i32; body_count];
3249 if on_plane_faces.len() == 1 && winding_rule.apply(1.0) {
3250 let nf = &norm_faces[on_plane_faces[0]];
3251 let all_interior_or_ccw = oe.iter().all(|&(e_id, dir_sign)| {
3252 let fw = &edges[e_id].face_windings;
3253 fw.is_empty() || (fw.len() == 1 && fw[0].1 == dir_sign)
3254 });
3255 if all_interior_or_ccw {
3256 delta_w[nf.body_index] = nf.orient_sign;
3257 } else {
3258 let w2d = consolidated_face_winding_2d(
3259 sample_2d,
3260 on_plane_faces[0],
3261 &surviving_edges,
3262 edges,
3263 verts,
3264 pl,
3265 );
3266 if w2d != 0 && winding_rule.apply(w2d as f64) {
3267 delta_w[nf.body_index] += nf.orient_sign;
3268 }
3269 }
3270 } else {
3271 for &nf_idx in on_plane_faces {
3272 let nf = &norm_faces[nf_idx];
3273 let w2d = consolidated_face_winding_2d(
3274 sample_2d,
3275 nf_idx,
3276 &surviving_edges,
3277 edges,
3278 verts,
3279 pl,
3280 );
3281 if w2d != 0 && winding_rule.apply(w2d as f64) {
3282 delta_w[nf.body_index] += nf.orient_sign;
3283 }
3284 }
3285 }
3286
3287 if delta_w.iter().any(|&dw| dw != 0) {
3288 out_patches.push(FacePatch3D {
3289 plane_id: p_id,
3290 merged_planes: vec![p_id],
3291 directed_edges: oe,
3292 outer_vids: ov,
3293 holes_vids: hv,
3294 sample_2d,
3295 area_2d: net_area,
3296 delta_w,
3297 alive: true,
3298 });
3299 }
3300 }
3301}
3302
3303pub fn execute_brep_3d<F>(
3305 bodies: &[&Solid3D],
3306 mut predicate: F,
3307 options: KernelOptions,
3308) -> BRepResult3D
3309where
3310 F: FnMut(&[f64]) -> bool,
3311{
3312 let body_count = bodies.len();
3313 if body_count == 0 {
3314 return BRepResult3D::default();
3315 }
3316
3317 let mut min_x = f64::INFINITY;
3319 let mut min_y = f64::INFINITY;
3320 let mut min_z = f64::INFINITY;
3321 let mut max_x = f64::NEG_INFINITY;
3322 let mut max_y = f64::NEG_INFINITY;
3323 let mut max_z = f64::NEG_INFINITY;
3324 let mut any_pt = false;
3325
3326 for solid in bodies {
3327 for face in &solid.faces {
3328 for pt in &face.outer {
3329 any_pt = true;
3330 min_x = min_x.min(pt.x);
3331 min_y = min_y.min(pt.y);
3332 min_z = min_z.min(pt.z);
3333 max_x = max_x.max(pt.x);
3334 max_y = max_y.max(pt.y);
3335 max_z = max_z.max(pt.z);
3336 }
3337 }
3338 }
3339
3340 if !any_pt {
3341 return BRepResult3D::default();
3342 }
3343
3344 let center = Point3D::new(
3345 0.5 * (min_x + max_x),
3346 0.5 * (min_y + max_y),
3347 0.5 * (min_z + max_z),
3348 );
3349 let dx = max_x - min_x;
3350 let dy = max_y - min_y;
3351 let dz = max_z - min_z;
3352 let diagonal = (dx * dx + dy * dy + dz * dz).sqrt().max(1.0);
3353 let r0 = options
3354 .snap_tolerance
3355 .unwrap_or(1e-8 * diagonal)
3356 .max(1e-9 * diagonal);
3357 let plane_dist_tol = r0;
3358 let plane_cos_tol = 1.0 - 1e-9;
3359
3360 let mut planes: Vec<CanonicalPlane> = Vec::new();
3362 let mut plane_buckets: [Vec<usize>; 256] = std::array::from_fn(|_| Vec::new());
3363 let use_plane_grid = plane_dist_tol <= 1e-4 * diagonal;
3364 let mut norm_faces: Vec<NormalizedInputFace> = Vec::new();
3365
3366 for (b_idx, solid) in bodies.iter().enumerate() {
3367 for face in &solid.faces {
3368 if face.outer.len() < 3 {
3369 continue;
3370 }
3371 let mut shifted_outer: Vec<Point3D> =
3372 face.outer.iter().map(|p| p.sub(center)).collect();
3373 let raw_normal = match compute_newell_normal(&shifted_outer) {
3374 Some(n) => n,
3375 None => continue,
3376 };
3377
3378 let mut f_min = Point3D::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
3379 let mut f_max = Point3D::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
3380 let mut avg = Point3D::default();
3381 for &p in &shifted_outer {
3382 avg = avg.add(p);
3383 f_min.x = f_min.x.min(p.x);
3384 f_min.y = f_min.y.min(p.y);
3385 f_min.z = f_min.z.min(p.z);
3386 f_max.x = f_max.x.max(p.x);
3387 f_max.y = f_max.y.max(p.y);
3388 f_max.z = f_max.z.max(p.z);
3389 }
3390 avg = avg.scale(1.0 / (shifted_outer.len() as f64));
3391 let raw_d = raw_normal.dot(avg);
3392 let (can_n, can_d, orient_sign) = canonicalize_plane(raw_normal, raw_d);
3393
3394 let mut found_plane = None;
3395 if use_plane_grid {
3396 let bx0 = (((can_n.x + 1.0) * 7.5 - 1e-3).floor() as isize).clamp(0, 15) as usize;
3397 let bx1 = (((can_n.x + 1.0) * 7.5 + 1e-3).ceil() as isize).clamp(0, 15) as usize;
3398 let by0 = (((can_n.y + 1.0) * 7.5 - 1e-3).floor() as isize).clamp(0, 15) as usize;
3399 let by1 = (((can_n.y + 1.0) * 7.5 + 1e-3).ceil() as isize).clamp(0, 15) as usize;
3400 'search_bucket: for bx in bx0..=bx1 {
3401 for by in by0..=by1 {
3402 for &p_idx in &plane_buckets[(bx << 4) | by] {
3403 let pl = &mut planes[p_idx];
3404 if (pl.d - can_d).abs() <= plane_dist_tol
3405 && pl.normal.dot(can_n) >= plane_cos_tol
3406 {
3407 pl.min_pt.x = pl.min_pt.x.min(f_min.x);
3408 pl.min_pt.y = pl.min_pt.y.min(f_min.y);
3409 pl.min_pt.z = pl.min_pt.z.min(f_min.z);
3410 pl.max_pt.x = pl.max_pt.x.max(f_max.x);
3411 pl.max_pt.y = pl.max_pt.y.max(f_max.y);
3412 pl.max_pt.z = pl.max_pt.z.max(f_max.z);
3413 found_plane = Some(p_idx);
3414 break 'search_bucket;
3415 }
3416 }
3417 }
3418 }
3419 } else {
3420 for (p_idx, pl) in planes.iter_mut().enumerate() {
3421 if (pl.d - can_d).abs() <= plane_dist_tol
3422 && pl.normal.dot(can_n) >= plane_cos_tol
3423 {
3424 pl.min_pt.x = pl.min_pt.x.min(f_min.x);
3425 pl.min_pt.y = pl.min_pt.y.min(f_min.y);
3426 pl.min_pt.z = pl.min_pt.z.min(f_min.z);
3427 pl.max_pt.x = pl.max_pt.x.max(f_max.x);
3428 pl.max_pt.y = pl.max_pt.y.max(f_max.y);
3429 pl.max_pt.z = pl.max_pt.z.max(f_max.z);
3430 found_plane = Some(p_idx);
3431 break;
3432 }
3433 }
3434 }
3435
3436 let plane_id = match found_plane {
3437 Some(idx) => idx,
3438 None => {
3439 let (u_axis, v_axis) = build_plane_frame(can_n);
3440 let origin = can_n.scale(can_d);
3441 let idx = planes.len();
3442 planes.push(CanonicalPlane {
3443 origin,
3444 u_axis,
3445 v_axis,
3446 normal: can_n,
3447 d: can_d,
3448 min_pt: f_min,
3449 max_pt: f_max,
3450 });
3451 if use_plane_grid {
3452 let bx = (((can_n.x + 1.0) * 7.5).floor() as isize).clamp(0, 15) as usize;
3453 let by = (((can_n.y + 1.0) * 7.5).floor() as isize).clamp(0, 15) as usize;
3454 plane_buckets[(bx << 4) | by].push(idx);
3455 }
3456 idx
3457 }
3458 };
3459
3460 let pl = &planes[plane_id];
3461 let mut outer_2d: Vec<Point2D> =
3462 shifted_outer.iter().map(|&p| pl.project_2d(p)).collect();
3463 let raw_area = signed_area_2d_pts(&outer_2d);
3464 if raw_area < 0.0 {
3465 outer_2d.reverse();
3466 shifted_outer.reverse();
3467 }
3468 let area_2d = raw_area.abs();
3469
3470 let mut holes_2d: Vec<Vec<Point2D>> = Vec::with_capacity(face.holes.len());
3471 let mut holes_3d_loops: Vec<Vec<Point3D>> = Vec::with_capacity(face.holes.len());
3472 for hole in &face.holes {
3473 if hole.len() < 3 {
3474 continue;
3475 }
3476 let mut shifted_h: Vec<Point3D> = hole.iter().map(|p| p.sub(center)).collect();
3477 let mut h_2d: Vec<Point2D> =
3478 shifted_h.iter().map(|&p| pl.project_2d(p)).collect();
3479 if signed_area_2d_pts(&h_2d) > 0.0 {
3480 h_2d.reverse();
3481 shifted_h.reverse();
3482 }
3483 holes_2d.push(h_2d);
3484 holes_3d_loops.push(shifted_h);
3485 }
3486
3487 let mut loops_2d = Vec::with_capacity(1 + holes_2d.len());
3488 loops_2d.push(outer_2d);
3489 loops_2d.extend(holes_2d);
3490
3491 let mut loops_3d = Vec::with_capacity(1 + holes_3d_loops.len());
3492 loops_3d.push(shifted_outer);
3493 loops_3d.extend(holes_3d_loops);
3494
3495 norm_faces.push(NormalizedInputFace {
3496 body_index: b_idx,
3497 plane_id,
3498 orient_sign,
3499 area_2d,
3500 loops_2d,
3501 loops_3d,
3502 min_pt: f_min,
3503 max_pt: f_max,
3504 });
3505 }
3506 }
3507
3508 let mut faces_on_plane: Vec<Vec<usize>> = vec![Vec::new(); planes.len()];
3509 let mut faces_by_body: Vec<Vec<usize>> = vec![Vec::new(); body_count];
3510 let mut body_min = vec![
3511 Point3D::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
3512 body_count
3513 ];
3514 let mut body_max = vec![
3515 Point3D::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
3516 body_count
3517 ];
3518 for (f_idx, nf) in norm_faces.iter().enumerate() {
3519 faces_on_plane[nf.plane_id].push(f_idx);
3520 let b = nf.body_index;
3521 faces_by_body[b].push(f_idx);
3522 body_min[b].x = body_min[b].x.min(nf.min_pt.x);
3523 body_min[b].y = body_min[b].y.min(nf.min_pt.y);
3524 body_min[b].z = body_min[b].z.min(nf.min_pt.z);
3525 body_max[b].x = body_max[b].x.max(nf.max_pt.x);
3526 body_max[b].y = body_max[b].y.max(nf.max_pt.y);
3527 body_max[b].z = body_max[b].z.max(nf.max_pt.z);
3528 }
3529
3530 let r_init = 0.5 * r0;
3531 let r_max = 2.0 * r0;
3532
3533 let mut verts: Vec<Vert3D> = Vec::new();
3535 let mut edges: Vec<Edge3D> = Vec::new();
3536 let mut vert_adj: Vec<Vec<usize>> = Vec::new();
3537 let mut nf_outer_vids: Vec<Vec<u32>> = vec![Vec::new(); norm_faces.len()];
3538 let mut nf_all_vids: Vec<Vec<u32>> = vec![Vec::new(); norm_faces.len()];
3539
3540 let x_span_inv = if dx > 1e-9 { 15.0 / dx } else { 0.0 };
3542 let y_span_inv = if dy > 1e-9 { 15.0 / dy } else { 0.0 };
3543 let x_low = min_x - center.x;
3544 let y_low = min_y - center.y;
3545 let mut vert_xy_buckets: [Vec<u32>; 256] = std::array::from_fn(|_| Vec::new());
3546
3547 for (nf_idx, nf) in norm_faces.iter().enumerate() {
3548 let p_tag = [nf.plane_id];
3549 let fw_tag = [(nf_idx, 1i32)];
3550 for (loop_idx, ring_3d) in nf.loops_3d.iter().enumerate() {
3551 let n = ring_3d.len();
3552 if n < 3 {
3553 continue;
3554 }
3555 let mut ring_vids: Vec<u32> = Vec::with_capacity(n);
3556 for &pt in ring_3d {
3557 let vid = if r0 <= 1e-4 * diagonal && dx > 1e-9 && dy > 1e-9 {
3558 let bx_min = (((pt.x - r0 - x_low) * x_span_inv).floor() as isize).clamp(0, 15) as usize;
3559 let bx_max = (((pt.x + r0 - x_low) * x_span_inv).ceil() as isize).clamp(0, 15) as usize;
3560 let by_min = (((pt.y - r0 - y_low) * y_span_inv).floor() as isize).clamp(0, 15) as usize;
3561 let by_max = (((pt.y + r0 - y_low) * y_span_inv).ceil() as isize).clamp(0, 15) as usize;
3562 let mut best_v = None;
3563 let mut best_d_sq = r0 * r0;
3564 for bx in bx_min..=bx_max {
3565 for by in by_min..=by_max {
3566 for &cand_id in &vert_xy_buckets[(bx << 4) | by] {
3567 let v_cand = &verts[cand_id as usize];
3568 if !v_cand.alive {
3569 continue;
3570 }
3571 let d_sq = v_cand.pos.sub(pt).norm_sq();
3572 if d_sq <= best_d_sq {
3573 best_d_sq = d_sq;
3574 best_v = Some(cand_id);
3575 }
3576 }
3577 }
3578 }
3579 if let Some(found_id) = best_v {
3580 let root = resolve_vert_3d(&mut verts, found_id);
3581 if merge_plane_tags(&mut verts[root as usize].planes, &p_tag) {
3582 verts[root as usize].dirty_proj = true;
3583 }
3584 root
3585 } else {
3586 let id = verts.len() as u32;
3587 let mut planes_v = Vec::with_capacity(4);
3588 merge_plane_tags(&mut planes_v, &p_tag);
3589 verts.push(Vert3D {
3590 pos: pt,
3591 rep: pt,
3592 radius: r_init,
3593 weight: 1,
3594 parent: id,
3595 planes: planes_v,
3596 dirty_proj: true,
3597 alive: true,
3598 });
3599 let bx_cur = (((pt.x - x_low) * x_span_inv).floor() as isize).clamp(0, 15) as usize;
3600 let by_cur = (((pt.y - y_low) * y_span_inv).floor() as isize).clamp(0, 15) as usize;
3601 vert_xy_buckets[(bx_cur << 4) | by_cur].push(id);
3602 id
3603 }
3604 } else {
3605 find_or_create_vert_3d(&mut verts, pt, r0, r_init, &p_tag)
3606 };
3607 if vert_adj.len() < verts.len() {
3608 vert_adj.resize_with(verts.len(), Vec::new);
3609 }
3610 ring_vids.push(vid);
3611 }
3612 for i in 0..n {
3613 let u = resolve_vert_3d(&mut verts, ring_vids[i]);
3614 let v = resolve_vert_3d(&mut verts, ring_vids[(i + 1) % n]);
3615 ring_vids[i] = u;
3616 if u == v {
3617 continue;
3618 }
3619 let (a, b, sign) = if u < v { (u, v, 1) } else { (v, u, -1) };
3620 let mut found_e = false;
3621 for &e_id in &vert_adj[a as usize] {
3622 let e = &mut edges[e_id];
3623 if e.alive && e.v0 == a && e.v1 == b {
3624 merge_plane_tags(&mut e.planes, &p_tag);
3625 merge_face_windings(&mut e.face_windings, &fw_tag, sign);
3626 found_e = true;
3627 break;
3628 }
3629 }
3630 if !found_e {
3631 let e_id = edges.len();
3632 let mut planes_vec = Vec::with_capacity(4);
3633 merge_plane_tags(&mut planes_vec, &p_tag);
3634 let mut fw = Vec::with_capacity(2);
3635 merge_face_windings(&mut fw, &fw_tag, sign);
3636 edges.push(Edge3D {
3637 v0: a,
3638 v1: b,
3639 planes: planes_vec,
3640 face_windings: fw,
3641 alive: true,
3642 });
3643 vert_adj[a as usize].push(e_id);
3644 }
3645 }
3646 if loop_idx == 0 {
3647 nf_outer_vids[nf_idx] = ring_vids.clone();
3648 }
3649 nf_all_vids[nf_idx].extend_from_slice(&ring_vids);
3650 }
3651 }
3652
3653 let num_stage1a_verts = verts.len();
3654 let num_stage1a_edges = edges.len();
3655 let all_bodies_closed = edges[..num_stage1a_edges].iter().all(|e| {
3656 if !e.alive {
3657 return true;
3658 }
3659 if e.face_windings.len() != 2 {
3660 return false;
3661 }
3662 let (f0, s0) = e.face_windings[0];
3663 let (f1, s1) = e.face_windings[1];
3664 norm_faces[f0].body_index == norm_faces[f1].body_index
3665 && s0 * norm_faces[f0].orient_sign + s1 * norm_faces[f1].orient_sign == 0
3666 });
3667 let mut plane_active_1b: Vec<bool> = vec![false; planes.len()];
3668
3669 let skip_closed_same_body = all_bodies_closed && r0 <= 1e-4 * diagonal && body_count >= 2;
3671 let z_low = min_z - center.z;
3672 let vox_x_inv = if dx > 1e-9 { 31.0 / dx } else { 0.0 };
3673 let vox_y_inv = if dy > 1e-9 { 31.0 / dy } else { 0.0 };
3674 let vox_z_inv = if dz > 1e-9 { 31.0 / dz } else { 0.0 };
3675 let vox_pad = 4.0 * r_max + plane_dist_tol;
3676
3677 let mut body_vox: Vec<[u32; 1024]> = vec![[0u32; 1024]; body_count];
3678 if body_count >= 2 && r0 <= 1e-4 * diagonal {
3679 for nf in &norm_faces {
3680 let b = nf.body_index;
3681 let vx0 = (((nf.min_pt.x - vox_pad - x_low) * vox_x_inv).floor() as isize).clamp(0, 31) as usize;
3682 let vx1 = (((nf.max_pt.x + vox_pad - x_low) * vox_x_inv).floor() as isize).clamp(0, 31) as usize;
3683 let vy0 = (((nf.min_pt.y - vox_pad - y_low) * vox_y_inv).floor() as isize).clamp(0, 31) as usize;
3684 let vy1 = (((nf.max_pt.y + vox_pad - y_low) * vox_y_inv).floor() as isize).clamp(0, 31) as usize;
3685 let vz0 = (((nf.min_pt.z - vox_pad - z_low) * vox_z_inv).floor() as isize).clamp(0, 31) as u32;
3686 let vz1 = (((nf.max_pt.z + vox_pad - z_low) * vox_z_inv).floor() as isize).clamp(0, 31) as u32;
3687 let width = vz1 - vz0 + 1;
3688 let z_mask = if width >= 32 {
3689 u32::MAX
3690 } else {
3691 ((1u32 << width) - 1) << vz0
3692 };
3693 let vox = &mut body_vox[b];
3694 for vx in vx0..=vx1 {
3695 let row = vx << 5;
3696 for vy in vy0..=vy1 {
3697 vox[row | vy] |= z_mask;
3698 }
3699 }
3700 }
3701 }
3702
3703 let y64_inv = if dy > 1e-9 { 63.0 / dy } else { 0.0 };
3704 let z64_inv = if dz > 1e-9 { 63.0 / dz } else { 0.0 };
3705 let range_mask_64 = |lo: f64, hi: f64, base: f64, inv: f64| -> u64 {
3706 if inv == 0.0 {
3707 return u64::MAX;
3708 }
3709 let b0 = (((lo - base) * inv).floor() as isize).clamp(0, 63) as u32;
3710 let b1 = (((hi - base) * inv).floor() as isize).clamp(0, 63) as u32;
3711 let width = b1 - b0 + 1;
3712 if width >= 64 {
3713 u64::MAX
3714 } else {
3715 ((1u64 << width) - 1) << b0
3716 }
3717 };
3718
3719 let mut face_near_other_vox: Vec<bool> = vec![true; norm_faces.len()];
3720 if body_count >= 2 && r0 <= 1e-4 * diagonal {
3721 let mut vox_cands: Vec<(f64, u64, u64, usize)> = Vec::new();
3722 for (nf_idx, nf) in norm_faces.iter().enumerate() {
3723 let b0 = nf.body_index;
3724 let vx0 = (((nf.min_pt.x - x_low) * vox_x_inv).floor() as isize).clamp(0, 31) as usize;
3725 let vx1 = (((nf.max_pt.x - x_low) * vox_x_inv).floor() as isize).clamp(0, 31) as usize;
3726 let vy0 = (((nf.min_pt.y - y_low) * vox_y_inv).floor() as isize).clamp(0, 31) as usize;
3727 let vy1 = (((nf.max_pt.y - y_low) * vox_y_inv).floor() as isize).clamp(0, 31) as usize;
3728 let vz0 = (((nf.min_pt.z - z_low) * vox_z_inv).floor() as isize).clamp(0, 31) as u32;
3729 let vz1 = (((nf.max_pt.z - z_low) * vox_z_inv).floor() as isize).clamp(0, 31) as u32;
3730 let width = vz1 - vz0 + 1;
3731 let z_mask = if width >= 32 {
3732 u32::MAX
3733 } else {
3734 ((1u32 << width) - 1) << vz0
3735 };
3736 let mut near_any = false;
3737 'body_loop: for b in 0..body_count {
3738 if b == b0
3739 || !aabb_overlap_3d(nf.min_pt, nf.max_pt, body_min[b], body_max[b], vox_pad)
3740 {
3741 continue;
3742 }
3743 let vox = &body_vox[b];
3744 for vx in vx0..=vx1 {
3745 let row = vx << 5;
3746 for vy in vy0..=vy1 {
3747 if (vox[row | vy] & z_mask) != 0 {
3748 near_any = true;
3749 break 'body_loop;
3750 }
3751 }
3752 }
3753 }
3754 face_near_other_vox[nf_idx] = false;
3755 if near_any {
3756 let my = range_mask_64(nf.min_pt.y - vox_pad, nf.max_pt.y + vox_pad, y_low, y64_inv);
3757 let mz = range_mask_64(nf.min_pt.z - vox_pad, nf.max_pt.z + vox_pad, z_low, z64_inv);
3758 vox_cands.push((nf.min_pt.x, my, mz, nf_idx));
3759 }
3760 }
3761 vox_cands.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
3763 for i in 0..vox_cands.len() {
3764 let (_, my_i, mz_i, fi) = vox_cands[i];
3765 let nf_i = &norm_faces[fi];
3766 let max_x_i = nf_i.max_pt.x + vox_pad;
3767 for j in (i + 1)..vox_cands.len() {
3768 let (min_x_j, my_j, mz_j, fj) = vox_cands[j];
3769 if min_x_j > max_x_i {
3770 break;
3771 }
3772 if (my_i & my_j) == 0 || (mz_i & mz_j) == 0 {
3773 continue;
3774 }
3775 let nf_j = &norm_faces[fj];
3776 if nf_i.body_index != nf_j.body_index
3777 && aabb_overlap_3d(nf_i.min_pt, nf_i.max_pt, nf_j.min_pt, nf_j.max_pt, vox_pad)
3778 {
3779 face_near_other_vox[fi] = true;
3780 face_near_other_vox[fj] = true;
3781 }
3782 }
3783 }
3784 }
3785
3786 let all_planes_single_body = skip_closed_same_body
3787 && faces_on_plane.iter().all(|op| {
3788 op.len() <= 1
3789 || op
3790 .iter()
3791 .all(|&fi| norm_faces[fi].body_index == norm_faces[op[0]].body_index)
3792 });
3793
3794 let mut faces_by_min_x: Vec<(f64, u64, u64, usize)> =
3795 Vec::with_capacity(norm_faces.len());
3796 for (idx, nf) in norm_faces.iter().enumerate() {
3797 if all_planes_single_body && !face_near_other_vox[idx] {
3798 continue;
3799 }
3800 let my = range_mask_64(nf.min_pt.y, nf.max_pt.y, y_low, y64_inv);
3801 let mz = range_mask_64(nf.min_pt.z, nf.max_pt.z, z_low, z64_inv);
3802 faces_by_min_x.push((nf.min_pt.x, my, mz, idx));
3803 }
3804 faces_by_min_x
3805 .sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
3806
3807 let mut on_plane_vids_buf: Vec<u32> = Vec::with_capacity(8);
3808 for (p_id, pl) in planes.iter().enumerate() {
3809 let on_plane_faces = &faces_on_plane[p_id];
3810 if on_plane_faces.is_empty() {
3811 continue;
3812 }
3813 let single_body_on_plane = if skip_closed_same_body {
3814 let b0 = norm_faces[on_plane_faces[0]].body_index;
3815 if on_plane_faces.iter().all(|&fi| norm_faces[fi].body_index == b0) {
3816 Some(b0)
3817 } else {
3818 None
3819 }
3820 } else {
3821 None
3822 };
3823 if single_body_on_plane.is_some()
3824 && !on_plane_faces.iter().any(|&fi| face_near_other_vox[fi])
3825 {
3826 continue;
3827 }
3828 let pl_mask_y = range_mask_64(
3829 pl.min_pt.y - plane_dist_tol,
3830 pl.max_pt.y + plane_dist_tol,
3831 y_low,
3832 y64_inv,
3833 );
3834 let pl_mask_z = range_mask_64(
3835 pl.min_pt.z - plane_dist_tol,
3836 pl.max_pt.z + plane_dist_tol,
3837 z_low,
3838 z64_inv,
3839 );
3840 let max_px = pl.max_pt.x + plane_dist_tol;
3841 for &(other_min_x, nf_mask_y, nf_mask_z, other_idx) in &faces_by_min_x {
3842 if other_min_x > max_px {
3843 break;
3844 }
3845 if (pl_mask_y & nf_mask_y) == 0 || (pl_mask_z & nf_mask_z) == 0 {
3846 continue;
3847 }
3848 let other_nf = &norm_faces[other_idx];
3849 if other_nf.plane_id == p_id {
3850 continue;
3851 }
3852 if single_body_on_plane == Some(other_nf.body_index) {
3853 continue;
3854 }
3855 if !aabb_overlap_3d(
3856 pl.min_pt,
3857 pl.max_pt,
3858 other_nf.min_pt,
3859 other_nf.max_pt,
3860 plane_dist_tol,
3861 ) {
3862 continue;
3863 }
3864 if !plane_straddles_aabb(
3865 pl.normal,
3866 pl.d,
3867 other_nf.min_pt,
3868 other_nf.max_pt,
3869 plane_dist_tol,
3870 ) {
3871 continue;
3872 }
3873 let other_pl = &planes[other_nf.plane_id];
3874 if !plane_straddles_aabb(
3875 other_pl.normal,
3876 other_pl.d,
3877 pl.min_pt,
3878 pl.max_pt,
3879 plane_dist_tol,
3880 ) {
3881 continue;
3882 }
3883
3884 if r0 <= 1e-4 * diagonal && on_plane_faces.len() == 1 {
3889 let tf_idx = on_plane_faces[0];
3890 let tf = &norm_faces[tf_idx];
3891 if tf.loops_3d.len() == 1
3892 && tf.loops_3d[0].len() <= 4
3893 && other_nf.loops_3d.len() == 1
3894 && other_nf.loops_3d[0].len() <= 4
3895 {
3896 let tf_vids = &nf_all_vids[tf_idx];
3897 let other_vids = &nf_all_vids[other_idx];
3898 let mut shared_vids = 0usize;
3899 for &uv in other_vids {
3900 if tf_vids.contains(&uv) {
3901 shared_vids += 1;
3902 }
3903 }
3904 if shared_vids >= 2 {
3905 continue;
3906 }
3907 }
3908 }
3909
3910 let mut pos_count = 0usize;
3914 let mut neg_count = 0usize;
3915 on_plane_vids_buf.clear();
3916 let mut v_cursor = 0usize;
3917 for ring_3d in &other_nf.loops_3d {
3918 for &pt in ring_3d {
3919 let vid = nf_all_vids[other_idx][v_cursor];
3920 v_cursor += 1;
3921 let h = pl.normal.dot(pt) - pl.d;
3922 if h > r0 {
3923 pos_count += 1;
3924 } else if h < -r0 {
3925 neg_count += 1;
3926 } else if !on_plane_vids_buf.contains(&vid) {
3927 on_plane_vids_buf.push(vid);
3928 }
3929 }
3930 }
3931 let strictly_straddles = pos_count > 0 && neg_count > 0;
3932 if !strictly_straddles {
3933 if on_plane_vids_buf.len() <= 1 {
3934 continue;
3935 }
3936 if on_plane_vids_buf.len() == 2 && r0 <= 1e-4 * diagonal {
3937 let va = on_plane_vids_buf[0].min(on_plane_vids_buf[1]);
3938 let vb = on_plane_vids_buf[0].max(on_plane_vids_buf[1]);
3939 if (va as usize) < vert_adj.len() {
3940 let already_shared_edge = vert_adj[va as usize].iter().any(|&e_id| {
3941 let e = &edges[e_id];
3942 e.alive && e.v0 == va && e.v1 == vb && e.planes.contains(&p_id)
3943 });
3944 if already_shared_edge {
3945 continue;
3946 }
3947 }
3948 }
3949 }
3950
3951 let near_tol = r0.max(1e-7);
3954 let any_target_active = on_plane_faces.iter().any(|&tf_idx| {
3955 let tf = &norm_faces[tf_idx];
3956 if !aabb_overlap_3d(
3957 tf.min_pt,
3958 tf.max_pt,
3959 other_nf.min_pt,
3960 other_nf.max_pt,
3961 r0,
3962 ) {
3963 return false;
3964 }
3965 let mut has_pos = false;
3966 let mut has_neg = false;
3967 for ring_3d in &tf.loops_3d {
3968 for &pt in ring_3d {
3969 let h = other_pl.normal.dot(pt) - other_pl.d;
3970 if h >= -near_tol {
3971 has_pos = true;
3972 }
3973 if h <= near_tol {
3974 has_neg = true;
3975 }
3976 if has_pos && has_neg {
3977 return true;
3978 }
3979 }
3980 }
3981 false
3982 });
3983 if !any_target_active {
3984 continue;
3985 }
3986
3987 if inject_face_plane_chords_3d(
3988 other_nf,
3989 other_pl,
3990 pl,
3991 p_id,
3992 on_plane_faces,
3993 &norm_faces,
3994 r0,
3995 r_init,
3996 options.winding_rule,
3997 &mut verts,
3998 &mut edges,
3999 &mut vert_xy_buckets,
4000 &mut vert_adj,
4001 x_low,
4002 y_low,
4003 x_span_inv,
4004 y_span_inv,
4005 ) {
4006 plane_active_1b[p_id] = true;
4007 plane_active_1b[other_nf.plane_id] = true;
4008 }
4009 }
4010 }
4011
4012 let mut vert_active: Vec<bool> = if r0 > 1e-4 * diagonal || body_count < 2 {
4015 vec![true; verts.len()]
4016 } else {
4017 let mut active = vec![false; verts.len()];
4018 for v_idx in num_stage1a_verts..verts.len() {
4019 active[v_idx] = true;
4020 }
4021 for (p_id, on_plane) in faces_on_plane.iter().enumerate() {
4022 let multi_body_or_unclosed = on_plane.len() > 1
4023 && (!all_bodies_closed
4024 || on_plane
4025 .iter()
4026 .any(|&fi| norm_faces[fi].body_index != norm_faces[on_plane[0]].body_index));
4027 if plane_active_1b[p_id] || multi_body_or_unclosed {
4028 for &nf_idx in on_plane {
4029 for &vid in &nf_all_vids[nf_idx] {
4030 let r = resolve_vert_3d(&mut verts, vid) as usize;
4031 active[r] = true;
4032 }
4033 }
4034 }
4035 }
4036 for (nf_idx, &near_other) in face_near_other_vox.iter().enumerate() {
4037 if near_other {
4038 for &vid in &nf_all_vids[nf_idx] {
4039 let r = resolve_vert_3d(&mut verts, vid) as usize;
4040 active[r] = true;
4041 }
4042 }
4043 }
4044 let min_clean_len_sq = 4.0 * r_max * r_max;
4045 for (e_idx, e) in edges.iter().enumerate() {
4046 if !e.alive {
4047 continue;
4048 }
4049 let u = e.v0 as usize;
4050 let v = e.v1 as usize;
4051 if e_idx >= num_stage1a_edges {
4052 active[u] = true;
4053 active[v] = true;
4054 continue;
4055 }
4056 let is_clean_2manifold = if e.face_windings.len() == 2 {
4057 let (f0, s0) = e.face_windings[0];
4058 let (f1, s1) = e.face_windings[1];
4059 norm_faces[f0].body_index == norm_faces[f1].body_index
4060 && s0 * norm_faces[f0].orient_sign + s1 * norm_faces[f1].orient_sign == 0
4061 && verts[u].pos.sub(verts[v].pos).norm_sq() > min_clean_len_sq
4062 } else {
4063 false
4064 };
4065 if !is_clean_2manifold {
4066 active[u] = true;
4067 active[v] = true;
4068 }
4069 }
4070 let seed_active = active.clone();
4072 for e in &edges[..num_stage1a_edges] {
4073 if !e.alive {
4074 continue;
4075 }
4076 let u = e.v0 as usize;
4077 let v = e.v1 as usize;
4078 if seed_active[u] || seed_active[v] {
4079 active[u] = true;
4080 active[v] = true;
4081 }
4082 }
4083 for (v_idx, &is_act) in active.iter().enumerate() {
4084 if !is_act {
4085 verts[v_idx].dirty_proj = false;
4086 }
4087 }
4088 active
4089 };
4090
4091 consolidate_3d_complex(
4093 &mut verts,
4094 &mut edges,
4095 &planes,
4096 r0,
4097 r_init,
4098 r_max,
4099 &mut vert_active,
4100 &mut vert_xy_buckets,
4101 &mut vert_adj,
4102 x_low,
4103 y_low,
4104 x_span_inv,
4105 y_span_inv,
4106 );
4107
4108 for ring in &mut nf_outer_vids {
4110 for vid in ring.iter_mut() {
4111 *vid = resolve_vert_3d(&mut verts, *vid);
4112 }
4113 }
4114
4115 let mut edges_by_plane: Vec<Vec<usize>> = vec![Vec::new(); planes.len()];
4117 for (e_id, e) in edges.iter().enumerate() {
4118 if !e.alive || e.v0 == e.v1 {
4119 continue;
4120 }
4121 for &p_id in &e.planes {
4122 edges_by_plane[p_id].push(e_id);
4123 }
4124 }
4125
4126 let mut patches: Vec<FacePatch3D> = Vec::new();
4127 let mut fast_dir_edges: Vec<(usize, i32)> = Vec::with_capacity(8);
4128 for (p_id, pl) in planes.iter().enumerate() {
4129 let on_plane = &faces_on_plane[p_id];
4130 let plane_edges = &edges_by_plane[p_id];
4131 if on_plane.is_empty() || plane_edges.len() < 3 {
4132 continue;
4133 }
4134
4135 let can_try_passive = if on_plane.len() == 1 {
4140 let nf_idx = on_plane[0];
4141 let nf = &norm_faces[nf_idx];
4142 let k = nf_outer_vids[nf_idx].len();
4143 nf.loops_3d.len() == 1 && (k == 3 || k == 4) && plane_edges.len() == k && nf.area_2d > 1e-11
4144 } else {
4145 all_bodies_closed
4146 && !plane_active_1b[p_id]
4147 && r0 <= 1e-4 * diagonal
4148 && on_plane.iter().all(|&fi| {
4149 let nf = &norm_faces[fi];
4150 let k = nf_outer_vids[fi].len();
4151 nf.body_index == norm_faces[on_plane[0]].body_index
4152 && nf.loops_3d.len() == 1
4153 && (k == 3 || k == 4)
4154 && nf.area_2d > 1e-11
4155 })
4156 && {
4157 let expected_he: usize = on_plane.iter().map(|&fi| nf_outer_vids[fi].len()).sum();
4158 let mut actual_he = 0usize;
4159 let mut all_from_plane = true;
4160 for &e_id in plane_edges {
4161 let c = edges[e_id]
4162 .face_windings
4163 .iter()
4164 .filter(|&&(fi, _)| norm_faces[fi].plane_id == p_id)
4165 .count();
4166 if c == 0 {
4167 all_from_plane = false;
4168 break;
4169 }
4170 actual_he += c;
4171 }
4172 all_from_plane && actual_he == expected_he
4173 }
4174 };
4175
4176 if can_try_passive && options.winding_rule.apply(1.0) {
4177 let start_patches_len = patches.len();
4178 let mut all_faces_ok = true;
4179 for &nf_idx in on_plane {
4180 let nf = &norm_faces[nf_idx];
4181 if nf.orient_sign == 0 {
4182 all_faces_ok = false;
4183 break;
4184 }
4185 let orig_vids = &nf_outer_vids[nf_idx];
4186 let k = orig_vids.len();
4187 let mut distinct_alive = true;
4188 for i in 0..k {
4189 let vi = orig_vids[i] as usize;
4190 if !verts[vi].alive {
4191 distinct_alive = false;
4192 break;
4193 }
4194 for j in (i + 1)..k {
4195 if orig_vids[i] == orig_vids[j] {
4196 distinct_alive = false;
4197 break;
4198 }
4199 }
4200 if !distinct_alive {
4201 break;
4202 }
4203 }
4204 if !distinct_alive {
4205 all_faces_ok = false;
4206 break;
4207 }
4208
4209 fast_dir_edges.clear();
4210 for i in 0..k {
4211 let u = orig_vids[i];
4212 let v = orig_vids[(i + 1) % k];
4213 let (a, b, sign) = if u < v { (u, v, 1) } else { (v, u, -1) };
4214 let mut matched_eid = None;
4215 for &e_id in plane_edges {
4216 let e = &edges[e_id];
4217 if e.v0 == a && e.v1 == b && e.face_windings.contains(&(nf_idx, sign)) {
4218 matched_eid = Some(e_id);
4219 break;
4220 }
4221 }
4222 if let Some(e_id) = matched_eid {
4223 fast_dir_edges.push((e_id, sign));
4224 } else {
4225 all_faces_ok = false;
4226 break;
4227 }
4228 }
4229 if !all_faces_ok {
4230 break;
4231 }
4232
4233 if k == 3 {
4234 let p0 = pl.project_2d(verts[orig_vids[0] as usize].rep);
4235 let p1 = pl.project_2d(verts[orig_vids[1] as usize].rep);
4236 let p2 = pl.project_2d(verts[orig_vids[2] as usize].rep);
4237 let area = 0.5 * cross_2d(p0, p1, p2);
4238 if area > 1e-11 {
4239 let sample_2d = Point2D {
4240 x: (p0.x + p1.x + p2.x) * (1.0 / 3.0),
4241 y: (p0.y + p1.y + p2.y) * (1.0 / 3.0),
4242 };
4243 let mut delta_w = vec![0i32; body_count];
4244 delta_w[nf.body_index] = nf.orient_sign;
4245 patches.push(FacePatch3D {
4246 plane_id: p_id,
4247 merged_planes: vec![p_id],
4248 directed_edges: fast_dir_edges.clone(),
4249 outer_vids: orig_vids.clone(),
4250 holes_vids: Vec::new(),
4251 sample_2d,
4252 area_2d: area,
4253 delta_w,
4254 alive: true,
4255 });
4256 } else {
4257 all_faces_ok = false;
4258 break;
4259 }
4260 } else {
4261 let p0 = pl.project_2d(verts[orig_vids[0] as usize].rep);
4262 let p1 = pl.project_2d(verts[orig_vids[1] as usize].rep);
4263 let p2 = pl.project_2d(verts[orig_vids[2] as usize].rep);
4264 let p3 = pl.project_2d(verts[orig_vids[3] as usize].rep);
4265 let a012 = 0.5 * cross_2d(p0, p1, p2);
4266 let a023 = 0.5 * cross_2d(p0, p2, p3);
4267 let a013 = 0.5 * cross_2d(p0, p1, p3);
4268 let a123 = 0.5 * cross_2d(p1, p2, p3);
4269 let sample_and_area = if a012 > 1e-11 && a023 > 1e-11 {
4270 let sample = if a012 >= a023 {
4271 Point2D {
4272 x: (p0.x + p1.x + p2.x) * (1.0 / 3.0),
4273 y: (p0.y + p1.y + p2.y) * (1.0 / 3.0),
4274 }
4275 } else {
4276 Point2D {
4277 x: (p0.x + p2.x + p3.x) * (1.0 / 3.0),
4278 y: (p0.y + p2.y + p3.y) * (1.0 / 3.0),
4279 }
4280 };
4281 Some((sample, a012 + a023))
4282 } else if a013 > 1e-11 && a123 > 1e-11 {
4283 let sample = if a013 >= a123 {
4284 Point2D {
4285 x: (p0.x + p1.x + p3.x) * (1.0 / 3.0),
4286 y: (p0.y + p1.y + p3.y) * (1.0 / 3.0),
4287 }
4288 } else {
4289 Point2D {
4290 x: (p1.x + p2.x + p3.x) * (1.0 / 3.0),
4291 y: (p1.y + p2.y + p3.y) * (1.0 / 3.0),
4292 }
4293 };
4294 Some((sample, a013 + a123))
4295 } else {
4296 None
4297 };
4298 if let Some((sample_2d, area)) = sample_and_area {
4299 let mut delta_w = vec![0i32; body_count];
4300 delta_w[nf.body_index] = nf.orient_sign;
4301 patches.push(FacePatch3D {
4302 plane_id: p_id,
4303 merged_planes: vec![p_id],
4304 directed_edges: fast_dir_edges.clone(),
4305 outer_vids: orig_vids.clone(),
4306 holes_vids: Vec::new(),
4307 sample_2d,
4308 area_2d: area,
4309 delta_w,
4310 alive: true,
4311 });
4312 } else {
4313 all_faces_ok = false;
4314 break;
4315 }
4316 }
4317 }
4318 if all_faces_ok {
4319 continue;
4320 }
4321 patches.truncate(start_patches_len);
4322 }
4323
4324 extract_plane_face_patches_3d(
4325 p_id,
4326 pl,
4327 plane_edges,
4328 on_plane,
4329 &norm_faces,
4330 &verts,
4331 &edges,
4332 body_count,
4333 options.winding_rule,
4334 &mut patches,
4335 );
4336 }
4337
4338 if patches.is_empty() {
4339 return BRepResult3D::default();
4340 }
4341
4342 let mut patches_by_min_edge: Vec<Vec<usize>> = vec![Vec::new(); edges.len()];
4344 for (i, patch) in patches.iter().enumerate() {
4345 if !patch.alive || patch.directed_edges.is_empty() {
4346 continue;
4347 }
4348 if let Some(min_e) = patch.directed_edges.iter().map(|&(e, _)| e).min() {
4349 patches_by_min_edge[min_e].push(i);
4350 }
4351 }
4352 for bucket in &patches_by_min_edge {
4353 if bucket.len() < 2 {
4354 continue;
4355 }
4356 for idx_a in 0..bucket.len() {
4357 let i = bucket[idx_a];
4358 if !patches[i].alive {
4359 continue;
4360 }
4361 for idx_b in (idx_a + 1)..bucket.len() {
4362 let j = bucket[idx_b];
4363 if !patches[j].alive
4364 || patches[j].directed_edges.len() != patches[i].directed_edges.len()
4365 {
4366 continue;
4367 }
4368 let (e0, s_i0) = patches[i].directed_edges[0];
4369 let Some(&(_, s_j0)) =
4370 patches[j].directed_edges.iter().find(|&&(e, _)| e == e0)
4371 else {
4372 continue;
4373 };
4374 let tau = s_i0 * s_j0;
4375 let same_edges = patches[i].directed_edges.iter().all(|&(e_id, s_i)| {
4376 patches[j]
4377 .directed_edges
4378 .iter()
4379 .any(|&(ej, s_j)| ej == e_id && s_i * s_j == tau)
4380 });
4381 if same_edges {
4382 let j_planes = core::mem::take(&mut patches[j].merged_planes);
4383 let j_dw = core::mem::take(&mut patches[j].delta_w);
4384 merge_plane_tags(&mut patches[i].merged_planes, &j_planes);
4385 for b in 0..body_count {
4386 patches[i].delta_w[b] += tau * j_dw[b];
4387 }
4388 patches[j].alive = false;
4389 }
4390 }
4391 if patches[i].delta_w.iter().all(|&dw| dw == 0) {
4392 patches[i].alive = false;
4393 }
4394 }
4395 }
4396
4397 loop {
4399 let mut edge_patch_deg: Vec<u32> = vec![0; edges.len()];
4400 for patch in &patches {
4401 if !patch.alive {
4402 continue;
4403 }
4404 for &(e_id, _) in &patch.directed_edges {
4405 edge_patch_deg[e_id] += 1;
4406 }
4407 }
4408 let mut pruned_any = false;
4409 for patch in &mut patches {
4410 if !patch.alive {
4411 continue;
4412 }
4413 if patch
4414 .directed_edges
4415 .iter()
4416 .any(|&(e_id, _)| edge_patch_deg[e_id] < 2)
4417 {
4418 patch.alive = false;
4419 pruned_any = true;
4420 }
4421 }
4422 if !pruned_any {
4423 break;
4424 }
4425 }
4426
4427 let m_patches = patches.len();
4428 if m_patches == 0 {
4429 return BRepResult3D::default();
4430 }
4431 let mut uf = DisjointSet::new(2 * m_patches);
4432 let mut edge_spokes: Vec<Vec<(usize, i32)>> = vec![Vec::new(); edges.len()];
4433 for (f_idx, patch) in patches.iter().enumerate() {
4434 if !patch.alive {
4435 continue;
4436 }
4437 for &(e_id, sign) in &patch.directed_edges {
4438 edge_spokes[e_id].push((f_idx, sign));
4439 }
4440 }
4441
4442 let patch_sample_3d: Vec<Point3D> = patches
4443 .iter()
4444 .map(|p| planes[p.plane_id].unproject_3d(p.sample_2d.x, p.sample_2d.y))
4445 .collect();
4446
4447 let mut patch_wb: Vec<Vec<f64>> = vec![vec![0.0; body_count]; m_patches];
4449 let mut patch_wf: Vec<Vec<f64>> = vec![vec![0.0; body_count]; m_patches];
4450 let mut patch_ib: Vec<Vec<i32>> = vec![vec![0; body_count]; m_patches];
4451 let mut patch_if: Vec<Vec<i32>> = vec![vec![0; body_count]; m_patches];
4452 let mut patch_evaluated: Vec<bool> = vec![false; m_patches];
4453
4454 let eval_patch_winding = |f_idx: usize,
4455 patch_evaluated: &mut [bool],
4456 patch_wb: &mut [Vec<f64>],
4457 patch_wf: &mut [Vec<f64>],
4458 patch_ib: &mut [Vec<i32>],
4459 patch_if: &mut [Vec<i32>]| {
4460 if patch_evaluated[f_idx] {
4461 return;
4462 }
4463 patch_evaluated[f_idx] = true;
4464 let patch = &patches[f_idx];
4465 if !patch.alive {
4466 return;
4467 }
4468 let p_3d = patch_sample_3d[f_idx];
4469 for b in 0..body_count {
4470 let dw = patch.delta_w[b];
4471 if dw == 0
4472 && (p_3d.x < body_min[b].x - plane_dist_tol
4473 || p_3d.x > body_max[b].x + plane_dist_tol
4474 || p_3d.y < body_min[b].y - plane_dist_tol
4475 || p_3d.y > body_max[b].y + plane_dist_tol
4476 || p_3d.z < body_min[b].z - plane_dist_tol
4477 || p_3d.z > body_max[b].z + plane_dist_tol)
4478 {
4479 continue;
4480 }
4481 let mut w_np = 0.0;
4482 for &nf_idx in &faces_by_body[b] {
4483 let other_nf = &norm_faces[nf_idx];
4484 if patch.merged_planes.contains(&other_nf.plane_id) {
4485 continue;
4486 }
4487 let other_pl = &planes[other_nf.plane_id];
4488 w_np += face_solid_angle_turns(p_3d, other_nf, other_pl);
4489 }
4490 let dw_f = dw as f64;
4491 let wb = w_np + 0.5 * dw_f;
4492 let wf = w_np - 0.5 * dw_f;
4493 patch_wb[f_idx][b] = wb;
4494 patch_wf[f_idx][b] = wf;
4495 patch_ib[f_idx][b] = wb.round() as i32;
4496 patch_if[f_idx][b] = wf.round() as i32;
4497 }
4498 };
4499
4500 for (e_id, spokes) in edge_spokes.iter_mut().enumerate() {
4501 let k = spokes.len();
4502 if k == 0 {
4503 continue;
4504 }
4505 if k == 1 {
4506 let (f_idx, _) = spokes[0];
4507 uf.union(2 * f_idx, 2 * f_idx + 1);
4508 continue;
4509 }
4510 if k == 2 {
4513 let (f0, sig0) = spokes[0];
4514 let (f1, sig1) = spokes[1];
4515 let cf_ccw0 = if sig0 == 1 { 2 * f0 } else { 2 * f0 + 1 };
4516 let cf_cw0 = if sig0 == 1 { 2 * f0 + 1 } else { 2 * f0 };
4517 let cf_ccw1 = if sig1 == 1 { 2 * f1 } else { 2 * f1 + 1 };
4518 let cf_cw1 = if sig1 == 1 { 2 * f1 + 1 } else { 2 * f1 };
4519 uf.union(cf_ccw0, cf_cw1);
4520 uf.union(cf_ccw1, cf_cw0);
4521 continue;
4522 }
4523
4524 let u = edges[e_id].v0 as usize;
4525 let v = edges[e_id].v1 as usize;
4526 let edge_vec = verts[v].rep.sub(verts[u].rep);
4527 let edge_len = edge_vec.norm();
4528 if edge_len <= 1e-15 {
4529 continue;
4530 }
4531 let t_vec = edge_vec.scale(1.0 / edge_len);
4532 let (e1, e2) = build_plane_frame(t_vec);
4533
4534 let mut radial: Vec<(f64, usize, i32, Point3D)> = Vec::with_capacity(k);
4535 for &(f_idx, sigma) in spokes.iter() {
4536 let n_p = planes[patches[f_idx].plane_id].normal;
4537 let b_dir = n_p.cross(t_vec).scale(sigma as f64);
4539 let b_unit = b_dir.normalized().unwrap_or(b_dir);
4540 let theta = b_dir.dot(e2).atan2(b_dir.dot(e1));
4541 radial.push((theta, f_idx, sigma, b_unit));
4542 }
4543 radial.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
4544
4545 let edge_mid = verts[u].rep.add(verts[v].rep).scale(0.5);
4548 for _pass in 0..k {
4549 let mut swapped = false;
4550 for idx in 0..k {
4551 let nxt = (idx + 1) % k;
4552 let (_, f_a, sig_a, b_a) = radial[idx];
4553 let (_, f_b, sig_b, b_b) = radial[nxt];
4554 if f_a == f_b || patches[f_a].plane_id == patches[f_b].plane_id {
4555 continue;
4556 }
4557 let pl_a = &planes[patches[f_a].plane_id];
4558 let pl_b = &planes[patches[f_b].plane_id];
4559 let gap_at_edge =
4560 ((pl_a.normal.dot(edge_mid) - pl_a.d) - (pl_b.normal.dot(edge_mid) - pl_b.d)).abs();
4561 if gap_at_edge > 1e-3 * r_max
4562 && b_a.dot(b_b) > 0.99
4563 && pl_a.normal.dot(pl_b.normal).abs() > 0.99
4564 {
4565 let s_a = patch_sample_3d[f_a];
4566 let s_b = patch_sample_3d[f_b];
4567 let h_b_vs_a = (sig_a as f64) * (pl_a.normal.dot(s_b) - pl_a.d);
4568 let h_a_vs_b = (sig_b as f64) * (pl_b.normal.dot(s_a) - pl_b.d);
4569 let delta_h = h_b_vs_a - h_a_vs_b;
4570 if delta_h < -1e-9 {
4571 radial.swap(idx, nxt);
4572 swapped = true;
4573 }
4574 }
4575 }
4576 if !swapped {
4577 break;
4578 }
4579 }
4580
4581 let mut ambiguous_spokes =
4584 !all_bodies_closed || r0 > 1e-4 * diagonal || edge_len <= 2.0 * r_max;
4585 if !ambiguous_spokes {
4586 for idx in 0..k {
4587 let nxt = (idx + 1) % k;
4588 if radial[idx].3.dot(radial[nxt].3) > 0.995 {
4589 let pa = patches[radial[idx].1].plane_id;
4590 let pb = patches[radial[nxt].1].plane_id;
4591 let pl_a = &planes[pa];
4592 let pl_b = &planes[pb];
4593 let dev = (pl_a.normal.dot(edge_mid) - pl_a.d)
4594 .abs()
4595 .max((pl_b.normal.dot(edge_mid) - pl_b.d).abs());
4596 if pa == pb || dev > 1e-3 * r_max {
4597 ambiguous_spokes = true;
4598 break;
4599 }
4600 }
4601 }
4602 }
4603
4604 if ambiguous_spokes {
4605 for &(_, f_idx, _, _) in &radial {
4606 eval_patch_winding(
4607 f_idx,
4608 &mut patch_evaluated,
4609 &mut patch_wb,
4610 &mut patch_wf,
4611 &mut patch_ib,
4612 &mut patch_if,
4613 );
4614 }
4615
4616 let spoke_w_cw = |f_idx: usize, sig: i32| -> &[i32] {
4617 if sig == 1 {
4618 &patch_ib[f_idx]
4619 } else {
4620 &patch_if[f_idx]
4621 }
4622 };
4623 let spoke_w_ccw = |f_idx: usize, sig: i32| -> &[i32] {
4624 if sig == 1 {
4625 &patch_if[f_idx]
4626 } else {
4627 &patch_ib[f_idx]
4628 }
4629 };
4630
4631 let all_spokes_quantized = radial.iter().all(|&(_, fi, _, _)| {
4632 (0..body_count).all(|b| {
4633 (patch_wb[fi][b] - (patch_ib[fi][b] as f64)).abs() < 0.15
4634 && (patch_wf[fi][b] - (patch_if[fi][b] as f64)).abs() < 0.15
4635 })
4636 });
4637
4638 let count_mismatches = |order: &[(f64, usize, i32, Point3D)]| -> usize {
4639 let mut m = 0usize;
4640 for idx in 0..k {
4641 let (_, f_cur, sig_cur, _) = order[idx];
4642 let (_, f_nxt, sig_nxt, _) = order[(idx + 1) % k];
4643 if spoke_w_ccw(f_cur, sig_cur) != spoke_w_cw(f_nxt, sig_nxt) {
4644 m += 1;
4645 }
4646 }
4647 m
4648 };
4649
4650 let init_mismatches = if all_spokes_quantized {
4651 count_mismatches(&radial)
4652 } else {
4653 0
4654 };
4655 if init_mismatches > 0 && k <= 7 {
4656 let orig = radial.clone();
4657 let mut perm: Vec<usize> = (0..k).collect();
4658 let mut best_perm = perm.clone();
4659 let mut best_score = (init_mismatches, 0usize);
4660
4661 fn search_cyclic_perms<'a>(
4662 pos: usize,
4663 k: usize,
4664 perm: &mut Vec<usize>,
4665 orig: &[(f64, usize, i32, Point3D)],
4666 spoke_w_cw: &dyn Fn(usize, i32) -> &'a [i32],
4667 spoke_w_ccw: &dyn Fn(usize, i32) -> &'a [i32],
4668 best_perm: &mut Vec<usize>,
4669 best_score: &mut (usize, usize),
4670 ) {
4671 if pos == k {
4672 let mut m = 0usize;
4673 for idx in 0..k {
4674 let (_, f_cur, sig_cur, _) = orig[perm[idx]];
4675 let (_, f_nxt, sig_nxt, _) = orig[perm[(idx + 1) % k]];
4676 if spoke_w_ccw(f_cur, sig_cur) != spoke_w_cw(f_nxt, sig_nxt) {
4677 m += 1;
4678 }
4679 }
4680 if m > best_score.0 {
4681 return;
4682 }
4683 let mut inv = 0usize;
4684 for a in 1..k {
4685 for b in (a + 1)..k {
4686 if perm[a] > perm[b] {
4687 inv += 1;
4688 }
4689 }
4690 }
4691 let score = (m, inv);
4692 if score < *best_score {
4693 *best_score = score;
4694 *best_perm = perm.clone();
4695 }
4696 return;
4697 }
4698 for i in pos..k {
4699 perm.swap(pos, i);
4700 search_cyclic_perms(
4701 pos + 1,
4702 k,
4703 perm,
4704 orig,
4705 spoke_w_cw,
4706 spoke_w_ccw,
4707 best_perm,
4708 best_score,
4709 );
4710 perm.swap(pos, i);
4711 }
4712 }
4713
4714 search_cyclic_perms(
4715 1,
4716 k,
4717 &mut perm,
4718 &orig,
4719 &spoke_w_cw,
4720 &spoke_w_ccw,
4721 &mut best_perm,
4722 &mut best_score,
4723 );
4724 if best_score < (init_mismatches, 0) {
4725 for idx in 0..k {
4726 radial[idx] = orig[best_perm[idx]];
4727 }
4728 }
4729 }
4730 }
4731
4732 for idx in 0..k {
4733 let (_, f_cur, sig_cur, _) = radial[idx];
4734 let (_, f_nxt, sig_nxt, _) = radial[(idx + 1) % k];
4735 let cf_ccw = if sig_cur == 1 {
4736 2 * f_cur
4737 } else {
4738 2 * f_cur + 1
4739 };
4740 let cf_cw = if sig_nxt == 1 {
4741 2 * f_nxt + 1
4742 } else {
4743 2 * f_nxt
4744 };
4745 uf.union(cf_ccw, cf_cw);
4746 }
4747 }
4748
4749 let num_cf = 2 * m_patches;
4751 let mut patch_roots: Vec<(usize, usize)> = vec![(0, 0); m_patches];
4752 let mut cf_patches: Vec<Vec<(bool, f64, usize)>> = vec![Vec::new(); num_cf];
4753 for (f_idx, patch) in patches.iter().enumerate() {
4754 if !patch.alive {
4755 continue;
4756 }
4757 let r_front = uf.find(2 * f_idx);
4758 let r_back = uf.find(2 * f_idx + 1);
4759 patch_roots[f_idx] = (r_front, r_back);
4760 let w_weight = patch.area_2d.max(1e-12);
4761 let p_3d = patch_sample_3d[f_idx];
4762 let mut outside_other = true;
4764 for b in 0..body_count {
4765 if patch.delta_w[b] == 0
4766 && p_3d.x >= body_min[b].x - plane_dist_tol
4767 && p_3d.x <= body_max[b].x + plane_dist_tol
4768 && p_3d.y >= body_min[b].y - plane_dist_tol
4769 && p_3d.y <= body_max[b].y + plane_dist_tol
4770 && p_3d.z >= body_min[b].z - plane_dist_tol
4771 && p_3d.z <= body_max[b].z + plane_dist_tol
4772 {
4773 outside_other = false;
4774 break;
4775 }
4776 }
4777 cf_patches[r_front].push((outside_other, w_weight, f_idx));
4778 if r_back != r_front {
4779 cf_patches[r_back].push((outside_other, w_weight, f_idx));
4780 }
4781 }
4782
4783 let mut cf_order: Vec<usize> = Vec::new();
4785 for (cf, list) in cf_patches.iter_mut().enumerate() {
4786 if list.is_empty() {
4787 continue;
4788 }
4789 list.sort_by(|a, b| {
4790 b.0.cmp(&a.0)
4791 .then_with(|| b.1.partial_cmp(&a.1).unwrap_or(core::cmp::Ordering::Equal))
4792 });
4793 cf_order.push(cf);
4794 }
4795 cf_order.sort_by(|&a, &b| cf_patches[b][0].0.cmp(&cf_patches[a][0].0));
4797
4798 let allow_early_stop = all_bodies_closed && r0 <= 1e-4 * diagonal;
4799 let no_leaked_patches = allow_early_stop
4800 && patches
4801 .iter()
4802 .enumerate()
4803 .all(|(fi, p)| !p.alive || patch_roots[fi].0 != patch_roots[fi].1);
4804
4805 let mut cf_has_int_w: Vec<bool> = vec![false; num_cf];
4806 let mut cf_int_w: Vec<Vec<i32>> = if no_leaked_patches {
4807 vec![vec![0i32; body_count]; num_cf]
4808 } else {
4809 Vec::new()
4810 };
4811 let mut bfs_consistent = no_leaked_patches;
4812
4813 let propagate_coface_bfs = |seed_cf: usize,
4814 cf_has_int_w: &mut [bool],
4815 cf_int_w: &mut [Vec<i32>],
4816 bfs_consistent: &mut bool| {
4817 if !*bfs_consistent {
4818 return;
4819 }
4820 let mut queue: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
4821 queue.push_back(seed_cf);
4822 let mut next_w = vec![0i32; body_count];
4823 while let Some(curr_cf) = queue.pop_front() {
4824 for &(_, _, f_idx) in &cf_patches[curr_cf] {
4825 let patch = &patches[f_idx];
4826 if !patch.alive {
4827 continue;
4828 }
4829 let (r_front, r_back) = patch_roots[f_idx];
4830 if curr_cf == r_front {
4831 for b in 0..body_count {
4832 next_w[b] = cf_int_w[r_front][b] + patch.delta_w[b];
4833 }
4834 if !cf_has_int_w[r_back] {
4835 cf_int_w[r_back].copy_from_slice(&next_w);
4836 cf_has_int_w[r_back] = true;
4837 queue.push_back(r_back);
4838 } else if cf_int_w[r_back] != next_w {
4839 *bfs_consistent = false;
4840 return;
4841 }
4842 } else if curr_cf == r_back {
4843 for b in 0..body_count {
4844 next_w[b] = cf_int_w[r_back][b] - patch.delta_w[b];
4845 }
4846 if !cf_has_int_w[r_front] {
4847 cf_int_w[r_front].copy_from_slice(&next_w);
4848 cf_has_int_w[r_front] = true;
4849 queue.push_back(r_front);
4850 } else if cf_int_w[r_front] != next_w {
4851 *bfs_consistent = false;
4852 return;
4853 }
4854 }
4855 }
4856 }
4857 };
4858
4859 for &cf in &cf_order {
4860 if bfs_consistent && cf_has_int_w[cf] {
4861 continue;
4862 }
4863 let list = &cf_patches[cf];
4864 if allow_early_stop {
4865 if let Some(&(_, _, f_idx)) = list.iter().find(|&&(_, _, f_idx)| {
4866 patch_evaluated[f_idx]
4867 && (0..body_count).all(|b| {
4868 (patch_wb[f_idx][b] - (patch_ib[f_idx][b] as f64)).abs() < 0.02
4869 && (patch_wf[f_idx][b] - (patch_if[f_idx][b] as f64)).abs() < 0.02
4870 })
4871 }) {
4872 if bfs_consistent {
4873 let (r_front, r_back) = patch_roots[f_idx];
4874 if !cf_has_int_w[r_front] {
4875 cf_int_w[r_front].copy_from_slice(&patch_if[f_idx]);
4876 cf_has_int_w[r_front] = true;
4877 propagate_coface_bfs(
4878 r_front,
4879 &mut cf_has_int_w,
4880 &mut cf_int_w,
4881 &mut bfs_consistent,
4882 );
4883 }
4884 if bfs_consistent && !cf_has_int_w[r_back] {
4885 cf_int_w[r_back].copy_from_slice(&patch_ib[f_idx]);
4886 cf_has_int_w[r_back] = true;
4887 propagate_coface_bfs(
4888 r_back,
4889 &mut cf_has_int_w,
4890 &mut cf_int_w,
4891 &mut bfs_consistent,
4892 );
4893 }
4894 if bfs_consistent {
4895 continue;
4896 }
4897 } else {
4898 continue;
4899 }
4900 }
4901 }
4902 for &(_, _, f_idx) in list.iter().take(8) {
4903 eval_patch_winding(
4904 f_idx,
4905 &mut patch_evaluated,
4906 &mut patch_wb,
4907 &mut patch_wf,
4908 &mut patch_ib,
4909 &mut patch_if,
4910 );
4911 if allow_early_stop {
4912 let cleanly_quantized = (0..body_count).all(|b| {
4913 (patch_wb[f_idx][b] - (patch_ib[f_idx][b] as f64)).abs() < 0.02
4914 && (patch_wf[f_idx][b] - (patch_if[f_idx][b] as f64)).abs() < 0.02
4915 });
4916 if cleanly_quantized {
4917 if bfs_consistent {
4918 let (r_front, r_back) = patch_roots[f_idx];
4919 if !cf_has_int_w[r_front] {
4920 cf_int_w[r_front].copy_from_slice(&patch_if[f_idx]);
4921 cf_has_int_w[r_front] = true;
4922 propagate_coface_bfs(
4923 r_front,
4924 &mut cf_has_int_w,
4925 &mut cf_int_w,
4926 &mut bfs_consistent,
4927 );
4928 }
4929 if bfs_consistent && !cf_has_int_w[r_back] {
4930 cf_int_w[r_back].copy_from_slice(&patch_ib[f_idx]);
4931 cf_has_int_w[r_back] = true;
4932 propagate_coface_bfs(
4933 r_back,
4934 &mut cf_has_int_w,
4935 &mut cf_int_w,
4936 &mut bfs_consistent,
4937 );
4938 }
4939 }
4940 break;
4941 }
4942 }
4943 }
4944 }
4945
4946 if no_leaked_patches && !bfs_consistent {
4948 for &cf in &cf_order {
4949 let list = &cf_patches[cf];
4950 let has_eval = list.iter().any(|&(_, _, fi)| patch_evaluated[fi]);
4951 if !has_eval {
4952 for &(_, _, f_idx) in list.iter().take(8) {
4953 eval_patch_winding(
4954 f_idx,
4955 &mut patch_evaluated,
4956 &mut patch_wb,
4957 &mut patch_wf,
4958 &mut patch_ib,
4959 &mut patch_if,
4960 );
4961 let cleanly_quantized = (0..body_count).all(|b| {
4962 (patch_wb[f_idx][b] - (patch_ib[f_idx][b] as f64)).abs() < 0.02
4963 && (patch_wf[f_idx][b] - (patch_if[f_idx][b] as f64)).abs() < 0.02
4964 });
4965 if cleanly_quantized {
4966 break;
4967 }
4968 }
4969 }
4970 }
4971 }
4972
4973 let mut vol_w_sum: Vec<Vec<f64>> = vec![vec![0.0; body_count]; num_cf];
4974 let mut vol_area_sum: Vec<f64> = vec![0.0; num_cf];
4975
4976 for (f_idx, patch) in patches.iter().enumerate() {
4977 if !patch.alive || !patch_evaluated[f_idx] {
4978 continue;
4979 }
4980 let r_front = uf.find(2 * f_idx);
4981 let r_back = uf.find(2 * f_idx + 1);
4982 let w_weight = patch.area_2d.max(1e-12);
4983 vol_area_sum[r_front] += w_weight;
4984 vol_area_sum[r_back] += w_weight;
4985
4986 for b in 0..body_count {
4987 vol_w_sum[r_back][b] += w_weight * patch_wb[f_idx][b];
4988 vol_w_sum[r_front][b] += w_weight * patch_wf[f_idx][b];
4989 }
4990 }
4991
4992 let mut vol_keep: Vec<bool> = vec![false; num_cf];
4993 let mut w_eval = vec![0.0f64; body_count];
4994 for cf in 0..num_cf {
4995 if uf.find(cf) != cf {
4996 continue;
4997 }
4998 if bfs_consistent && cf_has_int_w[cf] {
4999 for b in 0..body_count {
5000 w_eval[b] = cf_int_w[cf][b] as f64;
5001 }
5002 vol_keep[cf] = predicate(&w_eval);
5003 continue;
5004 }
5005 if vol_area_sum[cf] <= 0.0 {
5006 continue;
5007 }
5008 let inv_a = 1.0 / vol_area_sum[cf];
5009 for b in 0..body_count {
5010 w_eval[b] = (vol_w_sum[cf][b] * inv_a).round();
5011 }
5012 vol_keep[cf] = predicate(&w_eval);
5013 }
5014
5015 let mut plane_pos_patches: Vec<Vec<usize>> = vec![Vec::new(); planes.len()];
5018 let mut plane_neg_patches: Vec<Vec<usize>> = vec![Vec::new(); planes.len()];
5019
5020 for (f_idx, patch) in patches.iter().enumerate() {
5021 if !patch.alive {
5022 continue;
5023 }
5024 let keep_front = vol_keep[uf.find(2 * f_idx)];
5025 let keep_back = vol_keep[uf.find(2 * f_idx + 1)];
5026 if keep_back && !keep_front {
5027 plane_pos_patches[patch.plane_id].push(f_idx);
5028 } else if keep_front && !keep_back {
5029 plane_neg_patches[patch.plane_id].push(f_idx);
5030 }
5031 }
5032
5033 let mut unified_faces_vids: Vec<(usize, i32, Vec<u32>, Vec<Vec<u32>>)> = Vec::new();
5034 for p_id in 0..planes.len() {
5035 for &(orient_sign, ref p_list) in &[
5036 (1i32, &plane_pos_patches[p_id]),
5037 (-1i32, &plane_neg_patches[p_id]),
5038 ] {
5039 if p_list.is_empty() {
5040 continue;
5041 }
5042 if p_list.len() == 1 {
5045 let patch = &patches[p_list[0]];
5046 if patch.outer_vids.len() >= 3 {
5047 let de = &patch.directed_edges;
5048 let mut has_duplicate_edge = false;
5049 for i in 0..de.len() {
5050 for j in (i + 1)..de.len() {
5051 if de[i].0 == de[j].0 {
5052 has_duplicate_edge = true;
5053 break;
5054 }
5055 }
5056 if has_duplicate_edge {
5057 break;
5058 }
5059 }
5060 if !has_duplicate_edge {
5061 let mut outer_vids = patch.outer_vids.clone();
5062 let mut holes_vids = patch.holes_vids.clone();
5063 if orient_sign < 0 {
5064 outer_vids.reverse();
5065 for h in &mut holes_vids {
5066 h.reverse();
5067 }
5068 }
5069 unified_faces_vids.push((p_id, orient_sign, outer_vids, holes_vids));
5070 continue;
5071 }
5072 }
5073 }
5074 unify_coplanar_patches_3d(
5075 p_id,
5076 orient_sign,
5077 p_list,
5078 &patches,
5079 &edges,
5080 &verts,
5081 &planes[p_id],
5082 &mut unified_faces_vids,
5083 );
5084 }
5085 }
5086
5087 if options.simplify_collinear {
5089 simplify_global_collinear_vertices_3d(&mut unified_faces_vids, &verts, r0);
5090 }
5091
5092 assemble_brep_result_3d(unified_faces_vids, &verts, &planes, center, diagonal)
5094}
5095
5096fn unify_coplanar_patches_3d(
5099 p_id: usize,
5100 orient_sign: i32,
5101 patch_indices: &[usize],
5102 patches: &[FacePatch3D],
5103 edges: &[Edge3D],
5104 verts: &[Vert3D],
5105 pl: &CanonicalPlane,
5106 out_faces: &mut Vec<(usize, i32, Vec<u32>, Vec<Vec<u32>>)>,
5107) {
5108 let mut net_edges: Vec<(usize, i32)> = Vec::new();
5110 for &f_idx in patch_indices {
5111 for &(e_id, sign) in &patches[f_idx].directed_edges {
5112 if let Some(pos) = net_edges.iter().position(|&(id, _)| id == e_id) {
5113 net_edges[pos].1 += sign;
5114 } else {
5115 net_edges.push((e_id, sign));
5116 }
5117 }
5118 }
5119
5120 let mut dir_hes: Vec<(u32, u32)> = Vec::new();
5122 for (e_id, net) in net_edges {
5123 if net == 0 {
5124 continue;
5125 }
5126 let u = edges[e_id].v0;
5127 let v = edges[e_id].v1;
5128 if net > 0 {
5129 for _ in 0..net {
5130 dir_hes.push((u, v));
5131 }
5132 } else {
5133 for _ in 0..(-net) {
5134 dir_hes.push((v, u));
5135 }
5136 }
5137 }
5138 if dir_hes.len() < 3 {
5139 return;
5140 }
5141
5142 let mut local_vids: Vec<u32> = Vec::with_capacity(dir_hes.len() * 2);
5143 for &(u, v) in &dir_hes {
5144 local_vids.push(u);
5145 local_vids.push(v);
5146 }
5147 local_vids.sort_unstable();
5148 local_vids.dedup();
5149 let num_lv = local_vids.len();
5150 if num_lv < 3 {
5151 return;
5152 }
5153 let to_lv = |vid: u32| -> usize { local_vids.binary_search(&vid).unwrap() };
5154
5155 let v_2d: Vec<Point2D> = local_vids
5156 .iter()
5157 .map(|&vid| pl.project_2d(verts[vid as usize].rep))
5158 .collect();
5159 let num_he = dir_hes.len();
5160 let mut vert_in: Vec<Vec<usize>> = vec![Vec::new(); num_lv];
5161 let mut vert_out: Vec<Vec<usize>> = vec![Vec::new(); num_lv];
5162 for (h_idx, &(u, v)) in dir_hes.iter().enumerate() {
5163 vert_out[to_lv(u)].push(h_idx);
5164 vert_in[to_lv(v)].push(h_idx);
5165 }
5166
5167 let mut he_next: Vec<usize> = vec![usize::MAX; num_he];
5170 for lw in 0..num_lv {
5171 if vert_in[lw].is_empty() || vert_out[lw].is_empty() {
5172 continue;
5173 }
5174 if vert_in[lw].len() == 1 && vert_out[lw].len() == 1 {
5175 he_next[vert_in[lw][0]] = vert_out[lw][0];
5176 continue;
5177 }
5178 let pw = v_2d[lw];
5179 let mut spokes: Vec<(f64, bool, usize)> =
5180 Vec::with_capacity(vert_in[lw].len() + vert_out[lw].len());
5181 for &h_in in &vert_in[lw] {
5182 let pu = v_2d[to_lv(dir_hes[h_in].0)];
5183 let ang = (pu.y - pw.y).atan2(pu.x - pw.x);
5184 spokes.push((ang, false, h_in));
5185 }
5186 for &h_out in &vert_out[lw] {
5187 let pv = v_2d[to_lv(dir_hes[h_out].1)];
5188 let ang = (pv.y - pw.y).atan2(pv.x - pw.x);
5189 spokes.push((ang, true, h_out));
5190 }
5191 spokes.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
5192
5193 let m = spokes.len();
5194 let mut used_out = vec![false; num_he];
5195 for idx in 0..m {
5196 let (_, is_out, h_in) = spokes[idx];
5197 if is_out {
5198 continue;
5199 }
5200 for step in 1..=m {
5202 let (_, cand_out, h_cand) = spokes[(idx + m - step) % m];
5203 if cand_out && !used_out[h_cand] {
5204 used_out[h_cand] = true;
5205 he_next[h_in] = h_cand;
5206 break;
5207 }
5208 }
5209 }
5210 }
5211
5212 let mut visited = vec![false; num_he];
5213 let mut shells: Vec<(Vec<u32>, f64)> = Vec::new();
5214 let mut holes: Vec<(Vec<u32>, f64)> = Vec::new();
5215
5216 for h_start in 0..num_he {
5217 if visited[h_start] || he_next[h_start] == usize::MAX {
5218 continue;
5219 }
5220 let mut raw_vids: Vec<u32> = Vec::new();
5221 let mut cur = h_start;
5222 while cur != usize::MAX && !visited[cur] {
5223 visited[cur] = true;
5224 raw_vids.push(dir_hes[cur].0);
5225 cur = he_next[cur];
5226 }
5227 if raw_vids.len() < 3 {
5228 continue;
5229 }
5230
5231 let mut v_stack: Vec<u32> = Vec::with_capacity(raw_vids.len());
5233 let mut simple_rings: Vec<Vec<u32>> = Vec::new();
5234 for vid in raw_vids {
5235 if let Some(pos) = v_stack.iter().position(|&sv| sv == vid) {
5236 if v_stack.len() - pos >= 3 {
5237 simple_rings.push(v_stack[pos..].to_vec());
5238 }
5239 v_stack.truncate(pos);
5240 }
5241 v_stack.push(vid);
5242 }
5243 if v_stack.len() >= 3 {
5244 simple_rings.push(v_stack);
5245 }
5246
5247 for ring in simple_rings {
5248 let pts_2d: Vec<Point2D> = ring.iter().map(|&vid| v_2d[to_lv(vid)]).collect();
5249 let area = signed_area_2d_pts(&pts_2d);
5250 if area > 1e-11 {
5251 shells.push((ring, area));
5252 } else if area < -1e-11 {
5253 holes.push((ring, area.abs()));
5254 }
5255 }
5256 }
5257
5258 if shells.is_empty() {
5259 return;
5260 }
5261
5262 let mut poly_holes: Vec<Vec<Vec<u32>>> = vec![Vec::new(); shells.len()];
5263 if !holes.is_empty() {
5264 if shells.len() == 1 {
5265 for (h_ring, _) in holes {
5266 poly_holes[0].push(h_ring);
5267 }
5268 } else {
5269 let shells_2d: Vec<Vec<Point2D>> = shells
5270 .iter()
5271 .map(|(sh, _)| sh.iter().map(|&vid| v_2d[to_lv(vid)]).collect())
5272 .collect();
5273 for (h_ring, h_area) in holes {
5274 let test_pt = h_ring
5275 .iter()
5276 .map(|&vid| v_2d[to_lv(vid)])
5277 .find(|hp| {
5278 shells_2d.iter().all(|sh| {
5279 sh.iter()
5280 .all(|sp| (sp.x - hp.x).hypot(sp.y - hp.y) > 1e-8)
5281 })
5282 })
5283 .unwrap_or_else(|| v_2d[to_lv(h_ring[0])]);
5284 let mut best_s = 0usize;
5285 let mut best_area = f64::INFINITY;
5286 for (s_idx, (_, s_area)) in shells.iter().enumerate() {
5287 if *s_area <= h_area + 1e-11 {
5288 continue;
5289 }
5290 if loops_winding_2d(test_pt, core::slice::from_ref(&shells_2d[s_idx])) != 0
5291 && *s_area < best_area
5292 {
5293 best_area = *s_area;
5294 best_s = s_idx;
5295 }
5296 }
5297 poly_holes[best_s].push(h_ring);
5298 }
5299 }
5300 }
5301
5302 for (s_idx, (mut outer_vids, _)) in shells.into_iter().enumerate() {
5303 let mut h_list = core::mem::take(&mut poly_holes[s_idx]);
5304 if orient_sign < 0 {
5305 outer_vids.reverse();
5306 for h in &mut h_list {
5307 h.reverse();
5308 }
5309 }
5310 out_faces.push((p_id, orient_sign, outer_vids, h_list));
5311 }
5312}
5313
5314fn simplify_global_collinear_vertices_3d(
5316 faces: &mut [(usize, i32, Vec<u32>, Vec<Vec<u32>>)],
5317 verts: &[Vert3D],
5318 r0: f64,
5319) {
5320 let mut can_prune = vec![false; verts.len()];
5321 let mut any_gt3 = false;
5322 for (_, _, outer, holes) in faces.iter() {
5323 if outer.len() > 3 {
5324 any_gt3 = true;
5325 for &vid in outer {
5326 can_prune[vid as usize] = true;
5327 }
5328 }
5329 for h in holes {
5330 if h.len() > 3 {
5331 any_gt3 = true;
5332 for &vid in h {
5333 can_prune[vid as usize] = true;
5334 }
5335 }
5336 }
5337 }
5338 if !any_gt3 {
5339 return;
5340 }
5341
5342 let mut neighbors: Vec<Vec<u32>> = vec![Vec::new(); verts.len()];
5343 let mut add_neighbor = |u: u32, v: u32| {
5344 if u == v {
5345 return;
5346 }
5347 if can_prune[u as usize] && !neighbors[u as usize].contains(&v) {
5348 neighbors[u as usize].push(v);
5349 }
5350 if can_prune[v as usize] && !neighbors[v as usize].contains(&u) {
5351 neighbors[v as usize].push(u);
5352 }
5353 };
5354
5355 for (_, _, outer, holes) in faces.iter() {
5356 let n = outer.len();
5357 for i in 0..n {
5358 add_neighbor(outer[i], outer[(i + 1) % n]);
5359 }
5360 for h in holes {
5361 let hn = h.len();
5362 for i in 0..hn {
5363 add_neighbor(h[i], h[(i + 1) % hn]);
5364 }
5365 }
5366 }
5367
5368 let mut removable = vec![false; verts.len()];
5369 let mut any_removable = false;
5370 let sin_tol_sq = 1e-6;
5371 let dist_tol_sq = (0.25 * r0).max(1e-9).powi(2);
5372 for v_id in 0..verts.len() {
5373 if can_prune[v_id] && neighbors[v_id].len() == 2 {
5374 let u = neighbors[v_id][0] as usize;
5375 let w = neighbors[v_id][1] as usize;
5376 let pu = verts[u].rep;
5377 let pv = verts[v_id].rep;
5378 let pw = verts[w].rep;
5379 let d1 = pv.sub(pu);
5380 let d2 = pw.sub(pv);
5381 let l1_sq = d1.norm_sq();
5382 let l2_sq = d2.norm_sq();
5383 if l1_sq <= 1e-20 || l2_sq <= 1e-20 {
5384 removable[v_id] = true;
5385 any_removable = true;
5386 continue;
5387 }
5388 if d1.dot(d2) > 0.0 {
5389 let cr_sq = d1.cross(d2).norm_sq();
5390 let uw = pw.sub(pu);
5391 let uw_sq = uw.norm_sq();
5392 let perp_sq = if uw_sq > 1e-20 {
5393 cr_sq / uw_sq
5394 } else {
5395 0.0
5396 };
5397 if cr_sq <= sin_tol_sq * l1_sq * l2_sq && perp_sq <= dist_tol_sq {
5398 removable[v_id] = true;
5399 any_removable = true;
5400 }
5401 }
5402 }
5403 }
5404 if !any_removable {
5405 return;
5406 }
5407
5408 let prune_ring = |ring: &mut Vec<u32>| {
5409 if ring.len() <= 3 {
5410 return;
5411 }
5412 let mut i = 0;
5413 while i < ring.len() && ring.len() > 3 {
5414 let vid = ring[i] as usize;
5415 if removable[vid] {
5416 ring.remove(i);
5417 } else {
5418 i += 1;
5419 }
5420 }
5421 };
5422
5423 for (_, _, outer, holes) in faces.iter_mut() {
5424 prune_ring(outer);
5425 for h in holes.iter_mut() {
5426 prune_ring(h);
5427 }
5428 }
5429}
5430
5431fn assemble_brep_result_3d(
5434 unified_faces_vids: Vec<(usize, i32, Vec<u32>, Vec<Vec<u32>>)>,
5435 verts: &[Vert3D],
5436 planes: &[CanonicalPlane],
5437 center: Point3D,
5438 diagonal: f64,
5439) -> BRepResult3D {
5440 let planar_tol = 1e-5 * diagonal;
5441 let mut out_faces: Vec<Face3D> = Vec::with_capacity(unified_faces_vids.len());
5442 let mut vert_dir_edges: Vec<Vec<(u32, i32)>> = vec![Vec::new(); verts.len()];
5443 let mut used_vert = vec![false; verts.len()];
5444
5445 let mut record_half_edge = |u: u32, v: u32| {
5446 if u == v {
5447 return;
5448 }
5449 used_vert[u as usize] = true;
5450 used_vert[v as usize] = true;
5451 let (a, b, sign) = if u < v { (u, v, 1) } else { (v, u, -1) };
5452 let list = &mut vert_dir_edges[a as usize];
5453 for entry in list.iter_mut() {
5454 if entry.0 == b {
5455 entry.1 += sign;
5456 return;
5457 }
5458 }
5459 list.push((b, sign));
5460 };
5461
5462 for (p_id, orient_sign, outer_vids, holes_vids) in unified_faces_vids {
5463 if outer_vids.len() < 3 {
5464 continue;
5465 }
5466
5467 let carrier_n = if orient_sign >= 0 {
5468 planes[p_id].normal
5469 } else {
5470 planes[p_id].normal.scale(-1.0)
5471 };
5472
5473 if outer_vids.len() == 3 && holes_vids.is_empty() {
5474 let v0 = outer_vids[0];
5475 let v1 = outer_vids[1];
5476 let v2 = outer_vids[2];
5477 record_half_edge(v0, v1);
5478 record_half_edge(v1, v2);
5479 record_half_edge(v2, v0);
5480 let a = verts[v0 as usize].rep.add(center);
5481 let b = verts[v1 as usize].rep.add(center);
5482 let c = verts[v2 as usize].rep.add(center);
5483 let mut face_n = b.sub(a).cross(c.sub(a)).normalized().unwrap_or(carrier_n);
5484 if face_n.dot(carrier_n) < 0.0 {
5485 face_n = face_n.scale(-1.0);
5486 }
5487 out_faces.push(Face3D {
5488 outer: vec![a, b, c],
5489 holes: Vec::new(),
5490 normal: face_n,
5491 });
5492 continue;
5493 }
5494
5495 let outer_3d: Vec<Point3D> = outer_vids
5496 .iter()
5497 .map(|&vid| verts[vid as usize].rep.add(center))
5498 .collect();
5499 let holes_3d: Vec<Vec<Point3D>> = holes_vids
5500 .iter()
5501 .map(|h| {
5502 h.iter()
5503 .map(|&vid| verts[vid as usize].rep.add(center))
5504 .collect()
5505 })
5506 .collect();
5507
5508 let mut face_n = compute_newell_normal(&outer_3d).unwrap_or(carrier_n);
5509 if face_n.dot(carrier_n) < 0.0 {
5510 face_n = face_n.scale(-1.0);
5511 }
5512
5513 let p0 = outer_3d[0];
5515 let mut max_dev = 0.0f64;
5516 for &pt in &outer_3d {
5517 max_dev = max_dev.max(face_n.dot(pt.sub(p0)).abs());
5518 }
5519 for h in &holes_3d {
5520 for &pt in h {
5521 max_dev = max_dev.max(face_n.dot(pt.sub(p0)).abs());
5522 }
5523 }
5524
5525 if (outer_vids.len() > 3 || !holes_vids.is_empty()) && max_dev > planar_tol {
5526 let tris_vids =
5528 triangulate_face_vids_3d(&outer_vids, &holes_vids, &outer_3d, &holes_3d, face_n);
5529 for [va, vb, vc] in tris_vids {
5530 record_half_edge(va, vb);
5531 record_half_edge(vb, vc);
5532 record_half_edge(vc, va);
5533 let a = verts[va as usize].rep.add(center);
5534 let b = verts[vb as usize].rep.add(center);
5535 let c = verts[vc as usize].rep.add(center);
5536 let tn = compute_newell_normal(&[a, b, c]).unwrap_or(face_n);
5537 out_faces.push(Face3D {
5538 outer: vec![a, b, c],
5539 holes: Vec::new(),
5540 normal: tn,
5541 });
5542 }
5543 } else {
5544 let n_out = outer_vids.len();
5545 for i in 0..n_out {
5546 record_half_edge(outer_vids[i], outer_vids[(i + 1) % n_out]);
5547 }
5548 for h in &holes_vids {
5549 let hn = h.len();
5550 for i in 0..hn {
5551 record_half_edge(h[i], h[(i + 1) % hn]);
5552 }
5553 }
5554 out_faces.push(Face3D {
5555 outer: outer_3d,
5556 holes: holes_3d,
5557 normal: face_n,
5558 });
5559 }
5560 }
5561
5562 let vertex_count = used_vert.iter().filter(|&&u| u).count();
5563 let mut edge_count = 0usize;
5564 let mut unmatched_half_edge_count = 0usize;
5565 for list in &vert_dir_edges {
5566 edge_count += list.len();
5567 for &(_, net) in list {
5568 unmatched_half_edge_count += net.unsigned_abs() as usize;
5569 }
5570 }
5571
5572 let solid = Solid3D { faces: out_faces };
5573 let total_volume = solid.volume();
5574 let total_surface_area = solid.surface_area();
5575
5576 BRepResult3D {
5577 solid,
5578 total_volume,
5579 total_surface_area,
5580 vertex_count,
5581 edge_count,
5582 unmatched_half_edge_count,
5583 }
5584}
5585
5586pub fn union_3d(a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5588 let rule = options.winding_rule;
5589 execute_brep_3d(
5590 &[a, b],
5591 |w| rule.apply(w[0]) || rule.apply(w[1]),
5592 options,
5593 )
5594}
5595
5596pub fn intersect_3d(a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5598 let rule = options.winding_rule;
5599 execute_brep_3d(
5600 &[a, b],
5601 |w| rule.apply(w[0]) && rule.apply(w[1]),
5602 options,
5603 )
5604}
5605
5606pub fn difference_3d(a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5608 let rule = options.winding_rule;
5609 execute_brep_3d(
5610 &[a, b],
5611 |w| rule.apply(w[0]) && !rule.apply(w[1]),
5612 options,
5613 )
5614}
5615
5616pub fn xor_3d(a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5618 let rule = options.winding_rule;
5619 execute_brep_3d(
5620 &[a, b],
5621 |w| rule.apply(w[0]) != rule.apply(w[1]),
5622 options,
5623 )
5624}
5625
5626pub fn heal_3d(solid: &Solid3D, options: KernelOptions) -> BRepResult3D {
5628 let rule = options.winding_rule;
5629 execute_brep_3d(&[solid], |w| rule.apply(w[0]), options)
5630}
5631
5632pub fn clip_3d(op: BooleanOp, a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5634 match op {
5635 BooleanOp::Union => union_3d(a, b, options),
5636 BooleanOp::Intersect => intersect_3d(a, b, options),
5637 BooleanOp::Difference => difference_3d(a, b, options),
5638 BooleanOp::Xor => xor_3d(a, b, options),
5639 }
5640}
5641
5642#[inline]
5644pub fn union(a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5645 union_3d(a, b, options)
5646}
5647
5648#[inline]
5650pub fn intersect(a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5651 intersect_3d(a, b, options)
5652}
5653
5654#[inline]
5656pub fn difference(a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5657 difference_3d(a, b, options)
5658}
5659
5660#[inline]
5662pub fn xor(a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5663 xor_3d(a, b, options)
5664}
5665
5666#[inline]
5668pub fn heal(solid: &Solid3D, options: KernelOptions) -> BRepResult3D {
5669 heal_3d(solid, options)
5670}
5671
5672#[inline]
5674pub fn clip(op: BooleanOp, a: &Solid3D, b: &Solid3D, options: KernelOptions) -> BRepResult3D {
5675 clip_3d(op, a, b, options)
5676}
5677
5678#[inline]
5680pub fn triangulate_face(face: &Face3D) -> Vec<[Point3D; 3]> {
5681 triangulate_face_3d(face)
5682}
5683
5684#[inline]
5686pub fn execute_brep<F>(
5687 solids: &[&Solid3D],
5688 keep_predicate: F,
5689 options: KernelOptions,
5690) -> BRepResult3D
5691where
5692 F: Fn(&[f64]) -> bool,
5693{
5694 execute_brep_3d(solids, keep_predicate, options)
5695}
5696
5697struct WasmBRep3DBuffer {
5702 input_data: Vec<f64>,
5706 parsed_solids: Vec<Solid3D>,
5707 solids_dirty: bool,
5708 out_coords: Vec<f64>,
5710 out_loop_meta: Vec<u32>,
5712 out_face_normals: Vec<f64>,
5714 out_triangles: Vec<f64>,
5716 out_volume: f64,
5717 out_area: f64,
5718 out_vertex_count: u32,
5719 out_edge_count: u32,
5720 out_unmatched_he_count: u32,
5721}
5722
5723impl WasmBRep3DBuffer {
5724 fn new() -> Self {
5725 Self {
5726 input_data: Vec::new(),
5727 parsed_solids: Vec::new(),
5728 solids_dirty: true,
5729 out_coords: Vec::new(),
5730 out_loop_meta: Vec::new(),
5731 out_face_normals: Vec::new(),
5732 out_triangles: Vec::new(),
5733 out_volume: 0.0,
5734 out_area: 0.0,
5735 out_vertex_count: 0,
5736 out_edge_count: 0,
5737 out_unmatched_he_count: 0,
5738 }
5739 }
5740
5741 fn ensure_parsed(&mut self) {
5742 if !self.solids_dirty {
5743 return;
5744 }
5745 self.solids_dirty = false;
5746 self.parsed_solids.clear();
5747 let data = &self.input_data;
5748 if data.is_empty() {
5749 return;
5750 }
5751 let mut cursor = 0usize;
5752 let num_solids = data[cursor] as usize;
5753 cursor += 1;
5754
5755 self.parsed_solids.reserve(num_solids);
5756 for _ in 0..num_solids {
5757 if cursor >= data.len() {
5758 break;
5759 }
5760 let num_faces = data[cursor] as usize;
5761 cursor += 1;
5762 let mut faces = Vec::with_capacity(num_faces);
5763 for _ in 0..num_faces {
5764 if cursor + 4 > data.len() {
5765 break;
5766 }
5767 let nx = data[cursor];
5768 let ny = data[cursor + 1];
5769 let nz = data[cursor + 2];
5770 let num_loops = data[cursor + 3] as usize;
5771 cursor += 4;
5772
5773 let mut outer = Vec::new();
5774 let mut holes = Vec::new();
5775 for l_idx in 0..num_loops {
5776 if cursor >= data.len() {
5777 break;
5778 }
5779 let num_pts = data[cursor] as usize;
5780 cursor += 1;
5781 let mut ring = Vec::with_capacity(num_pts);
5782 for _ in 0..num_pts {
5783 if cursor + 3 > data.len() {
5784 break;
5785 }
5786 ring.push(Point3D::new(
5787 data[cursor],
5788 data[cursor + 1],
5789 data[cursor + 2],
5790 ));
5791 cursor += 3;
5792 }
5793 if l_idx == 0 {
5794 outer = ring;
5795 } else {
5796 holes.push(ring);
5797 }
5798 }
5799 if outer.len() >= 3 {
5800 faces.push(Face3D {
5801 outer,
5802 holes,
5803 normal: Point3D::new(nx, ny, nz),
5804 });
5805 }
5806 }
5807 self.parsed_solids.push(Solid3D { faces });
5808 }
5809 }
5810}
5811
5812std::thread_local! {
5813 static WASM_BREP3D_BUF: std::cell::RefCell<WasmBRep3DBuffer> =
5814 std::cell::RefCell::new(WasmBRep3DBuffer::new());
5815}
5816
5817#[no_mangle]
5818pub extern "C" fn wasm_brep3d_alloc_input(f64_count: usize) -> *mut f64 {
5819 WASM_BREP3D_BUF.with(|cell| {
5820 let mut buf = cell.borrow_mut();
5821 buf.input_data.clear();
5822 buf.input_data.resize(f64_count, 0.0);
5823 buf.solids_dirty = true;
5824 buf.input_data.as_mut_ptr()
5825 })
5826}
5827
5828#[no_mangle]
5835pub extern "C" fn wasm_brep3d_execute(
5836 op_code: u32,
5837 winding_rule: u32,
5838 snap_factor: f64,
5839 simplify_collinear: u32,
5840) -> u32 {
5841 WASM_BREP3D_BUF.with(|cell| {
5842 let mut buf = cell.borrow_mut();
5843 if buf.input_data.is_empty() {
5844 return 0;
5845 }
5846 buf.ensure_parsed();
5847
5848 let rule = match winding_rule {
5849 0 => WindingRule::Positive,
5850 2 => WindingRule::EvenOdd,
5851 _ => WindingRule::NonZero,
5852 };
5853 let opts = KernelOptions {
5854 winding_rule: rule,
5855 snap_tolerance: if snap_factor > 0.0 {
5856 Some(snap_factor)
5857 } else {
5858 None
5859 },
5860 simplify_collinear: simplify_collinear != 0,
5861 };
5862
5863 let empty_solid = Solid3D { faces: Vec::new() };
5864 let s0 = buf.parsed_solids.first().unwrap_or(&empty_solid);
5865 let s1 = buf.parsed_solids.get(1).unwrap_or(&empty_solid);
5866
5867 let res = match op_code {
5868 0 => union_3d(s0, s1, opts),
5869 1 => intersect_3d(s0, s1, opts),
5870 2 => difference_3d(s0, s1, opts),
5871 3 => xor_3d(s0, s1, opts),
5872 _ => heal_3d(s0, opts),
5873 };
5874
5875 buf.out_coords.clear();
5876 buf.out_loop_meta.clear();
5877 buf.out_face_normals.clear();
5878 buf.out_triangles.clear();
5879
5880 let mut pt_cursor = 0u32;
5881 for (f_idx, face) in res.solid.faces.iter().enumerate() {
5882 buf.out_face_normals.push(face.normal.x);
5883 buf.out_face_normals.push(face.normal.y);
5884 buf.out_face_normals.push(face.normal.z);
5885
5886 let outer_cnt = face.outer.len() as u32;
5887 buf.out_loop_meta.push(f_idx as u32);
5888 buf.out_loop_meta.push(0);
5889 buf.out_loop_meta.push(pt_cursor);
5890 buf.out_loop_meta.push(outer_cnt);
5891 for p in &face.outer {
5892 buf.out_coords.push(p.x);
5893 buf.out_coords.push(p.y);
5894 buf.out_coords.push(p.z);
5895 }
5896 pt_cursor += outer_cnt;
5897
5898 for hole in &face.holes {
5899 let hole_cnt = hole.len() as u32;
5900 buf.out_loop_meta.push(f_idx as u32);
5901 buf.out_loop_meta.push(1);
5902 buf.out_loop_meta.push(pt_cursor);
5903 buf.out_loop_meta.push(hole_cnt);
5904 for p in hole {
5905 buf.out_coords.push(p.x);
5906 buf.out_coords.push(p.y);
5907 buf.out_coords.push(p.z);
5908 }
5909 pt_cursor += hole_cnt;
5910 }
5911
5912 let f_num = f_idx as f64;
5913 let nx = face.normal.x;
5914 let ny = face.normal.y;
5915 let nz = face.normal.z;
5916 if face.holes.is_empty() && face.outer.len() == 3 {
5917 let a = face.outer[0];
5918 let b = face.outer[1];
5919 let c = face.outer[2];
5920 buf.out_triangles.extend_from_slice(&[
5921 a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, nx, ny, nz, f_num,
5922 ]);
5923 } else if face.holes.is_empty() && face.outer.len() == 4 {
5924 let p0 = face.outer[0];
5925 let p1 = face.outer[1];
5926 let p2 = face.outer[2];
5927 let p3 = face.outer[3];
5928 if quad_choose_diagonal_02(p0, p1, p2, p3, face.normal) {
5929 buf.out_triangles.extend_from_slice(&[
5930 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, nx, ny, nz, f_num,
5931 p0.x, p0.y, p0.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z, nx, ny, nz, f_num,
5932 ]);
5933 } else {
5934 buf.out_triangles.extend_from_slice(&[
5935 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z, p3.x, p3.y, p3.z, nx, ny, nz, f_num,
5936 p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z, nx, ny, nz, f_num,
5937 ]);
5938 }
5939 } else {
5940 let tris = triangulate_face_3d(face);
5941 for [a, b, c] in tris {
5942 buf.out_triangles.extend_from_slice(&[
5943 a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, nx, ny, nz, f_num,
5944 ]);
5945 }
5946 }
5947 }
5948
5949 buf.out_volume = res.total_volume;
5950 buf.out_area = res.total_surface_area;
5951 buf.out_vertex_count = res.vertex_count as u32;
5952 buf.out_edge_count = res.edge_count as u32;
5953 buf.out_unmatched_he_count = res.unmatched_half_edge_count as u32;
5954
5955 res.solid.faces.len() as u32
5956 })
5957}
5958
5959#[no_mangle]
5960pub extern "C" fn wasm_brep3d_get_out_coords_ptr() -> *const f64 {
5961 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_coords.as_ptr())
5962}
5963
5964#[no_mangle]
5965pub extern "C" fn wasm_brep3d_get_out_coords_len() -> usize {
5966 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_coords.len())
5967}
5968
5969#[no_mangle]
5970pub extern "C" fn wasm_brep3d_get_out_loop_meta_ptr() -> *const u32 {
5971 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_loop_meta.as_ptr())
5972}
5973
5974#[no_mangle]
5975pub extern "C" fn wasm_brep3d_get_out_loop_count() -> usize {
5976 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_loop_meta.len() / 4)
5977}
5978
5979#[no_mangle]
5980pub extern "C" fn wasm_brep3d_get_out_face_normals_ptr() -> *const f64 {
5981 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_face_normals.as_ptr())
5982}
5983
5984#[no_mangle]
5985pub extern "C" fn wasm_brep3d_get_out_face_count() -> usize {
5986 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_face_normals.len() / 3)
5987}
5988
5989#[no_mangle]
5990pub extern "C" fn wasm_brep3d_get_out_triangles_ptr() -> *const f64 {
5991 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_triangles.as_ptr())
5992}
5993
5994#[no_mangle]
5995pub extern "C" fn wasm_brep3d_get_out_triangles_len() -> usize {
5996 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_triangles.len())
5997}
5998
5999#[no_mangle]
6000pub extern "C" fn wasm_brep3d_get_out_volume() -> f64 {
6001 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_volume)
6002}
6003
6004#[no_mangle]
6005pub extern "C" fn wasm_brep3d_get_out_area() -> f64 {
6006 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_area)
6007}
6008
6009#[no_mangle]
6010pub extern "C" fn wasm_brep3d_get_out_vertex_count() -> u32 {
6011 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_vertex_count)
6012}
6013
6014#[no_mangle]
6015pub extern "C" fn wasm_brep3d_get_out_edge_count() -> u32 {
6016 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_edge_count)
6017}
6018
6019#[no_mangle]
6020pub extern "C" fn wasm_brep3d_get_out_unmatched_he_count() -> u32 {
6021 WASM_BREP3D_BUF.with(|cell| cell.borrow().out_unmatched_he_count)
6022}
6023
6024#[cfg(test)]
6025mod tests {
6026 use super::*;
6027
6028 #[test]
6029 fn test_3d_overlapping_boxes_inclusion_exclusion_and_manifold() {
6030 let box_a = Solid3D::from_box([0.0, 0.0, 0.0], [10.0, 10.0, 10.0]);
6032 let box_b = Solid3D::from_box([5.0, 5.0, 5.0], [15.0, 15.0, 15.0]);
6033 let opts = KernelOptions::default();
6034
6035 let u = union_3d(&box_a, &box_b, opts);
6036 let i = intersect_3d(&box_a, &box_b, opts);
6037 let d = difference_3d(&box_a, &box_b, opts);
6038 let x = xor_3d(&box_a, &box_b, opts);
6039
6040 assert!((u.total_volume - 1875.0).abs() < 1e-4, "u.vol = {}", u.total_volume);
6041 assert!((i.total_volume - 125.0).abs() < 1e-4, "i.vol = {}", i.total_volume);
6042 assert!((d.total_volume - 875.0).abs() < 1e-4, "d.vol = {}", d.total_volume);
6043 assert!((x.total_volume - 1750.0).abs() < 1e-4, "x.vol = {}", x.total_volume);
6044
6045 assert_eq!(u.unmatched_half_edge_count, 0);
6047 assert_eq!(i.unmatched_half_edge_count, 0);
6048 assert_eq!(d.unmatched_half_edge_count, 0);
6049 assert_eq!(x.unmatched_half_edge_count, 0);
6050 }
6051
6052 #[test]
6053 fn test_3d_coplanar_kissing_boxes_and_through_hole() {
6054 let left = Solid3D::from_box([0.0, 0.0, 0.0], [10.0, 10.0, 10.0]);
6057 let right = Solid3D::from_box([10.0, 0.0, 0.0], [20.0, 10.0, 10.0]);
6058 let opts = KernelOptions::default();
6059
6060 let u = union_3d(&left, &right, opts);
6061 assert!((u.total_volume - 2000.0).abs() < 1e-4);
6062 assert_eq!(u.solid.faces.len(), 6);
6063 assert_eq!(u.unmatched_half_edge_count, 0);
6064
6065 let i = intersect_3d(&left, &right, opts);
6067 assert_eq!(i.solid.faces.len(), 0);
6068 assert!(i.total_volume.abs() < 1e-6);
6069
6070 let pillar = Solid3D::from_box([3.0, 3.0, -2.0], [7.0, 7.0, 12.0]);
6073 let holed = difference_3d(&left, &pillar, opts);
6074 assert!((holed.total_volume - 840.0).abs() < 1e-4, "holed.vol = {}", holed.total_volume);
6076 assert_eq!(holed.unmatched_half_edge_count, 0);
6077 let faces_with_holes = holed.solid.faces.iter().filter(|f| !f.holes.is_empty()).count();
6079 assert_eq!(faces_with_holes, 2);
6080
6081 let mut tri_area = 0.0;
6083 let mut tri_vol6 = 0.0;
6084 for f in &holed.solid.faces {
6085 let f_tris = triangulate_face_3d(f);
6086 let mut f_tri_area = 0.0;
6087 for [a, b, c] in f_tris {
6088 let cr = b.sub(a).cross(c.sub(a));
6089 assert!(
6091 cr.dot(f.normal) > 0.0,
6092 "Triangle orientation inverted relative to face normal!"
6093 );
6094 f_tri_area += 0.5 * cr.norm();
6095 tri_vol6 += a.dot(b.cross(c));
6096 }
6097 assert!(
6098 (f_tri_area - f.area()).abs() < 1e-6,
6099 "Face triangulation area mismatch: {} != {}",
6100 f_tri_area,
6101 f.area()
6102 );
6103 tri_area += f_tri_area;
6104 }
6105 assert!((tri_area - holed.total_surface_area).abs() < 1e-5);
6106 assert!((tri_vol6 / 6.0 - holed.total_volume).abs() < 1e-5);
6107
6108 let block = Solid3D::from_box([-6.0, -6.0, -4.0], [6.0, 6.0, 4.0]);
6110 for &dx in &[-4.0, -3.5, -2.0, 0.0, 1.75, 3.5, 4.5] {
6111 for &dy in &[-3.5, 0.0, 2.25, 3.5] {
6112 let cutter = Solid3D::from_box(
6113 [-2.5 + dx, -2.5 + dy, -6.5],
6114 [2.5 + dx, 2.5 + dy, 6.5],
6115 );
6116 let res = difference_3d(&block, &cutter, opts);
6117 assert_eq!(
6118 res.unmatched_half_edge_count, 0,
6119 "Sweep dx={}, dy={} unmatched half-edges={}",
6120 dx, dy, res.unmatched_half_edge_count
6121 );
6122 for f in &res.solid.faces {
6123 let origin = f.outer[0];
6124 let (u_ax, v_ax) = build_plane_frame(f.normal);
6125 let proj = |p: &Point3D| Point2D {
6126 x: p.sub(origin).dot(u_ax),
6127 y: p.sub(origin).dot(v_ax),
6128 };
6129 let mut f_loops_2d = vec![f.outer.iter().map(proj).collect::<Vec<_>>()];
6130 for h in &f.holes {
6131 f_loops_2d.push(h.iter().map(proj).collect::<Vec<_>>());
6132 }
6133 let tris = triangulate_face_3d(f);
6134 let mut f_area = 0.0;
6135 for [ta, tb, tc] in tris {
6136 let cr = tb.sub(ta).cross(tc.sub(ta));
6137 assert!(cr.dot(f.normal) > 0.0);
6138 let cent_3d = ta.add(tb).add(tc).scale(1.0 / 3.0);
6139 assert_eq!(
6140 loops_winding_2d(proj(¢_3d), &f_loops_2d),
6141 1,
6142 "Sweep dx={}, dy={} triangle centroid inside hole or outside face!",
6143 dx,
6144 dy
6145 );
6146 f_area += 0.5 * cr.norm();
6147 }
6148 assert!((f_area - f.area()).abs() < 1e-5);
6149 }
6150 }
6151 }
6152
6153 let p1 = Solid3D::from_box([-4.5, -2.0, -6.0], [-1.0, 2.0, 6.0]);
6155 let p2 = Solid3D::from_box([1.0, -2.0, -6.0], [4.5, 2.0, 6.0]);
6156 let step1 = difference_3d(&block, &p1, opts);
6157 let step2 = difference_3d(&step1.solid, &p2, opts);
6158 assert_eq!(step2.unmatched_half_edge_count, 0);
6159 let expected_vol = 12.0 * 12.0 * 8.0 - 2.0 * (3.5 * 4.0 * 8.0);
6160 assert!((step2.total_volume - expected_vol).abs() < 1e-5);
6161 for f in &step2.solid.faces {
6162 if f.normal.z.abs() > 0.9 {
6163 assert_eq!(f.holes.len(), 2, "Cap face should have 2 interior holes!");
6164 }
6165 let tris = triangulate_face_3d(f);
6166 let f_area: f64 = tris
6167 .iter()
6168 .map(|[a, b, c]| 0.5 * b.sub(*a).cross(c.sub(*a)).norm())
6169 .sum();
6170 assert!((f_area - f.area()).abs() < 1e-5);
6171 }
6172 }
6173
6174 #[test]
6175 fn test_3d_extruded_polygons_and_adversarial_fuzzer() {
6176 let l_poly = [
6178 [0.0, 0.0],
6179 [12.0, 0.0],
6180 [12.0, 4.0],
6181 [4.0, 4.0],
6182 [4.0, 12.0],
6183 [0.0, 12.0],
6184 ];
6185 let l_solid = Solid3D::from_extruded_polygon(&l_poly, &[], 0.0, 8.0);
6186 assert!((l_solid.volume() - 640.0).abs() < 1e-4);
6188
6189 let cross_bar = Solid3D::from_box([2.0, 2.0, 2.0], [10.0, 10.0, 6.0]);
6190 let opts = KernelOptions::default();
6191
6192 let u = union_3d(&l_solid, &cross_bar, opts);
6193 let i = intersect_3d(&l_solid, &cross_bar, opts);
6194 let d_ab = difference_3d(&l_solid, &cross_bar, opts);
6195 let d_ba = difference_3d(&cross_bar, &l_solid, opts);
6196 let x = xor_3d(&l_solid, &cross_bar, opts);
6197
6198 let vol_a = l_solid.volume();
6199 let vol_b = cross_bar.volume();
6200 assert!(
6201 (u.total_volume + i.total_volume - (vol_a + vol_b)).abs() < 1e-3,
6202 "Inclusion-Exclusion failed: {} + {} != {} + {}",
6203 u.total_volume,
6204 i.total_volume,
6205 vol_a,
6206 vol_b
6207 );
6208 assert!(
6209 (d_ab.total_volume + d_ba.total_volume - x.total_volume).abs() < 1e-3,
6210 "Symmetric difference decomposition failed"
6211 );
6212 assert_eq!(u.unmatched_half_edge_count, 0);
6213 assert_eq!(i.unmatched_half_edge_count, 0);
6214 assert_eq!(d_ab.unmatched_half_edge_count, 0);
6215 assert_eq!(x.unmatched_half_edge_count, 0);
6216
6217 let mut seed = 0x9E37_79B9_7F4A_7C15u64;
6219 let mut next_f64 = || -> f64 {
6220 seed ^= seed << 13;
6221 seed ^= seed >> 7;
6222 seed ^= seed << 17;
6223 (seed as f64) / (u64::MAX as f64)
6224 };
6225
6226 for trial in 0..40 {
6227 let x0 = (next_f64() * 8.0).floor();
6228 let y0 = (next_f64() * 8.0).floor();
6229 let z0 = (next_f64() * 8.0).floor();
6230 let dx = 3.0 + (next_f64() * 8.0).floor();
6231 let dy = 3.0 + (next_f64() * 8.0).floor();
6232 let dz = 3.0 + (next_f64() * 8.0).floor();
6233 let a = Solid3D::from_box([x0, y0, z0], [x0 + dx, y0 + dy, z0 + dz]);
6234
6235 let jitter = if trial % 2 == 0 {
6237 0.0
6238 } else {
6239 (next_f64() - 0.5) * 0.37
6240 };
6241 let bx0 = (next_f64() * 8.0).floor() + jitter;
6242 let by0 = (next_f64() * 8.0).floor() + jitter;
6243 let bz0 = (next_f64() * 8.0).floor() + jitter;
6244 let bdx = 3.0 + (next_f64() * 8.0).floor();
6245 let bdy = 3.0 + (next_f64() * 8.0).floor();
6246 let bdz = 3.0 + (next_f64() * 8.0).floor();
6247 let b = Solid3D::from_box([bx0, by0, bz0], [bx0 + bdx, by0 + bdy, bz0 + bdz]);
6248
6249 let u = union_3d(&a, &b, opts);
6250 let i = intersect_3d(&a, &b, opts);
6251 let d = difference_3d(&a, &b, opts);
6252
6253 let va = a.volume();
6254 let vb = b.volume();
6255
6256 assert!(
6257 (u.total_volume + i.total_volume - (va + vb)).abs() < 1e-2,
6258 "Trial {} failed inclusion-exclusion: u={} + i={} != va={} + vb={}",
6259 trial,
6260 u.total_volume,
6261 i.total_volume,
6262 va,
6263 vb
6264 );
6265 assert!(
6266 (d.total_volume + i.total_volume - va).abs() < 1e-2,
6267 "Trial {} failed difference identity: d={} + i={} != va={}",
6268 trial,
6269 d.total_volume,
6270 i.total_volume,
6271 va
6272 );
6273 assert_eq!(
6274 u.unmatched_half_edge_count, 0,
6275 "Trial {} union non-manifold: {} unmatched half-edges",
6276 trial, u.unmatched_half_edge_count
6277 );
6278 assert_eq!(
6279 i.unmatched_half_edge_count, 0,
6280 "Trial {} intersect non-manifold: {} unmatched half-edges",
6281 trial, i.unmatched_half_edge_count
6282 );
6283 assert_eq!(
6284 d.unmatched_half_edge_count, 0,
6285 "Trial {} difference non-manifold: {} unmatched half-edges",
6286 trial, d.unmatched_half_edge_count
6287 );
6288
6289 for (label, res) in [("union", &u), ("intersect", &i), ("difference", &d)] {
6290 let mut mesh_vol6 = 0.0;
6291 for f in &res.solid.faces {
6292 let tris = triangulate_face_3d(f);
6293 let mut f_area = 0.0;
6294 let origin = f.outer[0];
6295 let (u_ax, v_ax) = build_plane_frame(f.normal);
6296 let proj = |p: &Point3D| Point2D {
6297 x: p.sub(origin).dot(u_ax),
6298 y: p.sub(origin).dot(v_ax),
6299 };
6300 let mut f_loops_2d = vec![f.outer.iter().map(proj).collect::<Vec<_>>()];
6301 for h in &f.holes {
6302 f_loops_2d.push(h.iter().map(proj).collect::<Vec<_>>());
6303 }
6304
6305 for [ta, tb, tc] in tris {
6306 let cr = tb.sub(ta).cross(tc.sub(ta));
6307 assert!(
6308 cr.dot(f.normal) > 0.0,
6309 "Trial {} {} triangle normal inverted!",
6310 trial,
6311 label
6312 );
6313 let cent_3d = ta.add(tb).add(tc).scale(1.0 / 3.0);
6314 let cent_2d = proj(¢_3d);
6315 assert_eq!(
6316 loops_winding_2d(cent_2d, &f_loops_2d),
6317 1,
6318 "Trial {} {} triangle centroid lies outside face or inside a hole!",
6319 trial,
6320 label
6321 );
6322 f_area += 0.5 * cr.norm();
6323 mesh_vol6 += ta.dot(tb.cross(tc));
6324 }
6325 assert!(
6326 (f_area - f.area()).abs() < 1e-4,
6327 "Trial {} {} face triangulation area mismatch: {} != {}",
6328 trial,
6329 label,
6330 f_area,
6331 f.area()
6332 );
6333 }
6334 assert!(
6335 (mesh_vol6 / 6.0 - res.total_volume).abs() < 1e-3,
6336 "Trial {} {} mesh volume mismatch: {} != {}",
6337 trial,
6338 label,
6339 mesh_vol6 / 6.0,
6340 res.total_volume
6341 );
6342 }
6343 }
6344 }
6345
6346 fn make_cylinder_3d(radius: f64, z_min: f64, z_max: f64, segs: usize) -> Solid3D {
6347 let mut ring = Vec::with_capacity(segs);
6348 for i in 0..segs {
6349 let a = (2.0 * core::f64::consts::PI * (i as f64)) / (segs as f64);
6350 ring.push((radius * a.cos(), radius * a.sin()));
6351 }
6352 let mut faces = Vec::new();
6353 faces.push(Face3D {
6354 outer: ring.iter().rev().map(|&(x, y)| Point3D::new(x, y, z_min)).collect(),
6355 holes: vec![],
6356 normal: Point3D::new(0.0, 0.0, -1.0),
6357 });
6358 faces.push(Face3D {
6359 outer: ring.iter().map(|&(x, y)| Point3D::new(x, y, z_max)).collect(),
6360 holes: vec![],
6361 normal: Point3D::new(0.0, 0.0, 1.0),
6362 });
6363 for i in 0..segs {
6364 let (x0, y0) = ring[i];
6365 let (x1, y1) = ring[(i + 1) % segs];
6366 let outer = vec![
6367 Point3D::new(x0, y0, z_min),
6368 Point3D::new(x1, y1, z_min),
6369 Point3D::new(x1, y1, z_max),
6370 Point3D::new(x0, y0, z_max),
6371 ];
6372 let normal = compute_newell_normal(&outer).unwrap();
6373 faces.push(Face3D { outer, holes: vec![], normal });
6374 }
6375 Solid3D { faces }
6376 }
6377
6378 fn rotate_and_shift_solid(
6379 solid: &Solid3D,
6380 rx_deg: f64,
6381 ry_deg: f64,
6382 rz_deg: f64,
6383 tx: f64,
6384 ty: f64,
6385 tz: f64,
6386 ) -> Solid3D {
6387 let rx = rx_deg.to_radians();
6388 let ry = ry_deg.to_radians();
6389 let rz = rz_deg.to_radians();
6390 let (cx, sx) = (rx.cos(), rx.sin());
6391 let (cy, sy) = (ry.cos(), ry.sin());
6392 let (cz, sz) = (rz.cos(), rz.sin());
6393 let xform = |p: Point3D| -> Point3D {
6394 let y1 = p.y * cx - p.z * sx;
6395 let z1 = p.y * sx + p.z * cx;
6396 let x2 = p.x * cy + z1 * sy;
6397 let z2 = -p.x * sy + z1 * cy;
6398 let x3 = x2 * cz - y1 * sz;
6399 let y3 = x2 * sz + y1 * cz;
6400 Point3D::new(x3 + tx, y3 + ty, z2 + tz)
6401 };
6402 let mut faces = Vec::with_capacity(solid.faces.len());
6403 for f in &solid.faces {
6404 let outer: Vec<Point3D> = f.outer.iter().copied().map(xform).collect();
6405 let normal = compute_newell_normal(&outer).unwrap();
6406 let holes = f.holes.iter().map(|h| h.iter().copied().map(xform).collect()).collect();
6407 faces.push(Face3D { outer, holes, normal });
6408 }
6409 Solid3D { faces }
6410 }
6411
6412 fn make_cone_3d(r_bot: f64, r_top: f64, z_min: f64, z_max: f64, segs: usize) -> Solid3D {
6413 let mut bot = Vec::with_capacity(segs);
6414 let mut top = Vec::with_capacity(segs);
6415 for i in 0..segs {
6416 let a = (2.0 * core::f64::consts::PI * (i as f64)) / (segs as f64);
6417 bot.push((r_bot * a.cos(), r_bot * a.sin()));
6418 top.push((r_top * a.cos(), r_top * a.sin()));
6419 }
6420 let mut faces = Vec::new();
6421 faces.push(Face3D {
6422 outer: bot
6423 .iter()
6424 .rev()
6425 .map(|&(x, y)| Point3D::new(x, y, z_min))
6426 .collect(),
6427 holes: vec![],
6428 normal: Point3D::new(0.0, 0.0, -1.0),
6429 });
6430 if r_top <= 1e-9 {
6431 let apex = Point3D::new(0.0, 0.0, z_max);
6432 for i in 0..segs {
6433 let (x0, y0) = bot[i];
6434 let (x1, y1) = bot[(i + 1) % segs];
6435 let outer = vec![Point3D::new(x0, y0, z_min), Point3D::new(x1, y1, z_min), apex];
6436 let normal = compute_newell_normal(&outer).unwrap();
6437 faces.push(Face3D {
6438 outer,
6439 holes: vec![],
6440 normal,
6441 });
6442 }
6443 } else {
6444 faces.push(Face3D {
6445 outer: top
6446 .iter()
6447 .map(|&(x, y)| Point3D::new(x, y, z_max))
6448 .collect(),
6449 holes: vec![],
6450 normal: Point3D::new(0.0, 0.0, 1.0),
6451 });
6452 for i in 0..segs {
6453 let (bx0, by0) = bot[i];
6454 let (bx1, by1) = bot[(i + 1) % segs];
6455 let (tx0, ty0) = top[i];
6456 let (tx1, ty1) = top[(i + 1) % segs];
6457 let outer = vec![
6458 Point3D::new(bx0, by0, z_min),
6459 Point3D::new(bx1, by1, z_min),
6460 Point3D::new(tx1, ty1, z_max),
6461 Point3D::new(tx0, ty0, z_max),
6462 ];
6463 let normal = compute_newell_normal(&outer).unwrap();
6464 faces.push(Face3D {
6465 outer,
6466 holes: vec![],
6467 normal,
6468 });
6469 }
6470 }
6471 Solid3D { faces }
6472 }
6473
6474 #[test]
6475 fn test_glancing_cylinders_and_cones_at_coarse_tolerance() {
6476 let opts = KernelOptions {
6477 snap_tolerance: Some(0.1),
6478 winding_rule: WindingRule::NonZero,
6479 simplify_collinear: false,
6480 };
6481
6482 let c2_a = make_cylinder_3d(3.5, -6.0, 6.0, 16);
6484 let c2_b = rotate_and_shift_solid(&c2_a, 1.75, 0.8, 7.5, 0.4, 0.0, 0.0);
6485 let u_c2 = union_3d(&c2_a, &c2_b, opts);
6486 let i_c2 = intersect_3d(&c2_a, &c2_b, opts);
6487 let d_c2 = difference_3d(&c2_a, &c2_b, opts);
6488 assert_eq!(u_c2.unmatched_half_edge_count, 0);
6489 assert_eq!(i_c2.unmatched_half_edge_count, 0);
6490 assert_eq!(d_c2.unmatched_half_edge_count, 0);
6491 assert!(
6492 (u_c2.total_volume + i_c2.total_volume - (c2_a.volume() + c2_b.volume())).abs() < 1.0,
6493 "C2 inclusion-exclusion failed: {} + {} != {} + {}",
6494 u_c2.total_volume,
6495 i_c2.total_volume,
6496 c2_a.volume(),
6497 c2_b.volume()
6498 );
6499 assert!((d_c2.total_volume + i_c2.total_volume - c2_a.volume()).abs() < 1.0);
6500 for tol in [1e-7, 1e-4, 1e-2, 0.1] {
6501 let t_opts = KernelOptions {
6502 snap_tolerance: Some(tol),
6503 winding_rule: WindingRule::NonZero,
6504 simplify_collinear: false,
6505 };
6506 let u = union_3d(&c2_a, &c2_b, t_opts);
6507 let i = intersect_3d(&c2_a, &c2_b, t_opts);
6508 assert!(
6509 (u.total_volume + i.total_volume - (c2_a.volume() + c2_b.volume())).abs() < 1.0,
6510 "C2 tol={} inclusion-exclusion failed: {} + {} != {} + {}",
6511 tol,
6512 u.total_volume,
6513 i.total_volume,
6514 c2_a.volume(),
6515 c2_b.volume()
6516 );
6517 }
6518
6519 let k3_a = make_cylinder_3d(4.0, -4.0, 4.0, 16);
6521 let k3_b = rotate_and_shift_solid(
6522 &make_cone_3d(5.0, 1.5, -4.0, 4.0, 16),
6523 2.2,
6524 0.0,
6525 11.25,
6526 0.15,
6527 0.0,
6528 0.0,
6529 );
6530 let u_k3 = union_3d(&k3_a, &k3_b, opts);
6531 let i_k3 = intersect_3d(&k3_a, &k3_b, opts);
6532 let d_k3 = difference_3d(&k3_a, &k3_b, opts);
6533 assert_eq!(u_k3.unmatched_half_edge_count, 0);
6534 assert_eq!(i_k3.unmatched_half_edge_count, 0);
6535 assert_eq!(d_k3.unmatched_half_edge_count, 0);
6536 assert!(
6537 (u_k3.total_volume + i_k3.total_volume - (k3_a.volume() + k3_b.volume())).abs() < 1.0,
6538 "K3 inclusion-exclusion failed: {} + {} != {} + {}",
6539 u_k3.total_volume,
6540 i_k3.total_volume,
6541 k3_a.volume(),
6542 k3_b.volume()
6543 );
6544 assert!((d_k3.total_volume + i_k3.total_volume - k3_a.volume()).abs() < 1.0);
6545
6546 let k4_a = make_cone_3d(4.0, 0.0, -4.0, 4.0, 16);
6548 let k4_b = rotate_and_shift_solid(&k4_a, 181.5, 1.0, 11.25, 3.75, 0.0, 0.0);
6549 let u_k4 = union_3d(&k4_a, &k4_b, opts);
6550 let i_k4 = intersect_3d(&k4_a, &k4_b, opts);
6551 let d_k4 = difference_3d(&k4_a, &k4_b, opts);
6552 assert_eq!(u_k4.unmatched_half_edge_count, 0);
6553 assert_eq!(i_k4.unmatched_half_edge_count, 0);
6554 assert_eq!(d_k4.unmatched_half_edge_count, 0);
6555 assert!(
6556 (u_k4.total_volume + i_k4.total_volume - (k4_a.volume() + k4_b.volume())).abs() < 2.0,
6557 "K4 inclusion-exclusion failed: {} + {} != {} + {}",
6558 u_k4.total_volume,
6559 i_k4.total_volume,
6560 k4_a.volume(),
6561 k4_b.volume()
6562 );
6563 assert!((d_k4.total_volume + i_k4.total_volume - k4_a.volume()).abs() < 2.0);
6564
6565 let box_a = Solid3D::from_box([-5.0, -5.0, -5.0], [5.0, 5.0, 5.0]);
6566 let box_b = rotate_and_shift_solid(
6567 &Solid3D::from_box([-4.5, -4.5, -4.5], [4.5, 4.5, 4.5]),
6568 -22.0 * 0.6,
6569 -22.0,
6570 -22.0 * 0.35,
6571 3.1,
6572 0.8,
6573 -0.4,
6574 );
6575 for tol in [1e-4, 0.05, 10.0f64.powf(-0.55), 0.5] {
6576 let b_opts = KernelOptions {
6577 snap_tolerance: Some(tol),
6578 winding_rule: WindingRule::NonZero,
6579 simplify_collinear: false,
6580 };
6581 let u_b = union_3d(&box_a, &box_b, b_opts);
6582 let i_b = intersect_3d(&box_a, &box_b, b_opts);
6583 let d_b = difference_3d(&box_a, &box_b, b_opts);
6584 let x_b = xor_3d(&box_a, &box_b, b_opts);
6585 assert!(
6586 u_b.total_volume > 0.0
6587 && i_b.total_volume > 0.0
6588 && d_b.total_volume > 0.0
6589 && x_b.total_volume > 0.0,
6590 "tol={} volumes: u={}, i={}, d={}, x={}",
6591 tol,
6592 u_b.total_volume,
6593 i_b.total_volume,
6594 d_b.total_volume,
6595 x_b.total_volume
6596 );
6597 }
6598
6599 let p1_a = Solid3D::from_extruded_polygon(
6601 &[[-6.0, -6.0], [6.0, -6.0], [6.0, 6.0], [-6.0, 6.0]],
6602 &[
6603 vec![[-4.0, -4.0], [-1.0, -4.0], [-1.0, -1.0], [-4.0, -1.0]],
6604 vec![[1.0, 1.0], [4.0, 1.0], [4.0, 4.0], [1.0, 4.0]],
6605 ],
6606 -2.0,
6607 2.0,
6608 );
6609 let p1_b = rotate_and_shift_solid(
6610 &make_cone_3d(4.5, 2.0, -4.0, 4.0, 16),
6611 1.3,
6612 -0.9,
6613 7.5,
6614 0.2,
6615 0.2,
6616 0.0,
6617 );
6618 for tol_p1 in [1e-4, 1e-2, 0.1] {
6619 let p1_opts = KernelOptions {
6620 snap_tolerance: Some(tol_p1),
6621 winding_rule: WindingRule::NonZero,
6622 simplify_collinear: false,
6623 };
6624 let u_p1 = union_3d(&p1_a, &p1_b, p1_opts);
6625 let i_p1 = intersect_3d(&p1_a, &p1_b, p1_opts);
6626 let d_p1 = difference_3d(&p1_a, &p1_b, p1_opts);
6627 assert_eq!(u_p1.unmatched_half_edge_count, 0);
6628 assert_eq!(i_p1.unmatched_half_edge_count, 0);
6629 assert_eq!(d_p1.unmatched_half_edge_count, 0);
6630 assert!(
6631 (u_p1.total_volume + i_p1.total_volume - (p1_a.volume() + p1_b.volume())).abs()
6632 < 1.0,
6633 "P1 tol={} inclusion-exclusion failed: {} + {} != {} + {}",
6634 tol_p1,
6635 u_p1.total_volume,
6636 i_p1.total_volume,
6637 p1_a.volume(),
6638 p1_b.volume()
6639 );
6640 assert!((d_p1.total_volume + i_p1.total_volume - p1_a.volume()).abs() < 1.0);
6641 }
6642 }
6643}