fidget_rhai/
types.rs

1//! Rhai bindings for Fidget's 2D and 3D vector types
2use crate::FromDynamic;
3use fidget_core::{
4    context::{Tree, TreeOp},
5    var::Var,
6};
7use fidget_shapes::types::{Axis, Plane, Vec2, Vec3, Vec4};
8use rhai::EvalAltResult;
9
10macro_rules! register_all {
11    ($engine:ident, $ty:ident) => {
12        register_binary!($engine, $ty, "+", add, Add);
13        register_binary!($engine, $ty, "*", mul, Mul);
14        register_binary!($engine, $ty, "-", sub, Sub);
15        register_binary!($engine, $ty, "/", div, Div);
16
17        register_binary!($engine, $ty, min);
18        register_binary!($engine, $ty, max);
19
20        register_unary!($engine, $ty, sqrt);
21        register_unary!($engine, $ty, abs);
22        $engine.register_fn("-", |v: $ty| -v);
23    };
24}
25
26macro_rules! register_binary {
27    ($engine:ident, $ty:ident, $rop:expr, $base_fn:ident $(, $op:ident)?) => {
28        $engine.register_fn($rop, |a: $ty, b: $ty| -> $ty {
29            $( use std::ops::$op; )?
30            a.$base_fn(b)
31        });
32        $engine.register_fn($rop, |a: $ty, b: f64| -> $ty {
33            $( use std::ops::$op; )?
34            a.$base_fn(b)
35        });
36        $engine.register_fn($rop, |a: $ty, b: i64| -> $ty {
37            $( use std::ops::$op; )?
38            a.$base_fn(b as f64)
39        });
40        $engine.register_fn($rop, |a: f64, b: $ty| -> $ty {
41            $( use std::ops::$op; )?
42            $ty::from(a).$base_fn(b)
43        });
44        $engine.register_fn($rop, |a: i64, b: $ty| -> $ty {
45            $( use std::ops::$op; )?
46            $ty::from(a as f64).$base_fn(b)
47        });
48    };
49    ($engine:ident, $ty:ident, $base_fn:ident) => {
50        register_binary!($engine, $ty, stringify!($base_fn), $base_fn)
51    };
52}
53
54macro_rules! register_unary {
55    ($engine:ident, $ty:ident, $base_fn:ident) => {
56        $engine.register_fn(stringify!($base_fn), |a: $ty| -> $ty {
57            a.$base_fn()
58        });
59    };
60}
61
62/// Installs common types (from [`fidget::shapes`](crate::shapes)) into the engine
63pub fn register(engine: &mut rhai::Engine) {
64    register_vec2(engine);
65    register_vec3(engine);
66    register_all!(engine, Vec2);
67    register_all!(engine, Vec3);
68
69    register_axis(engine);
70    register_plane(engine);
71}
72
73fn register_vec2(engine: &mut rhai::Engine) {
74    engine
75        .register_type_with_name::<Vec2>("Vec2")
76        .register_fn("to_string", |t: &mut Vec2| format!("[{}, {}]", t.x, t.y))
77        .register_fn("vec2", |v: Vec2| v) // idempotent
78        .register_fn(
79            "vec2",
80            |ctx: rhai::NativeCallContext,
81             x: rhai::Dynamic,
82             y: rhai::Dynamic|
83             -> Result<Vec2, Box<EvalAltResult>> {
84                let x = f64::from_dynamic(&ctx, x, None)?;
85                let y = f64::from_dynamic(&ctx, y, None)?;
86                Ok(Vec2 { x, y })
87            },
88        )
89        .register_fn(
90            "vec2",
91            |ctx: rhai::NativeCallContext,
92             array: rhai::Array|
93             -> Result<Vec2, Box<EvalAltResult>> {
94                vec2_from_rhai_array(&ctx, array)
95            },
96        )
97        .register_get_set("x", |v: &mut Vec2| v.x, |v: &mut Vec2, x| v.x = x)
98        .register_get_set("y", |v: &mut Vec2| v.y, |v: &mut Vec2, y| v.y = y);
99}
100
101fn register_vec3(engine: &mut rhai::Engine) {
102    engine
103        .register_type_with_name::<Vec3>("Vec3")
104        .register_fn("to_string", |t: &mut Vec3| {
105            format!("[{}, {}, {}]", t.x, t.y, t.z)
106        })
107        .register_fn("vec3", |v: Vec3| v) // idempotent
108        .register_fn(
109            "vec3",
110            |ctx: rhai::NativeCallContext,
111             x: rhai::Dynamic,
112             y: rhai::Dynamic,
113             z: rhai::Dynamic|
114             -> Result<Vec3, Box<EvalAltResult>> {
115                let x = f64::from_dynamic(&ctx, x, None)?;
116                let y = f64::from_dynamic(&ctx, y, None)?;
117                let z = f64::from_dynamic(&ctx, z, None)?;
118                Ok(Vec3 { x, y, z })
119            },
120        )
121        .register_fn(
122            "vec3",
123            |ctx: rhai::NativeCallContext,
124             array: rhai::Array|
125             -> Result<Vec3, Box<EvalAltResult>> {
126                vec3_from_rhai_array(&ctx, array, None)
127            },
128        )
129        .register_get_set("x", |v: &mut Vec3| v.x, |v: &mut Vec3, x| v.x = x)
130        .register_get_set("y", |v: &mut Vec3| v.y, |v: &mut Vec3, y| v.y = y)
131        .register_get_set("z", |v: &mut Vec3| v.z, |v: &mut Vec3, z| v.z = z);
132}
133
134impl FromDynamic for Vec2 {
135    fn from_dynamic(
136        ctx: &rhai::NativeCallContext,
137        d: rhai::Dynamic,
138        _default: Option<&Vec2>,
139    ) -> Result<Self, Box<EvalAltResult>> {
140        if let Some(v) = d.clone().try_cast() {
141            Ok(v)
142        } else {
143            let array = d.into_array().map_err(|ty| {
144                EvalAltResult::ErrorMismatchDataType(
145                    "array".to_string(),
146                    ty.to_string(),
147                    ctx.position(),
148                )
149            })?;
150            vec2_from_rhai_array(ctx, array)
151        }
152    }
153}
154
155fn vec2_from_rhai_array(
156    ctx: &rhai::NativeCallContext,
157    array: rhai::Array,
158) -> Result<Vec2, Box<EvalAltResult>> {
159    match array.len() {
160        2 => {
161            let x = f64::from_dynamic(ctx, array[0].clone(), None)?;
162            let y = f64::from_dynamic(ctx, array[1].clone(), None)?;
163            Ok(Vec2 { x, y })
164        }
165        n => Err(EvalAltResult::ErrorMismatchDataType(
166            "[float; 2]".to_string(),
167            format!("[dynamic; {n}]"),
168            ctx.position(),
169        )
170        .into()),
171    }
172}
173
174impl FromDynamic for Vec3 {
175    fn from_dynamic(
176        ctx: &rhai::NativeCallContext,
177        d: rhai::Dynamic,
178        default: Option<&Vec3>,
179    ) -> Result<Self, Box<EvalAltResult>> {
180        if let Ok(v) = Vec2::from_dynamic(ctx, d.clone(), None) {
181            Ok(Vec3 {
182                x: v.x,
183                y: v.y,
184                z: default.map(|d| d.z).unwrap_or(0.0),
185            })
186        } else if let Some(v) = d.clone().try_cast() {
187            Ok(v)
188        } else {
189            let array = d.into_array().map_err(|ty| {
190                EvalAltResult::ErrorMismatchDataType(
191                    "array".to_string(),
192                    ty.to_string(),
193                    ctx.position(),
194                )
195            })?;
196            vec3_from_rhai_array(ctx, array, default)
197        }
198    }
199}
200
201fn vec3_from_rhai_array(
202    ctx: &rhai::NativeCallContext,
203    array: rhai::Array,
204    default: Option<&Vec3>,
205) -> Result<Vec3, Box<EvalAltResult>> {
206    match array.len() {
207        2 => {
208            let x = f64::from_dynamic(ctx, array[0].clone(), None)?;
209            let y = f64::from_dynamic(ctx, array[1].clone(), None)?;
210            let z = default.map(|d| d.z).unwrap_or(0.0);
211            Ok(Vec3 { x, y, z })
212        }
213        3 => {
214            let x = f64::from_dynamic(ctx, array[0].clone(), None)?;
215            let y = f64::from_dynamic(ctx, array[1].clone(), None)?;
216            let z = f64::from_dynamic(ctx, array[2].clone(), None)?;
217            Ok(Vec3 { x, y, z })
218        }
219        n => Err(EvalAltResult::ErrorMismatchDataType(
220            "[float; 3]".to_string(),
221            format!("[dynamic; {n}]"),
222            ctx.position(),
223        )
224        .into()),
225    }
226}
227
228impl FromDynamic for Vec4 {
229    fn from_dynamic(
230        ctx: &rhai::NativeCallContext,
231        d: rhai::Dynamic,
232        _default: Option<&Vec4>,
233    ) -> Result<Self, Box<EvalAltResult>> {
234        if let Some(v) = d.clone().try_cast() {
235            Ok(v)
236        } else {
237            let array = d.into_array().map_err(|ty| {
238                EvalAltResult::ErrorMismatchDataType(
239                    "array".to_string(),
240                    ty.to_string(),
241                    ctx.position(),
242                )
243            })?;
244            match array.len() {
245                4 => {
246                    let x = f64::from_dynamic(ctx, array[0].clone(), None)?;
247                    let y = f64::from_dynamic(ctx, array[1].clone(), None)?;
248                    let z = f64::from_dynamic(ctx, array[2].clone(), None)?;
249                    let w = f64::from_dynamic(ctx, array[3].clone(), None)?;
250                    Ok(Vec4 { x, y, z, w })
251                }
252                n => Err(EvalAltResult::ErrorMismatchDataType(
253                    "[float; 4]".to_string(),
254                    format!("[dynamic; {n}]"),
255                    ctx.position(),
256                )
257                .into()),
258            }
259        }
260    }
261}
262
263impl FromDynamic for Axis {
264    fn from_dynamic(
265        ctx: &rhai::NativeCallContext,
266        d: rhai::Dynamic,
267        _default: Option<&Self>,
268    ) -> Result<Self, Box<EvalAltResult>> {
269        let out = if let Some(v) = d.clone().try_cast() {
270            Some(v)
271        } else if let Ok(v) = Vec3::from_dynamic(ctx, d.clone(), None) {
272            let v = v.try_into().map_err(|e| {
273                Box::new(EvalAltResult::ErrorMismatchDataType(
274                    format!("conversion failed: {e}"),
275                    "vec3 with reasonable length".to_string(),
276                    ctx.position(),
277                ))
278            })?;
279            Some(v)
280        } else if let Ok(s) = d.clone().into_immutable_string() {
281            match s.as_str() {
282                "x" | "X" => Some(Axis::X),
283                "y" | "Y" => Some(Axis::Y),
284                "z" | "Z" => Some(Axis::Z),
285                _ => None,
286            }
287        } else if let Ok(c) = d.clone().as_char() {
288            match c {
289                'x' | 'X' => Some(Axis::X),
290                'y' | 'Y' => Some(Axis::Y),
291                'z' | 'Z' => Some(Axis::Z),
292                _ => None,
293            }
294        } else if let Some(t) = d.clone().try_cast::<Tree>() {
295            match &*t {
296                TreeOp::Input(Var::X) => Some(Axis::X),
297                TreeOp::Input(Var::Y) => Some(Axis::Y),
298                TreeOp::Input(Var::Z) => Some(Axis::Z),
299                _ => None,
300            }
301        } else {
302            None
303        };
304
305        out.ok_or_else(|| {
306            EvalAltResult::ErrorMismatchDataType(
307                "vec3 or [float; 3]".to_string(),
308                d.type_name().to_owned(),
309                ctx.position(),
310            )
311            .into()
312        })
313    }
314}
315
316fn print_axis(t: Axis) -> String {
317    if t == Axis::X {
318        "axis(\"x\")".to_owned()
319    } else if t == Axis::Y {
320        "axis(\"y\")".to_owned()
321    } else if t == Axis::Z {
322        "axis(\"z\")".to_owned()
323    } else {
324        let v = t.vec();
325        format!("axis([{}, {}, {}])", v.x, v.y, v.z)
326    }
327}
328
329fn register_axis(engine: &mut rhai::Engine) {
330    engine
331        .register_type_with_name::<Axis>("Axis")
332        .register_fn("to_string", |t: &mut Axis| print_axis(*t))
333        .register_fn(
334            "axis",
335            |ctx: rhai::NativeCallContext,
336             v: rhai::Dynamic|
337             -> Result<Axis, Box<EvalAltResult>> {
338                Axis::from_dynamic(&ctx, v, None)
339            },
340        );
341}
342
343impl FromDynamic for Plane {
344    fn from_dynamic(
345        ctx: &rhai::NativeCallContext,
346        d: rhai::Dynamic,
347        _default: Option<&Self>,
348    ) -> Result<Self, Box<EvalAltResult>> {
349        let r = if let Some(v) = d.clone().try_cast() {
350            Some(v)
351        } else if let Ok(axis) = Axis::from_dynamic(ctx, d.clone(), None) {
352            Some(Self { axis, offset: 0.0 })
353        } else if let Ok(s) = d.clone().into_immutable_string() {
354            match s.as_str() {
355                "xy" | "XY" => Some(Plane::XY),
356                "yz" | "YZ" => Some(Plane::YZ),
357                "zx" | "ZX" => Some(Plane::ZX),
358                _ => None,
359            }
360        } else {
361            None
362        };
363
364        r.ok_or_else(|| {
365            EvalAltResult::ErrorMismatchDataType(
366                "axis or plane name".to_owned(),
367                d.type_name().to_owned(),
368                ctx.position(),
369            )
370            .into()
371        })
372    }
373}
374
375fn register_plane(engine: &mut rhai::Engine) {
376    engine
377        .register_type_with_name::<Plane>("Plane")
378        .register_fn("to_string", |t: &mut Plane| {
379            if t == &Plane::XY {
380                "plane(\"xy\")".to_owned()
381            } else if t == &Plane::YZ {
382                "plane(\"yz\")".to_owned()
383            } else if t == &Plane::ZX {
384                "plane(\"zx\")".to_owned()
385            } else {
386                let ax = print_axis(t.axis);
387                if t.offset == 0.0 {
388                    format!("plane({ax})")
389                } else {
390                    format!("plane({ax}, {})", t.offset)
391                }
392            }
393        })
394        .register_fn(
395            "plane",
396            |ctx: rhai::NativeCallContext,
397             v: rhai::Dynamic|
398             -> Result<Plane, Box<EvalAltResult>> {
399                Plane::from_dynamic(&ctx, v, None)
400            },
401        )
402        .register_fn(
403            "plane",
404            |ctx: rhai::NativeCallContext,
405             v: rhai::Dynamic,
406             offset: f64|
407             -> Result<Plane, Box<EvalAltResult>> {
408                let plane = Plane::from_dynamic(&ctx, v, None)?;
409                Ok(Plane {
410                    axis: plane.axis,
411                    offset,
412                })
413            },
414        );
415}
416
417#[cfg(test)]
418mod test {
419    use super::*;
420    use std::sync::{Arc, Mutex};
421
422    #[test]
423    fn type_constructors() {
424        let mut e = rhai::Engine::new();
425        register(&mut e);
426        assert_eq!(
427            e.eval::<Vec2>("vec2([5, 10.0])").unwrap(),
428            Vec2::new(5.0, 10.0),
429        );
430        assert_eq!(
431            e.eval::<Vec3>("vec3([1, 2, 3])").unwrap(),
432            Vec3::new(1.0, 2.0, 3.0),
433        );
434        assert_eq!(
435            e.eval::<Vec3>("vec3([1, 2])").unwrap(),
436            Vec3::new(1.0, 2.0, 0.0),
437        );
438
439        assert_eq!(e.eval::<Axis>("axis([1, 0])").unwrap(), Axis::X);
440        assert_eq!(e.eval::<Axis>("axis([0, 0, 1])").unwrap(), Axis::Z);
441        assert_eq!(e.eval::<Axis>("axis('z')").unwrap(), Axis::Z);
442        assert_eq!(e.eval::<Axis>("axis(\"z\")").unwrap(), Axis::Z);
443        assert!(e.eval::<Axis>("axis([0, 0, 0])").is_err());
444
445        assert_eq!(e.eval::<Plane>("plane([1, 0])").unwrap(), Plane::YZ);
446        assert_eq!(
447            e.eval::<Plane>("plane([1, 0], 0.5)").unwrap(),
448            Plane {
449                axis: Axis::X,
450                offset: 0.5
451            }
452        );
453        assert_eq!(e.eval::<Plane>("plane(\"yz\")").unwrap(), Plane::YZ);
454    }
455
456    #[test]
457    fn type_printing() {
458        let mut e = rhai::Engine::new();
459        register(&mut e);
460        let lines = Arc::new(Mutex::new(vec![]));
461        let lines_ = lines.clone();
462        e.on_print(move |s| lines_.lock().unwrap().push(s.to_string()));
463
464        e.eval::<()>("print(vec2(1, 0))").unwrap();
465        assert_eq!(
466            lines.lock().unwrap().last().map(|s| s.as_str()),
467            Some("[1, 0]")
468        );
469
470        e.eval::<()>("print(vec3(1, 2, 3))").unwrap();
471        assert_eq!(
472            lines.lock().unwrap().last().map(|s| s.as_str()),
473            Some("[1, 2, 3]")
474        );
475
476        e.eval::<()>("print(axis([1, 0]))").unwrap();
477        assert_eq!(
478            lines.lock().unwrap().last().map(|s| s.as_str()),
479            Some("axis(\"x\")")
480        );
481
482        e.eval::<()>("print(plane([1, 0]))").unwrap();
483        assert_eq!(
484            lines.lock().unwrap().last().map(|s| s.as_str()),
485            Some("plane(\"yz\")")
486        );
487    }
488
489    #[test]
490    fn type_ops() {
491        let mut e = rhai::Engine::new();
492        register(&mut e);
493        let v = e.eval::<Vec2>("-vec2(1, 2)").unwrap();
494        assert_eq!(v.x, -1.0);
495        assert_eq!(v.y, -2.0);
496    }
497}