1use std::sync::OnceLock;
63
64use super::geom::opening_mesh_thinnest_axis_dir;
65use super::{cutter_is_closed_manifold, GeometryRouter, OpeningType, VoidContext};
66use crate::cdt::triangulate_pslg;
67use crate::mesh::Mesh;
68use nalgebra::Point2;
69use rustc_hash::FxHashMap;
70
71pub(crate) mod closure_checks;
74mod vertex_dedup;
75mod finish;
76mod tjunction_close;
77mod planar_correction;
78#[cfg(test)]
79#[path = "prism_cut_tests.rs"]
80mod tests;
81use closure_checks::{closed_enough_to_emit, directed_closed, ClosureVerdict};
82#[cfg(test)]
83use closure_checks::closed_or_hairline;
84pub(crate) use vertex_dedup::dedup_cut_vertices;
85pub(in crate::router::voids) use finish::finish_cut_verdict;
86pub(super) use planar_correction::correct_planar_overlap;
87
88pub(super) fn enabled() -> bool {
89 static ON: OnceLock<bool> = OnceLock::new();
90 *ON.get_or_init(|| std::env::var("IFC_LITE_PRISM_CUT").as_deref() != Ok("0"))
91}
92
93#[cfg(any(feature = "observability", feature = "csg_capture", feature = "debug_geometry"))]
104mod diag {
105 use std::sync::atomic::{AtomicU64, Ordering};
106
107 static FIRES: AtomicU64 = AtomicU64::new(0);
108 static OPENINGS_ANALYTIC: AtomicU64 = AtomicU64::new(0);
109 static OPENINGS_RESIDUAL: AtomicU64 = AtomicU64::new(0);
110
111 const DEFER_NAMES: [&str; 10] = [
113 "host_not_closed",
114 "host_promote_fail",
115 "op_not_prism",
116 "op_tiny",
117 "op_no_overlap",
118 "op_engulf",
119 "op_veto_overlap",
120 "cut_cdt_fail",
121 "cut_partition_fail",
122 "out_not_closed",
123 ];
124 static DEFERS: [AtomicU64; 10] = [
125 AtomicU64::new(0),
126 AtomicU64::new(0),
127 AtomicU64::new(0),
128 AtomicU64::new(0),
129 AtomicU64::new(0),
130 AtomicU64::new(0),
131 AtomicU64::new(0),
132 AtomicU64::new(0),
133 AtomicU64::new(0),
134 AtomicU64::new(0),
135 ];
136
137 #[inline]
138 pub(super) fn defer(i: usize) {
139 #[cfg(feature = "opening-perf-trace")]
140 crate::opening_perf_trace::record(|c| c.prism_defers[i] = c.prism_defers[i].saturating_add(1));
141 crate::telemetry_transaction::record(move || {
142 DEFERS[i].fetch_add(1, Ordering::Relaxed);
143 });
144 }
145
146 #[inline]
148 pub(super) fn record_cut(committed_ops: usize, residual: usize) {
149 #[cfg(feature = "opening-perf-trace")]
150 crate::opening_perf_trace::record(|c| {
151 c.prism_candidate_successes = c.prism_candidate_successes.saturating_add(1);
152 c.prism_candidate_openings = c.prism_candidate_openings.saturating_add(committed_ops as u64);
153 c.prism_residual_openings = c.prism_residual_openings.saturating_add(residual as u64);
154 });
155 crate::telemetry_transaction::record(move || {
156 FIRES.fetch_add(1, Ordering::Relaxed);
157 OPENINGS_ANALYTIC.fetch_add(committed_ops as u64, Ordering::Relaxed);
158 OPENINGS_RESIDUAL.fetch_add(residual as u64, Ordering::Relaxed);
159 });
160 }
161
162 pub fn take_prism_defers() -> Vec<(&'static str, u64)> {
164 DEFER_NAMES
165 .iter()
166 .enumerate()
167 .map(|(i, n)| (*n, DEFERS[i].swap(0, Ordering::Relaxed)))
168 .collect()
169 }
170
171 pub fn take_prism_stats() -> (u64, u64, u64) {
177 (
178 FIRES.swap(0, Ordering::Relaxed),
179 OPENINGS_ANALYTIC.swap(0, Ordering::Relaxed),
180 OPENINGS_RESIDUAL.swap(0, Ordering::Relaxed),
181 )
182 }
183}
184
185#[cfg(not(any(feature = "observability", feature = "csg_capture", feature = "debug_geometry")))]
186mod diag {
187 #[inline]
188 pub(super) fn defer(_i: usize) {
189 #[cfg(feature = "opening-perf-trace")]
190 crate::opening_perf_trace::record(|c| c.prism_defers[_i] = c.prism_defers[_i].saturating_add(1));
191 }
192 #[inline]
193 pub(super) fn record_cut(_committed_ops: usize, _residual: usize) {
194 #[cfg(feature = "opening-perf-trace")]
195 crate::opening_perf_trace::record(|c| {
196 c.prism_candidate_successes = c.prism_candidate_successes.saturating_add(1);
197 c.prism_candidate_openings = c.prism_candidate_openings.saturating_add(_committed_ops as u64);
198 c.prism_residual_openings = c.prism_residual_openings.saturating_add(_residual as u64);
199 });
200 }
201 pub fn take_prism_defers() -> Vec<(&'static str, u64)> {
204 Vec::new()
205 }
206 pub fn take_prism_stats() -> (u64, u64, u64) {
207 (0, 0, 0)
208 }
209}
210
211use diag::{defer, record_cut};
212pub use diag::{take_prism_defers, take_prism_stats};
213
214pub(super) type V3 = [f64; 3];
219type V2 = [f64; 2];
220
221#[inline]
222pub(super) fn dot(a: V3, b: V3) -> f64 {
223 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
224}
225#[inline]
226pub(super) fn sub(a: V3, b: V3) -> V3 {
227 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
228}
229#[inline]
230pub(super) fn add(a: V3, b: V3) -> V3 {
231 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
232}
233#[inline]
234pub(super) fn scale(a: V3, s: f64) -> V3 {
235 [a[0] * s, a[1] * s, a[2] * s]
236}
237#[inline]
238pub(super) fn cross(a: V3, b: V3) -> V3 {
239 [
240 a[1] * b[2] - a[2] * b[1],
241 a[2] * b[0] - a[0] * b[2],
242 a[0] * b[1] - a[1] * b[0],
243 ]
244}
245#[inline]
246pub(super) fn norm(a: V3) -> f64 {
247 dot(a, a).sqrt()
248}
249#[inline]
250pub(super) fn normalize(a: V3) -> Option<V3> {
251 let n = norm(a);
252 if !n.is_finite() || n < 1.0e-12 {
253 None
254 } else {
255 Some(scale(a, 1.0 / n))
256 }
257}
258#[inline]
259pub(super) fn lerp(a: V3, b: V3, t: f64) -> V3 {
260 [
261 a[0] + (b[0] - a[0]) * t,
262 a[1] + (b[1] - a[1]) * t,
263 a[2] + (b[2] - a[2]) * t,
264 ]
265}
266#[inline]
267pub(super) fn bits(p: V3) -> [u64; 3] {
268 [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()]
269}
270
271fn pip(pt: V2, poly: &[V2]) -> bool {
275 let (x, y) = (pt[0], pt[1]);
276 let n = poly.len();
277 let mut inside = false;
278 let mut j = n - 1;
279 for i in 0..n {
280 let (xi, yi) = (poly[i][0], poly[i][1]);
281 let (xj, yj) = (poly[j][0], poly[j][1]);
282 if (yi > y) != (yj > y) {
283 let xc = xi + (y - yi) / (yj - yi) * (xj - xi);
284 if x < xc {
285 inside = !inside;
286 }
287 }
288 j = i;
289 }
290 inside
291}
292
293fn seg_cross_param(p: V2, q: V2, a: V2, b: V2) -> Option<f64> {
296 let r = [q[0] - p[0], q[1] - p[1]];
297 let s = [b[0] - a[0], b[1] - a[1]];
298 let denom = r[0] * s[1] - r[1] * s[0];
299 if denom.abs() < 1.0e-18 {
300 return None;
301 }
302 let ap = [a[0] - p[0], a[1] - p[1]];
303 let t = (ap[0] * s[1] - ap[1] * s[0]) / denom;
304 let u = (ap[0] * r[1] - ap[1] * r[0]) / denom;
305 const E: f64 = 1.0e-12;
306 if (-E..=1.0 + E).contains(&t) && (-E..=1.0 + E).contains(&u) {
307 Some(t.clamp(0.0, 1.0))
308 } else {
309 None
310 }
311}
312
313#[derive(Clone)]
315struct PTri {
316 p: [V3; 3],
317 n: [V3; 3],
318}
319
320impl PTri {
321 #[inline]
322 fn aabb(&self) -> (V3, V3) {
323 let mut lo = self.p[0];
324 let mut hi = self.p[0];
325 for v in &self.p[1..] {
326 for k in 0..3 {
327 lo[k] = lo[k].min(v[k]);
328 hi[k] = hi[k].max(v[k]);
329 }
330 }
331 (lo, hi)
332 }
333}
334
335fn partition_consistent(host: &[PTri], out: &[PTri], inside: &[PTri], bounds: &[(V3, V3)]) -> bool {
346 let mut lo = [f64::INFINITY; 3];
347 let mut hi = [f64::NEG_INFINITY; 3];
348 for (a, b) in bounds {
349 for k in 0..3 {
350 lo[k] = lo[k].min(a[k]);
351 hi[k] = hi[k].max(b[k]);
352 }
353 }
354 let origin: V3 = std::array::from_fn(|k| (lo[k] + hi[k]) * 0.5);
355 let extent = (0..3).map(|k| hi[k] - lo[k]).fold(0.0, f64::max);
356 let sum = |ts: &[PTri], reference: &V3| {
357 ts.iter()
358 .map(|t| {
359 crate::kernel::signed_volume::tetra_volume6(&t.p[0], &t.p[1], &t.p[2], reference)
360 })
361 .sum::<f64>()
362 / 6.0
363 };
364 let tolerance =
365 (1.0e-12 * (1.0 + extent).powi(3) + 1.0e-9).max(sum(host, &origin).abs() * 1.0e-6);
366 let residual =
367 |reference: &V3| sum(out, reference) + sum(inside, reference) - sum(host, reference);
368 let center = residual(&origin);
369 let range = (0..3)
370 .map(|axis| {
371 let mut reference = origin;
372 reference[axis] = hi[axis];
373 2.0 * (residual(&reference) - center).abs()
374 })
375 .sum::<f64>();
376 center.is_finite() && range.is_finite() && center.abs() <= tolerance && range <= tolerance
377}
378
379fn has_measurable_region(inside: &[PTri], caps: &[PTri]) -> bool {
385 let faces: Vec<PTri> = inside.iter().chain(caps).cloned().collect();
386 let mut geometry = mesh_from_ptris(&faces, &Mesh::new());
387 geometry.clean_degenerate();
388 if geometry.indices.is_empty() {
389 return false;
390 }
391 let normal = |t: &PTri| cross(sub(t.p[1], t.p[0]), sub(t.p[2], t.p[0]));
392 let Some(face) = faces
393 .iter()
394 .max_by(|a, b| norm(normal(a)).total_cmp(&norm(normal(b))))
395 else {
396 return false;
397 };
398 let origin = face.p[0];
399 let Some(u) = normalize(sub(face.p[1], origin)) else {
400 return false;
401 };
402 let Some(n) = normalize(normal(face)) else {
403 return false;
404 };
405 let axes = [u, cross(n, u), n];
406 let mut lo = [0.0f64; 3];
407 let mut hi = [0.0f64; 3];
408 for t in &faces {
409 for p in t.p {
410 for k in 0..3 {
411 let coordinate = dot(sub(p, origin), axes[k]);
412 lo[k] = lo[k].min(coordinate);
413 hi[k] = hi[k].max(coordinate);
414 }
415 }
416 }
417 let envelope = (0..3).map(|k| hi[k] - lo[k]).product::<f64>();
418 envelope.is_finite() && envelope >= 1.0e-9
419}
420
421fn consolidated_change_is_measurable(before: &Mesh, after: &Mesh) -> bool {
427 let (_, delta, drift) = mesh_change_interval(before, after);
428 delta.is_finite() && drift.is_finite() && delta.abs() + drift >= 1.0e-9
429}
430
431fn mesh_change_interval(before: &Mesh, after: &Mesh) -> (f64, f64, f64) {
432 use super::geom::{mesh_signed_volume_about, volume_reference};
433 let reference = volume_reference(before);
434 let volume = mesh_signed_volume_about(before, &reference);
435 let delta = mesh_signed_volume_about(after, &reference) - volume;
436 let (_, hi) = before.bounds();
437 let drift = (0..3)
438 .map(|axis| {
439 let mut probe = reference;
440 probe[axis] = hi[axis] as f64;
441 (mesh_signed_volume_about(after, &probe)
442 - mesh_signed_volume_about(before, &probe)
443 - delta)
444 .abs()
445 })
446 .sum::<f64>();
447 (volume, delta, drift)
448}
449
450fn result_volume_within_bounds(before: &Mesh, after: &Mesh, cutter_volume: f64) -> bool {
459 let (volume, raw_delta, drift) = mesh_change_interval(before, after);
460 let delta = raw_delta * volume.signum();
461 let area = |mesh: &Mesh| {
462 let p =
463 |i: u32| -> V3 { std::array::from_fn(|k| mesh.positions[i as usize * 3 + k] as f64) };
464 mesh.indices
465 .chunks_exact(3)
466 .map(|t| norm(cross(sub(p(t[1]), p(t[0])), sub(p(t[2]), p(t[0])))) * 0.5)
467 .sum::<f64>()
468 };
469 let (lo, hi) = before.bounds();
470 let extent = (0..3)
471 .map(|k| hi[k] as f64 - lo[k] as f64)
472 .fold(0.0, f64::max);
473 let floor = (1.0e-12 * (1.0 + extent).powi(3) + 1.0e-9).max(volume.abs() * 1.0e-6);
474 let tolerance = (area(before) + area(after)) * crate::kernel::mesh_bridge::SNAP_GRID + floor;
475 let ceiling = cutter_volume.min(volume.abs());
476 volume.is_finite()
477 && volume != 0.0
478 && cutter_volume.is_finite()
479 && cutter_volume >= 0.0
480 && delta.is_finite()
481 && drift.is_finite()
482 && tolerance.is_finite()
483 && delta + drift <= tolerance
484 && -delta + drift <= ceiling + tolerance
485}
486
487fn ptris_from_mesh(mesh: &Mesh) -> Option<Vec<PTri>> {
490 if !mesh.positions.len().is_multiple_of(3) || !mesh.indices.len().is_multiple_of(3) {
495 return None;
496 }
497 let vc = mesh.positions.len() / 3;
498 let have_normals = mesh.normals.len() == mesh.positions.len();
499 let mut out = Vec::with_capacity(mesh.indices.len() / 3);
500 for t in mesh.indices.chunks_exact(3) {
501 if t.iter().any(|&i| i as usize >= vc) {
502 return None;
503 }
504 let read = |i: u32| -> V3 {
505 let b = i as usize * 3;
506 [
507 mesh.positions[b] as f64,
508 mesh.positions[b + 1] as f64,
509 mesh.positions[b + 2] as f64,
510 ]
511 };
512 let p = [read(t[0]), read(t[1]), read(t[2])];
513 if p.iter().any(|v| v.iter().any(|c| !c.is_finite())) {
514 return None;
515 }
516 let n = if have_normals {
517 let rn = |i: u32| -> V3 {
518 let b = i as usize * 3;
519 [
520 mesh.normals[b] as f64,
521 mesh.normals[b + 1] as f64,
522 mesh.normals[b + 2] as f64,
523 ]
524 };
525 [rn(t[0]), rn(t[1]), rn(t[2])]
526 } else {
527 let fnrm =
528 normalize(cross(sub(p[1], p[0]), sub(p[2], p[0]))).unwrap_or([0.0, 0.0, 1.0]);
529 [fnrm, fnrm, fnrm]
530 };
531 out.push(PTri { p, n });
532 }
533 Some(out)
534}
535
536fn mesh_from_ptris(tris: &[PTri], template: &Mesh) -> Mesh {
540 let mut positions = Vec::with_capacity(tris.len() * 9);
541 let mut normals = Vec::with_capacity(tris.len() * 9);
542 let mut indices = Vec::with_capacity(tris.len() * 3);
543 for (i, t) in tris.iter().enumerate() {
544 let face = normalize(cross(sub(t.p[1], t.p[0]), sub(t.p[2], t.p[0])))
545 .unwrap_or([0.0, 0.0, 1.0]);
546 for v in 0..3 {
547 positions.push(t.p[v][0] as f32);
548 positions.push(t.p[v][1] as f32);
549 positions.push(t.p[v][2] as f32);
550 let n = normalize(t.n[v]).unwrap_or(face);
551 normals.push(n[0] as f32);
552 normals.push(n[1] as f32);
553 normals.push(n[2] as f32);
554 }
555 let base = (i * 3) as u32;
556 indices.extend_from_slice(&[base, base + 1, base + 2]);
557 }
558 Mesh {
559 positions,
560 normals,
561 indices,
562 rtc_applied: template.rtc_applied,
563 origin: template.origin,
564 instance_meta: None,
565 local_bounds: None,
566 local_to_world: None,
567 welded_in_object_frame: false,
568 plane_tags: None,
569 }
570}
571
572const MAX_PROFILE_VERTS: usize = 120;
577
578#[derive(Clone)]
584struct PrismFrame {
585 u: V3,
586 v: V3,
587 d: V3,
588 planes: Vec<f64>,
590 profiles: Vec<Vec<V2>>,
592 slab_area: Vec<f64>,
594 slab_interior: Vec<V2>,
597 bb: (V2, V2),
599}
600
601impl PrismFrame {
602 #[inline]
603 fn d0(&self) -> f64 {
604 self.planes[0]
605 }
606 #[inline]
607 fn d1(&self) -> f64 {
608 *self.planes.last().unwrap()
609 }
610 #[inline]
611 fn volume(&self) -> f64 {
612 (0..self.profiles.len())
613 .map(|k| self.slab_area[k] * (self.planes[k + 1] - self.planes[k]))
614 .sum()
615 }
616 #[inline]
619 fn lift(&self, uu: f64, vv: f64, w: f64) -> V3 {
620 add(add(scale(self.u, uu), scale(self.v, vv)), scale(self.d, w))
621 }
622 #[inline]
623 fn to2(&self, p: V3) -> V2 {
624 [dot(p, self.u), dot(p, self.v)]
625 }
626 #[inline]
628 fn contains(&self, p: V3) -> bool {
629 let w = dot(p, self.d);
630 if w <= self.d0() || w >= self.d1() {
631 return false;
632 }
633 let q = self.to2(p);
634 for k in 0..self.profiles.len() {
635 if w < self.planes[k + 1] {
636 return pip(q, &self.profiles[k]);
637 }
638 }
639 false
640 }
641 fn corner_mag(&self) -> f64 {
644 let mut mag = 0.0f64;
645 for prof in &self.profiles {
646 for &[uu, vv] in prof {
647 for w in [self.d0(), self.d1()] {
648 for c in self.lift(uu, vv, w) {
649 mag = mag.max(c.abs());
650 }
651 }
652 }
653 }
654 mag
655 }
656 fn obb(&self) -> ([V3; 3], V3, V3) {
659 (
660 [self.u, self.v, self.d],
661 [self.bb.0[0], self.bb.0[1], self.d0()],
662 [self.bb.1[0], self.bb.1[1], self.d1()],
663 )
664 }
665}
666
667#[derive(Clone, Copy)]
669enum Face {
670 Cap { j: usize },
673 Strip { k: usize, i: usize },
676}
677
678fn stack_faces(pf: &PrismFrame) -> Vec<Face> {
680 let mut out = Vec::new();
681 for j in 0..pf.planes.len() {
682 out.push(Face::Cap { j });
683 }
684 for k in 0..pf.profiles.len() {
685 for i in 0..pf.profiles[k].len() {
686 out.push(Face::Strip { k, i });
687 }
688 }
689 out
690}
691
692fn cap_xor(pf: &PrismFrame, j: usize, q: V2) -> bool {
696 let in_prev = j > 0 && pip(q, &pf.profiles[j - 1]);
697 let in_next = j < pf.profiles.len() && pip(q, &pf.profiles[j]);
698 in_prev != in_next
699}
700
701fn obb_overlaps(a: &([V3; 3], V3, V3), b: &([V3; 3], V3, V3), margin: f64) -> bool {
705 let centre = |x: &([V3; 3], V3, V3)| -> V3 {
706 let mid = [
707 (x.1[0] + x.2[0]) * 0.5,
708 (x.1[1] + x.2[1]) * 0.5,
709 (x.1[2] + x.2[2]) * 0.5,
710 ];
711 add(
712 add(scale(x.0[0], mid[0]), scale(x.0[1], mid[1])),
713 scale(x.0[2], mid[2]),
714 )
715 };
716 let half = |x: &([V3; 3], V3, V3), k: usize| (x.2[k] - x.1[k]) * 0.5;
717 let dvec = sub(centre(b), centre(a));
718 let mut axes: Vec<V3> = Vec::with_capacity(15);
719 for k in 0..3 {
720 axes.push(a.0[k]);
721 axes.push(b.0[k]);
722 }
723 for i in 0..3 {
724 for j in 0..3 {
725 let c = cross(a.0[i], b.0[j]);
726 if norm(c) > 1.0e-9 {
727 axes.push(c);
728 }
729 }
730 }
731 for l in axes {
732 let Some(l) = normalize(l) else { continue };
733 let ra: f64 = (0..3).map(|k| half(a, k) * dot(a.0[k], l).abs()).sum();
734 let rb: f64 = (0..3).map(|k| half(b, k) * dot(b.0[k], l).abs()).sum();
735 if dot(dvec, l).abs() > ra + rb + margin {
736 return false;
737 }
738 }
739 true
740}
741
742fn basis_from_depth(d: V3) -> Option<(V3, V3)> {
745 let seed = if d[2].abs() < 0.9 {
746 [0.0, 0.0, 1.0]
747 } else {
748 [0.0, 1.0, 0.0]
749 };
750 let u = normalize(cross(seed, d))?;
751 let v = normalize(cross(d, u))?;
752 Some((u, v))
753}
754
755const MAX_SLABS: usize = 6;
758
759fn simplify_collinear(profile: &mut Vec<V2>, tol: f64) {
765 loop {
766 let n = profile.len();
767 if n <= 3 {
768 return;
769 }
770 let mut removed = false;
771 let mut i = 0;
772 while profile.len() > 3 && i < profile.len() {
773 let m = profile.len();
774 let a = profile[(i + m - 1) % m];
775 let b = profile[i];
776 let c = profile[(i + 1) % m];
777 let ab = [b[0] - a[0], b[1] - a[1]];
778 let ac = [c[0] - a[0], c[1] - a[1]];
779 let lac = (ac[0] * ac[0] + ac[1] * ac[1]).sqrt();
780 if lac > 1.0e-12 {
781 let perp = (ab[0] * ac[1] - ab[1] * ac[0]).abs() / lac;
782 let t = (ab[0] * ac[0] + ab[1] * ac[1]) / (lac * lac);
786 if perp <= tol && (0.0..=1.0).contains(&t) {
787 profile.remove(i);
788 removed = true;
789 continue;
790 }
791 }
792 i += 1;
793 }
794 if !removed {
795 return;
796 }
797 }
798}
799
800fn ccw_area(profile: &mut Vec<V2>) -> Option<f64> {
802 profile.dedup_by(|a, b| (a[0] - b[0]).abs() < 1.0e-9 && (a[1] - b[1]).abs() < 1.0e-9);
803 while profile.len() > 1 {
804 let first = profile[0];
805 let last = *profile.last().unwrap();
806 if (first[0] - last[0]).abs() < 1.0e-9 && (first[1] - last[1]).abs() < 1.0e-9 {
807 profile.pop();
808 } else {
809 break;
810 }
811 }
812 if profile.len() < 3 {
813 return None;
814 }
815 let mut signed2 = 0.0;
816 for i in 0..profile.len() {
817 let a = profile[i];
818 let b = profile[(i + 1) % profile.len()];
819 signed2 += a[0] * b[1] - a[1] * b[0];
820 }
821 if signed2 < 0.0 {
822 profile.reverse();
823 signed2 = -signed2;
824 }
825 let area = signed2 * 0.5;
826 if area < 1.0e-8 {
827 None
828 } else {
829 Some(area)
830 }
831}
832
833fn ring_interior(profile: &[V2]) -> Option<V2> {
836 let pts: Vec<Point2<f64>> = profile.iter().map(|p| Point2::new(p[0], p[1])).collect();
837 let (tp, ti) = crate::cdt::triangulate_constrained(&pts, &[])?;
838 let mut best = (0.0, [0.0, 0.0]);
839 for t in ti.chunks_exact(3) {
840 let (a, b, c) = (tp[t[0]], tp[t[1]], tp[t[2]]);
841 let ar = ((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)).abs();
842 if ar > best.0 {
843 best = (ar, [(a.x + b.x + c.x) / 3.0, (a.y + b.y + c.y) / 3.0]);
844 }
845 }
846 (best.0 > 0.0).then_some(best.1)
847}
848
849fn cross_section_ring(
854 verts: &[V3],
855 tris: &[[usize; 3]],
856 d: V3,
857 u: V3,
858 v: V3,
859 w_mid: f64,
860) -> Option<Vec<V2>> {
861 let mut edge_cross: FxHashMap<(usize, usize), V3> = FxHashMap::default();
862 let mut crossing = |a: usize, b: usize| -> V3 {
863 let key = if a < b { (a, b) } else { (b, a) };
864 *edge_cross.entry(key).or_insert_with(|| {
865 let (pa, pb) = (verts[key.0], verts[key.1]);
866 let (da, db) = (dot(pa, d) - w_mid, dot(pb, d) - w_mid);
867 lerp(pa, pb, (da / (da - db)).clamp(0.0, 1.0))
868 })
869 };
870 let mut segs: Vec<(V3, V3)> = Vec::new();
871 for t in tris {
872 let dist = [
873 dot(verts[t[0]], d) - w_mid,
874 dot(verts[t[1]], d) - w_mid,
875 dot(verts[t[2]], d) - w_mid,
876 ];
877 let mut pts: Vec<V3> = Vec::new();
878 for e in 0..3 {
879 let (da, db) = (dist[e], dist[(e + 1) % 3]);
880 if (da > 0.0) != (db > 0.0) {
881 pts.push(crossing(t[e], t[(e + 1) % 3]));
882 }
883 }
884 if pts.len() == 2 {
885 segs.push((pts[0], pts[1]));
886 }
887 }
888 if segs.len() < 3 {
889 return None;
890 }
891 let mut adj: FxHashMap<[u64; 3], Vec<(usize, [u64; 3])>> = FxHashMap::default();
893 for (i, (a, b)) in segs.iter().enumerate() {
894 adj.entry(bits(*a)).or_default().push((i, bits(*b)));
895 adj.entry(bits(*b)).or_default().push((i, bits(*a)));
896 }
897 if adj.values().any(|v| v.len() != 2) {
898 return None; }
900 let start = *adj.keys().min()?;
901 let mut ring_keys: Vec<[u64; 3]> = vec![start];
902 let mut used = vec![false; segs.len()];
903 let mut cur = start;
904 loop {
905 let nbrs = adj.get(&cur)?;
906 let Some(&(si, nxt)) = nbrs.iter().find(|(si, _)| !used[*si]) else {
907 break;
908 };
909 used[si] = true;
910 if nxt == start {
911 break;
912 }
913 ring_keys.push(nxt);
914 if ring_keys.len() > segs.len() {
915 return None;
916 }
917 cur = nxt;
918 }
919 if used.iter().any(|&x| !x) {
920 return None; }
922 let to_v3 = |k: [u64; 3]| -> V3 {
923 [
924 f64::from_bits(k[0]),
925 f64::from_bits(k[1]),
926 f64::from_bits(k[2]),
927 ]
928 };
929 Some(
930 ring_keys
931 .iter()
932 .map(|&k| {
933 let p = to_v3(k);
934 [dot(p, u), dot(p, v)]
935 })
936 .collect(),
937 )
938}
939
940fn detect_prism(verts: &[V3], tris: &[[usize; 3]], mesh_vol: f64) -> Option<PrismFrame> {
945 if tris.len() < 8 || tris.len() > 4096 {
946 return None;
947 }
948 let mut normals: Vec<Option<V3>> = Vec::with_capacity(tris.len());
950 for t in tris {
951 let (a, b, c) = (verts[t[0]], verts[t[1]], verts[t[2]]);
952 normals.push(normalize(cross(sub(b, a), sub(c, a))));
953 }
954 let mut clusters: Vec<V3> = Vec::new();
955 for n in normals.iter().flatten() {
956 if !clusters.iter().any(|c| dot(*n, *c).abs() > 0.999) {
957 if clusters.len() > 64 {
958 return None; }
960 clusters.push(*n);
961 }
962 }
963 let mut mag = 0.0f64;
964 for v in verts {
965 for c in v {
966 mag = mag.max(c.abs());
967 }
968 }
969 let plane_tol = 1.0e-4_f64.max(mag * 1.0e-6);
970 'cand: for dc in &clusters {
974 let d = *dc;
975 let mut wlo = f64::INFINITY;
976 let mut whi = f64::NEG_INFINITY;
977 for v in verts {
978 let w = dot(*v, d);
979 wlo = wlo.min(w);
980 whi = whi.max(w);
981 }
982 if whi - wlo < 1.0e-4 {
983 continue;
984 }
985 let mut plane_ws: Vec<f64> = vec![wlo, whi];
987 for (t, n) in tris.iter().zip(&normals) {
988 let Some(n) = n else { continue };
989 let a = dot(*n, d).abs();
990 if a > 0.9999 {
991 let w = dot(verts[t[0]], d);
992 if t
994 .iter()
995 .any(|&i| (dot(verts[i], d) - w).abs() > plane_tol)
996 {
997 continue 'cand;
998 }
999 if !plane_ws.iter().any(|p| (p - w).abs() <= plane_tol) {
1000 plane_ws.push(w);
1001 }
1002 } else if a > 0.002 {
1003 continue 'cand;
1010 }
1011 }
1012 plane_ws.sort_by(f64::total_cmp);
1013 if plane_ws.len() > MAX_SLABS + 1 {
1014 continue;
1015 }
1016 let (u, v) = basis_from_depth(d)?;
1017 let n_slabs = plane_ws.len() - 1;
1019 let mut raw_rings: Vec<Vec<V2>> = Vec::with_capacity(n_slabs);
1020 for k in 0..n_slabs {
1021 let (w_a, w_b) = (plane_ws[k], plane_ws[k + 1]);
1022 if w_b - w_a < plane_tol * 2.0 {
1023 continue 'cand; }
1025 let w_mid = (w_a + w_b) * 0.5;
1026 let Some(ring) = cross_section_ring(verts, tris, d, u, v, w_mid) else {
1027 continue 'cand;
1028 };
1029 if ring.len() > MAX_PROFILE_VERTS {
1030 continue 'cand;
1031 }
1032 raw_rings.push(ring);
1033 }
1034 if raw_rings.is_empty() {
1035 continue;
1036 }
1037 let snap_tol = 3.0e-5_f64.max(mag * 6.0e-7);
1046 for axis in 0..2 {
1047 let mut vals: Vec<f64> = raw_rings
1048 .iter()
1049 .flat_map(|r| r.iter().map(|p| p[axis]))
1050 .collect();
1051 vals.sort_by(f64::total_cmp);
1052 let mut canon: Vec<f64> = Vec::new();
1053 for x in vals {
1054 match canon.last() {
1055 Some(&last) if (x - last).abs() <= snap_tol => {}
1056 _ => canon.push(x),
1057 }
1058 }
1059 for ring in raw_rings.iter_mut() {
1060 for pnt in ring.iter_mut() {
1061 let mut best = canon[0];
1064 for &cv in &canon {
1065 if (pnt[axis] - cv).abs() < (pnt[axis] - best).abs() {
1066 best = cv;
1067 }
1068 }
1069 pnt[axis] = best;
1070 }
1071 }
1072 }
1073 let mut profiles: Vec<Vec<V2>> = Vec::with_capacity(n_slabs);
1074 let mut slab_area: Vec<f64> = Vec::with_capacity(n_slabs);
1075 let mut slab_interior: Vec<V2> = Vec::with_capacity(n_slabs);
1076 let mut vol = 0.0;
1077 for (k, mut ring) in raw_rings.into_iter().enumerate() {
1078 let (w_a, w_b) = (plane_ws[k], plane_ws[k + 1]);
1079 simplify_collinear(&mut ring, 1.0e-6 * (1.0 + mag));
1080 let Some(area) = ccw_area(&mut ring) else {
1081 continue 'cand;
1082 };
1083 let nr = ring.len();
1088 for ii in 0..nr {
1089 let pa = ring[ii];
1090 let pb = ring[(ii + 1) % nr];
1091 let dx = pb[0] - pa[0];
1092 let dy = pb[1] - pa[1];
1093 if (dx * dx + dy * dy).sqrt() < 5.0e-4 {
1094 continue 'cand;
1095 }
1096 }
1097 let Some(interior) = ring_interior(&ring) else {
1098 continue 'cand;
1099 };
1100 vol += area * (w_b - w_a);
1101 profiles.push(ring);
1102 slab_area.push(area);
1103 slab_interior.push(interior);
1104 }
1105 if profiles.is_empty() {
1106 continue;
1107 }
1108 if !(0.98..1.02).contains(&(mesh_vol.abs() / vol)) {
1112 continue;
1113 }
1114 let mut lo = [f64::INFINITY; 2];
1115 let mut hi = [f64::NEG_INFINITY; 2];
1116 for prof in &profiles {
1117 for p in prof {
1118 for k in 0..2 {
1119 lo[k] = lo[k].min(p[k]);
1120 hi[k] = hi[k].max(p[k]);
1121 }
1122 }
1123 }
1124 return Some(PrismFrame {
1125 u,
1126 v,
1127 d,
1128 planes: plane_ws,
1129 profiles,
1130 slab_area,
1131 slab_interior,
1132 bb: (lo, hi),
1133 });
1134 }
1135 None
1136}
1137
1138fn prepare_prism(op: &OpeningType, origin: [f64; 3]) -> Option<PrismFrame> {
1142 match op {
1143 OpeningType::Rectangular(mn, mx, dir) => {
1144 let lo = [mn.x - origin[0], mn.y - origin[1], mn.z - origin[2]];
1145 let hi = [mx.x - origin[0], mx.y - origin[1], mx.z - origin[2]];
1146 if (0..3).any(|k| hi[k] - lo[k] < 1.0e-4 || !lo[k].is_finite() || !hi[k].is_finite())
1147 {
1148 return None;
1149 }
1150 let k = match dir {
1153 Some(d) => {
1154 let a = [d.x.abs(), d.y.abs(), d.z.abs()];
1155 let mut k = 0;
1156 for i in 1..3 {
1157 if a[i] > a[k] {
1158 k = i;
1159 }
1160 }
1161 k
1162 }
1163 None => {
1164 let ext = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
1165 let mut k = 0;
1166 for i in 1..3 {
1167 if ext[i] < ext[k] {
1168 k = i;
1169 }
1170 }
1171 k
1172 }
1173 };
1174 let mut d = [0.0; 3];
1175 d[k] = 1.0;
1176 let (u, v) = basis_from_depth(d)?;
1177 let mut plo = [f64::INFINITY; 2];
1179 let mut phi = [f64::NEG_INFINITY; 2];
1180 for ci in 0..8 {
1181 let c = [
1182 if ci & 1 == 0 { lo[0] } else { hi[0] },
1183 if ci & 2 == 0 { lo[1] } else { hi[1] },
1184 if ci & 4 == 0 { lo[2] } else { hi[2] },
1185 ];
1186 let uu = dot(c, u);
1187 let vv = dot(c, v);
1188 plo[0] = plo[0].min(uu);
1189 plo[1] = plo[1].min(vv);
1190 phi[0] = phi[0].max(uu);
1191 phi[1] = phi[1].max(vv);
1192 }
1193 let profile = vec![
1194 [plo[0], plo[1]],
1195 [phi[0], plo[1]],
1196 [phi[0], phi[1]],
1197 [plo[0], phi[1]],
1198 ];
1199 let area = (phi[0] - plo[0]) * (phi[1] - plo[1]);
1200 Some(PrismFrame {
1201 u,
1202 v,
1203 d,
1204 planes: vec![lo[k], hi[k]],
1205 profiles: vec![profile],
1206 slab_area: vec![area],
1207 slab_interior: vec![[(plo[0] + phi[0]) * 0.5, (plo[1] + phi[1]) * 0.5]],
1208 bb: (plo, phi),
1209 })
1210 }
1211 OpeningType::DiagonalRectangular(m, _) | OpeningType::NonRectangular(m, _, _, _) => {
1212 let deseamed;
1219 let m = if cutter_is_closed_manifold(m) {
1220 m
1221 } else {
1222 let dir = match op {
1223 OpeningType::DiagonalRectangular(_, frame) => frame.depth,
1224 OpeningType::NonRectangular(_, _, _, Some(d)) => *d,
1225 _ => opening_mesh_thinnest_axis_dir(m),
1226 };
1227 deseamed = GeometryRouter::remove_internal_membrane(m, dir);
1228 if !cutter_is_closed_manifold(&deseamed) {
1229 return None;
1230 }
1231 &deseamed
1232 };
1233 let o = m.origin;
1235 let vc = m.positions.len() / 3;
1236 let mut raw: Vec<V3> = Vec::with_capacity(vc);
1237 for c in m.positions.chunks_exact(3) {
1238 let p = [
1239 c[0] as f64 + o[0] - origin[0],
1240 c[1] as f64 + o[1] - origin[1],
1241 c[2] as f64 + o[2] - origin[2],
1242 ];
1243 if p.iter().any(|x| !x.is_finite()) {
1244 return None;
1245 }
1246 raw.push(p);
1247 }
1248 let mut vol6 = 0.0;
1250 for t in m.indices.chunks_exact(3) {
1251 if t.iter().any(|&i| i as usize >= vc) {
1252 return None;
1253 }
1254 let (a, b, c) = (
1255 raw[t[0] as usize],
1256 raw[t[1] as usize],
1257 raw[t[2] as usize],
1258 );
1259 vol6 += dot(a, cross(b, c));
1260 }
1261 let mut wverts: Vec<V3> = Vec::new();
1263 let mut wmap: FxHashMap<(i64, i64, i64), usize> = FxHashMap::default();
1264 let q = |x: f64| (x / 1.0e-6).round() as i64;
1265 let mut idx_of = vec![0usize; raw.len()];
1266 for (i, p) in raw.iter().enumerate() {
1267 let key = (q(p[0]), q(p[1]), q(p[2]));
1268 let id = *wmap.entry(key).or_insert_with(|| {
1269 wverts.push(*p);
1270 wverts.len() - 1
1271 });
1272 idx_of[i] = id;
1273 }
1274 let mut wtris: Vec<[usize; 3]> = Vec::with_capacity(m.indices.len() / 3);
1275 for t in m.indices.chunks_exact(3) {
1276 let a = idx_of[t[0] as usize];
1277 let b = idx_of[t[1] as usize];
1278 let c = idx_of[t[2] as usize];
1279 if a != b && b != c && c != a {
1280 wtris.push([a, b, c]);
1281 }
1282 }
1283 detect_prism(&wverts, &wtris, vol6 / 6.0)
1284 }
1285 }
1286}
1287
1288fn rings_equal(a: &[V2], b: &[V2], tol: f64) -> bool {
1290 let n = a.len();
1291 if b.len() != n {
1292 return false;
1293 }
1294 'off: for o in 0..n {
1295 for i in 0..n {
1296 let pa = a[i];
1297 let pb = b[(i + o) % n];
1298 if (pa[0] - pb[0]).abs() > tol || (pa[1] - pb[1]).abs() > tol {
1299 continue 'off;
1300 }
1301 }
1302 return true;
1303 }
1304 false
1305}
1306
1307fn try_merge_prisms(a: &PrismFrame, b: &PrismFrame, tol: f64) -> Option<PrismFrame> {
1316 const BASIS_TOL: f64 = 1.0 - 1.0e-8;
1324 if dot(a.d, b.d) < BASIS_TOL || dot(a.u, b.u) < BASIS_TOL || dot(a.v, b.v) < BASIS_TOL {
1325 return None; }
1327 let weld = 8.0 * crate::kernel::mesh_bridge::SNAP_GRID;
1335 let (lo, hi) = if (a.d1() - b.d0()).abs() <= weld {
1336 (a, b)
1337 } else if (b.d1() - a.d0()).abs() <= weld {
1338 (b, a)
1339 } else {
1340 return None;
1341 };
1342 if lo.profiles.len() + hi.profiles.len() > MAX_SLABS {
1343 return None;
1344 }
1345 let mut m = lo.clone();
1346 m.planes.extend(hi.planes[1..].iter().copied());
1348 m.profiles.extend(hi.profiles.iter().cloned());
1349 m.slab_area.extend(hi.slab_area.iter().copied());
1350 m.slab_interior.extend(hi.slab_interior.iter().copied());
1351 let k = lo.profiles.len(); if k > 0 && k < m.profiles.len() && rings_equal(&m.profiles[k - 1], &m.profiles[k], tol) {
1355 m.planes.remove(k);
1356 m.profiles.remove(k);
1357 m.slab_area.remove(k);
1358 m.slab_interior.remove(k);
1359 }
1360 m.bb = {
1361 let mut lo2 = [f64::INFINITY; 2];
1362 let mut hi2 = [f64::NEG_INFINITY; 2];
1363 for prof in &m.profiles {
1364 for p in prof {
1365 for k in 0..2 {
1366 lo2[k] = lo2[k].min(p[k]);
1367 hi2[k] = hi2[k].max(p[k]);
1368 }
1369 }
1370 }
1371 (lo2, hi2)
1372 };
1373 Some(m)
1374}
1375
1376fn opening_aabb_obb(op: &OpeningType, origin: [f64; 3]) -> Option<([V3; 3], V3, V3)> {
1381 let (mn, mx): (V3, V3) = match op {
1382 OpeningType::Rectangular(mn, mx, _) | OpeningType::NonRectangular(_, mn, mx, _) => {
1383 ([mn.x, mn.y, mn.z], [mx.x, mx.y, mx.z])
1384 }
1385 OpeningType::DiagonalRectangular(m, _) => {
1386 let (a, b) = m.bounds();
1387 let o = m.origin;
1388 (
1389 [a.x as f64 + o[0], a.y as f64 + o[1], a.z as f64 + o[2]],
1390 [b.x as f64 + o[0], b.y as f64 + o[1], b.z as f64 + o[2]],
1391 )
1392 }
1393 };
1394 let lo = [mn[0] - origin[0], mn[1] - origin[1], mn[2] - origin[2]];
1395 let hi = [mx[0] - origin[0], mx[1] - origin[1], mx[2] - origin[2]];
1396 if (0..3).any(|k| !(hi[k] > lo[k]) || !lo[k].is_finite() || !hi[k].is_finite()) {
1397 return None;
1398 }
1399 Some((
1400 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1401 lo,
1402 hi,
1403 ))
1404}
1405
1406const RAY_DIR: V3 = [0.573_257_1, 0.665_412_3, 0.477_889_5];
1411
1412fn point_inside(tris: &[PTri], aabbs: &[(V3, V3)], point: V3) -> bool {
1415 let mut crossings = 0usize;
1416 for (t, (_lo, hi)) in tris.iter().zip(aabbs) {
1417 if hi[0] < point[0] || hi[1] < point[1] || hi[2] < point[2] {
1421 continue;
1422 }
1423 let e1 = sub(t.p[1], t.p[0]);
1424 let e2 = sub(t.p[2], t.p[0]);
1425 let pvec = cross(RAY_DIR, e2);
1426 let det = dot(e1, pvec);
1427 if det.abs() < 1.0e-12 {
1428 continue;
1429 }
1430 let inv = 1.0 / det;
1431 let tvec = sub(point, t.p[0]);
1432 let u = dot(tvec, pvec) * inv;
1433 if !(-1.0e-9..=1.0 + 1.0e-9).contains(&u) {
1434 continue;
1435 }
1436 let qvec = cross(tvec, e1);
1437 let v = dot(RAY_DIR, qvec) * inv;
1438 if v < -1.0e-9 || u + v > 1.0 + 1.0e-9 {
1439 continue;
1440 }
1441 let dist = dot(e2, qvec) * inv;
1442 if dist > 1.0e-9 {
1443 crossings += 1;
1444 }
1445 }
1446 crossings % 2 == 1
1447}
1448
1449fn extend_prism_caps(pf: &mut PrismFrame, tris: &[PTri], aabbs: &[(V3, V3)]) {
1458 let span = pf.d1() - pf.d0();
1459 let delta = (span * 0.1).clamp(0.005, 0.5);
1460 let mag = pf.corner_mag();
1461 let band = (span.max(1.0) * 1.0e-3).max(mag * 4.0e-7);
1469 let pad = 1.0e-6 * (1.0 + mag);
1470 for hi_side in [false, true] {
1471 let (slab_lo, slab_hi) = if hi_side {
1472 (pf.d1() + band, pf.d1() + delta)
1473 } else {
1474 (pf.d0() - delta, pf.d0() - band)
1475 };
1476 if slab_hi <= slab_lo {
1477 continue;
1478 }
1479 let (k, interior) = if hi_side {
1480 (pf.profiles.len() - 1, pf.slab_interior[pf.profiles.len() - 1])
1481 } else {
1482 (0, pf.slab_interior[0])
1483 };
1484 let profile = &pf.profiles[k];
1485 let occupied = tris
1486 .iter()
1487 .any(|t| tri_touches_slab_footprint(t, pf, profile, slab_lo, slab_hi, pad));
1488 if occupied {
1489 continue;
1490 }
1491 let probe = pf.lift(interior[0], interior[1], (slab_lo + slab_hi) * 0.5);
1492 if point_inside(tris, aabbs, probe) {
1493 continue;
1494 }
1495 let last = pf.planes.len() - 1;
1496 if hi_side {
1497 pf.planes[last] += delta;
1498 } else {
1499 pf.planes[0] -= delta;
1500 }
1501 }
1502}
1503
1504fn point_in_or_on(p: V2, poly: &[V2], eps: f64) -> bool {
1517 let n = poly.len();
1518 let eps2 = eps * eps;
1519 for i in 0..n {
1520 let a = poly[i];
1521 let b = poly[(i + 1) % n];
1522 let ab = [b[0] - a[0], b[1] - a[1]];
1523 let ap = [p[0] - a[0], p[1] - a[1]];
1524 let len2 = ab[0] * ab[0] + ab[1] * ab[1];
1525 if len2 < 1.0e-20 {
1526 if ap[0] * ap[0] + ap[1] * ap[1] < eps2 {
1527 return true;
1528 }
1529 continue;
1530 }
1531 let t = ((ap[0] * ab[0] + ap[1] * ab[1]) / len2).clamp(0.0, 1.0);
1532 let proj = [a[0] + ab[0] * t, a[1] + ab[1] * t];
1533 let dx = p[0] - proj[0];
1534 let dy = p[1] - proj[1];
1535 if dx * dx + dy * dy < eps2 {
1536 return true;
1537 }
1538 }
1539 pip(p, poly)
1540}
1541
1542fn ring_contains_ring(outer: &[V2], inner: &[V2], eps: f64) -> bool {
1544 inner.iter().all(|&p| point_in_or_on(p, outer, eps))
1545}
1546
1547fn ring_is_simple(ring: &[V2]) -> bool {
1551 let n = ring.len();
1552 if n < 3 {
1553 return false;
1554 }
1555 for i in 0..n {
1556 let (a, b) = (ring[i], ring[(i + 1) % n]);
1557 for j in (i + 1)..n {
1558 if j == (i + 1) % n || (j + 1) % n == i {
1560 continue;
1561 }
1562 let (c, d) = (ring[j], ring[(j + 1) % n]);
1563 if seg_cross_param(a, b, c, d).is_some() {
1564 return false;
1565 }
1566 }
1567 }
1568 true
1569}
1570
1571fn extend_profile_edges(pf: &mut PrismFrame, tris: &[PTri], aabbs: &[(V3, V3)]) {
1572 let mag = pf.corner_mag();
1573 let band = 2.0e-4_f64.max(mag * 4.0e-7);
1574 for k in 0..pf.profiles.len() {
1575 let (w_a, w_b) = (pf.planes[k], pf.planes[k + 1]);
1576 let authored = pf.profiles[k].clone();
1579 let mut i = 0usize;
1580 while i < pf.profiles[k].len() {
1581 let prof = pf.profiles[k].clone();
1582 let n = prof.len();
1583 let a = prof[i];
1584 let b = prof[(i + 1) % n];
1585 let e = [b[0] - a[0], b[1] - a[1]];
1586 let len = (e[0] * e[0] + e[1] * e[1]).sqrt();
1587 if len < 1.0e-6 {
1588 i += 1;
1589 continue;
1590 }
1591 let ed = [e[0] / len, e[1] / len];
1592 let n2 = [ed[1], -ed[0]]; let delta = (len * 0.1).clamp(0.01, 0.5);
1594 let ap = prof[(i + n - 1) % n];
1597 let bn = prof[(i + 2) % n];
1598 let d_prev = [a[0] - ap[0], a[1] - ap[1]];
1599 let d_next = [bn[0] - b[0], bn[1] - b[1]];
1600 let cr_prev = ed[0] * d_prev[1] - ed[1] * d_prev[0];
1601 let cr_next = ed[0] * d_next[1] - ed[1] * d_next[0];
1602 let lp = (d_prev[0] * d_prev[0] + d_prev[1] * d_prev[1]).sqrt();
1603 let ln = (d_next[0] * d_next[0] + d_next[1] * d_next[1]).sqrt();
1604 if lp < 1.0e-9 || ln < 1.0e-9 || cr_prev.abs() / lp < 0.1 || cr_next.abs() / ln < 0.1
1605 {
1606 i += 1;
1607 continue;
1608 }
1609 let quad = [
1612 [a[0] + n2[0] * band, a[1] + n2[1] * band],
1613 [b[0] + n2[0] * band, b[1] + n2[1] * band],
1614 [b[0] + n2[0] * delta, b[1] + n2[1] * delta],
1615 [a[0] + n2[0] * delta, a[1] + n2[1] * delta],
1616 ];
1617 let pad = 1.0e-6 * (1.0 + mag);
1618 let occupied = tris
1619 .iter()
1620 .any(|t| tri_touches_slab_footprint(t, pf, &quad, w_a, w_b, pad));
1621 if occupied {
1622 i += 1;
1623 continue;
1624 }
1625 let mid_off = (band + delta) * 0.5;
1626 let probe2 = [
1627 (a[0] + b[0]) * 0.5 + n2[0] * mid_off,
1628 (a[1] + b[1]) * 0.5 + n2[1] * mid_off,
1629 ];
1630 let probe = pf.lift(probe2[0], probe2[1], (w_a + w_b) * 0.5);
1631 if point_inside(tris, aabbs, probe) {
1632 i += 1;
1633 continue;
1634 }
1635 let line_pt = [a[0] + n2[0] * delta, a[1] + n2[1] * delta];
1638 let isect = |p0: V2, dir0: V2| -> Option<V2> {
1639 let den = ed[0] * dir0[1] - ed[1] * dir0[0];
1641 if den.abs() < 1.0e-12 {
1642 return None;
1643 }
1644 let dx = [p0[0] - line_pt[0], p0[1] - line_pt[1]];
1645 let t = (dx[0] * dir0[1] - dx[1] * dir0[0]) / den;
1646 Some([line_pt[0] + ed[0] * t, line_pt[1] + ed[1] * t])
1647 };
1648 let (Some(na), Some(nb)) = (
1649 isect(ap, [d_prev[0] / lp, d_prev[1] / lp]),
1650 isect(b, [d_next[0] / ln, d_next[1] / ln]),
1651 ) else {
1652 i += 1;
1653 continue;
1654 };
1655 let mut cand = pf.profiles[k].clone();
1662 cand[i] = na;
1663 cand[(i + 1) % n] = nb;
1664 let contain_eps = 1.0e-6 * (1.0 + mag);
1670 if ring_is_simple(&cand) && ring_contains_ring(&cand, &authored, contain_eps) {
1671 pf.profiles[k] = cand;
1672 }
1673 i += 1;
1674 }
1675 let mut ring = pf.profiles[k].clone();
1678 if let Some(area) = ccw_area(&mut ring) {
1679 pf.profiles[k] = ring;
1680 pf.slab_area[k] = area;
1681 }
1682 }
1683 let mut lo = [f64::INFINITY; 2];
1685 let mut hi = [f64::NEG_INFINITY; 2];
1686 for prof in &pf.profiles {
1687 for p in prof {
1688 for k in 0..2 {
1689 lo[k] = lo[k].min(p[k]);
1690 hi[k] = hi[k].max(p[k]);
1691 }
1692 }
1693 }
1694 pf.bb = (lo, hi);
1695}
1696
1697fn tri_touches_slab_footprint(
1702 t: &PTri,
1703 pf: &PrismFrame,
1704 profile: &[V2],
1705 slab_lo: f64,
1706 slab_hi: f64,
1707 pad: f64,
1708) -> bool {
1709 let mut poly: Vec<V3> = t.p.to_vec();
1711 for hi_side in [false, true] {
1712 if poly.len() < 3 {
1713 return false;
1714 }
1715 let mut next: Vec<V3> = Vec::with_capacity(poly.len() + 1);
1716 let dist = |p: V3| -> f64 {
1717 let w = dot(p, pf.d);
1718 if hi_side {
1719 w - slab_hi
1720 } else {
1721 slab_lo - w
1722 }
1723 };
1724 for i in 0..poly.len() {
1725 let j = (i + 1) % poly.len();
1726 let (da, db) = (dist(poly[i]), dist(poly[j]));
1727 if da <= 0.0 {
1728 next.push(poly[i]);
1729 }
1730 if (da <= 0.0) != (db <= 0.0) {
1731 next.push(lerp(poly[i], poly[j], da / (da - db)));
1732 }
1733 }
1734 poly = next;
1735 }
1736 if poly.len() < 3 {
1737 return false;
1738 }
1739 let poly2: Vec<V2> = poly.iter().map(|p| pf.to2(*p)).collect();
1742 let mut lo = [f64::INFINITY; 2];
1748 let mut hi = [f64::NEG_INFINITY; 2];
1749 for p in &poly2 {
1750 for k in 0..2 {
1751 lo[k] = lo[k].min(p[k]);
1752 hi[k] = hi[k].max(p[k]);
1753 }
1754 }
1755 let mut footprint_lo = [f64::INFINITY; 2];
1756 let mut footprint_hi = [f64::NEG_INFINITY; 2];
1757 for p in profile {
1758 for k in 0..2 {
1759 footprint_lo[k] = footprint_lo[k].min(p[k]);
1760 footprint_hi[k] = footprint_hi[k].max(p[k]);
1761 }
1762 }
1763 if hi[0] < footprint_lo[0] - pad
1764 || lo[0] > footprint_hi[0] + pad
1765 || hi[1] < footprint_lo[1] - pad
1766 || lo[1] > footprint_hi[1] + pad
1767 {
1768 return false;
1769 }
1770 if poly2.iter().any(|p| pip(*p, profile)) {
1771 return true;
1772 }
1773 if profile.iter().any(|p| pip(*p, &poly2)) {
1774 return true;
1775 }
1776 let np = poly2.len();
1777 let nq = profile.len();
1778 for i in 0..np {
1779 let (a, b) = (poly2[i], poly2[(i + 1) % np]);
1780 for j in 0..nq {
1781 let (c, d2) = (profile[j], profile[(j + 1) % nq]);
1782 if seg_cross_param(a, b, c, d2).is_some() {
1783 return true;
1784 }
1785 }
1786 }
1787 false
1788}
1789
1790type CrossCache = FxHashMap<([u64; 3], [u64; 3], u16), V3>;
1796
1797struct Seam {
1800 a3: V3,
1801 b3: V3,
1802 a2: V2,
1803 b2: V2,
1804}
1805
1806fn face_plane(pf: &PrismFrame, face: Face) -> Option<(V3, f64)> {
1808 match face {
1809 Face::Cap { j } => Some((pf.d, pf.planes[j])),
1810 Face::Strip { k, i } => {
1811 let prof = &pf.profiles[k];
1812 let n = prof.len();
1813 let a = prof[i];
1814 let b = prof[(i + 1) % n];
1815 let e = [b[0] - a[0], b[1] - a[1]];
1816 let len = (e[0] * e[0] + e[1] * e[1]).sqrt();
1817 if len < 1.0e-9 {
1818 return None;
1819 }
1820 let n2 = [e[1] / len, -e[0] / len];
1822 let n3 = add(scale(pf.u, n2[0]), scale(pf.v, n2[1]));
1823 let c = n2[0] * a[0] + n2[1] * a[1];
1824 Some((n3, c))
1825 }
1826 }
1827}
1828
1829fn cached_crossing(cache: &mut CrossCache, f: u16, m: V3, c: f64, a: V3, b: V3) -> V3 {
1834 let (ka, kb) = (bits(a), bits(b));
1835 let (first, second, key) = if ka <= kb {
1836 (a, b, (ka, kb, f))
1837 } else {
1838 (b, a, (kb, ka, f))
1839 };
1840 if let Some(p) = cache.get(&key) {
1841 return *p;
1842 }
1843 let da = dot(first, m) - c;
1844 let db = dot(second, m) - c;
1845 let denom = da - db;
1846 let t = if denom.abs() < 1.0e-300 {
1847 0.5
1848 } else {
1849 (da / denom).clamp(0.0, 1.0)
1850 };
1851 let p = if t <= 0.0 {
1852 first
1853 } else if t >= 1.0 {
1854 second
1855 } else {
1856 lerp(first, second, t)
1857 };
1858 cache.insert(key, p);
1859 p
1860}
1861
1862fn face_coords(pf: &PrismFrame, face: Face, p: V3) -> V2 {
1864 match face {
1865 Face::Cap { .. } => pf.to2(p),
1866 Face::Strip { k, i } => {
1867 let prof = &pf.profiles[k];
1868 let n = prof.len();
1869 let a = prof[i];
1870 let b = prof[(i + 1) % n];
1871 let e = [b[0] - a[0], b[1] - a[1]];
1872 let len = (e[0] * e[0] + e[1] * e[1]).sqrt();
1873 let ed = [e[0] / len, e[1] / len];
1874 let q = pf.to2(p);
1875 [
1876 (q[0] - a[0]) * ed[0] + (q[1] - a[1]) * ed[1],
1877 dot(p, pf.d),
1878 ]
1879 }
1880 }
1881}
1882
1883fn cut_prism(tris: &[PTri], pf: &PrismFrame) -> Result<Vec<PTri>, usize> {
1887 let faces = stack_faces(pf);
1888 if faces.len() > u16::MAX as usize {
1889 return Err(7);
1890 }
1891 let mut planes: Vec<(V3, f64)> = Vec::with_capacity(faces.len());
1892 for &face in &faces {
1893 planes.push(face_plane(pf, face).ok_or(7usize)?);
1894 }
1895
1896 let aabbs: Vec<(V3, V3)> = tris.iter().map(PTri::aabb).collect();
1897 let mut cache: CrossCache = CrossCache::default();
1898
1899 let mut out: Vec<PTri> = Vec::with_capacity(tris.len() + 16);
1900 let mut inside: Vec<PTri> = Vec::new();
1901 let mut seams: Vec<Vec<Seam>> = (0..faces.len()).map(|_| Vec::new()).collect();
1902 let mut any_cut = false;
1903
1904 for t in tris {
1905 let w0 = dot(t.p[0], pf.d);
1907 let w1 = dot(t.p[1], pf.d);
1908 let w2 = dot(t.p[2], pf.d);
1909 if w0.max(w1).max(w2) < pf.d0() || w0.min(w1).min(w2) > pf.d1() {
1910 out.push(t.clone());
1911 continue;
1912 }
1913 {
1914 let q0 = pf.to2(t.p[0]);
1915 let q1 = pf.to2(t.p[1]);
1916 let q2 = pf.to2(t.p[2]);
1917 let lo = [q0[0].min(q1[0]).min(q2[0]), q0[1].min(q1[1]).min(q2[1])];
1918 let hi = [q0[0].max(q1[0]).max(q2[0]), q0[1].max(q1[1]).max(q2[1])];
1919 if hi[0] < pf.bb.0[0]
1920 || lo[0] > pf.bb.1[0]
1921 || hi[1] < pf.bb.0[1]
1922 || lo[1] > pf.bb.1[1]
1923 {
1924 out.push(t.clone());
1925 continue;
1926 }
1927 }
1928
1929 let mut segs: Vec<(usize, V3, V3)> = Vec::new(); let mut edge_pts: [Vec<V3>; 3] = Default::default();
1932 for (fi, (&face, &(m, c))) in faces.iter().zip(&planes).enumerate() {
1933 let dist = [
1936 dot(t.p[0], m) - c,
1937 dot(t.p[1], m) - c,
1938 dot(t.p[2], m) - c,
1939 ];
1940 let mut chord: Vec<(V3, usize)> = Vec::new(); for e in 0..3 {
1942 let (da, db) = (dist[e], dist[(e + 1) % 3]);
1943 if (da > 0.0) != (db > 0.0) {
1944 let p = cached_crossing(
1945 &mut cache,
1946 fi as u16,
1947 m,
1948 c,
1949 t.p[e],
1950 t.p[(e + 1) % 3],
1951 );
1952 chord.push((p, e));
1953 }
1954 }
1955 if chord.len() != 2 {
1956 continue;
1957 }
1958 let (q0, e0) = chord[0];
1959 let (q1, e1) = chord[1];
1960 match face {
1963 Face::Cap { j } => {
1964 let a2 = pf.to2(q0);
1965 let b2 = pf.to2(q1);
1966 if cap_xor(pf, j, a2) {
1969 edge_pts[e0].push(q0);
1970 }
1971 if cap_xor(pf, j, b2) {
1972 edge_pts[e1].push(q1);
1973 }
1974 let mut ts: Vec<f64> = vec![0.0, 1.0];
1977 for prof in [
1978 (j > 0).then(|| &pf.profiles[j - 1]),
1979 (j < pf.profiles.len()).then(|| &pf.profiles[j]),
1980 ]
1981 .into_iter()
1982 .flatten()
1983 {
1984 let npr = prof.len();
1985 for i in 0..npr {
1986 let (pa, pb) = (prof[i], prof[(i + 1) % npr]);
1987 if let Some(tt) = seg_cross_param(a2, b2, pa, pb) {
1988 ts.push(tt);
1989 }
1990 }
1991 }
1992 ts.sort_by(f64::total_cmp);
1993 ts.dedup_by(|x, y| (*x - *y).abs() < 1.0e-12);
1994 for wnd in ts.windows(2) {
1995 let (ta, tb) = (wnd[0], wnd[1]);
1996 if tb - ta < 1.0e-12 {
1997 continue;
1998 }
1999 let mid = [
2000 a2[0] + (b2[0] - a2[0]) * (ta + tb) * 0.5,
2001 a2[1] + (b2[1] - a2[1]) * (ta + tb) * 0.5,
2002 ];
2003 if cap_xor(pf, j, mid) {
2004 let sa = if ta == 0.0 { q0 } else { lerp(q0, q1, ta) };
2005 let sb = if tb == 1.0 { q1 } else { lerp(q0, q1, tb) };
2006 segs.push((fi, sa, sb));
2007 }
2008 }
2009 }
2010 Face::Strip { k, i } => {
2011 let prof = &pf.profiles[k];
2012 let npr = prof.len();
2013 let a = prof[i];
2014 let b = prof[(i + 1) % npr];
2015 let e2 = [b[0] - a[0], b[1] - a[1]];
2016 let len = (e2[0] * e2[0] + e2[1] * e2[1]).sqrt();
2017 let ed = [e2[0] / len, e2[1] / len];
2018 let (w_a, w_b) = (pf.planes[k], pf.planes[k + 1]);
2019 let sparam = |p: V3| -> f64 {
2020 let q = pf.to2(p);
2021 (q[0] - a[0]) * ed[0] + (q[1] - a[1]) * ed[1]
2022 };
2023 let wparam = |p: V3| dot(p, pf.d);
2024 let (s0, s1) = (sparam(q0), sparam(q1));
2025 let (wc0, wc1) = (wparam(q0), wparam(q1));
2026 let in_strip =
2028 |s: f64, w: f64| (0.0..=len).contains(&s) && (w_a..=w_b).contains(&w);
2029 if in_strip(s0, wc0) {
2030 edge_pts[e0].push(q0);
2031 }
2032 if in_strip(s1, wc1) {
2033 edge_pts[e1].push(q1);
2034 }
2035 let mut t_lo = 0.0f64;
2037 let mut t_hi = 1.0f64;
2038 for (p0, p1, lo, hi) in [(s0, s1, 0.0, len), (wc0, wc1, w_a, w_b)] {
2039 let dp = p1 - p0;
2040 if dp.abs() < 1.0e-15 {
2041 if p0 < lo || p0 > hi {
2042 t_lo = 1.0;
2043 t_hi = 0.0;
2044 }
2045 } else {
2046 let (mut ta, mut tb) = ((lo - p0) / dp, (hi - p0) / dp);
2047 if ta > tb {
2048 std::mem::swap(&mut ta, &mut tb);
2049 }
2050 t_lo = t_lo.max(ta);
2051 t_hi = t_hi.min(tb);
2052 }
2053 }
2054 if t_hi - t_lo > 1.0e-12 {
2055 let sa = if t_lo == 0.0 { q0 } else { lerp(q0, q1, t_lo) };
2056 let sb = if t_hi == 1.0 { q1 } else { lerp(q0, q1, t_hi) };
2057 segs.push((fi, sa, sb));
2058 }
2059 }
2060 }
2061 }
2062
2063 if segs.is_empty() && edge_pts.iter().all(|v| v.is_empty()) {
2064 let centroid = scale(add(add(t.p[0], t.p[1]), t.p[2]), 1.0 / 3.0);
2067 if pf.contains(centroid) {
2068 inside.push(t.clone());
2069 any_cut = true;
2070 } else {
2071 out.push(t.clone());
2072 }
2073 continue;
2074 }
2075
2076 if !decompose_tri(t, pf, &faces, &segs, &edge_pts, &mut out, &mut inside, &mut seams) {
2078 return Err(7);
2079 }
2080 any_cut = true;
2081 }
2082
2083 if !any_cut {
2084 return Err(4); }
2086
2087 let plane_pts: Vec<Vec<V2>> = (0..pf.planes.len())
2093 .map(|j| cap_plane_breakpoints(pf, j))
2094 .collect();
2095
2096 let mut caps: Vec<PTri> = Vec::new();
2099 for (fi, &face) in faces.iter().enumerate() {
2100 if !build_face_cap(pf, face, &seams[fi], &plane_pts, tris, &aabbs, &mut caps) {
2101 return Err(7);
2102 }
2103 }
2104
2105 if !partition_consistent(tris, &out, &inside, &aabbs) {
2106 return Err(8);
2107 }
2108 if !has_measurable_region(&inside, &caps) {
2109 return Err(4);
2110 }
2111
2112 out.append(&mut caps);
2117 Ok(out)
2118}
2119
2120struct PointPool {
2124 pts2: Vec<Point2<f64>>,
2125 pts3: Vec<V3>,
2126 nrm: Vec<V3>,
2127 buckets: FxHashMap<(i64, i64), Vec<usize>>,
2128 eps: f64,
2129}
2130
2131impl PointPool {
2132 fn new(eps: f64) -> Self {
2133 Self {
2134 pts2: Vec::new(),
2135 pts3: Vec::new(),
2136 nrm: Vec::new(),
2137 buckets: FxHashMap::default(),
2138 eps: eps.max(1.0e-15),
2139 }
2140 }
2141 fn intern(&mut self, q: V2, p3: V3, n: V3) -> usize {
2142 let cell = |x: f64| (x / (self.eps * 4.0)).floor() as i64;
2143 let (cx, cy) = (cell(q[0]), cell(q[1]));
2144 for dx in -1..=1 {
2145 for dy in -1..=1 {
2146 if let Some(ids) = self.buckets.get(&(cx + dx, cy + dy)) {
2147 for &i in ids {
2148 let d0 = self.pts2[i].x - q[0];
2149 let d1 = self.pts2[i].y - q[1];
2150 if d0.abs() <= self.eps && d1.abs() <= self.eps {
2151 return i;
2152 }
2153 }
2154 }
2155 }
2156 }
2157 let idx = self.pts2.len();
2158 self.pts2.push(Point2::new(q[0], q[1]));
2159 self.pts3.push(p3);
2160 self.nrm.push(n);
2161 self.buckets.entry((cx, cy)).or_default().push(idx);
2162 idx
2163 }
2164}
2165
2166#[allow(clippy::too_many_arguments)]
2171fn decompose_tri(
2172 t: &PTri,
2173 pf: &PrismFrame,
2174 faces: &[Face],
2175 segs: &[(usize, V3, V3)],
2176 edge_pts: &[Vec<V3>; 3],
2177 out: &mut Vec<PTri>,
2178 inside: &mut Vec<PTri>,
2179 seams: &mut [Vec<Seam>],
2180) -> bool {
2181 let Some(u) = normalize(sub(t.p[1], t.p[0])) else {
2183 out.push(t.clone());
2184 return true;
2185 };
2186 let Some(w) = normalize(cross(sub(t.p[1], t.p[0]), sub(t.p[2], t.p[0]))) else {
2187 out.push(t.clone());
2188 return true;
2189 };
2190 let v = cross(w, u);
2191 let to2 = |x: V3| -> V2 {
2192 let d = sub(x, t.p[0]);
2193 [dot(d, u), dot(d, v)]
2194 };
2195
2196 let mut mag = 0.0f64;
2197 for p in &t.p {
2198 for c in p {
2199 mag = mag.max(c.abs());
2200 }
2201 }
2202 let mut pool = PointPool::new(1.0e-9 * (1.0 + mag));
2203
2204 let tri_idx = [
2205 pool.intern(to2(t.p[0]), t.p[0], t.n[0]),
2206 pool.intern(to2(t.p[1]), t.p[1], t.n[1]),
2207 pool.intern(to2(t.p[2]), t.p[2], t.n[2]),
2208 ];
2209
2210 let mut segments: Vec<(usize, usize)> = Vec::new();
2211 for e in 0..3 {
2213 let a3 = t.p[e];
2214 let b3 = t.p[(e + 1) % 3];
2215 let dir = sub(b3, a3);
2216 let len2 = dot(dir, dir);
2217 if len2 <= 0.0 {
2218 continue;
2219 }
2220 let mut on_edge: Vec<(f64, usize)> = Vec::new();
2221 for p in &edge_pts[e] {
2222 let tt = (dot(sub(*p, a3), dir) / len2).clamp(0.0, 1.0);
2224 let nn = lerp(t.n[e], t.n[(e + 1) % 3], tt);
2225 let idx = pool.intern(to2(*p), *p, nn);
2226 on_edge.push((tt, idx));
2227 }
2228 on_edge.sort_by(|x, y| x.0.total_cmp(&y.0));
2229 let mut prev = tri_idx[e];
2230 for &(_, idx) in &on_edge {
2231 if idx != prev {
2232 segments.push((prev, idx));
2233 prev = idx;
2234 }
2235 }
2236 let last = tri_idx[(e + 1) % 3];
2237 if last != prev {
2238 segments.push((prev, last));
2239 }
2240 }
2241 let face_n = normalize(cross(sub(t.p[1], t.p[0]), sub(t.p[2], t.p[0]))).unwrap_or(w);
2244 for &(fi, a3, b3) in segs {
2245 let ia = pool.intern(to2(a3), a3, face_n);
2246 let ib = pool.intern(to2(b3), b3, face_n);
2247 if ia == ib {
2248 continue;
2249 }
2250 segments.push((ia, ib));
2251 seams[fi].push(Seam {
2252 a3,
2253 b3,
2254 a2: face_coords(pf, faces[fi], a3),
2255 b2: face_coords(pf, faces[fi], b3),
2256 });
2257 }
2258 segments.sort();
2259 segments.dedup();
2260 let t0 = t.p[0];
2261 let existing_points = pool.nrm.len();
2267 resolve_crossings(
2268 &mut pool,
2269 |q| add(t0, add(scale(u, q[0]), scale(v, q[1]))),
2270 &mut segments,
2271 );
2272 for n in pool.nrm.iter_mut().skip(existing_points) {
2273 *n = face_n;
2274 }
2275
2276 let Some((pts2, idx2)) = triangulate_pslg(&pool.pts2, &segments) else {
2277 return false;
2278 };
2279 if pts2.len() != pool.pts2.len() {
2280 return false; }
2282
2283 for tri in idx2.chunks_exact(3) {
2284 let (a, b, c) = (tri[0], tri[1], tri[2]);
2285 let p = [pool.pts3[a], pool.pts3[b], pool.pts3[c]];
2286 let centroid = scale(add(add(p[0], p[1]), p[2]), 1.0 / 3.0);
2287 let piece = PTri {
2288 p,
2289 n: [pool.nrm[a], pool.nrm[b], pool.nrm[c]],
2290 };
2291 if pf.contains(centroid) {
2292 inside.push(piece);
2293 } else {
2294 out.push(piece);
2295 }
2296 }
2297 true
2298}
2299
2300fn cap_plane_breakpoints(pf: &PrismFrame, j: usize) -> Vec<V2> {
2303 let mut profs: Vec<&[V2]> = Vec::new();
2304 if j > 0 {
2305 profs.push(&pf.profiles[j - 1]);
2306 }
2307 if j < pf.profiles.len() {
2308 profs.push(&pf.profiles[j]);
2309 }
2310 let mut out: Vec<V2> = Vec::new();
2311 for prof in &profs {
2312 out.extend_from_slice(prof);
2313 }
2314 if profs.len() == 2 {
2315 let (a, b) = (profs[0], profs[1]);
2316 let (na, nb) = (a.len(), b.len());
2317 for i in 0..na {
2318 let (p, q) = (a[i], a[(i + 1) % na]);
2319 for k in 0..nb {
2320 let (c, d) = (b[k], b[(k + 1) % nb]);
2321 if let Some(t) = seg_cross_param(p, q, c, d) {
2322 if (1.0e-9..=1.0 - 1.0e-9).contains(&t) {
2323 out.push([p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t]);
2324 }
2325 }
2326 }
2327 }
2328 }
2329 out
2330}
2331
2332fn build_face_cap(
2339 pf: &PrismFrame,
2340 face: Face,
2341 seams: &[Seam],
2342 plane_pts: &[Vec<V2>],
2343 host: &[PTri],
2344 aabbs: &[(V3, V3)],
2345 caps: &mut Vec<PTri>,
2346) -> bool {
2347 match face {
2348 Face::Strip { k, i } => {
2349 let prof = &pf.profiles[k];
2350 let npr = prof.len();
2351 let a = prof[i];
2352 let b = prof[(i + 1) % npr];
2353 let e2 = [b[0] - a[0], b[1] - a[1]];
2354 let len = (e2[0] * e2[0] + e2[1] * e2[1]).sqrt();
2355 if len < 1.0e-9 {
2356 return true;
2357 }
2358 let ed = [e2[0] / len, e2[1] / len];
2359 let (w_a, w_b) = (pf.planes[k], pf.planes[k + 1]);
2360 let lift_s =
2361 |s: f64, wd: f64| -> V3 { pf.lift(a[0] + ed[0] * s, a[1] + ed[1] * s, wd) };
2362 let ring2: Vec<V2> = vec![[0.0, w_a], [len, w_a], [len, w_b], [0.0, w_b]];
2363 let ring3: Vec<V3> = vec![
2364 lift_s(0.0, w_a),
2365 lift_s(len, w_a),
2366 lift_s(len, w_b),
2367 lift_s(0.0, w_b),
2368 ];
2369 let n2 = [e2[1] / len, -e2[0] / len];
2373 let outward = add(scale(pf.u, n2[0]), scale(pf.v, n2[1]));
2374 let desired = scale(outward, -1.0);
2375 let mut inject: Vec<(V2, V3)> = Vec::new();
2379 let on_line_tol = 1.0e-6 * (1.0 + pf.corner_mag());
2380 for (jj, wj) in [(k, w_a), (k + 1, w_b)] {
2381 for q in &plane_pts[jj] {
2382 let ap = [q[0] - a[0], q[1] - a[1]];
2383 let ss = ap[0] * ed[0] + ap[1] * ed[1];
2384 if !(0.0..=len).contains(&ss) {
2385 continue;
2386 }
2387 let perp = (ap[0] * ed[1] - ap[1] * ed[0]).abs();
2388 if perp <= on_line_tol {
2389 inject.push(([ss, wj], lift_s(ss, wj)));
2390 }
2391 }
2392 }
2393 if seams.is_empty() && inject.is_empty() {
2394 let mid = |kk: usize| (ring3[0][kk] + ring3[2][kk]) * 0.5;
2397 if !point_inside(host, aabbs, [mid(0), mid(1), mid(2)]) {
2398 return true;
2399 }
2400 emit_cap(caps, ring3[0], ring3[1], ring3[2], desired, true);
2401 emit_cap(caps, ring3[0], ring3[2], ring3[3], desired, true);
2402 return true;
2403 }
2404 if seams.is_empty() {
2405 let mid = |kk: usize| (ring3[0][kk] + ring3[2][kk]) * 0.5;
2409 if !point_inside(host, aabbs, [mid(0), mid(1), mid(2)]) {
2410 return true;
2411 }
2412 }
2413 cap_cdt(
2414 pf,
2415 &ring2,
2416 &ring3,
2417 &[],
2418 seams,
2419 &inject,
2420 desired,
2421 true,
2422 |q| pip(q, &ring2),
2423 host,
2424 aabbs,
2425 caps,
2426 )
2427 }
2428 Face::Cap { j } => {
2429 let wdepth = pf.planes[j];
2430 let mut lo = [f64::INFINITY; 2];
2433 let mut hi = [f64::NEG_INFINITY; 2];
2434 let mut aux: Vec<&[V2]> = Vec::new();
2435 if j > 0 {
2436 aux.push(&pf.profiles[j - 1]);
2437 }
2438 if j < pf.profiles.len() {
2439 aux.push(&pf.profiles[j]);
2440 }
2441 for prof in &aux {
2442 for p in prof.iter() {
2443 for k in 0..2 {
2444 lo[k] = lo[k].min(p[k]);
2445 hi[k] = hi[k].max(p[k]);
2446 }
2447 }
2448 }
2449 let m = 1.0e-3 * (1.0 + (hi[0] - lo[0]).abs().max((hi[1] - lo[1]).abs()));
2450 let (lo, hi) = ([lo[0] - m, lo[1] - m], [hi[0] + m, hi[1] + m]);
2451 let ring2: Vec<V2> = vec![
2452 [lo[0], lo[1]],
2453 [hi[0], lo[1]],
2454 [hi[0], hi[1]],
2455 [lo[0], hi[1]],
2456 ];
2457 let ring3: Vec<V3> = ring2
2458 .iter()
2459 .map(|&[uu, vv]| pf.lift(uu, vv, wdepth))
2460 .collect();
2461 let in_lower = |q: V2| j > 0 && pip(q, &pf.profiles[j - 1]);
2467 let in_upper = |q: V2| j < pf.profiles.len() && pip(q, &pf.profiles[j]);
2468 if seams.is_empty() && (j == 0 || j == pf.profiles.len()) {
2471 let k = if j == 0 { 0 } else { pf.profiles.len() - 1 };
2472 let interior = pf.slab_interior[k];
2473 let probe = pf.lift(interior[0], interior[1], wdepth);
2474 if !point_inside(host, aabbs, probe) {
2475 return true;
2476 }
2477 }
2478 let inject: Vec<(V2, V3)> = plane_pts[j]
2479 .iter()
2480 .map(|&q| (q, pf.lift(q[0], q[1], wdepth)))
2481 .collect();
2482 cap_cdt_xor(
2483 pf,
2484 &ring2,
2485 &ring3,
2486 &aux,
2487 seams,
2488 &inject,
2489 wdepth,
2490 in_lower,
2491 in_upper,
2492 host,
2493 aabbs,
2494 caps,
2495 )
2496 }
2497 }
2498}
2499
2500fn emit_cap(caps: &mut Vec<PTri>, a: V3, b: V3, c: V3, desired: V3, flip: bool) {
2503 let (b, c) = if flip { (c, b) } else { (b, c) };
2504 caps.push(PTri {
2505 p: [a, b, c],
2506 n: [desired, desired, desired],
2507 });
2508}
2509
2510#[allow(clippy::too_many_arguments)]
2513fn cap_cdt(
2514 pf: &PrismFrame,
2515 ring2: &[V2],
2516 ring3: &[V3],
2517 aux: &[&[V2]],
2518 seams: &[Seam],
2519 inject: &[(V2, V3)],
2520 desired: V3,
2521 flip: bool,
2522 region: impl Fn(V2) -> bool,
2523 host: &[PTri],
2524 aabbs: &[(V3, V3)],
2525 caps: &mut Vec<PTri>,
2526) -> bool {
2527 cap_cdt_impl(pf, ring2, ring3, aux, seams, inject, host, aabbs, caps, |q| {
2528 region(q).then_some((desired, flip))
2529 })
2530}
2531
2532#[allow(clippy::too_many_arguments)]
2536fn cap_cdt_xor(
2537 pf: &PrismFrame,
2538 ring2: &[V2],
2539 ring3: &[V3],
2540 aux: &[&[V2]],
2541 seams: &[Seam],
2542 inject: &[(V2, V3)],
2543 _wdepth: f64,
2544 in_lower: impl Fn(V2) -> bool,
2545 in_upper: impl Fn(V2) -> bool,
2546 host: &[PTri],
2547 aabbs: &[(V3, V3)],
2548 caps: &mut Vec<PTri>,
2549) -> bool {
2550 let d = pf.d;
2551 cap_cdt_impl(pf, ring2, ring3, aux, seams, inject, host, aabbs, caps, move |q| {
2552 let lo = in_lower(q);
2553 let up = in_upper(q);
2554 if lo == up {
2555 return None; }
2557 if lo {
2558 Some((scale(d, -1.0), true))
2561 } else {
2562 Some((d, false))
2563 }
2564 })
2565}
2566
2567
2568fn resolve_crossings(
2575 pool: &mut PointPool,
2576 lift3: impl Fn(V2) -> V3,
2577 segs: &mut Vec<(usize, usize)>,
2578) {
2579 let orig = segs.clone();
2580 let mut splits: Vec<Vec<(f64, usize)>> = vec![Vec::new(); orig.len()];
2581 for i in 0..orig.len() {
2584 for j in (i + 1)..orig.len() {
2585 let (a, b) = orig[i];
2586 let (c, d) = orig[j];
2587 if a == c || a == d || b == c || b == d {
2588 continue;
2589 }
2590 let pa = [pool.pts2[a].x, pool.pts2[a].y];
2591 let pb = [pool.pts2[b].x, pool.pts2[b].y];
2592 let pc = [pool.pts2[c].x, pool.pts2[c].y];
2593 let pd = [pool.pts2[d].x, pool.pts2[d].y];
2594 let r = [pb[0] - pa[0], pb[1] - pa[1]];
2595 let sv = [pd[0] - pc[0], pd[1] - pc[1]];
2596 let den = r[0] * sv[1] - r[1] * sv[0];
2597 if den.abs() < 1.0e-18 {
2598 continue;
2599 }
2600 let ap = [pc[0] - pa[0], pc[1] - pa[1]];
2601 let t = (ap[0] * sv[1] - ap[1] * sv[0]) / den;
2602 let u = (ap[0] * r[1] - ap[1] * r[0]) / den;
2603 const E: f64 = 1.0e-9;
2604 if !(E..=1.0 - E).contains(&t) || !(E..=1.0 - E).contains(&u) {
2605 continue;
2606 }
2607 let q = [pa[0] + r[0] * t, pa[1] + r[1] * t];
2608 let idx = pool.intern(q, lift3(q), [0.0, 0.0, 1.0]);
2609 splits[i].push((t, idx));
2610 splits[j].push((u, idx));
2611 }
2612 }
2613 let n_pts = pool.pts2.len();
2615 for k in 0..n_pts {
2616 let p = [pool.pts2[k].x, pool.pts2[k].y];
2617 for (i, &(a, b)) in orig.iter().enumerate() {
2618 if a == k || b == k {
2619 continue;
2620 }
2621 let pa = [pool.pts2[a].x, pool.pts2[a].y];
2622 let pb = [pool.pts2[b].x, pool.pts2[b].y];
2623 let e = [pb[0] - pa[0], pb[1] - pa[1]];
2624 let len2 = e[0] * e[0] + e[1] * e[1];
2625 if len2 < 1.0e-18 {
2626 continue;
2627 }
2628 let t = ((p[0] - pa[0]) * e[0] + (p[1] - pa[1]) * e[1]) / len2;
2629 if !(1.0e-9..=1.0 - 1.0e-9).contains(&t) {
2630 continue;
2631 }
2632 let perp = ((p[0] - pa[0]) * e[1] - (p[1] - pa[1]) * e[0]).abs() / len2.sqrt();
2633 if perp <= pool.eps * 4.0 {
2634 splits[i].push((t, k));
2635 }
2636 }
2637 }
2638 let mut out: Vec<(usize, usize)> = Vec::with_capacity(orig.len() + 8);
2639 for (i, &(a, b)) in orig.iter().enumerate() {
2640 if splits[i].is_empty() {
2641 out.push((a, b));
2642 continue;
2643 }
2644 let mut sp = std::mem::take(&mut splits[i]);
2645 sp.sort_by(|x, y| x.0.total_cmp(&y.0));
2646 let mut prev = a;
2647 for &(_, idx) in &sp {
2648 if idx != prev {
2649 out.push((prev, idx));
2650 prev = idx;
2651 }
2652 }
2653 if prev != b {
2654 out.push((prev, b));
2655 }
2656 }
2657 out.sort();
2658 out.dedup();
2659 *segs = out;
2660}
2661
2662#[allow(clippy::too_many_arguments)]
2667fn cap_cdt_impl(
2668 pf: &PrismFrame,
2669 ring2: &[V2],
2670 ring3: &[V3],
2671 aux: &[&[V2]],
2672 seams: &[Seam],
2673 inject: &[(V2, V3)],
2674 host: &[PTri],
2675 aabbs: &[(V3, V3)],
2676 caps: &mut Vec<PTri>,
2677 classify: impl Fn(V2) -> Option<(V3, bool)>,
2678) -> bool {
2679 let snap_eps = 1.0e-9 * (1.0 + pf.corner_mag());
2680 let mut pool = PointPool::new(snap_eps);
2681 let placeholder_n = [0.0, 0.0, 1.0];
2682 let ring_idx: Vec<usize> = ring2
2683 .iter()
2684 .zip(ring3)
2685 .map(|(&q, &p3)| pool.intern(q, p3, placeholder_n))
2686 .collect();
2687 let wdepth = dot(ring3[0], pf.d);
2690 let mut aux_idx: Vec<Vec<usize>> = Vec::new();
2691 for prof in aux {
2692 aux_idx.push(
2693 prof.iter()
2694 .map(|&[uu, vv]| pool.intern([uu, vv], pf.lift(uu, vv, wdepth), placeholder_n))
2695 .collect(),
2696 );
2697 }
2698 for &(q, p3) in inject {
2701 pool.intern(q, p3, placeholder_n);
2702 }
2703 let mut seam_segs: Vec<(usize, usize)> = Vec::new();
2704 for s in seams {
2705 let ia = pool.intern(s.a2, s.a3, placeholder_n);
2706 let ib = pool.intern(s.b2, s.b3, placeholder_n);
2707 if ia != ib {
2708 seam_segs.push((ia, ib));
2709 }
2710 }
2711 let n_pool = pool.pts2.len();
2715 let mut seg_list: Vec<(usize, usize)> = seam_segs;
2716 let chain = |ia: usize, ib: usize, seg_list: &mut Vec<(usize, usize)>| {
2717 let a = [pool.pts2[ia].x, pool.pts2[ia].y];
2718 let b = [pool.pts2[ib].x, pool.pts2[ib].y];
2719 let eb = [b[0] - a[0], b[1] - a[1]];
2720 let len2 = eb[0] * eb[0] + eb[1] * eb[1];
2721 if len2 < 1.0e-18 {
2722 return;
2723 }
2724 let edge_tol = snap_eps * 4.0;
2725 let mut on_edge: Vec<(f64, usize)> = Vec::new();
2726 for i in 0..n_pool {
2727 if i == ia || i == ib {
2728 continue;
2729 }
2730 let p = [pool.pts2[i].x, pool.pts2[i].y];
2731 let ap = [p[0] - a[0], p[1] - a[1]];
2732 let tt = (ap[0] * eb[0] + ap[1] * eb[1]) / len2;
2733 if !(0.0..=1.0).contains(&tt) {
2734 continue;
2735 }
2736 let perp = (ap[0] * eb[1] - ap[1] * eb[0]).abs() / len2.sqrt();
2737 if perp <= edge_tol {
2738 on_edge.push((tt, i));
2739 }
2740 }
2741 on_edge.sort_by(|x, y| x.0.total_cmp(&y.0));
2742 let mut prev = ia;
2743 for &(_, i) in &on_edge {
2744 if i != prev {
2745 seg_list.push((prev, i));
2746 prev = i;
2747 }
2748 }
2749 if prev != ib {
2750 seg_list.push((prev, ib));
2751 }
2752 };
2753 let nr = ring_idx.len();
2754 for e in 0..nr {
2755 chain(ring_idx[e], ring_idx[(e + 1) % nr], &mut seg_list);
2756 }
2757 for ridx in &aux_idx {
2758 let n = ridx.len();
2759 for e in 0..n {
2760 chain(ridx[e], ridx[(e + 1) % n], &mut seg_list);
2761 }
2762 }
2763 seg_list.sort();
2764 seg_list.dedup();
2765 resolve_crossings(&mut pool, |q| pf.lift(q[0], q[1], wdepth), &mut seg_list);
2766
2767 let Some((pts2, idx2)) = triangulate_pslg(&pool.pts2, &seg_list) else {
2768 return false;
2769 };
2770 if pts2.len() != pool.pts2.len() {
2771 return false;
2772 }
2773 for tri in idx2.chunks_exact(3) {
2774 let (a, b, c) = (tri[0], tri[1], tri[2]);
2775 let cu = (pts2[a].x + pts2[b].x + pts2[c].x) / 3.0;
2776 let cv = (pts2[a].y + pts2[b].y + pts2[c].y) / 3.0;
2777 let Some((desired, flip)) = classify([cu, cv]) else {
2778 continue;
2779 };
2780 let c3 = scale(
2782 add(add(pool.pts3[a], pool.pts3[b]), pool.pts3[c]),
2783 1.0 / 3.0,
2784 );
2785 if point_inside(host, aabbs, c3) {
2786 let (pa, pb, pc) = (pool.pts3[a], pool.pts3[b], pool.pts3[c]);
2787 emit_cap(caps, pa, pb, pc, desired, flip);
2788 }
2789 }
2790 true
2791}
2792
2793impl GeometryRouter {
2796 pub(super) fn try_prism_cut(
2806 &self,
2807 mesh: &Mesh,
2808 ctx: &VoidContext,
2809 ) -> Option<(Mesh, Option<VoidContext>)> {
2810 #[cfg(feature = "opening-perf-trace")]
2811 crate::opening_perf_trace::record(|c| c.prism_attempts = c.prism_attempts.saturating_add(1));
2812 if ctx.merged_openings.is_empty() || mesh.is_empty() || mesh.indices.len() < 12 {
2813 return None;
2814 }
2815 let origin = mesh.origin;
2821 let Some(mut tris) = ptris_from_mesh(mesh) else {
2822 defer(1);
2823 return None;
2824 };
2825 if !closed_enough_to_emit(mesh) {
2830 defer(0);
2831 return None;
2832 }
2833 let min_vol = Self::min_opening_volume(self.tessellation_quality);
2834
2835 struct Cand {
2841 pf: PrismFrame,
2842 ops: Vec<usize>, }
2844 let mut cands: Vec<Cand> = Vec::new();
2845 let mut blockers: Vec<([V3; 3], V3, V3)> = Vec::new();
2846 let mut residual_idx: Vec<usize> = Vec::new();
2847 for (i, op) in ctx.merged_openings.iter().enumerate() {
2848 match prepare_prism(op, origin) {
2849 Some(pf) => cands.push(Cand { pf, ops: vec![i] }),
2850 None => {
2851 defer(2);
2852 residual_idx.push(i);
2853 if let Some(blk) = opening_aabb_obb(op, origin) {
2854 blockers.push(blk);
2855 }
2856 }
2857 }
2858 }
2859 const PROFILE_FUSE_TOL: f64 = 8.0 * crate::kernel::mesh_bridge::SNAP_GRID;
2864 loop {
2865 let mut merged_pair: Option<(usize, usize, PrismFrame)> = None;
2866 'search: for i in 0..cands.len() {
2867 for j in (i + 1)..cands.len() {
2868 if let Some(m) = try_merge_prisms(&cands[i].pf, &cands[j].pf, PROFILE_FUSE_TOL) {
2869 merged_pair = Some((i, j, m));
2870 break 'search;
2871 }
2872 }
2873 }
2874 let Some((i, j, m)) = merged_pair else { break };
2875 let mut absorbed = cands.remove(j);
2876 cands[i].pf = m;
2877 cands[i].ops.append(&mut absorbed.ops);
2878 }
2879 let mut vetoed = vec![false; cands.len()];
2884 for i in 0..cands.len() {
2885 let obb_i = cands[i].pf.obb();
2886 for (j, cj) in cands.iter().enumerate().skip(i + 1) {
2887 if obb_overlaps(&obb_i, &cj.pf.obb(), 1.0e-3) {
2888 vetoed[i] = true;
2889 vetoed[j] = true;
2890 }
2891 }
2892 if blockers.iter().any(|b| obb_overlaps(&obb_i, b, 1.0e-3)) {
2893 vetoed[i] = true;
2894 }
2895 }
2896
2897 let mut committed_ops = 0usize;
2899 let mut committed_cutter_volume = 0.0;
2900 const REFINE_REGION_PAD: f64 = 1.0e-3;
2906 let mut refine_boxes: Vec<([f64; 3], [f64; 3])> = Vec::new();
2907 for (cand, veto) in cands.iter().zip(&vetoed) {
2908 crate::progress::tick();
2909 let mut ok = false;
2910 if *veto {
2911 defer(6);
2912 } else if cand.pf.volume() < min_vol {
2913 defer(3);
2916 } else {
2917 let mut pf = cand.pf.clone();
2918 let axes = [pf.u, pf.v, pf.d];
2923 let mut hlo = [f64::INFINITY; 3];
2924 let mut hhi = [f64::NEG_INFINITY; 3];
2925 for t in &tris {
2926 for p in &t.p {
2927 for k in 0..3 {
2928 let s = dot(*p, axes[k]);
2929 hlo[k] = hlo[k].min(s);
2930 hhi[k] = hhi[k].max(s);
2931 }
2932 }
2933 }
2934 let plo = [pf.bb.0[0], pf.bb.0[1], pf.d0()];
2935 let phi = [pf.bb.1[0], pf.bb.1[1], pf.d1()];
2936 let overlap = (0..3).all(|k| phi[k] > hlo[k] && plo[k] < hhi[k]);
2937 if !overlap {
2938 defer(4);
2939 } else {
2940 let aabbs: Vec<(V3, V3)> = tris.iter().map(PTri::aabb).collect();
2941 extend_prism_caps(&mut pf, &tris, &aabbs);
2942 extend_profile_edges(&mut pf, &tris, &aabbs);
2943 let plo = [pf.bb.0[0], pf.bb.0[1], pf.d0()];
2944 let phi = [pf.bb.1[0], pf.bb.1[1], pf.d1()];
2945 let engulfs = (0..3).all(|k| {
2946 let slack =
2947 (hhi[k] - hlo[k]).abs().max(1.0e-9) * super::ENGULF_TOLERANCE;
2948 plo[k] <= hlo[k] + slack && phi[k] >= hhi[k] - slack
2949 });
2950 if engulfs {
2951 defer(5);
2952 } else {
2953 match cut_prism(&tris, &pf) {
2954 Ok(outcome) => {
2955 committed_cutter_volume += pf.volume();
2956 tris = outcome;
2957 committed_ops += cand.ops.len();
2958 ok = true;
2959 let mut lo = [f64::INFINITY; 3];
2962 let mut hi = [f64::NEG_INFINITY; 3];
2963 for &uu in &[pf.bb.0[0], pf.bb.1[0]] {
2964 for &vv in &[pf.bb.0[1], pf.bb.1[1]] {
2965 for &w in &[pf.d0(), pf.d1()] {
2966 let p = pf.lift(uu, vv, w);
2967 for k in 0..3 {
2968 lo[k] = lo[k].min(p[k]);
2969 hi[k] = hi[k].max(p[k]);
2970 }
2971 }
2972 }
2973 }
2974 refine_boxes.push((
2975 [
2976 lo[0] - REFINE_REGION_PAD,
2977 lo[1] - REFINE_REGION_PAD,
2978 lo[2] - REFINE_REGION_PAD,
2979 ],
2980 [
2981 hi[0] + REFINE_REGION_PAD,
2982 hi[1] + REFINE_REGION_PAD,
2983 hi[2] + REFINE_REGION_PAD,
2984 ],
2985 ));
2986 }
2987 Err(reason) => defer(reason),
2988 }
2989 }
2990 }
2991 }
2992 if !ok {
2993 residual_idx.extend(cand.ops.iter().copied());
2994 }
2995 }
2996 if committed_ops == 0 {
2997 return None;
2998 }
2999
3000 let mut out = mesh_from_ptris(&tris, mesh);
3001 out = crate::csg::ClippingProcessor::consolidate_coplanar(out);
3010 let consolidated_closure = ClosureVerdict::for_mesh(&out);
3011 if consolidated_closure.closed_enough_to_emit()
3012 && !consolidated_change_is_measurable(mesh, &out)
3013 {
3014 defer(4);
3015 return None;
3016 }
3017 if residual_idx.is_empty() && consolidated_closure.is_directed_closed() {
3036 let mut refined =
3056 crate::facet_weld::refine_high_aspect_slivers_within(&out, &refine_boxes);
3057 if refined.indices.len() != out.indices.len() {
3058 for _ in 0..5 {
3059 let next =
3060 crate::facet_weld::refine_high_aspect_slivers_within(&refined, &refine_boxes);
3061 if next.indices.len() == refined.indices.len() {
3062 break;
3063 }
3064 refined = next;
3065 }
3066 let mut probe = refined.clone();
3067 probe.clean_degenerate();
3068 if directed_closed(&refined) && closed_enough_to_emit(&probe) {
3069 out = refined;
3070 }
3071 }
3072 }
3073 let Some((finished, directed)) = finish_cut_verdict(out, mesh) else {
3077 defer(9);
3078 return None;
3079 };
3080 out = finished;
3081 if mesh.origin != [0.0; 3] && !directed {
3086 if let Some(closed) = tjunction_close::close_seams(&out, mesh) {
3087 out = closed;
3088 }
3089 }
3090
3091 residual_idx.sort_unstable();
3093 let residual: Vec<OpeningType> = residual_idx
3094 .iter()
3095 .map(|&i| ctx.merged_openings[i].clone())
3096 .collect();
3097
3098 if !result_volume_within_bounds(mesh, &out, committed_cutter_volume) {
3099 defer(8);
3100 return None;
3101 }
3102 record_cut(committed_ops, residual.len());
3103
3104 let residual_ctx = if residual.is_empty() {
3105 None
3106 } else {
3107 Some(VoidContext {
3108 openings: residual.clone(),
3109 merged_openings: residual,
3110 param: None,
3111 bool2d: None,
3112 })
3113 };
3114 Some((out, residual_ctx))
3115 }
3116}