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}