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}