1use ezu_style as spec;
27use serde_json::Value;
28use xxhash_rust::xxh3::Xxh3;
29
30use crate::eval::{EvalCtx, EvalError};
31use crate::port::{PortKind, PortSpec};
32use crate::registry::{Connection, FactoryCtx, FactoryError};
33use crate::value::{PortValue, ScalarValue};
34
35pub const ACCEPTS_SCALAR: &[PortKind] = &[PortKind::Scalar];
37
38pub trait ScalarType: Copy {
40 const NAME: &'static str;
42 fn matches_kind(kind: spec::ParamKind) -> bool;
44 fn from_scalar(v: ScalarValue) -> Option<Self>;
46 fn from_json(v: &Value) -> Option<Self>;
48 fn hash_into(&self, h: &mut Xxh3);
50 fn clamp_decl(self, _min: Option<f64>, _max: Option<f64>) -> Self {
52 self
53 }
54}
55
56impl ScalarType for f64 {
57 const NAME: &'static str = "number";
58 fn matches_kind(kind: spec::ParamKind) -> bool {
59 kind == spec::ParamKind::Number
60 }
61 fn from_scalar(v: ScalarValue) -> Option<Self> {
62 v.as_number()
63 }
64 fn from_json(v: &Value) -> Option<Self> {
65 v.as_f64()
66 }
67 fn hash_into(&self, h: &mut Xxh3) {
68 h.update(&self.to_le_bytes());
69 }
70 fn clamp_decl(self, min: Option<f64>, max: Option<f64>) -> Self {
71 let mut v = self;
72 if let Some(m) = min {
73 v = v.max(m);
74 }
75 if let Some(m) = max {
76 v = v.min(m);
77 }
78 v
79 }
80}
81
82impl ScalarType for [f32; 4] {
85 const NAME: &'static str = "color";
86 fn matches_kind(kind: spec::ParamKind) -> bool {
87 kind == spec::ParamKind::Color
88 }
89 fn from_scalar(v: ScalarValue) -> Option<Self> {
90 v.as_color()
91 }
92 fn from_json(v: &Value) -> Option<Self> {
93 spec::parse_hex_color(v.as_str()?)
94 }
95 fn hash_into(&self, h: &mut Xxh3) {
96 for c in self {
97 h.update(&c.to_le_bytes());
98 }
99 }
100}
101
102impl ScalarType for bool {
103 const NAME: &'static str = "bool";
104 fn matches_kind(kind: spec::ParamKind) -> bool {
105 kind == spec::ParamKind::Bool
106 }
107 fn from_scalar(v: ScalarValue) -> Option<Self> {
108 v.as_bool()
109 }
110 fn from_json(v: &Value) -> Option<Self> {
111 v.as_bool()
112 }
113 fn hash_into(&self, h: &mut Xxh3) {
114 h.update(&[*self as u8]);
115 }
116}
117
118#[derive(Debug, Clone)]
121pub enum In<T> {
122 Const(T),
124 Param {
128 name: String,
129 fallback: T,
130 min: Option<f64>,
131 max: Option<f64>,
132 },
133 Port {
135 ix: usize,
137 name: &'static str,
139 },
140}
141
142impl<T: ScalarType> In<T> {
143 pub fn get(&self, ctx: &EvalCtx<'_>, inputs: &[Option<PortValue>]) -> Result<T, EvalError> {
145 match self {
146 In::Const(v) => Ok(*v),
147 In::Param {
148 name,
149 fallback,
150 min,
151 max,
152 } => {
153 let v = match ctx.params.get(name) {
154 None => *fallback,
155 Some(sv) => T::from_scalar(sv).ok_or_else(|| {
156 EvalError::Other(format!(
157 "param `${name}`: expected {}, got {}",
158 T::NAME,
159 sv.kind_name()
160 ))
161 })?,
162 };
163 Ok(v.clamp_decl(*min, *max))
164 }
165 In::Port { ix, name } => {
166 let v = inputs
167 .get(*ix)
168 .and_then(|o| o.as_ref())
169 .ok_or_else(|| EvalError::MissingInput((*name).into()))?;
170 let PortValue::Scalar(sv) = v else {
171 return Err(EvalError::Other(format!(
172 "port `{name}`: expected a scalar, got {}",
173 v.kind()
174 )));
175 };
176 T::from_scalar(*sv).ok_or_else(|| {
177 EvalError::Other(format!(
178 "port `{name}`: expected {}, got {}",
179 T::NAME,
180 sv.kind_name()
181 ))
182 })
183 }
184 }
185 }
186
187 pub fn param_hash(&self, h: &mut Xxh3) {
191 match self {
192 In::Const(v) => {
193 h.update(b"c");
194 v.hash_into(h);
195 }
196 In::Param { name, fallback, .. } => {
197 h.update(b"p");
198 h.update(name.as_bytes());
199 fallback.hash_into(h);
200 }
201 In::Port { ix, .. } => {
202 h.update(b"@");
203 h.update(&(*ix as u64).to_le_bytes());
204 }
205 }
206 }
207
208 pub fn static_bound(&self) -> Option<f64>
213 where
214 T: Into<f64>,
215 {
216 match self {
217 In::Const(v) => Some((*v).into()),
218 In::Param { max, .. } => *max,
219 In::Port { .. } => None,
220 }
221 }
222}
223
224pub fn parse_param_value(
229 decls: &indexmap::IndexMap<String, spec::ParamDecl>,
230 name: &str,
231 raw: &str,
232) -> Result<ScalarValue, String> {
233 let decl = decls
234 .get(name)
235 .ok_or_else(|| format!("unknown param `{name}`"))?;
236 match decl.kind {
237 spec::ParamKind::Number => {
238 let v: f64 = raw
239 .parse()
240 .map_err(|_| format!("param `{name}`: `{raw}` is not a number"))?;
241 if let Some(m) = decl.min {
242 if v < m {
243 return Err(format!("param `{name}`: {v} is below min {m}"));
244 }
245 }
246 if let Some(m) = decl.max {
247 if v > m {
248 return Err(format!("param `{name}`: {v} is above max {m}"));
249 }
250 }
251 Ok(ScalarValue::Number(v))
252 }
253 spec::ParamKind::Bool => match raw {
254 "true" | "1" => Ok(ScalarValue::Bool(true)),
255 "false" | "0" => Ok(ScalarValue::Bool(false)),
256 _ => Err(format!("param `{name}`: expected true/false, got `{raw}`")),
257 },
258 spec::ParamKind::Color => spec::parse_hex_color(raw)
259 .map(ScalarValue::Color)
260 .ok_or_else(|| format!("param `{name}`: `{raw}` is not a `#rrggbb[aa]` color")),
261 }
262}
263
264#[derive(Debug, Default)]
268pub struct InParts {
269 pub ports: Vec<PortSpec>,
270 pub connections: Vec<Connection>,
271 pub param_refs: Vec<String>,
272}
273
274pub struct InReader<'a, 'c> {
278 fields: &'a serde_json::Map<String, Value>,
279 ctx: &'a FactoryCtx<'c>,
280 parts: InParts,
281 next_port: usize,
282}
283
284impl<'a, 'c> InReader<'a, 'c> {
285 pub fn new(
289 fields: &'a serde_json::Map<String, Value>,
290 ctx: &'a FactoryCtx<'c>,
291 fixed_ports: usize,
292 ) -> Self {
293 Self {
294 fields,
295 ctx,
296 parts: InParts::default(),
297 next_port: fixed_ports,
298 }
299 }
300
301 pub fn number(&mut self, name: &'static str) -> Result<In<f64>, FactoryError> {
303 self.read(name, None)
304 }
305
306 pub fn number_or(&mut self, name: &'static str, default: f64) -> Result<In<f64>, FactoryError> {
308 self.read(name, Some(default))
309 }
310
311 pub fn color(&mut self, name: &'static str) -> Result<In<[f32; 4]>, FactoryError> {
313 self.read(name, None)
314 }
315
316 pub fn color_or(
318 &mut self,
319 name: &'static str,
320 default: [f32; 4],
321 ) -> Result<In<[f32; 4]>, FactoryError> {
322 self.read(name, Some(default))
323 }
324
325 pub fn color_opt(&mut self, name: &'static str) -> Result<Option<In<[f32; 4]>>, FactoryError> {
327 if !self.fields.contains_key(name) {
328 return Ok(None);
329 }
330 Ok(Some(self.read(name, None)?))
331 }
332
333 pub fn bool_or(&mut self, name: &'static str, default: bool) -> Result<In<bool>, FactoryError> {
335 self.read(name, Some(default))
336 }
337
338 pub fn nested<T: ScalarType>(&mut self, label: &str, v: &Value) -> Result<In<T>, FactoryError> {
348 if let Some(s) = v.as_str() {
349 match spec::FieldRef::classify(s) {
350 spec::FieldRef::Node(_) => {
351 return Err(FactoryError::BadField {
352 field: label.to_string(),
353 msg: format!(
354 "expected {} literal or `$param`, got a `@node` ref — a scalar \
355 port cannot be wired into one entry of a table",
356 T::NAME
357 ),
358 })
359 }
360 spec::FieldRef::Param(p) => return self.param_in(label, p),
361 spec::FieldRef::Literal(_) => {} }
363 }
364 T::from_json(v)
365 .map(In::Const)
366 .ok_or_else(|| FactoryError::BadField {
367 field: label.to_string(),
368 msg: format!("expected {} literal or `$param`", T::NAME),
369 })
370 }
371
372 fn param_in<T: ScalarType>(&mut self, label: &str, p: &str) -> Result<In<T>, FactoryError> {
376 let decl = self
377 .ctx
378 .params
379 .get(p)
380 .ok_or_else(|| FactoryError::UnknownParam(p.to_string()))?;
381 if !T::matches_kind(decl.kind) {
382 return Err(FactoryError::BadField {
383 field: label.to_string(),
384 msg: format!(
385 "param `${p}` is declared `{:?}`, but this field needs a {}",
386 decl.kind,
387 T::NAME
388 ),
389 });
390 }
391 let fallback = T::from_json(&decl.default).ok_or_else(|| FactoryError::BadField {
392 field: label.to_string(),
393 msg: format!("param `${p}` default is not a valid {}", T::NAME),
394 })?;
395 self.parts.param_refs.push(p.to_string());
396 Ok(In::Param {
397 name: p.to_string(),
398 fallback,
399 min: decl.min,
400 max: decl.max,
401 })
402 }
403
404 fn read<T: ScalarType>(
405 &mut self,
406 name: &'static str,
407 default: Option<T>,
408 ) -> Result<In<T>, FactoryError> {
409 let Some(v) = self.fields.get(name) else {
410 return default
411 .map(In::Const)
412 .ok_or_else(|| FactoryError::MissingField(name.to_string()));
413 };
414 if let Some(s) = v.as_str() {
415 match spec::FieldRef::classify(s) {
416 spec::FieldRef::Node(id) => {
417 let ix = self.next_port;
418 self.next_port += 1;
419 self.parts.ports.push(PortSpec {
420 name,
421 accepts: ACCEPTS_SCALAR,
422 optional: false,
423 });
424 self.parts.connections.push(Connection {
425 port: name.to_string(),
426 src: id.to_string(),
427 });
428 return Ok(In::Port { ix, name });
429 }
430 spec::FieldRef::Param(p) => return self.param_in(name, p),
431 spec::FieldRef::Literal(_) => {} }
433 }
434 T::from_json(v)
435 .map(In::Const)
436 .ok_or_else(|| FactoryError::BadField {
437 field: name.into(),
438 msg: if v.is_array() {
443 format!(
444 "expected {} literal, `$param`, or `@node`, got a JSON array — \
445 if that is a MapLibre expression, it belongs on `{name}-expr`",
446 T::NAME
447 )
448 } else {
449 format!("expected {} literal, `$param`, or `@node`", T::NAME)
450 },
451 })
452 }
453
454 pub fn finish(self) -> InParts {
456 self.parts
457 }
458}
459
460pub struct PaddingIn {
477 value: In<f64>,
478 bound: f64,
479 field: &'static str,
480 clamped: std::sync::atomic::AtomicBool,
481}
482
483impl PaddingIn {
484 pub fn read(
486 r: &mut InReader<'_, '_>,
487 fields: &serde_json::Map<String, Value>,
488 field: &'static str,
489 ) -> Result<Self, FactoryError> {
490 let value = r.number(field)?;
491 Self::from_value(value, fields, field)
492 }
493
494 pub fn read_or(
496 r: &mut InReader<'_, '_>,
497 fields: &serde_json::Map<String, Value>,
498 field: &'static str,
499 default: f64,
500 ) -> Result<Self, FactoryError> {
501 let value = r.number_or(field, default)?;
502 Self::from_value(value, fields, field)
503 }
504
505 pub fn from_value(
508 value: In<f64>,
509 fields: &serde_json::Map<String, Value>,
510 field: &'static str,
511 ) -> Result<Self, FactoryError> {
512 let ceiling = format!("{field}-max");
513 let declared = fields.get(&ceiling).and_then(Value::as_f64);
514 let bound = match (value.static_bound(), declared) {
515 (_, Some(d)) => d,
519 (Some(b), None) => b,
520 (None, None) => {
521 return Err(FactoryError::BadField {
522 field: field.to_string(),
523 msg: format!(
524 "canvas padding is fixed before rendering, so `{field}` needs an \
525 upper bound at build time: use a literal, a `$param` with `max`, \
526 or declare `{ceiling}` alongside the `@node` port"
527 ),
528 })
529 }
530 };
531 Ok(Self {
532 value,
533 bound,
534 field,
535 clamped: std::sync::atomic::AtomicBool::new(false),
536 })
537 }
538
539 pub fn bound(&self) -> f64 {
541 self.bound
542 }
543
544 pub fn get(
546 &self,
547 ctx: &crate::EvalCtx<'_>,
548 inputs: &[Option<crate::PortValue>],
549 ) -> Result<f64, crate::EvalError> {
550 let raw = self.value.get(ctx, inputs)?;
551 if raw > self.bound {
552 if !self
553 .clamped
554 .swap(true, std::sync::atomic::Ordering::Relaxed)
555 {
556 tracing::warn!(
557 "`{}`: {raw} exceeds the {} the canvas was padded for; clamping. \
558 Raise `{}-max` to let it through.",
559 self.field,
560 self.bound,
561 self.field,
562 );
563 }
564 return Ok(self.bound);
565 }
566 Ok(raw)
567 }
568
569 pub fn param_hash(&self, h: &mut xxhash_rust::xxh3::Xxh3) {
570 self.value.param_hash(h);
571 h.update(&self.bound.to_le_bytes());
572 }
573
574 pub fn param_refs(&self) -> Vec<String> {
575 match &self.value {
576 In::Param { name, .. } => vec![name.clone()],
577 _ => Vec::new(),
578 }
579 }
580}