Skip to main content

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}