diff --git a/.gitignore b/.gitignore index e42b161..215685e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /dist skyline-packer-output.png result.png +Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index 203f20b..aff6a15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4674,6 +4674,7 @@ dependencies = [ name = "rpack_cli" version = "0.0.0" dependencies = [ + "anyhow", "basis-universal", "bevy_rpack", "clap", @@ -4683,6 +4684,7 @@ dependencies = [ "serde", "serde_json", "texture_packer", + "thiserror 2.0.9", ] [[package]] diff --git a/crates/bevy_rpack/Cargo.toml b/crates/bevy_rpack/Cargo.toml index 4f7c90a..e4debc4 100644 --- a/crates/bevy_rpack/Cargo.toml +++ b/crates/bevy_rpack/Cargo.toml @@ -20,8 +20,8 @@ bevy = { version = "0.15", optional = true, default-features = false, features = "bevy_ui", ] } serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -thiserror = "2.0" +serde_json = "1" +thiserror = "2" [dev-dependencies] bevy = { version = "0.15", default-features = false, features = [ diff --git a/crates/bevy_rpack/src/plugin.rs b/crates/bevy_rpack/src/plugin.rs index fed2fd8..a0b8351 100644 --- a/crates/bevy_rpack/src/plugin.rs +++ b/crates/bevy_rpack/src/plugin.rs @@ -112,7 +112,7 @@ impl RpackAssetHelper for RpackAtlasAsset { }, self.image.clone(), )), - None => Err(RpackAtlasError::WrongKey), + _ => Err(RpackAtlasError::WrongKey), } } diff --git a/crates/rpack_cli/Cargo.toml b/crates/rpack_cli/Cargo.toml index 0e4736b..13c23f8 100644 --- a/crates/rpack_cli/Cargo.toml +++ b/crates/rpack_cli/Cargo.toml @@ -21,4 +21,6 @@ image = { version = "0.25", features = ["jpeg", "png"] } clap = { version = "4", features = ["derive"], optional = true } basis-universal = { version = "0.3.1", optional = true } image_dds = { version = "0.6.2", optional = true } -glob = { version = "0.3", optional = true } \ No newline at end of file +glob = { version = "0.3", optional = true } +anyhow = "1" +thiserror = "2" \ No newline at end of file diff --git a/crates/rpack_cli/src/commands.rs b/crates/rpack_cli/src/commands.rs new file mode 100644 index 0000000..00da693 --- /dev/null +++ b/crates/rpack_cli/src/commands.rs @@ -0,0 +1,201 @@ +use std::{io::Write, path::Path}; + +use clap::Subcommand; +use rpack_cli::{ImageFile, Spritesheet, TilemapGenerationConfig}; + +use rpack_cli::SaveImageFormat; + +#[derive(Subcommand, Debug, Clone)] +pub enum Commands { + /// Generates a tilemap + Generate { + /// Name of the tilemap to build, when no value is provided uses 'tilemap' + #[clap(action)] + name: Option, + /// size of the tilemap, default: 512 + #[arg(long)] + size: Option, + /// Image format + #[clap(short, long)] + format: Option, + }, + /// Creates a tilemap generation config that can be used by this tool + ConfigCreate { + /// path of the config to create + #[clap(action)] + config_path: String, + /// path of the tilemap to build, when no value is provided uses '/tilemap' + #[clap(long)] + output_path: Option, + /// size of the tilemap, default: 512 + #[arg(long)] + size: Option, + /// Image format, png by default + #[clap(short, long)] + format: Option, + /// Asset sources path, argument can be passed multiple times + #[clap(short, long)] + source_paths: Vec, + }, + /// Generates a tilemap from config + GenerateFromConfig { + /// path of the config to use + #[clap(action)] + config_path: String, + }, +} +impl Commands { + pub(crate) fn run(&self) -> anyhow::Result<()> { + match self.clone() { + Commands::Generate { name, size, format } => Self::generate_tilemap(name, size, format), + Commands::ConfigCreate { + config_path, + output_path, + size, + format, + source_paths, + } => Self::create_config(config_path, output_path, size, format, source_paths), + Commands::GenerateFromConfig { config_path } => { + Self::generate_tilemap_from_config(config_path) + } + } + } + + fn generate_tilemap( + name: Option, + size: Option, + format: Option, + ) -> anyhow::Result<()> { + let name = name.unwrap_or("tilemap".to_owned()); + let format = format.unwrap_or_default(); + let atlas_filename = format!("{}{}", name, format); + let atlas_json_filename = format!("{}.rpack.json", name); + let size = size.unwrap_or(512); + + let images: Vec = glob::glob("**/*png")? + .flatten() + .flat_map(|f| ImageFile::at_path(&f, f.to_str().unwrap_or_default())) + .collect(); + let spritesheet = Spritesheet::build( + texture_packer::TexturePackerConfig { + max_width: size, + max_height: size, + allow_rotation: false, + force_max_dimensions: true, + border_padding: 2, + texture_padding: 2, + texture_extrusion: 2, + trim: false, + texture_outlines: false, + }, + &images, + &atlas_filename, + )?; + + if Path::new(&atlas_json_filename).exists() { + std::fs::remove_file(&atlas_json_filename).expect("Could not remove the old file"); + } + if Path::new(&atlas_filename).exists() { + std::fs::remove_file(&atlas_filename).expect("Could not remove the old file"); + } + match format { + SaveImageFormat::Dds => { + #[cfg(feature = "dds")] + spritesheet.save_as_dds(&atlas_filename); + #[cfg(not(feature = "dds"))] + panic!("Program is compiled without support for dds. Compile it yourself with feature `dds` enabled."); + } + f => { + spritesheet + .image_data + .save_with_format(&atlas_filename, f.into())?; + } + } + let json = serde_json::to_string_pretty(&spritesheet.atlas_asset_json)?; + let mut file = std::fs::File::create(&atlas_json_filename)?; + file.write_all(json.as_bytes())?; + Ok(()) + } + fn create_config( + config_path: String, + output_path: Option, + size: Option, + format: Option, + source_paths: Vec, + ) -> Result<(), anyhow::Error> { + let name = output_path.unwrap_or("tilemap".to_owned()); + let format = format.unwrap_or_default(); + let size = size.unwrap_or(512); + let config = TilemapGenerationConfig { + size, + asset_paths: source_paths, + output_path: name, + format, + }; + + let json = serde_json::to_string_pretty(&config)?; + let mut file = std::fs::File::create(format!("{}.rpack_gen.json", config_path)).unwrap(); + file.write_all(json.as_bytes())?; + + Ok(()) + } + + fn generate_tilemap_from_config(config_path: String) -> anyhow::Result<()> { + let config_file = std::fs::read_to_string(&config_path)?; + let config: TilemapGenerationConfig = serde_json::from_str(&config_file)?; + + let images: Vec = config + .asset_paths + .iter() + .flat_map(|path| { + let pattern = format!("{}/*png", path); + glob::glob(&pattern) + .unwrap() + .flatten() + .flat_map(|f| ImageFile::at_path(&f, f.to_str().unwrap_or_default())) + }) + .collect(); + let atlas_filename = format!("{}{}", config.output_path, config.format); + let atlas_json_filename = format!("{}.rpack.json", config.output_path); + let spritesheet = Spritesheet::build( + texture_packer::TexturePackerConfig { + max_width: config.size, + max_height: config.size, + allow_rotation: false, + force_max_dimensions: true, + border_padding: 2, + texture_padding: 2, + texture_extrusion: 2, + trim: false, + texture_outlines: false, + }, + &images, + &atlas_filename, + )?; + + if Path::new(&atlas_json_filename).exists() { + std::fs::remove_file(&atlas_json_filename).expect("Could not remove the old file"); + } + if Path::new(&atlas_filename).exists() { + std::fs::remove_file(&atlas_filename).expect("Could not remove the old file"); + } + match config.format { + SaveImageFormat::Dds => { + #[cfg(feature = "dds")] + spritesheet.save_as_dds(&atlas_filename); + #[cfg(not(feature = "dds"))] + panic!("Program is compiled without support for dds. Compile it yourself with feature `dds` enabled."); + } + f => { + spritesheet + .image_data + .save_with_format(&atlas_filename, f.into())?; + } + } + let json = serde_json::to_string_pretty(&spritesheet.atlas_asset_json)?; + let mut file = std::fs::File::create(&atlas_json_filename)?; + file.write_all(json.as_bytes())?; + + Ok(()) + } +} diff --git a/crates/rpack_cli/src/lib.rs b/crates/rpack_cli/src/lib.rs index c409305..f98c0e9 100644 --- a/crates/rpack_cli/src/lib.rs +++ b/crates/rpack_cli/src/lib.rs @@ -1,7 +1,9 @@ use bevy_rpack::{AtlasFrame, SerializableRect}; use image::DynamicImage; +use thiserror::Error; +use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::path::Path; +use std::{fmt::Display, path::Path}; use texture_packer::{importer::ImageImporter, TexturePacker, TexturePackerConfig}; #[derive(Clone)] @@ -34,32 +36,68 @@ impl ImageFile { } } + +#[derive(Clone, Debug, Default, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "cli", derive(clap::ValueEnum))] +pub enum SaveImageFormat { + #[default] + Png, + Dds, +} + +impl Display for SaveImageFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SaveImageFormat::Png => f.write_str(".png"), + SaveImageFormat::Dds => f.write_str(".dds"), + } + } +} + +impl From for image::ImageFormat { + fn from(val: SaveImageFormat) -> Self { + match val { + SaveImageFormat::Png => image::ImageFormat::Png, + SaveImageFormat::Dds => image::ImageFormat::Dds, + } + } +} + + +/// Errors that can occur while building a `Spritesheet`. +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum SpritesheetError { + #[error("Cannot pack image: {0}")] + CannotPackImage(String), + #[error("Failed to export tilemap image")] + FailedToExportImage, + #[error("could not parse asset: {0}")] + ParsingError(#[from] serde_json::Error), + #[error("Failed to pack image into tilemap, tilemap to small")] + FailedToPackImage, +} + impl Spritesheet { pub fn build

