Skip to main content

axiolid_refine/
lib.rs

1//! Mesh refinement: more triangles, and optionally closer to the truth.
2//!
3//! Splitting a triangle is easy. The question this module exists to answer
4//! is *where the new vertex goes*.
5//!
6//! A mesh-only kernel has one option: the edge midpoint. That subdivides
7//! the approximation without improving it -- refining a faceted cylinder
8//! forever leaves the same faceted cylinder, with more triangles.
9//!
10//! When the mesh came from a tessellated B-rep and the source surface is
11//! still known, there is a better answer: invert the midpoint into the
12//! surface's parameter domain and evaluate the surface there. The new
13//! vertex lands on the ACTUAL cylinder. Refinement then converges on the
14//! real geometry instead of preserving a facet forever.
15//!
16//! Keeping the analytic surface alongside the mesh is what makes that
17//! possible, so it is the capability this module is really for.
18//!
19//! The crate also holds the operations that add or move vertices on a
20//! [`HalfedgeMesh`](axiolid_mesh::halfedge::HalfedgeMesh) to make a surface
21//! smoother rather than closer to a known one: [`fill_hole`] (triangulate,
22//! refine and fair a hole), [`fair()`] (solve for the smoothest placement of
23//! chosen vertices) and [`subdivide()`] (Loop and Catmull-Clark subdivision
24//! surfaces).
25
26pub mod fair;
27pub mod hole;
28pub mod remesh;
29pub mod smooth;
30pub mod subdivide;
31
32pub use fair::{fair, FairError, FairOptions, FairReport, FairingOrder, FairingWeights};
33pub use hole::{fill_hole, HoleFillError, HoleFillOptions, HoleFillReport};
34pub use subdivide::{
35    limit_positions, subdivide, SubdivisionError, SubdivisionOptions, SubdivisionScheme,
36};
37
38use ahash::AHashMap;
39
40use axiolid_core::{Point3, Scalar, Tolerance};
41use axiolid_mesh::{AttributeFate, TriMesh};
42use axiolid_surface::Surface;
43
44/// Why a refinement could not be performed.
45#[derive(Debug, thiserror::Error, PartialEq)]
46#[non_exhaustive]
47pub enum RefineError {
48    /// The index buffer is not a whole number of triangles.
49    #[error("index buffer length {0} is not a multiple of 3")]
50    RaggedIndices(usize),
51    /// A triangle references a vertex that does not exist.
52    #[error("triangle {0} references vertex {1}, which is out of range")]
53    IndexOutOfRange(usize, u32),
54    /// An edge-length target must be a positive, finite length.
55    #[error("edge length target {0} is not a positive finite length")]
56    InvalidTarget(Scalar),
57    /// A surface refused to place a projected vertex.
58    ///
59    /// Propagated rather than absorbed. Falling back to the linear midpoint
60    /// would return four times the triangles with none of the promised
61    /// accuracy, and the caller could not tell the difference.
62    #[error("surface-aware refinement refused: {0}")]
63    SurfaceRefused(String),
64    /// Refinement would exceed the triangle budget.
65    ///
66    /// Reported rather than silently truncated: a caller that asked for a
67    /// 1mm edge on a building-sized model wants to know its request was
68    /// impossible, not receive a partially refined mesh that looks fine.
69    #[error("refinement would produce {produced} triangles, over the {limit} budget")]
70    BudgetExceeded {
71        /// Triangles the request would have produced.
72        produced: usize,
73        /// The cap that was not raised.
74        limit: usize,
75    },
76}
77
78/// How much to refine.
79#[derive(Debug, Clone, Copy, PartialEq)]
80#[non_exhaustive]
81pub enum RefineTarget {
82    /// Split every triangle into four, `levels` times.
83    Uniform {
84        /// Number of subdivision passes.
85        levels: u32,
86    },
87    /// Split edges until none is longer than this.
88    EdgeLength {
89        /// Maximum permitted edge length, in model units.
90        max_edge: Scalar,
91    },
92}
93
94/// What a refinement actually did.
95///
96/// `max_deviation` is the honest part. A planar refinement must report
97/// exactly zero: a midpoint on a flat triangle lies in that triangle's
98/// plane, so any movement means a defect. A surface-aware refinement
99/// reports how far it MOVED the surface toward the analytic one, which is
100/// the measure of what the caller gained.
101#[derive(Debug, Clone, PartialEq)]
102#[non_exhaustive]
103pub struct RefineReport {
104    /// Triangles before.
105    pub input_triangles: usize,
106    /// Triangles after.
107    pub output_triangles: usize,
108    /// Vertices introduced.
109    pub vertices_added: usize,
110    /// Whether new vertices were placed on an analytic surface.
111    ///
112    /// `false` means linear midpoints: the result is a finer tessellation
113    /// of the same approximation, not a better approximation.
114    pub surface_aware: bool,
115    /// Largest distance a new vertex sits from the linear midpoint it
116    /// would otherwise have occupied, in model units.
117    ///
118    /// Zero for planar input even when surface-aware, because a plane's
119    /// midpoint already lies on the plane.
120    pub max_deviation: Scalar,
121    /// What happened to each named attribute channel.
122    pub attribute_fates: Vec<(String, AttributeFate)>,
123}
124
125impl RefineReport {
126    /// Whether the mesh was left untouched.
127    pub fn is_noop(&self) -> bool {
128        self.vertices_added == 0
129    }
130}
131
132/// Cap on output size, mirroring the budget discipline used elsewhere.
133const MAX_TRIANGLES: usize = 20_000_000;
134
135fn validate(mesh: &TriMesh) -> Result<(), RefineError> {
136    if !mesh.indices.len().is_multiple_of(3) {
137        return Err(RefineError::RaggedIndices(mesh.indices.len()));
138    }
139    let vertex_count = mesh.positions.len();
140    for (triangle, chunk) in mesh.indices.as_chunks::<3>().0.iter().enumerate() {
141        for &index in chunk {
142            if index as usize >= vertex_count {
143                return Err(RefineError::IndexOutOfRange(triangle, index));
144            }
145        }
146    }
147    Ok(())
148}
149
150/// Refine a mesh, optionally snapping new vertices onto a known surface.
151///
152/// Passing `Some(surface)` is what turns subdivision into approximation
153/// improvement. Passing `None` subdivides linearly and says so in the
154/// report rather than implying an accuracy gain it did not deliver.
155///
156/// Deterministic: new vertices are numbered in the order edges are first
157/// split during the triangle walk, which is fixed by the index buffer. The
158/// midpoint cache is keyed by the ordered vertex pair and is only ever
159/// queried by key, never iterated, so its internal ordering cannot reach
160/// the output. The same input produces the same output vertex ordering on
161/// every run and across processes.
162///
163/// # Errors
164///
165/// Refuses a ragged index buffer, out-of-range indices, a non-positive
166/// edge-length target, and a request that would exceed the triangle budget.
167pub fn refine(
168    mesh: &TriMesh,
169    target: RefineTarget,
170    surface: Option<&Surface>,
171    tolerance: Tolerance,
172) -> Result<(TriMesh, RefineReport), RefineError> {
173    validate(mesh)?;
174
175    let levels = match target {
176        RefineTarget::Uniform { levels } => levels,
177        RefineTarget::EdgeLength { max_edge } => {
178            if !max_edge.is_finite() || max_edge <= 0.0 {
179                return Err(RefineError::InvalidTarget(max_edge));
180            }
181            passes_for_edge_length(mesh, max_edge)
182        }
183    };
184
185    let input_triangles = mesh.triangle_count();
186    // Each pass quadruples the triangle count. Checking the projection up
187    // front turns an out-of-memory kill into a typed refusal.
188    let projected = input_triangles.saturating_mul(4usize.saturating_pow(levels));
189    if projected > MAX_TRIANGLES {
190        return Err(RefineError::BudgetExceeded {
191            produced: projected,
192            limit: MAX_TRIANGLES,
193        });
194    }
195
196    let mut positions = mesh.positions.clone();
197    let mut indices = mesh.indices.clone();
198    let mut max_deviation: Scalar = 0.0;
199
200    for _ in 0..levels {
201        let mut midpoints: AHashMap<(u32, u32), u32> = AHashMap::new();
202        let mut next = Vec::with_capacity(indices.len() * 4);
203
204        for chunk in indices.as_chunks::<3>().0 {
205            let [a, b, c] = [chunk[0], chunk[1], chunk[2]];
206            let ab = split_edge(
207                a,
208                b,
209                &mut positions,
210                &mut midpoints,
211                surface,
212                tolerance,
213                &mut max_deviation,
214            )?;
215            let bc = split_edge(
216                b,
217                c,
218                &mut positions,
219                &mut midpoints,
220                surface,
221                tolerance,
222                &mut max_deviation,
223            )?;
224            let ca = split_edge(
225                c,
226                a,
227                &mut positions,
228                &mut midpoints,
229                surface,
230                tolerance,
231                &mut max_deviation,
232            )?;
233
234            // Four children, each wound the same way as the parent so the
235            // result keeps the input's orientation.
236            next.extend_from_slice(&[a, ab, ca]);
237            next.extend_from_slice(&[ab, b, bc]);
238            next.extend_from_slice(&[ca, bc, c]);
239            next.extend_from_slice(&[ab, bc, ca]);
240        }
241        indices = next;
242    }
243
244    let vertices_added = positions.len() - mesh.positions.len();
245    let mut out = TriMesh::new(positions, indices);
246    out.normals = None;
247
248    // With no vertex created (zero levels, or no triangles to split) the
249    // geometry is the input's, so its channels and normals are still exact
250    // and are returned, not merely reported as surviving. Before this the
251    // report said `Preserved` while the mesh came back without the channel.
252    if vertices_added == 0 {
253        out.normals = mesh.normals.clone();
254        out.attributes = mesh.attributes.clone();
255    }
256
257    // A refinement creates vertices, so a channel survives only if its own
258    // blend rule permits deriving a value. Unlike a boolean cut, the new
259    // vertex HAS a preimage: it sits on a known edge between two vertices,
260    // so a blendable channel is genuinely interpolatable here.
261    let attribute_fates = mesh
262        .attributes
263        .iter()
264        .map(|channel| {
265            let fate = match channel.blend {
266                // Checked first: nothing was derived, so even a channel that
267                // forbids derivation came through untouched.
268                _ if vertices_added == 0 => AttributeFate::Preserved,
269                axiolid_mesh::Blend::None => {
270                    AttributeFate::Dropped(axiolid_mesh::DropReason::NotBlendable)
271                }
272                _ => AttributeFate::Dropped(axiolid_mesh::DropReason::ProviderLimitation),
273            };
274            (channel.name.clone(), fate)
275        })
276        .collect();
277
278    let report = RefineReport {
279        input_triangles,
280        output_triangles: out.triangle_count(),
281        vertices_added,
282        surface_aware: surface.is_some(),
283        max_deviation,
284        attribute_fates,
285    };
286    Ok((out, report))
287}
288
289/// Number of uniform passes needed to bring every edge under `max_edge`.
290///
291/// Each pass halves every edge, so the requirement is
292/// `longest / 2^n <= max_edge`. Computed rather than iterated so the
293/// budget check can happen before any memory is allocated.
294fn passes_for_edge_length(mesh: &TriMesh, max_edge: Scalar) -> u32 {
295    let mut longest: Scalar = 0.0;
296    for chunk in mesh.indices.as_chunks::<3>().0 {
297        for (from, to) in [(0, 1), (1, 2), (2, 0)] {
298            let a = mesh.positions[chunk[from] as usize];
299            let b = mesh.positions[chunk[to] as usize];
300            longest = longest.max((b - a).length());
301        }
302    }
303    if longest <= max_edge || !longest.is_finite() {
304        return 0;
305    }
306    (longest / max_edge).log2().ceil().max(0.0) as u32
307}
308
309/// Return the vertex splitting an edge, creating it on first encounter.
310///
311/// The edge key is ordered so both adjacent triangles find the same
312/// midpoint. Without that the mesh would crack along every shared edge.
313fn split_edge(
314    a: u32,
315    b: u32,
316    positions: &mut Vec<Point3>,
317    midpoints: &mut AHashMap<(u32, u32), u32>,
318    surface: Option<&Surface>,
319    tolerance: Tolerance,
320    max_deviation: &mut Scalar,
321) -> Result<u32, RefineError> {
322    let key = if a < b { (a, b) } else { (b, a) };
323    if let Some(&existing) = midpoints.get(&key) {
324        return Ok(existing);
325    }
326
327    let linear = positions[a as usize].midpoint(positions[b as usize]);
328    let placed = match surface {
329        // PROJECT, not invert. The midpoint of a chord across a faceted
330        // surface lies strictly off that surface, so inversion correctly
331        // refuses it; projection is the question actually being asked.
332        //
333        // A refusal propagates instead of falling back to `linear`. A
334        // silent fallback would return a mesh with four times the
335        // triangles and none of the promised accuracy, which is worse
336        // than an error because the caller cannot detect it.
337        Some(surface) => {
338            let (u, v) = axiolid_evaluate::surface::project(surface, linear, tolerance)
339                .map_err(|error| RefineError::SurfaceRefused(error.to_string()))?;
340            let on_surface = axiolid_evaluate::surface::evaluate(surface, u, v)
341                .map_err(|error| RefineError::SurfaceRefused(error.to_string()))?;
342            if !on_surface.is_finite() {
343                return Err(RefineError::SurfaceRefused(
344                    "projected midpoint is not finite".into(),
345                ));
346            }
347            on_surface
348        }
349        None => linear,
350    };
351
352    *max_deviation = max_deviation.max((placed - linear).length());
353    let index = positions.len() as u32;
354    positions.push(placed);
355    midpoints.insert(key, index);
356    Ok(index)
357}
358
359/// What a smoothing pass actually did.
360#[derive(Debug, Clone, PartialEq)]
361#[non_exhaustive]
362pub struct SmoothReport {
363    /// Vertices whose position changed.
364    pub vertices_moved: usize,
365    /// Vertices held fixed because they sit on an open border.
366    pub boundary_vertices: usize,
367    /// Largest distance any vertex moved, in model units.
368    pub max_movement: Scalar,
369    /// What happened to each named attribute channel.
370    pub attribute_fates: Vec<(String, AttributeFate)>,
371}
372
373/// Report every channel as dropped.
374///
375/// Smoothing moves vertices without creating them, but the values it would
376/// need to keep are only valid at the ORIGINAL positions. Rather than carry
377/// stale data forward under its old name, every channel is dropped and said
378/// to be dropped.
379fn carry_attributes(mesh: &TriMesh) -> Vec<(String, AttributeFate)> {
380    mesh.attributes
381        .iter()
382        .map(|channel| {
383            (
384                channel.name.clone(),
385                AttributeFate::Dropped(axiolid_mesh::DropReason::ProviderLimitation),
386            )
387        })
388        .collect()
389}