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#[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 #[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 #[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 #[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 #[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 #[wasm_bindgen(getter, return_description = "The domain of the parametric function.")]
94 pub fn domain(&self) -> ClosedInterval {
95 self.domain.clone()
96 }
97
98 #[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 #[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 #[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 #[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 #[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 #[wasm_bindgen(getter, return_description = "The threshold of the plot.")]
130 pub fn threshold(&self) -> f32 {
131 self.threshold
132 }
133
134 #[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 #[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 #[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 #[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 #[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 #[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}