mirror of https://github.com/Leinnan/rpack.git
CLI commands
This commit is contained in:
parent
e93aa6a674
commit
e5781c912d
|
|
@ -4,3 +4,4 @@
|
|||
/dist
|
||||
skyline-packer-output.png
|
||||
result.png
|
||||
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]]
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ impl RpackAssetHelper for RpackAtlasAsset {
|
|||
},
|
||||
self.image.clone(),
|
||||
)),
|
||||
None => Err(RpackAtlasError::WrongKey),
|
||||
_ => Err(RpackAtlasError::WrongKey),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue