index/objects/plotting/
function_plotter.rs

1use exmex::{parse, ExError, Express, FlatEx, FloatOpsFactory};
2use wasm_bindgen::prelude::*;
3use std::rc::Rc;
4
5use crate::{objects::vector_object::VectorObjectBuilder, utils::{bezier::CubicBezierTuple, console::error, interval::ClosedInterval, point2d::{Path2D, Point2D}}};
6
7/// A ParametricFunctionPlot represents a plot of a parametric function (x(t), y(t)).
8#[wasm_bindgen]
9#[derive(Clone)]
10pub struct ParametricFunctionPlot {
11    expression_x: Rc<String>,
12    expression_y: Rc<String>,
13    domain: ClosedInterval,
14    x_range: ClosedInterval,
15    y_range: ClosedInterval,
16    discontinuities: Rc<Vec<f32>>,
17    min_depth: u32,
18    max_depth: u32,
19    threshold: f32,
20    expr_x: FlatEx<f32, FloatOpsFactory<f32>>,
21    expr_y: FlatEx<f32, FloatOpsFactory<f32>>,
22    composition: Box<&'static dyn Fn(Point2D) -> Point2D>,
23}
24
25#[wasm_bindgen]
26impl ParametricFunctionPlot {
27    /// Creates a new ParametricFunctionPlot from an expression, domain, x-range, y-range, and other optional parameters.
28    #[wasm_bindgen(constructor, return_description = "A new parametric function plot.")]
29    pub fn new(
30        #[wasm_bindgen(param_description = "The x expression of the parametric function.")]
31        expression_x: String,
32        #[wasm_bindgen(param_description = "The y expression of the parametric function.")]
33        expression_y: String,
34        #[wasm_bindgen(param_description = "The domain of the parametric function.")]
35        domain: ClosedInterval,
36        #[wasm_bindgen(param_description = "The x-range of the plot.")]
37        x_range: ClosedInterval,
38        #[wasm_bindgen(param_description = "The y-range of the plot.")]
39        y_range: ClosedInterval,
40        #[wasm_bindgen(param_description = "The discontinuities of the plot.", unchecked_param_type = "number[]")]
41        discontinuities: Option<Vec<f32>>,
42        #[wasm_bindgen(param_description = "The minimum depth of the plot.")]
43        min_depth: Option<u32>,
44        #[wasm_bindgen(param_description = "The maximum depth of the plot.")]
45        max_depth: Option<u32>,
46        #[wasm_bindgen(param_description = "The threshold of the plot.")]
47        threshold: Option<f32>,
48    ) -> Result<ParametricFunctionPlot, JsError> {
49        let discontinuities = discontinuities.unwrap_or_default();
50        let min_depth = min_depth.unwrap_or(8);
51        let max_depth = max_depth.unwrap_or(14);
52        let threshold = threshold.unwrap_or(0.01);
53        let expr_x: Result<FlatEx<_, FloatOpsFactory<f32>>, ExError> = parse(&expression_x);
54        let expr_y: Result<FlatEx<_, FloatOpsFactory<f32>>, ExError> = parse(&expression_y);
55        match (expr_x, expr_y) {
56            (Ok(expr_x), Ok(expr_y)) => Ok(ParametricFunctionPlot {
57                expression_x: Rc::new(expression_x),
58                expression_y: Rc::new(expression_y),
59                domain,
60                x_range,
61                y_range,
62                discontinuities: Rc::new(discontinuities),
63                min_depth,
64                max_depth,
65                threshold,
66                expr_x,
67                expr_y,
68                composition: Box::new(&|point| point),
69            }),
70            _ => Err(JsError::new("Failed to parse parametric function."))
71        }
72    }
73
74    /// Returns the x expression of the parametric function.
75    #[wasm_bindgen(getter, return_description = "The x expression of the parametric function.")]
76    pub fn expression_x(&self) -> String {
77        self.expression_x.to_string()
78    }
79
80    /// Returns the y expression of the parametric function.
81    #[wasm_bindgen(getter, return_description = "The y expression of the parametric function.")]
82    pub fn expression_y(&self) -> String {
83        self.expression_y.to_string()
84    }
85
86    /// Returns the expression of the parametric function.
87    #[wasm_bindgen(getter, return_description = "The expression of the parametric function.")]
88    pub fn expression(&self) -> String {
89        format!("({}, {})", self.expression_x, self.expression_y)
90    }
91
92    /// Returns the domain of the parametric function.
93    #[wasm_bindgen(getter, return_description = "The domain of the parametric function.")]
94    pub fn domain(&self) -> ClosedInterval {
95        self.domain.clone()
96    }
97
98    /// Returns the x-range of the plot.
99    #[wasm_bindgen(getter, return_description = "The x-range of the plot.")]
100    pub fn x_range(&self) -> ClosedInterval {
101        self.x_range.clone()
102    }
103
104    /// Returns the y-range of the plot.
105    #[wasm_bindgen(getter, return_description = "The y-range of the plot.")]
106    pub fn y_range(&self) -> ClosedInterval {
107        self.y_range.clone()
108    }
109
110    /// Returns the discontinuities of the plot.
111    #[wasm_bindgen(getter, return_description = "The discontinuities of the plot.", unchecked_return_type = "number[]")]
112    pub fn discontinuities(&self) -> Vec<f32> {
113        self.discontinuities.to_vec()
114    }
115
116    /// Returns the minimum depth of the plot.
117    #[wasm_bindgen(getter, return_description = "The minimum depth of the plot.")]
118    pub fn min_depth(&self) -> u32 {
119        self.min_depth
120    }
121
122    /// Returns the maximum depth of the plot.
123    #[wasm_bindgen(getter, return_description = "The maximum depth of the plot.")]
124    pub fn max_depth(&self) -> u32 {
125        self.max_depth
126    }
127
128    /// Returns the threshold of the plot.
129    #[wasm_bindgen(getter, return_description = "The threshold of the plot.")]
130    pub fn threshold(&self) -> f32 {
131        self.threshold
132    }
133
134    /// Checks if a number is a discontinuity of the plot.
135    #[wasm_bindgen(return_description = "A boolean indicating if the number is a discontinuity of the plot.")]
136    pub fn is_discontinuity(
137        &self,
138        #[wasm_bindgen(param_description = "The number to check.")]
139        number: f32,
140    ) -> bool {
141        self.discontinuities.iter().any(|&discontinuity| (number - discontinuity).abs() < self.threshold)
142    }
143
144    /// Returns the error of both Point2Ds.
145    #[wasm_bindgen(js_name = error, return_description = "The error of both Point2Ds.")]
146    pub fn error(
147        &self,
148        #[wasm_bindgen(param_description = "The first Point2D.")]
149        point1: Point2D,
150        #[wasm_bindgen(param_description = "The second Point2D.")]
151        point2: Point2D,
152    ) -> f32 {
153        point1.distance_squared(&point2)
154    }
155
156    /// Cheap hash function for the plot. Reference: https://github.com/stevenpetryk/mafs/blob/85c954ee649bebe65963bc8e7ad5708797c394d6/src/display/Plot/PlotUtils.tsx#L26
157    #[wasm_bindgen(js_name = hash, return_description = "The hash of the plot.")]
158    pub fn hash(
159        &self,
160        #[wasm_bindgen(param_description = "The minimum value of the object to hash.")]
161        min: f32,
162        #[wasm_bindgen(param_description = "The maximum value of the object to hash.")]
163        max: f32,
164    ) -> f32 {
165        let result = (min * 12.9898 + max * 78.233).sin() * 43758.5453;
166        0.4 + 0.2 * (result - result.floor())
167    }
168
169    /// Checks if point is within the plot.
170    #[wasm_bindgen(return_description = "A boolean indicating if the point is within the plot.")]
171    pub fn contains(
172        &self,
173        #[wasm_bindgen(param_description = "The point to check.")]
174        point: Point2D,
175    ) -> bool {
176        self.x_range.contains(point.x) && self.y_range.contains(point.y)
177    }
178
179    /// Gets a VectorObjectBuilder with the plot's points.
180    #[wasm_bindgen(getter, return_description = "A VectorObjectBuilder with the plot's points.")]
181    pub fn vector_object_builder(&self) -> Result<VectorObjectBuilder, JsError> {
182        let mut builder = VectorObjectBuilder::default();
183        let mut path = Path2D::default();
184        let mut previous_was_discontinuity = false;
185        let t_min = self.domain.start();
186        let t_max = self.domain.end();
187        let p_min = self.evaluate(t_min).ok_or_else(|| JsError::new("Failed to evaluate parametric function."))?;
188        let p_max = self.evaluate(t_max).ok_or_else(|| JsError::new("Failed to evaluate parametric function."))?;
189        self.on_point(&mut path, t_min, &p_min, &mut previous_was_discontinuity);
190        self.subdivide(&mut path, &mut previous_was_discontinuity, t_min, t_max, 0, p_min, p_max).ok_or_else(|| JsError::new("Failed to subdivide plot."))?;
191        self.on_point(&mut path, t_max, &p_max, &mut previous_was_discontinuity);
192        builder = builder.set_path(path);
193        Ok(builder)
194    }
195
196    /// Evaluates the parametric function at a given value.
197    #[wasm_bindgen(return_description = "The evaluated point.")]
198    pub fn evaluate(
199        &self,
200        #[wasm_bindgen(param_description = "The value to evaluate the parametric function at.")]
201        t: f32,
202    ) -> Option<Point2D> {
203        let x = self.expr_x.eval(&[t.into()]);
204        let y = self.expr_y.eval(&[t.into()]);
205        match (x, y) {
206            (Ok(x), Ok(y)) => Some(Point2D::new(x, y)),
207            _ => {
208                error("Failed to evaluate parametric function.");
209                None
210            },
211        }
212    }
213}
214
215impl ParametricFunctionPlot {
216    pub fn subdivide(
217        &self,
218        path: &mut Path2D,
219        previous_was_discontinuity: &mut bool,
220        min: f32,
221        max: f32,
222        depth: u32,
223        p_min: Point2D,
224        p_max: Point2D,
225    ) -> Option<()> {
226        let t = self.hash(min, max);
227        let mid = min + (max - min) * t;
228        let p_mid = self.evaluate(mid);
229        if p_mid.is_none() {
230            return None;
231        }
232        let p_mid = p_mid.unwrap();
233        let mut deepen = || {
234            let result = self.subdivide(path, previous_was_discontinuity, min, mid, depth + 1, p_min, p_mid);
235            if result.is_none() {
236                return None;
237            }
238            if self.is_discontinuity(mid) {
239                self.on_discontinuity(previous_was_discontinuity);
240            } else {
241                self.on_point(path, mid, &p_mid, previous_was_discontinuity);
242            }
243            let result = self.subdivide(path, previous_was_discontinuity, mid, max, depth + 1, p_mid, p_max);
244            if result.is_none() {
245                return None;
246            }
247            Some(())
248        };
249        if depth < self.min_depth {
250            let result = deepen();
251            if result.is_none() {
252                return None;
253            }
254        } else if depth < self.max_depth {
255            let fn_midpoint = Point2D::lerp(&p_min, &p_max, t);
256            let error = self.error(p_mid, fn_midpoint);
257            if error > self.threshold * self.threshold {
258                let result = deepen();
259                if result.is_none() {
260                    return None;
261                }
262            }
263        }
264        Some(())
265    }
266
267    pub fn on_point(&self, path: &mut Path2D, t: f32, p: &Point2D, previous_was_discontinuity: &mut bool) {
268        if self.contains(*p) && p.is_finite() && !self.is_discontinuity(t) {
269            if path.is_empty() || *previous_was_discontinuity {
270                path.push((self.composition)(*p));
271                *previous_was_discontinuity = false;
272            } else {
273                path.push_bezier(CubicBezierTuple::from_line(path.last().unwrap(), (self.composition)(*p)));
274            }
275        } else {
276            *previous_was_discontinuity = true;
277        }
278    }
279
280    pub fn on_discontinuity(&self, previous_was_discontinuity: &mut bool) {
281        *previous_was_discontinuity = true;
282    }
283
284    pub fn compose(&mut self, composition: Box<&'static dyn Fn(Point2D) -> Point2D>) {
285        self.composition = composition;
286    }
287}