use gantz_ca::CaHash;
use gantz_core::node::{EvalConf, ExprCtx, ExprResult, MetaCtx, RegCtx};
use gantz_core::steel::SteelVal;
use gantz_nodetag::NodeTag;
use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
#[derive(Clone, Debug, Serialize, Deserialize, NodeTag)]
pub struct Number {
#[serde(default)]
min: Option<f64>,
#[serde(default)]
max: Option<f64>,
#[serde(default)]
precision: Option<u8>,
#[serde(default = "default_push_eval")]
push_eval_on_edit: bool,
}
impl Number {
pub fn min(&self) -> Option<f64> {
self.min
}
pub fn max(&self) -> Option<f64> {
self.max
}
pub fn precision(&self) -> Option<u8> {
self.precision
}
pub fn push_eval_on_edit(&self) -> bool {
self.push_eval_on_edit
}
pub fn set_min(&mut self, min: Option<f64>) {
self.min = min;
}
pub fn set_max(&mut self, max: Option<f64>) {
self.max = max;
}
pub fn set_precision(&mut self, precision: Option<u8>) {
self.precision = precision;
}
pub fn set_push_eval_on_edit(&mut self, push_eval_on_edit: bool) {
self.push_eval_on_edit = push_eval_on_edit;
}
pub fn clamp(&self, v: f64) -> f64 {
let v = self.min.map_or(v, |lo| v.max(lo));
self.max.map_or(v, |hi| v.min(hi))
}
}
impl Default for Number {
fn default() -> Self {
Number {
min: None,
max: None,
precision: None,
push_eval_on_edit: true,
}
}
}
impl PartialEq for Number {
fn eq(&self, other: &Self) -> bool {
self.min.map(f64::to_bits) == other.min.map(f64::to_bits)
&& self.max.map(f64::to_bits) == other.max.map(f64::to_bits)
&& self.precision == other.precision
&& self.push_eval_on_edit == other.push_eval_on_edit
}
}
impl Eq for Number {}
impl Hash for Number {
fn hash<H: Hasher>(&self, state: &mut H) {
Hash::hash(&self.min.map(f64::to_bits), state);
Hash::hash(&self.max.map(f64::to_bits), state);
Hash::hash(&self.precision, state);
Hash::hash(&self.push_eval_on_edit, state);
}
}
impl CaHash for Number {
fn hash(&self, hasher: &mut gantz_ca::Hasher) {
hasher.update("gantz.number".as_bytes());
if let Some(min) = self.min {
hasher.update(b"min");
CaHash::hash(&min.to_bits(), hasher);
}
if let Some(max) = self.max {
hasher.update(b"max");
CaHash::hash(&max.to_bits(), hasher);
}
if let Some(precision) = self.precision {
hasher.update(b"precision");
CaHash::hash(&precision, hasher);
}
if !self.push_eval_on_edit {
hasher.update(b"no-push-eval");
}
}
}
impl gantz_core::Node for Number {
fn n_inputs(&self, _ctx: MetaCtx) -> usize {
1
}
fn n_outputs(&self, _ctx: MetaCtx) -> usize {
1
}
fn push_eval(&self, _ctx: MetaCtx) -> Vec<EvalConf> {
vec![EvalConf::All]
}
fn expr(&self, ctx: ExprCtx<'_, '_>) -> ExprResult {
let expr = match ctx.inputs().get(0) {
Some(Some(val)) => {
let stored = clamp_steel(val, self.min, self.max);
format!("(begin (if (number? {val}) (set! state {stored}) void) state)")
}
_ => "(begin state)".to_string(),
};
gantz_core::node::parse_expr(&expr)
}
fn stateful(&self, _ctx: MetaCtx) -> bool {
true
}
fn register(&self, mut ctx: RegCtx<'_, '_>) {
let path = ctx.path();
let init = self.clamp(0.0);
gantz_core::node::state::init_value_if_absent(ctx.vm(), path, || SteelVal::NumV(init))
.unwrap()
}
}
fn default_push_eval() -> bool {
true
}
fn clamp_steel(val: &str, min: Option<f64>, max: Option<f64>) -> String {
match (min, max) {
(None, None) => val.to_string(),
(Some(lo), None) => format!("(let ((v {val})) (if (< v {lo:?}) {lo:?} v))"),
(None, Some(hi)) => format!("(let ((v {val})) (if (> v {hi:?}) {hi:?} v))"),
(Some(lo), Some(hi)) => {
format!("(let ((v {val})) (if (< v {lo:?}) {lo:?} (if (> v {hi:?}) {hi:?} v)))")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clamp_bounds() {
let n = Number {
min: Some(0.0),
max: Some(10.0),
precision: None,
push_eval_on_edit: true,
};
assert_eq!(n.clamp(-5.0), 0.0);
assert_eq!(n.clamp(5.0), 5.0);
assert_eq!(n.clamp(15.0), 10.0);
let lo = Number {
min: Some(3.0),
..Number::default()
};
assert_eq!(lo.clamp(1.0), 3.0);
assert_eq!(lo.clamp(100.0), 100.0);
}
#[test]
fn clamp_steel_forms() {
assert_eq!(clamp_steel("x", None, None), "x");
assert_eq!(
clamp_steel("x", Some(0.0), None),
"(let ((v x)) (if (< v 0.0) 0.0 v))",
);
assert_eq!(
clamp_steel("x", None, Some(10.0)),
"(let ((v x)) (if (> v 10.0) 10.0 v))",
);
assert_eq!(
clamp_steel("x", Some(0.0), Some(10.0)),
"(let ((v x)) (if (< v 0.0) 0.0 (if (> v 10.0) 10.0 v)))",
);
}
}