Skip to main content

volren_core/transfer_function/
opacity.rs

1//! Opacity (scalar opacity) transfer function.
2
3/// A piecewise-linear opacity transfer function.
4///
5/// Maps a scalar value to an opacity in `[0.0, 1.0]`.
6/// Control points are stored sorted by scalar value.
7///
8/// # VTK Equivalent
9/// `vtkPiecewiseFunction` used as `vtkVolumeProperty::ScalarOpacity`.
10#[derive(Debug, Clone)]
11pub struct OpacityTransferFunction {
12    points: Vec<(f64, f64)>,
13}
14
15impl OpacityTransferFunction {
16    /// Create an empty opacity function.
17    #[must_use]
18    pub fn new() -> Self {
19        Self { points: Vec::new() }
20    }
21
22    /// Create a linear ramp: fully transparent at `scalar_min`, fully opaque at `scalar_max`.
23    #[must_use]
24    pub fn linear_ramp(scalar_min: f64, scalar_max: f64) -> Self {
25        let mut otf = Self::new();
26        otf.add_point(scalar_min, 0.0);
27        otf.add_point(scalar_max, 1.0);
28        otf
29    }
30
31    /// Add or replace a control point.
32    ///
33    /// `opacity` is clamped to `[0, 1]`.
34    pub fn add_point(&mut self, scalar: f64, opacity: f64) {
35        let opacity = opacity.clamp(0.0, 1.0);
36        match self
37            .points
38            .binary_search_by(|(s, _)| s.partial_cmp(&scalar).unwrap())
39        {
40            Ok(pos) => self.points[pos] = (scalar, opacity),
41            Err(pos) => self.points.insert(pos, (scalar, opacity)),
42        }
43    }
44
45    /// Remove a control point within `epsilon` of `scalar`.
46    pub fn remove_point(&mut self, scalar: f64, epsilon: f64) {
47        if let Some(pos) = self
48            .points
49            .iter()
50            .position(|(s, _)| (s - scalar).abs() < epsilon)
51        {
52            self.points.remove(pos);
53        }
54    }
55
56    /// Evaluate opacity at `scalar`. Returns `0.0` if no points exist.
57    #[must_use]
58    pub fn evaluate(&self, scalar: f64) -> f64 {
59        if self.points.is_empty() {
60            return 0.0;
61        }
62        if scalar <= self.points.first().unwrap().0 {
63            return self.points.first().unwrap().1;
64        }
65        if scalar >= self.points.last().unwrap().0 {
66            return self.points.last().unwrap().1;
67        }
68        let pos = self
69            .points
70            .partition_point(|(s, _)| *s <= scalar)
71            .saturating_sub(1);
72        let (s0, a0) = self.points[pos];
73        let (s1, a1) = self.points[pos + 1];
74        let t = (scalar - s0) / (s1 - s0);
75        a0 + (a1 - a0) * t
76    }
77
78    /// Number of control points.
79    #[must_use]
80    pub fn len(&self) -> usize {
81        self.points.len()
82    }
83
84    /// `true` if no control points have been added.
85    #[must_use]
86    pub fn is_empty(&self) -> bool {
87        self.points.is_empty()
88    }
89}
90
91impl Default for OpacityTransferFunction {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97// ── Tests ─────────────────────────────────────────────────────────────────────
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use approx::assert_abs_diff_eq;
103
104    #[test]
105    fn linear_ramp_midpoint() {
106        let otf = OpacityTransferFunction::linear_ramp(0.0, 1.0);
107        assert_abs_diff_eq!(otf.evaluate(0.5), 0.5, epsilon = 1e-10);
108    }
109
110    #[test]
111    fn clamp_opacity_to_one() {
112        let mut otf = OpacityTransferFunction::new();
113        otf.add_point(0.0, 1.5);
114        assert_abs_diff_eq!(otf.evaluate(0.0), 1.0, epsilon = 1e-10);
115    }
116
117    #[test]
118    fn empty_returns_zero() {
119        assert_abs_diff_eq!(
120            OpacityTransferFunction::new().evaluate(0.5),
121            0.0,
122            epsilon = 1e-10
123        );
124    }
125
126    #[test]
127    fn monotone_ramp_is_monotone() {
128        let otf = OpacityTransferFunction::linear_ramp(-1000.0, 1000.0);
129        let mut prev = otf.evaluate(-1000.0);
130        for i in -999..=1000 {
131            let v = otf.evaluate(i as f64);
132            assert!(v >= prev - 1e-12, "monotonicity violated at {i}");
133            prev = v;
134        }
135    }
136}