use super::size_sync::{self, fitted_size};
use crate::widget::node_inspector;
use crate::widget::node_inspector::radio_option;
use crate::{
ContextMenuResponse, InspectorRowsResponse, NodeCtx, NodeUi, NodeUiResponse, NodeViewResponse,
Registry, SocketDoc, SocketKind,
};
use gantz_ca::CaHash;
use gantz_core::node::{self, ExprCtx, ExprResult, MetaCtx, RegCtx};
use gantz_nodetag::NodeTag;
use serde::{Deserialize, Serialize};
use steel::gc::Gc;
use steel::steel_vm::register_fn::RegisterFn;
use steel::{SteelVal, Vector};
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
#[serde(transparent)]
pub struct F32(pub f32);
impl F32 {
fn get(self) -> f32 {
self.0
}
}
impl std::hash::Hash for F32 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::hash::Hash::hash(&self.0.to_bits(), state);
}
}
impl CaHash for F32 {
fn hash(&self, hasher: &mut gantz_ca::Hasher) {
CaHash::hash(&self.0.to_bits(), hasher);
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize, CaHash)]
pub enum PlotMode {
Scope,
Signal,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize, CaHash)]
pub enum PlotStyle {
Bars,
Line,
}
#[derive(Clone, Debug, Hash, Deserialize, Serialize, CaHash, NodeTag)]
#[cahash("gantz.plot")]
pub struct Plot {
mode: PlotMode,
style: PlotStyle,
capacity: u32,
width: u16,
height: u16,
color: Option<[u8; 4]>,
show_grid: bool,
show_axes: bool,
interactive: bool,
margin: bool,
y_min: Option<F32>,
y_max: Option<F32>,
}
impl Plot {
pub const DEFAULT_SIZE: [u16; 2] = [120, 80];
pub const DEFAULT_CAPACITY: u32 = 256;
}
impl Default for Plot {
fn default() -> Self {
Self {
mode: PlotMode::Scope,
style: PlotStyle::Bars,
capacity: Self::DEFAULT_CAPACITY,
width: Self::DEFAULT_SIZE[0],
height: Self::DEFAULT_SIZE[1],
color: None,
show_grid: false,
show_axes: false,
interactive: false,
margin: true,
y_min: None,
y_max: None,
}
}
}
fn plot_push(state: SteelVal, val: SteelVal, cap: SteelVal) -> SteelVal {
let cap = match cap {
SteelVal::IntV(n) if n > 0 => n as usize,
_ => 0,
};
if let Some(chans) = per_channel_elems(&val) {
let old: Vec<SteelVal> = match &state {
SteelVal::VectorV(v) if v.iter().any(is_container) => v.iter().cloned().collect(),
SteelVal::ListV(l) if l.iter().any(is_container) => l.iter().cloned().collect(),
_ => Vec::new(),
};
let channels: Vector<SteelVal> = chans
.iter()
.enumerate()
.map(|(c, ch)| {
let history = push_capped(history_of(old.get(c)), ch, cap);
SteelVal::VectorV(Gc::new(history).into())
})
.collect();
return SteelVal::VectorV(Gc::new(channels).into());
}
let history = push_capped(history_of(Some(&state)), &val, cap);
SteelVal::VectorV(Gc::new(history).into())
}
fn is_num(v: &SteelVal) -> bool {
matches!(v, SteelVal::NumV(_) | SteelVal::IntV(_))
}
fn per_channel_elems(val: &SteelVal) -> Option<Vec<SteelVal>> {
let elems: Vec<SteelVal> = match val {
SteelVal::ListV(list) => list.iter().cloned().collect(),
SteelVal::VectorV(vec) => vec.iter().cloned().collect(),
_ => return None,
};
elems.iter().any(is_container).then_some(elems)
}
fn history_of(state: Option<&SteelVal>) -> Vector<SteelVal> {
match state {
Some(SteelVal::VectorV(v)) if !v.iter().any(is_container) => (**v).clone(),
Some(SteelVal::ListV(list)) => list.iter().filter(|v| is_num(v)).cloned().collect(),
_ => Vector::new(),
}
}
fn push_capped(mut history: Vector<SteelVal>, val: &SteelVal, cap: usize) -> Vector<SteelVal> {
match val {
SteelVal::ListV(items) => {
for v in items.iter().filter(|v| is_num(v)) {
history.push_back(v.clone());
}
}
SteelVal::VectorV(items) => {
for v in items.iter().filter(|v| is_num(v)) {
history.push_back(v.clone());
}
}
num @ (SteelVal::NumV(_) | SteelVal::IntV(_)) => history.push_back(num.clone()),
_ => {}
}
while history.len() > cap {
history.pop_front();
}
history
}
fn series(ctx: &NodeCtx) -> Vec<Vec<f64>> {
match ctx.extract_value() {
Ok(Some(val)) => split_channels(&val),
_ => Vec::new(),
}
}
fn split_channels(val: &SteelVal) -> Vec<Vec<f64>> {
let elems: Option<Vec<&SteelVal>> = match val {
SteelVal::ListV(list) => Some(list.iter().collect()),
SteelVal::VectorV(vec) => Some(vec.iter().collect()),
_ => None,
};
match elems {
Some(elems) if elems.iter().any(|v| is_container(v)) => {
elems.iter().map(|v| channel_numerics(v)).collect()
}
Some(elems) => vec![elems.iter().filter_map(|v| steel_num(v)).collect()],
None => vec![steel_num(val).into_iter().collect()],
}
}
fn is_container(v: &SteelVal) -> bool {
matches!(v, SteelVal::ListV(_) | SteelVal::VectorV(_))
}
fn channel_numerics(val: &SteelVal) -> Vec<f64> {
match val {
SteelVal::ListV(list) => list.iter().filter_map(steel_num).collect(),
SteelVal::VectorV(vec) => vec.iter().filter_map(steel_num).collect(),
other => steel_num(other).into_iter().collect(),
}
}
fn steel_num(val: &SteelVal) -> Option<f64> {
match val {
SteelVal::NumV(f) => Some(*f),
SteelVal::IntV(i) => Some(*i as f64),
_ => None,
}
}
fn resolve_color(color: Option<[u8; 4]>, ui: &egui::Ui) -> egui::Color32 {
match color {
Some([r, g, b, a]) => egui::Color32::from_rgba_unmultiplied(r, g, b, a),
None => ui.visuals().strong_text_color(),
}
}
impl gantz_core::Node for Plot {
fn n_inputs(&self, _ctx: MetaCtx) -> usize {
1
}
fn n_outputs(&self, _ctx: MetaCtx) -> usize {
1
}
fn stateful(&self, _ctx: MetaCtx) -> bool {
true
}
fn expr(&self, ctx: ExprCtx<'_, '_>) -> ExprResult {
let expr = match ctx.inputs().get(0) {
Some(Some(val)) => match self.mode {
PlotMode::Scope => format!(
"(begin (set! state (plot-push state {val} {cap})) {val})",
cap = self.capacity,
),
PlotMode::Signal => format!("(begin (set! state {val}) {val})"),
},
_ => "(begin state)".to_string(),
};
node::parse_expr(&expr)
}
fn register(&self, mut ctx: RegCtx<'_, '_>) {
let path = ctx.path();
node::state::init_value_if_absent(ctx.vm(), path, || {
SteelVal::VectorV(std::iter::empty::<SteelVal>().collect())
})
.unwrap();
if ctx.vm().extract_value("plot-push").is_err() {
ctx.vm().register_fn("plot-push", plot_push);
}
}
}
impl Plot {
fn plot_body(
&self,
channels: &[Vec<f64>],
plot_id: egui::Id,
size: egui::Vec2,
ui: &mut egui::Ui,
) -> egui::Response {
if channels.len() <= 1 {
let ys = channels.first().map(Vec::as_slice).unwrap_or(&[]);
return self.plot_channel(ys, plot_id, size, ui);
}
let sub_h = size.y / channels.len() as f32;
ui.vertical(|ui| {
let mut resp: Option<egui::Response> = None;
for (i, ch) in channels.iter().enumerate() {
let r = self.plot_channel(ch, plot_id.with(i), egui::vec2(size.x, sub_h), ui);
resp = Some(match resp.take() {
Some(prev) => prev.union(r),
None => r,
});
}
resp.expect("at least two channels")
})
.inner
}
fn plot_channel(
&self,
ys: &[f64],
plot_id: egui::Id,
size: egui::Vec2,
ui: &mut egui::Ui,
) -> egui::Response {
let color = resolve_color(self.color, ui);
let plot_style = self.style;
let interactive = self.interactive;
let bounds = value_bounds(ys, plot_style, self.y_min, self.y_max);
let mut plot = egui_plot::Plot::new(plot_id)
.width(size.x)
.height(size.y)
.show_background(false)
.show_axes(egui::Vec2b::new(self.show_axes, self.show_axes))
.show_grid(egui::Vec2b::new(self.show_grid, self.show_grid))
.allow_drag(false)
.allow_zoom(false)
.allow_scroll(false)
.allow_boxed_zoom(false)
.sense(egui::Sense::hover());
if !interactive {
plot = plot.cursor_color(egui::Color32::TRANSPARENT);
}
let plot_resp = plot
.show(ui, |plot_ui| {
match plot_style {
PlotStyle::Bars => {
let bars = ys
.iter()
.enumerate()
.map(|(i, &y)| {
egui_plot::Bar::new(i as f64, y)
.width(1.0)
.fill(color)
.stroke(egui::Stroke::NONE)
})
.collect();
plot_ui
.bar_chart(egui_plot::BarChart::new("", bars).allow_hover(interactive));
}
PlotStyle::Line => {
let points = egui_plot::PlotPoints::from_ys_f64(ys);
plot_ui.line(
egui_plot::Line::new("", points)
.color(color)
.allow_hover(interactive),
);
}
}
let ([xlo, ylo], [xhi, yhi]) = bounds;
plot_ui.set_plot_bounds_x(xlo..=xhi);
plot_ui.set_plot_bounds_y(ylo..=yhi);
})
.response;
if !interactive && plot_resp.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::Default);
}
plot_resp
}
}
impl NodeUi for Plot {
fn name(&self, _: &dyn Registry) -> &str {
"plot"
}
fn description(&self) -> Option<&'static str> {
Some("Plot incoming values as a scrolling scope or a signal/array")
}
fn ui(&mut self, ctx: NodeCtx, uictx: egui_graph::NodeCtx) -> NodeUiResponse {
let mut changed = false;
let style = uictx.style();
let interaction = uictx.interaction();
let mut frame = egui_graph::node::default_frame(style, interaction);
frame.fill = style.visuals.extreme_bg_color;
if !self.margin {
frame.inner_margin = egui::Margin::ZERO;
frame.corner_radius = egui::CornerRadius::ZERO;
}
let node_egui_id = uictx.egui_id();
let resize_id = node_egui_id.with("resize");
let plot_id = node_egui_id.with("plot");
let min_size = egui::Vec2::splat(style.interaction.interact_radius * 2.0);
let default_size = egui::vec2(self.width as f32, self.height as f32);
let ys = series(&ctx);
let size_sync_id = node_egui_id.with("size_sync");
let framed = uictx.framed_with(frame, |ui, _sockets| {
let size_sync::Decisions {
resizing,
push_external,
drag_released,
} = size_sync::begin(ui, size_sync_id, resize_id, [self.width, self.height]);
let resize = egui::containers::Resize::default()
.id(resize_id)
.with_stroke(false);
let resize = if push_external {
ui.ctx().request_repaint();
let w = (self.width as f32).max(min_size.x);
let h = (self.height as f32).max(min_size.y);
resize.fixed_size(egui::vec2(w, h))
} else {
let resizable = egui::Vec2b::new(interaction.selected, interaction.selected);
resize
.resizable(resizable)
.default_size(default_size)
.min_size(min_size)
};
let inner = resize.show(ui, |ui| {
let avail = ui.available_size();
let fitted = fitted_size(avail.x.max(min_size.x), avail.y.max(min_size.y));
if drag_released && [self.width, self.height] != fitted {
[self.width, self.height] = fitted;
changed = true;
}
self.plot_body(&ys, plot_id, avail, ui)
});
size_sync::store(
ui,
size_sync_id,
[self.width, self.height],
push_external,
resizing,
);
inner
});
let mut resp = NodeUiResponse::new(framed);
resp.set_changed(changed);
resp
}
fn view_no_margin(&self) -> bool {
true
}
fn view_ui(&mut self, ctx: NodeCtx, ui: &mut egui::Ui) -> NodeViewResponse {
let plot_id = ui.id().with("plot-view");
let ys = series(&ctx);
let size = ui.available_size();
let resp = self.plot_body(&ys, plot_id, size, ui);
let mut out = NodeViewResponse::default();
out.inner = Some(resp);
out
}
fn inspector_rows(
&mut self,
ctx: &mut NodeCtx,
body: &mut egui_extras::TableBody,
) -> InspectorRowsResponse {
let row_h = node_inspector::table_row_h(body.ui_mut());
let mut changed = false;
let chans = series(ctx);
let total: usize = chans.iter().map(Vec::len).sum();
let summary = if chans.len() > 1 {
format!("{total} samples · {} channels", chans.len())
} else {
format!("{total} samples")
};
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("state");
});
row.col(|ui| {
ui.label(summary);
});
});
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("mode");
});
row.col(|ui| {
ui.horizontal(|ui| {
changed |= radio_option(
ui,
&mut self.mode,
PlotMode::Scope,
"scope",
"accumulate a scrolling history",
);
changed |= radio_option(
ui,
&mut self.mode,
PlotMode::Signal,
"signal",
"plot the incoming value directly",
);
});
});
});
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("style");
});
row.col(|ui| {
ui.horizontal(|ui| {
changed |= radio_option(
ui,
&mut self.style,
PlotStyle::Bars,
"bars",
"draw as contiguous bars",
);
changed |= radio_option(
ui,
&mut self.style,
PlotStyle::Line,
"line",
"draw as a connected line",
);
});
});
});
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("capacity");
});
row.col(|ui| {
let mut c = self.capacity as i32;
if ui
.add(egui::DragValue::new(&mut c).range(1..=4096).speed(1.0))
.on_hover_text("max samples retained in scope mode")
.changed()
{
self.capacity = c.clamp(1, 4096) as u32;
changed = true;
}
});
});
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("margin");
});
row.col(|ui| {
if ui
.checkbox(&mut self.margin, "")
.on_hover_text(
"inset the data within the node frame's margin (rounded corners)",
)
.changed()
{
changed = true;
}
});
});
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("colour");
});
row.col(|ui| {
ui.horizontal(|ui| {
let mut col = resolve_color(self.color, ui);
if ui
.color_edit_button_srgba(&mut col)
.on_hover_text("the line/bar colour")
.changed()
{
self.color = Some([col.r(), col.g(), col.b(), col.a()]);
changed = true;
}
if self.color.is_some()
&& ui
.button("theme")
.on_hover_text("follow the theme's strong text colour")
.clicked()
{
self.color = None;
changed = true;
}
});
});
});
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("range");
});
row.col(|ui| {
egui::Grid::new("plot_range").num_columns(2).show(ui, |ui| {
let mut y_min = self.y_min.map(F32::get);
if node_inspector::bound_col(ui, "minimum", &mut y_min) {
self.y_min = y_min.map(F32);
changed = true;
}
let mut y_max = self.y_max.map(F32::get);
if node_inspector::bound_col(ui, "maximum", &mut y_max) {
self.y_max = y_max.map(F32);
changed = true;
}
ui.end_row();
});
});
});
body.row(row_h, |mut row| {
row.col(|ui| {
ui.label("display");
});
row.col(|ui| {
ui.horizontal(|ui| {
changed |= ui
.checkbox(&mut self.show_grid, "grid")
.on_hover_text("draw the background grid")
.changed();
changed |= ui
.checkbox(&mut self.show_axes, "axes")
.on_hover_text("draw the axes")
.changed();
changed |= ui
.checkbox(&mut self.interactive, "interactive")
.on_hover_text("show a crosshair and value readout on hover")
.changed();
});
});
});
let mut resp = InspectorRowsResponse::default();
resp.set_changed(changed);
resp
}
fn context_menu(&mut self, ctx: &mut NodeCtx, ui: &mut egui::Ui) -> ContextMenuResponse {
if ui
.button("clear history")
.on_hover_text("empty the plotted series")
.clicked()
{
ctx.update_value(SteelVal::VectorV(std::iter::empty::<SteelVal>().collect()))
.ok();
ui.close();
}
ContextMenuResponse::default()
}
fn socket_doc(&self, _: &dyn Registry, kind: SocketKind, _ix: usize) -> Option<SocketDoc> {
Some(match kind {
SocketKind::Input => SocketDoc::ty("number or list").with_description(
"scope: a number (or list) appended to the history; signal: the value to plot",
),
SocketKind::Output => {
SocketDoc::ty("any").with_description("the input value, unchanged")
}
})
}
fn show_state(&self) -> bool {
false
}
}
fn value_bounds(
ys: &[f64],
style: PlotStyle,
y_min: Option<F32>,
y_max: Option<F32>,
) -> ([f64; 2], [f64; 2]) {
let n = ys.len() as f64;
let (xlo, xhi) = match style {
PlotStyle::Bars => (-0.5, (n - 0.5).max(0.5)),
PlotStyle::Line => (0.0, (n - 1.0).max(1.0)),
};
let (dmin, dmax) = ys
.iter()
.copied()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| {
(lo.min(v), hi.max(v))
});
let (mut ylo, mut yhi) = if dmin <= dmax {
match style {
PlotStyle::Bars => (dmin.min(0.0), dmax.max(0.0)),
PlotStyle::Line => (dmin, dmax),
}
} else {
(0.0, 1.0)
};
if (yhi - ylo).abs() < 1e-9 {
ylo -= 1.0;
yhi += 1.0;
}
if let Some(v) = y_min {
ylo = v.get() as f64;
}
if let Some(v) = y_max {
yhi = v.get() as f64;
}
([xlo, ylo], [xhi, yhi])
}
#[cfg(test)]
mod tests {
use super::*;
use gantz_core::node::{Node, WithPushEval};
use gantz_core::{
Edge, ROOT_STATE,
compile::{entry_fn_name, entrypoint, push_pull_entrypoints},
};
use steel::steel_vm::engine::Engine;
fn no_lookup(_: &gantz_ca::ContentAddr) -> Option<&'static dyn Node> {
None
}
fn vm_for(g: &petgraph::graph::DiGraph<Box<dyn Node>, Edge>) -> Engine {
let eps = push_pull_entrypoints(&no_lookup, g);
let module = gantz_core::compile::module(&no_lookup, g, &eps, &Default::default()).unwrap();
let mut vm = Engine::new_base();
vm.register_value(ROOT_STATE, SteelVal::empty_hashmap());
gantz_core::graph::register(&no_lookup, g, &[], &mut vm);
for f in module {
vm.run(format!("{f}")).unwrap();
}
vm
}
fn fire(
vm: &mut Engine,
g: &petgraph::graph::DiGraph<Box<dyn Node>, Edge>,
ix: usize,
n: usize,
) {
let ctx = node::MetaCtx::new(&no_lookup);
let outs = g[petgraph::graph::NodeIndex::new(ix)].n_outputs(ctx) as u8;
let ep = entrypoint::push(vec![ix], outs);
let fn_name = entry_fn_name(&ep.id());
for _ in 0..n {
vm.call_function_by_name_with_args(&fn_name, vec![])
.unwrap();
}
}
fn samples_of(vm: &Engine, ix: usize) -> Vec<f64> {
match node::state::extract_value(vm, &[ix]).unwrap().unwrap() {
SteelVal::ListV(list) => list.iter().filter_map(steel_num).collect(),
SteelVal::VectorV(vec) => vec.iter().filter_map(steel_num).collect(),
other => panic!("expected list/vector state, got {other:?}"),
}
}
fn graph_with(
src: Box<dyn Node>,
plot: Plot,
) -> (petgraph::graph::DiGraph<Box<dyn Node>, Edge>, usize, usize) {
let mut g = petgraph::graph::DiGraph::new();
let s = g.add_node(src);
let p = g.add_node(Box::new(plot) as Box<dyn Node>);
g.add_edge(s, p, Edge::from((0, 0)));
(g, s.index(), p.index())
}
#[test]
fn scope_accumulates_bounded_history() {
let src = gantz_core::node::expr("5").unwrap().with_push_eval();
let plot = Plot {
mode: PlotMode::Scope,
capacity: 3,
..Default::default()
};
let (g, s, p) = graph_with(Box::new(src) as Box<dyn Node>, plot);
let mut vm = vm_for(&g);
fire(&mut vm, &g, s, 5);
assert_eq!(samples_of(&vm, p), vec![5.0, 5.0, 5.0]);
}
#[test]
fn scope_extends_with_list() {
let src = gantz_core::node::expr("(list 1 2 3)")
.unwrap()
.with_push_eval();
let plot = Plot {
mode: PlotMode::Scope,
capacity: 10,
..Default::default()
};
let (g, s, p) = graph_with(Box::new(src) as Box<dyn Node>, plot);
let mut vm = vm_for(&g);
fire(&mut vm, &g, s, 2);
assert_eq!(samples_of(&vm, p), vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
}
#[test]
fn scope_list_over_capacity_keeps_tail() {
let src = gantz_core::node::expr("(list 1 2 3 4 5)")
.unwrap()
.with_push_eval();
let plot = Plot {
mode: PlotMode::Scope,
capacity: 3,
..Default::default()
};
let (g, s, p) = graph_with(Box::new(src) as Box<dyn Node>, plot);
let mut vm = vm_for(&g);
fire(&mut vm, &g, s, 1);
assert_eq!(samples_of(&vm, p), vec![3.0, 4.0, 5.0]);
fire(&mut vm, &g, s, 1);
assert_eq!(samples_of(&vm, p), vec![3.0, 4.0, 5.0]);
}
#[test]
fn scope_accumulates_per_channel_histories() {
let src = gantz_core::node::expr("(list (list 1 2) (list -1 -2))")
.unwrap()
.with_push_eval();
let plot = Plot {
mode: PlotMode::Scope,
capacity: 3,
..Default::default()
};
let (g, s, p) = graph_with(Box::new(src) as Box<dyn Node>, plot);
let mut vm = vm_for(&g);
fire(&mut vm, &g, s, 2);
let state = node::state::extract_value(&vm, &[p]).unwrap().unwrap();
assert_eq!(
split_channels(&state),
vec![vec![2.0, 1.0, 2.0], vec![-2.0, -1.0, -2.0]],
);
}
#[test]
fn scope_shape_switch_discards_prior_history() {
let num = |n: f64| SteelVal::NumV(n);
let list = |vals: Vec<SteelVal>| SteelVal::ListV(vals.into_iter().collect());
let cap = SteelVal::IntV(8);
let flat = plot_push(SteelVal::Void, num(1.0), cap.clone());
let chans = plot_push(flat, list(vec![list(vec![num(2.0)])]), cap.clone());
assert_eq!(split_channels(&chans), vec![vec![2.0]]);
let flat_again = plot_push(chans, num(3.0), cap);
assert_eq!(split_channels(&flat_again), vec![vec![3.0]]);
}
#[test]
fn scope_list_trims_oldest_to_cap() {
let src = gantz_core::node::expr("(list 1 2 3)")
.unwrap()
.with_push_eval();
let plot = Plot {
mode: PlotMode::Scope,
capacity: 4,
..Default::default()
};
let (g, s, p) = graph_with(Box::new(src) as Box<dyn Node>, plot);
let mut vm = vm_for(&g);
fire(&mut vm, &g, s, 2);
assert_eq!(samples_of(&vm, p), vec![3.0, 1.0, 2.0, 3.0]);
}
#[test]
fn split_channels_by_shape() {
let num = |n: f64| SteelVal::NumV(n);
let list = |xs: Vec<SteelVal>| SteelVal::ListV(xs.into_iter().collect());
let vector = |xs: Vec<SteelVal>| SteelVal::VectorV(xs.into_iter().collect());
assert_eq!(
split_channels(&list(vec![num(1.0), num(2.0), num(3.0)])),
vec![vec![1.0, 2.0, 3.0]],
);
assert_eq!(
split_channels(&vector(vec![num(1.0), num(2.0), num(3.0)])),
vec![vec![1.0, 2.0, 3.0]],
);
assert_eq!(split_channels(&num(7.0)), vec![vec![7.0]]);
let expected = vec![vec![1.0, 3.0], vec![2.0, 4.0]];
assert_eq!(
split_channels(&list(vec![
list(vec![num(1.0), num(3.0)]),
list(vec![num(2.0), num(4.0)]),
])),
expected,
);
assert_eq!(
split_channels(&vector(vec![
vector(vec![num(1.0), num(3.0)]),
vector(vec![num(2.0), num(4.0)]),
])),
expected,
);
assert_eq!(
split_channels(&list(vec![
vector(vec![num(1.0), num(3.0)]),
vector(vec![num(2.0), num(4.0)]),
])),
expected,
);
}
#[test]
fn plot_push_accepts_vector() {
let num = |n: f64| SteelVal::NumV(n);
let vector = |xs: Vec<SteelVal>| SteelVal::VectorV(xs.into_iter().collect());
let empty = SteelVal::VectorV(std::iter::empty::<SteelVal>().collect());
let s1 = plot_push(
empty,
vector(vec![num(1.0), num(2.0), num(3.0)]),
SteelVal::IntV(4),
);
let s2 = plot_push(s1, vector(vec![num(4.0), num(5.0)]), SteelVal::IntV(4));
let got: Vec<f64> = match s2 {
SteelVal::VectorV(v) => v.iter().filter_map(steel_num).collect(),
other => panic!("expected vector state, got {other:?}"),
};
assert_eq!(got, vec![2.0, 3.0, 4.0, 5.0]);
}
#[test]
fn signal_stores_list() {
let src = gantz_core::node::expr("(list 1 2 3)")
.unwrap()
.with_push_eval();
let plot = Plot {
mode: PlotMode::Signal,
..Default::default()
};
let (g, s, p) = graph_with(Box::new(src) as Box<dyn Node>, plot);
let mut vm = vm_for(&g);
fire(&mut vm, &g, s, 1);
assert_eq!(samples_of(&vm, p), vec![1.0, 2.0, 3.0]);
}
#[test]
fn signal_stores_scalar() {
let src = gantz_core::node::expr("7").unwrap().with_push_eval();
let plot = Plot {
mode: PlotMode::Signal,
..Default::default()
};
let (g, s, p) = graph_with(Box::new(src) as Box<dyn Node>, plot);
let mut vm = vm_for(&g);
fire(&mut vm, &g, s, 1);
let state = node::state::extract_value(&vm, &[p]).unwrap().unwrap();
assert!(matches!(state, SteelVal::IntV(7)));
}
#[test]
fn re_registration_keeps_plot_push_working() {
let src = gantz_core::node::expr("5").unwrap().with_push_eval();
let plot = Plot {
mode: PlotMode::Scope,
capacity: 3,
..Default::default()
};
let (g, s, p) = graph_with(Box::new(src) as Box<dyn Node>, plot);
let mut vm = vm_for(&g);
gantz_core::graph::register(&no_lookup, &g, &[], &mut vm);
fire(&mut vm, &g, s, 5);
assert_eq!(samples_of(&vm, p), vec![5.0, 5.0, 5.0]);
}
}