1use fast_mesh::compute_vertex_normals;
26use fast_mesh_store::{Mesh, Transaction, UV_CHANNEL, VertexId};
27
28use crate::topology::{EdgeMap, boundary_loops, proper_triangle};
29use crate::{DegenerateOptions, OpsError, Result, filled, push, remove_degenerate, reserve};
30
31#[derive(Debug, Clone, Copy, Default, PartialEq)]
33pub struct OffsetOptions {
34 pub distance: f32,
38}
39
40impl OffsetOptions {
41 pub fn builder() -> OffsetOptionsBuilder {
43 OffsetOptionsBuilder::default()
44 }
45}
46
47#[derive(Debug, Clone, Default)]
49pub struct OffsetOptionsBuilder {
50 options: OffsetOptions,
51}
52
53impl OffsetOptionsBuilder {
54 pub fn distance(mut self, distance: f32) -> Self {
56 self.options.distance = distance;
57 self
58 }
59
60 pub fn build(self) -> OffsetOptions {
62 self.options
63 }
64}
65
66#[derive(Debug, Clone, Copy, Default, PartialEq)]
68pub struct OffsetReport {
69 pub moved: usize,
71 pub max_displacement: f32,
74 pub degenerate_removed: usize,
76 pub vertices_removed: usize,
78}
79
80pub fn offset(tx: &mut Transaction, options: &OffsetOptions) -> Result<OffsetReport> {
99 let distance = options.distance;
100 if !distance.is_finite() {
101 return Err(OpsError::InvalidInput("offset distance must be finite"));
102 }
103 let mut report = OffsetReport::default();
104 if distance == 0.0 {
105 return Ok(report);
106 }
107 let mesh = tx.mesh();
108 if mesh.triangle_count() == 0 {
109 return Ok(report);
110 }
111 let normals = smooth_normals(mesh)?;
112 let positions = mesh.positions();
113 let mut ids: Vec<VertexId> = Vec::new();
114 let mut moved_to: Vec<[f32; 3]> = Vec::new();
115 for (v, p) in positions.iter().enumerate() {
116 let v = v as VertexId;
117 if !mesh.is_vertex_alive(v) {
118 continue;
119 }
120 let n = [
121 normals[3 * v as usize],
122 normals[3 * v as usize + 1],
123 normals[3 * v as usize + 2],
124 ];
125 let displaced = [
126 p[0] + distance * n[0],
127 p[1] + distance * n[1],
128 p[2] + distance * n[2],
129 ];
130 let length = n.iter().map(|n| (distance * n).powi(2)).sum::<f32>().sqrt();
131 if !length.is_finite() || length <= 0.0 {
132 continue;
133 }
134 push(&mut ids, v)?;
135 push(&mut moved_to, displaced)?;
136 report.max_displacement = report.max_displacement.max(length);
137 }
138 report.moved = ids.len();
139 if !ids.is_empty() {
140 tx.set_positions(&ids, &moved_to)?;
141 }
142 let cleanup = remove_degenerate(tx, &DegenerateOptions::default())?;
143 report.degenerate_removed = cleanup.removed_triangles;
144 report.vertices_removed = cleanup.removed_vertices;
145 Ok(report)
146}
147
148#[derive(Debug, Clone, Copy, PartialEq)]
150pub struct ShellOptions {
151 pub thickness: f32,
154}
155
156impl Default for ShellOptions {
157 fn default() -> Self {
158 ShellOptions { thickness: 1.0 }
159 }
160}
161
162impl ShellOptions {
163 pub fn builder() -> ShellOptionsBuilder {
165 ShellOptionsBuilder::default()
166 }
167}
168
169#[derive(Debug, Clone, Default)]
171pub struct ShellOptionsBuilder {
172 options: ShellOptions,
173}
174
175impl ShellOptionsBuilder {
176 pub fn thickness(mut self, thickness: f32) -> Self {
178 self.options.thickness = thickness;
179 self
180 }
181
182 pub fn build(self) -> ShellOptions {
184 self.options
185 }
186}
187
188#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
190pub struct ShellReport {
191 pub vertices_added: usize,
193 pub triangles_added: usize,
195 pub loops_closed: usize,
197 pub degenerate_removed: usize,
199 pub vertices_removed: usize,
201}
202
203#[derive(Debug, Clone, Copy)]
206enum Corner {
207 Kept(VertexId),
208 Copy(usize),
209}
210
211pub fn shell(tx: &mut Transaction, options: &ShellOptions) -> Result<ShellReport> {
237 let thickness = options.thickness;
238 if !thickness.is_finite() || thickness <= 0.0 {
239 return Err(OpsError::InvalidInput(
240 "shell thickness must be positive and finite",
241 ));
242 }
243 let mut report = ShellReport::default();
244 let mesh = tx.mesh();
245 if mesh.triangle_count() == 0 {
246 return Ok(report);
247 }
248 let normals = smooth_normals(mesh)?;
249 let positions = mesh.positions();
250 let mut copied: Vec<[f32; 3]> = Vec::new();
253 let source_uvs = mesh.uvs();
255 let mut copied_uvs: Vec<f32> = Vec::new();
256 let mut at: Vec<Option<usize>> = filled(mesh.vertex_slots(), None)?;
257 for (v, p) in positions.iter().enumerate() {
258 let v = v as VertexId;
259 if !mesh.is_vertex_alive(v) {
260 continue;
261 }
262 if !p.iter().all(|c| c.is_finite()) {
263 return Err(OpsError::InvalidInput(
264 "shell needs finite vertex positions",
265 ));
266 }
267 let n = [
268 normals[3 * v as usize],
269 normals[3 * v as usize + 1],
270 normals[3 * v as usize + 2],
271 ];
272 let q = [
273 p[0] - thickness * n[0],
274 p[1] - thickness * n[1],
275 p[2] - thickness * n[2],
276 ];
277 if !q.iter().all(|c| c.is_finite()) {
278 return Err(OpsError::InvalidInput("shell thickness leaves f32 range"));
279 }
280 push(&mut copied, q)?;
281 if let Some(uv) = source_uvs {
282 reserve(&mut copied_uvs, 2)?;
283 copied_uvs.extend_from_slice(&uv[2 * v as usize..2 * v as usize + 2]);
284 }
285 at[v as usize] = Some(copied.len() - 1);
286 }
287 let mut planned: Vec<[Corner; 3]> = Vec::new();
290 for t in 0..mesh.triangle_slots() as u32 {
291 if let Some([a, b, c]) = proper_triangle(mesh, t) {
292 let [ia, ib, ic] = [a, b, c].map(|v| at[v as usize].expect("live corner"));
293 push(
294 &mut planned,
295 [Corner::Copy(ic), Corner::Copy(ib), Corner::Copy(ia)],
296 )?;
297 }
298 }
299 let edges = EdgeMap::build(mesh)?;
300 for ((u, v), tris) in edges.edges() {
301 if tris.len() != 1 {
302 continue;
303 }
304 let [a, b, c] = mesh.triangle(tris[0]).expect("mapped triangle is alive");
305 let (x, y) = [(a, b), (b, c), (c, a)]
306 .into_iter()
307 .find(|&(p, q)| (p == u && q == v) || (p == v && q == u))
308 .expect("mapped edge uses its triangle");
309 let (ix, iy) = (
310 at[x as usize].expect("live corner"),
311 at[y as usize].expect("live corner"),
312 );
313 push(
314 &mut planned,
315 [Corner::Kept(y), Corner::Kept(x), Corner::Copy(ix)],
316 )?;
317 push(
318 &mut planned,
319 [Corner::Kept(y), Corner::Copy(ix), Corner::Copy(iy)],
320 )?;
321 }
322 report.loops_closed = boundary_loops(mesh)?.len();
323 let has_uvs = source_uvs.is_some();
324 let fresh = tx.add_vertices(&copied)?;
325 if has_uvs {
326 tx.write_channel(UV_CHANNEL, &fresh, &copied_uvs)?;
327 }
328 let resolve = |c: Corner| match c {
329 Corner::Kept(v) => v,
330 Corner::Copy(i) => fresh[i],
331 };
332 let mut added: Vec<[VertexId; 3]> = Vec::new();
333 reserve(&mut added, planned.len())?;
334 for tri in planned {
335 added.push(tri.map(resolve));
336 }
337 tx.add_triangles(&added)?;
338 report.vertices_added = fresh.len();
339 report.triangles_added = added.len();
340 let cleanup = remove_degenerate(tx, &DegenerateOptions::default())?;
341 report.degenerate_removed = cleanup.removed_triangles;
342 report.vertices_removed = cleanup.removed_vertices;
343 Ok(report)
344}
345
346fn smooth_normals(mesh: &Mesh) -> Result<Vec<f32>> {
349 let positions = mesh.positions();
350 let mut flat: Vec<f32> = Vec::new();
351 reserve(&mut flat, positions.len() * 3)?;
352 flat.extend(positions.iter().flatten().copied());
353 let mut indices: Vec<u32> = Vec::new();
354 reserve(&mut indices, mesh.triangle_slots() * 3)?;
355 for t in 0..mesh.triangle_slots() as u32 {
356 if let Some(tri) = mesh.triangle(t) {
357 indices.extend(tri);
358 }
359 }
360 compute_vertex_normals(&flat, &indices)
361 .map_err(|_| OpsError::InvalidInput("mesh buffers are not a valid mesh"))
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::test_util::{box_mesh, edit, grid, mesh_of, parts};
368
369 fn distances(mesh: &Mesh, c: [f32; 3]) -> Vec<f32> {
371 let mut out = Vec::new();
372 for (v, p) in mesh.positions().iter().enumerate() {
373 if !mesh.is_vertex_alive(v as VertexId) {
374 continue;
375 }
376 out.push(
377 ((p[0] - c[0]).powi(2) + (p[1] - c[1]).powi(2) + (p[2] - c[2]).powi(2)).sqrt(),
378 );
379 }
380 out
381 }
382
383 #[test]
384 fn offset_moves_along_normals_by_distance() {
385 let mut mesh = grid(2);
386 let old = parts(&mesh).0;
387 let options = OffsetOptions::builder().distance(0.5).build();
388 let (report, empty) = edit(&mut mesh, |tx| offset(tx, &options));
389 assert!(!empty);
390 assert_eq!(report.moved, 9);
393 assert!((report.max_displacement - 0.5).abs() < 1e-6);
394 assert_eq!(report.degenerate_removed, 0);
395 assert_eq!(report.vertices_removed, 0);
396 let new = parts(&mesh).0;
397 for v in 0..9 {
398 assert!((new[v * 3] - old[v * 3]).abs() < 1e-6);
399 assert!((new[v * 3 + 1] - old[v * 3 + 1]).abs() < 1e-6);
400 assert!((new[v * 3 + 2] - old[v * 3 + 2] - 0.5).abs() < 1e-6);
401 }
402 }
403
404 #[test]
405 fn offset_sign_follows_winding() {
406 let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
413 let c = [0.5; 3];
414 let before = distances(&mesh, c);
415 let (report, _) = edit(&mut mesh, |tx| {
416 offset(tx, &OffsetOptions::builder().distance(0.25).build())
417 });
418 assert_eq!(report.moved, 8);
419 assert!((report.max_displacement - 0.25).abs() < 1e-6);
420 let grown = distances(&mesh, c);
421 for (a, b) in before.iter().zip(&grown) {
422 assert!(*b > *a, "{a} -> {b}");
423 assert!((b - a) <= 0.25 + 1e-6, "{a} -> {b}");
424 }
425 let prev = parts(&box_mesh([0.0; 3], [1.0; 3])).0;
427 let next = parts(&mesh).0;
428 for v in 0..8 {
429 let d = ((next[v * 3] - prev[v * 3]).powi(2)
430 + (next[v * 3 + 1] - prev[v * 3 + 1]).powi(2)
431 + (next[v * 3 + 2] - prev[v * 3 + 2]).powi(2))
432 .sqrt();
433 assert!((d - 0.25).abs() < 1e-5, "{d}");
434 }
435 edit(&mut mesh, |tx| {
437 offset(tx, &OffsetOptions::builder().distance(-0.5).build()).map(drop)
438 });
439 let sunk = distances(&mesh, c);
440 for (a, b) in before.iter().zip(&sunk) {
441 assert!(*b < *a, "{a} -> {b}");
442 }
443 }
444
445 #[test]
446 fn offset_zero_distance_and_empty_mesh_are_noops() {
447 let mut mesh = grid(2);
448 let before = parts(&mesh);
449 let mut tx = mesh.begin_edit();
450 let report = offset(&mut tx, &OffsetOptions::builder().distance(0.0).build()).unwrap();
451 assert_eq!(report, OffsetReport::default());
452 assert!(tx.commit().unwrap().is_empty());
453 assert_eq!(parts(&mesh), before);
454 let mut mesh = Mesh::new();
456 let mut tx = mesh.begin_edit();
457 let report = offset(&mut tx, &OffsetOptions::builder().distance(1.0).build()).unwrap();
458 assert_eq!(report, OffsetReport::default());
459 assert!(tx.commit().unwrap().is_empty());
460 }
461
462 #[test]
463 fn offset_rejects_nonfinite_distance() {
464 for distance in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
465 let mut mesh = grid(2);
466 let before = parts(&mesh);
467 let mut tx = mesh.begin_edit();
468 assert!(matches!(
469 offset(
470 &mut tx,
471 &OffsetOptions::builder().distance(distance).build()
472 ),
473 Err(OpsError::InvalidInput(_))
474 ));
475 assert!(tx.commit().unwrap().is_empty());
476 assert_eq!(parts(&mesh), before);
477 }
478 }
479
480 #[test]
481 fn offset_collapse_without_pinch_stays_valid() {
482 let mut mesh = mesh_of(
489 &[1., 1., 1., 1., -1., -1., -1., 1., -1., -1., -1., 1.],
490 &[[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]],
491 );
492 assert_eq!(mesh.vertex_count(), 4);
493 let c = [0.0; 3];
494 let radius = distances(&mesh, c)[0];
495 let (report, _) = edit(&mut mesh, |tx| {
496 offset(tx, &OffsetOptions::builder().distance(-radius).build())
497 });
498 assert_eq!(report.moved, 4);
499 assert!((report.max_displacement - radius).abs() < 1e-4);
500 assert_eq!(report.degenerate_removed, 0);
501 assert_eq!(mesh.triangle_count(), 4);
502 for d in distances(&mesh, c) {
503 assert!(d < 1e-6, "{d}");
504 }
505 }
506
507 #[test]
508 fn offset_cleans_preexisting_degenerate_triangles() {
509 let mut mesh = grid(2);
512 let (flat, mut tris) = parts(&mesh);
513 tris.push([0, 0, 1]);
514 mesh = mesh_of(&flat, &tris);
515 assert_eq!(mesh.triangle_count(), 9);
516 let options = OffsetOptions::builder().distance(0.5).build();
517 let (report, _) = edit(&mut mesh, |tx| offset(tx, &options));
518 assert_eq!(report.moved, 9);
519 assert_eq!(report.degenerate_removed, 1);
520 assert_eq!(mesh.triangle_count(), 8);
521 }
522
523 #[test]
524 fn shell_stitches_an_open_triangle() {
525 let mut mesh = mesh_of(&[0., 0., 0., 1., 0., 0., 0., 1., 0.], &[[0, 1, 2]]);
528 let (report, empty) = edit(&mut mesh, |tx| {
529 shell(tx, &ShellOptions::builder().thickness(0.5).build())
530 });
531 assert!(!empty);
532 assert_eq!(
533 report,
534 ShellReport {
535 vertices_added: 3,
536 triangles_added: 1 + 6,
537 loops_closed: 1,
538 degenerate_removed: 0,
539 vertices_removed: 0,
540 }
541 );
542 assert_eq!(mesh.vertex_count(), 6);
543 assert_eq!(mesh.triangle_count(), 8);
544 let (flat, _) = parts(&mesh);
546 assert_eq!(&flat[0..9], &[0., 0., 0., 1., 0., 0., 0., 1., 0.][..]);
547 assert_eq!(
548 &flat[9..18],
549 &[0., 0., -0.5, 1., 0., -0.5, 0., 1., -0.5][..]
550 );
551 let analysis = crate::analyze(&mesh, &crate::AnalyzeOptions::default()).unwrap();
553 assert!(analysis.loops.is_empty());
554 assert_eq!(analysis.orientation_conflicts, 0);
555 assert!(analysis.components.iter().all(|c| c.closed));
556 }
557
558 #[test]
559 fn shell_nests_a_closed_cube() {
560 let mut mesh = box_mesh([0.0; 3], [2.0; 3]);
562 let (report, _) = edit(&mut mesh, |tx| {
563 shell(tx, &ShellOptions::builder().thickness(0.25).build())
564 });
565 assert_eq!(report.vertices_added, 8);
566 assert_eq!(report.triangles_added, 12);
567 assert_eq!(report.loops_closed, 0);
568 assert_eq!(report.degenerate_removed, 0);
569 assert_eq!(mesh.triangle_count(), 24);
570 let analysis = crate::analyze(&mesh, &crate::AnalyzeOptions::default()).unwrap();
571 assert!(analysis.loops.is_empty());
572 assert_eq!(analysis.orientation_conflicts, 0);
573 assert_eq!(analysis.components.len(), 2);
574 assert!(analysis.components.iter().all(|c| c.closed));
575 let (flat, _) = parts(&mesh);
577 assert_eq!(flat.len(), 16 * 3);
578 let outer: Vec<f32> = (0..8)
579 .flat_map(|i| (0..3).map(move |k| if i >> k & 1 == 1 { 2.0 } else { 0.0 }))
580 .collect();
581 assert_eq!(&flat[0..24], &outer[..]);
582 for v in 0..8 {
587 let (o, q) = (&flat[v * 3..v * 3 + 3], &flat[(v + 8) * 3..(v + 8) * 3 + 3]);
588 let d = ((q[0] - o[0]).powi(2) + (q[1] - o[1]).powi(2) + (q[2] - o[2]).powi(2)).sqrt();
589 assert!((d - 0.25).abs() < 1e-5, "{d}");
590 for k in 0..3 {
591 assert!(q[k] > 0.0 && q[k] < 2.0, "{q:?}");
592 }
593 }
594 }
595
596 #[test]
597 fn shell_rejects_bad_thickness() {
598 for thickness in [0.0, -1.0, f32::NAN, f32::INFINITY] {
599 let mut mesh = grid(2);
600 let before = parts(&mesh);
601 let mut tx = mesh.begin_edit();
602 assert!(matches!(
603 shell(
604 &mut tx,
605 &ShellOptions::builder().thickness(thickness).build()
606 ),
607 Err(OpsError::InvalidInput(_))
608 ));
609 assert!(tx.commit().unwrap().is_empty());
610 assert_eq!(parts(&mesh), before);
611 }
612 }
613
614 #[test]
615 fn shell_empty_mesh_is_a_noop() {
616 let mut mesh = Mesh::new();
617 let mut tx = mesh.begin_edit();
618 let report = shell(&mut tx, &ShellOptions::builder().thickness(0.5).build()).unwrap();
619 assert_eq!(report, ShellReport::default());
620 assert!(tx.commit().unwrap().is_empty());
621 }
622
623 #[test]
624 fn shell_first_failure_writes_nothing() {
625 let mut mesh = grid(2);
628 let before = parts(&mesh);
629 let mut tx = mesh.begin_edit();
630 crate::fail_allocations_after(Some(0));
631 let r = shell(&mut tx, &ShellOptions::builder().thickness(0.5).build());
632 crate::fail_allocations_after(None);
633 assert_eq!(
634 r,
635 Err(OpsError::Store(fast_mesh_store::StoreError::OutOfMemory))
636 );
637 assert!(tx.commit().unwrap().is_empty());
638 assert_eq!(parts(&mesh), before);
639 }
640}