Skip to main content

oxihuman_morph/
delta_painter.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Programmatic vertex delta painting for creating morph targets.
5//!
6//! Provides a brush-based workflow for interactively or procedurally
7//! building morph target deltas, including masking, mirroring, and
8//! Laplacian smoothing.
9
10use serde::{Deserialize, Serialize};
11
12// ── Brush types ──────────────────────────────────────────────────────────────
13
14/// Brush falloff function controlling how influence decreases with distance.
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
16pub enum BrushFalloff {
17    /// Linearly decreasing from center to edge.
18    Linear,
19    /// Hermite (smooth-step) curve — soft center, soft edge.
20    Smooth,
21    /// Sharp peak at centre, rapid drop-off.
22    Sharp,
23    /// Uniform influence across the entire brush radius.
24    Flat,
25}
26
27impl BrushFalloff {
28    /// Evaluate the falloff at a normalised distance `t ∈ [0, 1]`.
29    /// Returns a value in `[0, 1]` where 1 is full influence.
30    pub fn evaluate(self, t: f64) -> f64 {
31        let t = t.clamp(0.0, 1.0);
32        match self {
33            Self::Linear => 1.0 - t,
34            Self::Smooth => {
35                let u = 1.0 - t;
36                // Hermite smooth-step: 3u² - 2u³
37                u * u * (3.0 - 2.0 * u)
38            }
39            Self::Sharp => {
40                let u = 1.0 - t;
41                u * u * u
42            }
43            Self::Flat => 1.0,
44        }
45    }
46}
47
48/// A painting brush that determines how deltas are applied.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PaintBrush {
51    /// World-space radius of the brush.
52    pub radius: f64,
53    /// Falloff function.
54    pub falloff: BrushFalloff,
55    /// Overall strength multiplier `[0, 1]`.
56    pub strength: f64,
57}
58
59impl Default for PaintBrush {
60    fn default() -> Self {
61        Self {
62            radius: 0.05,
63            falloff: BrushFalloff::Smooth,
64            strength: 1.0,
65        }
66    }
67}
68
69// ── Mirror axis ──────────────────────────────────────────────────────────────
70
71/// Axis across which to mirror deltas.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73pub enum MirrorAxis {
74    X,
75    Y,
76    Z,
77}
78
79impl MirrorAxis {
80    /// Index into `[f64; 3]` for this axis.
81    #[inline]
82    pub fn idx(self) -> usize {
83        match self {
84            Self::X => 0,
85            Self::Y => 1,
86            Self::Z => 2,
87        }
88    }
89}
90
91// ── MorphTargetData ──────────────────────────────────────────────────────────
92
93/// Exported morph-target data ready for storage or further processing.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct MorphTargetData {
96    /// Human-readable name.
97    pub name: String,
98    /// Full (dense) delta array, length = vertex count.
99    pub deltas: Vec<[f64; 3]>,
100    /// Indices of vertices whose delta is non-zero.
101    pub sparse_indices: Vec<usize>,
102    /// Corresponding non-zero deltas (same order as `sparse_indices`).
103    pub sparse_deltas: Vec<[f64; 3]>,
104}
105
106// ── DeltaPainter ─────────────────────────────────────────────────────────────
107
108/// Interactive / procedural delta painter.
109///
110/// Accumulates per-vertex displacements and an optional mask, then exports
111/// the result as a [`MorphTargetData`].
112pub struct DeltaPainter {
113    vertex_count: usize,
114    deltas: Vec<[f64; 3]>,
115    /// Per-vertex mask; 0 = no effect, 1 = full effect.
116    mask: Vec<f64>,
117}
118
119impl DeltaPainter {
120    /// Create a new painter for the given vertex count (all deltas zero, mask = 1).
121    pub fn new(vertex_count: usize) -> Self {
122        Self {
123            vertex_count,
124            deltas: vec![[0.0; 3]; vertex_count],
125            mask: vec![1.0; vertex_count],
126        }
127    }
128
129    /// Number of vertices managed by this painter.
130    #[inline]
131    pub fn vertex_count(&self) -> usize {
132        self.vertex_count
133    }
134
135    // ── painting ─────────────────────────────────────────────────────────
136
137    /// Paint `delta` at `center_vertex`, with brush falloff to neighbours.
138    ///
139    /// `vertex_positions` must have length ≥ `self.vertex_count`.
140    pub fn paint_at(
141        &mut self,
142        center_vertex: usize,
143        delta: [f64; 3],
144        brush: &PaintBrush,
145        vertex_positions: &[[f64; 3]],
146    ) -> anyhow::Result<()> {
147        if center_vertex >= self.vertex_count {
148            anyhow::bail!(
149                "center_vertex {} out of range (vertex_count = {})",
150                center_vertex,
151                self.vertex_count
152            );
153        }
154        if vertex_positions.len() < self.vertex_count {
155            anyhow::bail!(
156                "vertex_positions length {} < vertex_count {}",
157                vertex_positions.len(),
158                self.vertex_count
159            );
160        }
161        if brush.radius <= 0.0 {
162            anyhow::bail!("brush radius must be positive, got {}", brush.radius);
163        }
164
165        let center_pos = vertex_positions[center_vertex];
166        let radius_sq = brush.radius * brush.radius;
167
168        for (i, vpos) in vertex_positions.iter().enumerate().take(self.vertex_count) {
169            let dx = vpos[0] - center_pos[0];
170            let dy = vpos[1] - center_pos[1];
171            let dz = vpos[2] - center_pos[2];
172            let dist_sq = dx * dx + dy * dy + dz * dz;
173            if dist_sq > radius_sq {
174                continue;
175            }
176            let dist = dist_sq.sqrt();
177            let t = dist / brush.radius;
178            let influence = brush.falloff.evaluate(t) * brush.strength;
179            self.deltas[i][0] += delta[0] * influence;
180            self.deltas[i][1] += delta[1] * influence;
181            self.deltas[i][2] += delta[2] * influence;
182        }
183        Ok(())
184    }
185
186    /// Paint along a stroke — a series of vertex indices.
187    ///
188    /// The same `delta` and brush are applied at each vertex in turn.
189    pub fn paint_stroke(
190        &mut self,
191        vertices: &[usize],
192        delta: [f64; 3],
193        brush: &PaintBrush,
194        vertex_positions: &[[f64; 3]],
195    ) -> anyhow::Result<()> {
196        for &v in vertices {
197            self.paint_at(v, delta, brush, vertex_positions)?;
198        }
199        Ok(())
200    }
201
202    // ── masking ──────────────────────────────────────────────────────────
203
204    /// Set the mask value for a single vertex.
205    pub fn set_mask(&mut self, vertex: usize, value: f64) -> anyhow::Result<()> {
206        if vertex >= self.vertex_count {
207            anyhow::bail!(
208                "vertex {} out of range (vertex_count = {})",
209                vertex,
210                self.vertex_count
211            );
212        }
213        self.mask[vertex] = value.clamp(0.0, 1.0);
214        Ok(())
215    }
216
217    /// Set the mask for all vertices in `vertices` to `value`.
218    pub fn mask_vertex_group(&mut self, vertices: &[usize], value: f64) {
219        let clamped = value.clamp(0.0, 1.0);
220        for &v in vertices {
221            if v < self.vertex_count {
222                self.mask[v] = clamped;
223            }
224        }
225    }
226
227    /// Clear the mask (set all to 1.0).
228    pub fn clear_mask(&mut self) {
229        for m in &mut self.mask {
230            *m = 1.0;
231        }
232    }
233
234    /// Invert the mask (1 - m for each vertex).
235    pub fn invert_mask(&mut self) {
236        for m in &mut self.mask {
237            *m = 1.0 - *m;
238        }
239    }
240
241    // ── mirror ───────────────────────────────────────────────────────────
242
243    /// Mirror deltas across the given axis.
244    ///
245    /// For each vertex at position P, finds the closest vertex at the
246    /// mirror-reflected position (within `tolerance`) and copies the delta
247    /// with the axis component negated.
248    pub fn mirror(
249        &mut self,
250        axis: MirrorAxis,
251        vertex_positions: &[[f64; 3]],
252        tolerance: f64,
253    ) -> anyhow::Result<()> {
254        if vertex_positions.len() < self.vertex_count {
255            anyhow::bail!(
256                "vertex_positions length {} < vertex_count {}",
257                vertex_positions.len(),
258                self.vertex_count
259            );
260        }
261        if tolerance <= 0.0 {
262            anyhow::bail!("tolerance must be positive, got {}", tolerance);
263        }
264
265        let ax = axis.idx();
266        let tol_sq = tolerance * tolerance;
267        let original_deltas = self.deltas.clone();
268
269        // Build pairs: for each vertex on the positive side, find its mirror.
270        for i in 0..self.vertex_count {
271            let pos = vertex_positions[i];
272            // Only process vertices on the positive side of the axis.
273            if pos[ax] < 0.0 {
274                continue;
275            }
276
277            // Construct mirror position.
278            let mut mirror_pos = pos;
279            mirror_pos[ax] = -mirror_pos[ax];
280
281            // Find closest vertex to mirror_pos.
282            let mut best_j: Option<usize> = None;
283            let mut best_dist_sq = f64::MAX;
284            for (j, jpos) in vertex_positions.iter().enumerate().take(self.vertex_count) {
285                let dp0 = jpos[0] - mirror_pos[0];
286                let dp1 = jpos[1] - mirror_pos[1];
287                let dp2 = jpos[2] - mirror_pos[2];
288                let dsq = dp0 * dp0 + dp1 * dp1 + dp2 * dp2;
289                if dsq < best_dist_sq {
290                    best_dist_sq = dsq;
291                    best_j = Some(j);
292                }
293            }
294
295            if let Some(j) = best_j {
296                if best_dist_sq <= tol_sq {
297                    let mut d = original_deltas[i];
298                    d[ax] = -d[ax]; // negate the mirror axis component
299                    self.deltas[j] = d;
300                }
301            }
302        }
303        Ok(())
304    }
305
306    // ── smoothing ────────────────────────────────────────────────────────
307
308    /// Apply Laplacian smoothing to the accumulated deltas.
309    ///
310    /// `adjacency[v]` is the list of vertex indices adjacent to vertex `v`.
311    pub fn smooth(&mut self, iterations: usize, adjacency: &[Vec<usize>]) -> anyhow::Result<()> {
312        if adjacency.len() < self.vertex_count {
313            anyhow::bail!(
314                "adjacency length {} < vertex_count {}",
315                adjacency.len(),
316                self.vertex_count
317            );
318        }
319        for _ in 0..iterations {
320            let prev = self.deltas.clone();
321            for (i, nbrs) in adjacency[..self.vertex_count].iter().enumerate() {
322                if nbrs.is_empty() {
323                    continue;
324                }
325                let mut avg = [0.0_f64; 3];
326                let mut count = 0usize;
327                for &nb in nbrs {
328                    if nb < self.vertex_count {
329                        avg[0] += prev[nb][0];
330                        avg[1] += prev[nb][1];
331                        avg[2] += prev[nb][2];
332                        count += 1;
333                    }
334                }
335                if count > 0 {
336                    let c = count as f64;
337                    self.deltas[i] = [avg[0] / c, avg[1] / c, avg[2] / c];
338                }
339            }
340        }
341        Ok(())
342    }
343
344    // ── output ───────────────────────────────────────────────────────────
345
346    /// Return the final delta array with the mask applied.
347    pub fn get_deltas(&self) -> Vec<[f64; 3]> {
348        self.deltas
349            .iter()
350            .zip(self.mask.iter())
351            .map(|(d, &m)| [d[0] * m, d[1] * m, d[2] * m])
352            .collect()
353    }
354
355    /// Return a reference to the raw (un-masked) deltas.
356    pub fn raw_deltas(&self) -> &[[f64; 3]] {
357        &self.deltas
358    }
359
360    /// Return a reference to the mask.
361    pub fn mask(&self) -> &[f64] {
362        &self.mask
363    }
364
365    /// Clear all deltas (reset to zero).
366    pub fn clear(&mut self) {
367        for d in &mut self.deltas {
368            *d = [0.0; 3];
369        }
370    }
371
372    /// Export as a [`MorphTargetData`].
373    pub fn to_morph_target(&self, name: &str) -> MorphTargetData {
374        let deltas = self.get_deltas();
375        let threshold = 1e-12;
376        let mut sparse_indices = Vec::new();
377        let mut sparse_deltas = Vec::new();
378        for (i, d) in deltas.iter().enumerate() {
379            let mag_sq = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
380            if mag_sq > threshold * threshold {
381                sparse_indices.push(i);
382                sparse_deltas.push(*d);
383            }
384        }
385        MorphTargetData {
386            name: name.to_owned(),
387            deltas,
388            sparse_indices,
389            sparse_deltas,
390        }
391    }
392
393    /// Set a specific vertex delta directly (bypassing brush).
394    pub fn set_delta(&mut self, vertex: usize, delta: [f64; 3]) -> anyhow::Result<()> {
395        if vertex >= self.vertex_count {
396            anyhow::bail!(
397                "vertex {} out of range (vertex_count = {})",
398                vertex,
399                self.vertex_count
400            );
401        }
402        self.deltas[vertex] = delta;
403        Ok(())
404    }
405
406    /// Add to a specific vertex delta directly (bypassing brush).
407    pub fn add_delta(&mut self, vertex: usize, delta: [f64; 3]) -> anyhow::Result<()> {
408        if vertex >= self.vertex_count {
409            anyhow::bail!(
410                "vertex {} out of range (vertex_count = {})",
411                vertex,
412                self.vertex_count
413            );
414        }
415        self.deltas[vertex][0] += delta[0];
416        self.deltas[vertex][1] += delta[1];
417        self.deltas[vertex][2] += delta[2];
418        Ok(())
419    }
420
421    /// Scale all deltas by a uniform factor.
422    pub fn scale_all(&mut self, factor: f64) {
423        for d in &mut self.deltas {
424            d[0] *= factor;
425            d[1] *= factor;
426            d[2] *= factor;
427        }
428    }
429
430    /// Blend another painter's deltas into this one.
431    pub fn blend_from(&mut self, other: &DeltaPainter, weight: f64) -> anyhow::Result<()> {
432        if other.vertex_count != self.vertex_count {
433            anyhow::bail!(
434                "vertex_count mismatch: self={}, other={}",
435                self.vertex_count,
436                other.vertex_count
437            );
438        }
439        let w = weight.clamp(0.0, 1.0);
440        for i in 0..self.vertex_count {
441            self.deltas[i][0] += other.deltas[i][0] * w;
442            self.deltas[i][1] += other.deltas[i][1] * w;
443            self.deltas[i][2] += other.deltas[i][2] * w;
444        }
445        Ok(())
446    }
447}
448
449// ── Unit tests ───────────────────────────────────────────────────────────────
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    fn simple_positions() -> Vec<[f64; 3]> {
456        vec![
457            [0.0, 0.0, 0.0],
458            [1.0, 0.0, 0.0],
459            [0.0, 1.0, 0.0],
460            [1.0, 1.0, 0.0],
461        ]
462    }
463
464    #[test]
465    fn test_new_painter() {
466        let p = DeltaPainter::new(10);
467        assert_eq!(p.vertex_count(), 10);
468        assert_eq!(p.raw_deltas().len(), 10);
469        assert_eq!(p.mask().len(), 10);
470        for d in p.raw_deltas() {
471            assert_eq!(*d, [0.0; 3]);
472        }
473        for &m in p.mask() {
474            assert!((m - 1.0).abs() < 1e-15);
475        }
476    }
477
478    #[test]
479    fn test_paint_at_center() {
480        let positions = simple_positions();
481        let mut p = DeltaPainter::new(4);
482        let brush = PaintBrush {
483            radius: 0.5,
484            falloff: BrushFalloff::Flat,
485            strength: 1.0,
486        };
487        p.paint_at(0, [0.0, 0.0, 1.0], &brush, &positions)
488            .expect("paint_at should succeed");
489
490        // Vertex 0 is at center — should get full delta
491        let d = p.raw_deltas()[0];
492        assert!((d[2] - 1.0).abs() < 1e-10);
493
494        // Vertex 1 is at distance 1.0 > radius 0.5 — should be zero
495        let d1 = p.raw_deltas()[1];
496        assert!(d1[2].abs() < 1e-10);
497    }
498
499    #[test]
500    fn test_paint_at_out_of_range() {
501        let positions = simple_positions();
502        let mut p = DeltaPainter::new(4);
503        let brush = PaintBrush::default();
504        let result = p.paint_at(10, [0.0, 0.0, 1.0], &brush, &positions);
505        assert!(result.is_err());
506    }
507
508    #[test]
509    fn test_paint_stroke() {
510        let positions = simple_positions();
511        let mut p = DeltaPainter::new(4);
512        let brush = PaintBrush {
513            radius: 0.01,
514            falloff: BrushFalloff::Flat,
515            strength: 1.0,
516        };
517        p.paint_stroke(&[0, 1], [0.0, 1.0, 0.0], &brush, &positions)
518            .expect("paint_stroke should succeed");
519
520        // Both vertices 0 and 1 should have Y delta
521        assert!(p.raw_deltas()[0][1] > 0.5);
522        assert!(p.raw_deltas()[1][1] > 0.5);
523    }
524
525    #[test]
526    fn test_mask_blocks_output() {
527        let mut p = DeltaPainter::new(3);
528        p.set_delta(0, [1.0, 0.0, 0.0]).expect("set ok");
529        p.set_delta(1, [1.0, 0.0, 0.0]).expect("set ok");
530        p.set_mask(0, 0.0).expect("mask ok");
531        p.set_mask(1, 0.5).expect("mask ok");
532
533        let out = p.get_deltas();
534        assert!(out[0][0].abs() < 1e-15, "masked vertex should be zero");
535        assert!((out[1][0] - 0.5).abs() < 1e-10, "half-masked should be 0.5");
536        assert!((out[2][0]).abs() < 1e-15, "untouched vertex should be zero");
537    }
538
539    #[test]
540    fn test_mask_vertex_group() {
541        let mut p = DeltaPainter::new(5);
542        p.mask_vertex_group(&[1, 3], 0.25);
543        assert!((p.mask()[1] - 0.25).abs() < 1e-15);
544        assert!((p.mask()[3] - 0.25).abs() < 1e-15);
545        assert!((p.mask()[0] - 1.0).abs() < 1e-15);
546    }
547
548    #[test]
549    fn test_invert_mask() {
550        let mut p = DeltaPainter::new(3);
551        p.set_mask(0, 0.0).expect("ok");
552        p.set_mask(1, 0.3).expect("ok");
553        p.invert_mask();
554        assert!((p.mask()[0] - 1.0).abs() < 1e-15);
555        assert!((p.mask()[1] - 0.7).abs() < 1e-10);
556        assert!((p.mask()[2] - 0.0).abs() < 1e-15);
557    }
558
559    #[test]
560    fn test_smooth_reduces_peak() {
561        let mut p = DeltaPainter::new(3);
562        p.set_delta(0, [10.0, 0.0, 0.0]).expect("ok");
563        // Simple chain: 0-1, 1-2
564        let adj = vec![vec![1], vec![0, 2], vec![1]];
565        let before = p.raw_deltas()[0][0];
566        p.smooth(1, &adj).expect("smooth ok");
567        let after = p.raw_deltas()[0][0];
568        assert!(after < before, "smoothing should reduce peak");
569    }
570
571    #[test]
572    fn test_mirror_x() {
573        // Two vertices symmetric about X=0
574        let positions = vec![[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
575        let mut p = DeltaPainter::new(2);
576        p.set_delta(1, [0.5, 0.3, 0.1]).expect("ok");
577        p.mirror(MirrorAxis::X, &positions, 0.1).expect("mirror ok");
578        // Vertex 0 should get the mirror: X negated
579        let d0 = p.raw_deltas()[0];
580        assert!(
581            (d0[0] - (-0.5)).abs() < 1e-10,
582            "X component should be negated"
583        );
584        assert!((d0[1] - 0.3).abs() < 1e-10);
585        assert!((d0[2] - 0.1).abs() < 1e-10);
586    }
587
588    #[test]
589    fn test_to_morph_target_sparse() {
590        let mut p = DeltaPainter::new(5);
591        p.set_delta(1, [1.0, 0.0, 0.0]).expect("ok");
592        p.set_delta(3, [0.0, 2.0, 0.0]).expect("ok");
593        let mt = p.to_morph_target("test_sparse");
594        assert_eq!(mt.name, "test_sparse");
595        assert_eq!(mt.deltas.len(), 5);
596        assert_eq!(mt.sparse_indices.len(), 2);
597        assert_eq!(mt.sparse_deltas.len(), 2);
598        assert!(mt.sparse_indices.contains(&1));
599        assert!(mt.sparse_indices.contains(&3));
600    }
601
602    #[test]
603    fn test_clear() {
604        let mut p = DeltaPainter::new(3);
605        p.set_delta(0, [1.0, 2.0, 3.0]).expect("ok");
606        p.clear();
607        for d in p.raw_deltas() {
608            assert_eq!(*d, [0.0; 3]);
609        }
610    }
611
612    #[test]
613    fn test_scale_all() {
614        let mut p = DeltaPainter::new(2);
615        p.set_delta(0, [1.0, 2.0, 3.0]).expect("ok");
616        p.scale_all(0.5);
617        let d = p.raw_deltas()[0];
618        assert!((d[0] - 0.5).abs() < 1e-15);
619        assert!((d[1] - 1.0).abs() < 1e-15);
620        assert!((d[2] - 1.5).abs() < 1e-15);
621    }
622
623    #[test]
624    fn test_blend_from() {
625        let mut a = DeltaPainter::new(3);
626        a.set_delta(0, [1.0, 0.0, 0.0]).expect("ok");
627        let mut b = DeltaPainter::new(3);
628        b.set_delta(0, [0.0, 2.0, 0.0]).expect("ok");
629        a.blend_from(&b, 0.5).expect("blend ok");
630        let d = a.raw_deltas()[0];
631        assert!((d[0] - 1.0).abs() < 1e-15);
632        assert!((d[1] - 1.0).abs() < 1e-15);
633    }
634
635    #[test]
636    fn test_brush_falloff_linear() {
637        assert!((BrushFalloff::Linear.evaluate(0.0) - 1.0).abs() < 1e-15);
638        assert!((BrushFalloff::Linear.evaluate(1.0)).abs() < 1e-15);
639        assert!((BrushFalloff::Linear.evaluate(0.5) - 0.5).abs() < 1e-15);
640    }
641
642    #[test]
643    fn test_brush_falloff_smooth() {
644        assert!((BrushFalloff::Smooth.evaluate(0.0) - 1.0).abs() < 1e-15);
645        assert!((BrushFalloff::Smooth.evaluate(1.0)).abs() < 1e-15);
646        // Smooth at t=0.5 should be 0.5 (Hermite property)
647        assert!((BrushFalloff::Smooth.evaluate(0.5) - 0.5).abs() < 1e-15);
648    }
649
650    #[test]
651    fn test_brush_falloff_sharp() {
652        assert!((BrushFalloff::Sharp.evaluate(0.0) - 1.0).abs() < 1e-15);
653        assert!((BrushFalloff::Sharp.evaluate(1.0)).abs() < 1e-15);
654        // Sharp at 0.5 = (0.5)^3 = 0.125
655        assert!((BrushFalloff::Sharp.evaluate(0.5) - 0.125).abs() < 1e-15);
656    }
657
658    #[test]
659    fn test_brush_falloff_flat() {
660        assert!((BrushFalloff::Flat.evaluate(0.0) - 1.0).abs() < 1e-15);
661        assert!((BrushFalloff::Flat.evaluate(0.5) - 1.0).abs() < 1e-15);
662        assert!((BrushFalloff::Flat.evaluate(1.0) - 1.0).abs() < 1e-15);
663    }
664
665    #[test]
666    fn test_set_mask_out_of_range() {
667        let mut p = DeltaPainter::new(3);
668        assert!(p.set_mask(5, 0.5).is_err());
669    }
670
671    #[test]
672    fn test_smooth_adjacency_too_short() {
673        let mut p = DeltaPainter::new(5);
674        let adj = vec![vec![1], vec![0]]; // too short
675        assert!(p.smooth(1, &adj).is_err());
676    }
677
678    #[test]
679    fn test_mirror_tolerance_too_small() {
680        let positions = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
681        let mut p = DeltaPainter::new(2);
682        assert!(p.mirror(MirrorAxis::X, &positions, -0.1).is_err());
683    }
684
685    #[test]
686    fn test_add_delta() {
687        let mut p = DeltaPainter::new(2);
688        p.set_delta(0, [1.0, 2.0, 3.0]).expect("ok");
689        p.add_delta(0, [0.5, 0.5, 0.5]).expect("ok");
690        let d = p.raw_deltas()[0];
691        assert!((d[0] - 1.5).abs() < 1e-15);
692        assert!((d[1] - 2.5).abs() < 1e-15);
693        assert!((d[2] - 3.5).abs() < 1e-15);
694    }
695
696    #[test]
697    fn test_clear_mask() {
698        let mut p = DeltaPainter::new(3);
699        p.set_mask(0, 0.0).expect("ok");
700        p.set_mask(1, 0.5).expect("ok");
701        p.clear_mask();
702        for &m in p.mask() {
703            assert!((m - 1.0).abs() < 1e-15);
704        }
705    }
706}