diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..024db51 --- /dev/null +++ b/build.rs @@ -0,0 +1,207 @@ +use heck::ToSnakeCase as _; +use resvg::{tiny_skia, usvg}; +use std::{ + collections::BTreeMap, + env, + fs::{self, File}, + hash::{DefaultHasher, Hash, Hasher}, + io::{self, Write as _}, + path::{Path, PathBuf} +}; + +const TILE_SIZE: u32 = 64; + +#[derive(Default)] +struct Assets { + /// Assets directly contained within this asset group, mapping their name to the name + /// of the constant storing their png. + assets: BTreeMap, + + /// Asset groups contained within this asset group, mapping their name to the assets + /// that group contains. + groups: BTreeMap> +} + +struct AssetsWriter { + file: File, + root: Assets +} + +impl AssetsWriter { + fn new>(path: P) -> Self { + let mut file = File::create(path).expect("Failed to create assets file"); + writeln!(file, "// @generated").unwrap(); + Self { + file, + root: Assets::default() + } + } + + fn add_png>( + &mut self, + canonical_path: P, + png: tiny_skia::Pixmap + ) -> String { + let mut hasher = DefaultHasher::new(); + canonical_path.as_ref().hash(&mut hasher); + let hash = hasher.finish(); + let const_name = format!("ASSET_{hash:X}"); + + write!(self.file, "const {const_name}: &'static [u8] = &[").unwrap(); + for byte in png.encode_png().expect("Failed to encode png") { + write!(self.file, "{byte}, ").unwrap(); + } + writeln!(self.file, "];").unwrap(); + + const_name + } + + fn finish(mut self) { + fn write_assets_struct( + file: &mut File, + root: &Assets, + indent: &str + ) -> io::Result<()> { + for (group_name, group) in &root.groups { + writeln!(file, "{indent}mod {group_name} {{")?; + write_assets_struct(file, group, &format!("{indent}\t"))?; + writeln!(file, "}}")?; + } + + writeln!(file, "{indent}struct Asset {{")?; + for asset_name in root.assets.keys() { + writeln!( + file, + "{indent}\t{}: comfy::TextureHandle,", + asset_name.to_snake_case() + )?; + } + for group_name in root.groups.keys() { + writeln!( + file, + "{indent}\t{group_name}: &'static {group_name}::Assets," + )?; + } + writeln!(file, "{indent}}}")?; + + writeln!(file, "{indent}impl Assets {{")?; + writeln!( + file, + "{indent}\tpub fn load(c: &mut comfy::EngineContext) {{" + )?; + for asset_const_name in root.assets.values() { + writeln!(file, "{indent}\t\tc.load_texture_from_bytes({asset_const_name:?}, {asset_const_name});")?; + } + for group_name in root.groups.keys() { + writeln!(file, "{indent}\t\t{group_name}::Assets::load(c);")?; + } + writeln!(file, "{indent}\t}}")?; + writeln!(file, "{indent}}}")?; + + writeln!( + file, + "{indent}const ASSETS: comfy::Lazy = Lazy::new(|| Assets {{" + )?; + for (asset_name, asset_const_name) in &root.assets { + writeln!( + file, + "{indent}\t{}: comfy::texture_id({asset_const_name:?}),", + asset_name.to_snake_case() + )?; + } + for group_name in root.groups.keys() { + writeln!( + file, + "{indent}\t{group_name}: {group_name}::ASSETS.force()," + )?; + } + writeln!(file, "{indent}}});")?; + + Ok(()) + } + + write_assets_struct(&mut self.file, &self.root, "").unwrap(); + } +} + +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!("cargo::rerun-if-changed=assets/"); + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let assets = PathBuf::from(out_dir).join("assets.rs"); + println!("cargo::warning=Writing assets to {}", assets.display()); + println!("cargo::rustc-env=ASSETS_RS={}", assets.display()); + let mut writer = AssetsWriter::new(assets); + process_dir("assets", &mut writer, &mut Vec::new()); + writer.finish(); +} + +fn process_dir + Copy>( + dir: P, + writer: &mut AssetsWriter, + groups: &mut Vec +) { + for entry in fs::read_dir(dir).expect("Cannot read dir") { + let entry = entry.expect("Cannot read dir entry"); + let path = entry.path(); + if entry + .metadata() + .expect("Cannot read dir entry metadata") + .is_dir() + { + groups.push( + path.file_name() + .unwrap() + .to_str() + .expect("Non-UTF8 file names aren't allowed") + .into() + ); + process_dir(&path, writer, groups); + groups.pop(); + } else if path.extension().map(|ext| ext == "svg").unwrap_or(false) { + process_svg(&path, dir, writer, groups); + } + } +} + +fn process_svg + Copy, Q: AsRef>( + file: P, + dir: Q, + writer: &mut AssetsWriter, + groups: &[String] +) { + let bytes = fs::read(file).expect("Cannot read svg file"); + let tree = usvg::Tree::from_data(&bytes, &usvg::Options { + resources_dir: Some(dir.as_ref().to_owned()), + ..Default::default() + }) + .unwrap_or_else(|err| { + panic!("Cannot parse svg file {}: {err}", file.as_ref().display()) + }); + let mut pixmap = tiny_skia::Pixmap::new(TILE_SIZE, TILE_SIZE).unwrap(); + resvg::render(&tree, Default::default(), &mut pixmap.as_mut()); + + let const_name = writer.add_png( + file.as_ref() + .canonicalize() + .expect("Failed to canonicalize"), + pixmap + ); + let mut group = &mut writer.root; + for group_name in groups { + if !group.groups.contains_key(group_name) { + group.groups.insert(group_name.to_owned(), Box::default()); + } + group = group.groups.get_mut(group_name).unwrap(); + } + group.assets.insert( + file.as_ref() + .file_stem() + .expect("File doesn't have a stem") + .to_str() + .expect("Non-UTF8 file names aren't allowed") + .into(), + const_name + ); +}