use std::collections::HashMap;
use super::bundle::{Brush, BrushMetadata};
use crate::brush::wire::BrushWireType;
use crate::nodegraph::Graph;
#[derive(Clone, Debug, serde::Serialize)]
pub struct BrushInfo {
pub name: String,
pub category: String,
pub author: String,
pub description: String,
pub tags: Vec<String>,
}
impl From<&BrushMetadata> for BrushInfo {
fn from(p: &BrushMetadata) -> Self {
BrushInfo {
name: p.name.clone(),
category: p.category.clone(),
author: p.author.clone(),
description: p.description.clone(),
tags: p.tags.clone(),
}
}
}
pub struct BrushLibrary {
brushes: HashMap<String, Brush>,
dab_thumbnails: HashMap<String, Vec<u8>>,
}
impl BrushLibrary {
pub fn new() -> Self {
BrushLibrary {
brushes: HashMap::new(),
dab_thumbnails: HashMap::new(),
}
}
pub fn list(&self) -> Vec<BrushInfo> {
let mut infos: Vec<BrushInfo> = self
.brushes
.values()
.map(|b| BrushInfo::from(&b.metadata))
.collect();
infos.sort_by(|a, b| a.name.cmp(&b.name));
infos
}
pub fn get(&self, name: &str) -> Option<&Brush> {
self.brushes.get(name)
}
pub fn graph(&self, name: &str) -> Option<&Graph<BrushWireType>> {
self.brushes.get(name).map(|b| &b.metadata.graph)
}
pub fn insert(&mut self, brush: Brush) {
let name = brush.metadata.name.clone();
self.brushes.insert(name, brush);
}
pub fn remove(&mut self, name: &str) -> bool {
self.brushes.remove(name).is_some()
}
pub fn thumbnail_png(&self, name: &str) -> Option<&[u8]> {
self.brushes
.get(name)
.and_then(|b| b.thumbnail_png.as_deref())
}
pub fn clear_thumbnails(&mut self) {
for brush in self.brushes.values_mut() {
brush.thumbnail_png = None;
}
self.dab_thumbnails.clear();
}
pub fn dab_thumbnail_png(&self, name: &str) -> Option<&[u8]> {
self.dab_thumbnails.get(name).map(|v| v.as_slice())
}
pub fn set_dab_thumbnail(&mut self, name: &str, png: Vec<u8>) {
self.dab_thumbnails.insert(name.to_string(), png);
}
pub fn set_thumbnail(&mut self, name: &str, png: Vec<u8>) -> bool {
match self.brushes.get_mut(name) {
Some(brush) => {
brush.thumbnail_png = Some(png);
true
}
None => false,
}
}
pub fn import_bytes(&mut self, bytes: &[u8]) -> Result<String, String> {
let brush = Brush::from_bytes(bytes)?;
let name = brush.metadata.name.clone();
self.insert(brush);
Ok(name)
}
pub fn export_bytes(&self, name: &str) -> Result<Vec<u8>, String> {
let brush = self
.brushes
.get(name)
.ok_or_else(|| format!("brush '{}' not found", name))?;
brush.to_bytes()
}
pub fn len(&self) -> usize {
self.brushes.len()
}
pub fn is_empty(&self) -> bool {
self.brushes.is_empty()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn scan_directory(&mut self, dir: &std::path::Path) -> Result<usize, String> {
let entries = std::fs::read_dir(dir)
.map_err(|e| format!("failed to read directory '{}': {e}", dir.display()))?;
let mut count = 0;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("darkly-brush") {
match Brush::load(&path) {
Ok(brush) => {
self.insert(brush);
count += 1;
}
Err(e) => {
log::warn!("skipping brush '{}': {e}", path.display());
}
}
}
}
Ok(count)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn save_to_directory(
&self,
name: &str,
dir: &std::path::Path,
) -> Result<std::path::PathBuf, String> {
let brush = self
.brushes
.get(name)
.ok_or_else(|| format!("brush '{}' not found", name))?;
let filename = sanitize_filename(name);
let path = dir.join(format!("{filename}.darkly-brush"));
brush.save(&path)?;
Ok(path)
}
}
impl Default for BrushLibrary {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(target_arch = "wasm32"))]
fn sanitize_filename(name: &str) -> String {
name.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
_ => c,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::brush;
use crate::brush::bundle::BrushMetadata;
#[test]
fn library_insert_list_get() {
let mut lib = BrushLibrary::new();
assert!(lib.is_empty());
let metadata = BrushMetadata::from_graph("Alpha", brush::default_graph());
lib.insert(Brush::from_metadata(metadata));
let metadata2 = BrushMetadata::from_graph("Beta", brush::default_graph());
lib.insert(Brush::from_metadata(metadata2));
assert_eq!(lib.len(), 2);
let list = lib.list();
assert_eq!(list.len(), 2);
assert_eq!(list[0].name, "Alpha");
assert_eq!(list[1].name, "Beta");
assert!(lib.get("Alpha").is_some());
assert!(lib.get("Missing").is_none());
}
#[test]
fn library_import_export_round_trip() {
let mut lib = BrushLibrary::new();
let metadata = BrushMetadata::from_graph("Roundtrip", brush::default_graph());
let brush = Brush::from_metadata(metadata);
let bytes = brush.to_bytes().unwrap();
let name = lib.import_bytes(&bytes).unwrap();
assert_eq!(name, "Roundtrip");
let exported = lib.export_bytes("Roundtrip").unwrap();
let reloaded = Brush::from_bytes(&exported).unwrap();
assert_eq!(reloaded.metadata.name, "Roundtrip");
}
#[test]
fn library_remove() {
let mut lib = BrushLibrary::new();
let metadata = BrushMetadata::from_graph("ToRemove", brush::default_graph());
lib.insert(Brush::from_metadata(metadata));
assert_eq!(lib.len(), 1);
assert!(lib.remove("ToRemove"));
assert!(lib.is_empty());
assert!(!lib.remove("ToRemove"));
}
#[test]
fn library_scan_directory() {
let dir = std::env::temp_dir().join("darkly_brush_library_test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
for name in &["Scan A", "Scan B"] {
let metadata = BrushMetadata::from_graph(*name, brush::default_graph());
let brush = Brush::from_metadata(metadata);
brush
.save(&dir.join(format!("{name}.darkly-brush")))
.unwrap();
}
std::fs::write(dir.join("readme.txt"), "not a brush").unwrap();
let mut lib = BrushLibrary::new();
let count = lib.scan_directory(&dir).unwrap();
assert_eq!(count, 2);
assert_eq!(lib.len(), 2);
assert!(lib.get("Scan A").is_some());
assert!(lib.get("Scan B").is_some());
let _ = std::fs::remove_dir_all(&dir);
}
}