1use std::collections::{BTreeMap, BTreeSet};
44
45use fast_mesh_query::Plane;
46use fast_mesh_store::{Mesh, StoreError, Transaction, TriangleId, VertexId};
47
48use crate::fill::{FillOptions, FillOptionsBuilder, Plan};
49use crate::fill_dp::{triangulate_flat_loop, triangulate_loop};
50use crate::fill_holes;
51use crate::refine::refine;
52use crate::topology::{BoundaryLoop, EdgeMap, LoopId, VertexFans, boundary_loops};
53use crate::weld::weld_roots;
54use crate::{OpsError, Result, filled, push, reserve};
55
56#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct CutOptions {
59 pub cap: bool,
62}
63
64impl Default for CutOptions {
65 fn default() -> Self {
66 CutOptions { cap: true }
67 }
68}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub struct CutReport {
73 pub kept_triangles: usize,
75 pub removed_triangles: usize,
77 pub cap_triangles: usize,
79}
80
81#[derive(Debug)]
84pub struct CutOutput {
85 pub removed: Mesh,
87 pub report: CutReport,
89}
90
91fn oom() -> OpsError {
92 OpsError::Store(StoreError::OutOfMemory)
93}
94
95pub fn cut_by_plane(
114 tx: &mut Transaction,
115 plane: &Plane,
116 options: &CutOptions,
117) -> Result<CutOutput> {
118 let mesh = tx.mesh();
119 let origin = mesh.origin();
120 let slots = mesh.vertex_slots();
121 let tri_slots = mesh.triangle_slots();
122
123 let mut points: Vec<[f64; 3]> = Vec::new();
126 reserve(&mut points, slots)?;
127 for v in 0..slots as VertexId {
128 points.push(mesh.position(v).map_or([f64::NAN; 3], |p| p.map(f64::from)));
129 }
130 let mut side: Vec<[bool; 3]> = Vec::new();
131 reserve(&mut side, tri_slots)?;
132 let mut straddling: Vec<TriangleId> = Vec::new();
133 for t in 0..tri_slots as TriangleId {
134 let Some(tri) = mesh.triangle(t) else {
135 side.push([true; 3]);
136 continue;
137 };
138 let s = tri.map(|v| plane.signed_distance(points[v as usize]) >= 0.0);
139 if s[0] != s[1] || s[1] != s[2] {
140 push(&mut straddling, t)?;
141 }
142 push(&mut side, s)?;
143 }
144 if straddling.is_empty() {
145 return Err(OpsError::InvalidInput("plane does not cut the mesh"));
146 }
147 let by_position = mesh.uvs().is_some();
151 let mut at: BTreeMap<(VertexId, VertexId), VertexId> = BTreeMap::new();
152 let mut point_of: BTreeMap<(VertexId, VertexId), [f64; 3]> = BTreeMap::new();
153 for &t in &straddling {
154 let s = side[t as usize];
156 let tri = mesh
157 .triangle(t)
158 .ok_or(OpsError::InvalidInput("plane does not cut the mesh"))?;
159 let (e1, e2) = crossed_edges(tri, lone(s));
160 for e in [e1, e2] {
161 if at.contains_key(&e) {
162 continue;
163 }
164 let (a, b) = e;
165 let (pa, pb) = (points[a as usize], points[b as usize]);
166 let (sa, sb) = (plane.signed_distance(pa), plane.signed_distance(pb));
167 debug_assert!((sa >= 0.0) != (sb >= 0.0), "edge {e:?} is not crossed");
168 if sa == 0.0 {
169 at.insert(e, a);
170 } else if sb == 0.0 {
171 at.insert(e, b);
172 } else {
173 point_of.insert(e, crossing_point(pa, pb, sa, sb, by_position));
174 }
175 }
176 }
177
178 let mut added_points: BTreeMap<VertexId, [f64; 3]> = BTreeMap::new();
181 for (&e, &p) in &point_of {
182 let (a, b) = e;
183 let q = p.map(|c| c as f32);
184 if !q.iter().all(|c| c.is_finite()) {
185 return Err(OpsError::InvalidInput("a cut point leaves f32 range"));
186 }
187 let (sa, sb) = (
188 plane.signed_distance(points[a as usize]),
189 plane.signed_distance(points[b as usize]),
190 );
191 let f = (sa / (sa - sb)) as f32;
192 let id = tx.add_vertex_with_parents(q, &[(a, 1.0 - f), (b, f)])?;
193 at.insert(e, id);
194 added_points.insert(id, p);
195 }
196
197 let mut rem_index: BTreeMap<VertexId, u32> = BTreeMap::new();
201 let mut rem_positions: Vec<[f64; 3]> = Vec::new();
202 let mut rem_tris: Vec<[u32; 3]> = Vec::new();
203 let mut kept_cut: BTreeSet<(VertexId, VertexId)> = BTreeSet::new();
204 let mut rem_cut: BTreeSet<(u32, u32)> = BTreeSet::new();
205 let mut removed_tris: Vec<TriangleId> = Vec::new();
206 for &t in &straddling {
207 let tri = tx
208 .mesh()
209 .triangle(t)
210 .ok_or(OpsError::InvalidInput("plane does not cut the mesh"))?;
211 let s = side[t as usize];
212 let i = lone(s);
213 let (e1, e2) = crossed_edges(tri, i);
214 let (x1, x2) = (at[&e1], at[&e2]);
215 if x1 != x2 {
216 kept_cut.insert(key(x1, x2));
217 }
218 let (kept_poly, removed_poly) = if s[i] {
219 (
220 vec![tri[i], x1, x2],
221 vec![x1, tri[(i + 1) % 3], tri[(i + 2) % 3], x2],
222 )
223 } else {
224 (
225 vec![x1, tri[(i + 1) % 3], tri[(i + 2) % 3], x2],
226 vec![tri[i], x1, x2],
227 )
228 };
229 write_poly(tx, t, &kept_poly)?;
230 fn rem_id(
233 v: VertexId,
234 points: &[[f64; 3]],
235 added_points: &BTreeMap<VertexId, [f64; 3]>,
236 rem_index: &mut BTreeMap<VertexId, u32>,
237 rem_positions: &mut Vec<[f64; 3]>,
238 ) -> Result<u32> {
239 if let Some(&idx) = rem_index.get(&v) {
240 return Ok(idx);
241 }
242 let p = match added_points.get(&v) {
243 Some(&p) => p,
244 None => points[v as usize],
245 };
246 if !p.iter().all(|c| c.is_finite()) {
247 return Err(OpsError::InvalidInput("a removed vertex is not finite"));
248 }
249 let idx = u32::try_from(rem_positions.len()).map_err(|_| oom())?;
250 reserve(rem_positions, 1)?;
251 rem_positions.push(p);
252 rem_index.insert(v, idx);
253 Ok(idx)
254 }
255 let mut quad: Vec<u32> = Vec::with_capacity(removed_poly.len());
256 for v in removed_poly {
257 quad.push(rem_id(
258 v,
259 &points,
260 &added_points,
261 &mut rem_index,
262 &mut rem_positions,
263 )?);
264 }
265 let before = rem_tris.len();
266 emit_fan(&mut rem_tris, quad)?;
267 if rem_tris.len() > before && x1 != x2 {
268 match (rem_index.get(&x1), rem_index.get(&x2)) {
269 (Some(&r1), Some(&r2)) if r1 != r2 => {
270 rem_cut.insert((r1.min(r2), r1.max(r2)));
271 }
272 _ => {}
273 }
274 }
275 }
276 for t in 0..tri_slots as TriangleId {
279 if side[t as usize] != [false; 3] {
280 continue;
281 }
282 let tri = tx
283 .mesh()
284 .triangle(t)
285 .ok_or(OpsError::InvalidInput("plane does not cut the mesh"))?;
286 let mut idx = [0; 3];
287 for (k, v) in tri.iter().enumerate() {
288 if !rem_index.contains_key(v) {
290 let p = points[*v as usize];
291 let n = u32::try_from(rem_positions.len()).map_err(|_| oom())?;
292 reserve(&mut rem_positions, 1)?;
293 rem_positions.push(p);
294 rem_index.insert(*v, n);
295 }
296 idx[k] = rem_index[v];
297 }
298 push(&mut rem_tris, idx)?;
299 push(&mut removed_tris, t)?;
300 }
301 if !removed_tris.is_empty() {
302 tx.remove_triangles(&removed_tris)?;
303 }
304 let mut orphans: Vec<VertexId> = Vec::new();
306 for v in 0..tx.mesh().vertex_slots() as VertexId {
307 if tx.mesh().is_vertex_alive(v) && tx.mesh().corner_count(v) == 0 {
308 push(&mut orphans, v)?;
309 }
310 }
311 if !orphans.is_empty() {
312 tx.remove_vertices(&orphans)?;
313 }
314
315 let mut cap_triangles = 0;
317 if options.cap {
318 let mut seam_twins: BTreeSet<PositionEdge> = BTreeSet::new();
320 if by_position {
321 let original = |v: VertexId| {
322 points
323 .get(v as usize)
324 .filter(|&&p| plane.signed_distance(p) == 0.0)
325 .map(|p| p.map(|c| c as f32))
326 };
327 for &(a, b) in &kept_cut {
328 if let (Some(p), Some(q)) = (original(a), original(b)) {
329 seam_twins.insert(position_edge(p, q));
330 }
331 }
332 }
333 let rims = find_rims(tx.mesh(), &kept_cut, &seam_twins)?;
334 cap_triangles += cap_rims(tx, &rims, plane.normal())?;
335 }
336 let kept_triangles = tx.mesh().triangle_count();
337
338 let mut flat_positions: Vec<f64> = Vec::new();
340 reserve(&mut flat_positions, rem_positions.len() * 3)?;
341 for p in &rem_positions {
342 flat_positions.extend_from_slice(&[origin[0] + p[0], origin[1] + p[1], origin[2] + p[2]]);
343 }
344 let mut flat_tris: Vec<u32> = Vec::new();
345 reserve(&mut flat_tris, rem_tris.len() * 3)?;
346 for t in &rem_tris {
347 flat_tris.extend_from_slice(t);
348 }
349 let mut removed = Mesh::from_buffers_f64_with_origin(&flat_positions, &flat_tris, origin)?;
350 if let Some(uvs) = tx.mesh().uvs() {
353 let mut rem_uvs = filled(2 * rem_positions.len(), 0.0f32)?;
354 for (&v, &i) in &rem_index {
355 rem_uvs[2 * i as usize] = uvs[2 * v as usize];
356 rem_uvs[2 * i as usize + 1] = uvs[2 * v as usize + 1];
357 }
358 removed.add_uvs(&rem_uvs)?;
359 }
360 if options.cap {
361 let rims = find_rims(&removed, &rem_cut, &BTreeSet::new())?;
364 if !rims.whole.is_empty() || !rims.by_position.is_empty() {
365 let mut rtx = removed.begin_edit();
366 cap_triangles += cap_rims(&mut rtx, &rims, plane.normal())?;
367 rtx.commit()?;
368 }
369 }
370 let removed_triangles = removed.triangle_count();
371
372 Ok(CutOutput {
373 removed,
374 report: CutReport {
375 kept_triangles,
376 removed_triangles,
377 cap_triangles,
378 },
379 })
380}
381
382fn crossing_point(a: [f64; 3], b: [f64; 3], sa: f64, sb: f64, by_position: bool) -> [f64; 3] {
387 let swap = by_position && b < a;
388 let (a, b, sa, sb) = if swap { (b, a, sb, sa) } else { (a, b, sa, sb) };
389 let f = sa / (sa - sb);
390 std::array::from_fn(|k| a[k] + f * (b[k] - a[k]))
391}
392
393fn lone(s: [bool; 3]) -> usize {
395 if s[0] == s[1] {
396 2
397 } else if s[0] == s[2] {
398 1
399 } else {
400 0
401 }
402}
403
404fn key(a: VertexId, b: VertexId) -> (VertexId, VertexId) {
405 (a.min(b), a.max(b))
406}
407
408fn crossed_edges(tri: [VertexId; 3], i: usize) -> ((VertexId, VertexId), (VertexId, VertexId)) {
411 let (a, b, c) = (tri[i], tri[(i + 1) % 3], tri[(i + 2) % 3]);
412 (key(a, b), key(c, a))
413}
414
415fn write_poly(tx: &mut Transaction, t: TriangleId, poly: &[VertexId]) -> Result<()> {
418 let mut dedup: Vec<VertexId> = Vec::with_capacity(poly.len());
419 for &v in poly {
420 if dedup.last() != Some(&v) {
421 dedup.push(v);
422 }
423 }
424 while dedup.len() > 1 && dedup[0] == *dedup.last().unwrap() {
425 dedup.pop();
426 }
427 match dedup.len() {
428 0..=2 => tx.remove_triangles(&[t]).map_err(OpsError::from),
429 3 => tx
430 .set_triangles(&[t], &[dedup.as_slice().try_into().unwrap()])
431 .map_err(OpsError::from),
432 _ => {
433 tx.set_triangles(&[t], &[[dedup[0], dedup[1], dedup[2]]])?;
434 tx.add_triangles(&[[dedup[0], dedup[2], dedup[3]]])?;
435 Ok(())
436 }
437 }
438}
439
440fn emit_fan(tris: &mut Vec<[u32; 3]>, poly: Vec<u32>) -> Result<()> {
442 let mut dedup: Vec<u32> = Vec::with_capacity(poly.len());
443 for v in poly {
444 if dedup.last() != Some(&v) {
445 dedup.push(v);
446 }
447 }
448 while dedup.len() > 1 && dedup[0] == *dedup.last().unwrap() {
449 dedup.pop();
450 }
451 if dedup.len() == 4 {
452 reserve(tris, 2)?;
453 tris.push([dedup[0], dedup[1], dedup[2]]);
454 tris.push([dedup[0], dedup[2], dedup[3]]);
455 } else if dedup.len() == 3 {
456 push(tris, [dedup[0], dedup[1], dedup[2]])?;
457 }
458 Ok(())
459}
460
461fn is_cut_rim(l: &BoundaryLoop, cut: &BTreeSet<(u32, u32)>) -> bool {
463 l.closed && loop_edges(&l.vertices).iter().all(|e| cut.contains(e))
464}
465
466fn loop_edges(v: &[VertexId]) -> Vec<(u32, u32)> {
468 let mut out = Vec::with_capacity(v.len());
469 for (k, &a) in v.iter().enumerate() {
470 let b = v[(k + 1) % v.len()];
471 out.push((a.min(b), a.max(b)));
472 }
473 out
474}
475
476struct Rims {
478 whole: Vec<BoundaryLoop>,
480 by_position: Vec<(VertexId, VertexId)>,
483}
484
485fn position_key(p: [f32; 3]) -> [u32; 3] {
487 p.map(|x| (x + 0.0).to_bits())
488}
489
490fn position_edge(a: [f32; 3], b: [f32; 3]) -> PositionEdge {
492 let (a, b) = (position_key(a), position_key(b));
493 if a <= b { (a, b) } else { (b, a) }
494}
495
496type PositionEdge = ([u32; 3], [u32; 3]);
497
498fn find_rims(
505 mesh: &Mesh,
506 cut: &BTreeSet<(VertexId, VertexId)>,
507 seam_twins: &BTreeSet<PositionEdge>,
508) -> Result<Rims> {
509 let loops = boundary_loops(mesh)?;
510 let seamed = mesh.uvs().is_some();
511 let twin = |(a, b): (VertexId, VertexId)| match (mesh.position(a), mesh.position(b)) {
512 (Some(p), Some(q)) => seam_twins.contains(&position_edge(p, q)),
513 _ => false,
514 };
515 let mut rims = Rims {
516 whole: Vec::new(),
517 by_position: Vec::new(),
518 };
519 for l in loops {
520 if is_cut_rim(&l, cut) {
521 if l.vertices.len() > LARGE_RIM {
522 let edges = loop_edges(&l.vertices);
523 reserve(&mut rims.by_position, edges.len())?;
524 rims.by_position.extend(edges);
525 } else {
526 push(&mut rims.whole, l)?;
527 }
528 } else if seamed {
529 for e in loop_edges(&l.vertices) {
530 if cut.contains(&e) || twin(e) {
531 push(&mut rims.by_position, e)?;
532 }
533 }
534 }
535 }
536 Ok(rims)
537}
538
539fn cap_rims(tx: &mut Transaction, rims: &Rims, normal: [f64; 3]) -> Result<usize> {
541 let before = tx.mesh().triangle_count();
542 let mut by_position = rims.by_position.clone();
543 if !rims.whole.is_empty() {
544 let mut ids: Vec<LoopId> = Vec::new();
545 reserve(&mut ids, rims.whole.len())?;
546 ids.extend(rims.whole.iter().map(|l| l.id));
547 let report = fill_holes(
548 tx,
549 &FillOptionsBuilder::default()
550 .loops(ids)
551 .fair(false)
552 .allow_unclean(true)
553 .build(),
554 )?;
555 let refused = report
557 .failed
558 .iter()
559 .map(|&(id, _)| id)
560 .chain(report.skipped.iter().map(|&(id, _)| id));
561 for id in refused {
562 if let Some(l) = rims.whole.iter().find(|l| l.id == id) {
563 let edges = loop_edges(&l.vertices);
564 reserve(&mut by_position, edges.len())?;
565 by_position.extend(edges);
566 }
567 }
568 }
569 if !by_position.is_empty() {
570 cap_by_position(tx, &by_position, normal)?;
571 }
572 Ok(tx.mesh().triangle_count() - before)
573}
574
575const LARGE_RIM: usize = 2048;
579
580const CAP_VERTEX_BUDGET: usize = 20_000;
585
586type SeamRim = Vec<(VertexId, VertexId)>;
589
590fn seam_rims(
597 mesh: &Mesh,
598 edges: &[(VertexId, VertexId)],
599 roots: &[VertexId],
600) -> Result<Vec<SeamRim>> {
601 let map = EdgeMap::build(mesh)?;
602 let mut directed: BTreeMap<(VertexId, VertexId), Vec<VertexId>> = BTreeMap::new();
604 for &(a, b) in edges {
605 let [t] = map.triangles(a, b) else {
607 continue;
608 };
609 let [x, y, z] = mesh.triangles()[*t as usize];
610 let forward = [(x, y), (y, z), (z, x)].contains(&(a, b));
611 let (from, to) = if forward { (a, b) } else { (b, a) };
612 let (ra, rb) = (roots[from as usize], roots[to as usize]);
613 if ra != rb {
614 directed.entry((ra, rb)).or_default().push(from);
615 }
616 }
617 let mut leaving: BTreeMap<VertexId, Vec<(VertexId, VertexId)>> = BTreeMap::new();
619 let mut arriving: BTreeMap<VertexId, usize> = BTreeMap::new();
620 let mut neighbours: BTreeMap<VertexId, Vec<VertexId>> = BTreeMap::new();
621 for (&(ra, rb), froms) in &directed {
622 let closed = directed.get(&(rb, ra)).map_or(0, Vec::len);
623 for &from in froms.iter().skip(closed) {
624 leaving.entry(ra).or_default().push((from, rb));
625 *arriving.entry(rb).or_default() += 1;
626 neighbours.entry(ra).or_default().push(rb);
627 neighbours.entry(rb).or_default().push(ra);
628 }
629 }
630 let mut rims: Vec<SeamRim> = Vec::new();
631 let mut seen: BTreeSet<VertexId> = BTreeSet::new();
632 let mut group: Vec<VertexId> = Vec::new();
633 for &start in neighbours.keys() {
634 if !seen.insert(start) {
635 continue;
636 }
637 group.clear();
638 push(&mut group, start)?;
639 let mut next = 0;
640 while let Some(&r) = group.get(next) {
641 next += 1;
642 for &n in &neighbours[&r] {
643 if seen.insert(n) {
644 push(&mut group, n)?;
645 }
646 }
647 }
648 if group.iter().any(|r| neighbours[r].len() % 2 == 1) {
649 continue;
650 }
651 let single = |r: &VertexId| {
652 leaving.get(r).is_some_and(|l| l.len() == 1) && arriving.get(r) == Some(&1)
653 };
654 if !group.iter().all(single) {
655 return Err(OpsError::InvalidInput("a cut rim cannot be capped"));
656 }
657 let mut rim: SeamRim = Vec::new();
658 reserve(&mut rim, group.len())?;
659 let mut r = start;
660 loop {
661 let (from, to) = leaving[&r][0];
662 rim.push((r, from));
663 r = to;
664 if r == start {
665 break;
666 }
667 }
668 rims.push(rim);
669 }
670 Ok(rims)
671}
672
673fn cap_by_position(
683 tx: &mut Transaction,
684 edges: &[(VertexId, VertexId)],
685 normal: [f64; 3],
686) -> Result<()> {
687 let mesh = tx.mesh();
688 let roots = weld_roots(mesh, 0.0)?;
689 let rims = seam_rims(mesh, edges, &roots)?;
690 if rims.is_empty() {
691 return Ok(());
692 }
693 let mut on_rim = filled(mesh.vertex_slots(), false)?;
694 for rim in &rims {
695 for &(r, _) in rim {
696 on_rim[r as usize] = true;
697 }
698 }
699 const NONE: u32 = u32::MAX;
700 let mut local = filled(mesh.vertex_slots(), NONE)?;
701 let mut positions: Vec<f32> = Vec::new();
702 let mut indices: Vec<u32> = Vec::new();
703 for t in 0..mesh.triangle_slots() as TriangleId {
704 let Some(tri) = mesh.triangle(t) else {
705 continue;
706 };
707 let tri = tri.map(|v| roots[v as usize]);
708 let distinct = tri[0] != tri[1] && tri[1] != tri[2] && tri[2] != tri[0];
709 if !distinct || !tri.iter().any(|&r| on_rim[r as usize]) {
710 continue;
711 }
712 reserve(&mut indices, 3)?;
713 for r in tri {
714 if local[r as usize] == NONE {
715 local[r as usize] = (positions.len() / 3) as u32;
716 reserve(&mut positions, 3)?;
717 positions.extend(mesh.positions()[r as usize]);
718 }
719 indices.push(local[r as usize]);
720 }
721 }
722 let scratch = Mesh::from_buffers(&positions, &indices)?;
724 let map = EdgeMap::build(&scratch)?;
725 let fans = VertexFans::build(&scratch)?;
726 let defaults = FillOptions::default();
727 let mut plan = Plan::default();
728 let mut wall = filled(positions.len() / 3, NONE)?;
729 let mut order: Vec<VertexId> = Vec::new();
730 for rim in &rims {
731 order.clear();
732 reserve(&mut order, rim.len())?;
733 for &(r, from) in rim {
734 order.push(local[r as usize]);
735 wall[local[r as usize] as usize] = from;
736 }
737 let searched = if order.len() <= defaults.dp_limit {
738 triangulate_loop(&scratch, &order, &map, defaults.dp_limit, None)?.ok()
739 } else {
740 None
741 };
742 let tris = match searched {
743 Some(tris) => tris,
744 None => triangulate_flat_loop(&scratch, &order, &map, normal)?
745 .map_err(|_| OpsError::InvalidInput("a cut rim cannot be capped"))?,
746 };
747 let mut patch = refine(
748 &scratch,
749 &fans,
750 &map,
751 &order,
752 &tris,
753 Some(CAP_VERTEX_BUDGET),
754 )?;
755 for v in &mut patch.old {
756 *v = wall[*v as usize];
757 }
758 plan.add(&patch)?;
759 }
760 plan.write(tx)
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use crate::test_util::{edit, grid_without, mesh_of};
767 use fast_mesh_store::Mesh;
768
769 fn cube() -> Mesh {
771 Mesh::from_buffers(
772 &[
773 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
781 ],
782 &[
783 0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 2, 3, 7, 2, 7, 6, 1, 2, 6, 1, 6, 5, 3, 0, 4, 3, 4, 7, ],
790 )
791 .unwrap()
792 }
793
794 fn volume6(mesh: &Mesh) -> f64 {
796 let mut v = 0.0;
797 for t in 0..mesh.triangle_slots() as TriangleId {
798 let Some(tri) = mesh.triangle(t) else {
799 continue;
800 };
801 let [a, b, c] = tri.map(|x| mesh.position(x).unwrap().map(f64::from));
802 v += a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
803 + a[2] * (b[0] * c[1] - b[1] * c[0]);
804 }
805 v
806 }
807
808 fn plane(origin: [f64; 3], normal: [f64; 3]) -> Plane {
809 Plane::new(origin, normal).unwrap()
810 }
811
812 fn cut_kept(mesh: &mut Mesh, plane: &Plane, cap: bool) -> CutOutput {
813 let (out, _) = edit(mesh, |tx| cut_by_plane(tx, plane, &CutOptions { cap }));
814 out
815 }
816
817 fn kept_x(mesh: &Mesh, v: VertexId) -> f64 {
819 mesh.position(v).unwrap()[0] as f64 + mesh.origin()[0]
820 }
821
822 #[test]
823 fn cube_cut_in_half_is_two_closed_halves() {
824 let mut mesh = cube();
825 let origin = mesh.origin();
826 let out = cut_kept(&mut mesh, &plane([0.5, 0.0, 0.0], [1.0, 0.0, 0.0]), true);
827 assert!(boundary_loops(&mesh).unwrap().is_empty());
829 assert!(boundary_loops(&out.removed).unwrap().is_empty());
830 let (vk, vr) = (volume6(&mesh), volume6(&out.removed));
832 assert!(vk > 0.0 && vr > 0.0, "{vk} {vr}");
833 assert!((vk + vr - 6.0).abs() < 1e-9, "{vk} {vr}");
834 assert!((vk - vr).abs() < 1e-6, "{vk} {vr}");
835 assert!((0..mesh.vertex_slots() as VertexId).all(|v| {
837 mesh.position(v)
838 .is_none_or(|_| kept_x(&mesh, v) >= 0.5 - 1e-6)
839 }));
840 assert!(
841 out.removed
842 .positions()
843 .iter()
844 .all(|p| p[0] as f64 + out.removed.origin()[0] <= 0.5 + 1e-6)
845 );
846 assert_eq!(out.report.kept_triangles, mesh.triangle_count());
847 assert_eq!(out.report.removed_triangles, out.removed.triangle_count());
848 assert!(out.report.cap_triangles >= 4, "{:?}", out.report);
849 assert_eq!(out.removed.origin(), origin);
850 }
851
852 #[test]
853 fn cap_off_leaves_both_sides_open() {
854 let mut mesh = cube();
855 let out = cut_kept(&mut mesh, &plane([0.5, 0.0, 0.0], [1.0, 0.0, 0.0]), false);
856 assert!(!boundary_loops(&mesh).unwrap().is_empty());
857 assert!(!boundary_loops(&out.removed).unwrap().is_empty());
858 assert_eq!(out.report.cap_triangles, 0);
859 assert!(
861 mesh.positions()
862 .iter()
863 .any(|p| (kept_x_of(&mesh, p) - 0.5).abs() < 1e-6)
864 );
865 }
866
867 fn kept_x_of(mesh: &Mesh, p: &[f32; 3]) -> f64 {
868 p[0] as f64 + mesh.origin()[0]
869 }
870
871 #[test]
872 fn plane_missing_the_mesh_is_rejected_and_changes_nothing() {
873 let mut mesh = cube();
874 let mut tx = mesh.begin_edit();
875 let r = cut_by_plane(
876 &mut tx,
877 &plane([5.0, 0.0, 0.0], [1.0, 0.0, 0.0]),
878 &CutOptions::default(),
879 );
880 assert_eq!(
881 r.unwrap_err(),
882 OpsError::InvalidInput("plane does not cut the mesh")
883 );
884 tx.rollback();
885 assert_eq!(mesh.triangle_count(), 12);
886 }
887
888 #[test]
889 fn open_grid_cut_keeps_origin_and_sides() {
890 let mut mesh = grid_without(8, &[]);
892 let origin = mesh.origin();
893 let out = cut_kept(&mut mesh, &plane([3.5, 0.0, 0.0], [1.0, 0.0, 0.0]), false);
894 assert_eq!(out.removed.origin(), origin);
895 let alive = |m: &Mesh, v: VertexId| m.is_vertex_alive(v);
896 assert!(
897 (0..mesh.vertex_slots() as VertexId)
898 .filter(|&v| alive(&mesh, v))
899 .all(|v| kept_x(&mesh, v) >= 3.5 - 1e-6)
900 );
901 assert!(
902 out.removed
903 .positions()
904 .iter()
905 .all(|p| p[0] as f64 + out.removed.origin()[0] <= 3.5 + 1e-6)
906 );
907 }
908
909 fn seamed_tube(slices: u32, rows: u32, lids: bool) -> Mesh {
917 let (mut positions, mut uvs, mut triangles) = (Vec::new(), Vec::new(), Vec::new());
918 let id = |i: u32, j: u32| i * (slices + 1) + j;
919 for i in 0..=rows {
920 for j in 0..=slices {
921 let a = std::f32::consts::TAU * (j % slices) as f32 / slices as f32;
922 positions.extend([a.cos(), a.sin(), i as f32]);
923 uvs.extend([j as f32 / slices as f32, i as f32 / rows as f32]);
924 }
925 }
926 for i in 0..rows {
927 for j in 0..slices {
928 let (a, b) = (id(i, j), id(i, j + 1));
929 let (c, d) = (id(i + 1, j + 1), id(i + 1, j));
930 triangles.extend([[a, b, c], [a, c, d]]);
931 }
932 }
933 if lids {
934 let bottom = (positions.len() / 3) as u32;
935 positions.extend([0.0, 0.0, 0.0, 0.0, 0.0, rows as f32]);
936 uvs.extend([0.5, 0.0, 0.5, 1.0]);
937 for j in 0..slices {
938 triangles.push([bottom, id(0, j + 1), id(0, j)]);
939 triangles.push([bottom + 1, id(rows, j), id(rows, j + 1)]);
940 }
941 }
942 let mut mesh = mesh_of(&positions, &triangles);
943 mesh.add_uvs(&uvs).unwrap();
944 mesh
945 }
946
947 fn cube_of_islands() -> Mesh {
950 let whole = cube();
951 let (mut positions, mut uvs, mut triangles) = (Vec::new(), Vec::new(), Vec::new());
952 for (t, tri) in whole.triangles().iter().enumerate() {
953 let island = t as f32;
954 let base = (positions.len() / 3) as u32;
955 for (k, &v) in tri.iter().enumerate() {
956 positions.extend(whole.position(v).unwrap());
957 uvs.extend([island / 12.0, k as f32 / 3.0]);
958 }
959 triangles.push([base, base + 1, base + 2]);
960 }
961 let mut mesh = mesh_of(&positions, &triangles);
962 mesh.add_uvs(&uvs).unwrap();
963 mesh
964 }
965
966 fn welded_by_position(mesh: &Mesh) -> Mesh {
968 let roots = crate::weld::weld_roots(mesh, 0.0).unwrap();
969 let positions: Vec<f32> = mesh.positions().iter().flatten().copied().collect();
970 let triangles: Vec<[u32; 3]> = (0..mesh.triangle_slots() as TriangleId)
971 .filter_map(|t| mesh.triangle(t))
972 .map(|t| t.map(|v| roots[v as usize]))
973 .collect();
974 mesh_of(&positions, &triangles)
975 }
976
977 fn assert_closed_by_position(mesh: &Mesh) {
979 let welded = welded_by_position(mesh);
980 let mut uses: BTreeMap<(u32, u32), i32> = BTreeMap::new();
981 for t in 0..welded.triangle_slots() as TriangleId {
982 let Some([a, b, c]) = welded.triangle(t) else {
983 continue;
984 };
985 if a == b || b == c || c == a {
988 continue;
989 }
990 for (x, y) in [(a, b), (b, c), (c, a)] {
991 *uses.entry((x, y)).or_default() += 1;
992 }
993 }
994 for (&(a, b), &n) in &uses {
995 assert_eq!(n, 1, "edge {a}-{b}");
996 assert_eq!(uses.get(&(b, a)), Some(&1), "edge {b}-{a} is open");
997 }
998 }
999
1000 fn assert_cut_closes(mut mesh: Mesh, plane: &Plane) -> (Mesh, CutOutput) {
1005 assert_closed_by_position(&mesh);
1006 let whole = volume6(&mesh);
1007 let triangles = mesh.triangle_count();
1008 let out = cut_kept(&mut mesh, plane, true);
1009 assert_closed_by_position(&mesh);
1010 assert_closed_by_position(&out.removed);
1011 let (vk, vr) = (volume6(&mesh), volume6(&out.removed));
1012 assert!(vk > 0.0 && vr > 0.0, "{vk} {vr}");
1013 assert!(
1014 (vk + vr - whole).abs() < 1e-4 * whole,
1015 "{vk} + {vr} != {whole}"
1016 );
1017 assert_eq!(out.report.kept_triangles, mesh.triangle_count());
1018 assert_eq!(out.report.removed_triangles, out.removed.triangle_count());
1019 assert!(out.report.cap_triangles > 0, "{:?}", out.report);
1020 assert!(mesh.triangle_count() + out.removed.triangle_count() > triangles);
1021 for m in [&mesh, &out.removed] {
1022 assert_eq!(m.uvs().unwrap().len(), 2 * m.vertex_slots());
1023 assert!(m.uvs().unwrap().iter().all(|u| u.is_finite()));
1024 }
1025 (mesh, out)
1026 }
1027
1028 #[test]
1029 fn a_seam_across_the_plane_is_capped_on_both_sides() {
1030 let plane = plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]);
1031 let (kept, out) = assert_cut_closes(seamed_tube(16, 3, true), &plane);
1032 assert!(!boundary_loops(&kept).unwrap().is_empty());
1035 assert!(!boundary_loops(&out.removed).unwrap().is_empty());
1036 for m in [&kept, &out.removed] {
1039 let uvs = m.uvs().unwrap();
1040 let on_plane = (0..m.vertex_slots() as VertexId)
1041 .filter(|&v| {
1042 m.position(v)
1043 .is_some_and(|p| (f64::from(p[2]) + m.origin()[2] - 1.5).abs() < 1e-5)
1044 })
1045 .collect::<Vec<_>>();
1046 assert!(on_plane.len() >= 17, "{}", on_plane.len());
1047 for v in on_plane {
1048 assert!((uvs[2 * v as usize + 1] - 0.5).abs() < 1e-5, "vertex {v}");
1049 assert!((0.0..=1.0).contains(&uvs[2 * v as usize]), "vertex {v}");
1050 }
1051 }
1052 }
1053
1054 #[test]
1055 fn every_edge_a_seam_is_capped_on_both_sides() {
1056 for p in [
1057 plane([0.5, 0.5, 0.5], [1.0, 0.0, 0.0]),
1058 plane([0.25, 0.5, 0.5], [1.0, 0.25, 0.125]),
1059 plane([0.5, 0.5, 0.5], [1.0, 1.0, 1.0]),
1060 ] {
1061 assert_cut_closes(cube_of_islands(), &p);
1062 }
1063 }
1064
1065 #[test]
1066 fn oblique_cuts_across_a_seam_close() {
1067 for p in [
1068 plane([0.0, 0.0, 1.5], [0.3, 0.2, 1.0]),
1069 plane([0.1, 0.0, 1.2], [1.0, 0.1, 1.0]),
1070 plane([0.0, 0.0, 1.5], [0.2, 1.0, 0.1]),
1072 ] {
1073 assert_cut_closes(seamed_tube(24, 3, true), &p);
1074 }
1075 }
1076
1077 #[test]
1078 fn a_plane_through_the_seam_vertices_closes() {
1079 assert_cut_closes(
1081 seamed_tube(16, 4, true),
1082 &plane([0.0, 0.0, 2.0], [0.0, 0.0, 1.0]),
1083 );
1084 assert_cut_closes(
1085 seamed_tube(16, 4, true),
1086 &plane([0.0, 0.0, 2.0], [0.0, 0.0, -1.0]),
1087 );
1088 }
1089
1090 #[test]
1091 fn a_plane_along_the_seam_closes() {
1092 assert_cut_closes(
1095 seamed_tube(16, 3, true),
1096 &plane([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
1097 );
1098 assert_cut_closes(
1099 seamed_tube(16, 3, true),
1100 &plane([0.0, 0.0, 0.0], [0.0, -1.0, 0.0]),
1101 );
1102 }
1103
1104 #[test]
1105 fn a_plane_on_a_face_of_islands_closes_like_a_plain_cube() {
1106 let on_face = plane([0.0, 0.0, 1.0], [0.0, 0.0, 1.0]);
1108 let mut plain = cube();
1109 let mut islands = cube_of_islands();
1110 let (a, b) = (
1111 cut_kept(&mut plain, &on_face, true),
1112 cut_kept(&mut islands, &on_face, true),
1113 );
1114 assert!(a.report.cap_triangles > 0 && b.report.cap_triangles > 0);
1115 assert_closed_by_position(&islands);
1116 assert_closed_by_position(&b.removed);
1117 }
1118
1119 #[test]
1120 fn an_open_tube_with_a_seam_gets_its_cut_capped_and_its_ends_left() {
1121 let mut mesh = seamed_tube(16, 3, false);
1122 let out = cut_kept(&mut mesh, &plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]), true);
1123 assert!(out.report.cap_triangles > 0);
1124 for m in [&mesh, &out.removed] {
1126 let open = boundary_loops(&welded_by_position(m)).unwrap();
1127 assert_eq!(open.len(), 1, "{open:?}");
1128 assert_eq!(open[0].vertices.len(), 16);
1129 }
1130 }
1131
1132 #[test]
1133 fn an_open_sheet_with_a_seam_stays_open_without_an_error() {
1134 let mut mesh = seamed_tube(16, 3, false);
1136 let half = plane([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1137 let out = cut_kept(&mut mesh, &half, false);
1140 assert_eq!(out.report.cap_triangles, 0);
1141 let across = plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]);
1142 let again = cut_kept(&mut mesh, &across, true);
1143 assert_eq!(again.report.cap_triangles, 0);
1144 assert_eq!(boundary_loops(&mesh).unwrap().len(), 1);
1145 }
1146
1147 #[test]
1148 fn a_rim_at_a_seam_wound_against_itself_is_an_error_and_writes_nothing() {
1149 let whole = cube_of_islands();
1152 let positions: Vec<f32> = whole.positions().iter().flatten().copied().collect();
1153 let mut triangles = whole.triangles().to_vec();
1154 triangles[4].swap(1, 2);
1156 triangles[5].swap(1, 2);
1157 let mut mesh = mesh_of(&positions, &triangles);
1158 mesh.add_uvs(whole.uvs().unwrap()).unwrap();
1159 let before = crate::test_util::parts(&mesh);
1160 let mut tx = mesh.begin_edit();
1161 let r = cut_by_plane(
1162 &mut tx,
1163 &plane([0.5, 0.0, 0.0], [1.0, 0.0, 0.0]),
1164 &CutOptions::default(),
1165 );
1166 assert_eq!(
1167 r.unwrap_err(),
1168 OpsError::InvalidInput("a cut rim cannot be capped")
1169 );
1170 tx.rollback();
1171 assert_eq!(crate::test_util::parts(&mesh), before);
1172 }
1173
1174 #[test]
1175 fn overlaid_seamed_sheets_are_an_error_rather_than_a_silent_gap() {
1176 let one = seamed_tube(16, 3, true);
1177 let (p, t) = crate::test_util::parts(&one);
1178 let base = (p.len() / 3) as u32;
1179 let positions: Vec<f32> = p.iter().chain(&p).copied().collect();
1180 let triangles: Vec<[u32; 3]> = t
1181 .iter()
1182 .copied()
1183 .chain(t.iter().map(|x| x.map(|v| v + base)))
1184 .collect();
1185 let uvs: Vec<f32> = one.uvs().unwrap().repeat(2);
1186 let mut mesh = mesh_of(&positions, &triangles);
1187 mesh.add_uvs(&uvs).unwrap();
1188 let mut tx = mesh.begin_edit();
1189 let r = cut_by_plane(
1190 &mut tx,
1191 &plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]),
1192 &CutOptions::default(),
1193 );
1194 assert_eq!(
1195 r.unwrap_err(),
1196 OpsError::InvalidInput("a cut rim cannot be capped")
1197 );
1198 tx.rollback();
1199 }
1200
1201 #[test]
1202 fn cap_off_leaves_a_seamed_mesh_open() {
1203 let mut mesh = seamed_tube(16, 3, true);
1204 let out = cut_kept(&mut mesh, &plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]), false);
1205 assert_eq!(out.report.cap_triangles, 0);
1206 assert!(
1207 !boundary_loops(&welded_by_position(&mesh))
1208 .unwrap()
1209 .is_empty()
1210 );
1211 }
1212
1213 #[test]
1214 fn without_uvs_split_vertices_are_read_by_index_as_before() {
1215 let seamed = seamed_tube(16, 3, true);
1218 let (p, t) = crate::test_util::parts(&seamed);
1219 let mut mesh = mesh_of(&p, &t);
1220 let out = cut_kept(&mut mesh, &plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]), true);
1221 assert_eq!(out.report.cap_triangles, 0);
1222 assert!(
1223 !boundary_loops(&welded_by_position(&mesh))
1224 .unwrap()
1225 .is_empty()
1226 );
1227 }
1228
1229 #[test]
1230 fn a_seamless_mesh_with_uvs_caps_exactly_as_one_without() {
1231 let p = plane([0.5, 0.0, 0.0], [1.0, 0.0, 0.0]);
1232 let mut plain = cube();
1233 let mut with_uvs = cube();
1234 with_uvs.add_uvs(&[0.25; 16]).unwrap();
1235 let (a, b) = (
1236 cut_kept(&mut plain, &p, true),
1237 cut_kept(&mut with_uvs, &p, true),
1238 );
1239 assert_eq!(a.report, b.report);
1240 assert_eq!(
1241 crate::test_util::parts(&plain),
1242 crate::test_util::parts(&with_uvs)
1243 );
1244 assert_eq!(
1245 crate::test_util::parts(&a.removed),
1246 crate::test_util::parts(&b.removed)
1247 );
1248 }
1249
1250 #[test]
1251 fn a_crossing_is_the_same_point_from_either_end_when_asked() {
1252 let p = plane([0.1, 0.2, 0.3], [0.3, -0.7, 0.9]);
1253 let at = |i: u32, s: f64| {
1255 let t = f64::from(i);
1256 [
1257 s * (1.0 + (t * 0.37).sin()) / 3.0,
1258 s * (t * 0.11).cos() / 7.0,
1259 s * (2.0 + (t * 0.73).cos()) / 11.0,
1260 ]
1261 };
1262 let mut differ = 0;
1263 for i in 0..1000 {
1264 let (a, b) = (at(i, 1.0), at(i + 1, -1.0));
1265 let (sa, sb) = (p.signed_distance(a), p.signed_distance(b));
1266 if (sa >= 0.0) == (sb >= 0.0) {
1267 continue;
1268 }
1269 assert_eq!(
1270 crossing_point(a, b, sa, sb, true),
1271 crossing_point(b, a, sb, sa, true)
1272 );
1273 differ += usize::from(
1274 crossing_point(a, b, sa, sb, false) != crossing_point(b, a, sb, sa, false),
1275 );
1276 }
1277 assert!(differ > 0);
1279 }
1280
1281 fn seamless(mesh: &Mesh) -> Mesh {
1283 let welded = welded_by_position(mesh);
1284 let (p, t) = crate::test_util::parts(&welded);
1285 mesh_of(&p, &t)
1286 }
1287
1288 #[test]
1289 fn a_sweep_of_planes_never_leaves_a_seamed_mesh_open() {
1290 let meshes: [fn() -> Mesh; 3] = [
1291 || seamed_tube(12, 3, true),
1292 || seamed_tube(7, 2, true),
1293 cube_of_islands,
1294 ];
1295 let origins = [0.0, 0.25, 0.5, 1.0, 1.5, 0.3137];
1297 let normals = [
1298 [1.0, 0.0, 0.0],
1299 [0.0, 1.0, 0.0],
1300 [0.0, 0.0, 1.0],
1301 [0.0, -1.0, 0.0],
1302 [1.0, 1.0, 0.0],
1303 [1.0, 1.0, 1.0],
1304 [0.3, -0.5, 0.8],
1305 [-0.7, 0.2, 0.1],
1306 ];
1307 let (mut cuts, mut refused) = (0, 0);
1308 for make in meshes {
1309 for &o in &origins {
1310 for &n in &normals {
1311 let p = plane(n.map(|c: f64| c * o), n);
1312 let mut mesh = make();
1313 let whole = volume6(&mesh);
1314 let mut tx = mesh.begin_edit();
1315 let r = cut_by_plane(&mut tx, &p, &CutOptions::default());
1316 let Ok(out) = r else {
1317 let err = r.unwrap_err();
1321 if err == OpsError::InvalidInput("a cut rim cannot be capped") {
1322 refused += 1;
1323 } else {
1324 assert_eq!(
1325 err,
1326 OpsError::InvalidInput("plane does not cut the mesh"),
1327 "{o} {n:?}"
1328 );
1329 }
1330 tx.rollback();
1331 continue;
1332 };
1333 tx.commit().unwrap();
1334 cuts += 1;
1335 assert_closed_by_position(&mesh);
1336 assert_closed_by_position(&out.removed);
1337 let sum = volume6(&mesh) + volume6(&out.removed);
1338 assert!(
1339 (sum - whole).abs() < 1e-4 * whole,
1340 "{o} {n:?}: {sum} {whole}"
1341 );
1342 }
1343 }
1344 }
1345 assert!(cuts > 80, "{cuts}");
1346 assert!(refused <= 3, "{refused}");
1347 }
1348
1349 #[test]
1350 fn a_rim_above_the_search_limit_caps_across_a_seam_too() {
1351 let slices = FillOptions::default().dp_limit as u32 + 40;
1354 let tube = seamed_tube(slices, 2, true);
1355 let (mut p, t) = crate::test_util::parts(&tube);
1356 for c in p.as_chunks_mut::<3>().0 {
1357 if c[2] != 1.0 {
1358 c[0] *= 2.0;
1359 c[1] *= 2.0;
1360 }
1361 }
1362 let mut hourglass = mesh_of(&p, &t);
1363 hourglass.add_uvs(tube.uvs().unwrap()).unwrap();
1364 let waist = plane([0.0, 0.0, 1.0], [0.0, 0.0, 1.0]);
1365 let (kept, out) = assert_cut_closes(hourglass, &waist);
1366 assert!(kept.triangle_count() > 2 * slices as usize);
1367 assert!(out.removed.triangle_count() > 2 * slices as usize);
1368 }
1369
1370 fn seamed_sphere(stacks: u32, slices: u32) -> Mesh {
1375 let (mut positions, mut uvs, mut triangles) = (Vec::new(), Vec::new(), Vec::new());
1376 let id = |i: u32, j: u32| 2 + (i - 1) * (slices + 1) + j;
1377 positions.extend([0.0, 0.0, -1.0, 0.0, 0.0, 1.0]);
1378 uvs.extend([0.5, 0.0, 0.5, 1.0]);
1379 for i in 1..stacks {
1380 let v = i as f32 / stacks as f32;
1381 let lat = std::f32::consts::PI * (v - 0.5);
1382 for j in 0..=slices {
1383 let a = std::f32::consts::TAU * (j % slices) as f32 / slices as f32;
1384 positions.extend([lat.cos() * a.cos(), lat.cos() * a.sin(), lat.sin()]);
1385 uvs.extend([j as f32 / slices as f32, v]);
1386 }
1387 }
1388 for j in 0..slices {
1389 triangles.push([0, id(1, j + 1), id(1, j)]);
1390 triangles.push([1, id(stacks - 1, j), id(stacks - 1, j + 1)]);
1391 for i in 1..stacks - 1 {
1392 let (a, b) = (id(i, j), id(i, j + 1));
1393 let (c, d) = (id(i + 1, j + 1), id(i + 1, j));
1394 triangles.extend([[a, b, c], [a, c, d]]);
1395 }
1396 }
1397 let mut mesh = mesh_of(&positions, &triangles);
1398 mesh.add_uvs(&uvs).unwrap();
1399 mesh
1400 }
1401
1402 #[test]
1403 fn seamed_spheres_and_tubes_cap_past_the_search_limit() {
1404 let across = plane([0.0, 0.0, 0.13], [0.0, 0.0, 1.0]);
1405 let tilted = plane([0.0, 0.0, 0.13], [0.2, 0.1, 1.0]);
1406 let along = plane([0.0, 0.0, 0.0], [0.3, 1.0, 0.0]);
1407 let on_seam = plane([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1409 let off_seam = plane([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1410 for slices in [128, 256] {
1411 for p in [&across, &tilted, &along, &on_seam, &off_seam] {
1412 assert_cut_closes(seamed_sphere(32, slices), p);
1413 }
1414 let mid = plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]);
1415 let tilted = plane([0.0, 0.0, 1.5], [0.2, 0.1, 1.0]);
1416 for p in [&mid, &tilted] {
1417 assert_cut_closes(seamed_tube(slices, 3, true), p);
1418 }
1419 }
1420 }
1421
1422 #[test]
1423 fn a_plane_through_a_wall_vertex_caps() {
1424 let mut refused = 0;
1429 for slices in [12, 64, 256] {
1430 for (i, j) in [
1431 (1, 0),
1432 (1, slices / 4),
1433 (2, slices / 2),
1434 (2, 3 * slices / 4),
1435 ] {
1436 let a = std::f32::consts::TAU * j as f32 / slices as f32;
1437 let at = [f64::from(a.cos()), f64::from(a.sin()), f64::from(i)];
1438 for n in [[0.3, -0.5, 0.8], [-0.7, 0.2, 0.1], [0.1, 0.9, -0.2]] {
1439 let mut mesh = seamed_tube(slices, 3, true);
1440 let mut tx = mesh.begin_edit();
1441 let p = plane(at, n);
1442 let r = cut_by_plane(&mut tx, &p, &CutOptions::default());
1443 tx.rollback();
1444 match r {
1445 Ok(_) => {
1446 assert_cut_closes(seamed_tube(slices, 3, true), &p);
1447 }
1448 Err(e) => {
1449 assert_eq!(e, OpsError::InvalidInput("a cut rim cannot be capped"));
1450 refused += 1;
1451 }
1452 }
1453 }
1454 }
1455 }
1456 assert!(refused <= 3, "{refused} of 36");
1457 }
1458
1459 #[test]
1460 fn moving_a_refused_plane_a_little_caps_it() {
1461 let n = [-0.7, 0.2, 0.1];
1463 let moved = plane(n.map(|c: f64| c * 1e-3), n);
1464 assert_cut_closes(seamed_tube(12, 3, true), &moved);
1465 }
1466
1467 #[test]
1468 fn a_rim_of_tens_of_thousands_of_edges_caps_across_a_seam() {
1469 let (kept, out) = assert_cut_closes(
1470 seamed_tube(12_000, 2, true),
1471 &plane([0.0, 0.0, 0.5], [0.0, 0.0, 1.0]),
1472 );
1473 for m in [&kept, &out.removed] {
1475 assert!(m.triangle_count() < 1_000_000, "{}", m.triangle_count());
1476 }
1477 }
1478
1479 #[test]
1480 fn a_seamless_rim_past_the_search_limit_caps_too() {
1481 let p = plane([0.0, 0.0, 0.13], [0.0, 0.0, 1.0]);
1483 let mut plain = seamless(&seamed_sphere(32, 256));
1484 let whole = volume6(&plain);
1485 let out = cut_kept(&mut plain, &p, true);
1486 assert!(boundary_loops(&plain).unwrap().is_empty());
1487 assert!(boundary_loops(&out.removed).unwrap().is_empty());
1488 let sum = volume6(&plain) + volume6(&out.removed);
1489 assert!((sum - whole).abs() < 1e-4 * whole, "{sum} {whole}");
1490 }
1491
1492 fn mouth_on_the_plane_and_a_box(uvs: bool) -> Mesh {
1497 let tube = seamless(&seamed_tube(16, 2, false));
1498 let (mut positions, mut triangles) = crate::test_util::parts(&tube);
1499 let base = (positions.len() / 3) as u32;
1500 let (cube_p, cube_t) = crate::test_util::parts(&cube());
1501 for c in cube_p.as_chunks::<3>().0 {
1502 positions.extend([c[0] + 5.0, c[1], c[2] - 0.5]);
1503 }
1504 triangles.extend(cube_t.iter().map(|t| t.map(|v| v + base)));
1505 let mut mesh = mesh_of(&positions, &triangles);
1506 if uvs {
1507 let values = vec![0.25; 2 * mesh.vertex_slots()];
1508 mesh.add_uvs(&values).unwrap();
1509 }
1510 mesh
1511 }
1512
1513 #[test]
1514 fn a_real_hole_in_the_plane_stays_open_with_or_without_uvs() {
1515 let on_mouth = plane([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
1516 let mut plain = mouth_on_the_plane_and_a_box(false);
1517 let mut with_uvs = mouth_on_the_plane_and_a_box(true);
1518 let (a, b) = (
1519 cut_kept(&mut plain, &on_mouth, true),
1520 cut_kept(&mut with_uvs, &on_mouth, true),
1521 );
1522 assert_eq!(a.report, b.report);
1523 assert_eq!(boundary_loops(&with_uvs).unwrap().len(), 2);
1525 assert_eq!(
1526 boundary_loops(&plain).unwrap().len(),
1527 boundary_loops(&with_uvs).unwrap().len()
1528 );
1529 }
1530
1531 #[test]
1532 fn a_real_hole_with_a_seam_in_the_plane_stays_open() {
1533 let tube = seamed_tube(16, 2, false);
1535 let (mut positions, mut triangles) = crate::test_util::parts(&tube);
1536 let mut uvs = tube.uvs().unwrap().to_vec();
1537 let base = (positions.len() / 3) as u32;
1538 let (cube_p, cube_t) = crate::test_util::parts(&cube());
1539 for c in cube_p.as_chunks::<3>().0 {
1540 positions.extend([c[0] + 5.0, c[1], c[2] - 0.5]);
1541 uvs.extend([0.25, 0.25]);
1542 }
1543 triangles.extend(cube_t.iter().map(|t| t.map(|v| v + base)));
1544 let mut mesh = mesh_of(&positions, &triangles);
1545 mesh.add_uvs(&uvs).unwrap();
1546 let out = cut_kept(&mut mesh, &plane([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]), true);
1547 let mut plain = mouth_on_the_plane_and_a_box(false);
1549 let want = cut_kept(&mut plain, &plane([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]), true);
1550 assert_eq!(out.report.cap_triangles, want.report.cap_triangles);
1551 }
1552}