graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/param.rs (29K)
1 //! Node parameters: what a parameter holds, typed.
2 //!
3 //! A [`ParamDef`] keeps two things about its value and keeps them together:
4 //! the TEXT, exactly as the user or the file wrote it (`"0.50"` stays
5 //! `"0.50"`, so a save with nothing edited is byte-identical and the sim
6 //! cache keys that hash a simnet's JSON do not move), and the [`ParamSlot`]
7 //! that text parses to under the parameter's [`ParamKind`] — a number, a
8 //! vector, a switch, an option, or an expression still to be evaluated. Both
9 //! are private, and every setter re-parses, so the two cannot disagree: this
10 //! is phases 3 and 4 of the typed-value migration (CLAUDE.md, "Parameter
11 //! kinds").
12 //!
13 //! A text that does not fit its kind is kept verbatim as
14 //! [`ParamSlot::Invalid`] rather than dropped or coerced: readers then fall
15 //! back exactly as they did when every read parsed the string, the load says
16 //! how many values are affected, and the text survives for a person to fix.
17 //! What REFUSES a bad value is the entry points a person types into — the
18 //! params pane and MCP's `set_param` — via [`ParamDef::check`]; a load never
19 //! refuses anything.
20
21 use crate::app::FsNode;
22 use crate::expr::{fmt_num, Value};
23 use serde::{Deserialize, Serialize};
24
25 /// What a parameter HOLDS, parsed from its `type` string — the one place
26 /// that string is interpreted. The kind says how the text parses and which
27 /// control the params pane draws.
28 ///
29 /// The head before the first `:` names the kind; what follows is the
30 /// kind's own detail (`slider:-2:2` a range, `choice:UV,Icosphere,Cube`
31 /// the options), read by the pane and by [`ParamDef::choice_options`]. A
32 /// type naming no kind is a template bug: `load_fs_tree` drops the template
33 /// and says so, and `every_shipped_template_param_has_a_known_kind` walks
34 /// the shipped ones.
35 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
36 pub enum ParamKind {
37 /// Free text: a name, a path, a group or attribute name. `string` is
38 /// the same kind — it is what an ABSENT type deserializes to.
39 Text,
40 /// A number with no range — a threshold, a scale factor, a manual
41 /// ramp end — shown as a text row, because the pane's slider holds a
42 /// fraction of its range and would clamp anything outside it.
43 Float,
44 /// A number over a range (`min`/`max`, or inline `slider:lo:hi`).
45 Slider,
46 /// A whole number, stepped.
47 Spin,
48 /// Two numbers, `x:y` — a range's two ends (a Remap's From and To,
49 /// Visualize's Manual Range). Shown as two sliders over a SOFT range
50 /// that adapts to the value (`app::value_row_span`), since like a
51 /// `float` it holds numbers no fixed range would.
52 Float2,
53 /// Three numbers, `x:y:z`.
54 Float3,
55 /// One of a fixed set of options, stored as the option's text.
56 Choice,
57 /// `true` / `false`.
58 Toggle,
59 /// A press, not a value: the pane writes `clicked` and the app clears it.
60 Button,
61 /// A program (a wrangle's script). Never an expression.
62 Code,
63 /// The NAME of another node, resolved sibling-first by
64 /// `geometry::find_input_node` — an `Input` wire, a Boolean's `With`,
65 /// a Relax's `Rest`. Empty means unconnected.
66 Node,
67 /// The NAME of a point attribute on the node's input — one it reads
68 /// (Visualize's Attribute, Neighbour's Direction) or one it writes
69 /// (Normal's Attribute, Suture's Counter). Any text is a valid value;
70 /// what the kind changes is the pane, which offers the input's
71 /// attributes as a picker on every row of this kind
72 /// (`State::add_pick_lists`), where it used to know four rows by name.
73 Attribute,
74 /// The NAME of a point group on the node's input, read or written; the
75 /// pane offers the input's groups. Empty means every point, which is
76 /// what every Group row's default is.
77 Group,
78 }
79
80 impl ParamKind {
81 /// The type-string heads [`ParamKind::parse`] accepts, for messages.
82 /// `string` is left out: it is an alias, not something to ask for.
83 pub const NAMES: &'static [&'static str] =
84 &["text", "float", "slider", "spinbox", "float2", "float3", "choice", "toggle", "button", "code", "node", "attribute", "group"];
85
86 /// The kind's name — the type-string head that names it.
87 pub fn name(self) -> &'static str {
88 match self {
89 Self::Text => "text",
90 Self::Float => "float",
91 Self::Slider => "slider",
92 Self::Spin => "spinbox",
93 Self::Float2 => "float2",
94 Self::Float3 => "float3",
95 Self::Choice => "choice",
96 Self::Toggle => "toggle",
97 Self::Button => "button",
98 Self::Code => "code",
99 Self::Node => "node",
100 Self::Attribute => "attribute",
101 Self::Group => "group",
102 }
103 }
104
105 /// The kind a `type` string names, or `None` when it names none.
106 pub fn parse(ty: &str) -> Option<Self> {
107 let head = ty.split(':').next().unwrap_or("").trim();
108 Some(match head {
109 "text" | "string" => Self::Text,
110 "float" => Self::Float,
111 "slider" => Self::Slider,
112 "spinbox" => Self::Spin,
113 "float2" => Self::Float2,
114 "float3" => Self::Float3,
115 "choice" => Self::Choice,
116 "toggle" => Self::Toggle,
117 "button" => Self::Button,
118 "code" => Self::Code,
119 "node" => Self::Node,
120 "attribute" => Self::Attribute,
121 "group" => Self::Group,
122 _ => return None,
123 })
124 }
125 }
126
127 /// A parameter's value, parsed.
128 #[derive(Clone, Debug, PartialEq)]
129 pub enum ParamValue {
130 /// Float and Slider.
131 Number(f32),
132 /// Spin: a whole number.
133 Int(i64),
134 Vec2([f32; 2]),
135 Vec3([f32; 3]),
136 Bool(bool),
137 /// The option, spelled as the options list spells it (the text may
138 /// differ in case, and an empty text means the first option).
139 Choice(String),
140 /// Text, Node, Attribute, Group, Code and Button: the text is the value.
141 Text(String),
142 }
143
144 impl ParamValue {
145 /// The value as text. Numbers go through `expr::fmt_num` — the
146 /// formatting every evaluated expression has always been written back
147 /// with — so a typed write and the string write it replaced agree.
148 pub fn to_text(&self) -> String {
149 match self {
150 ParamValue::Number(n) => fmt_num(*n as f64),
151 ParamValue::Int(i) => i.to_string(),
152 ParamValue::Vec2(v) => v.iter().map(|c| fmt_num(*c as f64)).collect::<Vec<_>>().join(":"),
153 ParamValue::Vec3(v) => v.iter().map(|c| fmt_num(*c as f64)).collect::<Vec<_>>().join(":"),
154 ParamValue::Bool(b) => b.to_string(),
155 ParamValue::Choice(s) | ParamValue::Text(s) => s.clone(),
156 }
157 }
158 }
159
160 /// What a parameter's text parsed to.
161 #[derive(Clone, Debug, PartialEq)]
162 pub enum ParamSlot {
163 Value(ParamValue),
164 /// An expression (`ch("../sphere1/radius") * 2`), evaluated wherever the
165 /// node is — the text is the expression. See `expr.rs`.
166 Expr,
167 /// A text that does not fit the kind, and why. Kept, never coerced.
168 Invalid(String),
169 }
170
171 /// Whether `s` is a parameter NAME: lowercase ASCII letters, digits and
172 /// underscores, not empty — `base_resolution`, `input_2`. The name is what
173 /// a `ch()` path, a `show_when` condition, MCP and the code spell; the
174 /// params pane never shows it, showing the LABEL (`Base Resolution`)
175 /// instead. Node names follow the same convention, so a path reads as one
176 /// thing: `../sphere1/radius`.
177 pub fn is_param_name(s: &str) -> bool {
178 !s.is_empty() && s.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
179 }
180
181 /// The parameter name a label or an older name becomes: lowercased, every
182 /// run of anything but a letter or digit one underscore, none at either
183 /// end — `Base Resolution` → `base_resolution`, `Relax in 3D Space` →
184 /// `relax_in_3d_space`. What format 4's load step renames a save's
185 /// parameters by (`Project::migrate_format`), and what MCP's `set_param`
186 /// tries last for a name it does not find.
187 pub fn param_name_of(s: &str) -> String {
188 let mut out = String::with_capacity(s.len());
189 let mut gap = false;
190 for c in s.chars() {
191 if c.is_ascii_alphanumeric() {
192 if gap && !out.is_empty() {
193 out.push('_');
194 }
195 gap = false;
196 out.push(c.to_ascii_lowercase());
197 } else {
198 gap = true;
199 }
200 }
201 out
202 }
203
204 /// One node parameter: the template-owned UI metadata (label, type, range,
205 /// options, condition) and the instance-owned value.
206 #[derive(Clone)]
207 pub struct ParamDef {
208 pub name: String,
209 pub label: String,
210 param_type: String,
211 text: String,
212 slot: ParamSlot,
213 options: Vec<String>,
214 pub min: Option<f32>,
215 pub max: Option<f32>,
216 pub step: Option<f32>,
217 /// When this parameter should be SHOWN, as a condition over its siblings'
218 /// current values. Empty means always.
219 ///
220 /// Grammar, deliberately tiny: `Mode == Twist`, `Mode == Twist|Bend` for
221 /// any-of, `Mode != Bleed` for unless, and ` && ` between clauses. It
222 /// exists because collapsing fifty operators into ten traded node count
223 /// for parameter count — Attribute reached sixteen parameters, of which
224 /// four matter at any moment — and a pane showing twelve irrelevant rows
225 /// is worse than the twelve nodes it replaced.
226 ///
227 /// Houdini calls this `hideWhen`. Phrased the positive way round here
228 /// because a template author is describing when a control APPLIES, and
229 /// stating that directly is easier to get right than stating its negation.
230 pub show_when: String,
231 /// How a float3-valued row is SHOWN, where there is a choice:
232 /// `trackball` for the ball beside the three sliders, `sliders` for
233 /// the sliders alone, empty for the row's default
234 /// ([`Self::wants_trackball`]). The row menu's Show / Hide Trackball
235 /// writes it. It is the one piece of UI metadata the INSTANCE owns —
236 /// a preference about a control, set by the person using it — so the
237 /// template merge fills it only where the instance has not chosen.
238 /// Serialized only when set, so a file that never used it is
239 /// byte-identical to what it was.
240 pub view: String,
241 /// What the parameter does, in a sentence or two, shown in its row's
242 /// right-click menu. The TEMPLATE's: `adopt_ui_from` hands it to every
243 /// instance with the rest of the UI metadata, and it is read from a
244 /// template file and never written — a save carries values, and a
245 /// description kept there would go stale the day the template's
246 /// wording changed (and move `sim_solve_key` the day it did).
247 pub description: String,
248 /// Which run of related parameters this one belongs to, by name — the
249 /// params pane draws a separator wherever two rows it shows belong to
250 /// different groups (`param_display`). The TEMPLATE's, read and never
251 /// written, as the description is. Empty is the node's ungrouped run;
252 /// the leading wires are a group of their own without saying so.
253 pub group: String,
254 }
255
256 /// The file shape of a [`ParamDef`] — the struct as it was before the value
257 /// was typed, field for field and in the same order, so the JSON a save
258 /// writes (and `sim_solve_key` hashes) is unchanged. `expr` is serialized
259 /// only when set, as it always was.
260 #[derive(Clone, Deserialize, Serialize)]
261 struct ParamDefRepr {
262 name: String,
263 #[serde(default)]
264 label: String,
265 #[serde(rename = "type")]
266 #[serde(default = "default_param_type")]
267 param_type: String,
268 #[serde(default)]
269 default: String,
270 #[serde(default)]
271 options: Vec<String>,
272 #[serde(default)]
273 min: Option<f32>,
274 #[serde(default)]
275 max: Option<f32>,
276 #[serde(default)]
277 step: Option<f32>,
278 #[serde(default)]
279 show_when: String,
280 /// Whether `default` is an EXPRESSION to evaluate rather than a value.
281 /// A flag and not a guess about the text, because a script contains
282 /// `chf(`, a node name is an identifier and `0.5` parses as an
283 /// expression too; Houdini makes the same choice.
284 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
285 expr: bool,
286 /// [`ParamDef::view`]; absent unless one was chosen.
287 #[serde(default, skip_serializing_if = "String::is_empty")]
288 view: String,
289 /// [`ParamDef::description`]; read, never written.
290 #[serde(default, skip_serializing)]
291 description: String,
292 /// [`ParamDef::group`]; read, never written.
293 #[serde(default, skip_serializing)]
294 group: String,
295 }
296
297 fn default_param_type() -> String {
298 "string".to_string()
299 }
300
301 impl<'de> Deserialize<'de> for ParamDef {
302 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
303 let r = ParamDefRepr::deserialize(d)?;
304 let mut p = ParamDef {
305 name: r.name,
306 label: r.label,
307 param_type: r.param_type,
308 text: r.default,
309 slot: ParamSlot::Expr,
310 options: r.options,
311 min: r.min,
312 max: r.max,
313 step: r.step,
314 show_when: r.show_when,
315 view: r.view,
316 description: r.description,
317 group: r.group,
318 };
319 if !r.expr {
320 p.reparse();
321 }
322 Ok(p)
323 }
324 }
325
326 impl Serialize for ParamDef {
327 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
328 ParamDefRepr {
329 name: self.name.clone(),
330 label: self.label.clone(),
331 param_type: self.param_type.clone(),
332 default: self.text.clone(),
333 options: self.options.clone(),
334 min: self.min,
335 max: self.max,
336 step: self.step,
337 show_when: self.show_when.clone(),
338 expr: self.is_expr(),
339 view: self.view.clone(),
340 description: String::new(),
341 group: String::new(),
342 }
343 .serialize(s)
344 }
345 }
346
347 /// `text` parsed under `kind`, with `options` the choices a Choice takes.
348 pub fn parse_value(kind: ParamKind, text: &str, options: &[String]) -> Result<ParamValue, String> {
349 let t = text.trim();
350 let number = |t: &str| t.parse::<f32>().ok().filter(|n| n.is_finite());
351 match kind {
352 ParamKind::Text | ParamKind::Node | ParamKind::Attribute | ParamKind::Group | ParamKind::Code | ParamKind::Button => {
353 Ok(ParamValue::Text(text.to_string()))
354 }
355 ParamKind::Float | ParamKind::Slider => {
356 number(t).map(ParamValue::Number).ok_or_else(|| format!("'{text}' is not a number"))
357 }
358 ParamKind::Spin => match t.parse::<f64>() {
359 Ok(n) if n.is_finite() && n.fract() == 0.0 => Ok(ParamValue::Int(n as i64)),
360 Ok(_) => Err(format!("'{text}' is not a whole number")),
361 Err(_) => Err(format!("'{text}' is not a number")),
362 },
363 ParamKind::Float2 => {
364 let parts: Vec<&str> = t.split(':').collect();
365 match parts[..] {
366 [x, y] => match (number(x.trim()), number(y.trim())) {
367 (Some(x), Some(y)) => Ok(ParamValue::Vec2([x, y])),
368 _ => Err(format!("'{text}' is not two numbers")),
369 },
370 _ => Err(format!("'{text}' is not x:y")),
371 }
372 }
373 ParamKind::Float3 => {
374 let parts: Vec<&str> = t.split(':').collect();
375 match parts[..] {
376 [x, y, z] => match (number(x.trim()), number(y.trim()), number(z.trim())) {
377 (Some(x), Some(y), Some(z)) => Ok(ParamValue::Vec3([x, y, z])),
378 _ => Err(format!("'{text}' is not three numbers")),
379 },
380 _ => Err(format!("'{text}' is not x:y:z")),
381 }
382 }
383 ParamKind::Toggle => match t.to_ascii_lowercase().as_str() {
384 "true" | "1" | "on" => Ok(ParamValue::Bool(true)),
385 "false" | "0" | "off" => Ok(ParamValue::Bool(false)),
386 _ => Err(format!("'{text}' is not true or false")),
387 },
388 ParamKind::Choice => {
389 if options.is_empty() {
390 return Ok(ParamValue::Choice(text.to_string()));
391 }
392 if t.is_empty() {
393 return Ok(ParamValue::Choice(options[0].clone()));
394 }
395 options
396 .iter()
397 .find(|o| o.eq_ignore_ascii_case(t))
398 .map(|o| ParamValue::Choice(o.clone()))
399 .ok_or_else(|| format!("'{text}' is not one of {}", options.join(", ")))
400 }
401 }
402 }
403
404 impl ParamDef {
405 /// A parameter of type `ty` holding `text`, parsed. The rest of the
406 /// metadata starts empty; the `with_*` builders fill it in.
407 pub fn new(name: impl Into<String>, ty: impl Into<String>, text: impl Into<String>) -> Self {
408 let mut p = ParamDef {
409 name: name.into(),
410 label: String::new(),
411 param_type: ty.into(),
412 text: text.into(),
413 slot: ParamSlot::Expr,
414 options: Vec::new(),
415 min: None,
416 max: None,
417 step: None,
418 show_when: String::new(),
419 view: String::new(),
420 description: String::new(),
421 group: String::new(),
422 };
423 p.reparse();
424 p
425 }
426
427 /// Whether a float3-valued row of this parameter shows the trackball:
428 /// the instance's choice when it made one, `default` otherwise.
429 pub fn wants_trackball(&self, default: bool) -> bool {
430 match self.view.as_str() {
431 "trackball" => true,
432 "sliders" => false,
433 _ => default,
434 }
435 }
436
437 /// What the params pane shows for the row, and keys it by: the label,
438 /// or the name where there is none (a parameter added by hand or over
439 /// MCP).
440 pub fn shown_name(&self) -> &str {
441 if self.label.is_empty() {
442 &self.name
443 } else {
444 &self.label
445 }
446 }
447
448 pub fn with_label(mut self, label: impl Into<String>) -> Self {
449 self.label = label.into();
450 self
451 }
452
453 pub fn with_range(mut self, min: Option<f32>, max: Option<f32>) -> Self {
454 self.min = min;
455 self.max = max;
456 self
457 }
458
459 pub fn with_step(mut self, step: Option<f32>) -> Self {
460 self.step = step;
461 self
462 }
463
464 pub fn with_options(mut self, options: Vec<String>) -> Self {
465 self.options = options;
466 self.reparse();
467 self
468 }
469
470 pub fn with_show_when(mut self, cond: impl Into<String>) -> Self {
471 self.show_when = cond.into();
472 self
473 }
474
475 /// This parameter holding its text as an expression.
476 pub fn as_expr(mut self) -> Self {
477 self.set_expr(true);
478 self
479 }
480
481 /// The value as written: the expression for an expression, the verbatim
482 /// text otherwise.
483 pub fn text(&self) -> &str {
484 &self.text
485 }
486
487 /// The `type` string, detail and all (`slider:-2:2`).
488 pub fn ty(&self) -> &str {
489 &self.param_type
490 }
491
492 /// The options list the template gave (a Choice may carry its options
493 /// in the type string instead — see [`ParamDef::choice_options`]).
494 pub fn options(&self) -> &[String] {
495 &self.options
496 }
497
498 pub fn slot(&self) -> &ParamSlot {
499 &self.slot
500 }
501
502 /// The parsed value, when the text is a value that fits the kind.
503 pub fn value(&self) -> Option<&ParamValue> {
504 match &self.slot {
505 ParamSlot::Value(v) => Some(v),
506 _ => None,
507 }
508 }
509
510 pub fn is_expr(&self) -> bool {
511 self.slot == ParamSlot::Expr
512 }
513
514 /// Why the text does not fit the kind, when it does not.
515 pub fn invalid(&self) -> Option<&str> {
516 match &self.slot {
517 ParamSlot::Invalid(why) => Some(why),
518 _ => None,
519 }
520 }
521
522 /// This parameter's kind. A type that names none reads as text — the
523 /// row stays editable and its value survives — but a shipped template
524 /// cannot carry one (see [`ParamKind`]).
525 pub fn kind(&self) -> ParamKind {
526 ParamKind::parse(&self.param_type).unwrap_or(ParamKind::Text)
527 }
528
529 /// A Choice's options: the list when there is one, else the ones the
530 /// type string names (`choice:UV,Icosphere,Cube`).
531 pub fn choice_options(&self) -> Vec<String> {
532 if !self.options.is_empty() {
533 self.options.clone()
534 } else {
535 self.param_type
536 .strip_prefix("choice:")
537 .map(|o| o.split(',').map(|x| x.trim().to_string()).collect())
538 .unwrap_or_default()
539 }
540 }
541
542 /// The range the pane holds this parameter to, as `(min, max, step)`,
543 /// for the kinds that have one — Slider, Float3 and Spin (a Float2's
544 /// adapts to its value, and is the pane's to choose). An inline
545 /// detail (`slider:-2:2`) wins, then the template's `min` / `max`, then
546 /// the pane's own defaults (0..2, -10..10, 1..10000), which are what a
547 /// row with none declared clamps to. `param_display` builds the pane's
548 /// row from this and the row menu's `Range:` reads it, so the two
549 /// cannot disagree. `None` for a kind with no range.
550 pub fn range(&self) -> Option<(f32, f32, Option<f32>)> {
551 let (lo, hi) = match self.kind() {
552 ParamKind::Slider => (0.0, 2.0),
553 ParamKind::Float3 => (-10.0, 10.0),
554 ParamKind::Spin => (1.0, 10000.0),
555 _ => return None,
556 };
557 let (min, max, step) = self.declared_range();
558 let step = match self.kind() {
559 ParamKind::Spin => Some(step.unwrap_or(1.0)),
560 _ => step,
561 };
562 Some((min.unwrap_or(lo), max.unwrap_or(hi), step))
563 }
564
565 /// The `(min, max, step)` the template DECLARES for this parameter —
566 /// an inline `slider:-2:2` counts as declaring both ends — each `None`
567 /// where it says nothing and the pane's default stands in
568 /// ([`ParamDef::range`] is the result). What the row menu's `Min:` /
569 /// `Max:` / `Step:` read, so a `none` there means the template left it
570 /// to the pane.
571 pub fn declared_range(&self) -> (Option<f32>, Option<f32>, Option<f32>) {
572 let parts: Vec<&str> = self.param_type.split(':').collect();
573 let inline = match parts[..] {
574 [_, lo, hi] => lo.trim().parse::<f32>().ok().zip(hi.trim().parse::<f32>().ok()),
575 _ => None,
576 };
577 match inline {
578 Some((lo, hi)) => (Some(lo), Some(hi), self.step),
579 None => (self.min, self.max, self.step),
580 }
581 }
582
583 /// Whether a value that READS as a reference should become an
584 /// expression here. Not for a code parameter: a kernel or a wrangle
585 /// script is a program, and one whose whole text happens to be
586 /// `ch("../a/radius")` is a one-line program, not a channel — flagging
587 /// it would evaluate the script to a number before it ever ran.
588 pub fn takes_expressions(&self) -> bool {
589 !(self.kind() == ParamKind::Code || self.name == "code")
590 }
591
592 /// Whether `text` would be a valid VALUE here — what an entry point a
593 /// person types into asks before it writes. Nothing is stored.
594 pub fn check(&self, text: &str) -> Result<(), String> {
595 parse_value(self.kind(), text, &self.choice_options()).map(|_| ())
596 }
597
598 /// Replace the text, keeping whether it is an expression, and re-parse.
599 /// The one way a value changes: a text that does not fit is kept as
600 /// [`ParamSlot::Invalid`], so a writer never loses what it wrote — the
601 /// entry points that should refuse one ask [`ParamDef::check`] first.
602 pub fn set_text(&mut self, text: impl Into<String>) {
603 self.text = text.into();
604 if !self.is_expr() {
605 self.reparse();
606 }
607 }
608
609 /// Store a value, as text formatted by [`ParamValue::to_text`]. Clears
610 /// the expression flag: a value is not an expression.
611 pub fn set_value(&mut self, v: ParamValue) {
612 self.text = v.to_text();
613 self.reparse();
614 }
615
616 /// A value written as text, clearing the expression flag — what an
617 /// evaluated expression or Delete Expression leaves behind.
618 pub fn bake(&mut self, text: impl Into<String>) {
619 self.text = text.into();
620 self.reparse();
621 }
622
623 /// Mark the text as an expression, or as a plain value again.
624 pub fn set_expr(&mut self, on: bool) {
625 if on {
626 self.slot = ParamSlot::Expr;
627 } else if self.is_expr() {
628 self.reparse();
629 }
630 }
631
632 /// Change the type, re-parsing the value under the new kind.
633 pub fn set_type(&mut self, ty: impl Into<String>) {
634 self.param_type = ty.into();
635 if !self.is_expr() {
636 self.reparse();
637 }
638 }
639
640 /// Take the template's UI metadata — type, label, options, range, step,
641 /// condition — keeping this instance's value, which is re-parsed under
642 /// the (possibly new) kind. The template owns the surface, the instance
643 /// owns its value.
644 pub fn adopt_ui_from(&mut self, template: &ParamDef) {
645 self.param_type = template.param_type.clone();
646 self.label = template.label.clone();
647 self.options = template.options.clone();
648 self.min = template.min;
649 self.max = template.max;
650 self.step = template.step;
651 self.show_when = template.show_when.clone();
652 self.description = template.description.clone();
653 self.group = template.group.clone();
654 // The view is the instance's to choose; the template's is what it
655 // starts from.
656 if self.view.is_empty() {
657 self.view = template.view.clone();
658 }
659 if !self.is_expr() {
660 self.reparse();
661 }
662 }
663
664 fn reparse(&mut self) {
665 self.slot = match parse_value(self.kind(), &self.text, &self.choice_options()) {
666 Ok(v) => ParamSlot::Value(v),
667 Err(why) => ParamSlot::Invalid(why),
668 };
669 }
670
671 /// An evaluated expression's result as the value this parameter holds —
672 /// the row decides: a number into a toggle is its truth, into a choice
673 /// the option at that index, into a spinbox the whole part; a string is
674 /// parsed as the row's text would be. `None` when it fits nothing (a
675 /// string into a slider that is not a number), which leaves the text
676 /// the old write-back produced to be stored and flagged.
677 pub fn value_from_expr(&self, v: &Value) -> Option<ParamValue> {
678 let kind = self.kind();
679 match (kind, v) {
680 (ParamKind::Toggle, _) => Some(ParamValue::Bool(v.truthy())),
681 (ParamKind::Choice, Value::Num(n)) => {
682 let options = self.choice_options();
683 if options.is_empty() {
684 Some(ParamValue::Choice(fmt_num(*n)))
685 } else {
686 let i = (n.round().max(0.0) as usize).min(options.len() - 1);
687 Some(ParamValue::Choice(options[i].clone()))
688 }
689 }
690 (ParamKind::Spin, Value::Num(n)) if n.is_finite() => Some(ParamValue::Int(n.trunc() as i64)),
691 (ParamKind::Float | ParamKind::Slider, Value::Num(n)) => Some(ParamValue::Number(*n as f32)),
692 _ => parse_value(kind, &v.as_str(), &self.choice_options()).ok(),
693 }
694 }
695 }
696
697 /// Every parameter in `node`'s tree whose type names no [`ParamKind`], as
698 /// `(path, parameter, type)`. Empty for a well-formed template.
699 pub fn unknown_param_kinds(node: &FsNode) -> Vec<(String, String, String)> {
700 fn walk(node: &FsNode, path: &str, out: &mut Vec<(String, String, String)>) {
701 for p in &node.params {
702 if ParamKind::parse(&p.param_type).is_none() {
703 out.push((path.to_string(), p.name.clone(), p.param_type.clone()));
704 }
705 }
706 for c in &node.children {
707 walk(c, &format!("{path}/{}", c.name), out);
708 }
709 }
710 let mut out = Vec::new();
711 walk(node, &node.name, &mut out);
712 out
713 }
714
715 /// Every parameter in `node`'s tree whose name is not a parameter name
716 /// ([`is_param_name`]), as `(path, name)` — what refuses a template.
717 pub fn misnamed_params(node: &FsNode) -> Vec<(String, String)> {
718 fn walk(node: &FsNode, path: &str, out: &mut Vec<(String, String)>) {
719 for p in &node.params {
720 if !is_param_name(&p.name) {
721 out.push((path.to_string(), p.name.clone()));
722 }
723 }
724 for c in &node.children {
725 walk(c, &format!("{path}/{}", c.name), out);
726 }
727 }
728 let mut out = Vec::new();
729 walk(node, &node.name, &mut out);
730 out
731 }
732
733 /// Every parameter in `node`'s tree whose text does not fit its kind, as
734 /// `(path, parameter, why)` — what a load reports on the status line.
735 pub fn invalid_params(node: &FsNode) -> Vec<(String, String, String)> {
736 fn walk(node: &FsNode, path: &str, out: &mut Vec<(String, String, String)>) {
737 for p in &node.params {
738 if let Some(why) = p.invalid() {
739 // The label: this is read off the status line, by a
740 // person, beside the pane that shows it.
741 out.push((path.to_string(), p.shown_name().to_string(), why.to_string()));
742 }
743 }
744 for c in &node.children {
745 walk(c, &format!("{path}/{}", c.name), out);
746 }
747 }
748 let mut out = Vec::new();
749 for c in &node.children {
750 walk(c, &c.name, &mut out);
751 }
752 out
753 }