1mod edits;
76mod reference;
77
78use axiolid_core::{Point3, Scalar};
79use axiolid_mesh::{
80 AttributeFate, EdgeId, HalfedgeBuildError, HalfedgeEditError, HalfedgeMesh, TriMesh,
81};
82
83use edits::Remesher;
84use reference::Reference;
85
86#[derive(Debug, Clone, PartialEq, thiserror::Error)]
88#[non_exhaustive]
89pub enum RemeshError {
90 #[error("target edge length {0} is not a positive finite length")]
92 InvalidTarget(Scalar),
93 #[error("feature angle {0} is not within [0, pi] radians")]
95 InvalidFeatureAngle(Scalar),
96 #[error("input is not an oriented manifold triangle mesh: {0}")]
99 InvalidMesh(#[from] HalfedgeBuildError),
100 #[error("vertex {vertex} has a non-finite position")]
102 NonFinitePosition {
103 vertex: usize,
105 },
106 #[error("triangle {triangle} is degenerate")]
108 DegenerateTriangle {
109 triangle: usize,
111 },
112 #[error("remeshing would produce about {produced} triangles, over the {limit} budget")]
114 BudgetExceeded {
115 produced: usize,
117 limit: usize,
119 },
120 #[error("internal halfedge edit refused: {0}")]
123 Internal(HalfedgeEditError),
124}
125
126#[derive(Debug, Clone, Copy, PartialEq)]
128#[non_exhaustive]
129pub struct RemeshOptions {
130 pub target_edge_length: Scalar,
132 pub iterations: u32,
134 pub feature_angle: Option<Scalar>,
138}
139
140impl RemeshOptions {
141 pub const DEFAULT_ITERATIONS: u32 = 5;
143
144 pub const DEFAULT_FEATURE_ANGLE: Scalar = core::f64::consts::FRAC_PI_3;
146
147 #[must_use]
150 pub fn new(target_edge_length: Scalar) -> Self {
151 Self {
152 target_edge_length,
153 iterations: Self::DEFAULT_ITERATIONS,
154 feature_angle: Some(Self::DEFAULT_FEATURE_ANGLE),
155 }
156 }
157
158 #[must_use]
160 pub fn with_iterations(mut self, iterations: u32) -> Self {
161 self.iterations = iterations;
162 self
163 }
164
165 #[must_use]
168 pub fn with_feature_angle(mut self, feature_angle: Option<Scalar>) -> Self {
169 self.feature_angle = feature_angle;
170 self
171 }
172}
173
174#[derive(Debug, Clone, PartialEq)]
176#[non_exhaustive]
177pub struct RemeshReport {
178 pub input_triangles: usize,
180 pub output_triangles: usize,
182 pub splits: usize,
184 pub collapses: usize,
186 pub flips: usize,
188 pub moves: usize,
190 pub protected_edges: usize,
192 pub edges_in_band: Scalar,
194 pub valence_deviation_before: Scalar,
197 pub valence_deviation_after: Scalar,
199 pub min_angle: Scalar,
201 pub max_vertex_deviation: Scalar,
204 pub attribute_fates: Vec<(String, AttributeFate)>,
207}
208
209pub fn remesh(
222 mesh: &TriMesh,
223 options: RemeshOptions,
224) -> Result<(TriMesh, RemeshReport), RemeshError> {
225 let target = options.target_edge_length;
226 if !target.is_finite() || target <= 0.0 {
227 return Err(RemeshError::InvalidTarget(target));
228 }
229 if let Some(angle) = options.feature_angle {
230 if !(0.0..=core::f64::consts::PI).contains(&angle) {
231 return Err(RemeshError::InvalidFeatureAngle(angle));
232 }
233 }
234 let input = HalfedgeMesh::from_tri_mesh(mesh)?;
235 if let Some(vertex) = mesh.positions.iter().position(|p| !p.is_finite()) {
236 return Err(RemeshError::NonFinitePosition { vertex });
237 }
238 let triangles: Vec<[Point3; 3]> = mesh
239 .triangles()
240 .map(|t| t.map(|i| mesh.positions[i as usize]))
241 .collect();
242 let mut area = 0.0;
243 for (index, t) in triangles.iter().enumerate() {
244 let n = edits::normal(*t);
245 let scale = (t[1] - t[0]).length() * (t[2] - t[0]).length();
246 if n.length() <= DEGENERATE_SINE * scale {
247 return Err(RemeshError::DegenerateTriangle { triangle: index });
248 }
249 area += n.length() / 2.0;
250 }
251 let expected = area / (3.0_f64.sqrt() / 4.0 * target * target);
253 if !expected.is_finite() || expected > crate::MAX_TRIANGLES as Scalar {
254 return Err(RemeshError::BudgetExceeded {
255 produced: if expected.is_finite() {
256 expected as usize
257 } else {
258 usize::MAX
259 },
260 limit: crate::MAX_TRIANGLES,
261 });
262 }
263
264 let protected = protected_edges(&input, &triangles, options.feature_angle);
265 let patch = patches(&input, &protected);
266 let reference = Reference::new(triangles, &patch);
267 let valence_deviation_before = valence_deviation(&input);
268 let input_triangles = input.face_count();
269
270 let mut remesher = Remesher::new(
271 input,
272 protected,
273 patch,
274 &reference,
275 target,
276 crate::MAX_TRIANGLES,
277 );
278 for _ in 0..options.iterations {
279 remesher.split_long_edges()?;
280 remesher.collapse_short_edges();
281 remesher.equalize_valences();
282 remesher.relax_and_project();
283 }
284
285 let result = &remesher.mesh;
286 let (low, high) = (target * 4.0 / 5.0, target * 4.0 / 3.0);
287 let mut in_band = 0usize;
288 let mut protected_count = 0usize;
289 for e in result.edges() {
290 let [a, b] = result.edge_vertices(e);
291 let length = (result.position(a) - result.position(b)).length();
292 if (low..=high).contains(&length) {
293 in_band += 1;
294 }
295 if remesher.is_protected(e) {
296 protected_count += 1;
297 }
298 }
299 let edges_in_band = if result.edge_count() == 0 {
300 1.0
301 } else {
302 in_band as Scalar / result.edge_count() as Scalar
303 };
304 let max_vertex_deviation = result
305 .vertices()
306 .filter(|&v| !result.is_isolated(v))
307 .map(|v| reference.distance(result.position(v)))
308 .fold(0.0, Scalar::max);
309 let min_angle = result
310 .faces()
311 .map(|f| {
312 let mut corners = result.face_vertices(f).map(|v| result.position(v));
313 let t = [(); 3].map(|()| corners.next().unwrap_or(Point3::ZERO));
314 edits::min_angle(t)
315 })
316 .fold(core::f64::consts::PI, Scalar::min);
317 let valence_deviation_after = valence_deviation(result);
318 let out = result.to_tri_mesh().map_err(RemeshError::Internal)?;
319 let counts = remesher.counts;
320
321 let attribute_fates = crate::carry_attributes(mesh);
322 let report = RemeshReport {
323 input_triangles,
324 output_triangles: out.triangle_count(),
325 splits: counts.splits,
326 collapses: counts.collapses,
327 flips: counts.flips,
328 moves: counts.moves,
329 protected_edges: protected_count,
330 edges_in_band,
331 valence_deviation_before,
332 valence_deviation_after,
333 min_angle,
334 max_vertex_deviation,
335 attribute_fates,
336 };
337 Ok((out, report))
338}
339
340const DEGENERATE_SINE: Scalar = 1e-12;
343
344fn protected_edges(
347 mesh: &HalfedgeMesh,
348 triangles: &[[Point3; 3]],
349 feature_angle: Option<Scalar>,
350) -> Vec<bool> {
351 let mut protected = vec![false; mesh.edges().last().map_or(0, |e| e.index() + 1)];
352 for e in mesh.edges() {
353 protected[e.index()] = if mesh.is_boundary_edge(e) {
354 true
355 } else if let Some(angle) = feature_angle {
356 let normal = |side| {
357 let f = mesh.face(mesh.edge_halfedge(e, side)).map(|f| f.index());
358 f.and_then(|f| edits::normal(triangles[f]).try_normalize())
359 };
360 match (normal(0), normal(1)) {
361 (Some(n0), Some(n1)) => n0.dot(n1).clamp(-1.0, 1.0).acos() > angle,
362 _ => true,
363 }
364 } else {
365 false
366 };
367 }
368 protected
369}
370
371fn patches(mesh: &HalfedgeMesh, protected: &[bool]) -> Vec<u32> {
374 let count = mesh.faces().last().map_or(0, |f| f.index() + 1);
375 let mut patch = vec![u32::MAX; count];
376 let mut next = 0u32;
377 let mut stack = Vec::new();
378 for seed in mesh.faces() {
379 if patch[seed.index()] != u32::MAX {
380 continue;
381 }
382 patch[seed.index()] = next;
383 stack.push(seed);
384 while let Some(f) = stack.pop() {
385 for h in mesh.face_halfedges(f) {
386 let e: EdgeId = mesh.edge(h);
387 if protected[e.index()] {
388 continue;
389 }
390 if let Some(g) = mesh.face(mesh.opposite(h)) {
391 if patch[g.index()] == u32::MAX {
392 patch[g.index()] = next;
393 stack.push(g);
394 }
395 }
396 }
397 }
398 next += 1;
399 }
400 patch
401}
402
403fn valence_deviation(mesh: &HalfedgeMesh) -> Scalar {
406 let mut total = 0i64;
407 let mut count = 0usize;
408 for v in mesh.vertices().filter(|&v| !mesh.is_isolated(v)) {
409 let optimal = if mesh.is_boundary_vertex(v) { 4 } else { 6 };
410 total += (mesh.degree(v) as i64 - optimal).abs();
411 count += 1;
412 }
413 if count == 0 {
414 0.0
415 } else {
416 total as Scalar / count as Scalar
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn protected_edges_cut_a_box_into_its_six_faces() {
426 let p = |x: Scalar, y: Scalar, z: Scalar| Point3::new(x, y, z);
427 let positions = vec![
428 p(0.0, 0.0, 0.0),
429 p(1.0, 0.0, 0.0),
430 p(1.0, 1.0, 0.0),
431 p(0.0, 1.0, 0.0),
432 p(0.0, 0.0, 1.0),
433 p(1.0, 0.0, 1.0),
434 p(1.0, 1.0, 1.0),
435 p(0.0, 1.0, 1.0),
436 ];
437 let indices = vec![
438 0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5, 2, 3, 7, 2, 7,
439 6, 3, 0, 4, 3, 4, 7,
440 ];
441 let mesh = TriMesh::new(positions, indices);
442 let he = HalfedgeMesh::from_tri_mesh(&mesh).expect("closed box");
443 let triangles: Vec<[Point3; 3]> = mesh
444 .triangles()
445 .map(|t| t.map(|i| mesh.positions[i as usize]))
446 .collect();
447 let protected =
448 protected_edges(&he, &triangles, Some(RemeshOptions::DEFAULT_FEATURE_ANGLE));
449 assert_eq!(protected.iter().filter(|&&p| p).count(), 12);
450 let patch = patches(&he, &protected);
451 assert_eq!(patch, vec![0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5]);
452 let protected = protected_edges(&he, &triangles, None);
454 assert_eq!(patches(&he, &protected), vec![0; 12]);
455 }
456}