ezu_graph/eval.rs
1//! Evaluation context, asset loader, and error types used during
2//! `Node::eval`.
3
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use crate::buf::{OpaqueValue, RasterBuf, ScalarField, SpriteSheet};
8use crate::value::ScalarValue;
9
10/// Tile coordinate (z/x/y in TMS-ish form; the meaning is up to the host).
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct TileId {
13 pub z: u8,
14 pub x: u32,
15 pub y: u32,
16}
17
18/// Per-render canvas geometry. The padded buffer is the actual size all
19/// `Raster` ports must produce; the final tile is the inner
20/// `tile_w` × `tile_h` region.
21///
22/// A map tile is square and [`square`](Self::square) is how one is
23/// asked for. The two axes are separate because not every render is a
24/// tile: a legend swatch is whatever shape the legend has room for, and
25/// its geometry is synthetic, so there is no projection to distort.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct CanvasInfo {
28 pub tile_w: u32,
29 pub tile_h: u32,
30 pub pad: u32,
31}
32
33impl CanvasInfo {
34 /// The usual case: a square tile of `tile_size` px with `pad` px of
35 /// margin on every side.
36 pub fn square(tile_size: u32, pad: u32) -> Self {
37 Self {
38 tile_w: tile_size,
39 tile_h: tile_size,
40 pad,
41 }
42 }
43
44 pub fn padded_w(&self) -> u32 {
45 self.tile_w + 2 * self.pad
46 }
47
48 pub fn padded_h(&self) -> u32 {
49 self.tile_h + 2 * self.pad
50 }
51
52 /// Both padded axes at once — the shape every `Raster` port must
53 /// produce.
54 pub fn padded_dims(&self) -> (u32, u32) {
55 (self.padded_w(), self.padded_h())
56 }
57}
58
59/// One asset fetched by an [`AssetLoader`].
60///
61/// The shape is uniform across input kinds (images, brushes, feature
62/// layers, …) so every source-style node consumes the host through the
63/// same trait — like a shader sampling a typed uniform binding.
64/// `Features` carries a type-erased payload; by convention the
65/// concrete type is `Arc<ezu_features::FeatureLayer>`. `Font` and
66/// `Glyphs` are likewise type-erased (by convention
67/// `Arc<ezu_core::text::Font>` / `Arc<ezu_core::text::SdfFontStack>`)
68/// so this crate gains no font or raster-drawing dependencies.
69#[derive(Debug, Clone)]
70pub enum Asset {
71 Image(Arc<RasterBuf>),
72 Brush(OpaqueValue),
73 Features(OpaqueValue),
74 ScalarField(Arc<ScalarField>),
75 /// A sprite atlas + name→rect index; the `icon` node crops named rects.
76 Sprite(Arc<SpriteSheet>),
77 /// A loaded font face; the `text` node shapes and draws with it.
78 Font(OpaqueValue),
79 /// A glyph-PBF (SDF) fontstack; the `text` node's compat backend.
80 Glyphs(OpaqueValue),
81}
82
83#[derive(Debug, thiserror::Error)]
84pub enum AssetError {
85 #[error("asset not found: `{0}`")]
86 NotFound(String),
87 #[error("asset decode failed for `{src}`: {msg}")]
88 Decode { src: String, msg: String },
89 #[error("asset error: {0}")]
90 Other(String),
91}
92
93/// Pluggable backend for resolving named asset bindings (images,
94/// brushes, tile features, …). Names without a scheme prefix
95/// (`<source>` or `<source>.<layer>`) are by convention tile-scoped —
96/// the host rebinds them per render. Asset srcs carry a scheme
97/// (`builtin:`, `file:`, `http(s)://`) and are document-scoped.
98///
99/// `hash` returns a stable content/identity hash the evaluator folds
100/// into every consuming node's cache key, so changes in a bound asset
101/// invalidate caches automatically. Implementations may return `0` if
102/// the binding never changes (document-scoped, fixed disk file, etc.).
103pub trait AssetLoader: Send + Sync {
104 fn load(&self, name: &str) -> Result<Asset, AssetError>;
105
106 /// Content/identity hash for cache invalidation. The default of
107 /// `0` is safe for assets that never change for the lifetime of a
108 /// loader (typical for in-memory image / brush banks).
109 fn hash(&self, _name: &str) -> u128 {
110 0
111 }
112}
113
114/// A no-op asset loader. Every load returns `NotFound`. Useful for
115/// tests of graphs that don't touch any asset.
116pub struct NoAssets;
117impl AssetLoader for NoAssets {
118 fn load(&self, name: &str) -> Result<Asset, AssetError> {
119 Err(AssetError::NotFound(name.to_string()))
120 }
121}
122
123/// Resolved parameter values for one render. Nodes look up `$name`
124/// references here.
125#[derive(Debug, Default, Clone)]
126pub struct ParamValues {
127 pub values: HashMap<String, ScalarValue>,
128}
129
130impl ParamValues {
131 pub fn new() -> Self {
132 Self::default()
133 }
134
135 pub fn set(&mut self, name: impl Into<String>, value: ScalarValue) {
136 self.values.insert(name.into(), value);
137 }
138
139 pub fn get(&self, name: &str) -> Option<ScalarValue> {
140 self.values.get(name).copied()
141 }
142}
143
144/// Read-only environment a node sees during `eval`.
145#[derive(Clone, Copy)]
146pub struct EvalCtx<'a> {
147 pub tile: TileId,
148 pub canvas: CanvasInfo,
149 pub assets: &'a dyn AssetLoader,
150 pub params: &'a ParamValues,
151 /// Deterministic root seed for this render. World-anchored nodes
152 /// hash this with world coordinates to produce per-feature seeds.
153 pub rng_seed: u64,
154 /// How far outside the canvas *this* node's geometry can still reach
155 /// the rendered tile — see [`crate::Node::influence_pad`]. A source
156 /// may drop geometry that lies further out than this; `u32::MAX`
157 /// means the reach is unbounded and nothing may be dropped.
158 pub influence_pad: u32,
159}
160
161impl EvalCtx<'_> {
162 /// The canvas rectangle, grown by this node's influence, outside
163 /// which geometry cannot affect the rendered tile. In padded-canvas
164 /// pixels; `None` when the reach is unbounded.
165 pub fn cull_rect(&self) -> Option<(f64, f64, f64, f64)> {
166 if self.influence_pad == u32::MAX {
167 return None;
168 }
169 let m = self.influence_pad as f64;
170 Some((
171 -m,
172 -m,
173 self.canvas.padded_w() as f64 + m,
174 self.canvas.padded_h() as f64 + m,
175 ))
176 }
177}
178
179#[derive(Debug, thiserror::Error)]
180pub enum EvalError {
181 #[error("input port `{0}` was not supplied")]
182 MissingInput(String),
183 #[error("input `{port}` has wrong kind: expected {expected:?}, got {got:?}")]
184 InputKindMismatch {
185 port: String,
186 expected: crate::port::PortKind,
187 got: crate::port::PortKind,
188 },
189 #[error(transparent)]
190 Asset(#[from] AssetError),
191 #[error("{0}")]
192 Other(String),
193}