Skip to main content

wowlab_parsers/parsers/scaling/
curve.rs

1//! Curve interpolation for item scaling.
2
3use wowlab_types::{data::ItemScalingData, numeric::interpolate_sorted};
4
5/// Interpolate a piecewise-linear curve at `x`; values outside the range clamp to the nearest endpoint.
6#[must_use]
7pub fn interpolate_curve(scaling_data: &ItemScalingData, curve_id: i32, x: f64) -> Option<f64> {
8    let points = scaling_data.curve_points.get(&curve_id)?;
9    // order_index is not always reliable; sort by pos_0 (x value).
10    let mut sorted_points: Vec<_> = points
11        .iter()
12        .map(|point| (point.pos_0, point.pos_1))
13        .collect();
14
15    sorted_points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
16
17    interpolate_sorted(&sorted_points, x).or_else(|| sorted_points.last().map(|(_, value)| *value))
18}
19
20#[cfg(test)]
21mod tests {
22    use googletest::prelude::*;
23    use wowlab_types::data::CurvePointFlat;
24
25    use super::*;
26
27    fn make_scaling_data(data: &[(f64, f64)]) -> Result<ItemScalingData> {
28        let points = data
29            .iter()
30            .enumerate()
31            .map(|(i, (x, y))| -> Result<_> {
32                let index = i32::try_from(i).or_fail()?;
33
34                Ok(CurvePointFlat {
35                    id: index,
36                    curve_id: 1,
37                    order_index: index,
38                    pos_0: *x,
39                    pos_1: *y,
40                    pos_pre_squish_0: *x,
41                    pos_pre_squish_1: *y,
42                })
43            })
44            .collect::<Result<Vec<_>>>()?;
45        let mut scaling_data = ItemScalingData::default();
46
47        scaling_data.curve_points.insert(1, points);
48
49        Ok(scaling_data)
50    }
51
52    #[gtest]
53    fn test_interpolate_exact_point() -> Result<()> {
54        let data = make_scaling_data(&[(100.0, 50.0), (200.0, 100.0), (300.0, 150.0)])?;
55
56        verify_that!(interpolate_curve(&data, 1, 100.0), eq(Some(50.0)))?;
57        verify_that!(interpolate_curve(&data, 1, 200.0), eq(Some(100.0)))?;
58        verify_that!(interpolate_curve(&data, 1, 300.0), eq(Some(150.0)))?;
59
60        Ok(())
61    }
62
63    #[gtest]
64    fn test_interpolate_between_points() -> Result<()> {
65        let data = make_scaling_data(&[(100.0, 50.0), (200.0, 100.0)])?;
66
67        verify_that!(interpolate_curve(&data, 1, 150.0), eq(Some(75.0)))?;
68
69        Ok(())
70    }
71
72    #[gtest]
73    fn test_interpolate_clamp_below() -> Result<()> {
74        let data = make_scaling_data(&[(100.0, 50.0), (200.0, 100.0)])?;
75
76        verify_that!(interpolate_curve(&data, 1, 50.0), eq(Some(50.0)))?;
77
78        Ok(())
79    }
80
81    #[gtest]
82    fn test_interpolate_clamp_above() -> Result<()> {
83        let data = make_scaling_data(&[(100.0, 50.0), (200.0, 100.0)])?;
84
85        verify_that!(interpolate_curve(&data, 1, 250.0), eq(Some(100.0)))?;
86
87        Ok(())
88    }
89
90    #[gtest]
91    fn test_interpolate_empty() -> Result<()> {
92        let data = make_scaling_data(&[])?;
93
94        verify_that!(interpolate_curve(&data, 1, 100.0), eq(None))?;
95
96        Ok(())
97    }
98
99    #[gtest]
100    fn test_interpolate_nan_falls_back_to_last_point() -> Result<()> {
101        let data = make_scaling_data(&[(100.0, 50.0), (200.0, 100.0)])?;
102
103        verify_that!(interpolate_curve(&data, 1, f64::NAN), eq(Some(100.0)))?;
104
105        Ok(())
106    }
107}