geometry: a face split along a plane for meshing only (#508)

The face's triangles are cut along the plane, split:<name>:a on its
negative side and :b on the other, both on the face's surface, with the
cut as a new edge between them and the face's edges cut where the plane
crosses them. Applied after the merges; the B-rep is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-16 15:25:26 -04:00
1 parent 59a02b0172
commit e8e3fc647c
5 files changed
+438 -7

No files matched your search

+1 -1
View File
@@ -129,7 +129,7 @@ pub(crate) fn rebuild(doc: &Document) -> Result<Design> {
.map(|(i, name)| Face { name: name.clone(), exact: naming::face_name_exact(b, i), members: Vec::new() })
.collect())
})?;
bodies.push(Shape { name: b.name.clone(), body: b.body.clone(), faces, face_map: Vec::new() });
bodies.push(Shape { name: b.name.clone(), body: b.body.clone(), faces, face_map: Vec::new(), cuts: Vec::new() });
}
// A feature that failed leaves a different solid than SolidCraft's: each one names the bodies
// it would have changed, so opening never hands back a silently different solid.
+402
View File
@@ -0,0 +1,402 @@
//! Virtual topology: a face split in two along its curve through a plane, for meshing only.
//!
//! A partition cuts whole bodies into cells with the kernel; a [`FaceSplit`] splits one face of
//! a body along the curve where a plane crosses it, in what the meshers take from the body (the
//! B-rep is unchanged): the face's triangles are cut along the plane, the pieces on the plane's
//! negative side are `split:<name>:a` and the others `split:<name>:b` (the partition's
//! convention), both on the face's surface, and the cut is a new edge between them, so the
//! meshers put a row of nodes along it. Edges of the face that the plane crosses are cut there
//! too. Splits apply after the merges ([`crate::virtual_faces`]), by persistent names, whenever
//! the design is opened; a merge cannot name a split face.
use std::collections::BTreeMap;
use crate::edges::EDGE_TOLERANCE;
use crate::{Design, Error, Face, FeatureError, Result, Shape, SurfaceMesh};
/// Most splits a design takes.
pub const MAX_SPLITS: usize = 1024;
/// A face of a body split by a plane.
#[derive(Debug, Clone, PartialEq)]
pub struct FaceSplit {
/// Names the two faces (`split:<name>:a|b`).
pub name: String,
pub body: String,
pub face: String,
/// A point on the plane and its normal (any length but zero).
pub point: [f64; 3],
pub normal: [f64; 3],
}
/// One split as a body applies it: the face it cuts (kept as the `a` side), the `b` face's
/// index, and the plane (unit normal).
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Cut {
a: u32,
b: u32,
point: [f64; 3],
normal: [f64; 3],
}
impl Cut {
pub(crate) fn faces(&self) -> (u32, u32) {
(self.a, self.b)
}
}
fn signed(c: &Cut, p: [f64; 3]) -> f64 {
(0..3).map(|k| (p[k] - c.point[k]) * c.normal[k]).sum()
}
impl Design {
/// The design with `splits` applied in order (after its partitions and merges). A split that
/// fails (a body or face that is not there, a plane that does not cross the face) is listed
/// in [`Design::errors`] (feature `split <name>`) and changes nothing.
pub fn split(mut self, splits: &[FaceSplit]) -> Result<Design> {
if splits.len() > MAX_SPLITS {
return Err(Error::Invalid(format!("a design takes at most {MAX_SPLITS} face splits")));
}
for sp in splits {
let feature = format!("split {}", sp.name);
let Some(body) = self.bodies.iter_mut().find(|b| b.name == sp.body) else {
self.errors.push(FeatureError { feature, message: format!("no body `{}`", sp.body), bodies: vec![sp.body.clone()] });
continue;
};
match body.split_face(&sp.name, &sp.face, sp.point, sp.normal) {
Ok(split) => *body = split,
Err(e) => self.errors.push(FeatureError { feature, message: e.to_string(), bodies: vec![sp.body.clone()] }),
}
}
if self.bodies.iter().any(|b| !b.cuts.is_empty()) {
let opened = self.source.iter().map(|s| self.bodies.iter().find(|b| b.name == s.name).unwrap_or(s).clone()).collect();
self.opened = std::sync::Arc::new(opened);
}
Ok(self)
}
}
impl Shape {
/// This shape with face `face` split along the plane through `point` normal to `normal`:
/// `split:<name>:a` on its negative side, `split:<name>:b` on the other.
pub fn split_face(&self, name: &str, face: &str, point: [f64; 3], normal: [f64; 3]) -> Result<Shape> {
let len = normal.iter().map(|v| v * v).sum::<f64>().sqrt();
if !(point.iter().chain(&normal).all(|v| v.is_finite() && v.abs() <= 1e9) && len > 1e-12) {
return Err(Error::Invalid("a split needs a plane with a finite point and a non-zero normal".into()));
}
let named = self.faces_named(face);
let [a] = named.as_slice() else {
return Err(Error::Invalid(format!("body `{}` has no face `{face}` (or several pieces of it: name one)", self.name)));
};
let (an, bn) = (format!("split:{name}:a"), format!("split:{name}:b"));
if self.faces.iter().any(|f| f.name == an || f.name == bn) {
return Err(Error::Invalid(format!("body `{}` already has the faces of a split `{name}`", self.name)));
}
let b = u32::try_from(self.faces.len()).map_err(|_| Error::Invalid("too many faces".into()))?;
let mut shape = self.clone();
let original = shape.faces.get(*a as usize).cloned().ok_or_else(|| Error::Invalid("no such face".into()))?;
let members = if original.members.is_empty() { vec![original.name.clone()] } else { original.members.clone() };
if let Some(f) = shape.faces.get_mut(*a as usize) {
*f = Face { name: an, exact: original.exact, members: members.clone() };
}
shape.faces.push(Face { name: bn, exact: original.exact, members });
shape.cuts.push(Cut { a: *a, b, point, normal: normal.map(|v| v / len) });
// The plane must cross the face: both pieces have triangles.
let mesh = shape.tessellate((self.size()? * EDGE_TOLERANCE).max(1e-4))?;
for side in [*a, b] {
if !mesh.tri_face.contains(&side) {
return Err(Error::Invalid(format!("the plane does not cross face `{face}` of body `{}`", self.name)));
}
}
Ok(shape)
}
}
/// `mesh` with each of `cuts` applied in order (see the module docs).
pub(crate) fn apply(mesh: &mut SurfaceMesh, cuts: &[Cut]) {
for c in cuts {
cut(mesh, c);
}
}
fn lerp(p: [f64; 3], q: [f64; 3], t: f64) -> [f64; 3] {
[p[0] + t * (q[0] - p[0]), p[1] + t * (q[1] - p[1]), p[2] + t * (q[2] - p[2])]
}
fn cut(mesh: &mut SurfaceMesh, c: &Cut) {
let scale = mesh.positions.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs())).max(1.0);
let eps = 1e-12 * scale;
let side = |d: f64| {
if d > eps {
1
} else if d < -eps {
-1
} else {
0
}
};
// A new point on the side (u, v) where the plane crosses it, once per side.
let mut made: BTreeMap<(u32, u32), u32> = BTreeMap::new();
let mut segments: Vec<(u32, u32)> = Vec::new();
let n = mesh.triangles.len();
for t in 0..n {
let (Some(&tri), Some(&tag)) = (mesh.triangles.get(t), mesh.tri_face.get(t)) else { continue };
if tag != c.a {
continue;
}
let pos = |i: u32, m: &SurfaceMesh| m.positions.get(i as usize).copied().unwrap_or([0.0; 3]);
let s = tri.map(|i| side(signed(c, pos(i, mesh))));
let tag_of = |sg: i32| if sg > 0 { c.b } else { c.a };
if s.iter().all(|&x| x >= 0) || s.iter().all(|&x| x <= 0) {
let sg = s.iter().copied().find(|&x| x != 0).unwrap_or(-1);
if let Some(f) = mesh.tri_face.get_mut(t) {
*f = tag_of(sg);
}
// A side on the plane between pieces of both sides is the cut too (picked up when
// chaining from the other triangles' points; left to the edges otherwise).
continue;
}
let mut point_on = |u: u32, v: u32, m: &mut SurfaceMesh| -> u32 {
let key = (u.min(v), u.max(v));
if let Some(&i) = made.get(&key) {
return i;
}
let (p, q) = (pos(key.0, m), pos(key.1, m));
let (dp, dq) = (signed(c, p), signed(c, q));
let x = lerp(p, q, dp / (dp - dq));
let i = u32::try_from(m.positions.len()).unwrap_or(u32::MAX);
m.positions.push(x);
made.insert(key, i);
i
};
let rot = |k: usize| [tri[k], tri[(k + 1) % 3], tri[(k + 2) % 3]];
let rs = |k: usize| [s[k], s[(k + 1) % 3], s[(k + 2) % 3]];
let mut out: Vec<([u32; 3], i32)> = Vec::with_capacity(3);
if let Some(k) = (0..3).find(|&k| s[k] == 0) {
// A corner on the plane, the other two on either side.
let ([z, u, v], [_, su, sv]) = (rot(k), rs(k));
let m = point_on(u, v, mesh);
out.push(([z, u, m], su));
out.push(([z, m, v], sv));
segments.push((z, m));
} else {
// One corner alone on its side.
let k = (0..3).find(|&k| s[k] != s[(k + 1) % 3] && s[k] != s[(k + 2) % 3]).unwrap_or(0);
let ([l, u, v], [sl, su, _]) = (rot(k), rs(k));
let (pu, pv) = (point_on(l, u, mesh), point_on(l, v, mesh));
out.push(([l, pu, pv], sl));
out.push(([pu, u, v], su));
out.push(([pu, v, pv], su));
segments.push((pu, pv));
}
let mut first = true;
for (tri, sg) in out {
if first {
if let (Some(slot), Some(f)) = (mesh.triangles.get_mut(t), mesh.tri_face.get_mut(t)) {
*slot = tri;
*f = tag_of(sg);
}
first = false;
} else {
mesh.triangles.push(tri);
mesh.tri_face.push(tag_of(sg));
}
}
}
// The face's edges: cut where the plane crosses them, each piece on its side's face.
let mut edges = Vec::with_capacity(mesh.edges.len() + 1);
let mut edge_faces = Vec::with_capacity(mesh.edges.len() + 1);
for (points, faces) in std::mem::take(&mut mesh.edges).into_iter().zip(std::mem::take(&mut mesh.edge_faces)) {
if !faces.contains(&c.a) {
edges.push(points);
edge_faces.push(faces);
continue;
}
let mut piece: Vec<[f64; 3]> = Vec::new();
let mut piece_side = 0;
let mut flush = |piece: &mut Vec<[f64; 3]>, sg: i32| {
if piece.len() >= 2 {
let on = if sg > 0 { c.b } else { c.a };
edges.push(std::mem::take(piece));
edge_faces.push(faces.iter().map(|&f| if f == c.a { on } else { f }).collect());
}
piece.clear();
};
for (k, &p) in points.iter().enumerate() {
let sp = side(signed(c, p));
if k > 0
&& let Some(&q) = points.get(k - 1)
{
let sq = side(signed(c, q));
if sp != 0 && sq != 0 && sp != sq {
let (dq, dp) = (signed(c, q), signed(c, p));
let x = lerp(q, p, dq / (dq - dp));
piece.push(x);
flush(&mut piece, piece_side);
piece.push(x);
}
}
if sp != 0 {
piece_side = sp;
}
piece.push(p);
}
flush(&mut piece, piece_side);
}
// The cut: its segments chained into polylines, between the two faces. Copies of one point
// (a tessellation may repeat points) are one point for the chaining.
let close = (2.0 * mesh.weld_distance).max(eps);
let mut canon: BTreeMap<u32, u32> = BTreeMap::new();
let mut reps: Vec<(u32, [f64; 3])> = Vec::new();
for &i in segments.iter().flat_map(|(u, v)| [u, v]) {
if canon.contains_key(&i) {
continue;
}
let p = mesh.positions.get(i as usize).copied().unwrap_or([0.0; 3]);
let r = match reps.iter().find(|(_, q)| (0..3).map(|k| (p[k] - q[k]).powi(2)).sum::<f64>().sqrt() <= close) {
Some((r, _)) => *r,
None => {
reps.push((i, p));
i
}
};
canon.insert(i, r);
}
let segments: Vec<(u32, u32)> =
segments.iter().map(|(u, v)| (canon.get(u).copied().unwrap_or(*u), canon.get(v).copied().unwrap_or(*v))).filter(|(u, v)| u != v).collect();
for line in chain(&segments) {
let points: Vec<[f64; 3]> = line.iter().filter_map(|&i| mesh.positions.get(i as usize).copied()).collect();
if points.len() >= 2 {
edges.push(points);
edge_faces.push(vec![c.a, c.b]);
}
}
mesh.edges = edges;
mesh.edge_faces = edge_faces;
}
/// Segments (point index pairs) chained end to end into polylines, in a fixed order.
fn chain(segments: &[(u32, u32)]) -> Vec<Vec<u32>> {
let mut at: BTreeMap<u32, Vec<usize>> = BTreeMap::new();
for (i, &(u, v)) in segments.iter().enumerate() {
at.entry(u).or_default().push(i);
at.entry(v).or_default().push(i);
}
let mut used = vec![false; segments.len()];
let mut lines = Vec::new();
// Start at ends (points on one segment) first, then what is left (closed loops).
let ends: Vec<usize> = at.values().filter(|s| s.len() == 1).flatten().copied().collect();
let starts: Vec<usize> = ends.into_iter().chain(0..segments.len()).collect();
for s in starts {
if used.get(s).copied().unwrap_or(true) {
continue;
}
let Some(&(u, v)) = segments.get(s) else { continue };
// From an end segment, start at its free point.
let (mut line, mut last) = if at.get(&v).is_some_and(|x| x.len() == 1) { (vec![v, u], u) } else { (vec![u, v], v) };
if let Some(x) = used.get_mut(s) {
*x = true;
}
while let Some(&next) = at.get(&last).and_then(|x| x.iter().find(|&&i| !used.get(i).copied().unwrap_or(true))) {
if let Some(x) = used.get_mut(next) {
*x = true;
}
let Some(&(a, b)) = segments.get(next) else { break };
last = if a == last { b } else { a };
line.push(last);
}
lines.push(line);
}
lines
}
#[cfg(test)]
mod tests {
use crate::open_design;
use crate::sample::{self, Plate};
use super::*;
/// The plate's top (z = 10, 40 × 30 with the Ø10 hole at (15, 15)) split at x = 30: two
/// faces on the top's plane, a straight cut edge between them along x = 30, the top's rim
/// edges at y = 0 and y = 30 cut there; triangles all on one side of the plane each.
#[test]
fn the_plates_top_splits_along_a_plane() {
let d = open_design(&sample::plate_design(&Plate::default()).unwrap()).unwrap();
let body = d.bodies[0].name.clone();
let b = &d.bodies[0];
let surfaces = b.face_surfaces().unwrap();
let top = b
.faces()
.iter()
.zip(&surfaces)
.find(|(_, s)| matches!(s, Some(crate::FaceSurface::Plane { normal, point }) if normal[2] > 0.5 && point[2] > 9.0))
.unwrap()
.0
.name
.clone();
let split = |x: f64| FaceSplit { name: "S".into(), body: body.clone(), face: top.clone(), point: [x, 0.0, 0.0], normal: [1.0, 0.0, 0.0] };
let d = d.split(&[split(30.0)]).unwrap();
assert!(d.errors.is_empty(), "{:?}", d.errors);
let b = &d.bodies[0];
assert_eq!(b.faces().len(), 9);
let (fa, fb) = (b.face_index("split:S:a").unwrap(), b.face_index("split:S:b").unwrap());
let s = b.face_surfaces().unwrap();
assert_eq!(s[fa as usize], s[fb as usize]);
let mesh = b.tessellate(0.01).unwrap();
for (t, &f) in mesh.triangles.iter().zip(&mesh.tri_face) {
let x = t.map(|i| mesh.positions[i as usize][0]);
if f == fa {
assert!(x.iter().all(|&v| v <= 30.0 + 1e-9), "{x:?}");
} else if f == fb {
assert!(x.iter().all(|&v| v >= 30.0 - 1e-9), "{x:?}");
}
}
// The area is kept: 40 × 30 less the hole, split 30 × 30 − π25 and 10 × 30.
let area = |f: u32| -> f64 {
mesh.triangles
.iter()
.zip(&mesh.tri_face)
.filter(|(_, g)| **g == f)
.map(|(t, _)| {
let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
let (u, v) = ([b[0] - a[0], b[1] - a[1], b[2] - a[2]], [c[0] - a[0], c[1] - a[1], c[2] - a[2]]);
0.5 * ((u[1] * v[2] - u[2] * v[1]).powi(2) + (u[2] * v[0] - u[0] * v[2]).powi(2) + (u[0] * v[1] - u[1] * v[0]).powi(2)).sqrt()
})
.sum()
};
assert!((area(fb) - 300.0).abs() < 1e-6, "{}", area(fb));
assert!((area(fa) - (900.0 - std::f64::consts::PI * 25.0)).abs() < 0.5, "{}", area(fa));
let edges = b.edges().unwrap();
let cut: Vec<_> = edges.iter().filter(|e| e.faces.contains(&fa) && e.faces.contains(&fb)).collect();
assert_eq!(cut.len(), 1, "{:?}", edges.iter().map(|e| &e.name).collect::<Vec<_>>());
assert!((cut[0].length() - 30.0).abs() < 1e-9 && cut[0].points.iter().all(|p| (p[0] - 30.0).abs() < 1e-9));
// The top's rim edges along x are cut at x = 30: four pieces where there were two.
let rim = edges.iter().filter(|e| e.faces.iter().any(|&f| f == fa || f == fb) && !(e.faces.contains(&fa) && e.faces.contains(&fb))).count();
assert_eq!(rim, 2 + 2 + 2 + 2, "four sides, the two along x in two pieces, and the hole's two rim arcs");
}
#[test]
fn bad_splits_are_listed() {
let d = open_design(&sample::plate_design(&Plate::default()).unwrap()).unwrap();
let body = d.bodies[0].name.clone();
let face = d.bodies[0].faces()[0].name.clone();
let sp = |n: &str, b: &str, f: &str, x: f64, normal: [f64; 3]| FaceSplit {
name: n.into(),
body: b.into(),
face: f.into(),
point: [x, 0.0, 0.0],
normal,
};
let d = d
.split(&[
sp("Far", &body, &face, 1000.0, [1.0, 0.0, 0.0]),
sp("NoFace", &body, "nope", 20.0, [1.0, 0.0, 0.0]),
sp("NoBody", "x", &face, 20.0, [1.0, 0.0, 0.0]),
sp("Zero", &body, &face, 20.0, [0.0; 3]),
])
.unwrap();
assert_eq!(d.errors.len(), 4, "{:?}", d.errors);
assert_eq!(d.bodies[0].faces().len(), 8);
}
}
+5 -2
View File
@@ -20,6 +20,7 @@
pub mod check;
mod design;
mod edges;
pub mod face_split;
pub mod features;
pub mod partition;
pub mod sample;
@@ -70,6 +71,8 @@ pub struct Shape {
faces: Vec<Face>,
/// The virtual face (index into `faces`) of each B-rep face; empty: they are the same.
face_map: Vec<u32>,
/// Faces split along a plane ([`face_split`]), applied after the merges, in order.
cuts: Vec<face_split::Cut>,
}
/// A B-rep face's persistent identity.
@@ -156,7 +159,7 @@ fn xyz(v: solidcraft_geom::Vec3) -> [f64; 3] {
pub fn block(a: [f64; 3], b: [f64; 3]) -> Result<Shape> {
let v = |p: [f64; 3]| solidcraft_geom::Vec3 { x: p[0], y: p[1], z: p[2] };
let body = guard("block", || solidcraft_kernel::box_solid(v(a), v(b)))?;
Ok(Shape { name: "Block".into(), body, faces: Vec::new(), face_map: Vec::new() })
Ok(Shape { name: "Block".into(), body, faces: Vec::new(), face_map: Vec::new(), cuts: Vec::new() })
}
impl Shape {
@@ -246,7 +249,7 @@ impl Shape {
centroid: xyz(m.centroid),
bbox_min: xyz(m.bbox.min),
bbox_max: xyz(m.bbox.max),
faces: if self.face_map.is_empty() { m.faces } else { self.faces.len() },
faces: if self.face_map.is_empty() && self.cuts.is_empty() { m.faces } else { self.faces.len() },
})
}
+1 -1
View File
@@ -277,7 +277,7 @@ fn split(body: &Shape, plane: &Plane, n: [f64; 3], cut: &Cut) -> Result<Split> {
let parent = Faces::of(body, tol)?;
let mut cells: Vec<(bool, [f64; 3], Shape)> = Vec::with_capacity(parts.len());
for part in parts {
let mut shape = Shape { name: String::new(), body: part, faces: Vec::new(), face_map: Vec::new() };
let mut shape = Shape { name: String::new(), body: part, faces: Vec::new(), face_map: Vec::new(), cuts: Vec::new() };
let m = shape.measure()?;
let negative = dot(sub(m.centroid, cut.point), n) < 0.0;
shape.faces = (0..m.faces).map(|_| Face { name: String::new(), exact: false, members: Vec::new() }).collect();
+29 -3
View File
@@ -50,7 +50,7 @@ pub fn merged_face_name(name: &str) -> String {
/// Whether a feature error comes from the model's partitions or merges (applied again from the
/// design as opened) rather than from the design's own timeline.
pub(crate) fn derived(feature: &str) -> bool {
feature.starts_with("partition ") || feature.starts_with("merge ")
feature.starts_with("partition ") || feature.starts_with("merge ") || feature.starts_with("split ")
}
impl Design {
@@ -119,6 +119,9 @@ impl Shape {
if faces.len() > MAX_MERGE_FACES {
return Err(Error::Invalid(format!("a merge takes at most {MAX_MERGE_FACES} faces")));
}
if !self.cuts.is_empty() {
return Err(Error::Invalid("merges come before splits: a merge cannot take a split face".into()));
}
let mut members = BTreeSet::new();
for f in faces {
let found = self.faces_named(f);
@@ -162,7 +165,7 @@ impl Shape {
}
let brep = if self.face_map.is_empty() { (0..u32::try_from(self.faces.len()).unwrap_or(0)).collect() } else { self.face_map.clone() };
let face_map = brep.iter().map(|&v| renumber.get(v as usize).copied().unwrap_or(v)).collect();
Ok(Shape { name: self.name.clone(), body: self.body.clone(), faces: kept, face_map })
Ok(Shape { name: self.name.clone(), body: self.body.clone(), faces: kept, face_map, cuts: Vec::new() })
}
/// The neighbour face `face` shares the longest edges with (by name): where a tiny or sliver
@@ -214,6 +217,8 @@ impl Shape {
/// for a free-form one.
pub fn member_surfaces(&self, face: u32) -> Result<Vec<Option<FaceSurface>>> {
let brep = self.brep_face_surfaces()?;
// The b side of a split is made of the face it was split from.
let face = self.cuts.iter().rev().fold(face, |f, c| if c.faces().1 == f { c.faces().0 } else { f });
if self.face_map.is_empty() {
return Ok(brep.get(face as usize).copied().into_iter().collect());
}
@@ -223,6 +228,21 @@ impl Shape {
/// `brep` (surfaces by B-rep face) by face: a merged face has its B-rep faces' surface when
/// they share one, else none.
pub(crate) fn virtual_surfaces(&self, brep: Vec<Option<FaceSurface>>) -> Result<Vec<Option<FaceSurface>>> {
let mut out = self.merged_surfaces(brep)?;
for c in &self.cuts {
let (a, b) = c.faces();
let s = out.get(a as usize).copied().flatten();
if out.len() <= b as usize {
out.resize(b as usize + 1, None);
}
if let Some(slot) = out.get_mut(b as usize) {
*slot = s;
}
}
Ok(out)
}
fn merged_surfaces(&self, brep: Vec<Option<FaceSurface>>) -> Result<Vec<Option<FaceSurface>>> {
if self.face_map.is_empty() {
return Ok(brep);
}
@@ -243,7 +263,13 @@ impl Shape {
/// `mesh` (a tessellation of the B-rep) with the merged faces: triangles tagged by face,
/// the edges inside a merged face dropped and the pieces of an edge it leaves joined.
pub(crate) fn virtual_mesh(&self, mut mesh: SurfaceMesh) -> SurfaceMesh {
pub(crate) fn virtual_mesh(&self, mesh: SurfaceMesh) -> SurfaceMesh {
let mut mesh = self.merged_mesh(mesh);
crate::face_split::apply(&mut mesh, &self.cuts);
mesh
}
fn merged_mesh(&self, mut mesh: SurfaceMesh) -> SurfaceMesh {
if self.face_map.is_empty() {
return mesh;
}