Skip to main content

fast_mesh_ops/
protect.rs

1//! Per-vertex protection around a position-moving operation.
2//!
3//! [`with_protection`] runs any operation in a transaction, then scales back
4//! what it did to the listed vertices. It knows nothing of the operation's
5//! insides, so brushes keep one code path whether or not anything is protected.
6
7use fast_mesh_store::{Mesh, Transaction, VertexId};
8
9use crate::{OpsError, Result, reserve};
10
11/// Runs `op` in `tx`, then scales each listed vertex's move by its weight:
12/// `0` leaves the vertex at the bits it had before `op`, `1` keeps the whole
13/// move, and values in between land on the straight line between the two.
14///
15/// Vertices that end where they started are not written. `op` must leave the
16/// listed vertices alive. What `op` reports describes its own move, before
17/// the scaling: a neighbor it computed against a protected vertex's moved
18/// position keeps that result.
19///
20/// # Errors
21/// - [`OpsError::InvalidInput`] when the lists differ in length, a weight is
22///   not in `[0, 1]`, or a listed vertex is not alive; `op` has not run then.
23/// - Whatever `op` returns, and [`OpsError::Store`] when memory runs out. The
24///   caller's transaction should then be rolled back.
25pub fn with_protection<R>(
26    tx: &mut Transaction,
27    vertices: &[VertexId],
28    weights: &[f32],
29    op: impl FnOnce(&mut Transaction) -> Result<R>,
30) -> Result<R> {
31    check_protection(tx.mesh(), vertices, weights)?;
32    let positions = tx.mesh().positions();
33    let mut before: Vec<[f32; 3]> = Vec::new();
34    reserve(&mut before, vertices.len())?;
35    before.extend(vertices.iter().map(|&v| positions[v as usize]));
36    // Reserved before `op` writes anything: running out of memory here leaves
37    // the transaction untouched, like every operation.
38    let mut ids: Vec<VertexId> = Vec::new();
39    reserve(&mut ids, vertices.len())?;
40    let mut back: Vec<[f32; 3]> = Vec::new();
41    reserve(&mut back, vertices.len())?;
42    let report = op(tx)?;
43    let positions = tx.mesh().positions();
44    for ((&v, &w), from) in vertices.iter().zip(weights).zip(&before) {
45        let to = positions[v as usize];
46        if w == 1.0 || same_bits(&to, from) {
47            continue;
48        }
49        let at = if w == 0.0 {
50            *from
51        } else {
52            [
53                from[0] + w * (to[0] - from[0]),
54                from[1] + w * (to[1] - from[1]),
55                from[2] + w * (to[2] - from[2]),
56            ]
57        };
58        ids.push(v);
59        back.push(at);
60    }
61    if !ids.is_empty() {
62        tx.set_positions(&ids, &back)?;
63    }
64    Ok(report)
65}
66
67/// Checks a protection list the way [`with_protection`] does.
68///
69/// # Errors
70/// [`OpsError::InvalidInput`] when the lists differ in length, a weight is not
71/// in `[0, 1]`, or a listed vertex is not alive.
72pub fn check_protection(mesh: &Mesh, vertices: &[VertexId], weights: &[f32]) -> Result<()> {
73    if vertices.len() != weights.len() {
74        return Err(OpsError::InvalidInput(
75            "protection needs one weight per vertex",
76        ));
77    }
78    if weights.iter().any(|w| !(0.0..=1.0).contains(w)) {
79        return Err(OpsError::InvalidInput(
80            "protection weights must be between 0 and 1",
81        ));
82    }
83    if vertices.iter().any(|&v| !mesh.is_vertex_alive(v)) {
84        return Err(OpsError::InvalidInput("a protected vertex is not alive"));
85    }
86    Ok(())
87}
88
89fn same_bits(a: &[f32; 3], b: &[f32; 3]) -> bool {
90    a.iter().zip(b).all(|(a, b)| a.to_bits() == b.to_bits())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::deform::{self, Brush, Falloff, GrabOptions, InflateOptions, SmoothOptions};
97    use crate::test_util::{mesh_of, sweep};
98
99    /// A flat grid of `n x n` quads in the xy plane, over [0, 1]².
100    fn grid(n: u32) -> Mesh {
101        let mut positions = Vec::new();
102        for y in 0..=n {
103            for x in 0..=n {
104                positions.extend([x as f32 / n as f32, y as f32 / n as f32, 0.0]);
105            }
106        }
107        let at = |x: u32, y: u32| y * (n + 1) + x;
108        let mut triangles = Vec::new();
109        for y in 0..n {
110            for x in 0..n {
111                triangles.push([at(x, y), at(x + 1, y), at(x + 1, y + 1)]);
112                triangles.push([at(x, y), at(x + 1, y + 1), at(x, y + 1)]);
113            }
114        }
115        mesh_of(&positions, &triangles)
116    }
117
118    fn brush() -> Brush {
119        Brush {
120            center: [0.5, 0.5, 0.0],
121            radius: 0.4,
122            falloff: Falloff::Smooth,
123            strength: 0.1,
124        }
125    }
126
127    /// The left half of the grid: the vertices with `x < 0.5`.
128    fn left_half(mesh: &Mesh) -> Vec<VertexId> {
129        mesh.positions()
130            .iter()
131            .enumerate()
132            .filter(|(_, p)| p[0] < 0.5)
133            .map(|(v, _)| v as VertexId)
134            .collect()
135    }
136
137    /// Runs `op` under a full hold on the left half; asserts the held
138    /// vertices keep their bits and some others move.
139    fn holds(op: impl FnOnce(&mut Transaction) -> Result<()>) {
140        let mut mesh = grid(20);
141        let held = left_half(&mesh);
142        let weights = vec![0.0; held.len()];
143        let before = mesh.positions().to_vec();
144        let mut tx = mesh.begin_edit();
145        with_protection(&mut tx, &held, &weights, op).unwrap();
146        tx.commit().unwrap();
147        for &v in &held {
148            let (a, b) = (before[v as usize], mesh.positions()[v as usize]);
149            assert!(same_bits(&a, &b), "held vertex {v} moved");
150        }
151        let moved = before
152            .iter()
153            .zip(mesh.positions())
154            .filter(|(a, b)| !same_bits(a, b))
155            .count();
156        assert!(moved > 0, "nothing moved on the free side");
157    }
158
159    #[test]
160    fn inflate_moves_only_free_vertices() {
161        holds(|tx| {
162            deform::inflate(tx, &InflateOptions { brush: brush() })?;
163            Ok(())
164        });
165    }
166
167    #[test]
168    fn grab_moves_only_free_vertices() {
169        holds(|tx| {
170            let options = GrabOptions {
171                brush: brush(),
172                direction: [0.0, 0.0, 0.2],
173            };
174            deform::grab(tx, &options)?;
175            Ok(())
176        });
177    }
178
179    #[test]
180    fn smooth_moves_only_free_vertices() {
181        let mut mesh = grid(20);
182        // A bump to melt: a flat grid would not move under smoothing.
183        let mut tx = mesh.begin_edit();
184        deform::grab(
185            &mut tx,
186            &GrabOptions {
187                brush: brush(),
188                direction: [0.0, 0.0, 0.3],
189            },
190        )
191        .unwrap();
192        tx.commit().unwrap();
193        let held = left_half(&mesh);
194        let weights = vec![0.0; held.len()];
195        let before = mesh.positions().to_vec();
196        let mut tx = mesh.begin_edit();
197        with_protection(&mut tx, &held, &weights, |tx| {
198            let options = SmoothOptions {
199                brush: Brush {
200                    strength: 0.5,
201                    ..brush()
202                },
203                cycles: 2,
204                ..SmoothOptions::default()
205            };
206            deform::smooth(tx, &options)
207        })
208        .unwrap();
209        tx.commit().unwrap();
210        for &v in &held {
211            assert!(same_bits(
212                &before[v as usize],
213                &mesh.positions()[v as usize]
214            ));
215        }
216        assert!(
217            before
218                .iter()
219                .zip(mesh.positions())
220                .any(|(a, b)| !same_bits(a, b))
221        );
222    }
223
224    #[test]
225    fn a_weight_scales_the_move() {
226        let run = |weight: Option<f32>| {
227            let mut mesh = grid(10);
228            let v = 5 * 11 + 5; // The center vertex.
229            let (ids, weights) = match weight {
230                Some(w) => (vec![v as VertexId], vec![w]),
231                None => (vec![], vec![]),
232            };
233            let mut tx = mesh.begin_edit();
234            with_protection(&mut tx, &ids, &weights, |tx| {
235                deform::inflate(tx, &InflateOptions { brush: brush() })
236            })
237            .unwrap();
238            tx.commit().unwrap();
239            mesh.positions()[v][2]
240        };
241        let whole = run(None);
242        assert!(whole != 0.0);
243        assert_eq!(run(Some(1.0)).to_bits(), whole.to_bits());
244        assert!((run(Some(0.25)) - 0.25 * whole).abs() < 1e-6);
245        assert_eq!(run(Some(0.0)).to_bits(), 0f32.to_bits());
246    }
247
248    #[test]
249    fn undo_restores_every_vertex() {
250        let mut mesh = grid(10);
251        let held = left_half(&mesh);
252        let weights = vec![0.0; held.len()];
253        let before = mesh.positions().to_vec();
254        let mut tx = mesh.begin_edit();
255        with_protection(&mut tx, &held, &weights, |tx| {
256            deform::inflate(tx, &InflateOptions { brush: brush() })
257        })
258        .unwrap();
259        let patch = tx.commit().unwrap();
260        assert!(mesh.positions() != before.as_slice());
261        patch.revert(&mut mesh).unwrap();
262        assert_eq!(mesh.positions(), before.as_slice());
263    }
264
265    #[test]
266    fn running_out_of_memory_writes_nothing() {
267        let mesh = grid(4);
268        let held = left_half(&mesh);
269        let weights = vec![0.0; held.len()];
270        let failures = sweep(mesh, |tx| {
271            with_protection(tx, &held, &weights, |tx| {
272                deform::inflate(tx, &InflateOptions { brush: brush() })?;
273                Ok(())
274            })
275        });
276        assert!(failures >= 3);
277    }
278
279    #[test]
280    fn rejects_bad_input_before_running() {
281        let mut mesh = grid(2);
282        let mut tx = mesh.begin_edit();
283        let mut ran = false;
284        let mut try_with = |ids: &[VertexId], weights: &[f32]| {
285            with_protection(&mut tx, ids, weights, |_| {
286                ran = true;
287                Ok(())
288            })
289            .unwrap_err()
290        };
291        assert!(matches!(try_with(&[0], &[]), OpsError::InvalidInput(_)));
292        assert!(matches!(try_with(&[0], &[1.5]), OpsError::InvalidInput(_)));
293        assert!(matches!(
294            try_with(&[0], &[f32::NAN]),
295            OpsError::InvalidInput(_)
296        ));
297        assert!(matches!(
298            try_with(&[9999], &[0.0]),
299            OpsError::InvalidInput(_)
300        ));
301        assert!(!ran);
302    }
303}