Skip to main content

ezu_graph/
input.rs

1//! `In<T>` — a scalar node field that accepts an inline literal, a
2//! `$param` reference (resolved against [`EvalCtx::params`] at eval
3//! time), or a `@node` connection on a [`PortKind::Scalar`] input port.
4//!
5//! Factories read fields through an [`InReader`], which classifies each
6//! value and accumulates the scalar [`PortSpec`]s / [`Connection`]s /
7//! param names the node must expose:
8//!
9//! ```ignore
10//! let mut r = InReader::new(fields, ctx, FIXED_PORTS.len());
11//! let sigma = r.number(\"sigma\")?;          // In<f64>
12//! let color = r.color_or(\"color\", WHITE)?; // In<[f32; 4]>
13//! let parts = r.finish();
14//! // node stores `sigma`, `color`, `parts.ports` (appended after the
15//! // fixed ports), returns `parts.param_refs` from `Node::param_refs`,
16//! // and the factory returns `parts.connections` alongside any fixed
17//! // ones.
18//! ```
19//!
20//! Cache correctness: `In::param_hash` feeds the *static* identity
21//! (literal value / param name / port index) into `Node::param_hash`;
22//! the runtime value of a `$param` is folded into the cache key by the
23//! evaluator via [`Node::param_refs`](crate::node::Node::param_refs),
24//! and a port-fed value arrives through the upstream node's hash.
25
26use ezu_style as spec;
27use serde_json::Value;
28use xxhash_rust::xxh3::Xxh3;
29
30use crate::eval::{EvalCtx, EvalError};
31use crate::port::{PortKind, PortSpec};
32use crate::registry::{Connection, FactoryCtx, FactoryError};
33use crate::value::{PortValue, ScalarValue};
34
35/// Accepts-list for a scalar input port.
36pub const ACCEPTS_SCALAR: &[PortKind] = &[PortKind::Scalar];
37
38/// A scalar field type readable through [`In<T>`] / [`InReader`].
39pub trait ScalarType: Copy {
40    /// Human-readable type name for error messages.
41    const NAME: &'static str;
42    /// Whether a param declaration of `kind` supplies this type.
43    fn matches_kind(kind: spec::ParamKind) -> bool;
44    /// Extract from a runtime scalar value.
45    fn from_scalar(v: ScalarValue) -> Option<Self>;
46    /// Parse from a JSON literal (field value or param default).
47    fn from_json(v: &Value) -> Option<Self>;
48    /// Feed into a cache-key hasher.
49    fn hash_into(&self, h: &mut Xxh3);
50    /// Clamp to a declaration's `min` / `max`. Identity for non-numbers.
51    fn clamp_decl(self, _min: Option<f64>, _max: Option<f64>) -> Self {
52        self
53    }
54}
55
56impl ScalarType for f64 {
57    const NAME: &'static str = "number";
58    fn matches_kind(kind: spec::ParamKind) -> bool {
59        kind == spec::ParamKind::Number
60    }
61    fn from_scalar(v: ScalarValue) -> Option<Self> {
62        v.as_number()
63    }
64    fn from_json(v: &Value) -> Option<Self> {
65        v.as_f64()
66    }
67    fn hash_into(&self, h: &mut Xxh3) {
68        h.update(&self.to_le_bytes());
69    }
70    fn clamp_decl(self, min: Option<f64>, max: Option<f64>) -> Self {
71        let mut v = self;
72        if let Some(m) = min {
73            v = v.max(m);
74        }
75        if let Some(m) = max {
76            v = v.min(m);
77        }
78        v
79    }
80}
81
82/// Straight (non-premultiplied) sRGB-encoded RGBA in `[0, 1]` — the
83/// same convention as a parsed `#rrggbb[aa]` literal.
84impl ScalarType for [f32; 4] {
85    const NAME: &'static str = "color";
86    fn matches_kind(kind: spec::ParamKind) -> bool {
87        kind == spec::ParamKind::Color
88    }
89    fn from_scalar(v: ScalarValue) -> Option<Self> {
90        v.as_color()
91    }
92    fn from_json(v: &Value) -> Option<Self> {
93        spec::parse_hex_color(v.as_str()?)
94    }
95    fn hash_into(&self, h: &mut Xxh3) {
96        for c in self {
97            h.update(&c.to_le_bytes());
98        }
99    }
100}
101
102impl ScalarType for bool {
103    const NAME: &'static str = "bool";
104    fn matches_kind(kind: spec::ParamKind) -> bool {
105        kind == spec::ParamKind::Bool
106    }
107    fn from_scalar(v: ScalarValue) -> Option<Self> {
108        v.as_bool()
109    }
110    fn from_json(v: &Value) -> Option<Self> {
111        v.as_bool()
112    }
113    fn hash_into(&self, h: &mut Xxh3) {
114        h.update(&[*self as u8]);
115    }
116}
117
118/// A node field whose value is an inline literal, a `$param` reference,
119/// or a scalar input port. Resolve with [`In::get`] inside `Node::eval`.
120#[derive(Debug, Clone)]
121pub enum In<T> {
122    /// Inline literal, baked at build time.
123    Const(T),
124    /// `$param` reference: read `EvalCtx::params` at eval time, fall
125    /// back to the declaration default, clamp numbers to the
126    /// declaration's `min` / `max`.
127    Param {
128        name: String,
129        fallback: T,
130        min: Option<f64>,
131        max: Option<f64>,
132    },
133    /// `@node` connection on a scalar input port.
134    Port {
135        /// Positional index into `Node::inputs()`.
136        ix: usize,
137        /// Field name, for error messages.
138        name: &'static str,
139    },
140}
141
142impl<T: ScalarType> In<T> {
143    /// Resolve the field's value for one eval.
144    pub fn get(&self, ctx: &EvalCtx<'_>, inputs: &[Option<PortValue>]) -> Result<T, EvalError> {
145        match self {
146            In::Const(v) => Ok(*v),
147            In::Param {
148                name,
149                fallback,
150                min,
151                max,
152            } => {
153                let v = match ctx.params.get(name) {
154                    None => *fallback,
155                    Some(sv) => T::from_scalar(sv).ok_or_else(|| {
156                        EvalError::Other(format!(
157                            "param `${name}`: expected {}, got {}",
158                            T::NAME,
159                            sv.kind_name()
160                        ))
161                    })?,
162                };
163                Ok(v.clamp_decl(*min, *max))
164            }
165            In::Port { ix, name } => {
166                let v = inputs
167                    .get(*ix)
168                    .and_then(|o| o.as_ref())
169                    .ok_or_else(|| EvalError::MissingInput((*name).into()))?;
170                let PortValue::Scalar(sv) = v else {
171                    return Err(EvalError::Other(format!(
172                        "port `{name}`: expected a scalar, got {}",
173                        v.kind()
174                    )));
175                };
176                T::from_scalar(*sv).ok_or_else(|| {
177                    EvalError::Other(format!(
178                        "port `{name}`: expected {}, got {}",
179                        T::NAME,
180                        sv.kind_name()
181                    ))
182                })
183            }
184        }
185    }
186
187    /// Feed the field's *static* identity into `Node::param_hash`.
188    /// Runtime `$param` values are keyed via `Node::param_refs`; a
189    /// port-fed value is keyed via the upstream node's hash.
190    pub fn param_hash(&self, h: &mut Xxh3) {
191        match self {
192            In::Const(v) => {
193                h.update(b"c");
194                v.hash_into(h);
195            }
196            In::Param { name, fallback, .. } => {
197                h.update(b"p");
198                h.update(name.as_bytes());
199                fallback.hash_into(h);
200            }
201            In::Port { ix, .. } => {
202                h.update(b"@");
203                h.update(&(*ix as u64).to_le_bytes());
204            }
205        }
206    }
207
208    /// Static upper bound usable for build-time decisions (pad
209    /// propagation): the literal value, or a `$param`'s declared `max`.
210    /// `None` for ports and unbounded params — pad-affecting fields
211    /// must reject those at build time.
212    pub fn static_bound(&self) -> Option<f64>
213    where
214        T: Into<f64>,
215    {
216        match self {
217            In::Const(v) => Some((*v).into()),
218            In::Param { max, .. } => *max,
219            In::Port { .. } => None,
220        }
221    }
222}
223
224/// Parse a caller-supplied parameter assignment (CLI `--param k=v`,
225/// server query string) against the document's declarations. Unknown
226/// names, type mismatches, and out-of-range numbers are errors — hosts
227/// report them up front instead of silently clamping at eval time.
228pub fn parse_param_value(
229    decls: &indexmap::IndexMap<String, spec::ParamDecl>,
230    name: &str,
231    raw: &str,
232) -> Result<ScalarValue, String> {
233    let decl = decls
234        .get(name)
235        .ok_or_else(|| format!("unknown param `{name}`"))?;
236    match decl.kind {
237        spec::ParamKind::Number => {
238            let v: f64 = raw
239                .parse()
240                .map_err(|_| format!("param `{name}`: `{raw}` is not a number"))?;
241            if let Some(m) = decl.min {
242                if v < m {
243                    return Err(format!("param `{name}`: {v} is below min {m}"));
244                }
245            }
246            if let Some(m) = decl.max {
247                if v > m {
248                    return Err(format!("param `{name}`: {v} is above max {m}"));
249                }
250            }
251            Ok(ScalarValue::Number(v))
252        }
253        spec::ParamKind::Bool => match raw {
254            "true" | "1" => Ok(ScalarValue::Bool(true)),
255            "false" | "0" => Ok(ScalarValue::Bool(false)),
256            _ => Err(format!("param `{name}`: expected true/false, got `{raw}`")),
257        },
258        spec::ParamKind::Color => spec::parse_hex_color(raw)
259            .map(ScalarValue::Color)
260            .ok_or_else(|| format!("param `{name}`: `{raw}` is not a `#rrggbb[aa]` color")),
261    }
262}
263
264/// What an [`InReader`] accumulated: scalar input ports (to append
265/// after the node's fixed ports), their connections, and the names of
266/// `$param` references (returned from `Node::param_refs`).
267#[derive(Debug, Default)]
268pub struct InParts {
269    pub ports: Vec<PortSpec>,
270    pub connections: Vec<Connection>,
271    pub param_refs: Vec<String>,
272}
273
274/// Field reader used by node factories: classifies each field as
275/// literal / `$param` / `@node` and accumulates the node's scalar
276/// port plumbing. See the module docs for the usage pattern.
277pub struct InReader<'a, 'c> {
278    fields: &'a serde_json::Map<String, Value>,
279    ctx: &'a FactoryCtx<'c>,
280    parts: InParts,
281    next_port: usize,
282}
283
284impl<'a, 'c> InReader<'a, 'c> {
285    /// `fixed_ports` is the number of input ports the node declares
286    /// before any scalar ports (e.g. 1 for `blur`'s `input`). Scalar
287    /// port indices start there.
288    pub fn new(
289        fields: &'a serde_json::Map<String, Value>,
290        ctx: &'a FactoryCtx<'c>,
291        fixed_ports: usize,
292    ) -> Self {
293        Self {
294            fields,
295            ctx,
296            parts: InParts::default(),
297            next_port: fixed_ports,
298        }
299    }
300
301    /// Required number field.
302    pub fn number(&mut self, name: &'static str) -> Result<In<f64>, FactoryError> {
303        self.read(name, None)
304    }
305
306    /// Optional number field with a default.
307    pub fn number_or(&mut self, name: &'static str, default: f64) -> Result<In<f64>, FactoryError> {
308        self.read(name, Some(default))
309    }
310
311    /// Required color field (`#rrggbb[aa]`, `$param`, or `@node`).
312    pub fn color(&mut self, name: &'static str) -> Result<In<[f32; 4]>, FactoryError> {
313        self.read(name, None)
314    }
315
316    /// Optional color field with a default.
317    pub fn color_or(
318        &mut self,
319        name: &'static str,
320        default: [f32; 4],
321    ) -> Result<In<[f32; 4]>, FactoryError> {
322        self.read(name, Some(default))
323    }
324
325    /// Optional color field; `None` when absent.
326    pub fn color_opt(&mut self, name: &'static str) -> Result<Option<In<[f32; 4]>>, FactoryError> {
327        if !self.fields.contains_key(name) {
328            return Ok(None);
329        }
330        Ok(Some(self.read(name, None)?))
331    }
332
333    /// Optional bool field with a default.
334    pub fn bool_or(&mut self, name: &'static str, default: bool) -> Result<In<bool>, FactoryError> {
335        self.read(name, Some(default))
336    }
337
338    /// Read a scalar that lives *inside* a composite field — one stop of
339    /// a colour ramp, one entry of a palette, one number of a dash
340    /// pattern. `label` names the position for error messages
341    /// (`"stops[2].color"`).
342    ///
343    /// Accepts a literal or a `$param`, but not a `@node`: a port needs
344    /// a `&'static str` name and its own slot in `Node::inputs()`, and a
345    /// table of unknown length can offer neither. Resolve the whole
346    /// table once per eval, never per pixel.
347    pub fn nested<T: ScalarType>(&mut self, label: &str, v: &Value) -> Result<In<T>, FactoryError> {
348        if let Some(s) = v.as_str() {
349            match spec::FieldRef::classify(s) {
350                spec::FieldRef::Node(_) => {
351                    return Err(FactoryError::BadField {
352                        field: label.to_string(),
353                        msg: format!(
354                            "expected {} literal or `$param`, got a `@node` ref — a scalar \
355                             port cannot be wired into one entry of a table",
356                            T::NAME
357                        ),
358                    })
359                }
360                spec::FieldRef::Param(p) => return self.param_in(label, p),
361                spec::FieldRef::Literal(_) => {} // fall through
362            }
363        }
364        T::from_json(v)
365            .map(In::Const)
366            .ok_or_else(|| FactoryError::BadField {
367                field: label.to_string(),
368                msg: format!("expected {} literal or `$param`", T::NAME),
369            })
370    }
371
372    /// Build a `$param`-backed `In` and record the reference, checking
373    /// the declaration's type and default. Shared by whole-field and
374    /// nested reads; `label` is only for error messages.
375    fn param_in<T: ScalarType>(&mut self, label: &str, p: &str) -> Result<In<T>, FactoryError> {
376        let decl = self
377            .ctx
378            .params
379            .get(p)
380            .ok_or_else(|| FactoryError::UnknownParam(p.to_string()))?;
381        if !T::matches_kind(decl.kind) {
382            return Err(FactoryError::BadField {
383                field: label.to_string(),
384                msg: format!(
385                    "param `${p}` is declared `{:?}`, but this field needs a {}",
386                    decl.kind,
387                    T::NAME
388                ),
389            });
390        }
391        let fallback = T::from_json(&decl.default).ok_or_else(|| FactoryError::BadField {
392            field: label.to_string(),
393            msg: format!("param `${p}` default is not a valid {}", T::NAME),
394        })?;
395        self.parts.param_refs.push(p.to_string());
396        Ok(In::Param {
397            name: p.to_string(),
398            fallback,
399            min: decl.min,
400            max: decl.max,
401        })
402    }
403
404    fn read<T: ScalarType>(
405        &mut self,
406        name: &'static str,
407        default: Option<T>,
408    ) -> Result<In<T>, FactoryError> {
409        let Some(v) = self.fields.get(name) else {
410            return default
411                .map(In::Const)
412                .ok_or_else(|| FactoryError::MissingField(name.to_string()));
413        };
414        if let Some(s) = v.as_str() {
415            match spec::FieldRef::classify(s) {
416                spec::FieldRef::Node(id) => {
417                    let ix = self.next_port;
418                    self.next_port += 1;
419                    self.parts.ports.push(PortSpec {
420                        name,
421                        accepts: ACCEPTS_SCALAR,
422                        optional: false,
423                    });
424                    self.parts.connections.push(Connection {
425                        port: name.to_string(),
426                        src: id.to_string(),
427                    });
428                    return Ok(In::Port { ix, name });
429                }
430                spec::FieldRef::Param(p) => return self.param_in(name, p),
431                spec::FieldRef::Literal(_) => {} // fall through
432            }
433        }
434        T::from_json(v)
435            .map(In::Const)
436            .ok_or_else(|| FactoryError::BadField {
437                field: name.into(),
438                // A JSON array here is almost always a MapLibre expression
439                // aimed at the wrong field: paint properties that accept one
440                // keep it on a `<name>-expr` sibling, since an expression is
441                // evaluated per feature rather than wired as a port.
442                msg: if v.is_array() {
443                    format!(
444                        "expected {} literal, `$param`, or `@node`, got a JSON array — \
445                         if that is a MapLibre expression, it belongs on `{name}-expr`",
446                        T::NAME
447                    )
448                } else {
449                    format!("expected {} literal, `$param`, or `@node`", T::NAME)
450                },
451            })
452    }
453
454    /// Hand back the accumulated ports / connections / param refs.
455    pub fn finish(self) -> InParts {
456        self.parts
457    }
458}
459
460/// A numeric field whose value decides how much canvas padding the graph
461/// needs — a blur's sigma, a morphology radius, a warp amplitude.
462///
463/// Padding is fixed before evaluation starts, so the build has to know an
464/// upper bound for these. A literal or a `$param` with a declared `max`
465/// carries one ([`In::static_bound`]); an `@node` port does not, and used
466/// to be rejected outright — which left no way to drive a blur from a
467/// `math` chain, since a computed value cannot be a param.
468///
469/// So the bound becomes something the style can state on its own, as a
470/// literal `<field>-max` sibling. Padding is computed from that, and the
471/// value the port produces is clamped to it at render time: the canvas
472/// cannot grow mid-render, so a larger value would read past the margin
473/// and clamp the tile's edge pixels instead. Clamping is reported once
474/// per node — quietly weakening a filter the style asked for is worth a
475/// line in the log, but not one per tile.
476pub struct PaddingIn {
477    value: In<f64>,
478    bound: f64,
479    field: &'static str,
480    clamped: std::sync::atomic::AtomicBool,
481}
482
483impl PaddingIn {
484    /// Read a required padding-determining field.
485    pub fn read(
486        r: &mut InReader<'_, '_>,
487        fields: &serde_json::Map<String, Value>,
488        field: &'static str,
489    ) -> Result<Self, FactoryError> {
490        let value = r.number(field)?;
491        Self::from_value(value, fields, field)
492    }
493
494    /// Read an optional padding-determining field with a default.
495    pub fn read_or(
496        r: &mut InReader<'_, '_>,
497        fields: &serde_json::Map<String, Value>,
498        field: &'static str,
499        default: f64,
500    ) -> Result<Self, FactoryError> {
501        let value = r.number_or(field, default)?;
502        Self::from_value(value, fields, field)
503    }
504
505    /// Pair an already-read `In` with its bound — for fields whose value
506    /// comes from a fallback chain (`amp-px` seeding `amp-x-px`).
507    pub fn from_value(
508        value: In<f64>,
509        fields: &serde_json::Map<String, Value>,
510        field: &'static str,
511    ) -> Result<Self, FactoryError> {
512        let ceiling = format!("{field}-max");
513        let declared = fields.get(&ceiling).and_then(Value::as_f64);
514        let bound = match (value.static_bound(), declared) {
515            // A declared ceiling wins even over a literal: it is the
516            // author saying "no more than this", and clamping to it is
517            // then a no-op for a literal within range.
518            (_, Some(d)) => d,
519            (Some(b), None) => b,
520            (None, None) => {
521                return Err(FactoryError::BadField {
522                    field: field.to_string(),
523                    msg: format!(
524                        "canvas padding is fixed before rendering, so `{field}` needs an \
525                         upper bound at build time: use a literal, a `$param` with `max`, \
526                         or declare `{ceiling}` alongside the `@node` port"
527                    ),
528                })
529            }
530        };
531        Ok(Self {
532            value,
533            bound,
534            field,
535            clamped: std::sync::atomic::AtomicBool::new(false),
536        })
537    }
538
539    /// The bound padding was computed from.
540    pub fn bound(&self) -> f64 {
541        self.bound
542    }
543
544    /// The value for this render, clamped to [`PaddingIn::bound`].
545    pub fn get(
546        &self,
547        ctx: &crate::EvalCtx<'_>,
548        inputs: &[Option<crate::PortValue>],
549    ) -> Result<f64, crate::EvalError> {
550        let raw = self.value.get(ctx, inputs)?;
551        if raw > self.bound {
552            if !self
553                .clamped
554                .swap(true, std::sync::atomic::Ordering::Relaxed)
555            {
556                tracing::warn!(
557                    "`{}`: {raw} exceeds the {} the canvas was padded for; clamping. \
558                     Raise `{}-max` to let it through.",
559                    self.field,
560                    self.bound,
561                    self.field,
562                );
563            }
564            return Ok(self.bound);
565        }
566        Ok(raw)
567    }
568
569    pub fn param_hash(&self, h: &mut xxhash_rust::xxh3::Xxh3) {
570        self.value.param_hash(h);
571        h.update(&self.bound.to_le_bytes());
572    }
573
574    pub fn param_refs(&self) -> Vec<String> {
575        match &self.value {
576            In::Param { name, .. } => vec![name.clone()],
577            _ => Vec::new(),
578        }
579    }
580}