Skip to main content

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}