Skip to main content

fast_mesh_scene/
sculpt.rs

1//! Brush dabs whose cost follows the brush region, not the mesh.
2
3use fast_mesh_ops::deform::{
4    Brush, GrabOptions, InflateOptions, SmoothOptions, grab, grab_region, inflate, inflate_region,
5    smooth, smooth_region,
6};
7use fast_mesh_ops::{check_protection, with_protection};
8use fast_mesh_query::{Region, vertices_in};
9use fast_mesh_store::VertexId;
10
11use crate::scene::Entry;
12use crate::{MeshId, Result, Scene, SceneError, UpdateMode};
13
14/// One brush dab for [`Scene::dab`].
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum Dab {
17    /// [`inflate`](fast_mesh_ops::deform::inflate): along the smooth normals.
18    Inflate(InflateOptions),
19    /// [`smooth`](fast_mesh_ops::deform::smooth): toward the neighbors' centroid.
20    Smooth(SmoothOptions),
21    /// [`grab`](fast_mesh_ops::deform::grab): along one vector.
22    Grab(GrabOptions),
23}
24
25impl Dab {
26    fn brush(&self) -> &Brush {
27        match self {
28            Dab::Inflate(o) => &o.brush,
29            Dab::Smooth(o) => &o.brush,
30            Dab::Grab(o) => &o.brush,
31        }
32    }
33}
34
35/// What [`Scene::dab`] did.
36#[derive(Debug, Clone, Copy, Default, PartialEq)]
37pub struct DabReport {
38    /// How many vertices moved (displacement above 0).
39    pub moved: usize,
40    /// The largest displacement applied, in local units; 0 when nothing moved.
41    pub max_displacement: f32,
42}
43
44/// The ascending vertices under `brush`, through the mesh's BVH, with the
45/// incidence index the region deforms read. `None` when the brush cannot be
46/// searched for (an invalid one, which the whole-mesh deform then reports) or
47/// the BVH cannot serve: stale under Manual, or a failed update.
48fn region_of(entry: &mut Entry, brush: &Brush) -> Option<Vec<VertexId>> {
49    let valid = brush.radius.is_finite()
50        && brush.radius > 0.0
51        && brush.center.iter().all(|c| c.is_finite())
52        && brush.strength.is_finite();
53    if !valid || (entry.mode == UpdateMode::Manual && !entry.is_current()) {
54        return None;
55    }
56    entry.update_bvh().ok()?;
57    entry.mesh.ensure_incidence().ok()?;
58    let mut region = Vec::new();
59    let sphere = Region::Sphere {
60        center: brush.center,
61        radius: brush.radius,
62    };
63    vertices_in(&entry.mesh, &entry.bvh, &sphere, &mut region).ok()?;
64    Some(region)
65}
66
67/// Whether every vertex of the ascending `region` is listed at weight 0.
68fn fully_held(region: &[VertexId], vertices: &[VertexId], weights: &[f32]) -> bool {
69    let mut left = region.len();
70    let mut seen = vec![false; region.len()];
71    for (v, w) in vertices.iter().zip(weights) {
72        if *w == 0.0
73            && let Ok(k) = region.binary_search(v)
74            && !seen[k]
75        {
76            seen[k] = true;
77            left -= 1;
78        }
79    }
80    left == 0
81}
82
83impl Scene {
84    /// Applies one brush dab as one edit: the same history, reports and
85    /// failure behaviour as [`Scene::edit`] with the matching
86    /// [`deform`](fast_mesh_ops::deform) function, and the same result, bit
87    /// for bit, except that a vertex no triangle uses is out of the brush's
88    /// reach. The cost follows the vertices under the brush, plus a walk down
89    /// the BVH, however large the mesh is; where the BVH cannot serve (stale
90    /// under [`UpdateMode::Manual`]) it scans the mesh instead.
91    ///
92    /// The first dab on a mesh builds its vertex-to-triangles index, once.
93    ///
94    /// # Errors
95    /// As for [`Scene::edit`] with the deform function:
96    /// [`SceneError::UnknownMesh`] for a stale id, [`SceneError::Ops`] for an
97    /// invalid brush, [`SceneError::Store`] when memory runs out. Nothing
98    /// changes in any of these cases.
99    pub fn dab(&mut self, id: MeshId, dab: &Dab) -> Result<DabReport> {
100        self.dab_protected(id, dab, &[], &[])
101    }
102
103    /// [`Scene::dab`] under [`with_protection`]: each listed vertex's move is
104    /// scaled by its weight, at a cost that follows the brush region plus the
105    /// list. List only what the brush can reach.
106    ///
107    /// A dab with every vertex under its brush held fully (weight 0) moves
108    /// nothing, so it edits nothing either: it reports 0 and leaves no undo step.
109    ///
110    /// # Errors
111    /// As for [`Scene::dab`], and as [`check_protection`] for the list.
112    /// Nothing changes in any of these cases.
113    pub fn dab_protected(
114        &mut self,
115        id: MeshId,
116        dab: &Dab,
117        vertices: &[VertexId],
118        weights: &[f32],
119    ) -> Result<DabReport> {
120        self.drop_field_unit();
121        let entry = self.entries.get_mut(id).ok_or(SceneError::UnknownMesh)?;
122        let region = region_of(entry, dab.brush());
123        check_protection(&entry.mesh, vertices, weights)?;
124        if let Some(r) = &region
125            && !vertices.is_empty()
126            && fully_held(r, vertices, weights)
127        {
128            return Ok(DabReport::default());
129        }
130        self.edit(id, |tx| -> std::result::Result<DabReport, SceneError> {
131            let (moved, max_displacement) = with_protection(tx, vertices, weights, |tx| {
132                Ok(match (dab, &region) {
133                    (Dab::Inflate(o), Some(r)) => {
134                        let r = inflate_region(tx, o, r)?;
135                        (r.moved, r.max_displacement)
136                    }
137                    (Dab::Inflate(o), None) => {
138                        let r = inflate(tx, o)?;
139                        (r.moved, r.max_displacement)
140                    }
141                    (Dab::Smooth(o), Some(r)) => {
142                        let r = smooth_region(tx, o, r)?;
143                        (r.moved, r.max_displacement)
144                    }
145                    (Dab::Smooth(o), None) => {
146                        let r = smooth(tx, o)?;
147                        (r.moved, r.max_displacement)
148                    }
149                    (Dab::Grab(o), Some(r)) => {
150                        let r = grab_region(tx, o, r)?;
151                        (r.moved, r.max_displacement)
152                    }
153                    (Dab::Grab(o), None) => {
154                        let r = grab(tx, o)?;
155                        (r.moved, r.max_displacement)
156                    }
157                })
158            })?;
159            Ok(DabReport {
160                moved,
161                max_displacement,
162            })
163        })
164    }
165}
166
167#[cfg(test)]
168mod protected_tests {
169    use fast_mesh_ops::deform::Falloff;
170    use fast_mesh_store::Mesh;
171
172    use super::*;
173    use crate::{MeshOptions, SceneOptions};
174
175    /// A flat `n x n` grid, with its vertices left of the middle held.
176    fn setup(n: u32) -> (Scene, MeshId, Vec<VertexId>, Dab) {
177        let mut positions = Vec::new();
178        for y in 0..=n {
179            for x in 0..=n {
180                positions.extend([x as f32, y as f32, 0.0]);
181            }
182        }
183        let mut indices = Vec::new();
184        for y in 0..n {
185            for x in 0..n {
186                let a = y * (n + 1) + x;
187                indices.extend([a, a + 1, a + n + 2, a, a + n + 2, a + n + 1]);
188            }
189        }
190        let mut scene = Scene::new(SceneOptions::default());
191        let id = scene
192            .add_mesh(
193                Mesh::from_buffers(&positions, &indices).unwrap(),
194                MeshOptions::default(),
195            )
196            .unwrap();
197        let local = scene.mesh(id).unwrap().positions().to_vec();
198        let middle = local[((n / 2) * (n + 1) + n / 2) as usize];
199        let held = local
200            .iter()
201            .enumerate()
202            .filter(|(_, p)| p[0] < middle[0])
203            .map(|(v, _)| v as VertexId)
204            .collect();
205        let dab = Dab::Inflate(InflateOptions {
206            brush: Brush {
207                center: middle,
208                radius: n as f32 / 4.0,
209                falloff: Falloff::Smooth,
210                strength: 0.5,
211            },
212        });
213        (scene, id, held, dab)
214    }
215
216    #[test]
217    fn holds_listed_vertices_and_matches_the_transaction_form() {
218        let (mut a, id_a, held, dab) = setup(20);
219        let weights = vec![0.0; held.len()];
220        let before = a.mesh(id_a).unwrap().positions().to_vec();
221        let report = a.dab_protected(id_a, &dab, &held, &weights).unwrap();
222        assert!(report.moved > 0);
223        let after = a.mesh(id_a).unwrap().positions().to_vec();
224        for &v in &held {
225            let (p, q) = (before[v as usize], after[v as usize]);
226            assert_eq!(p.map(f32::to_bits), q.map(f32::to_bits));
227        }
228        assert_ne!(before, after);
229        // The same as the whole-mesh deform under the same protection, bit for bit.
230        let (mut b, id_b, _, _) = setup(20);
231        let Dab::Inflate(o) = dab else { unreachable!() };
232        b.edit(id_b, |tx| {
233            with_protection(tx, &held, &weights, |tx| inflate(tx, &o))
234        })
235        .unwrap();
236        let other = b.mesh(id_b).unwrap().positions().to_vec();
237        assert_eq!(after, other);
238        // One undo step, restoring everything.
239        a.undo().unwrap();
240        assert_eq!(a.mesh(id_a).unwrap().positions(), before.as_slice());
241    }
242
243    #[test]
244    fn a_fully_held_brush_edits_nothing() {
245        let (mut scene, id, _, dab) = setup(20);
246        let all: Vec<VertexId> = (0..scene.mesh(id).unwrap().vertex_slots() as u32).collect();
247        let weights = vec![0.0; all.len()];
248        let report = scene.dab_protected(id, &dab, &all, &weights).unwrap();
249        assert_eq!(report, DabReport::default());
250        assert!(!scene.can_undo());
251    }
252
253    #[test]
254    fn a_soft_weight_is_not_fully_held() {
255        let (mut scene, id, _, dab) = setup(20);
256        let all: Vec<VertexId> = (0..scene.mesh(id).unwrap().vertex_slots() as u32).collect();
257        let weights = vec![0.5; all.len()];
258        assert!(scene.dab_protected(id, &dab, &all, &weights).unwrap().moved > 0);
259        assert!(scene.can_undo());
260    }
261
262    #[test]
263    fn rejects_a_bad_list_and_changes_nothing() {
264        let (mut scene, id, _, dab) = setup(4);
265        assert!(scene.dab_protected(id, &dab, &[9999], &[0.0]).is_err());
266        assert!(scene.dab_protected(id, &dab, &[0], &[2.0]).is_err());
267        assert!(scene.dab_protected(id, &dab, &[0], &[]).is_err());
268        assert!(!scene.can_undo());
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use fast_mesh_ops::deform::{Falloff, SmoothMethod};
275    use fast_mesh_store::Mesh;
276
277    use super::*;
278    use crate::{MeshOptions, SceneOptions};
279
280    fn terrain(n: u32) -> Mesh {
281        let mut positions = Vec::new();
282        for y in 0..=n {
283            for x in 0..=n {
284                let (fx, fy) = (x as f32, y as f32);
285                positions.extend([fx, fy, (fx * 0.3).sin() * (fy * 0.2).cos()]);
286            }
287        }
288        let mut indices = Vec::new();
289        for y in 0..n {
290            for x in 0..n {
291                let a = y * (n + 1) + x;
292                indices.extend([a, a + 1, a + n + 2, a, a + n + 2, a + n + 1]);
293            }
294        }
295        Mesh::from_buffers(&positions, &indices).unwrap()
296    }
297
298    fn scene_with(mode: UpdateMode) -> (Scene, MeshId) {
299        let mut scene = Scene::new(SceneOptions::default());
300        let options = MeshOptions {
301            mode,
302            ..MeshOptions::default()
303        };
304        let id = scene.add_mesh(terrain(24), options).unwrap();
305        (scene, id)
306    }
307
308    fn brush(center: [f32; 3]) -> Brush {
309        Brush {
310            center,
311            radius: 3.5,
312            falloff: Falloff::Smooth,
313            strength: 0.4,
314        }
315    }
316
317    fn dabs() -> Vec<Dab> {
318        let mut out = Vec::new();
319        for k in 0..12 {
320            let b = brush([6.0 + k as f32, 9.0, 0.5]);
321            out.push(match k % 3 {
322                0 => Dab::Inflate(InflateOptions { brush: b }),
323                1 => Dab::Smooth(SmoothOptions {
324                    brush: b,
325                    cycles: 2,
326                    method: SmoothMethod::Laplacian,
327                }),
328                _ => Dab::Grab(GrabOptions {
329                    brush: b,
330                    direction: [0.2, 0.1, 0.5],
331                }),
332            });
333        }
334        out
335    }
336
337    fn whole(scene: &mut Scene, id: MeshId, dab: &Dab) -> DabReport {
338        scene
339            .edit(id, |tx| -> std::result::Result<_, SceneError> {
340                Ok(match dab {
341                    Dab::Inflate(o) => {
342                        let r = inflate(tx, o)?;
343                        DabReport {
344                            moved: r.moved,
345                            max_displacement: r.max_displacement,
346                        }
347                    }
348                    Dab::Smooth(o) => {
349                        let r = smooth(tx, o)?;
350                        DabReport {
351                            moved: r.moved,
352                            max_displacement: r.max_displacement,
353                        }
354                    }
355                    Dab::Grab(o) => {
356                        let r = grab(tx, o)?;
357                        DabReport {
358                            moved: r.moved,
359                            max_displacement: r.max_displacement,
360                        }
361                    }
362                })
363            })
364            .unwrap()
365    }
366
367    fn bits(scene: &Scene, id: MeshId) -> Vec<[u32; 3]> {
368        scene
369            .mesh(id)
370            .unwrap()
371            .positions()
372            .iter()
373            .map(|p| p.map(f32::to_bits))
374            .collect()
375    }
376
377    #[test]
378    fn dabs_match_the_whole_mesh_deforms_bit_for_bit_in_every_mode() {
379        for mode in [UpdateMode::Eager, UpdateMode::Lazy, UpdateMode::Manual] {
380            let (mut a, ia) = scene_with(mode);
381            let (mut b, ib) = scene_with(mode);
382            for dab in dabs() {
383                let ra = a.dab(ia, &dab).unwrap();
384                let rb = whole(&mut b, ib, &dab);
385                assert_eq!(ra, rb, "{mode:?}");
386                assert_eq!(bits(&a, ia), bits(&b, ib), "{mode:?}");
387            }
388        }
389    }
390
391    #[test]
392    fn a_dab_is_one_undo_step_and_reports_its_change() {
393        let (mut scene, id) = scene_with(UpdateMode::Eager);
394        let before = bits(&scene, id);
395        scene.take_changes();
396        let report = scene
397            .dab(
398                id,
399                &Dab::Inflate(InflateOptions {
400                    brush: brush([12.0, 12.0, 0.0]),
401                }),
402            )
403            .unwrap();
404        assert!(report.moved > 0);
405        assert_eq!(scene.take_changes().len(), 1);
406        assert_ne!(bits(&scene, id), before);
407        scene.undo().unwrap();
408        assert_eq!(bits(&scene, id), before);
409        assert!(!scene.can_undo());
410    }
411
412    #[test]
413    fn dabs_in_an_open_step_undo_together() {
414        let (mut scene, id) = scene_with(UpdateMode::Eager);
415        let before = bits(&scene, id);
416        scene.begin_step();
417        for dab in dabs() {
418            scene.dab(id, &dab).unwrap();
419        }
420        scene.end_step().unwrap();
421        assert_ne!(bits(&scene, id), before);
422        scene.undo().unwrap();
423        assert_eq!(bits(&scene, id), before);
424    }
425
426    #[test]
427    fn an_empty_region_records_nothing_and_a_bad_brush_fails() {
428        let (mut scene, id) = scene_with(UpdateMode::Eager);
429        let far = Dab::Grab(GrabOptions {
430            brush: brush([1000.0, 0.0, 0.0]),
431            direction: [0.0, 0.0, 1.0],
432        });
433        assert_eq!(scene.dab(id, &far).unwrap(), DabReport::default());
434        assert!(!scene.can_undo());
435        let mut bad = brush([0.0; 3]);
436        bad.radius = -1.0;
437        assert!(matches!(
438            scene.dab(id, &Dab::Inflate(InflateOptions { brush: bad })),
439            Err(SceneError::Ops(_))
440        ));
441    }
442
443    #[test]
444    fn a_stale_manual_mesh_still_dabs_by_scanning() {
445        let (mut scene, id) = scene_with(UpdateMode::Manual);
446        let dab = Dab::Inflate(InflateOptions {
447            brush: brush([12.0, 12.0, 0.0]),
448        });
449        scene.dab(id, &dab).unwrap();
450        // The BVH is stale now: the next dab cannot use it, and must not fail.
451        assert!(scene.dab(id, &dab).unwrap().moved > 0);
452    }
453
454    #[test]
455    fn a_dab_reaches_vertices_grown_into_the_region_by_earlier_dabs() {
456        // Drag a lump far from where the BVH was built, then dab where it is
457        // now: the region search must follow the moved surface.
458        let (mut scene, id) = scene_with(UpdateMode::Eager);
459        let mut at = [12.0, 12.0, 0.0];
460        for _ in 0..30 {
461            let dab = Dab::Grab(GrabOptions {
462                brush: Brush {
463                    center: at,
464                    radius: 2.0,
465                    falloff: Falloff::Smooth,
466                    strength: 1.0,
467                },
468                direction: [0.0, 0.0, 1.0],
469            });
470            assert!(scene.dab(id, &dab).unwrap().moved > 0);
471            at[2] += 1.0;
472        }
473        let top = scene
474            .mesh(id)
475            .unwrap()
476            .positions()
477            .iter()
478            .map(|p| p[2])
479            .fold(f32::MIN, f32::max);
480        assert!(top > 25.0, "the lump followed: {top}");
481    }
482}