git.lucas.co / cce-designer
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 }