Skip to main content

ezu_graph/
buf.rs

1//! Concrete buffer types flowing along `Raster` edges.
2//!
3//! These are deliberately small and dependency-free so node
4//! implementations from different crates can produce / consume them
5//! without a shared dependency on `tiny-skia` or `hokusai`. Nodes
6//! that wrap those engines do conversions at their boundaries.
7
8use std::any::Any;
9use std::sync::Arc;
10
11/// RGBA8 raster, sRGB color space, premultiplied alpha. Layout is
12/// row-major, four bytes per pixel `[R, G, B, A]`.
13#[derive(Debug, Clone)]
14pub struct RasterBuf {
15    pub width: u32,
16    pub height: u32,
17    pub pixels: Vec<u8>,
18}
19
20impl RasterBuf {
21    pub fn new(width: u32, height: u32) -> Self {
22        Self {
23            width,
24            height,
25            pixels: vec![0; (width * height * 4) as usize],
26        }
27    }
28
29    pub fn filled(width: u32, height: u32, rgba: [u8; 4]) -> Self {
30        let mut s = Self::new(width, height);
31        for px in s.pixels.chunks_exact_mut(4) {
32            px.copy_from_slice(&rgba);
33        }
34        s
35    }
36
37    pub fn pixel(&self, x: u32, y: u32) -> [u8; 4] {
38        let i = ((y * self.width + x) * 4) as usize;
39        [
40            self.pixels[i],
41            self.pixels[i + 1],
42            self.pixels[i + 2],
43            self.pixels[i + 3],
44        ]
45    }
46}
47
48/// Type-erased value carried on `Features` and `Brush` ports. Concrete
49/// types are a convention between producer and consumer node impls;
50/// downcasts happen inside nodes. The DAG only checks the `PortKind`.
51pub type OpaqueValue = Arc<dyn Any + Send + Sync>;
52
53/// Per-pixel `f32` scalar grid flowing along `ScalarField` ports.
54///
55/// The general carrier for single-channel floating-point data —
56/// elevation, signed distance, scalar noise, slope angle, anything
57/// "one number per pixel". Layout is row-major, one `f32` per pixel.
58/// `width` / `height` MUST match the canvas's `padded_size()` so
59/// consumers can pair samples with the same geometry as their raster
60/// output.
61///
62/// `geo_scale` is populated when the values represent a quantity
63/// measured per real-world distance (e.g. elevation in metres at a
64/// particular latitude). Gradient-based consumers (`hillshade`,
65/// `slope`) read it to compute geographically faithful results.
66/// `None` means the field is unitless / in pixel space — fine for
67/// `color-ramp` style mapping but stylization-only
68/// for gradient ops.
69///
70/// Missing samples (e.g. ocean nodata in some DEMs) surface as
71/// `nodata`; consumers fall back to `0.0` or pass-through.
72#[derive(Debug, Clone)]
73pub struct ScalarField {
74    pub width: u32,
75    pub height: u32,
76    pub values: Arc<[f32]>,
77    pub nodata: Option<f32>,
78    pub geo_scale: Option<GeoScale>,
79}
80
81/// Geographic per-pixel scaling for a `ScalarField`. Filled by the
82/// producer from tile geometry and latitude (Web Mercator's scale is
83/// latitude-dependent), so consumers like `slope` don't need to
84/// re-derive tile geometry.
85#[derive(Debug, Clone, Copy)]
86pub struct GeoScale {
87    pub metres_per_pixel_x: f32,
88    pub metres_per_pixel_y: f32,
89}
90
91impl ScalarField {
92    pub fn sample(&self, x: u32, y: u32) -> f32 {
93        self.values[(y * self.width + x) as usize]
94    }
95
96    /// Real-world metres per pixel along X, or `1.0` when the field
97    /// has no geographic scaling. Lets gradient consumers stay
98    /// branch-free; the fallback is a no-op scaling that produces
99    /// pixel-space gradients — geographically inaccurate but useful
100    /// for stylization over non-DEM inputs.
101    pub fn metres_per_pixel_x(&self) -> f32 {
102        self.geo_scale.map(|g| g.metres_per_pixel_x).unwrap_or(1.0)
103    }
104
105    pub fn metres_per_pixel_y(&self) -> f32 {
106        self.geo_scale.map(|g| g.metres_per_pixel_y).unwrap_or(1.0)
107    }
108}