pub mod builtin_brushes;
pub mod bundle;
pub mod checkpoint_ring;
pub mod composite_pipeline;
pub mod curve_math;
pub mod eval;
pub mod gpu_context;
pub mod import;
pub mod interpolation;
pub mod library;
pub mod node;
pub mod nodes;
pub mod paint_info;
pub mod paint_target_ext;
pub mod pipeline;
pub mod portable;
pub mod preview_renderer;
pub mod save_points;
pub mod scratch;
pub mod spacing;
pub mod stabilizer;
pub mod stabilizers;
pub mod state;
pub mod stroke_buffer;
pub mod stroke_engine;
pub mod wgsl;
pub mod wire;
use std::collections::HashMap;
use std::sync::OnceLock;
use crate::nodegraph::NodeRegistration;
use wire::BrushWireType;
pub const DAB_REFERENCE_SIZE: u32 = 512;
pub use node::BrushNodeRegistration;
pub struct BrushNodeRegistry {
map: HashMap<String, BrushNodeRegistration>,
nodegraph_map: HashMap<String, NodeRegistration<BrushWireType>>,
}
impl BrushNodeRegistry {
fn build() -> Self {
let regs = nodes::registrations();
let mut map = HashMap::with_capacity(regs.len());
let mut nodegraph_map = HashMap::with_capacity(regs.len());
for reg in regs {
let type_id = reg.node.type_id.to_string();
nodegraph_map.insert(type_id.clone(), reg.node.clone());
map.insert(type_id, reg);
}
Self { map, nodegraph_map }
}
pub fn get(&self, type_id: &str) -> Option<&BrushNodeRegistration> {
self.map.get(type_id)
}
pub fn types(&self) -> impl Iterator<Item = &BrushNodeRegistration> {
self.map.values()
}
pub fn as_map(&self) -> &HashMap<String, NodeRegistration<BrushWireType>> {
&self.nodegraph_map
}
pub fn evaluators(&self) -> HashMap<String, Box<dyn eval::BrushNodeEvaluator>> {
let mut out = HashMap::with_capacity(self.map.len());
for (type_id, reg) in &self.map {
out.insert(type_id.clone(), (reg.evaluator)());
}
out
}
}
pub fn registry() -> &'static BrushNodeRegistry {
static REGISTRY: OnceLock<BrushNodeRegistry> = OnceLock::new();
REGISTRY.get_or_init(BrushNodeRegistry::build)
}
pub fn find_terminal(
graph: &crate::nodegraph::Graph<BrushWireType>,
) -> Result<crate::nodegraph::NodeId, crate::nodegraph::FindTerminalError> {
let registry = registry();
graph.find_terminal(registry.as_map())
}
pub fn default_graph() -> crate::nodegraph::Graph<BrushWireType> {
use crate::gpu::params::ParamValue;
use crate::nodegraph::{Graph, PortRef};
let registry = registry();
let mut graph = Graph::new();
let pen = graph.add_node(
"pen_input",
registry.get("pen_input").unwrap().ports.clone(),
vec![],
);
let paint_color = graph.add_node(
"paint_color",
registry.get("paint_color").unwrap().ports.clone(),
vec![],
);
let shape = graph.add_node(
"shape",
registry.get("shape").unwrap().ports.clone(),
vec![ParamValue::Int(0)], );
let stamp = graph.add_node(
"stamp",
registry.get("stamp").unwrap().ports.clone(),
vec![],
);
let terminal = graph.add_node(
"paint",
registry.get("paint").unwrap().ports.clone(),
vec![],
);
let wires = [
(pen, "pressure", terminal, "flow"),
(paint_color, "color", stamp, "color"),
(shape, "mask", stamp, "tip"),
(stamp, "dab", terminal, "rgba"),
(pen, "position", terminal, "position"),
];
for (fnode, fport, tnode, tport) in wires {
graph
.connect(
PortRef {
node: fnode,
port: fport.into(),
},
PortRef {
node: tnode,
port: tport.into(),
},
)
.unwrap();
}
graph
}
pub fn compile_graph(
graph: &crate::nodegraph::Graph<BrushWireType>,
) -> Result<eval::BrushGraphRunner, String> {
let registry = registry();
let rewritten = {
let mut g = graph.clone();
nodes::switch::apply_to(&mut g);
g
};
let evaluators = registry.evaluators();
let mut runner = eval::BrushGraphRunner::new(&rewritten, registry.as_map(), evaluators)
.map_err(|e| format!("{e}"))?;
if runner.has_terminal() {
let plan =
crate::nodegraph::compile(&rewritten, registry.as_map()).map_err(|e| format!("{e}"))?;
let compile_evals = registry.evaluators();
let compiled = wgsl::compile_brush_to_wgsl(&rewritten, &plan, &compile_evals)
.map_err(|e| format!("paint WGSL compilation failed: {e}"))?;
runner.set_compiled_brush(std::sync::Arc::new(compiled));
}
Ok(runner)
}
pub fn default_runner() -> Result<eval::BrushGraphRunner, String> {
compile_graph(&default_graph())
}
pub fn compile_from_json(json: &str) -> Result<eval::BrushGraphRunner, String> {
let graph: crate::nodegraph::Graph<BrushWireType> =
serde_json::from_str(json).map_err(|e| format!("invalid graph JSON: {e}"))?;
compile_graph(&graph)
}
pub fn reset_exposed_scrubs(graph: &mut crate::nodegraph::Graph<BrushWireType>) {
let registry = registry();
let mut resets: Vec<(crate::nodegraph::NodeId, String, f32)> = Vec::new();
for (id, node) in graph.nodes() {
let Some(reg) = registry.get(&node.type_id) else {
continue;
};
for port in ®.ports {
if port.exposed
&& port.dir == crate::nodegraph::PortDir::Input
&& !port.persist_in_thumbnail
{
resets.push((*id, port.name.clone(), port.default));
}
}
}
for (id, name, default) in resets {
let _ = graph.set_port_default(id, &name, default);
}
}
pub fn validate_graph_json(json: &str) -> Result<(), String> {
let mut graph: crate::nodegraph::Graph<BrushWireType> =
serde_json::from_str(json).map_err(|e| format!("invalid graph JSON: {e}"))?;
nodes::switch::apply_to(&mut graph);
let registry = registry();
crate::nodegraph::compile(&graph, registry.as_map())
.map_err(|e| format!("graph validation failed: {e}"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::eval::BrushGraphRunner;
use super::paint_info::{PaintInformation, StrokeRecord};
use super::wire::{BrushWireType, ScalarValue};
use super::*;
use crate::gpu::params::ParamValue;
use crate::nodegraph::{Graph, NodeId, PortRef};
fn pressure_times_half_graph() -> (Graph<BrushWireType>, NodeId, NodeId) {
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 mul_reg = registry.get("multiply").unwrap();
let multiply = graph.add_node("multiply", mul_reg.ports.clone(), vec![]);
graph
.connect(
PortRef {
node: pen,
port: "pressure".into(),
},
PortRef {
node: multiply,
port: "a".into(),
},
)
.unwrap();
graph.set_port_default(multiply, "b", 0.5).unwrap();
(graph, pen, multiply)
}
#[test]
fn pressure_times_port_default() {
let (graph, _pen, multiply) = pressure_times_half_graph();
let registry = registry();
let evaluators = registry.evaluators();
let mut runner = BrushGraphRunner::new(&graph, registry.as_map(), evaluators).unwrap();
let info = PaintInformation {
pressure: 0.8,
..Default::default()
};
runner.seed_sensors(&info, [0.0, 0.0, 0.0, 1.0], 42, 0);
runner.execute_cpu();
let slot = runner.find_node_output_slot(multiply, "result").unwrap();
let result = runner.read_slot(slot).unwrap();
match result {
ScalarValue::Scalar(v) => assert!((v - 0.4).abs() < 1e-6, "expected 0.4, got {v}"),
other => panic!("expected Scalar, got {:?}", other),
}
}
#[test]
fn curve_spline_identity() {
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![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 evaluators = registry.evaluators();
let mut runner = BrushGraphRunner::new(&graph, registry.as_map(), evaluators).unwrap();
let info = PaintInformation {
pressure: 0.5,
..Default::default()
};
runner.seed_sensors(&info, [0.0, 0.0, 0.0, 1.0], 42, 0);
runner.execute_cpu();
let slot = runner.find_node_output_slot(curve, "output").unwrap();
let result = runner.read_slot(slot).unwrap();
match result {
ScalarValue::Scalar(v) => {
assert!((v - 0.5).abs() < 0.02, "identity curve at 0.5: got {v}");
}
other => panic!("expected Scalar, got {:?}", other),
}
}
#[test]
fn curve_spline_s_curve() {
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![ParamValue::Curve(vec![[0.0, 0.0], [0.5, 0.2], [1.0, 1.0]])],
);
graph
.connect(
PortRef {
node: pen,
port: "pressure".into(),
},
PortRef {
node: curve,
port: "input".into(),
},
)
.unwrap();
let evaluators = registry.evaluators();
let mut runner = BrushGraphRunner::new(&graph, registry.as_map(), evaluators).unwrap();
let info = PaintInformation {
pressure: 0.5,
..Default::default()
};
runner.seed_sensors(&info, [0.0, 0.0, 0.0, 1.0], 42, 0);
runner.execute_cpu();
let slot = runner.find_node_output_slot(curve, "output").unwrap();
let result = runner.read_slot(slot).unwrap();
match result {
ScalarValue::Scalar(v) => {
assert!(
(v - 0.2).abs() < 0.05,
"s-curve at 0.5: expected ~0.2, got {v}"
);
}
other => panic!("expected Scalar, got {:?}", other),
}
}
#[test]
fn stroke_record_accumulates() {
let mut record = StrokeRecord::new([1.0, 0.0, 0.0, 1.0], "test_brush".into());
assert!(record.is_empty());
for i in 0..10 {
record.push(PaintInformation {
pos: [i as f32 * 10.0, 50.0],
pressure: 0.5 + (i as f32) * 0.05,
index: i,
..Default::default()
});
}
assert_eq!(record.len(), 10);
assert_eq!(record.events[0].index, 0);
assert_eq!(record.events[9].index, 9);
assert!((record.events[0].pressure - 0.5).abs() < 1e-6);
}
#[test]
fn disconnected_input_uses_default() {
let registry = registry();
let mut graph = Graph::new();
let mul_reg = registry.get("multiply").unwrap();
let multiply = graph.add_node("multiply", mul_reg.ports.clone(), vec![]);
let evaluators = registry.evaluators();
let mut runner = BrushGraphRunner::new(&graph, registry.as_map(), evaluators).unwrap();
runner.execute_cpu();
let slot = runner.find_node_output_slot(multiply, "result").unwrap();
let result = runner.read_slot(slot).unwrap();
match result {
ScalarValue::Scalar(v) => {
assert!(
(v - 1.0).abs() < 1e-6,
"expected 1.0 (default * default), got {v}"
);
}
other => panic!("expected Scalar, got {:?}", other),
}
}
#[test]
fn paint_color_output() {
let registry = registry();
let mut graph = Graph::new();
let color_reg = registry.get("paint_color").unwrap();
let color_node = graph.add_node("paint_color", color_reg.ports.clone(), vec![]);
let evaluators = registry.evaluators();
let mut runner = BrushGraphRunner::new(&graph, registry.as_map(), evaluators).unwrap();
let fg_color = [1.0, 0.2, 0.3, 1.0];
let info = PaintInformation::default();
runner.seed_sensors(&info, fg_color, 42, 0);
runner.execute_cpu();
let slot = runner.find_node_output_slot(color_node, "color").unwrap();
let result = runner.read_slot(slot).unwrap();
match result {
ScalarValue::Vec4(c) => assert_eq!(c, fg_color),
other => panic!("expected Vec4, got {:?}", other),
}
}
#[test]
fn default_runner_compiles() {
let runner = super::default_runner();
assert!(
runner.is_ok(),
"default_runner() failed: {:?}",
runner.err()
);
}
}