use crate::brush::eval::{BrushNodeEvaluator, EvalContext};
use crate::brush::node::BrushNodeRegistration;
use crate::brush::wgsl::{CompileWgslCtx, NodeWgsl};
use crate::brush::wire::{BrushWireType, ScalarValue};
use crate::nodegraph::{Connection, Graph, NodeRegistration, PortDef, PortDir, PortRef};
pub const TYPE_ID: &str = "switch";
const TRIPLETS: &[(&str, &str, &str)] = &[
("in_0_scalar", "in_1_scalar", "out_scalar"),
("in_0_int", "in_1_int", "out_int"),
("in_0_bool", "in_1_bool", "out_bool"),
("in_0_vec2", "in_1_vec2", "out_vec2"),
("in_0_vec4", "in_1_vec4", "out_vec4"),
];
const SELECT_PORT: &str = "select";
pub fn register() -> BrushNodeRegistration {
BrushNodeRegistration::compute(
NodeRegistration {
type_id: TYPE_ID,
category: "math",
display_name: "Switch",
description: "Toggle that picks one of two inputs. Wire one side only and it acts as an on/off switch for that branch.",
ports: vec![
PortDef::input("in_0_scalar", BrushWireType::Scalar)
.with_description("Routed to out_scalar when select=0"),
PortDef::input("in_1_scalar", BrushWireType::Scalar)
.with_description("Routed to out_scalar when select=1"),
PortDef::output("out_scalar", BrushWireType::Scalar)
.with_description("Selected scalar"),
PortDef::input("in_0_int", BrushWireType::Int)
.with_description("Routed to out_int when select=0"),
PortDef::input("in_1_int", BrushWireType::Int)
.with_description("Routed to out_int when select=1"),
PortDef::output("out_int", BrushWireType::Int).with_description("Selected int"),
PortDef::input("in_0_bool", BrushWireType::Bool)
.with_description("Routed to out_bool when select=0"),
PortDef::input("in_1_bool", BrushWireType::Bool)
.with_description("Routed to out_bool when select=1"),
PortDef::output("out_bool", BrushWireType::Bool).with_description("Selected bool"),
PortDef::input("in_0_vec2", BrushWireType::Vec2)
.with_description("Routed to out_vec2 when select=0"),
PortDef::input("in_1_vec2", BrushWireType::Vec2)
.with_description("Routed to out_vec2 when select=1"),
PortDef::output("out_vec2", BrushWireType::Vec2).with_description("Selected vec2"),
PortDef::input("in_0_vec4", BrushWireType::Vec4)
.with_description("Routed to out_vec4 when select=0"),
PortDef::input("in_1_vec4", BrushWireType::Vec4)
.with_description("Routed to out_vec4 when select=1"),
PortDef::output("out_vec4", BrushWireType::Vec4).with_description("Selected vec4"),
PortDef::input(SELECT_PORT, BrushWireType::Bool)
.with_range(0.0, 1.0, 0.0)
.with_step(1.0)
.with_label("Select")
.with_description(
"Routes one of two inputs to the output. If the \
chosen input is unconnected, downstream falls back \
to its own port default.",
),
],
params: &[],
is_gpu: false,
is_terminal: false,
supports_erase: true,
},
|| Box::new(SwitchEvaluator),
)
}
pub struct SwitchEvaluator;
impl BrushNodeEvaluator for SwitchEvaluator {
fn evaluate_cpu(&self, ctx: &EvalContext) -> Vec<(String, ScalarValue)> {
let select_to_1 = ctx.input(SELECT_PORT).as_f32() >= 0.5;
let mut out = Vec::with_capacity(TRIPLETS.len());
for (in_0, in_1, out_name) in TRIPLETS {
let chosen = if select_to_1 { *in_1 } else { *in_0 };
out.push(((*out_name).to_string(), ctx.input(chosen)));
}
out
}
fn compile_wgsl(&self, _cctx: &CompileWgslCtx) -> Result<NodeWgsl, String> {
Err(
"Switch.select must be a static value; dynamic wiring is not \
yet supported. Disconnect the wire on the `select` port and \
toggle it via its exposed slider/value instead."
.into(),
)
}
}
pub(crate) fn apply_to(graph: &mut Graph<BrushWireType>) {
let switch_ids: Vec<_> = graph
.nodes()
.iter()
.filter(|(_, n)| n.type_id == TYPE_ID)
.map(|(id, _)| *id)
.collect();
for switch_id in switch_ids {
let select_wired = graph
.connections
.iter()
.any(|c| c.to.node == switch_id && c.to.port == SELECT_PORT);
if select_wired {
continue;
}
let select_to_1 = {
let Some(node) = graph.nodes().get(&switch_id) else {
continue;
};
node.ports
.iter()
.find(|p| p.name == SELECT_PORT && p.dir == PortDir::Input)
.map(|p| p.default >= 0.5)
.unwrap_or(false)
};
for (in_0, in_1, out_name) in TRIPLETS {
let chosen_in = if select_to_1 { *in_1 } else { *in_0 };
let upstream: Option<PortRef> = graph
.connections
.iter()
.find(|c| c.to.node == switch_id && c.to.port == chosen_in)
.map(|c| c.from.clone());
let downstreams: Vec<PortRef> = graph
.connections
.iter()
.filter(|c| c.from.node == switch_id && c.from.port == *out_name)
.map(|c| c.to.clone())
.collect();
graph.connections.retain(|c| {
!(c.to.node == switch_id && (c.to.port == *in_0 || c.to.port == *in_1))
&& !(c.from.node == switch_id && c.from.port == *out_name)
});
if let Some(up) = upstream {
for down in downstreams {
graph.connections.push(Connection {
from: up.clone(),
to: down,
});
}
}
}
let _ = graph.remove_node(switch_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::brush;
use crate::gpu::params::ParamValue;
use crate::nodegraph::{Connection, Graph, NodeId, PortRef};
fn add_node(graph: &mut Graph<BrushWireType>, type_id: &str) -> NodeId {
let registry = brush::registry();
let reg = registry
.get(type_id)
.unwrap_or_else(|| panic!("no registration for {type_id}"));
let params: Vec<ParamValue> = reg.params.iter().map(|p| p.default_value()).collect();
graph.add_node(type_id, reg.ports.clone(), params)
}
fn pr(node: NodeId, port: &str) -> PortRef {
PortRef {
node,
port: port.into(),
}
}
#[test]
fn enabled_default_splices_through() {
let mut graph = Graph::<BrushWireType>::new();
let paint_color = add_node(&mut graph, "paint_color");
let switch_id = add_node(&mut graph, TYPE_ID);
let stamp = add_node(&mut graph, "stamp");
graph
.connect(pr(paint_color, "color"), pr(switch_id, "in_0_vec4"))
.unwrap();
graph
.connect(pr(switch_id, "out_vec4"), pr(stamp, "color"))
.unwrap();
apply_to(&mut graph);
assert!(
graph.nodes().get(&switch_id).is_none(),
"switch node should be removed after rewrite"
);
assert!(
graph
.connections
.iter()
.any(|c| c.from == pr(paint_color, "color") && c.to == pr(stamp, "color")),
"expected paint_color → stamp.color after splice; got {:?}",
graph.connections
);
}
#[test]
fn disabled_drops_wire_so_downstream_uses_default() {
let mut graph = Graph::<BrushWireType>::new();
let paint_color = add_node(&mut graph, "paint_color");
let switch_id = add_node(&mut graph, TYPE_ID);
let stamp = add_node(&mut graph, "stamp");
graph
.connect(pr(paint_color, "color"), pr(switch_id, "in_0_vec4"))
.unwrap();
graph
.connect(pr(switch_id, "out_vec4"), pr(stamp, "color"))
.unwrap();
graph.set_port_default(switch_id, SELECT_PORT, 1.0).unwrap();
apply_to(&mut graph);
assert!(graph.nodes().get(&switch_id).is_none());
assert!(
!graph.connections.iter().any(|c| c.to == pr(stamp, "color")),
"stamp.color should be unconnected after rewrite; got {:?}",
graph.connections
);
}
#[test]
fn mux_mode_routes_chosen_input_and_drops_unchosen() {
{
let mut graph = Graph::<BrushWireType>::new();
let paint_color = add_node(&mut graph, "paint_color");
let switch_id = add_node(&mut graph, TYPE_ID);
let stamp = add_node(&mut graph, "stamp");
let paint_color_b = add_node(&mut graph, "paint_color");
graph
.connect(pr(paint_color, "color"), pr(switch_id, "in_0_vec4"))
.unwrap();
graph
.connect(pr(paint_color_b, "color"), pr(switch_id, "in_1_vec4"))
.unwrap();
graph
.connect(pr(switch_id, "out_vec4"), pr(stamp, "color"))
.unwrap();
apply_to(&mut graph);
assert!(graph.nodes().get(&switch_id).is_none());
assert!(
graph
.connections
.iter()
.any(|c| c.from == pr(paint_color, "color") && c.to == pr(stamp, "color")),
"select=0 should route paint_color (in_0) → stamp.color"
);
assert!(
!graph
.connections
.iter()
.any(|c| c.from == pr(paint_color_b, "color")),
"paint_color_b (in_1) should be disconnected"
);
}
{
let mut graph = Graph::<BrushWireType>::new();
let paint_color = add_node(&mut graph, "paint_color");
let switch_id = add_node(&mut graph, TYPE_ID);
let stamp = add_node(&mut graph, "stamp");
let paint_color_b = add_node(&mut graph, "paint_color");
graph
.connect(pr(paint_color, "color"), pr(switch_id, "in_0_vec4"))
.unwrap();
graph
.connect(pr(paint_color_b, "color"), pr(switch_id, "in_1_vec4"))
.unwrap();
graph
.connect(pr(switch_id, "out_vec4"), pr(stamp, "color"))
.unwrap();
graph.set_port_default(switch_id, SELECT_PORT, 1.0).unwrap();
apply_to(&mut graph);
assert!(graph.nodes().get(&switch_id).is_none());
assert!(
graph
.connections
.iter()
.any(|c| c.from == pr(paint_color_b, "color") && c.to == pr(stamp, "color")),
"select=1 should route paint_color_b (in_1) → stamp.color"
);
assert!(
!graph
.connections
.iter()
.any(|c| c.from == pr(paint_color, "color")),
"paint_color (in_0) should be disconnected"
);
}
}
#[test]
fn dynamic_select_leaves_switch_in_place() {
let mut graph = Graph::<BrushWireType>::new();
let paint_color = add_node(&mut graph, "paint_color");
let switch_id = add_node(&mut graph, TYPE_ID);
let stamp = add_node(&mut graph, "stamp");
graph
.connect(pr(paint_color, "color"), pr(switch_id, "in_0_vec4"))
.unwrap();
graph
.connect(pr(switch_id, "out_vec4"), pr(stamp, "color"))
.unwrap();
graph.connections.push(Connection {
from: pr(paint_color, "color"),
to: pr(switch_id, SELECT_PORT),
});
apply_to(&mut graph);
assert!(
graph.nodes().get(&switch_id).is_some(),
"switch with dynamic select should NOT be rewritten away"
);
assert!(
graph
.connections
.iter()
.any(|c| c.to == pr(switch_id, SELECT_PORT)),
"dynamic select wire should still be present"
);
}
}