use darkly::brush::eval::BrushGraphRunner;
use darkly::brush::paint_info::PaintInformation;
use darkly::brush::registry;
use darkly::brush::wire::ScalarValue;
use darkly::gpu::params::ParamValue;
use darkly::nodegraph::{Graph, PortDef, PortRef};
fn random_params() -> Vec<ParamValue> {
vec![ParamValue::Int(0)]
}
fn read_scalar(runner: &BrushGraphRunner, type_id: &str, port: &str) -> f32 {
let slot = runner
.find_output_slot(type_id, port)
.unwrap_or_else(|| panic!("no slot for {type_id}.{port}"));
match runner.read_slot(slot).expect("slot has value") {
ScalarValue::Scalar(v) => v,
other => panic!("expected Scalar, got {other:?}"),
}
}
fn run_one_dab(runner: &mut BrushGraphRunner, pressure: f32, dab_index: u32) {
let info = PaintInformation {
pressure,
..Default::default()
};
runner.seed_sensors(&info, [0.0, 0.0, 0.0, 1.0], 42, dab_index);
runner.clear_slots();
runner.seed_sensors(&info, [0.0, 0.0, 0.0, 1.0], 42, dab_index);
runner.execute_cpu();
}
#[test]
fn random_outputs_unit_range_and_varies_per_dab() {
let registry = registry();
let mut graph = Graph::new();
let random_reg = registry.get("random").unwrap();
graph.add_node("random", random_reg.ports.clone(), random_params());
let mut runner =
BrushGraphRunner::new(&graph, registry.as_map(), registry.evaluators()).unwrap();
let mut samples = Vec::with_capacity(16);
for dab in 0..16 {
run_one_dab(&mut runner, 0.5, dab);
let v = read_scalar(&runner, "random", "value");
assert!(
(0.0..1.0).contains(&v),
"random output {v} outside [0, 1) on dab {dab}",
);
samples.push(v);
}
let mut sorted = samples.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
sorted.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
assert!(
sorted.len() >= 10,
"expected ≥10 distinct random values over 16 dabs, got {} (samples: {:?})",
sorted.len(),
samples,
);
}
#[test]
fn random_to_wide_range_input_remaps() {
let registry = registry();
let mut graph = Graph::new();
let random_reg = registry.get("random").unwrap();
let random = graph.add_node("random", random_reg.ports.clone(), random_params());
let multiply_reg = registry.get("multiply").unwrap();
let mut multiply_ports = multiply_reg.ports.clone();
for p in multiply_ports.iter_mut() {
if p.name == "a" {
*p = std::mem::replace(p, PortDef::input("placeholder", p.wire_type))
.with_natural_range(0.0, 1024.0);
}
}
let multiply = graph.add_node("multiply", multiply_ports, vec![]);
graph
.connect(
PortRef {
node: random,
port: "value".into(),
},
PortRef {
node: multiply,
port: "a".into(),
},
)
.unwrap();
let mut runner =
BrushGraphRunner::new(&graph, registry.as_map(), registry.evaluators()).unwrap();
let mut samples = Vec::new();
for dab in 0..16 {
run_one_dab(&mut runner, 0.5, dab);
let v = read_scalar(&runner, "multiply", "result");
assert!(
(0.0..=1024.0).contains(&v),
"multiply.result {v} outside [0, 1024] on dab {dab}",
);
samples.push(v);
}
let min = samples.iter().cloned().fold(f32::INFINITY, f32::min);
let max = samples.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
assert!(
max - min > 500.0,
"expected wide spread across [0, 1024], got [{min}, {max}] (samples {samples:?})",
);
}
#[test]
fn pen_pressure_to_frequency_range_remaps() {
let registry = registry();
let mut graph = Graph::new();
let pen_reg = registry.get("pen_input").unwrap();
let pen = graph.add_node("pen_input", pen_reg.ports.clone(), vec![]);
let multiply_reg = registry.get("multiply").unwrap();
let mut multiply_ports = multiply_reg.ports.clone();
for p in multiply_ports.iter_mut() {
if p.name == "a" {
*p = std::mem::replace(p, PortDef::input("placeholder", p.wire_type))
.with_natural_range(1.0, 16.0);
}
}
let multiply = graph.add_node("multiply", multiply_ports, vec![]);
graph
.connect(
PortRef {
node: pen,
port: "pressure".into(),
},
PortRef {
node: multiply,
port: "a".into(),
},
)
.unwrap();
let mut runner =
BrushGraphRunner::new(&graph, registry.as_map(), registry.evaluators()).unwrap();
run_one_dab(&mut runner, 0.0, 0);
let v = read_scalar(&runner, "multiply", "result");
assert!(
(v - 1.0).abs() < 1e-4,
"pressure 0 → a expected 1.0, got {v}"
);
run_one_dab(&mut runner, 0.5, 1);
let v = read_scalar(&runner, "multiply", "result");
assert!(
(v - 8.5).abs() < 1e-4,
"pressure 0.5 → a expected 8.5, got {v}"
);
run_one_dab(&mut runner, 1.0, 2);
let v = read_scalar(&runner, "multiply", "result");
assert!(
(v - 16.0).abs() < 1e-4,
"pressure 1.0 → a expected 16.0, got {v}"
);
}
#[test]
fn math_node_output_passes_through_to_ranged_input() {
let registry = registry();
let mut graph = Graph::new();
let multiply_src_reg = registry.get("multiply").unwrap();
let mut multiply_src_ports = multiply_src_reg.ports.clone();
for p in multiply_src_ports.iter_mut() {
if p.name == "a" || p.name == "b" {
p.default = 0.5;
}
}
let multiply_src = graph.add_node("multiply", multiply_src_ports, vec![]);
let multiply_sink_reg = registry.get("multiply").unwrap();
let mut multiply_sink_ports = multiply_sink_reg.ports.clone();
for p in multiply_sink_ports.iter_mut() {
if p.name == "a" {
*p = std::mem::replace(p, PortDef::input("placeholder", p.wire_type))
.with_natural_range(0.0, 1024.0);
}
}
let multiply_sink = graph.add_node("multiply", multiply_sink_ports, vec![]);
graph
.connect(
PortRef {
node: multiply_src,
port: "result".into(),
},
PortRef {
node: multiply_sink,
port: "a".into(),
},
)
.unwrap();
let mut runner =
BrushGraphRunner::new(&graph, registry.as_map(), registry.evaluators()).unwrap();
run_one_dab(&mut runner, 0.0, 0);
let slot = runner
.find_node_output_slot(multiply_sink, "result")
.expect("sink slot exists");
let v = match runner.read_slot(slot).expect("slot has value") {
ScalarValue::Scalar(v) => v,
other => panic!("expected Scalar, got {other:?}"),
};
assert!((v - 0.25).abs() < 1e-4, "expected raw 0.25, got {v}");
}
#[test]
fn ranged_source_to_unranged_input_passes_through() {
let registry = registry();
let mut graph = Graph::new();
let random_reg = registry.get("random").unwrap();
let random = graph.add_node("random", random_reg.ports.clone(), random_params());
let multiply_reg = registry.get("multiply").unwrap();
let mut multiply_ports = multiply_reg.ports.clone();
for p in multiply_ports.iter_mut() {
if p.name == "a" {
p.natural_range = None;
}
}
let multiply = graph.add_node("multiply", multiply_ports, vec![]);
graph
.connect(
PortRef {
node: random,
port: "value".into(),
},
PortRef {
node: multiply,
port: "a".into(),
},
)
.unwrap();
let mut runner =
BrushGraphRunner::new(&graph, registry.as_map(), registry.evaluators()).unwrap();
for dab in 0..8 {
run_one_dab(&mut runner, 0.5, dab);
let v = read_scalar(&runner, "multiply", "result");
assert!(
(0.0..1.0).contains(&v),
"unranged dest should see raw random in [0, 1), got {v} on dab {dab}",
);
}
}
#[test]
fn identity_range_is_a_noop() {
let registry = registry();
let mut graph = Graph::new();
let pen_reg = registry.get("pen_input").unwrap();
let pen = graph.add_node("pen_input", pen_reg.ports.clone(), vec![]);
let curve_reg = registry.get("curve").unwrap();
let curve = graph.add_node(
"curve",
curve_reg.ports.clone(),
vec![darkly::gpu::params::ParamValue::Curve(vec![
[0.0, 0.0],
[1.0, 1.0],
])],
);
graph
.connect(
PortRef {
node: pen,
port: "pressure".into(),
},
PortRef {
node: curve,
port: "input".into(),
},
)
.unwrap();
let mut runner =
BrushGraphRunner::new(&graph, registry.as_map(), registry.evaluators()).unwrap();
for pressure in [0.0_f32, 0.25, 0.5, 0.75, 1.0] {
run_one_dab(&mut runner, pressure, 0);
let v = read_scalar(&runner, "curve", "output");
assert!(
(v - pressure).abs() < 0.02,
"identity remap should preserve pressure {pressure}, got {v}",
);
}
}
#[test]
fn unit_source_to_bipolar_dest_spans_full_range() {
let registry = registry();
let mut graph = Graph::new();
let random_reg = registry.get("random").unwrap();
let random = graph.add_node("random", random_reg.ports.clone(), random_params());
let multiply_reg = registry.get("multiply").unwrap();
let mut multiply_ports = multiply_reg.ports.clone();
for p in multiply_ports.iter_mut() {
if p.name == "a" {
*p = std::mem::replace(p, PortDef::input("placeholder", p.wire_type))
.with_natural_range(-100.0, 100.0);
}
}
let multiply = graph.add_node("multiply", multiply_ports, vec![]);
graph
.connect(
PortRef {
node: random,
port: "value".into(),
},
PortRef {
node: multiply,
port: "a".into(),
},
)
.unwrap();
let mut runner =
BrushGraphRunner::new(&graph, registry.as_map(), registry.evaluators()).unwrap();
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
for dab in 0..32 {
run_one_dab(&mut runner, 0.5, dab);
let v = read_scalar(&runner, "multiply", "result");
assert!(
(-100.0..=100.0).contains(&v),
"value {v} outside [-100, 100] on dab {dab}",
);
if v < min {
min = v;
}
if v > max {
max = v;
}
}
assert!(
min < 0.0,
"expected at least one negative sample, min was {min}"
);
assert!(
max > 0.0,
"expected at least one positive sample, max was {max}"
);
}
#[test]
fn radian_to_radian_wire_passes_through() {
let registry = registry();
let mut graph = Graph::new();
let pen_reg = registry.get("pen_input").unwrap();
let pen = graph.add_node("pen_input", pen_reg.ports.clone(), vec![]);
let multiply_reg = registry.get("multiply").unwrap();
let mut multiply_ports = multiply_reg.ports.clone();
for p in multiply_ports.iter_mut() {
if p.name == "a" {
p.natural_range = None;
}
}
let multiply = graph.add_node("multiply", multiply_ports, vec![]);
graph
.connect(
PortRef {
node: pen,
port: "drawing_angle".into(),
},
PortRef {
node: multiply,
port: "a".into(),
},
)
.unwrap();
let mut runner =
BrushGraphRunner::new(&graph, registry.as_map(), registry.evaluators()).unwrap();
let info = PaintInformation {
drawing_angle: std::f32::consts::PI,
..Default::default()
};
runner.seed_sensors(&info, [0.0, 0.0, 0.0, 1.0], 42, 0);
runner.clear_slots();
runner.seed_sensors(&info, [0.0, 0.0, 0.0, 1.0], 42, 0);
runner.execute_cpu();
let v = read_scalar(&runner, "multiply", "result");
assert!(
(v - std::f32::consts::PI).abs() < 1e-5,
"drawing_angle PI rad should pass through to rotation as PI rad, got {v}",
);
}