darkly 0.5.0

A GPU-native paint engine on wgpu: brushes, layers, blend modes, masks, selections, and undo.
Documentation
//! Built-in brushes shipped with the application.
//!
//! Each brush is a YAML file under `crates/darkly/brushes/` that
//! describes its node graph in the [`PortableBrush`] format. The
//! build script (`crates/darkly/build.rs`) embeds every `.yaml` in
//! that directory at compile time — adding a new brush is "drop a
//! file, no code changes." See the modularity rules in CLAUDE.md.

use crate::brush::bundle::Brush;
use crate::brush::portable::PortableBrush;

// `BUILTIN_BRUSHES_YAML: &[(filename, yaml_source)]` — generated by
// `crates/darkly/build.rs` from `crates/darkly/brushes/*.yaml`.
include!(concat!(env!("OUT_DIR"), "/builtin_brushes_gen.rs"));

/// All built-in brushes, parsed from their YAML sources.
///
/// Parse and import failures panic — a built-in brush failing to load
/// is a build-time bug in the shipped YAML, not a runtime error the
/// caller can recover from.
pub fn all() -> Vec<Brush> {
    let registry = crate::brush::registry();
    BUILTIN_BRUSHES_YAML
        .iter()
        .map(|(filename, yaml)| {
            let portable: PortableBrush = serde_yaml_ng::from_str(yaml)
                .unwrap_or_else(|e| panic!("invalid built-in brush '{filename}': {e}"));
            portable
                .into_brush(registry)
                .unwrap_or_else(|e| panic!("invalid built-in brush '{filename}': {e}"))
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builtin_brushes_compile() {
        for brush in all() {
            let result = crate::brush::compile_graph(&brush.metadata.graph);
            assert!(
                result.is_ok(),
                "brush '{}' failed to compile: {:?}",
                brush.metadata.name,
                result.err(),
            );
        }
    }

    #[test]
    fn builtin_brushes_round_trip() {
        for brush in all() {
            let name = brush.metadata.name.clone();
            let bytes = brush.to_bytes().unwrap();
            let loaded = Brush::from_bytes(&bytes).unwrap();
            assert_eq!(loaded.metadata.name, name);
        }
    }

    #[test]
    fn builtin_brushes_no_overlapping_nodes() {
        for brush in all() {
            let layout = brush.metadata.graph.auto_layout();
            let positions: Vec<[i32; 2]> = layout
                .values()
                .map(|p| [p[0] as i32, p[1] as i32])
                .collect();
            for (i, a) in positions.iter().enumerate() {
                for b in &positions[i + 1..] {
                    assert_ne!(
                        a, b,
                        "brush '{}' has overlapping nodes at {:?}",
                        brush.metadata.name, a,
                    );
                }
            }
        }
    }

    #[test]
    fn builtin_brushes_unique_names() {
        let brushes = all();
        let mut names: Vec<_> = brushes.iter().map(|b| b.metadata.name.clone()).collect();
        names.sort();
        names.dedup();
        assert_eq!(names.len(), brushes.len(), "duplicate brush names");
    }

    /// Liquify needs much tighter spacing than the paint default — its
    /// per-dab displacement is ~25% of radius, so spacing must be well
    /// below that for warps to compose smoothly. Don't let this drift back
    /// to the default 10%.
    #[test]
    fn liquify_brush_has_tight_spacing() {
        let brush = all()
            .into_iter()
            .find(|b| b.metadata.name == "Liquify")
            .expect("Liquify built-in must exist");
        let pen = brush
            .metadata
            .graph
            .nodes()
            .values()
            .find(|n| n.type_id == crate::brush::nodes::pen_input::TYPE_ID)
            .expect("liquify brush has a pen_input node");
        let spacing = pen
            .ports
            .iter()
            .find(|p| p.name == "spacing")
            .expect("pen_input has a spacing port");
        assert!(
            spacing.default <= 0.05,
            "liquify spacing default is {}, expected <= 5% for smooth warps",
            spacing.default
        );
    }
}