Skip to main content

pax_runtime_api/
unit_value.rs

1//! Normalized unit-domain values.
2
3use super::*;
4
5/// A scalar value in a normalized unit domain.
6///
7/// `UnitValue` accepts both unitless numbers and percents. It is useful for
8/// properties whose authoring domain is "part of a whole", such as path drawing
9/// progress where `0.5` and `50%` describe the same position.
10#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
11#[serde(crate = "crate::serde")]
12pub enum UnitValue {
13    /// Unitless normalized value, where `0.5` means halfway through the domain.
14    Unitless(Numeric),
15    /// Percent value, where `50%` means halfway through the domain.
16    Percent(Numeric),
17}
18
19impl Default for UnitValue {
20    fn default() -> Self {
21        Self::Unitless(Numeric::F64(0.0))
22    }
23}
24
25impl Display for UnitValue {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            Self::Unitless(value) => write!(f, "{}", value),
29            Self::Percent(value) => write!(f, "{}%", value),
30        }
31    }
32}
33
34impl UnitValue {
35    /// Returns the wrapped value as a normalized unitless float.
36    ///
37    /// `UnitValue::Unitless(0.5)` returns `0.5`, and
38    /// `UnitValue::Percent(50)` returns `0.5`.
39    pub fn to_unit_float(&self) -> f64 {
40        match self {
41            Self::Unitless(value) => value.to_float(),
42            Self::Percent(value) => value.to_float() / 100.0,
43        }
44    }
45
46    /// Returns `to_unit_float()` clamped into the closed unit interval.
47    pub fn to_clamped_unit_float(&self) -> f64 {
48        self.to_unit_float().clamp(0.0, 1.0)
49    }
50}
51
52impl Interpolatable for UnitValue {
53    fn interpolate(&self, other: &Self, t: f64) -> Self {
54        let start = self.to_unit_float();
55        let end = other.to_unit_float();
56        Self::Unitless(Numeric::F64(start + (end - start) * t))
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::{CoercionRules, PaxValue, Percent};
64
65    #[test]
66    fn numeric_and_percent_resolve_to_same_unit_float() {
67        let numeric = UnitValue::Unitless(Numeric::F64(0.5));
68        let percent = UnitValue::Percent(Numeric::F64(50.0));
69
70        assert_eq!(numeric.to_unit_float(), percent.to_unit_float());
71    }
72
73    #[test]
74    fn coerces_numeric_and_percent_values() {
75        let numeric = UnitValue::try_coerce(PaxValue::Numeric(Numeric::F64(0.25))).unwrap();
76        let percent =
77            UnitValue::try_coerce(PaxValue::Percent(Percent(Numeric::F64(25.0)))).unwrap();
78
79        assert_eq!(numeric, UnitValue::Unitless(Numeric::F64(0.25)));
80        assert_eq!(percent, UnitValue::Percent(Numeric::F64(25.0)));
81        assert_eq!(numeric.to_unit_float(), percent.to_unit_float());
82    }
83
84    #[test]
85    fn clamped_unit_float_limits_out_of_range_values() {
86        assert_eq!(
87            UnitValue::Unitless(Numeric::F64(-0.25)).to_clamped_unit_float(),
88            0.0
89        );
90        assert_eq!(
91            UnitValue::Percent(Numeric::F64(125.0)).to_clamped_unit_float(),
92            1.0
93        );
94    }
95
96    #[test]
97    fn interpolation_resolves_mixed_units_to_unitless() {
98        let start = UnitValue::Unitless(Numeric::F64(0.25));
99        let end = UnitValue::Percent(Numeric::F64(75.0));
100
101        assert_eq!(
102            start.interpolate(&end, 0.5),
103            UnitValue::Unitless(Numeric::F64(0.5))
104        );
105    }
106}