use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
pub struct AssetPipeline;
impl AssetPipeline {
pub fn run<P: AsRef<Path>>(asset_dir: P) -> Result<()> {
let dir = asset_dir.as_ref();
if !dir.exists() {
println!(
"No assets directory found at {:?}. Skipping asset pipeline.",
dir
);
return Ok(());
}
println!("🎨 CVKG Asset Pipeline executing...");
let entries: Vec<_> = walkdir::WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.collect();
let total = entries.len();
let pb = indicatif::ProgressBar::new(total as u64);
pb.set_style(
indicatif::ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}",
)
.unwrap()
.progress_chars("#>-"),
);
let mut shader_count = 0;
let mut image_count = 0;
let mut error_count = 0;
for entry in &entries {
let path = entry.path();
pb.set_message(
path.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
if let Some(ext) = path.extension() {
match ext.to_str().unwrap_or("") {
"wgsl" => {
if let Err(e) = Self::validate_shader(path) {
log::warn!("Shader validation failed for {:?}: {}", path, e);
error_count += 1;
}
shader_count += 1;
}
"glsl" => {
if let Err(e) = Self::validate_shader(path) {
log::warn!("Shader validation for {:?}: {}", path, e);
error_count += 1;
}
shader_count += 1;
}
"png" | "jpg" | "jpeg" => {
if let Err(e) = Self::optimize_image(path) {
log::warn!("Image optimization failed for {:?}: {}", path, e);
error_count += 1;
}
image_count += 1;
}
_ => {}
}
}
pb.inc(1);
}
pb.finish_with_message(format!(
"✅ Asset Pipeline complete: {} shaders validated, {} images processed{}",
shader_count,
image_count,
if error_count > 0 {
format!(", {} errors", error_count)
} else {
String::new()
}
));
Ok(())
}
fn validate_shader(path: &Path) -> Result<()> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read shader file: {:?}", path))?;
if content.trim().is_empty() {
log::warn!("Shader file {:?} is empty", path);
return Ok(());
}
let module = naga::front::wgsl::parse_str(&content)
.map_err(|e| anyhow::anyhow!("Shader parse error in {:?}: {}", path, e))?;
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
validator
.validate(&module)
.map_err(|e| anyhow::anyhow!("Shader validation failed in {:?}: {}", path, e))?;
Ok(())
}
fn optimize_image(path: &Path) -> Result<()> {
let metadata = fs::metadata(path)
.with_context(|| format!("Failed to read image metadata: {:?}", path))?;
let size_mb = metadata.len() as f64 / 1024.0 / 1024.0;
if metadata.len() > 1024 * 1024 {
log::warn!(
"Image {:?} is {:.2} MB. Consider compressing it to reduce bundle size.",
path,
size_mb
);
}
match image::open(path) {
Ok(img) => {
log::debug!(
"Image {:?} verified ({}x{}, {:?})",
path,
img.width(),
img.height(),
img.color()
);
Ok(())
}
Err(e) => {
anyhow::bail!("Failed to decode image {:?}: {}", path, e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn write_temp_file(name: &str, content: &str) -> (PathBuf, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("Failed to create test dir");
let path = dir.path().join(name);
std::fs::write(&path, content).expect("Failed to write test file");
(path, dir)
}
#[test]
fn test_validate_shader_valid_wgsl() {
let (path, _dir) = write_temp_file(
"test.wgsl",
r#"
@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
let pos = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
return vec4<f32>(pos[idx], 0.0, 1.0);
}
"#,
);
let result = AssetPipeline::validate_shader(&path);
assert!(
result.is_ok(),
"Valid WGSL shader should pass validation: {:?}",
result.err()
);
}
#[test]
fn test_validate_shader_invalid() {
let (path, _dir) = write_temp_file("invalid.wgsl", "this is not valid wgsl {{{");
let result = AssetPipeline::validate_shader(&path);
assert!(result.is_err(), "Invalid WGSL should fail validation");
}
#[test]
fn test_validate_shader_empty() {
let (path, _dir) = write_temp_file("empty.wgsl", " \n \n");
let result = AssetPipeline::validate_shader(&path);
assert!(result.is_ok(), "Empty shader should pass (with warning)");
}
#[test]
fn test_optimize_image_valid_png() {
let dir = tempfile::tempdir().expect("Failed to create temp dir");
let path = dir.path().join("test.png");
let img = image::RgbaImage::from_raw(1, 1, vec![255, 0, 0, 255]).unwrap();
img.save(&path).unwrap();
let result = AssetPipeline::optimize_image(&path);
assert!(result.is_ok(), "Valid PNG should pass: {:?}", result.err());
}
#[test]
fn test_optimize_image_invalid() {
let (path, _dir) = write_temp_file("fake.png", "this is not a real png file");
let result = AssetPipeline::optimize_image(&path);
assert!(result.is_err(), "Invalid image should fail");
}
}