( config: TexturePackerConfig, images: &[ImageFile], filename: P, - ) -> Result + ) -> Result where P: AsRef, { let mut packer = TexturePacker::new_skyline(config); for image in images.iter() { if !packer.can_pack(&image.image) { - return Err(format!( - "Consider making atlas bigger. Could not make atlas, failed on: {}", - image.id - )); + return Err(SpritesheetError::CannotPackImage(image.id.clone())); } - if let Err(err) = packer.pack_own(&image.id, image.image.clone()) { - return Err(format!( - "Could not make atlas, failed on: {}, {:?}", - image.id, err - )); + if let Err(_err) = packer.pack_own(&image.id, image.image.clone()) { + return Err(SpritesheetError::FailedToPackImage); } } let Ok(image_data) = texture_packer::exporter::ImageExporter::export(&packer, None) else { - return Err("Failed to export image".to_owned()); + return Err(SpritesheetError::FailedToExportImage); }; let atlas_asset = bevy_rpack::AtlasAsset { @@ -81,9 +119,7 @@ impl Spritesheet { }) .collect(), }; - let Ok(atlas_asset_json) = serde_json::to_value(&atlas_asset) else { - return Err("Failed to deserialize".to_owned()); - }; + let atlas_asset_json = serde_json::to_value(&atlas_asset)?; Ok(Spritesheet { image_data, @@ -230,3 +266,11 @@ impl Spritesheet { .unwrap(); } } + +#[derive(Clone, Serialize, Deserialize)] +pub struct TilemapGenerationConfig { + pub asset_paths: Vec, + pub output_path: String, + pub format: SaveImageFormat, + pub size: u32 +} diff --git a/crates/rpack_cli/src/main.rs b/crates/rpack_cli/src/main.rs index 149c201..cb1fa91 100644 --- a/crates/rpack_cli/src/main.rs +++ b/crates/rpack_cli/src/main.rs @@ -1,114 +1,18 @@ -use clap::{Parser, ValueEnum}; -use rpack_cli::{ImageFile, Spritesheet}; -use std::{fmt::Display, io::Write, path::Path}; +use clap::Parser; -#[derive(Clone, Debug, Default, Copy, ValueEnum)] -pub enum SaveImageFormat { - #[default] - Png, - Dds, -} - -impl Display for SaveImageFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SaveImageFormat::Png => f.write_str(".png"), - SaveImageFormat::Dds => f.write_str(".dds"), - } - } -} - -impl From for image::ImageFormat { - fn from(val: SaveImageFormat) -> Self { - match val { - SaveImageFormat::Png => image::ImageFormat::Png, - SaveImageFormat::Dds => image::ImageFormat::Dds, - } - } -} +pub mod commands; /// Build rpack tilemaps with ease #[derive(Parser, Debug)] -#[command(version, about, long_about = None)] +#[command(version, about, long_about = "rpack CLI tool")] struct Args { - /// Name of the tilemap to build, when no value is provided uses 'tilemap' - #[arg(action)] - name: Option, - /// size of the tilemap, default: 512 - #[arg(long)] - size: Option, - /// Image format - #[clap(short, long)] - format: Option, + #[command(subcommand)] + command: crate::commands::Commands, } -fn main() { +fn main() -> anyhow::Result<()> { let args = Args::parse(); - let name = args.name.unwrap_or("tilemap".to_owned()); - let format = args.format.unwrap_or_default(); - let atlas_filename = format!("{}{}", name, format); - let atlas_json_filename = format!("{}.png", name); - let size = args.size.unwrap_or(512); - let images: Vec = glob::glob("**/*png") - .expect("Failed to find the png files") - .flatten() - .flat_map(|f| ImageFile::at_path(&f, f.to_str().unwrap_or_default())) - .collect(); - let spritesheet = Spritesheet::build( - texture_packer::TexturePackerConfig { - max_width: size, - max_height: size, - allow_rotation: false, - force_max_dimensions: true, - border_padding: 2, - texture_padding: 2, - texture_extrusion: 2, - trim: false, - texture_outlines: false, - }, - &images, - &atlas_filename, - ) - .expect("Failed to build spritesheet"); - - if Path::new(&atlas_json_filename).exists() { - std::fs::remove_file(&atlas_json_filename).expect("Could not remove the old file"); - } - if Path::new(&atlas_filename).exists() { - std::fs::remove_file(&atlas_filename).expect("Could not remove the old file"); - } - match format { - SaveImageFormat::Dds => { - #[cfg(feature = "dds")] - spritesheet.save_as_dds(&atlas_filename); - #[cfg(not(feature = "dds"))] - panic!("Program is compiled without support for dds. Compile it yourself with feature `dds` enabled."); - } - f => { - let write_result = spritesheet - .image_data - .save_with_format(&atlas_filename, f.into()); - - if write_result.is_err() { - eprintln!( - "Could not make atlas, error: {:?}", - write_result.unwrap_err() - ); - } else { - println!("Output texture stored in {}", atlas_json_filename); - } - } - } - let json = serde_json::to_string_pretty(&spritesheet.atlas_asset_json).unwrap(); - let mut file = std::fs::File::create(format!("{}.rpack.json", name)).unwrap(); - let write_result = file.write_all(json.as_bytes()); - if write_result.is_err() { - eprintln!( - "Could not make atlas, error: {:?}", - write_result.unwrap_err() - ); - } else { - println!("Output data stored in {:?}", file); - } + args.command.run()?; + Ok(()) }