1use fast_mesh::compute_vertex_normals;
17use fast_mesh_store::{Incidence, Mesh, Transaction, VertexId};
18
19use crate::topology::VertexFans;
20use crate::{OpsError, Result, push, reserve};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Falloff {
25 Linear,
27 Smooth,
29}
30
31pub(crate) fn falloff_value(falloff: Falloff, t: f32) -> f32 {
35 match falloff {
36 Falloff::Linear => 1.0 - t,
37 Falloff::Smooth => {
38 let s = 1.0 - t;
39 s * s * (3.0 - 2.0 * s)
40 }
41 }
42}
43
44fn has_region_member(mesh: &Mesh, brush: &Brush) -> bool {
47 mesh.positions().iter().enumerate().any(|(v, p)| {
48 if !mesh.is_vertex_alive(v as VertexId) {
49 return false;
50 }
51 let dist = brush
52 .center
53 .iter()
54 .zip(p)
55 .map(|(c, p)| (p - c).powi(2))
56 .sum::<f32>()
57 .sqrt();
58 let t = dist / brush.radius;
59 t.is_finite() && t < 1.0
60 })
61}
62
63fn brush_weight(brush: &Brush, p: &[f32; 3]) -> Option<f32> {
66 let dist = brush
67 .center
68 .iter()
69 .zip(p)
70 .map(|(c, p)| (p - c).powi(2))
71 .sum::<f32>()
72 .sqrt();
73 let t = dist / brush.radius;
74 (t.is_finite() && t < 1.0).then(|| brush.strength * falloff_value(brush.falloff, t))
75}
76
77fn region_index(mesh: &Mesh) -> Result<&Incidence> {
79 mesh.incidence().ok_or(OpsError::InvalidInput(
80 "region deforms need Mesh::ensure_incidence and no topology edit in the transaction",
81 ))
82}
83
84#[derive(Debug, Clone, Copy, PartialEq)]
86pub struct Brush {
87 pub center: [f32; 3],
89 pub radius: f32,
92 pub falloff: Falloff,
94 pub strength: f32,
97}
98
99impl Default for Brush {
100 fn default() -> Self {
101 Brush {
102 center: [0.0; 3],
103 radius: 1.0,
104 falloff: Falloff::Linear,
105 strength: 1.0,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, Default, PartialEq)]
112pub struct InflateOptions {
113 pub brush: Brush,
115}
116
117impl InflateOptions {
118 pub fn builder() -> InflateOptionsBuilder {
120 InflateOptionsBuilder::default()
121 }
122}
123
124#[derive(Debug, Clone, Default)]
126pub struct InflateOptionsBuilder {
127 options: InflateOptions,
128}
129
130impl InflateOptionsBuilder {
131 pub fn center(mut self, center: [f32; 3]) -> Self {
133 self.options.brush.center = center;
134 self
135 }
136
137 pub fn radius(mut self, radius: f32) -> Self {
139 self.options.brush.radius = radius;
140 self
141 }
142
143 pub fn falloff(mut self, falloff: Falloff) -> Self {
145 self.options.brush.falloff = falloff;
146 self
147 }
148
149 pub fn strength(mut self, strength: f32) -> Self {
151 self.options.brush.strength = strength;
152 self
153 }
154
155 pub fn build(self) -> InflateOptions {
157 self.options
158 }
159}
160
161#[derive(Debug, Clone, Copy, Default, PartialEq)]
163pub struct InflateReport {
164 pub moved: usize,
166 pub max_displacement: f32,
169}
170
171fn check_inflate(brush: &Brush) -> Result<()> {
172 if !brush.radius.is_finite() || brush.radius <= 0.0 {
173 return Err(OpsError::InvalidInput(
174 "inflate brush radius must be positive and finite",
175 ));
176 }
177 if brush.center.iter().any(|c| !c.is_finite()) {
178 return Err(OpsError::InvalidInput(
179 "inflate brush center must be finite",
180 ));
181 }
182 if !brush.strength.is_finite() {
183 return Err(OpsError::InvalidInput(
184 "inflate brush strength must be finite",
185 ));
186 }
187 Ok(())
188}
189
190pub fn inflate(tx: &mut Transaction, options: &InflateOptions) -> Result<InflateReport> {
207 let brush = &options.brush;
208 check_inflate(brush)?;
209 let mesh = tx.mesh();
210 let mut report = InflateReport::default();
211 if mesh.vertex_slots() == 0 {
212 return Ok(report);
213 }
214 if !has_region_member(mesh, brush) {
217 return Ok(report);
218 }
219 let positions = mesh.positions();
222 let mut flat: Vec<f32> = Vec::new();
223 reserve(&mut flat, positions.len() * 3)?;
224 flat.extend(positions.iter().flatten().copied());
225 let mut indices: Vec<u32> = Vec::new();
226 reserve(&mut indices, mesh.triangle_slots() * 3)?;
227 for t in 0..mesh.triangle_slots() as u32 {
228 if let Some(tri) = mesh.triangle(t) {
229 indices.extend(tri);
230 }
231 }
232 let normals = compute_vertex_normals(&flat, &indices)
233 .map_err(|_| OpsError::InvalidInput("mesh buffers are not a valid mesh"))?;
234 let mut ids: Vec<VertexId> = Vec::new();
235 let mut moved_to: Vec<[f32; 3]> = Vec::new();
236 for (v, p) in positions.iter().enumerate() {
237 let v = v as VertexId;
238 if !mesh.is_vertex_alive(v) {
239 continue;
240 }
241 let dist = brush
242 .center
243 .iter()
244 .zip(p)
245 .map(|(c, p)| (p - c).powi(2))
246 .sum::<f32>()
247 .sqrt();
248 let t = dist / brush.radius;
249 if !t.is_finite() || t >= 1.0 {
250 continue;
251 }
252 let w = brush.strength * falloff_value(brush.falloff, t);
253 let n = [
254 normals[3 * v as usize],
255 normals[3 * v as usize + 1],
256 normals[3 * v as usize + 2],
257 ];
258 let displaced = [p[0] + w * n[0], p[1] + w * n[1], p[2] + w * n[2]];
259 let length = n.iter().map(|n| (w * n).powi(2)).sum::<f32>().sqrt();
260 if !length.is_finite() || length <= 0.0 {
261 continue;
262 }
263 push(&mut ids, v)?;
264 push(&mut moved_to, displaced)?;
265 report.max_displacement = report.max_displacement.max(length);
266 }
267 report.moved = ids.len();
268 if !ids.is_empty() {
269 tx.set_positions(&ids, &moved_to)?;
270 }
271 Ok(report)
272}
273
274pub const TAUBIN_LAMBDA: f32 = 0.33;
277
278pub const TAUBIN_MU: f32 = -0.34;
282
283#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
286pub enum SmoothMethod {
287 #[default]
290 Taubin,
291 Laplacian,
294}
295
296#[derive(Debug, Clone, Copy, Default, PartialEq)]
298pub struct SmoothOptions {
299 pub brush: Brush,
301 pub cycles: u32,
303 pub method: SmoothMethod,
305}
306
307impl SmoothOptions {
308 pub fn builder() -> SmoothOptionsBuilder {
310 SmoothOptionsBuilder::default()
311 }
312}
313
314#[derive(Debug, Clone, Default)]
316pub struct SmoothOptionsBuilder {
317 options: SmoothOptions,
318}
319
320impl SmoothOptionsBuilder {
321 pub fn center(mut self, center: [f32; 3]) -> Self {
323 self.options.brush.center = center;
324 self
325 }
326
327 pub fn radius(mut self, radius: f32) -> Self {
329 self.options.brush.radius = radius;
330 self
331 }
332
333 pub fn falloff(mut self, falloff: Falloff) -> Self {
335 self.options.brush.falloff = falloff;
336 self
337 }
338
339 pub fn strength(mut self, strength: f32) -> Self {
341 self.options.brush.strength = strength;
342 self
343 }
344
345 pub fn cycles(mut self, cycles: u32) -> Self {
347 self.options.cycles = cycles;
348 self
349 }
350
351 pub fn method(mut self, method: SmoothMethod) -> Self {
353 self.options.method = method;
354 self
355 }
356
357 pub fn build(self) -> SmoothOptions {
359 self.options
360 }
361}
362
363#[derive(Debug, Clone, Copy, Default, PartialEq)]
365pub struct SmoothReport {
366 pub moved: usize,
368 pub max_displacement: f32,
371}
372
373fn check_smooth(brush: &Brush) -> Result<()> {
374 if !brush.radius.is_finite() || brush.radius <= 0.0 {
375 return Err(OpsError::InvalidInput(
376 "smooth brush radius must be positive and finite",
377 ));
378 }
379 if brush.center.iter().any(|c| !c.is_finite()) {
380 return Err(OpsError::InvalidInput("smooth brush center must be finite"));
381 }
382 if !brush.strength.is_finite() {
383 return Err(OpsError::InvalidInput(
384 "smooth brush strength must be finite",
385 ));
386 }
387 Ok(())
388}
389
390pub fn smooth(tx: &mut Transaction, options: &SmoothOptions) -> Result<SmoothReport> {
414 let brush = &options.brush;
415 check_smooth(brush)?;
416 let report = SmoothReport::default();
417 if options.cycles == 0 {
418 return Ok(report);
419 }
420 let mesh = tx.mesh();
421 if mesh.vertex_slots() == 0 {
422 return Ok(report);
423 }
424 if !has_region_member(mesh, brush) {
427 return Ok(report);
428 }
429 let fans = VertexFans::build(mesh)?;
430 let positions = mesh.positions();
431 let mut members: Vec<(VertexId, f32)> = Vec::new();
433 for (v, p) in positions.iter().enumerate() {
434 let v = v as VertexId;
435 if !mesh.is_vertex_alive(v) {
436 continue;
437 }
438 let dist = brush
439 .center
440 .iter()
441 .zip(p)
442 .map(|(c, p)| (p - c).powi(2))
443 .sum::<f32>()
444 .sqrt();
445 let t = dist / brush.radius;
446 if !t.is_finite() || t >= 1.0 {
447 continue;
448 }
449 push(
450 &mut members,
451 (v, brush.strength * falloff_value(brush.falloff, t)),
452 )?;
453 }
454 let mut current: Vec<[f32; 3]> = Vec::new();
456 reserve(&mut current, positions.len())?;
457 current.extend_from_slice(positions);
458 let mut before: Vec<[f32; 3]> = Vec::new();
459 let mut adjacent: Vec<VertexId> = Vec::new();
460 let passes: &[f32] = match options.method {
461 SmoothMethod::Taubin => &[TAUBIN_LAMBDA, TAUBIN_MU],
462 SmoothMethod::Laplacian => &[1.0],
463 };
464 for _ in 0..options.cycles {
465 for &k in passes {
466 before.clear();
467 let missing = current.len().saturating_sub(before.capacity());
468 reserve(&mut before, missing)?;
469 before.extend_from_slice(¤t);
470 for &(v, w) in &members {
471 adjacent.clear();
472 for &t in fans.fan(v) {
473 if let Some(tri) = mesh.triangle(t) {
474 for u in tri {
475 if u != v {
476 push(&mut adjacent, u)?;
477 }
478 }
479 }
480 }
481 if adjacent.is_empty() {
482 continue;
483 }
484 adjacent.sort_unstable();
485 adjacent.dedup();
486 let mut centroid = [0.0f32; 3];
487 for &u in &adjacent {
488 for axis in 0..3 {
489 centroid[axis] += before[u as usize][axis];
490 }
491 }
492 for c in &mut centroid {
493 *c /= adjacent.len() as f32;
494 }
495 let p = before[v as usize];
496 for axis in 0..3 {
497 current[v as usize][axis] = p[axis] + w * k * (centroid[axis] - p[axis]);
498 }
499 }
500 }
501 }
502 let mut report = SmoothReport::default();
503 let mut ids: Vec<VertexId> = Vec::new();
504 let mut moved_to: Vec<[f32; 3]> = Vec::new();
505 for &(v, _) in &members {
506 let length = (0..3)
507 .map(|k| (current[v as usize][k] - positions[v as usize][k]).powi(2))
508 .sum::<f32>()
509 .sqrt();
510 if !length.is_finite() || length <= 0.0 {
511 continue;
512 }
513 push(&mut ids, v)?;
514 push(&mut moved_to, current[v as usize])?;
515 report.max_displacement = report.max_displacement.max(length);
516 }
517 report.moved = ids.len();
518 if !ids.is_empty() {
519 tx.set_positions(&ids, &moved_to)?;
520 }
521 Ok(report)
522}
523
524#[derive(Debug, Clone, Copy, Default, PartialEq)]
526pub struct GrabOptions {
527 pub brush: Brush,
529 pub direction: [f32; 3],
533}
534
535impl GrabOptions {
536 pub fn builder() -> GrabOptionsBuilder {
538 GrabOptionsBuilder::default()
539 }
540}
541
542#[derive(Debug, Clone, Default)]
544pub struct GrabOptionsBuilder {
545 options: GrabOptions,
546}
547
548impl GrabOptionsBuilder {
549 pub fn center(mut self, center: [f32; 3]) -> Self {
551 self.options.brush.center = center;
552 self
553 }
554
555 pub fn radius(mut self, radius: f32) -> Self {
557 self.options.brush.radius = radius;
558 self
559 }
560
561 pub fn falloff(mut self, falloff: Falloff) -> Self {
563 self.options.brush.falloff = falloff;
564 self
565 }
566
567 pub fn strength(mut self, strength: f32) -> Self {
570 self.options.brush.strength = strength;
571 self
572 }
573
574 pub fn direction(mut self, direction: [f32; 3]) -> Self {
576 self.options.direction = direction;
577 self
578 }
579
580 pub fn build(self) -> GrabOptions {
582 self.options
583 }
584}
585
586#[derive(Debug, Clone, Copy, Default, PartialEq)]
588pub struct GrabReport {
589 pub moved: usize,
591 pub max_displacement: f32,
594}
595
596fn check_grab(options: &GrabOptions) -> Result<()> {
597 let brush = &options.brush;
598 if !brush.radius.is_finite() || brush.radius <= 0.0 {
599 return Err(OpsError::InvalidInput(
600 "grab brush radius must be positive and finite",
601 ));
602 }
603 if brush.center.iter().any(|c| !c.is_finite()) {
604 return Err(OpsError::InvalidInput("grab brush center must be finite"));
605 }
606 if !brush.strength.is_finite() {
607 return Err(OpsError::InvalidInput("grab brush strength must be finite"));
608 }
609 if options.direction.iter().any(|c| !c.is_finite()) {
610 return Err(OpsError::InvalidInput("grab direction must be finite"));
611 }
612 Ok(())
613}
614
615pub fn grab(tx: &mut Transaction, options: &GrabOptions) -> Result<GrabReport> {
632 let brush = &options.brush;
633 check_grab(options)?;
634 let mesh = tx.mesh();
635 let mut report = GrabReport::default();
636 if mesh.vertex_slots() == 0 {
637 return Ok(report);
638 }
639 let positions = mesh.positions();
640 let mut ids: Vec<VertexId> = Vec::new();
641 let mut moved_to: Vec<[f32; 3]> = Vec::new();
642 for (v, p) in positions.iter().enumerate() {
643 let v = v as VertexId;
644 if !mesh.is_vertex_alive(v) {
645 continue;
646 }
647 let dist = brush
648 .center
649 .iter()
650 .zip(p)
651 .map(|(c, p)| (p - c).powi(2))
652 .sum::<f32>()
653 .sqrt();
654 let t = dist / brush.radius;
655 if !t.is_finite() || t >= 1.0 {
656 continue;
657 }
658 let w = brush.strength * falloff_value(brush.falloff, t);
659 let displaced = [
660 p[0] + w * options.direction[0],
661 p[1] + w * options.direction[1],
662 p[2] + w * options.direction[2],
663 ];
664 let length = options
665 .direction
666 .iter()
667 .map(|d| (w * d).powi(2))
668 .sum::<f32>()
669 .sqrt();
670 if !length.is_finite() || length <= 0.0 {
671 continue;
672 }
673 push(&mut ids, v)?;
674 push(&mut moved_to, displaced)?;
675 report.max_displacement = report.max_displacement.max(length);
676 }
677 report.moved = ids.len();
678 if !ids.is_empty() {
679 tx.set_positions(&ids, &moved_to)?;
680 }
681 Ok(report)
682}
683
684fn write_moves(tx: &mut Transaction, ids: &[VertexId], moved_to: &[[f32; 3]]) -> Result<()> {
686 if !ids.is_empty() {
687 tx.set_positions(ids, moved_to)?;
688 }
689 Ok(())
690}
691
692pub fn inflate_region(
703 tx: &mut Transaction,
704 options: &InflateOptions,
705 region: &[VertexId],
706) -> Result<InflateReport> {
707 let brush = &options.brush;
708 check_inflate(brush)?;
709 let mesh = tx.mesh();
710 let index = region_index(mesh)?;
711 let positions = mesh.positions();
712 let mut report = InflateReport::default();
713 let mut ids: Vec<VertexId> = Vec::new();
714 let mut moved_to: Vec<[f32; 3]> = Vec::new();
715 for &v in region {
716 if !mesh.is_vertex_alive(v) {
717 continue;
718 }
719 let p = &positions[v as usize];
720 let Some(w) = brush_weight(brush, p) else {
721 continue;
722 };
723 let mut n = [0.0f32; 3];
726 for &t in index.triangles(v) {
727 if let Some([a, b, c]) = mesh.triangle(t) {
728 let (pa, pb, pc) = (
729 positions[a as usize],
730 positions[b as usize],
731 positions[c as usize],
732 );
733 let e1 = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
734 let e2 = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
735 n[0] += e1[1] * e2[2] - e1[2] * e2[1];
736 n[1] += e1[2] * e2[0] - e1[0] * e2[2];
737 n[2] += e1[0] * e2[1] - e1[1] * e2[0];
738 }
739 }
740 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
741 if len > 0.0 {
742 n.iter_mut().for_each(|x| *x /= len);
743 }
744 let length = n.iter().map(|n| (w * n).powi(2)).sum::<f32>().sqrt();
745 if !length.is_finite() || length <= 0.0 {
746 continue;
747 }
748 push(&mut ids, v)?;
749 push(
750 &mut moved_to,
751 [p[0] + w * n[0], p[1] + w * n[1], p[2] + w * n[2]],
752 )?;
753 report.max_displacement = report.max_displacement.max(length);
754 }
755 report.moved = ids.len();
756 write_moves(tx, &ids, &moved_to)?;
757 Ok(report)
758}
759
760pub fn smooth_region(
766 tx: &mut Transaction,
767 options: &SmoothOptions,
768 region: &[VertexId],
769) -> Result<SmoothReport> {
770 #[derive(Clone, Copy)]
772 enum Source {
773 Member(u32),
774 Fixed([f32; 3]),
775 }
776 let brush = &options.brush;
777 check_smooth(brush)?;
778 let mut report = SmoothReport::default();
779 if options.cycles == 0 {
780 return Ok(report);
781 }
782 let mesh = tx.mesh();
783 let index = region_index(mesh)?;
784 let positions = mesh.positions();
785 let mut members: Vec<(VertexId, f32)> = Vec::new();
786 for &v in region {
787 if mesh.is_vertex_alive(v)
788 && let Some(w) = brush_weight(brush, &positions[v as usize])
789 {
790 push(&mut members, (v, w))?;
791 }
792 }
793 let mut starts: Vec<u32> = Vec::new();
796 reserve(&mut starts, members.len() + 1)?;
797 starts.push(0);
798 let mut sources: Vec<Source> = Vec::new();
799 let mut adjacent: Vec<VertexId> = Vec::new();
800 for &(v, _) in &members {
801 adjacent.clear();
802 for &t in index.triangles(v) {
803 if let Some([a, b, c]) = mesh.triangle(t)
804 && a != b
805 && b != c
806 && a != c
807 {
808 for u in [a, b, c] {
809 if u != v {
810 push(&mut adjacent, u)?;
811 }
812 }
813 }
814 }
815 adjacent.sort_unstable();
816 adjacent.dedup();
817 for &u in &adjacent {
818 let source = match members.binary_search_by_key(&u, |m| m.0) {
819 Ok(k) => Source::Member(k as u32),
820 Err(_) => Source::Fixed(positions[u as usize]),
821 };
822 push(&mut sources, source)?;
823 }
824 starts.push(sources.len() as u32);
825 }
826 let mut current: Vec<[f32; 3]> = Vec::new();
827 reserve(&mut current, members.len())?;
828 current.extend(members.iter().map(|&(v, _)| positions[v as usize]));
829 let mut before: Vec<[f32; 3]> = Vec::new();
830 reserve(&mut before, members.len())?;
831 let passes: &[f32] = match options.method {
832 SmoothMethod::Taubin => &[TAUBIN_LAMBDA, TAUBIN_MU],
833 SmoothMethod::Laplacian => &[1.0],
834 };
835 for _ in 0..options.cycles {
836 for &k in passes {
837 before.clear();
838 before.extend_from_slice(¤t);
839 for (m, &(_, w)) in members.iter().enumerate() {
840 let adjacent = &sources[starts[m] as usize..starts[m + 1] as usize];
841 if adjacent.is_empty() {
842 continue;
843 }
844 let mut centroid = [0.0f32; 3];
845 for source in adjacent {
846 let q = match *source {
847 Source::Member(u) => before[u as usize],
848 Source::Fixed(q) => q,
849 };
850 for axis in 0..3 {
851 centroid[axis] += q[axis];
852 }
853 }
854 for c in &mut centroid {
855 *c /= adjacent.len() as f32;
856 }
857 let p = before[m];
858 for axis in 0..3 {
859 current[m][axis] = p[axis] + w * k * (centroid[axis] - p[axis]);
860 }
861 }
862 }
863 }
864 let mut ids: Vec<VertexId> = Vec::new();
865 let mut moved_to: Vec<[f32; 3]> = Vec::new();
866 for (m, &(v, _)) in members.iter().enumerate() {
867 let length = (0..3)
868 .map(|k| (current[m][k] - positions[v as usize][k]).powi(2))
869 .sum::<f32>()
870 .sqrt();
871 if !length.is_finite() || length <= 0.0 {
872 continue;
873 }
874 push(&mut ids, v)?;
875 push(&mut moved_to, current[m])?;
876 report.max_displacement = report.max_displacement.max(length);
877 }
878 report.moved = ids.len();
879 write_moves(tx, &ids, &moved_to)?;
880 Ok(report)
881}
882
883pub fn grab_region(
891 tx: &mut Transaction,
892 options: &GrabOptions,
893 region: &[VertexId],
894) -> Result<GrabReport> {
895 let brush = &options.brush;
896 check_grab(options)?;
897 let mesh = tx.mesh();
898 let positions = mesh.positions();
899 let mut report = GrabReport::default();
900 let mut ids: Vec<VertexId> = Vec::new();
901 let mut moved_to: Vec<[f32; 3]> = Vec::new();
902 for &v in region {
903 if !mesh.is_vertex_alive(v) {
904 continue;
905 }
906 let p = &positions[v as usize];
907 let Some(w) = brush_weight(brush, p) else {
908 continue;
909 };
910 let d = options.direction;
911 let length = d.iter().map(|d| (w * d).powi(2)).sum::<f32>().sqrt();
912 if !length.is_finite() || length <= 0.0 {
913 continue;
914 }
915 push(&mut ids, v)?;
916 push(
917 &mut moved_to,
918 [p[0] + w * d[0], p[1] + w * d[1], p[2] + w * d[2]],
919 )?;
920 report.max_displacement = report.max_displacement.max(length);
921 }
922 report.moved = ids.len();
923 write_moves(tx, &ids, &moved_to)?;
924 Ok(report)
925}
926
927#[cfg(test)]
928mod tests {
929 use fast_mesh::compute_vertex_normals;
930 use fast_mesh_store::Mesh;
931
932 use super::*;
933 use crate::OpsError;
934 use crate::fail_allocations_after;
935 use crate::test_util::{edit, mesh_of, parts, sweep};
936
937 fn centered_grid() -> Mesh {
940 let mut positions = Vec::new();
941 for y in 0..3 {
942 for x in 0..3 {
943 positions.extend([x as f32 - 1.0, y as f32 - 1.0, 0.0]);
944 }
945 }
946 let mut triangles = Vec::new();
947 for y in 0..2u32 {
948 for x in 0..2u32 {
949 let a = y * 3 + x;
950 triangles.extend([[a, a + 1, a + 4], [a, a + 4, a + 3]]);
951 }
952 }
953 mesh_of(&positions, &triangles)
954 }
955
956 fn center_normal(mesh: &Mesh) -> [f32; 3] {
959 let (positions, triangles) = parts(mesh);
960 let indices: Vec<u32> = triangles.iter().flatten().copied().collect();
961 let normals = compute_vertex_normals(&positions, &indices).unwrap();
962 [normals[4 * 3], normals[4 * 3 + 1], normals[4 * 3 + 2]]
963 }
964
965 fn options(center: [f32; 3], radius: f32, falloff: Falloff, strength: f32) -> InflateOptions {
966 InflateOptions::builder()
967 .center(center)
968 .radius(radius)
969 .falloff(falloff)
970 .strength(strength)
971 .build()
972 }
973
974 fn displacement(old: &[f32], new: &[f32], id: usize) -> f32 {
976 (0..3)
977 .map(|k| (new[id * 3 + k] - old[id * 3 + k]).powi(2))
978 .sum::<f32>()
979 .sqrt()
980 }
981
982 #[test]
983 fn falloff_exact_values() {
984 assert!((falloff_value(Falloff::Linear, 0.5) - 0.5).abs() < 1e-6);
985 assert!((falloff_value(Falloff::Smooth, 0.0) - 1.0).abs() < 1e-6);
986 assert!((falloff_value(Falloff::Smooth, 1.0) - 0.0).abs() < 1e-6);
987 }
988
989 #[test]
990 fn falloff_boundary_values() {
991 assert!((falloff_value(Falloff::Linear, 0.0) - 1.0).abs() < 1e-6);
992 assert!((falloff_value(Falloff::Linear, 1.0) - 0.0).abs() < 1e-6);
993 assert!((falloff_value(Falloff::Smooth, 0.5) - 0.5).abs() < 1e-6);
994 }
995
996 #[test]
997 fn inflate_moves_center_along_normal() {
998 let mut mesh = centered_grid();
999 let old = parts(&mesh).0;
1000 let n = center_normal(&mesh);
1001 let options = options([0.0; 3], 10.0, Falloff::Linear, 2.0);
1002 let (report, empty) = edit(&mut mesh, |tx| inflate(tx, &options));
1003 assert!(!empty);
1004 assert_eq!(report.moved, 9);
1005 assert!((report.max_displacement - 2.0).abs() < 1e-4);
1006 let new = parts(&mesh).0;
1007 for k in 0..3 {
1008 assert!(
1009 (new[4 * 3 + k] - (old[4 * 3 + k] + 2.0 * n[k])).abs() < 1e-4,
1010 "center follows strength * normal"
1011 );
1012 }
1013 assert!(displacement(&old, &new, 0) < displacement(&old, &new, 4));
1014 }
1015
1016 #[test]
1017 fn inflate_negative_strength_deflates() {
1018 let mut mesh = centered_grid();
1019 let old = parts(&mesh).0;
1020 let n = center_normal(&mesh);
1021 let options = options([0.0; 3], 10.0, Falloff::Linear, -2.0);
1022 let (report, empty) = edit(&mut mesh, |tx| inflate(tx, &options));
1023 assert!(!empty);
1024 assert_eq!(report.moved, 9);
1025 assert!((report.max_displacement - 2.0).abs() < 1e-4);
1026 let new = parts(&mesh).0;
1027 for k in 0..3 {
1028 assert!(
1029 (new[4 * 3 + k] - (old[4 * 3 + k] - 2.0 * n[k])).abs() < 1e-4,
1030 "negative strength pulls along the normal"
1031 );
1032 }
1033 }
1034
1035 #[test]
1036 fn inflate_empty_region_moves_nothing() {
1037 let mut mesh = centered_grid();
1038 let before = parts(&mesh);
1039 let options = options([100.0, 0.0, 0.0], 1.0, Falloff::Linear, 2.0);
1040 let mut tx = mesh.begin_edit();
1041 let report = inflate(&mut tx, &options).unwrap();
1042 assert_eq!(report.moved, 0);
1043 assert_eq!(report.max_displacement, 0.0);
1044 assert!(tx.commit().unwrap().is_empty());
1045 assert_eq!(parts(&mesh), before);
1046 }
1047
1048 #[test]
1049 fn inflate_zero_radius_rejected() {
1050 let mut mesh = centered_grid();
1051 let options = options([0.0; 3], 0.0, Falloff::Linear, 2.0);
1052 let mut tx = mesh.begin_edit();
1053 assert!(matches!(
1054 inflate(&mut tx, &options),
1055 Err(OpsError::InvalidInput(_))
1056 ));
1057 assert!(tx.commit().unwrap().is_empty());
1058 }
1059
1060 #[test]
1061 fn inflate_negative_radius_rejected() {
1062 let mut mesh = centered_grid();
1063 let options = options([0.0; 3], -1.0, Falloff::Linear, 2.0);
1064 let mut tx = mesh.begin_edit();
1065 assert!(matches!(
1066 inflate(&mut tx, &options),
1067 Err(OpsError::InvalidInput(_))
1068 ));
1069 assert!(tx.commit().unwrap().is_empty());
1070 }
1071
1072 #[test]
1073 fn inflate_nan_strength_rejected() {
1074 let mut mesh = centered_grid();
1075 let options = options([0.0; 3], 10.0, Falloff::Linear, f32::NAN);
1076 let mut tx = mesh.begin_edit();
1077 assert!(matches!(
1078 inflate(&mut tx, &options),
1079 Err(OpsError::InvalidInput(_))
1080 ));
1081 assert!(tx.commit().unwrap().is_empty());
1082 }
1083
1084 fn smooth_options(
1085 center: [f32; 3],
1086 radius: f32,
1087 falloff: Falloff,
1088 strength: f32,
1089 cycles: u32,
1090 ) -> SmoothOptions {
1091 SmoothOptions::builder()
1092 .center(center)
1093 .radius(radius)
1094 .falloff(falloff)
1095 .strength(strength)
1096 .cycles(cycles)
1097 .build()
1098 }
1099
1100 #[test]
1101 fn taubin_pyramid_exact_after_one_cycle() {
1102 assert_eq!(TAUBIN_LAMBDA, 0.33);
1103 assert_eq!(TAUBIN_MU, -0.34);
1104 let positions = [
1109 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.5, 0.5, 1.0, ];
1115 let triangles = [[0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4]];
1116 let mut mesh = mesh_of(&positions, &triangles);
1117 let options = smooth_options([0.5, 0.5, 1.0], 0.1, Falloff::Linear, 1.0, 1);
1118 let (report, empty) = edit(&mut mesh, |tx| smooth(tx, &options));
1119 assert!(!empty);
1120 assert_eq!(report.moved, 1);
1121 let (flat, _) = parts(&mesh);
1122 assert!((flat[4 * 3 + 2] - 0.8978).abs() < 1e-4);
1126 assert_eq!(flat[4 * 3], 0.5);
1127 assert_eq!(flat[4 * 3 + 1], 0.5);
1128 for v in 0..4 {
1129 assert_eq!(&flat[v * 3..v * 3 + 3], &positions[v * 3..v * 3 + 3]);
1130 }
1131 assert!((report.max_displacement - (1.0 - 0.8978)).abs() < 1e-4);
1132 }
1133
1134 #[test]
1135 fn laplacian_pyramid_melts_to_centroid() {
1136 assert_eq!(
1137 SmoothOptions::builder().build().method,
1138 SmoothMethod::Taubin
1139 );
1140 let positions = [
1143 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.5, 0.5, 1.0, ];
1149 let triangles = [[0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4]];
1150 let mut mesh = mesh_of(&positions, &triangles);
1151 let options = SmoothOptions::builder()
1152 .center([0.5, 0.5, 1.0])
1153 .radius(0.1)
1154 .falloff(Falloff::Linear)
1155 .strength(1.0)
1156 .cycles(1)
1157 .method(SmoothMethod::Laplacian)
1158 .build();
1159 let (report, empty) = edit(&mut mesh, |tx| smooth(tx, &options));
1160 assert!(!empty);
1161 assert_eq!(report.moved, 1);
1162 let (flat, _) = parts(&mesh);
1163 assert_eq!(&flat[4 * 3..4 * 3 + 3], &[0.5, 0.5, 0.0]);
1165 assert!((report.max_displacement - 1.0).abs() < 1e-6);
1166 let mut mesh = mesh_of(&positions, &triangles);
1168 let options = SmoothOptions::builder()
1169 .center([0.5, 0.5, 1.0])
1170 .radius(0.1)
1171 .falloff(Falloff::Linear)
1172 .strength(0.5)
1173 .cycles(2)
1174 .method(SmoothMethod::Laplacian)
1175 .build();
1176 edit(&mut mesh, |tx| smooth(tx, &options).map(drop));
1177 let (flat, _) = parts(&mesh);
1178 assert!((flat[4 * 3 + 2] - 0.25).abs() < 1e-6);
1179 }
1180
1181 #[test]
1182 fn smooth_zero_cycles_moves_nothing() {
1183 let mut mesh = centered_grid();
1184 let before = parts(&mesh);
1185 let options = smooth_options([0.0; 3], 10.0, Falloff::Linear, 1.0, 0);
1186 let mut tx = mesh.begin_edit();
1187 let report = smooth(&mut tx, &options).unwrap();
1188 assert_eq!(report.moved, 0);
1189 assert_eq!(report.max_displacement, 0.0);
1190 assert!(tx.commit().unwrap().is_empty());
1191 assert_eq!(parts(&mesh), before);
1192 }
1193
1194 #[test]
1195 fn smooth_empty_region_moves_nothing() {
1196 let mut mesh = centered_grid();
1197 let before = parts(&mesh);
1198 let options = smooth_options([100.0, 0.0, 0.0], 1.0, Falloff::Linear, 1.0, 1);
1199 let mut tx = mesh.begin_edit();
1200 let report = smooth(&mut tx, &options).unwrap();
1201 assert_eq!(report.moved, 0);
1202 assert_eq!(report.max_displacement, 0.0);
1203 assert!(tx.commit().unwrap().is_empty());
1204 assert_eq!(parts(&mesh), before);
1205 }
1206
1207 #[test]
1208 fn smooth_invalid_brush_rejected() {
1209 let mut mesh = centered_grid();
1210 let cases = [
1211 smooth_options([0.0; 3], 0.0, Falloff::Linear, 1.0, 1),
1212 smooth_options([0.0; 3], -1.0, Falloff::Linear, 1.0, 1),
1213 smooth_options([0.0; 3], f32::NAN, Falloff::Linear, 1.0, 1),
1214 smooth_options([0.0; 3], 10.0, Falloff::Linear, f32::NAN, 1),
1215 smooth_options([f32::INFINITY, 0.0, 0.0], 10.0, Falloff::Linear, 1.0, 1),
1216 ];
1217 for options in cases {
1218 let mut tx = mesh.begin_edit();
1219 assert!(matches!(
1220 smooth(&mut tx, &options),
1221 Err(OpsError::InvalidInput(_))
1222 ));
1223 assert!(tx.commit().unwrap().is_empty());
1224 }
1225 }
1226
1227 #[test]
1228 fn smooth_nonmanifold_fan_moves_toward_centroid() {
1229 let positions = [
1232 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0,
1234 ];
1235 let triangles = [[0, 1, 2], [0, 3, 4]];
1236 let mut mesh = mesh_of(&positions, &triangles);
1237 let options = smooth_options([0.0, 0.0, 1.0], 0.1, Falloff::Linear, 1.0, 1);
1238 let (report, _) = edit(&mut mesh, |tx| smooth(tx, &options));
1239 assert_eq!(report.moved, 1);
1240 let (flat, _) = parts(&mesh);
1241 let dist = |p: &[f32]| ((p[0] - 0.5).powi(2) + (p[1] - 0.5).powi(2) + p[2].powi(2)).sqrt();
1242 assert!(dist(&flat[0..3]) < dist(&positions[0..3]));
1243 }
1244
1245 #[test]
1246 fn inflate_nonfinite_center_rejected() {
1247 let mut mesh = centered_grid();
1248 let options = options([f32::INFINITY, 0.0, 0.0], 10.0, Falloff::Linear, 2.0);
1249 let mut tx = mesh.begin_edit();
1250 assert!(matches!(
1251 inflate(&mut tx, &options),
1252 Err(OpsError::InvalidInput(_))
1253 ));
1254 assert!(tx.commit().unwrap().is_empty());
1255 }
1256
1257 #[test]
1258 fn inflate_empty_region_skips_temporaries() {
1259 let mut mesh = centered_grid();
1261 let before = parts(&mesh);
1262 let options = options([100.0, 0.0, 0.0], 1.0, Falloff::Linear, 2.0);
1263 let mut tx = mesh.begin_edit();
1264 fail_allocations_after(Some(0));
1265 let report = inflate(&mut tx, &options);
1266 fail_allocations_after(None);
1267 let report = report.unwrap();
1268 assert_eq!(report.moved, 0);
1269 assert_eq!(report.max_displacement, 0.0);
1270 assert!(tx.commit().unwrap().is_empty());
1271 assert_eq!(parts(&mesh), before);
1272 }
1273
1274 #[test]
1275 fn smooth_empty_region_skips_temporaries() {
1276 let mut mesh = centered_grid();
1278 let before = parts(&mesh);
1279 let options = smooth_options([100.0, 0.0, 0.0], 1.0, Falloff::Linear, 1.0, 1);
1280 let mut tx = mesh.begin_edit();
1281 fail_allocations_after(Some(0));
1282 let report = smooth(&mut tx, &options);
1283 fail_allocations_after(None);
1284 let report = report.unwrap();
1285 assert_eq!(report.moved, 0);
1286 assert_eq!(report.max_displacement, 0.0);
1287 assert!(tx.commit().unwrap().is_empty());
1288 assert_eq!(parts(&mesh), before);
1289 }
1290
1291 #[test]
1292 fn inflate_out_of_memory_is_reported_and_writes_nothing() {
1293 let options = options([0.0; 3], 10.0, Falloff::Linear, 2.0);
1294 assert!(sweep(centered_grid(), |tx| inflate(tx, &options).map(drop)) > 5);
1295 }
1296
1297 #[test]
1298 fn smooth_out_of_memory_is_reported_and_writes_nothing() {
1299 let options = smooth_options([0.0; 3], 10.0, Falloff::Linear, 1.0, 1);
1300 assert!(sweep(centered_grid(), |tx| smooth(tx, &options).map(drop)) > 5);
1301 }
1302
1303 fn grab_options(
1304 center: [f32; 3],
1305 radius: f32,
1306 falloff: Falloff,
1307 strength: f32,
1308 direction: [f32; 3],
1309 ) -> GrabOptions {
1310 GrabOptions::builder()
1311 .center(center)
1312 .radius(radius)
1313 .falloff(falloff)
1314 .strength(strength)
1315 .direction(direction)
1316 .build()
1317 }
1318
1319 #[test]
1320 fn grab_moves_region_along_direction() {
1321 let mut mesh = centered_grid();
1322 let old = parts(&mesh).0;
1323 let options = grab_options([0.0; 3], 10.0, Falloff::Linear, 1.0, [2.0, 0.0, 0.0]);
1324 let (report, empty) = edit(&mut mesh, |tx| grab(tx, &options));
1325 assert!(!empty);
1326 assert_eq!(report.moved, 9);
1327 assert!((report.max_displacement - 2.0).abs() < 1e-4);
1328 let new = parts(&mesh).0;
1329 for k in 0..3 {
1330 let want = [2.0, 0.0, 0.0][k];
1331 assert!(
1332 (new[4 * 3 + k] - (old[4 * 3 + k] + want)).abs() < 1e-4,
1333 "center follows strength * direction"
1334 );
1335 }
1336 assert!(displacement(&old, &new, 0) < displacement(&old, &new, 4));
1337 }
1338
1339 #[test]
1340 fn grab_strength_scales_the_drag() {
1341 let mut mesh = centered_grid();
1342 let old = parts(&mesh).0;
1343 let options = grab_options([0.0; 3], 10.0, Falloff::Linear, 0.5, [2.0, 0.0, 0.0]);
1344 let (report, _) = edit(&mut mesh, |tx| grab(tx, &options));
1345 assert_eq!(report.moved, 9);
1346 let new = parts(&mesh).0;
1347 assert!((new[4 * 3] - (old[4 * 3] + 1.0)).abs() < 1e-4);
1348 }
1349
1350 #[test]
1351 fn grab_zero_direction_moves_nothing() {
1352 let mut mesh = centered_grid();
1353 let before = parts(&mesh);
1354 let options = grab_options([0.0; 3], 10.0, Falloff::Linear, 1.0, [0.0; 3]);
1355 let mut tx = mesh.begin_edit();
1356 let report = grab(&mut tx, &options).unwrap();
1357 assert_eq!(report.moved, 0);
1358 assert_eq!(report.max_displacement, 0.0);
1359 assert!(tx.commit().unwrap().is_empty());
1360 assert_eq!(parts(&mesh), before);
1361 }
1362
1363 #[test]
1364 fn grab_empty_region_moves_nothing() {
1365 let mut mesh = centered_grid();
1366 let before = parts(&mesh);
1367 let options = grab_options(
1368 [100.0, 0.0, 0.0],
1369 1.0,
1370 Falloff::Linear,
1371 1.0,
1372 [2.0, 0.0, 0.0],
1373 );
1374 let mut tx = mesh.begin_edit();
1375 let report = grab(&mut tx, &options).unwrap();
1376 assert_eq!(report.moved, 0);
1377 assert_eq!(report.max_displacement, 0.0);
1378 assert!(tx.commit().unwrap().is_empty());
1379 assert_eq!(parts(&mesh), before);
1380 }
1381
1382 #[test]
1383 fn grab_invalid_brush_rejected() {
1384 let mut mesh = centered_grid();
1385 let cases = [
1386 grab_options([0.0; 3], 0.0, Falloff::Linear, 1.0, [1.0, 0.0, 0.0]),
1387 grab_options([0.0; 3], -1.0, Falloff::Linear, 1.0, [1.0, 0.0, 0.0]),
1388 grab_options([0.0; 3], f32::NAN, Falloff::Linear, 1.0, [1.0, 0.0, 0.0]),
1389 grab_options([0.0; 3], 10.0, Falloff::Linear, f32::NAN, [1.0, 0.0, 0.0]),
1390 grab_options(
1391 [f32::INFINITY, 0.0, 0.0],
1392 10.0,
1393 Falloff::Linear,
1394 1.0,
1395 [1.0, 0.0, 0.0],
1396 ),
1397 grab_options([0.0; 3], 10.0, Falloff::Linear, 1.0, [f32::NAN, 0.0, 0.0]),
1398 grab_options(
1399 [0.0; 3],
1400 10.0,
1401 Falloff::Linear,
1402 1.0,
1403 [0.0, f32::INFINITY, 0.0],
1404 ),
1405 ];
1406 for options in cases {
1407 let mut tx = mesh.begin_edit();
1408 assert!(matches!(
1409 grab(&mut tx, &options),
1410 Err(OpsError::InvalidInput(_))
1411 ));
1412 assert!(tx.commit().unwrap().is_empty());
1413 }
1414 }
1415
1416 #[test]
1417 fn grab_empty_region_skips_temporaries() {
1418 let mut mesh = centered_grid();
1420 let before = parts(&mesh);
1421 let options = grab_options(
1422 [100.0, 0.0, 0.0],
1423 1.0,
1424 Falloff::Linear,
1425 1.0,
1426 [2.0, 0.0, 0.0],
1427 );
1428 let mut tx = mesh.begin_edit();
1429 fail_allocations_after(Some(0));
1430 let report = grab(&mut tx, &options);
1431 fail_allocations_after(None);
1432 let report = report.unwrap();
1433 assert_eq!(report.moved, 0);
1434 assert_eq!(report.max_displacement, 0.0);
1435 assert!(tx.commit().unwrap().is_empty());
1436 assert_eq!(parts(&mesh), before);
1437 }
1438
1439 #[test]
1440 fn grab_out_of_memory_is_reported_and_writes_nothing() {
1441 let options = grab_options([0.0; 3], 10.0, Falloff::Linear, 1.0, [2.0, 0.0, 0.0]);
1442 assert!(sweep(centered_grid(), |tx| grab(tx, &options).map(drop)) > 5);
1443 }
1444
1445 fn wavy_grid(n: u32) -> Mesh {
1447 let mut positions = Vec::new();
1448 for y in 0..=n {
1449 for x in 0..=n {
1450 let (fx, fy) = (x as f32, y as f32);
1451 positions.extend([fx, fy, (fx * 0.7).sin() * (fy * 0.5).cos()]);
1452 }
1453 }
1454 let mut triangles = Vec::new();
1455 for y in 0..n {
1456 for x in 0..n {
1457 let a = y * (n + 1) + x;
1458 triangles.extend([[a, a + 1, a + n + 2], [a, a + n + 2, a + n + 1]]);
1459 }
1460 }
1461 let mut mesh = mesh_of(&positions, &triangles);
1462 let mut tx = mesh.begin_edit();
1463 tx.remove_triangles(&[10]).unwrap();
1464 tx.commit().unwrap();
1465 mesh.ensure_incidence().unwrap();
1466 mesh
1467 }
1468
1469 fn every_slot(mesh: &Mesh) -> Vec<VertexId> {
1471 (0..mesh.vertex_slots() as VertexId).collect()
1472 }
1473
1474 fn after<R>(mesh: &mut Mesh, f: impl FnOnce(&mut Transaction) -> Result<R>) -> (R, Vec<f32>) {
1476 let (r, _) = edit(mesh, f);
1477 (r, parts(mesh).0)
1478 }
1479
1480 fn bits(v: &[f32]) -> Vec<u32> {
1481 v.iter().map(|x| x.to_bits()).collect()
1482 }
1483
1484 #[test]
1485 fn region_functions_match_the_whole_mesh_ones_bit_for_bit() {
1486 let brush = Brush {
1487 center: [5.3, 4.1, 0.0],
1488 radius: 3.2,
1489 falloff: Falloff::Smooth,
1490 strength: 0.4,
1491 };
1492 let near = |mesh: &Mesh| -> Vec<VertexId> {
1494 (0..mesh.vertex_slots() as VertexId)
1495 .filter(|&v| brush_weight(&brush, &mesh.positions()[v as usize]).is_some())
1496 .collect()
1497 };
1498 let inflate_options = InflateOptions { brush };
1499 let (mut a, mut b, mut c) = (wavy_grid(12), wavy_grid(12), wavy_grid(12));
1500 let (ra, pa) = after(&mut a, |tx| inflate(tx, &inflate_options));
1501 let region = near(&b);
1502 let (rb, pb) = after(&mut b, |tx| inflate_region(tx, &inflate_options, ®ion));
1503 let region = every_slot(&c);
1504 let (rc, pc) = after(&mut c, |tx| inflate_region(tx, &inflate_options, ®ion));
1505 assert_eq!(ra, rb);
1506 assert_eq!(ra, rc);
1507 assert_eq!(bits(&pa), bits(&pb));
1508 assert_eq!(bits(&pa), bits(&pc));
1509
1510 for method in [SmoothMethod::Taubin, SmoothMethod::Laplacian] {
1511 let smooth_options = SmoothOptions {
1512 brush,
1513 cycles: 3,
1514 method,
1515 };
1516 let (mut a, mut b) = (wavy_grid(12), wavy_grid(12));
1517 let (ra, pa) = after(&mut a, |tx| smooth(tx, &smooth_options));
1518 let region = near(&b);
1519 let (rb, pb) = after(&mut b, |tx| smooth_region(tx, &smooth_options, ®ion));
1520 assert_eq!(ra, rb);
1521 assert_eq!(bits(&pa), bits(&pb));
1522 }
1523
1524 let grab_options = GrabOptions {
1525 brush,
1526 direction: [0.3, -0.2, 0.9],
1527 };
1528 let (mut a, mut b) = (wavy_grid(12), wavy_grid(12));
1529 let (ra, pa) = after(&mut a, |tx| grab(tx, &grab_options));
1530 let region = near(&b);
1531 let (rb, pb) = after(&mut b, |tx| grab_region(tx, &grab_options, ®ion));
1532 assert_eq!(ra, rb);
1533 assert_eq!(bits(&pa), bits(&pb));
1534 }
1535
1536 #[test]
1537 fn region_functions_ignore_candidates_outside_the_brush() {
1538 let mut mesh = wavy_grid(6);
1539 let options = GrabOptions {
1540 brush: Brush {
1541 center: [0.0, 0.0, 0.0],
1542 radius: 1.5,
1543 falloff: Falloff::Smooth,
1544 strength: 1.0,
1545 },
1546 direction: [0.0, 0.0, 1.0],
1547 };
1548 let region = every_slot(&mesh);
1549 let (report, _) = after(&mut mesh, |tx| grab_region(tx, &options, ®ion));
1550 assert!(report.moved > 0 && report.moved < 8);
1551 }
1552
1553 #[test]
1554 fn region_functions_need_the_incidence_index_and_validate() {
1555 let mut mesh = centered_grid();
1556 let region = every_slot(&mesh);
1557 let good = InflateOptions::default();
1558 let mut tx = mesh.begin_edit();
1559 assert!(matches!(
1560 inflate_region(&mut tx, &good, ®ion),
1561 Err(OpsError::InvalidInput(_))
1562 ));
1563 drop(tx);
1564 mesh.ensure_incidence().unwrap();
1565 let bad = InflateOptions::builder().radius(0.0).build();
1566 let mut tx = mesh.begin_edit();
1567 assert!(matches!(
1568 inflate_region(&mut tx, &bad, ®ion),
1569 Err(OpsError::InvalidInput(_))
1570 ));
1571 let bad = SmoothOptions::builder().radius(-1.0).cycles(1).build();
1572 assert!(matches!(
1573 smooth_region(&mut tx, &bad, ®ion),
1574 Err(OpsError::InvalidInput(_))
1575 ));
1576 let bad = GrabOptions::builder().direction([f32::NAN; 3]).build();
1577 assert!(matches!(
1578 grab_region(&mut tx, &bad, ®ion),
1579 Err(OpsError::InvalidInput(_))
1580 ));
1581 }
1582
1583 fn max_normal_step(mesh: &Mesh, n: u32, center: f32, radius: f32, band: (f32, f32)) -> f32 {
1586 let (positions, triangles) = parts(mesh);
1587 let indices: Vec<u32> = triangles.iter().flatten().copied().collect();
1588 let normals = compute_vertex_normals(&positions, &indices).unwrap();
1589 let at = |x: u32, y: u32| ((y * (n + 1) + x) * 3) as usize;
1590 let mut worst = 0.0f32;
1591 for y in 1..n {
1592 for x in 1..n {
1593 let t = (x as f32 - center).hypot(y as f32 - center) / radius;
1594 if t < band.0 || t > band.1 {
1595 continue;
1596 }
1597 let a = at(x, y);
1598 for b in [at(x + 1, y), at(x, y + 1)] {
1599 let dot: f32 = (0..3).map(|k| normals[a + k] * normals[b + k]).sum();
1600 worst = worst.max(dot.clamp(-1.0, 1.0).acos());
1601 }
1602 }
1603 }
1604 worst
1605 }
1606
1607 fn flat_dabbed(n: u32, radius: f32, falloff: Falloff) -> Mesh {
1609 let mut positions = Vec::new();
1610 for y in 0..=n {
1611 for x in 0..=n {
1612 positions.extend([x as f32, y as f32, 0.0]);
1613 }
1614 }
1615 let mut triangles = Vec::new();
1616 for y in 0..n {
1617 for x in 0..n {
1618 let a = y * (n + 1) + x;
1619 triangles.extend([[a, a + 1, a + n + 2], [a, a + n + 2, a + n + 1]]);
1620 }
1621 }
1622 let mut mesh = mesh_of(&positions, &triangles);
1623 let c = n as f32 / 2.0;
1624 let options = InflateOptions::builder()
1625 .center([c, c, 0.0])
1626 .radius(radius)
1627 .falloff(falloff)
1628 .strength(radius * 0.25)
1629 .build();
1630 edit(&mut mesh, |tx| inflate(tx, &options));
1631 mesh
1632 }
1633
1634 #[test]
1635 fn smooth_falloff_has_no_crease_at_the_rim() {
1636 let rim_step = |n: u32, falloff: Falloff| {
1642 let radius = n as f32 / 4.0;
1643 let band = (1.0 - 1.5 / radius, 1.0 + 1.5 / radius);
1644 let mesh = flat_dabbed(n, radius, falloff);
1645 max_normal_step(&mesh, n, n as f32 / 2.0, radius, band)
1646 };
1647 let (smooth_coarse, smooth_fine) = (
1648 rim_step(160, Falloff::Smooth),
1649 rim_step(320, Falloff::Smooth),
1650 );
1651 let (linear_coarse, linear_fine) = (
1652 rim_step(160, Falloff::Linear),
1653 rim_step(320, Falloff::Linear),
1654 );
1655 assert!(smooth_coarse < 0.06, "smooth rim step {smooth_coarse}");
1656 assert!(
1657 smooth_fine < 0.6 * smooth_coarse,
1658 "smooth rim step shrinks with the edge: {smooth_coarse} to {smooth_fine}"
1659 );
1660 assert!(linear_coarse > 0.15, "linear rim step {linear_coarse}");
1661 assert!(
1662 linear_fine > 0.9 * linear_coarse,
1663 "linear rim step stays: {linear_coarse} to {linear_fine}"
1664 );
1665 }
1666}