fast_mesh_ops/lib.rs
1//! Repair operations on meshes stored in [`fast_mesh_store`].
2//!
3//! Operations read the mesh through a transaction's `tx.mesh()`, build the
4//! maps they need from its triangles, compute and validate their whole plan,
5//! and only then write. Positions are read as the stored `f32` local
6//! coordinates and all geometry is computed in `f64`, without the origin.
7//! Every growable temporary is reserved fallibly, so that running out of
8//! memory is [`OpsError::Store`] with [`StoreError::OutOfMemory`]. Results do
9//! not depend on hash iteration order: every list is sorted explicitly.
10//!
11//! - Edges: an undirected edge is keyed `(min, max)`. It is a boundary edge
12//! when one triangle uses it, interior with two and non-manifold with three
13//! or more ([`EdgeKind`]). Triangles with a repeated vertex id are skipped.
14//! - Components: triangles connect through shared edges of any kind
15//! ([`Component`]).
16//! - Boundary loops: [`boundary_loops`] chains the boundary edges into
17//! [`BoundaryLoop`]s by walking each vertex's triangle fan, so that two holes
18//! touching at one vertex come out as two loops. Each loop has a content
19//! fingerprint, a [`LoopId`], that callers use to pick loops to fill.
20//! - Analysis: [`analyze`] reports a mesh's defects without changing it, as
21//! an [`Analysis`]: loops, coincident vertices, degenerate and duplicate
22//! triangles, non-manifold edges and vertices, pinch vertices, components,
23//! orientation conflicts and, given a BVH, self-intersection.
24//! - Orientation: [`orient()`] makes the winding consistent patch by patch
25//! and turns closed components outward.
26//! - Booleans: [`boolean_plan`] splits every triangle of mesh A that mesh B
27//! crosses (and vice versa) so that each piece lies strictly on one side
28//! of the other mesh, classifies the pieces for [`BooleanOp`] and
29//! [`apply_boolean`] writes the plan, stitching both rims without any
30//! winding assumption. Inputs must be closed, manifold and consistently
31//! wound.
32//! - Offsetting and shells: [`offset`] moves a surface along its smooth
33//! normals by a signed distance, and [`shell`] solidifies a mesh into a
34//! printable shell of wall `thickness` by adding a reversed inner copy and
35//! stitching every rim; both drop pinched triangles and report them.
36//! - Hole filling: [`fill_holes`] closes the boundary loops chosen by size
37//! or by id with triangle patches that agree with their neighbours' winding
38//! and never make an edge non-manifold, refined to the surrounding density
39//! and faired to continue the surrounding curvature. Run [`orient()`]
40//! first on meshes that may contain flipped triangles: a hole whose rim
41//! crosses an orientation seam is not clean and is skipped.
42//! - Auto repair: [`auto_repair()`] runs the standard cleanup pipeline (weld,
43//! remove degenerate, orient, remove small components, fill holes) in one
44//! transaction, with a report per stage.
45//! - Measurement: [`measure`] reports a mesh's surface area (always) and
46//! enclosed volume (when watertight, with the reason otherwise).
47//! - Simplification: [`simplify`](simplify::simplify) shrinks a mesh toward
48//! a target triangle count by quadric-error edge collapses, keeping
49//! boundary vertices in place when asked.
50//! - Subdivision: [`subdivide`](subdivide::subdivide) refines every triangle
51//! into four per level by Loop's scheme, with boundary vertices relaxing
52//! along their own curve and other creases holding their midpoints.
53//! - Registration: [`icp()`] aligns points to a mesh by rigid, point-to-plane
54//! ICP with trimmed outlier rejection, and reports how far it got.
55//! - Brush deformation: [`inflate`](deform::inflate) and
56//! [`smooth`](deform::smooth) move the vertices under a brush along their
57//! smooth normals or toward their neighbours, and [`grab`](deform::grab)
58//! drags them along one vector. [`with_protection()`] holds chosen vertices
59//! back from any of them, per vertex.
60//! - UVs: every operation either keeps a mesh's UV channel aligned or drops
61//! it undoably, never leaving stale values; the per-operation table is in
62//! [`docs/uv-channel.md`](https://github.com/YoonDongGeun/fast-mesh-wasm/blob/main/docs/uv-channel.md).
63
64mod analyze;
65pub mod append;
66mod auto_repair;
67pub mod boolean;
68pub mod bridge;
69mod cleanup;
70mod cut;
71pub mod deform;
72mod error;
73mod fair;
74mod fill;
75mod fill_dp;
76pub mod icp;
77mod measure;
78mod offset;
79mod orient;
80pub mod protect;
81mod refine;
82pub mod remesh;
83pub mod simplify;
84mod sparse;
85pub mod stretch;
86pub mod subdivide;
87#[cfg(test)]
88mod test_util;
89mod topology;
90#[cfg(test)]
91mod uv_table;
92mod weld;
93
94pub use analyze::{Analysis, AnalyzeOptions, AnalyzeOptionsBuilder, analyze};
95pub use append::{AppendReport, AppendSource, append_mesh, append_mesh_with_uvs};
96pub use auto_repair::{
97 AutoRepairOptions, AutoRepairOptionsBuilder, AutoRepairReport, AutoRepairStage, auto_repair,
98};
99pub use boolean::{BooleanOp, BooleanPlan, BooleanReport, apply_boolean, boolean_plan};
100pub use bridge::{BridgeOptions, BridgeOptionsBuilder, BridgeReport, bridge_loops};
101pub use cleanup::{
102 ComponentOptions, ComponentOptionsBuilder, DegenerateOptions, DegenerateOptionsBuilder,
103 DegenerateReport, RemoveReport, WeldReport, remove_degenerate, remove_small_components, weld,
104};
105pub use cut::{CutOptions, CutOutput, CutReport, cut_by_plane};
106pub use deform::{Brush, Falloff, InflateOptions, InflateOptionsBuilder, InflateReport, inflate};
107pub use error::{OpsError, Result};
108pub use fill::{
109 FailReason, FairWeights, FillOptions, FillOptionsBuilder, FillReport, FilledLoop, SkipReason,
110 fill_holes,
111};
112pub use icp::{IcpMotion, IcpOptions, IcpReport, IcpStatus, icp, sample_surface};
113pub use measure::{MeasureReport, VolumeReason, measure, measure_placed};
114pub use offset::{
115 OffsetOptions, OffsetOptionsBuilder, OffsetReport, ShellOptions, ShellOptionsBuilder,
116 ShellReport, offset, shell,
117};
118pub use orient::{OrientReport, orient};
119pub use protect::{check_protection, with_protection};
120pub use topology::{BoundaryLoop, Component, EdgeKind, LoopId, boundary_loops};
121
122use fast_mesh_store::StoreError;
123
124#[cfg(test)]
125thread_local! {
126 /// Test-only fault injection: number of fallible reservations that still succeed.
127 static ALLOCATIONS_LEFT: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
128}
129
130/// Makes the `n`-th next fallible reservation on this thread (0-based) and every one
131/// after it fail with `OutOfMemory`. `None` turns failures off. While armed,
132/// parallel work runs on this thread so the hook sees every reservation.
133#[cfg(test)]
134pub(crate) fn fail_allocations_after(n: Option<usize>) {
135 // Only on arming or disarming: the count must see one `true` per arm.
136 let was = ALLOCATIONS_LEFT.with(|c| c.replace(n)).is_some();
137 if was != n.is_some() {
138 fast_mesh_store::par::set_force_sequential(n.is_some());
139 }
140}
141
142/// Fails with `OutOfMemory` when the test fault hook says so; always succeeds
143/// outside tests.
144pub(crate) fn allocation_allowed() -> Result<()> {
145 #[cfg(test)]
146 {
147 if let Some(n) = ALLOCATIONS_LEFT.with(|c| c.get()) {
148 if n == 0 {
149 return Err(OpsError::Store(StoreError::OutOfMemory));
150 }
151 ALLOCATIONS_LEFT.with(|c| c.set(Some(n - 1)));
152 }
153 }
154 Ok(())
155}
156
157/// Reserves room for `additional` more elements, honouring the test fault hook.
158pub(crate) fn reserve<T>(v: &mut Vec<T>, additional: usize) -> Result<()> {
159 allocation_allowed()?;
160 v.try_reserve(additional)
161 .map_err(|_| OpsError::Store(StoreError::OutOfMemory))
162}
163
164/// Pushes with a fallible reservation.
165pub(crate) fn push<T>(v: &mut Vec<T>, value: T) -> Result<()> {
166 reserve(v, 1)?;
167 v.push(value);
168 Ok(())
169}
170
171/// A vector of `len` copies of `value`, reserved fallibly.
172pub(crate) fn filled<T: Clone>(len: usize, value: T) -> Result<Vec<T>> {
173 let mut v = Vec::new();
174 reserve(&mut v, len)?;
175 v.resize(len, value);
176 Ok(v)
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::test_util::{damaged_sphere, edit, grid_without};
183
184 #[test]
185 fn fault_hook_fails_the_nth_reservation() {
186 let mut v: Vec<u32> = Vec::new();
187 fail_allocations_after(Some(1));
188 assert_eq!(push(&mut v, 1), Ok(()));
189 assert_eq!(
190 push(&mut v, 2),
191 Err(OpsError::Store(StoreError::OutOfMemory))
192 );
193 fail_allocations_after(None);
194 assert_eq!(v, vec![1]);
195 assert_eq!(filled(3, 7u8), Ok(vec![7, 7, 7]));
196 }
197
198 #[test]
199 /// Small inputs to every parallel site of this crate (analyze, weld,
200 /// fairing) take no parallel path.
201 fn small_inputs_stay_sequential() {
202 use fast_mesh_bvh::{BuildOptions, Bvh};
203 use fast_mesh_store::par;
204 let before = par::parallel_entries();
205 let damaged = damaged_sphere();
206 let bvh = Bvh::build(&damaged, &BuildOptions::default()).unwrap();
207 let options = AnalyzeOptions {
208 weld_tolerance: 0.01,
209 self_intersection: Some(&bvh),
210 ..Default::default()
211 };
212 analyze(&damaged, &options).unwrap();
213 let mut mesh = damaged_sphere();
214 edit(&mut mesh, |tx| weld(tx, 0.01));
215 // A 4 x 4 hole, faired.
216 let cells: Vec<(u32, u32)> = (2..6).flat_map(|y| (2..6).map(move |x| (x, y))).collect();
217 let mut mesh = grid_without(8, &cells);
218 let options = FillOptions {
219 max_edges: Some(20),
220 ..FillOptions::default()
221 };
222 let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
223 assert!(r.filled.len() == 1 && r.filled[0].weights.is_some());
224 assert_eq!(par::parallel_entries(), before);
225 }
226}