CLI commands

This commit is contained in:
Piotr Siuszko 2025-01-13 21:20:23 +01:00
parent e93aa6a674
commit e5781c912d
8 changed files with 277 additions and 123 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
/dist
skyline-packer-output.png
result.png
Cargo.lock

2
Cargo.lock generated
View File

@ -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]]

View File

@ -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 = [

View File

@ -112,7 +112,7 @@ impl RpackAssetHelper for RpackAtlasAsset {
},
self.image.clone(),
)),
None => Err(RpackAtlasError::WrongKey),
_ => Err(RpackAtlasError::WrongKey),
}
}

View File

@ -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 }
glob = { version = "0.3", optional = true }
anyhow = "1"
thiserror = "2"

View File

@ -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<String>,
/// size of the tilemap, default: 512
#[arg(long)]
size: Option<u32>,
/// Image format
#[clap(short, long)]
format: Option<SaveImageFormat>,
},
/// 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<String>,
/// size of the tilemap, default: 512
#[arg(long)]
size: Option<u32>,
/// Image format, png by default
#[clap(short, long)]
format: Option<SaveImageFormat>,
/// Asset sources path, argument can be passed multiple times
#[clap(short, long)]
source_paths: Vec<String>,
},
/// 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<String>,
size: Option<u32>,
format: Option<SaveImageFormat>,
) -> 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<ImageFile> = 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<String>,
size: Option<u32>,
format: Option<SaveImageFormat>,
source_paths: Vec<String>,
) -> 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<ImageFile> = 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(())
}
}

View File

@ -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<SaveImageFormat> 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<P>(
config: TexturePackerConfig,
images: &[ImageFile],
filename: P,
) -> Result<Self, String>
) -> Result<Self, SpritesheetError>
where
P: AsRef<str>,
{
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<String>,
pub output_path: String,
pub format: SaveImageFormat,
pub size: u32
}

View File

@ -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<SaveImageFormat> 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<String>,
/// size of the tilemap, default: 512
#[arg(long)]
size: Option<u32>,
/// Image format
#[clap(short, long)]
format: Option<SaveImageFormat>,
#[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<ImageFile> = 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(())
}