fidget_rhai/lib.rs
1//! Rhai bindings to Fidget
2//!
3//! The [`engine`] function lets you construct a [`rhai::Engine`] with
4//! Fidget-specific bindings. The rest of this documentation explains the
5//! behavior of those bindings; for low-level details, see the [`engine`]
6//! docstring.
7//!
8//! # Introduction
9//! Rhai is a general-purpose scripting language embedded in Rust. When used
10//! for Fidget scripting, we use the Rhai script to capture a math expression.
11//! This math expression can then be processed by Fidget's optimized evaluators.
12//!
13//! It's important to distinguish between the Rhai script and the target math
14//! expression:
15//!
16//! - The Rhai script is general-purpose, evaluated a single time to compute
17//! math expressions, and supports language features like conditionals and
18//! loops
19//! - The math expression is closed-form arithmetic, evaluated _many_ times
20//! over the course of rendering, and only supports operations from
21//! [`TreeOp`](fidget_core::context::TreeOp)
22//!
23//! The purpose of evaluating the Rhai script is to capture a _trace_ of a math
24//! expression. Evaluating `x + y` in a Rhai script does not actually do any
25//! arithmetic; instead, it creates a math expression `Add(Var::X, Var::Y)`.
26//!
27//! Operator overloading makes this ergonomic, but can mask the fact that the
28//! Rhai script is not the math expression!
29//!
30//! # Trees
31//! The basic type for math expressions is a `Tree`, which is equivalent to
32//! [`fidget_core::context::Tree`]. Trees are typically built from `(x, y, z)`
33//! primitives, which can be constructed with the `axes()` function:
34//!
35//! ```
36//! # fidget_rhai::engine().run("
37//! let xyz = axes();
38//! xyz.x + xyz.y
39//! # ").unwrap();
40//! ```
41//!
42//! `x, y, z` variables are also automatically injected into the `Engine`'s
43//! context before evaluation.
44//!
45//! # Mathematical constants
46//! The Rhai context includes common mathematical constants that can be used
47//! in expressions:
48//!
49//! ```
50//! # fidget_rhai::engine().run("
51//! // Use PI for angular calculations
52//! let angle = PI / 4.0;
53//! let radius = 2.0;
54//! circle(#{ center: [radius * cos(angle), radius * sin(angle)], radius: 1.0 })
55//! # ").unwrap();
56//! ```
57//!
58//! Available constants include:
59//! - `PI` - π (3.14159...)
60//! - `E` - Euler's number (2.71828...)
61//! - `TAU` - 2π (6.28318...)
62//! - `PHI` / `GOLDEN_RATIO` - Golden ratio (1.61803...)
63//! - `SQRT_2`, `SQRT_3` - Square roots
64//! - `FRAC_PI_2`, `FRAC_PI_4`, etc. - Fractions of π
65//! - `LN_2`, `LN_10` - Natural logarithms
66//! - And many more mathematical constants
67//!
68//! # Vector types
69//! The Rhai context includes `vec2`, `vec3`, and `vec4` types, which are
70//! roughly analogous to their GLSL equivalents (for floating-point only).
71//! Many (but not all) functions are implemented and overloaded for these types;
72//! if you encounter something missing, feel free to open an issue.
73//!
74//! # Shapes
75//! In Rhai scripts, shapes can be constructed using object map notation:
76//! ```
77//! # fidget_rhai::engine().run("
78//! circle(#{ center: vec2(1.0, 2.0), radius: 3.0 })
79//! # ").unwrap();
80//! ```
81//!
82//! This works for any object type; in addition, there are a bunch of ergonomic
83//! improvements on top of this low-level syntax.
84//!
85//! ## Type coercions
86//! Shapes are built from a set of Rust primitives, with generous conversions
87//! from Rhai's native types:
88//!
89//! - Scalar values (`f64`)
90//! - Both floating-point and integer Rhai values will be accepted
91//! - Vectors (`vec2` and `vec3`)
92//! - These may be explicitly constructed with `vec2(x, y)` and
93//! `vec3(x, y, z)`
94//! - Appropriately-sized arrays of numbers will be automatically converted
95//! - A `vec2` (or something convertible into a `vec2`) will be converted
96//! into a `vec3` with a default `z` value. This default value is
97//! shape-specific, e.g. it will be 0 for a position and 1 for a scale.
98//! ```
99//! # fidget_rhai::engine().run("
100//! // array -> vec2
101//! let c = circle(#{ center: [1, 2], radius: 3 });
102//!
103//! // array -> vec3
104//! let s = sphere(#{ center: [1, 2, 4], radius: 3 });
105//!
106//! // array -> vec2 -> vec3
107//! move(#{ shape: c, offset: [1, 1] });
108//! # ").unwrap();
109//! ```
110//!
111//! ## Default values
112//! Many shape fields have sensibly-defined default values; these are usually
113//! either 0 or 1 (or the equivalent `VecX` values). Fields with default values
114//! may be omitted from the map:
115//!
116//! ```
117//! # fidget_rhai::engine().run("
118//! let c = circle(#{ center: [1, 2] }); // radius = 1
119//! let s = sphere(#{ radius: 3 }); // center = [0, 0, 0]
120//! # ").unwrap();
121//! ```
122//!
123//! ## Uniquely typed functions
124//! Any shape with unique arguments may skip the object map and pass arguments
125//! directly; order doesn't matter, because the type is unambiguous.
126//!
127//! ```
128//! # fidget_rhai::engine().run("
129//! // array -> vec2
130//! let c1 = circle([1, 2], 3);
131//! let c2 = circle(3, [1, 2]); // order doesn't matter!
132//! # ").unwrap();
133//! ```
134//!
135//! In addition, fields with default values may be skipped:
136//!
137//! ```
138//! # fidget_rhai::engine().run("
139//! // array -> vec2
140//! let c1 = circle([1, 2]); // radius = 1
141//! let c2 = circle(); // center = [0, 0], radius = 1
142//! # ").unwrap();
143//! ```
144//!
145//! `vec2 -> vec3` coercion also works in this regime, if the `vec3` has a
146//! default value:
147//!
148//! ```
149//! # fidget_rhai::engine().run("
150//! // array -> vec2 -> vec3
151//! let c1 = sphere([1, 1], 4); // z = 0
152//! # ").unwrap();
153//! ```
154//!
155//! ## Function chaining
156//! Shapes with a single initial `Tree` member are typically transforms (e.g.
157//! `move` from above). These functions may be called with the tree as their
158//! first (unnamed) argument, followed by an object map of remaining parameters.
159//!
160//! ```
161//! # fidget_rhai::engine().run("
162//! let c = circle(#{ center: [1, 2], radius: 3 });
163//! move(c, #{ offset: [1, 1] });
164//! # ").unwrap();
165//! ```
166//!
167//! Given Rhai's dispatch strategy, this can also be written as a function
168//! chain, which is more ergonomic for a string of transforms:
169//!
170//! ```
171//! # fidget_rhai::engine().run("
172//! circle(#{ center: [1, 2], radius: 3 })
173//! .move(#{ offset: [1, 1] });
174//! # ").unwrap();
175//! ```
176//!
177//! A transform which only take a single argument may skip the object map:
178//!
179//! ```
180//! # fidget_rhai::engine().run("
181//! circle(#{ center: [1, 2], radius: 3 })
182//! .move([1, 1]);
183//! # ").unwrap();
184//! ```
185//!
186//! ## Functions of two trees
187//! Shapes which take two trees can be called with two (unnamed) arguments:
188//!
189//! ```
190//! # fidget_rhai::engine().run("
191//! let a = circle(#{ center: [0, 0], radius: 1 });
192//! let b = circle(#{ center: [1, 0], radius: 0.5 });
193//! difference(a, b);
194//! # ").unwrap();
195//! ```
196//!
197//! ## Tree reduction functions
198//! Any function which takes a single `Vec<Tree>` will accept both an array of
199//! trees or individual tree arguments (up to an 8-tuple).
200//!
201//! ```
202//! # fidget_rhai::engine().run("
203//! let a = circle(#{ center: [1, 1], radius: 3 });
204//! let b = circle(#{ center: [2, 2], radius: 3 });
205//! let c = circle(#{ center: [3, 3], radius: 3 });
206//! union([a, b, c]);
207//! union(a, b, c);
208//! union(a, b, c, a, b, c, a, b);
209//! # ").unwrap();
210//! ```
211//!
212//! ## Automatic tree reduction
213//! Any shape which takes a `Tree` will also accept an array of trees, which are
214//! automatically reduced with a union operation.
215//!
216//! ```
217//! # fidget_rhai::engine().run("
218//! [
219//! circle(#{ center: [0, 0], radius: 3 }),
220//! circle(#{ center: [2, 2], radius: 3 }),
221//! ]
222//! .move(#{ offset: [1, 1] });
223//! # ").unwrap();
224//! ```
225#![warn(missing_docs)]
226
227pub mod constants;
228pub mod shapes;
229pub mod tree;
230pub mod types;
231
232use fidget_core::context::Tree;
233
234/// Build a new engine with Fidget-specific bindings and settings
235///
236/// - Mathematical constants (PI, E, TAU, etc.), provided by the [`resolver`] function
237/// - `Tree`-specific type, overloads, and `axes()` ([`tree::register`])
238/// - Custom types (e.g. GLSL-style vectors), provided by [`types::register`]
239/// - Shapes and transforms ([`shapes::register`])
240/// - An `on_progress` limit of 50,000 steps (chosen arbitrarily)
241/// - Max expression and function expression depths of 64 and 32 (also chosen
242/// arbitrarily)
243/// - [`set_fail_on_invalid_map_property`](rhai::Engine::set_fail_on_invalid_map_property)
244/// set to `true`, so that missing map items raise an error
245/// - A custom resolver ([`resolver`]) which provides fallbacks for `x`, `y`,
246/// `z`, and mathematical constants (if not defined)
247pub fn engine() -> rhai::Engine {
248 let mut engine = rhai::Engine::new();
249
250 tree::register(&mut engine);
251 types::register(&mut engine);
252 shapes::register(&mut engine);
253
254 engine.set_fail_on_invalid_map_property(true);
255 engine.set_max_expr_depths(64, 32);
256 engine.on_progress(move |count| {
257 if count > 50_000 {
258 Some("script runtime exceeded".into())
259 } else {
260 None
261 }
262 });
263
264 #[allow(deprecated, reason = "not actually deprecated, just unstable")]
265 engine.on_var(resolver);
266
267 engine
268}
269
270/// Variable resolver which provides `x`, `y`, `z` and mathematical constants if not found
271pub fn resolver(
272 name: &str,
273 _index: usize,
274 ctx: rhai::EvalContext,
275) -> Result<Option<rhai::Dynamic>, Box<rhai::EvalAltResult>> {
276 if ctx.scope().contains(name) {
277 Ok(None)
278 } else {
279 match name {
280 "x" => Ok(Some(rhai::Dynamic::from(Tree::x()))),
281 "y" => Ok(Some(rhai::Dynamic::from(Tree::y()))),
282 "z" => Ok(Some(rhai::Dynamic::from(Tree::z()))),
283 _ => {
284 // Try to resolve as a mathematical constant
285 if let Some(constant) = constants::get_constant(name) {
286 Ok(Some(rhai::Dynamic::from(constant)))
287 } else {
288 Ok(None)
289 }
290 }
291 }
292 }
293}
294
295////////////////////////////////////////////////////////////////////////////////
296
297/// Helper trait to go from a Rhai dynamic object to a particular type
298pub trait FromDynamic
299where
300 Self: Sized,
301{
302 /// Build an object from a dynamic value and optional default
303 fn from_dynamic(
304 ctx: &rhai::NativeCallContext,
305 v: rhai::Dynamic,
306 default: Option<&Self>,
307 ) -> Result<Self, Box<rhai::EvalAltResult>>;
308}
309
310impl FromDynamic for f64 {
311 fn from_dynamic(
312 ctx: &rhai::NativeCallContext,
313 d: rhai::Dynamic,
314 _default: Option<&f64>,
315 ) -> Result<Self, Box<rhai::EvalAltResult>> {
316 let ty = d.type_name();
317 d.clone()
318 .try_cast::<f64>()
319 .or_else(|| d.try_cast::<i64>().map(|f| f as f64))
320 .ok_or_else(|| {
321 rhai::EvalAltResult::ErrorMismatchDataType(
322 "float".to_string(),
323 ty.to_string(),
324 ctx.position(),
325 )
326 .into()
327 })
328 }
329}
330
331////////////////////////////////////////////////////////////////////////////////
332
333#[cfg(test)]
334mod test {
335 use super::*;
336 use fidget_core::Context;
337
338 #[test]
339 fn test_eval() {
340 let engine = engine();
341 let t = engine.eval("x + y").unwrap();
342 let mut ctx = Context::new();
343 let sum = ctx.import(&t);
344 assert_eq!(ctx.eval_xyz(sum, 1.0, 2.0, 0.0).unwrap(), 3.0);
345 }
346
347 #[test]
348 fn test_eval_multiline() {
349 let engine = engine();
350 let t = engine.eval("let foo = x; foo + y").unwrap();
351 let mut ctx = Context::new();
352 let sum = ctx.import(&t);
353 assert_eq!(ctx.eval_xyz(sum, 1.0, 2.0, 0.0).unwrap(), 3.0);
354 }
355
356 #[test]
357 fn test_no_comparison() {
358 let engine = engine();
359 let out = engine.run("x < 0");
360 assert!(out.is_err());
361 }
362
363 #[test]
364 fn resolver_fallback() {
365 let engine = engine();
366 let out: bool = engine.eval("let x = 1; x < 0").unwrap();
367 assert!(!out);
368 }
369
370 #[test]
371 fn push() {
372 let engine = engine();
373 let out: i64 =
374 engine.eval("let foo = []; foo.push(1); foo.len()").unwrap();
375 assert_eq!(out, 1);
376 }
377
378 #[test]
379 fn test_constants() {
380 let engine = engine();
381
382 // Test that PI constant is available
383 let pi: f64 = engine.eval("PI").unwrap();
384 assert!((pi - std::f64::consts::PI).abs() < f64::EPSILON);
385
386 // Test using constants in angular calculations
387 let t = engine
388 .eval("let angle = PI / 4.0; x * cos(angle) + y * sin(angle)")
389 .unwrap();
390 let mut ctx = Context::new();
391 let expr = ctx.import(&t);
392
393 // Evaluate at (1, 1) - should be sqrt(2) since cos(π/4) = sin(π/4) = 1/√2
394 let result = ctx.eval_xyz(expr, 1.0, 1.0, 0.0).unwrap();
395 let expected = std::f64::consts::SQRT_2;
396 assert!((result - expected).abs() < 1e-10);
397 }
398}