Skip to main content

ezu_graph/
node.rs

1//! The `Node` trait — what every operation in the graph implements.
2
3use xxhash_rust::xxh3::Xxh3;
4
5use crate::eval::{AssetLoader, EvalCtx, EvalError};
6use crate::port::{CoordSpace, PortKind, PortSpec};
7use crate::value::PortValue;
8
9/// How far a brush puts ink from the path it is dragged along.
10///
11/// Carries the radius it was measured at, because an op may override the
12/// brush's radius — everything else about a dab (its elliptical ratio,
13/// its jitters) scales with the radius, so the reach does too.
14#[derive(Debug, Clone, Copy)]
15pub struct InkReach {
16    /// Furthest a dab reaches from the path, at `radius_px`.
17    pub reach_px: f64,
18    /// The radius that reach was measured at.
19    pub radius_px: f64,
20}
21
22impl InkReach {
23    /// The reach this brush would have at a different radius.
24    pub fn at_radius(&self, radius_px: f64) -> f64 {
25        if self.radius_px <= 0.0 {
26            return self.reach_px;
27        }
28        self.reach_px * (radius_px / self.radius_px).max(1.0)
29    }
30}
31
32/// What a node needs in order to say how far outside the canvas its
33/// input geometry can still matter — see [`Node::influence_pad`].
34pub struct InfluenceCtx<'a> {
35    /// Reach already claimed by everything downstream of this node.
36    pub downstream: u32,
37    /// The brush feeding this node, if one does — see
38    /// [`Node::ink_reach`].
39    pub brush: Option<InkReach>,
40    pub assets: &'a dyn AssetLoader,
41}
42
43impl InfluenceCtx<'_> {
44    /// The reach is not bounded by anything this node knows, so nothing
45    /// upstream of it may be dropped.
46    pub const UNBOUNDED: u32 = u32::MAX;
47
48    /// `downstream` plus `extra` px, saturating into
49    /// [`Self::UNBOUNDED`] rather than wrapping.
50    pub fn plus(&self, extra: f64) -> u32 {
51        if !extra.is_finite() || extra < 0.0 {
52            return Self::UNBOUNDED;
53        }
54        let extra = extra.ceil();
55        if extra >= u32::MAX as f64 {
56            return Self::UNBOUNDED;
57        }
58        self.downstream.saturating_add(extra as u32)
59    }
60
61    /// `downstream` plus a bound that may not exist — an `@node` port
62    /// or an unbounded `$param` has no static ceiling, and a field that
63    /// moves geometry outward without one cannot be culled against.
64    pub fn plus_bound(&self, bound: Option<f64>) -> u32 {
65        match bound {
66            Some(b) => self.plus(b.abs()),
67            None => Self::UNBOUNDED,
68        }
69    }
70}
71
72/// One operation in the DAG. Stored as `Box<dyn Node>` inside
73/// [`crate::Graph`]; the graph never mutates a node after construction.
74pub trait Node: Send + Sync {
75    /// Stable identifier for the operation (e.g. `"blur"`,
76    /// `"scatter-dabs"`). Matches the `op` field in the style JSON.
77    fn op_name(&self) -> &'static str;
78
79    /// Declared input ports in positional order. The style JSON
80    /// connects each port by name; `eval` receives values in this same
81    /// positional order.
82    fn inputs(&self) -> &[PortSpec];
83
84    /// The kind of value this node produces.
85    ///
86    /// `input_kinds` carries the resolved [`PortKind`] of each input
87    /// port, in the same positional order as [`Node::inputs`]. Entries
88    /// are `Some` for connected ports (including optional ones) and
89    /// `None` for unconnected optional ports.
90    ///
91    /// Most nodes return a constant; polymorphic nodes (e.g. `blur`
92    /// accepting both `Raster` and `Sprite`) inspect `input_kinds` and
93    /// mirror the upstream kind. The graph builder resolves nodes in
94    /// topological order, so upstream kinds are always known when this
95    /// is called.
96    fn output(&self, input_kinds: &[Option<PortKind>]) -> PortKind;
97
98    /// Reject a combination of upstream kinds this node cannot serve,
99    /// with a message explaining why.
100    ///
101    /// [`Node::output`] has to answer with *some* kind, so a node whose
102    /// requirement spans several ports — `switch` with a runtime
103    /// `select`, which can only promise one output kind if both of its
104    /// inputs share one — says so here instead. Called once per node at
105    /// build time, right after the upstream kinds are resolved and
106    /// before `output`.
107    fn validate_kinds(&self, _input_kinds: &[Option<PortKind>]) -> Result<(), String> {
108        Ok(())
109    }
110
111    /// Coordinate space the node operates in. Defaults to inheriting
112    /// from inputs.
113    fn coord_space(&self) -> CoordSpace {
114        CoordSpace::Inherit
115    }
116
117    /// How much canvas padding this node requires *upstream* given the
118    /// padding requested by downstream consumers. Blur-like ops grow
119    /// the value; most pass it through unchanged.
120    fn required_pad(&self, downstream: u32) -> u32 {
121        downstream
122    }
123
124    /// How far outside the canvas this node's *input geometry* can still
125    /// end up mattering, given the distance already claimed downstream.
126    ///
127    /// This is the mirror of [`Node::required_pad`], and deliberately a
128    /// separate number. `required_pad` asks how much canvas a node needs
129    /// because it *reads* neighbouring pixels; a brush stroke reads
130    /// nothing, so it declares none and the canvas stays small. But a
131    /// stroke *writes* a dab's radius away from its vertex, and a wave
132    /// displaces a vertex by its amplitude before that — so geometry
133    /// sitting outside the canvas can still put ink inside it. Answering
134    /// "how far outside?" is what lets a source drop geometry it can
135    /// prove is invisible, which is the difference between a deeply
136    /// overzoomed tile costing its ancestor's whole extent and costing
137    /// its own.
138    ///
139    /// Raster ops inherit their read distance, since ink pulled inward by
140    /// a blur matters as much as ink painted there. Ops that displace or
141    /// grow geometry add their own reach. Return [`u32::MAX`] to say the
142    /// reach cannot be bounded, which keeps every upstream feature.
143    ///
144    /// Like `required_pad`, this must be a worst case over the values a
145    /// field can take, not the value this render happens to use.
146    fn influence_pad(&self, ctx: &InfluenceCtx<'_>) -> u32 {
147        self.required_pad(ctx.downstream)
148    }
149
150    /// How far from a stroke's path this node's *output* can lay ink,
151    /// for the op that consumes it. Only a brush answers: how wide a dab
152    /// reaches is a property of the brush, not of the op holding it, and
153    /// the op receives it through a port it cannot inspect until eval.
154    /// The graph hands it to the consumer as [`InfluenceCtx::brush`].
155    ///
156    /// `None` from a node that *is* a brush means its reach could not be
157    /// established, which leaves the consumer unbounded.
158    fn ink_reach(&self, _assets: &dyn AssetLoader) -> Option<InkReach> {
159        None
160    }
161
162    /// Produce this node's output given resolved inputs. `inputs` has
163    /// one entry per declared port, in the order returned by
164    /// [`Node::inputs`]; unconnected optional ports are `None`.
165    fn eval(&self, ctx: &EvalCtx<'_>, inputs: &[Option<PortValue>])
166        -> Result<PortValue, EvalError>;
167
168    /// Stable content hash of this node's *own* parameters (not inputs).
169    /// Used as part of the cache key. Implementations should feed every
170    /// configuration value that influences output into the hasher.
171    fn param_hash(&self, hasher: &mut Xxh3);
172
173    /// Named asset bindings this node samples via the
174    /// [`AssetLoader`](crate::eval::AssetLoader). The evaluator folds
175    /// each binding's `AssetLoader::hash` into this node's cache key,
176    /// so changes in bound data invalidate caches automatically. Like
177    /// declaring uniforms in a shader.
178    ///
179    /// Default: no bindings.
180    fn asset_inputs(&self) -> Vec<String> {
181        Vec::new()
182    }
183
184    /// Named document params this node reads from
185    /// [`EvalCtx::params`](crate::eval::EvalCtx) at eval time (fields
186    /// built from `$param` references). The evaluator folds each
187    /// referenced param's *runtime* value into this node's cache key,
188    /// so overriding a param invalidates exactly the nodes that read
189    /// it — and nothing else.
190    ///
191    /// Default: no param reads.
192    fn param_refs(&self) -> Vec<String> {
193        Vec::new()
194    }
195}