graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/wrangle.rs (34.9K)
1 //! The wrangle node: a script run once per element, over the Detail's own
2 //! surface.
3 //!
4 //! Houdini's attribwrangle, on an engine someone else maintains. The script
5 //! language is Rhai, chosen in `shapeshifter.md` Phase 7 for being pure Rust
6 //! with no C toolchain, sandboxed behind an operation budget — the step budget
7 //! the retired `kernel_cpu` interpreter used to count by hand — and compiled
8 //! once to an AST that is cached by source, as the retired launcher cached
9 //! kernels. What this module adds is the BINDING: how a script reaches the
10 //! geometry, and nothing else.
11 //!
12 //! The vocabulary, deliberately VEX-shaped so it reads as it does there:
13 //!
14 //! - `@P`, `@Cd`, `@N`, `@id`, `@ptnum` / `@primnum`, `@numpt` / `@numprim`,
15 //! `@Frame`, and `@name` for any attribute of any type. `@` is sugar —
16 //! Rhai has no such token — rewritten by [`desugar`] into an index on the
17 //! element (`__at["name"]`), so `@mass += 2.0` and `@P.y = 0.0` are ordinary
18 //! Rhai once the script reaches the engine. Naming an attribute creates it,
19 //! typed by the value written: a float, an int, a `vec3`, an array of two
20 //! or four. A float2 reads as a `vec3` with z = 0 and a float4 as an array.
21 //! - `ch("path")`, `chs`, `chv`, `chi` — the node's own parameters and, by
22 //! Houdini's relative paths, any other node's. The paths are resolved BEFORE
23 //! the script runs, through the expression scope, so an expression-valued
24 //! parameter is evaluated first and the script sees its value; the cost per
25 //! element is a map lookup. A path therefore has to be a string literal.
26 //! - `neighbours(pt)`, `prims(pt)`, `points(prim)` off the derived topology —
27 //! what decision 1 kept out of the kernel language because the interpreter
28 //! could not carry it — and `nearest(pos, radius)` off the point grid.
29 //! - `point(name, i)` / `setpoint`, `prim(name, i)` / `setprim`,
30 //! `detail(name)` / `setdetail`, `ingroup(name)` / `setgroup(name, bool)`.
31 //! - `addpoint(pos)`, `addprim([a, b, c])`, `removepoint(i)` — DEFERRED and
32 //! applied after the run, so a script iterating points sees a stable count.
33 //! `addpoint` returns the index the point will have.
34 //! - `vec3(x, y, z)`, `dot`, `cross`, `length`, `normalize`, `distance`,
35 //! `lerp`, `fit`, `clamp`, `rand(seed)`, and Rhai's own math.
36 //!
37 //! CPU only, and deliberately: an interpreter is an order of magnitude or
38 //! more below native Rust, which is fine for a wrangle over tens of thousands
39 //! of elements per edit and wrong for a solver at a million per frame — that
40 //! is Phase 7's step 4, WGSL compute through the renderer, and not this.
41
42 use crate::detail::{AttribType, AttribValue, Class, Detail};
43 use crate::spatial::PointGrid;
44 use glam::Vec3;
45 use rhai::{Array, Dynamic, Engine, EvalAltResult, ImmutableString, Scope, AST, FLOAT, INT};
46 use std::cell::{Cell, RefCell};
47 use std::collections::HashMap;
48 use std::rc::Rc;
49 use std::time::{Duration, Instant};
50
51 /// Per-element operation budget. A real script is tens to hundreds of
52 /// operations; this is a loop that never ends, caught in milliseconds.
53 const OPS_PER_ELEMENT: u64 = 2_000_000;
54 /// Wall-clock budget for the whole run, so a script that is merely slow over
55 /// a large input is an error rather than a frozen UI.
56 const RUN_BUDGET: Duration = Duration::from_secs(8);
57 /// Compiled scripts kept by source. Small: a project has a handful of
58 /// wrangles, and each edit of one is a new key.
59 const AST_CACHE_CAP: usize = 64;
60
61 /// A channel value resolved before the run: what `ch` / `chs` / `chv` read.
62 #[derive(Clone, Debug, PartialEq)]
63 pub struct Chan {
64 pub num: f64,
65 pub text: String,
66 }
67
68 impl Chan {
69 pub fn new(num: f64, text: impl Into<String>) -> Self {
70 Chan { num, text: text.into() }
71 }
72
73 /// `chv`: three `:`-separated components, or the number splatted.
74 fn vec(&self) -> Vec3 {
75 let parts: Vec<f32> = self.text.split(':').filter_map(|p| p.trim().parse::<f32>().ok()).collect();
76 if parts.len() == 3 {
77 Vec3::new(parts[0], parts[1], parts[2])
78 } else {
79 Vec3::splat(self.num as f32)
80 }
81 }
82 }
83
84 /// The Class parameter: which element the script runs once per.
85 pub fn parse_class(s: &str) -> Class {
86 let t = s.trim();
87 if t.eq_ignore_ascii_case("primitives") || t.eq_ignore_ascii_case("prims") || t.eq_ignore_ascii_case("primitive") {
88 Class::Prim
89 } else if t.eq_ignore_ascii_case("detail") {
90 Class::Detail
91 } else {
92 Class::Point
93 }
94 }
95
96 /// `@name` → `__at["name"]`, outside strings and comments.
97 ///
98 /// Rhai has no `@` token, so this is the whole of the sugar: an identifier
99 /// after `@` becomes an index on the element marker, and everything after it
100 /// — `.x`, `+=`, `[0]` — is Rhai's own syntax on the value that comes back.
101 /// String literals (double-quoted, backtick, and char literals) and both
102 /// comment forms are copied through untouched, so `"@"` in a message stays a
103 /// character.
104 pub fn desugar(code: &str) -> String {
105 let b = code.as_bytes();
106 let mut out = String::with_capacity(code.len() + 32);
107 let mut i = 0;
108 while i < b.len() {
109 let c = b[i];
110 // Comments.
111 if c == b'/' && i + 1 < b.len() && b[i + 1] == b'/' {
112 let end = code[i..].find('\n').map_or(b.len(), |n| i + n);
113 out.push_str(&code[i..end]);
114 i = end;
115 continue;
116 }
117 if c == b'/' && i + 1 < b.len() && b[i + 1] == b'*' {
118 let end = code[i + 2..].find("*/").map_or(b.len(), |n| i + 2 + n + 2);
119 out.push_str(&code[i..end]);
120 i = end;
121 continue;
122 }
123 // String and char literals: copy to the matching close, honouring
124 // backslash escapes.
125 if c == b'"' || c == b'`' || c == b'\'' {
126 let quote = c;
127 let mut j = i + 1;
128 while j < b.len() {
129 if b[j] == b'\\' && quote != b'`' {
130 j += 2;
131 continue;
132 }
133 if b[j] == quote {
134 j += 1;
135 break;
136 }
137 j += 1;
138 }
139 let j = j.min(b.len());
140 out.push_str(&code[i..j]);
141 i = j;
142 continue;
143 }
144 if c == b'@' && i + 1 < b.len() && (b[i + 1].is_ascii_alphabetic() || b[i + 1] == b'_') {
145 let mut j = i + 1;
146 while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_') {
147 j += 1;
148 }
149 out.push_str("__at[\"");
150 out.push_str(&code[i + 1..j]);
151 out.push_str("\"]");
152 i = j;
153 continue;
154 }
155 out.push(c as char);
156 i += 1;
157 }
158 out
159 }
160
161 /// The channel paths a script names as string literals — `ch("../Radius")`,
162 /// `chs`, `chv`, `chi`, `chf`, `chb` — so the caller can resolve them before
163 /// the run. A path built at runtime is not found here and errors when read.
164 pub fn channel_refs(code: &str) -> Vec<String> {
165 let src = strip_comments(code);
166 let b = src.as_bytes();
167 let mut out: Vec<String> = Vec::new();
168 for call in ["ch(", "chs(", "chv(", "chi(", "chf(", "chb("] {
169 let mut from = 0;
170 while let Some(pos) = src[from..].find(call) {
171 let at = from + pos;
172 from = at + call.len();
173 // A whole identifier: `search(` contains `ch(` and must not count.
174 if at > 0 && (b[at - 1].is_ascii_alphanumeric() || b[at - 1] == b'_') {
175 continue;
176 }
177 let rest = src[from..].trim_start();
178 let Some(quote) = rest.chars().next().filter(|c| *c == '"' || *c == '\'') else { continue };
179 let inner = &rest[1..];
180 let Some(end) = inner.find(quote) else { continue };
181 let path = inner[..end].to_string();
182 if !path.is_empty() && !out.contains(&path) {
183 out.push(path);
184 }
185 }
186 }
187 out
188 }
189
190 fn strip_comments(code: &str) -> String {
191 let mut out = String::with_capacity(code.len());
192 let mut rest = code;
193 while !rest.is_empty() {
194 if let Some(stripped) = rest.strip_prefix("//") {
195 let end = stripped.find('\n').unwrap_or(stripped.len());
196 rest = &stripped[end..];
197 } else if let Some(stripped) = rest.strip_prefix("/*") {
198 let end = stripped.find("*/").map_or(stripped.len(), |n| n + 2);
199 rest = &stripped[end..];
200 } else {
201 let mut chars = rest.chars();
202 out.push(chars.next().unwrap());
203 rest = chars.as_str();
204 }
205 }
206 out
207 }
208
209 // ------------------------------------------------------------ the binding
210
211 /// The marker the desugared `@` indexes: `__at["P"]`. Carries nothing; the
212 /// indexers reach the shared context through their captured handle.
213 #[derive(Clone, Copy)]
214 struct El;
215
216 /// What the script runs against: the geometry, which element it is on, and
217 /// the edits it has asked for that apply after the run.
218 struct Ctx {
219 d: Detail,
220 class: Class,
221 i: usize,
222 frame: i32,
223 normals: Option<Vec<Vec3>>,
224 grid: Option<(f32, PointGrid)>,
225 adds: Vec<Vec3>,
226 prim_adds: Vec<Vec<u32>>,
227 removes: Vec<usize>,
228 chans: HashMap<String, Result<Chan, String>>,
229 }
230
231 type Shared = Rc<RefCell<Ctx>>;
232 type RhaiResult<T> = Result<T, Box<EvalAltResult>>;
233
234 fn rt<T>(msg: impl Into<String>) -> RhaiResult<T> {
235 Err(msg.into().into())
236 }
237
238 fn class_name(c: Class) -> &'static str {
239 match c {
240 Class::Point => "point",
241 Class::Vertex => "vertex",
242 Class::Prim => "primitive",
243 Class::Detail => "detail",
244 }
245 }
246
247 /// A number out of anything numeric the script can hold.
248 fn num(v: &Dynamic) -> Result<f64, String> {
249 if v.is_float() {
250 Ok(v.as_float().unwrap_or(0.0))
251 } else if v.is_int() {
252 Ok(v.as_int().unwrap_or(0) as f64)
253 } else if v.is_bool() {
254 Ok(if v.as_bool().unwrap_or(false) { 1.0 } else { 0.0 })
255 } else {
256 Err(format!("expected a number, got {}", v.type_name()))
257 }
258 }
259
260 fn to_vec3(v: &Dynamic) -> Result<Vec3, String> {
261 if let Some(x) = v.clone().try_cast::<Vec3>() {
262 return Ok(x);
263 }
264 if v.is_array() {
265 let a = v.clone().into_array().unwrap_or_default();
266 if a.len() >= 3 {
267 return Ok(Vec3::new(num(&a[0])? as f32, num(&a[1])? as f32, num(&a[2])? as f32));
268 }
269 if a.len() == 2 {
270 return Ok(Vec3::new(num(&a[0])? as f32, num(&a[1])? as f32, 0.0));
271 }
272 return Err(format!("expected a vec3, got an array of {}", a.len()));
273 }
274 Ok(Vec3::splat(num(v)? as f32))
275 }
276
277 fn to_dyn(v: AttribValue) -> Dynamic {
278 match v {
279 AttribValue::Float(f) => Dynamic::from_float(f as FLOAT),
280 AttribValue::Int(i) => Dynamic::from_int(i as INT),
281 AttribValue::Float3(a) => Dynamic::from(Vec3::from(a)),
282 AttribValue::Float2(a) => Dynamic::from(Vec3::new(a[0], a[1], 0.0)),
283 AttribValue::Float4(a) => Dynamic::from_array(a.iter().map(|&f| Dynamic::from_float(f as FLOAT)).collect()),
284 }
285 }
286
287 /// A script value converted to an attribute's type — the type of the
288 /// attribute being written, so a whole number into a float attribute is a
289 /// float and a float into an int attribute is truncated, as the kernel
290 /// vocabulary does.
291 fn to_attr(v: &Dynamic, ty: AttribType) -> Result<AttribValue, String> {
292 Ok(match ty {
293 AttribType::Float => AttribValue::Float(to_scalar(v)? as f32),
294 AttribType::Int => AttribValue::Int(to_scalar(v)?.trunc() as i32),
295 AttribType::Float3 => AttribValue::Float3(to_vec3(v)?.to_array()),
296 AttribType::Float2 => {
297 let a = to_vec3(v)?;
298 AttribValue::Float2([a.x, a.y])
299 }
300 AttribType::Float4 => {
301 if v.is_array() {
302 let a = v.clone().into_array().unwrap_or_default();
303 if a.len() != 4 {
304 return Err(format!("expected four components, got {}", a.len()));
305 }
306 AttribValue::Float4([num(&a[0])? as f32, num(&a[1])? as f32, num(&a[2])? as f32, num(&a[3])? as f32])
307 } else if let Some(x) = v.clone().try_cast::<Vec3>() {
308 AttribValue::Float4([x.x, x.y, x.z, 1.0])
309 } else {
310 AttribValue::Float4([num(v)? as f32; 4])
311 }
312 }
313 })
314 }
315
316 /// A scalar out of a number or a vector's first component.
317 fn to_scalar(v: &Dynamic) -> Result<f64, String> {
318 if let Some(x) = v.clone().try_cast::<Vec3>() {
319 return Ok(x.x as f64);
320 }
321 num(v)
322 }
323
324 /// The attribute type a fresh attribute takes from the first value written.
325 fn infer_type(v: &Dynamic) -> Result<AttribType, String> {
326 if v.is_float() {
327 Ok(AttribType::Float)
328 } else if v.is_int() || v.is_bool() {
329 Ok(AttribType::Int)
330 } else if v.clone().try_cast::<Vec3>().is_some() {
331 Ok(AttribType::Float3)
332 } else if v.is_array() {
333 match v.clone().into_array().unwrap_or_default().len() {
334 2 => Ok(AttribType::Float2),
335 3 => Ok(AttribType::Float3),
336 4 => Ok(AttribType::Float4),
337 n => Err(format!("an array of {n} is not an attribute type (2, 3 or 4 components)")),
338 }
339 } else {
340 Err(format!("a {} cannot be stored as an attribute", v.type_name()))
341 }
342 }
343
344 fn zero_of(ty: AttribType) -> AttribValue {
345 match ty {
346 AttribType::Float => AttribValue::Float(0.0),
347 AttribType::Int => AttribValue::Int(0),
348 AttribType::Float2 => AttribValue::Float2([0.0; 2]),
349 AttribType::Float3 => AttribValue::Float3([0.0; 3]),
350 AttribType::Float4 => AttribValue::Float4([0.0; 4]),
351 }
352 }
353
354 impl Ctx {
355 fn check(&self, class: Class, i: usize) -> Result<(), String> {
356 let n = match class {
357 Class::Point => self.d.num_points(),
358 Class::Prim => self.d.num_prims(),
359 Class::Vertex => self.d.num_verts(),
360 Class::Detail => 1,
361 };
362 if i >= n {
363 return Err(format!("{} {i} is out of range ({n} {}s)", class_name(class), class_name(class)));
364 }
365 Ok(())
366 }
367
368 /// One element's attribute, or one of the intrinsics that read like one.
369 fn read(&mut self, class: Class, i: usize, name: &str) -> Result<Dynamic, String> {
370 match name {
371 "ptnum" | "primnum" | "elemnum" => return Ok(Dynamic::from_int(i as INT)),
372 "numpt" => return Ok(Dynamic::from_int(self.d.num_points() as INT)),
373 "numprim" => return Ok(Dynamic::from_int(self.d.num_prims() as INT)),
374 "Frame" => return Ok(Dynamic::from_int(self.frame as INT)),
375 _ => {}
376 }
377 self.check(class, i)?;
378 match (class, name) {
379 (Class::Point, "P") => Ok(Dynamic::from(self.d.pos(i))),
380 (Class::Prim, "P") => {
381 let pts = self.d.prim_points(i);
382 let sum: Vec3 = pts.iter().map(|&p| self.d.pos(p as usize)).sum();
383 Ok(Dynamic::from(if pts.is_empty() { Vec3::ZERO } else { sum / pts.len() as f32 }))
384 }
385 (Class::Point, "Cd") => Ok(Dynamic::from(Vec3::from(self.d.color(i)))),
386 (Class::Point, "id") => Ok(Dynamic::from_int(self.d.id(i).unwrap_or(0) as INT)),
387 (Class::Point, "N") if !self.d.points().has("N") => {
388 if self.normals.is_none() {
389 self.normals = Some(crate::geometry::point_normals(&self.d));
390 }
391 Ok(Dynamic::from(self.normals.as_ref().unwrap()[i]))
392 }
393 _ => match self.d.store(class).value(name, i) {
394 Some(v) => Ok(to_dyn(v)),
395 None => Ok(Dynamic::from_float(0.0)),
396 },
397 }
398 }
399
400 fn write(&mut self, class: Class, i: usize, name: &str, v: &Dynamic) -> Result<(), String> {
401 match name {
402 "ptnum" | "primnum" | "elemnum" | "numpt" | "numprim" | "Frame" | "id" => {
403 return Err(format!("@{name} is read-only"));
404 }
405 _ => {}
406 }
407 self.check(class, i)?;
408 match (class, name) {
409 (Class::Point, "P") => {
410 self.d.set_pos(i, to_vec3(v)?);
411 return Ok(());
412 }
413 (Class::Point, "Cd") => {
414 self.d.set_color(i, to_vec3(v)?.to_array());
415 return Ok(());
416 }
417 (_, "P") => return Err(format!("@P is read-only on a {}", class_name(class))),
418 _ => {}
419 }
420 let store = self.d.store_mut(class);
421 let ty = match store.get(name) {
422 Some(data) => data.ty(),
423 None => {
424 let ty = infer_type(v)?;
425 store.create(name, zero_of(ty));
426 ty
427 }
428 };
429 store.set_value(name, i, to_attr(v, ty)?)
430 }
431 }
432
433 fn wrap<T>(r: Result<T, String>) -> RhaiResult<T> {
434 r.map_err(|e| e.into())
435 }
436
437 fn index_arg(v: &Dynamic) -> Result<usize, String> {
438 let n = num(v)?;
439 if n < 0.0 {
440 return Err(format!("index {n} is negative"));
441 }
442 Ok(n as usize)
443 }
444
445 /// A deterministic unit float from a seed: splitmix64 over the seed's bits,
446 /// so `rand(@ptnum)` and `rand(@P.x * 7.3)` both give a stable draw.
447 fn rand_unit(seed: &Dynamic) -> f64 {
448 let bits: u64 = if seed.is_float() {
449 seed.as_float().unwrap_or(0.0).to_bits()
450 } else {
451 seed.as_int().unwrap_or(0) as u64
452 };
453 let mut z = bits.wrapping_add(0x9E37_79B9_7F4A_7C15);
454 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
455 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
456 z ^= z >> 31;
457 (z >> 11) as f64 / (1u64 << 53) as f64
458 }
459
460 fn build_engine(ctx: &Shared) -> Engine {
461 let mut engine = Engine::new();
462 engine.set_max_operations(OPS_PER_ELEMENT);
463
464 // Numbers mix. Rhai keeps ints and floats apart by default, and a script
465 // that writes `@P.y * 2` should not have to know which it wrote.
466 engine
467 .register_fn("+", |a: INT, b: FLOAT| a as FLOAT + b)
468 .register_fn("+", |a: FLOAT, b: INT| a + b as FLOAT)
469 .register_fn("-", |a: INT, b: FLOAT| a as FLOAT - b)
470 .register_fn("-", |a: FLOAT, b: INT| a - b as FLOAT)
471 .register_fn("*", |a: INT, b: FLOAT| a as FLOAT * b)
472 .register_fn("*", |a: FLOAT, b: INT| a * b as FLOAT)
473 .register_fn("/", |a: INT, b: FLOAT| a as FLOAT / b)
474 .register_fn("/", |a: FLOAT, b: INT| a / b as FLOAT)
475 .register_fn("%", |a: INT, b: FLOAT| (a as FLOAT) % b)
476 .register_fn("%", |a: FLOAT, b: INT| a % b as FLOAT)
477 .register_fn("**", |a: INT, b: FLOAT| (a as FLOAT).powf(b))
478 .register_fn("**", |a: FLOAT, b: INT| a.powf(b as FLOAT))
479 .register_fn("<", |a: INT, b: FLOAT| (a as FLOAT) < b)
480 .register_fn("<", |a: FLOAT, b: INT| a < b as FLOAT)
481 .register_fn(">", |a: INT, b: FLOAT| (a as FLOAT) > b)
482 .register_fn(">", |a: FLOAT, b: INT| a > b as FLOAT)
483 .register_fn("<=", |a: INT, b: FLOAT| (a as FLOAT) <= b)
484 .register_fn("<=", |a: FLOAT, b: INT| a <= b as FLOAT)
485 .register_fn(">=", |a: INT, b: FLOAT| (a as FLOAT) >= b)
486 .register_fn(">=", |a: FLOAT, b: INT| a >= b as FLOAT)
487 .register_fn("==", |a: INT, b: FLOAT| (a as FLOAT) == b)
488 .register_fn("==", |a: FLOAT, b: INT| a == b as FLOAT)
489 .register_fn("!=", |a: INT, b: FLOAT| (a as FLOAT) != b)
490 .register_fn("!=", |a: FLOAT, b: INT| a != b as FLOAT);
491
492 // vec3: glam's, with components, arithmetic and the usual functions.
493 engine
494 .register_type_with_name::<Vec3>("vec3")
495 .register_get_set("x", |v: &mut Vec3| v.x as FLOAT, |v: &mut Vec3, x: FLOAT| v.x = x as f32)
496 .register_get_set("y", |v: &mut Vec3| v.y as FLOAT, |v: &mut Vec3, y: FLOAT| v.y = y as f32)
497 .register_get_set("z", |v: &mut Vec3| v.z as FLOAT, |v: &mut Vec3, z: FLOAT| v.z = z as f32)
498 .register_set("x", |v: &mut Vec3, x: INT| v.x = x as f32)
499 .register_set("y", |v: &mut Vec3, y: INT| v.y = y as f32)
500 .register_set("z", |v: &mut Vec3, z: INT| v.z = z as f32)
501 .register_indexer_get(|v: &mut Vec3, i: INT| -> RhaiResult<FLOAT> {
502 match i {
503 0 => Ok(v.x as FLOAT),
504 1 => Ok(v.y as FLOAT),
505 2 => Ok(v.z as FLOAT),
506 _ => rt(format!("vec3 index {i} out of range")),
507 }
508 })
509 .register_indexer_set(|v: &mut Vec3, i: INT, x: Dynamic| -> RhaiResult<()> {
510 let x = wrap(num(&x))? as f32;
511 match i {
512 0 => v.x = x,
513 1 => v.y = x,
514 2 => v.z = x,
515 _ => return rt(format!("vec3 index {i} out of range")),
516 }
517 Ok(())
518 })
519 .register_fn("vec3", |x: Dynamic, y: Dynamic, z: Dynamic| -> RhaiResult<Vec3> {
520 Ok(Vec3::new(wrap(num(&x))? as f32, wrap(num(&y))? as f32, wrap(num(&z))? as f32))
521 })
522 .register_fn("vec3", |x: Dynamic| -> RhaiResult<Vec3> { wrap(to_vec3(&x)) })
523 .register_fn("+", |a: Vec3, b: Vec3| a + b)
524 .register_fn("-", |a: Vec3, b: Vec3| a - b)
525 .register_fn("-", |a: Vec3| -a)
526 .register_fn("*", |a: Vec3, b: Vec3| a * b)
527 .register_fn("*", |a: Vec3, b: FLOAT| a * b as f32)
528 .register_fn("*", |a: FLOAT, b: Vec3| b * a as f32)
529 .register_fn("*", |a: Vec3, b: INT| a * b as f32)
530 .register_fn("*", |a: INT, b: Vec3| b * a as f32)
531 .register_fn("/", |a: Vec3, b: Vec3| a / b)
532 .register_fn("/", |a: Vec3, b: FLOAT| a / b as f32)
533 .register_fn("/", |a: Vec3, b: INT| a / b as f32)
534 .register_fn("==", |a: Vec3, b: Vec3| a == b)
535 .register_fn("!=", |a: Vec3, b: Vec3| a != b)
536 .register_fn("to_string", |v: Vec3| format!("{}:{}:{}", v.x, v.y, v.z))
537 .register_fn("to_debug", |v: Vec3| format!("vec3({}, {}, {})", v.x, v.y, v.z))
538 .register_fn("dot", |a: Vec3, b: Vec3| a.dot(b) as FLOAT)
539 .register_fn("cross", |a: Vec3, b: Vec3| a.cross(b))
540 .register_fn("length", |a: Vec3| a.length() as FLOAT)
541 .register_fn("length2", |a: Vec3| a.length_squared() as FLOAT)
542 .register_fn("normalize", |a: Vec3| a.normalize_or_zero())
543 .register_fn("distance", |a: Vec3, b: Vec3| a.distance(b) as FLOAT)
544 .register_fn("abs", |a: Vec3| a.abs())
545 .register_fn("min", |a: Vec3, b: Vec3| a.min(b))
546 .register_fn("max", |a: Vec3, b: Vec3| a.max(b))
547 .register_fn("lerp", |a: Vec3, b: Vec3, t: Dynamic| -> RhaiResult<Vec3> { Ok(a.lerp(b, wrap(num(&t))? as f32)) })
548 .register_fn("lerp", |a: FLOAT, b: FLOAT, t: FLOAT| a + (b - a) * t)
549 .register_fn("clamp", |v: Vec3, lo: Vec3, hi: Vec3| v.clamp(lo, hi))
550 .register_fn("clamp", |v: Dynamic, lo: Dynamic, hi: Dynamic| -> RhaiResult<FLOAT> {
551 let (v, lo, hi) = (wrap(num(&v))?, wrap(num(&lo))?, wrap(num(&hi))?);
552 Ok(v.max(lo).min(hi))
553 })
554 .register_fn("fit", |v: Dynamic, a: Dynamic, b: Dynamic, c: Dynamic, d: Dynamic| -> RhaiResult<FLOAT> {
555 let (v, a, b, c, d) = (wrap(num(&v))?, wrap(num(&a))?, wrap(num(&b))?, wrap(num(&c))?, wrap(num(&d))?);
556 let t = if (b - a).abs() < 1e-12 { 0.0 } else { ((v - a) / (b - a)).clamp(0.0, 1.0) };
557 Ok(c + (d - c) * t)
558 })
559 .register_fn("rand", |seed: Dynamic| rand_unit(&seed));
560
561 // The element: `@name` desugars to `__at["name"]`.
562 engine.register_type_with_name::<El>("element");
563 let c = ctx.clone();
564 engine.register_indexer_get(move |_: &mut El, name: ImmutableString| -> RhaiResult<Dynamic> {
565 let mut c = c.borrow_mut();
566 let (class, i) = (c.class, c.i);
567 wrap(c.read(class, i, &name))
568 });
569 let c = ctx.clone();
570 engine.register_indexer_set(move |_: &mut El, name: ImmutableString, v: Dynamic| -> RhaiResult<()> {
571 let mut c = c.borrow_mut();
572 let (class, i) = (c.class, c.i);
573 wrap(c.write(class, i, &name, &v))
574 });
575
576 // Other elements, by index.
577 let c = ctx.clone();
578 engine.register_fn("point", move |name: ImmutableString, i: Dynamic| -> RhaiResult<Dynamic> {
579 let i = wrap(index_arg(&i))?;
580 wrap(c.borrow_mut().read(Class::Point, i, &name))
581 });
582 let c = ctx.clone();
583 engine.register_fn("setpoint", move |name: ImmutableString, i: Dynamic, v: Dynamic| -> RhaiResult<()> {
584 let i = wrap(index_arg(&i))?;
585 wrap(c.borrow_mut().write(Class::Point, i, &name, &v))
586 });
587 let c = ctx.clone();
588 engine.register_fn("prim", move |name: ImmutableString, i: Dynamic| -> RhaiResult<Dynamic> {
589 let i = wrap(index_arg(&i))?;
590 wrap(c.borrow_mut().read(Class::Prim, i, &name))
591 });
592 let c = ctx.clone();
593 engine.register_fn("setprim", move |name: ImmutableString, i: Dynamic, v: Dynamic| -> RhaiResult<()> {
594 let i = wrap(index_arg(&i))?;
595 wrap(c.borrow_mut().write(Class::Prim, i, &name, &v))
596 });
597 let c = ctx.clone();
598 engine.register_fn("detail", move |name: ImmutableString| -> RhaiResult<Dynamic> {
599 wrap(c.borrow_mut().read(Class::Detail, 0, &name))
600 });
601 let c = ctx.clone();
602 engine.register_fn("setdetail", move |name: ImmutableString, v: Dynamic| -> RhaiResult<()> {
603 wrap(c.borrow_mut().write(Class::Detail, 0, &name, &v))
604 });
605 let c = ctx.clone();
606 engine.register_fn("npoints", move || c.borrow().d.num_points() as INT);
607 let c = ctx.clone();
608 engine.register_fn("nprims", move || c.borrow().d.num_prims() as INT);
609
610 // Topology.
611 let c = ctx.clone();
612 engine.register_fn("neighbours", move |i: Dynamic| -> RhaiResult<Array> {
613 let i = wrap(index_arg(&i))?;
614 let c = c.borrow();
615 wrap(c.check(Class::Point, i))?;
616 Ok(c.d.point_neighbours(i).iter().map(|&n| Dynamic::from_int(n as INT)).collect())
617 });
618 let c = ctx.clone();
619 engine.register_fn("neighbors", move |i: Dynamic| -> RhaiResult<Array> {
620 let i = wrap(index_arg(&i))?;
621 let c = c.borrow();
622 wrap(c.check(Class::Point, i))?;
623 Ok(c.d.point_neighbours(i).iter().map(|&n| Dynamic::from_int(n as INT)).collect())
624 });
625 let c = ctx.clone();
626 engine.register_fn("prims", move |i: Dynamic| -> RhaiResult<Array> {
627 let i = wrap(index_arg(&i))?;
628 let c = c.borrow();
629 wrap(c.check(Class::Point, i))?;
630 Ok(c.d.point_prims(i).iter().map(|&n| Dynamic::from_int(n as INT)).collect())
631 });
632 let c = ctx.clone();
633 engine.register_fn("points", move |i: Dynamic| -> RhaiResult<Array> {
634 let i = wrap(index_arg(&i))?;
635 let c = c.borrow();
636 wrap(c.check(Class::Prim, i))?;
637 Ok(c.d.prim_points(i).iter().map(|&n| Dynamic::from_int(n as INT)).collect())
638 });
639 let c = ctx.clone();
640 engine.register_fn("nearest", move |pos: Dynamic, radius: Dynamic| -> RhaiResult<Array> {
641 let pos = wrap(to_vec3(&pos))?;
642 let radius = wrap(num(&radius))? as f32;
643 if radius <= 0.0 {
644 return Ok(Array::new());
645 }
646 let mut c = c.borrow_mut();
647 // The grid is built from the positions as they stand at the first
648 // call and keyed by radius; a script that moves points and asks
649 // again reads the earlier layout, which is what a per-element pass
650 // should see anyway.
651 if c.grid.as_ref().map_or(true, |(r, _)| (*r - radius).abs() > 1e-6) {
652 let pts: Vec<Vec3> = (0..c.d.num_points()).map(|p| c.d.pos(p)).collect();
653 c.grid = Some((radius, PointGrid::build(&pts, radius)));
654 }
655 let mut out = Vec::new();
656 c.grid.as_ref().unwrap().1.within(pos, radius, &mut out);
657 out.sort_by(|&a, &b| {
658 let da = c.d.pos(a as usize).distance_squared(pos);
659 let db = c.d.pos(b as usize).distance_squared(pos);
660 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
661 });
662 Ok(out.into_iter().map(|n| Dynamic::from_int(n as INT)).collect())
663 });
664
665 // Groups.
666 let c = ctx.clone();
667 engine.register_fn("ingroup", move |name: ImmutableString| -> bool {
668 let c = c.borrow();
669 c.d.store(c.class).in_group(&name, c.i)
670 });
671 let c = ctx.clone();
672 engine.register_fn("ingroup", move |name: ImmutableString, i: Dynamic| -> RhaiResult<bool> {
673 let i = wrap(index_arg(&i))?;
674 let c = c.borrow();
675 Ok(c.d.store(c.class).in_group(&name, i))
676 });
677 let c = ctx.clone();
678 engine.register_fn("setgroup", move |name: ImmutableString, on: Dynamic| -> RhaiResult<()> {
679 let on = wrap(num(&on))? != 0.0;
680 let mut c = c.borrow_mut();
681 let (class, i) = (c.class, c.i);
682 let store = c.d.store_mut(class);
683 if on {
684 if !store.has_group(&name) {
685 store.create_group(&name);
686 }
687 store.add_to_group(&name, i);
688 } else if store.has_group(&name) {
689 // No remove-one on the store: rebuild the membership without i.
690 let members: Vec<u32> = store.group_members(&name).into_iter().filter(|&m| m as usize != i).collect();
691 store.remove_group(&name);
692 store.create_group(&name);
693 for m in members {
694 store.add_to_group(&name, m as usize);
695 }
696 }
697 Ok(())
698 });
699
700 // Deferred structural edits.
701 let c = ctx.clone();
702 engine.register_fn("addpoint", move |pos: Dynamic| -> RhaiResult<INT> {
703 let pos = wrap(to_vec3(&pos))?;
704 let mut c = c.borrow_mut();
705 let idx = c.d.num_points() + c.adds.len();
706 c.adds.push(pos);
707 Ok(idx as INT)
708 });
709 let c = ctx.clone();
710 engine.register_fn("addprim", move |pts: Array| -> RhaiResult<INT> {
711 let mut c = c.borrow_mut();
712 let total = c.d.num_points() + c.adds.len();
713 let mut idx = Vec::with_capacity(pts.len());
714 for p in &pts {
715 let p = wrap(index_arg(p))?;
716 if p >= total {
717 return rt(format!("addprim: point {p} does not exist ({total} points)"));
718 }
719 idx.push(p as u32);
720 }
721 if idx.len() < 2 {
722 return rt("addprim: a primitive needs at least two points");
723 }
724 let n = c.d.num_prims() + c.prim_adds.len();
725 c.prim_adds.push(idx);
726 Ok(n as INT)
727 });
728 let c = ctx.clone();
729 engine.register_fn("removepoint", move |i: Dynamic| -> RhaiResult<()> {
730 let i = wrap(index_arg(&i))?;
731 let mut c = c.borrow_mut();
732 wrap(c.check(Class::Point, i))?;
733 c.removes.push(i);
734 Ok(())
735 });
736
737 // Channels, resolved before the run.
738 fn chan(c: &Shared, path: &str) -> RhaiResult<Chan> {
739 match c.borrow().chans.get(path) {
740 Some(Ok(ch)) => Ok(ch.clone()),
741 Some(Err(e)) => rt(e.clone()),
742 None => rt(format!("ch(\"{path}\"): a channel path must be a string literal, so it can be resolved before the script runs")),
743 }
744 }
745 let c = ctx.clone();
746 engine.register_fn("ch", move |path: ImmutableString| -> RhaiResult<FLOAT> { Ok(chan(&c, &path)?.num) });
747 let c = ctx.clone();
748 engine.register_fn("chf", move |path: ImmutableString| -> RhaiResult<FLOAT> { Ok(chan(&c, &path)?.num) });
749 let c = ctx.clone();
750 engine.register_fn("chi", move |path: ImmutableString| -> RhaiResult<INT> { Ok(chan(&c, &path)?.num.trunc() as INT) });
751 let c = ctx.clone();
752 engine.register_fn("chb", move |path: ImmutableString| -> RhaiResult<bool> { Ok(chan(&c, &path)?.num != 0.0) });
753 let c = ctx.clone();
754 engine.register_fn("chs", move |path: ImmutableString| -> RhaiResult<ImmutableString> { Ok(chan(&c, &path)?.text.into()) });
755 let c = ctx.clone();
756 engine.register_fn("chv", move |path: ImmutableString| -> RhaiResult<Vec3> { Ok(chan(&c, &path)?.vec()) });
757
758 engine
759 }
760
761 thread_local! {
762 static AST_CACHE: RefCell<HashMap<String, Rc<AST>>> = RefCell::new(HashMap::new());
763 }
764
765 fn compile(engine: &Engine, src: &str) -> Result<Rc<AST>, String> {
766 if let Some(ast) = AST_CACHE.with(|c| c.borrow().get(src).cloned()) {
767 return Ok(ast);
768 }
769 let ast = Rc::new(engine.compile(src).map_err(|e| format!("syntax: {e}"))?);
770 AST_CACHE.with(|c| {
771 let mut c = c.borrow_mut();
772 if c.len() >= AST_CACHE_CAP {
773 c.clear();
774 }
775 c.insert(src.to_string(), ast.clone());
776 });
777 Ok(ast)
778 }
779
780 /// Run `code` once per element of `class` in `input` (over `group`, if
781 /// named), with the channel values the caller resolved. On any error — a
782 /// syntax error, a runtime error on some element, the budget — the whole run
783 /// fails and the caller keeps its input: a half-wrangled geometry is not a
784 /// result.
785 pub fn run_wrangle(
786 input: Detail,
787 code: &str,
788 class: Class,
789 group: &str,
790 frame: i32,
791 chans: HashMap<String, Result<Chan, String>>,
792 ) -> Result<Detail, String> {
793 let class = match class {
794 Class::Vertex => Class::Point,
795 c => c,
796 };
797 let ctx: Shared = Rc::new(RefCell::new(Ctx {
798 d: input,
799 class,
800 i: 0,
801 frame,
802 normals: None,
803 grid: None,
804 adds: Vec::new(),
805 prim_adds: Vec::new(),
806 removes: Vec::new(),
807 chans,
808 }));
809 let mut engine = build_engine(&ctx);
810
811 // The wall-clock budget, checked every so often rather than per operation.
812 let started = Instant::now();
813 let ticks = Cell::new(0u32);
814 engine.on_progress(move |_| {
815 ticks.set(ticks.get().wrapping_add(1));
816 if ticks.get() % 4096 == 0 && started.elapsed() > RUN_BUDGET {
817 Some(Dynamic::from(format!("the script ran for more than {} s and was stopped", RUN_BUDGET.as_secs())))
818 } else {
819 None
820 }
821 });
822
823 let src = desugar(code);
824 let ast = compile(&engine, &src)?;
825
826 let group = group.trim();
827 let elements: Vec<usize> = {
828 let c = ctx.borrow();
829 let n = match class {
830 Class::Point => c.d.num_points(),
831 Class::Prim => c.d.num_prims(),
832 _ => 1,
833 };
834 (0..n).filter(|&i| class == Class::Detail || group.is_empty() || c.d.store(class).in_group(group, i)).collect()
835 };
836
837 let mut scope = Scope::new();
838 // A variable, not a constant: Rhai refuses to assign through an indexer
839 // on a constant, and `@P = ...` is exactly that.
840 scope.push("__at", El);
841 let base = scope.len();
842 for i in elements {
843 ctx.borrow_mut().i = i;
844 scope.rewind(base);
845 if let Err(e) = engine.run_ast_with_scope(&mut scope, &ast) {
846 let e = e.to_string();
847 let e = e.strip_prefix("Runtime error: ").unwrap_or(&e).to_string();
848 return Err(match class {
849 Class::Detail => e,
850 _ => format!("{} {i}: {e}", class_name(class)),
851 });
852 }
853 }
854
855 drop(scope);
856 drop(engine);
857 let mut c = ctx.borrow_mut();
858 let mut d = std::mem::replace(&mut c.d, Detail::new());
859 let adds = std::mem::take(&mut c.adds);
860 let prim_adds = std::mem::take(&mut c.prim_adds);
861 let removes = std::mem::take(&mut c.removes);
862 drop(c);
863
864 for p in adds {
865 d.add_point(p);
866 }
867 for pts in prim_adds {
868 d.add_prim(&pts);
869 }
870 if !removes.is_empty() {
871 let mut keep = vec![true; d.num_points()];
872 for r in removes {
873 if r < keep.len() {
874 keep[r] = false;
875 }
876 }
877 d.keep_points(&keep);
878 }
879 Ok(d)
880 }