use std::any::Any;
use std::collections::BTreeMap;
use std::sync::Arc;
use ezu_core::TileId as CoreTileId;
use ezu_graph::{EvalCtx, EvalError, FactoryCtx, FactoryError, PortKind, PortValue, RasterBuf};
use ezu_style as spec;
use hokusai::Brush;
use serde_json::Value;
use crate::Canvas;
pub struct FeatureGroup {
pub properties: Arc<BTreeMap<String, maplibre_expr::Value>>,
pub polygons: Vec<ezu_features::Polygon>,
pub lines: Vec<Vec<(i32, i32)>>,
pub points: Vec<(i32, i32)>,
}
impl FeatureGroup {
pub fn synthetic(
polygons: Vec<ezu_features::Polygon>,
lines: Vec<Vec<(i32, i32)>>,
points: Vec<(i32, i32)>,
) -> FeatureGroup {
FeatureGroup {
properties: Arc::new(BTreeMap::new()),
polygons,
lines,
points,
}
}
}
pub struct FilteredFeatures {
pub extent: u32,
pub groups: Vec<FeatureGroup>,
}
impl FilteredFeatures {
pub fn points(&self) -> impl Iterator<Item = (i32, i32)> + '_ {
self.groups.iter().flat_map(|g| g.points.iter().copied())
}
pub fn lines(&self) -> impl Iterator<Item = &Vec<(i32, i32)>> + '_ {
self.groups.iter().flat_map(|g| g.lines.iter())
}
pub fn polygons(&self) -> impl Iterator<Item = &ezu_features::Polygon> + '_ {
self.groups.iter().flat_map(|g| g.polygons.iter())
}
pub fn has_points(&self) -> bool {
self.groups.iter().any(|g| !g.points.is_empty())
}
pub fn has_lines(&self) -> bool {
self.groups.iter().any(|g| !g.lines.is_empty())
}
pub fn has_polygons(&self) -> bool {
self.groups.iter().any(|g| !g.polygons.is_empty())
}
}
pub type BrushPayload = Brush;
pub(super) fn resolve_field(
fields: &serde_json::Map<String, Value>,
name: &str,
ctx: &FactoryCtx<'_>,
) -> Result<Value, FactoryError> {
let v = fields
.get(name)
.ok_or_else(|| FactoryError::MissingField(name.to_string()))?;
if let Some(s) = v.as_str() {
match spec::FieldRef::classify(s) {
spec::FieldRef::Param(p) => {
let decl = ctx
.params
.get(p)
.ok_or_else(|| FactoryError::UnknownParam(p.to_string()))?;
tracing::warn!(
"field `{name}`: `${p}` is read when the graph is built, so its \
declared default ({}) is baked in and render-time overrides of \
`{p}` will not change this field",
decl.default,
);
return Ok(decl.default.clone());
}
spec::FieldRef::Node(_) => {
return Err(FactoryError::BadField {
field: name.into(),
msg: "expected literal or $param, got @node-ref".into(),
});
}
spec::FieldRef::Literal(_) => {}
}
}
Ok(v.clone())
}
pub(super) fn read_number(
fields: &serde_json::Map<String, Value>,
name: &str,
ctx: &FactoryCtx<'_>,
) -> Result<f64, FactoryError> {
let v = resolve_field(fields, name, ctx)?;
v.as_f64().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: "expected number".into(),
})
}
pub(super) fn read_number_or(
fields: &serde_json::Map<String, Value>,
name: &str,
ctx: &FactoryCtx<'_>,
default: f64,
) -> Result<f64, FactoryError> {
if !fields.contains_key(name) {
return Ok(default);
}
read_number(fields, name, ctx)
}
pub(super) fn read_bool_or(
fields: &serde_json::Map<String, Value>,
name: &str,
ctx: &FactoryCtx<'_>,
default: bool,
) -> Result<bool, FactoryError> {
if !fields.contains_key(name) {
return Ok(default);
}
let v = resolve_field(fields, name, ctx)?;
v.as_bool().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: "expected boolean".into(),
})
}
pub(super) fn read_string_or(
fields: &serde_json::Map<String, Value>,
name: &str,
ctx: &FactoryCtx<'_>,
default: &str,
) -> Result<String, FactoryError> {
if !fields.contains_key(name) {
return Ok(default.to_string());
}
let v = resolve_field(fields, name, ctx)?;
let s = v.as_str().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: "expected string".into(),
})?;
Ok(s.to_string())
}
pub(super) fn read_optional_zoom(
fields: &serde_json::Map<String, Value>,
key: &str,
) -> Result<Option<u8>, FactoryError> {
let Some(v) = fields.get(key) else {
return Ok(None);
};
let n = v.as_u64().ok_or_else(|| FactoryError::BadField {
field: key.into(),
msg: "expected non-negative integer".into(),
})?;
if n > 24 {
return Err(FactoryError::BadField {
field: key.into(),
msg: format!("zoom {n} out of range (0..=24)"),
});
}
Ok(Some(n as u8))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum BoundaryMode {
Clamp,
Transparent,
Mirror,
}
pub(super) fn read_boundary(
fields: &serde_json::Map<String, Value>,
name: &str,
default: BoundaryMode,
) -> Result<BoundaryMode, FactoryError> {
let Some(v) = fields.get(name) else {
return Ok(default);
};
let s = v.as_str().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: "expected string".into(),
})?;
match s {
"clamp" => Ok(BoundaryMode::Clamp),
"transparent" => Ok(BoundaryMode::Transparent),
"mirror" => Ok(BoundaryMode::Mirror),
_ => Err(FactoryError::BadField {
field: name.into(),
msg: format!("unknown boundary `{s}`, expected clamp/transparent/mirror"),
}),
}
}
#[inline]
fn wrap_index(i: i64, dim: u32, mode: BoundaryMode) -> Option<u32> {
let d = dim as i64;
if d == 0 {
return None;
}
if i >= 0 && i < d {
return Some(i as u32);
}
match mode {
BoundaryMode::Clamp => Some(i.clamp(0, d - 1) as u32),
BoundaryMode::Transparent => None,
BoundaryMode::Mirror => {
if d == 1 {
return Some(0);
}
let period = 2 * (d - 1);
let mut k = i % period;
if k < 0 {
k += period;
}
if k >= d {
k = period - k;
}
Some(k as u32)
}
}
}
#[inline]
fn read_pixel_or(src: &RasterBuf, ix: i64, iy: i64, mode: BoundaryMode) -> [f32; 4] {
let Some(x) = wrap_index(ix, src.width, mode) else {
return [0.0; 4];
};
let Some(y) = wrap_index(iy, src.height, mode) else {
return [0.0; 4];
};
let p = src.pixel(x, y);
[p[0] as f32, p[1] as f32, p[2] as f32, p[3] as f32]
}
pub(super) fn sample_bilinear(src: &RasterBuf, x: f64, y: f64, mode: BoundaryMode) -> [u8; 4] {
let fx = x.floor();
let fy = y.floor();
let tx = (x - fx) as f32;
let ty = (y - fy) as f32;
let ix = fx as i64;
let iy = fy as i64;
let p00 = read_pixel_or(src, ix, iy, mode);
let p10 = read_pixel_or(src, ix + 1, iy, mode);
let p01 = read_pixel_or(src, ix, iy + 1, mode);
let p11 = read_pixel_or(src, ix + 1, iy + 1, mode);
let mut out = [0u8; 4];
for c in 0..4 {
let a = p00[c] + (p10[c] - p00[c]) * tx;
let b = p01[c] + (p11[c] - p01[c]) * tx;
let v = a + (b - a) * ty;
out[c] = v.round().clamp(0.0, 255.0) as u8;
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Anchor {
Tile,
World,
}
pub(super) fn default_field_seed(anchor: Anchor, rng_seed: u64) -> u32 {
match anchor {
Anchor::World => 0x5EED_1F1E,
Anchor::Tile => (rng_seed as u32) ^ ((rng_seed >> 32) as u32),
}
}
pub(super) fn read_anchor(
fields: &serde_json::Map<String, Value>,
name: &str,
ctx: &FactoryCtx<'_>,
) -> Result<Anchor, FactoryError> {
let s = read_string_or(fields, name, ctx, "tile")?;
match s.as_str() {
"tile" => Ok(Anchor::Tile),
"world" => Ok(Anchor::World),
_ => Err(FactoryError::BadField {
field: name.into(),
msg: format!("expected `tile` or `world`, got `{s}`"),
}),
}
}
pub(super) fn read_xy(
fields: &serde_json::Map<String, Value>,
name: &str,
ctx: &FactoryCtx<'_>,
default: [f32; 2],
) -> Result<[f32; 2], FactoryError> {
if !fields.contains_key(name) {
return Ok(default);
}
let v = resolve_field(fields, name, ctx)?;
let arr = v.as_array().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: "expected [x, y] array".into(),
})?;
if arr.len() != 2 {
return Err(FactoryError::BadField {
field: name.into(),
msg: format!("expected exactly 2 numbers, got {}", arr.len()),
});
}
let x = arr[0].as_f64().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: "x must be number".into(),
})? as f32;
let y = arr[1].as_f64().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: "y must be number".into(),
})? as f32;
Ok([x, y])
}
pub(super) type StopsIn = Vec<(ezu_graph::In<f64>, ezu_graph::In<[f32; 4]>)>;
pub(super) fn read_stops(
fields: &serde_json::Map<String, Value>,
name: &str,
r: &mut ezu_graph::InReader<'_, '_>,
) -> Result<StopsIn, FactoryError> {
let v = fields
.get(name)
.ok_or_else(|| FactoryError::MissingField(name.to_string()))?;
let arr = v.as_array().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: "expected array of [t, color] pairs".into(),
})?;
if arr.len() < 2 {
return Err(FactoryError::BadField {
field: name.into(),
msg: "gradient needs at least 2 stops".into(),
});
}
let mut out = Vec::with_capacity(arr.len());
for (i, pt) in arr.iter().enumerate() {
let pair = pt.as_array().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: format!("entry {i}: expected [t, color] pair"),
})?;
if pair.len() != 2 {
return Err(FactoryError::BadField {
field: name.into(),
msg: format!("entry {i}: expected exactly 2 entries"),
});
}
out.push((
r.nested(&format!("{name}[{i}][0]"), &pair[0])?,
r.nested(&format!("{name}[{i}][1]"), &pair[1])?,
));
}
Ok(out)
}
pub(super) fn resolve_stops(
stops: &StopsIn,
ctx: &EvalCtx<'_>,
inputs: &[Option<PortValue>],
) -> Result<Vec<(f32, [f32; 4])>, EvalError> {
let mut out = Vec::with_capacity(stops.len());
for (t, c) in stops {
out.push((t.get(ctx, inputs)? as f32, c.get(ctx, inputs)?));
}
out.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
Ok(out)
}
pub(super) fn hash_stops(stops: &StopsIn, h: &mut xxhash_rust::xxh3::Xxh3) {
for (t, c) in stops {
t.param_hash(h);
c.param_hash(h);
}
}
pub(super) fn read_space(
fields: &serde_json::Map<String, Value>,
) -> Result<crate::color_interp::InterpSpace, FactoryError> {
match fields.get("space") {
None | Some(Value::Null) => Ok(crate::color_interp::InterpSpace::default()),
Some(v) => {
let s = v.as_str().ok_or_else(|| FactoryError::BadField {
field: "space".into(),
msg: "expected a string".into(),
})?;
crate::color_interp::InterpSpace::parse(s).ok_or_else(|| FactoryError::BadField {
field: "space".into(),
msg: format!("unknown colour space `{s}` (rgb|hsl|hsv|hcl|lab)"),
})
}
}
}
pub(super) fn sample_stops(
stops: &[(f32, [f32; 4])],
t: f32,
space: crate::color_interp::InterpSpace,
) -> [f32; 4] {
if stops.is_empty() {
return [0.0; 4];
}
if t <= stops[0].0 {
return stops[0].1;
}
let last = stops.last().expect("stops is non-empty (guarded above)");
if t >= last.0 {
return last.1;
}
for w in stops.windows(2) {
if t >= w[0].0 && t <= w[1].0 {
let d = w[1].0 - w[0].0;
if d < 1e-6 {
return w[1].1;
}
let f = (t - w[0].0) / d;
return crate::color_interp::interpolate(w[0].1, w[1].1, f, space);
}
}
last.1
}
pub(super) fn read_optional_string(
fields: &serde_json::Map<String, Value>,
name: &str,
) -> Result<Option<String>, FactoryError> {
let Some(v) = fields.get(name) else {
return Ok(None);
};
let s = v.as_str().ok_or_else(|| FactoryError::BadField {
field: name.into(),
msg: "expected string".into(),
})?;
Ok(Some(s.to_string()))
}
pub(super) fn color_f32_to_u8(c: [f32; 4]) -> [u8; 4] {
[
(c[0].clamp(0.0, 1.0) * 255.0).round() as u8,
(c[1].clamp(0.0, 1.0) * 255.0).round() as u8,
(c[2].clamp(0.0, 1.0) * 255.0).round() as u8,
(c[3].clamp(0.0, 1.0) * 255.0).round() as u8,
]
}
pub(super) fn srgb_to_linear_rgba(c: [f32; 4]) -> [f32; 4] {
fn ch(v: f32) -> f32 {
if v <= 0.04045 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4)
}
}
[ch(c[0]), ch(c[1]), ch(c[2]), c[3]]
}
pub(super) fn color_to_premul_u8(c: [f32; 4]) -> [u8; 4] {
let a = c[3].clamp(0.0, 1.0);
[
(c[0] * a * 255.0).round() as u8,
(c[1] * a * 255.0).round() as u8,
(c[2] * a * 255.0).round() as u8,
(a * 255.0).round() as u8,
]
}
pub(super) fn rgba8_to_color(c: [u8; 4]) -> tiny_skia::Color {
tiny_skia::Color::from_rgba8(c[0], c[1], c[2], c[3])
}
pub(super) fn tint_alpha_color(c: [u8; 4], alpha_mul: f32) -> tiny_skia::Color {
let a = ((c[3] as f32) * alpha_mul.clamp(0.0, 1.0)).round() as u8;
tiny_skia::Color::from_rgba8(c[0], c[1], c[2], a)
}
pub(super) fn make_canvas(ctx: &EvalCtx<'_>) -> Result<Canvas, EvalError> {
Canvas::new_padded(ctx.canvas.tile_w, ctx.canvas.tile_h, ctx.canvas.pad).ok_or_else(|| {
EvalError::Other(format!(
"canvas allocation failed for tile={}x{} pad={}",
ctx.canvas.tile_w, ctx.canvas.tile_h, ctx.canvas.pad
))
})
}
pub(super) fn canvas_into_raster(canvas: Canvas) -> RasterBuf {
let pixmap = canvas.into_pixmap();
let (w, h) = (pixmap.width(), pixmap.height());
RasterBuf {
width: w,
height: h,
pixels: pixmap.take(),
}
}
pub(super) fn empty_raster(ctx: &EvalCtx<'_>) -> PortValue {
let (pw, ph) = ctx.canvas.padded_dims();
PortValue::Raster(Arc::new(RasterBuf::new(pw, ph)))
}
pub(super) fn core_tile(ctx: &EvalCtx<'_>) -> CoreTileId {
CoreTileId::new(ctx.tile.z, ctx.tile.x, ctx.tile.y)
}
pub(super) fn features_value(extent: u32, groups: Vec<FeatureGroup>) -> PortValue {
let payload = FilteredFeatures { extent, groups };
PortValue::Features(Arc::new(payload) as Arc<dyn Any + Send + Sync>)
}
pub(super) const ACCEPTS_RASTER_OR_SPRITE: &[PortKind] = &[PortKind::Raster, PortKind::Sprite];
pub(super) fn unwrap_raster_or_sprite(
v: &PortValue,
port_name: &str,
) -> Result<(Arc<RasterBuf>, RasterKind), EvalError> {
match v {
PortValue::Raster(r) => Ok((r.clone(), RasterKind::Raster)),
PortValue::Sprite(s) => Ok((s.clone(), RasterKind::Sprite)),
_ => Err(EvalError::MissingInput(port_name.into())),
}
}
pub(super) fn wrap_raster_like(buf: Arc<RasterBuf>, kind: RasterKind) -> PortValue {
match kind {
RasterKind::Raster => PortValue::Raster(buf),
RasterKind::Sprite => PortValue::Sprite(buf),
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum RasterKind {
Raster,
Sprite,
}
pub(super) fn raster_or_sprite_output(input_kinds: &[Option<PortKind>]) -> PortKind {
match input_kinds.first().and_then(|k| *k) {
Some(PortKind::Sprite) => PortKind::Sprite,
_ => PortKind::Raster,
}
}
pub(super) fn downcast_features(v: &PortValue) -> Result<Arc<FilteredFeatures>, EvalError> {
let PortValue::Features(o) = v else {
return Err(EvalError::Other("expected Features".into()));
};
o.clone()
.downcast::<FilteredFeatures>()
.map_err(|_| EvalError::Other("features payload type mismatch".into()))
}
pub(super) fn downcast_brush(v: &PortValue) -> Result<Arc<BrushPayload>, EvalError> {
let PortValue::Brush(o) = v else {
return Err(EvalError::Other("expected Brush".into()));
};
o.clone()
.downcast::<BrushPayload>()
.map_err(|_| EvalError::Other("brush payload type mismatch".into()))
}