Configs, docs, CLI

This commit is contained in:
Piotr Siuszko 2025-01-14 11:58:34 +01:00
parent d41a39688f
commit b6395891bc
56 changed files with 621 additions and 7074 deletions

3
.gitignore vendored
View File

@ -4,4 +4,7 @@
/dist
skyline-packer-output.png
result.png
/Cargo.lock
/.idea
/.vscode
Cargo.lock

6831
Cargo.lock generated

File diff suppressed because it is too large Load Diff

107
README.md
View File

@ -27,4 +27,109 @@ Repository contains example how to use plugin in Bevy.
## rPack CLI
Command line interface for generating tilemaps. Code stored in `crates/rpack_cli` directory. Right now this is the part of the project that could change the most.
Command line interface for generating tilemaps.
```sh
Build rpack tilemaps with ease
Usage: rpack_cli <COMMAND>
Commands:
generate Generates a tilemap
config-create Creates a tilemap generation config
generate-from-config Generates a tilemap from config
help Print this message or the help of the given subcommand(s)
Options:
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
```
Available at [crates/rpack_cli](https://github.com/Leinnan/rpack/tree/master/crates/v).
## Used formats
rpack tools provides and work with two json based files.
### Atlas files
Tilemaps are using `.rpack.json` extension.
Fields:
- `size`: two element array- width and height of the tilemap
- `filename`: string- path to the atlas image file, relative to the config file
- `frames`: array- contain info about each frame in tilemap, contains `key` string field and `frame` field that is made up from fields:
- `h`- image height
- `w`- image width
- `x`- x start pos of the image in the tilemap
- `y`- y start pos of the image in the tilemap
Example:
```json
{
"filename": "tilemap.png",
"frames": [
{
"frame": {
"h": 42,
"w": 42,
"x": 418,
"y": 66
},
"key": "tiles/ship/spaceBuilding_001"
},
{
"frame": {
"h": 44,
"w": 34,
"x": 2,
"y": 2
},
"key": "tiles/agents/spaceAstronauts_004"
},
],
"size": [
512,
512
]
}
```
### Generation config files
Config files are using `.rpack_gen.json` extension.
Fields:
- `output_path`: string- path relative to the config, without extension, this is where tilemap image and `.rpack.json` config file are going to be saved
- `asset_patterns`: array of strings- search patterns for images to be included, relative paths to the config
- `format`: optional(defaults to `Png`), format of the tilemap image, currently supported values: `Png`, `Dds`
- `size`: optional(defaults to `2048`), size of the tilemap image
- `texture_padding`: optional(defaults to `2`), size of the padding between frames in pixel
- `border_padding`: optional(defaults to `0`), size of the padding on the outer edge of the packed image in pixel
Example:
```json
{
"asset_patterns": [
"tiles/agents/*",
"tiles/effects/*",
"tiles/missiles/*",
"tiles/ship/spaceBuilding_00*",
"tiles/ship/spaceBuilding_01*"
],
"output_path": "assets/tilemap",
"format": "Png",
"size": 512,
"texture_padding": 2,
"border_padding": 2
}
```

View File

@ -3,10 +3,12 @@ name = "bevy_rpack"
description = "Bevy plugin with rpack atlas support"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/Leinnan/rpack"
repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack"
authors = ["Piotr Siuszko <siuszko@zoho.com>"]
license = "MIT OR Apache-2.0"
keywords = ["bevy", "2d", "plugin"]
exclude = ["assets","tiles","*.rpack_gen.json", "justfile"]
[features]
default = ["bevy"]

View File

@ -5,6 +5,8 @@ A Bevy plugin with support for the `rpack.json` atlases.
## Example
```rust
//! Simple example that loads the tilemap and once is loaded it creates a sprite with it.
use bevy::prelude::*;
use bevy_rpack::prelude::*;
@ -21,7 +23,7 @@ fn main() {
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.insert_resource(Holder(asset_server.load("Tilemap.rpack.json")));
commands.insert_resource(Holder(asset_server.load("tilemap.rpack.json")));
commands.spawn(Camera2d);
}
@ -35,13 +37,13 @@ fn on_loaded(
continue;
};
if let Ok(sprite) = assets.make_sprite_from_atlas("Sword006") {
if let Ok(sprite) = assets.try_make_sprite_from_atlas("agents/spaceAstronauts_005") {
commands.spawn(Sprite {
color: Color::linear_rgb(1.0, 0.0, 0.0),
..sprite
});
};
if let Ok(image_node) = assets.make_image_node_from_atlas("Axe010") {
if let Ok(image_node) = assets.try_make_image_node_from_atlas("agents/spaceShips_006") {
commands.spawn(image_node);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -3,70 +3,205 @@
"frames": [
{
"frame": {
"h": 34,
"w": 34,
"x": 74,
"y": 2
"h": 42,
"w": 42,
"x": 462,
"y": 89
},
"key": "Sword007"
"key": "ship/spaceBuilding_001"
},
{
"frame": {
"h": 34,
"w": 34,
"x": 38,
"h": 44,
"w": 50,
"x": 125,
"y": 2
},
"key": "Axe011"
"key": "agents/spaceAstronauts_012"
},
{
"frame": {
"h": 34,
"w": 34,
"h": 44,
"w": 37,
"x": 41,
"y": 2
},
"key": "agents/spaceAstronauts_005"
},
{
"frame": {
"h": 28,
"w": 30,
"x": 439,
"y": 31
},
"key": "effects/spaceEffects_010"
},
{
"frame": {
"h": 44,
"w": 50,
"x": 180,
"y": 2
},
"key": "agents/spaceAstronauts_018"
},
{
"frame": {
"h": 84,
"w": 42,
"x": 146,
"y": 92
},
"key": "ship/spaceBuilding_002"
},
{
"frame": {
"h": 44,
"w": 37,
"x": 83,
"y": 2
},
"key": "agents/spaceAstronauts_008"
},
{
"frame": {
"h": 57,
"w": 55,
"x": 268,
"y": 101
},
"key": "ship/spaceBuilding_012"
},
{
"frame": {
"h": 36,
"w": 37,
"x": 158,
"y": 51
},
"key": "effects/spaceEffects_013"
},
{
"frame": {
"h": 32,
"w": 32,
"x": 121,
"y": 51
},
"key": "effects/spaceEffects_012"
},
{
"frame": {
"h": 82,
"w": 114,
"x": 2,
"y": 74
"y": 51
},
"key": "Axe010"
"key": "agents/spaceShips_009"
},
{
"frame": {
"h": 34,
"h": 46,
"w": 46,
"x": 217,
"y": 101
},
"key": "ship/spaceBuilding_006"
},
{
"frame": {
"h": 21,
"w": 21,
"x": 439,
"y": 2
},
"key": "effects/spaceEffects_008"
},
{
"frame": {
"h": 24,
"w": 28,
"x": 465,
"y": 2
},
"key": "effects/spaceEffects_009"
},
{
"frame": {
"h": 22,
"w": 16,
"x": 474,
"y": 62
},
"key": "missiles/spaceMissiles_014"
},
{
"frame": {
"h": 44,
"w": 34,
"x": 2,
"y": 2
},
"key": "Sword006"
"key": "agents/spaceAstronauts_004"
},
{
"frame": {
"h": 34,
"w": 34,
"x": 2,
"y": 38
"h": 148,
"w": 94,
"x": 340,
"y": 2
},
"key": "Sword009"
"key": "agents/spaceShips_006"
},
{
"frame": {
"h": 34,
"w": 34,
"x": 74,
"y": 38
"h": 48,
"w": 12,
"x": 200,
"y": 51
},
"key": "Axe009"
"key": "missiles/spaceMissiles_010"
},
{
"frame": {
"h": 34,
"w": 34,
"x": 38,
"y": 38
"h": 94,
"w": 100,
"x": 235,
"y": 2
},
"key": "Sword008"
"key": "agents/spaceShips_003"
},
{
"frame": {
"h": 26,
"w": 26,
"x": 474,
"y": 31
},
"key": "effects/spaceEffects_011"
},
{
"frame": {
"h": 33,
"w": 18,
"x": 439,
"y": 64
},
"key": "missiles/spaceMissiles_028"
},
{
"frame": {
"h": 35,
"w": 20,
"x": 121,
"y": 88
},
"key": "missiles/spaceMissiles_040"
}
],
"size": [
128,
128
512,
512
]
}

View File

@ -0,0 +1,14 @@
{
"asset_patterns": [
"tiles/agents/*",
"tiles/effects/*",
"tiles/missiles/*",
"tiles/ship/spaceBuilding_00*",
"tiles/ship/spaceBuilding_01*"
],
"output_path": "assets/tilemap",
"format": "Png",
"texture_padding": 5,
"border_padding": 2,
"size": 512
}

View File

@ -30,13 +30,13 @@ fn on_loaded(
continue;
};
if let Ok(sprite) = assets.make_sprite_from_atlas("Sword006") {
if let Ok(sprite) = assets.try_make_sprite_from_atlas("agents/spaceAstronauts_005") {
commands.spawn(Sprite {
color: Color::linear_rgb(1.0, 0.0, 0.0),
..sprite
});
};
if let Ok(image_node) = assets.make_image_node_from_atlas("Axe010") {
if let Ok(image_node) = assets.try_make_image_node_from_atlas("agents/spaceShips_006") {
commands.spawn(image_node);
}
}

View File

@ -0,0 +1,5 @@
build_atlas:
rpack_cli example_config.rpack_gen.json
prepare:
cargo install --path ../rpack_cli

View File

@ -47,15 +47,23 @@ pub trait RpackAssetHelper {
key: T,
) -> Result<(TextureAtlas, Handle<Image>), RpackAtlasError>;
/// Creates a [`Sprite`] component for the given atlas key, if available in any of the loaded Atlases.
fn make_sprite_from_atlas<T: AsRef<str>>(&self, key: T) -> Result<Sprite, RpackAtlasError>;
fn try_make_sprite_from_atlas<T: AsRef<str>>(&self, key: T) -> Result<Sprite, RpackAtlasError>;
/// Creates a [`ImageNode`] component for the given atlas key, if available in any of the loaded Atlases.
fn make_image_node_from_atlas<T: AsRef<str>>(
fn try_make_image_node_from_atlas<T: AsRef<str>>(
&self,
key: T,
) -> Result<ImageNode, RpackAtlasError>;
/// Provides list of all loaded atlas data keys
fn atlas_data_keys(&self) -> Vec<&str>;
}
impl RpackAssetHelper for Assets<RpackAtlasAsset> {
fn atlas_data_keys(&self) -> Vec<&str> {
self.iter()
.flat_map(|(_, e)| e.files.keys().map(|e| e.as_ref()))
.collect()
}
fn find_atlas_data_by_key<T: AsRef<str>>(
&self,
key: T,
@ -64,26 +72,26 @@ impl RpackAssetHelper for Assets<RpackAtlasAsset> {
return Err(RpackAtlasError::NoAtlas);
}
for (_, a) in self.iter() {
if let Ok(atlas_data) = a.find_atlas_data_by_key(key.as_ref()) {
if let Ok(atlas_data) = a.get_atlas_data(key.as_ref()) {
return Ok(atlas_data);
}
}
Err(RpackAtlasError::WrongKey)
}
fn make_sprite_from_atlas<T: AsRef<str>>(&self, key: T) -> Result<Sprite, RpackAtlasError> {
fn try_make_sprite_from_atlas<T: AsRef<str>>(&self, key: T) -> Result<Sprite, RpackAtlasError> {
if self.is_empty() {
return Err(RpackAtlasError::NoAtlas);
}
for (_, a) in self.iter() {
if let Ok(sprite) = a.make_sprite_from_atlas(key.as_ref()) {
if let Ok(sprite) = a.try_make_sprite(key.as_ref()) {
return Ok(sprite);
}
}
Err(RpackAtlasError::WrongKey)
}
fn make_image_node_from_atlas<T: AsRef<str>>(
fn try_make_image_node_from_atlas<T: AsRef<str>>(
&self,
key: T,
) -> Result<ImageNode, RpackAtlasError> {
@ -91,7 +99,7 @@ impl RpackAssetHelper for Assets<RpackAtlasAsset> {
return Err(RpackAtlasError::NoAtlas);
}
for (_, a) in self.iter() {
if let Ok(image_node) = a.make_image_node_from_atlas(key.as_ref()) {
if let Ok(image_node) = a.try_make_image_node(key.as_ref()) {
return Ok(image_node);
}
}
@ -99,8 +107,9 @@ impl RpackAssetHelper for Assets<RpackAtlasAsset> {
}
}
impl RpackAssetHelper for RpackAtlasAsset {
fn find_atlas_data_by_key<T: AsRef<str>>(
impl RpackAtlasAsset {
/// Retrieves the atlas data (texture atlas and image) for the given atlas key, if available.
pub fn get_atlas_data<T: AsRef<str>>(
&self,
key: T,
) -> Result<(TextureAtlas, Handle<Image>), RpackAtlasError> {
@ -116,19 +125,18 @@ impl RpackAssetHelper for RpackAtlasAsset {
}
}
fn make_sprite_from_atlas<T: AsRef<str>>(&self, key: T) -> Result<Sprite, RpackAtlasError> {
if let Ok((atlas, image)) = self.find_atlas_data_by_key(key) {
/// Creates a [`Sprite`] component for the given atlas key
pub fn try_make_sprite<T: AsRef<str>>(&self, key: T) -> Result<Sprite, RpackAtlasError> {
if let Ok((atlas, image)) = self.get_atlas_data(key) {
Ok(Sprite::from_atlas_image(image, atlas))
} else {
Err(RpackAtlasError::WrongKey)
}
}
fn make_image_node_from_atlas<T: AsRef<str>>(
&self,
key: T,
) -> Result<ImageNode, RpackAtlasError> {
if let Ok((atlas, image)) = self.find_atlas_data_by_key(key) {
/// Creates a [`ImageNode`] component for the given atlas key, if available in any of the loaded Atlases.
pub fn try_make_image_node<T: AsRef<str>>(&self, key: T) -> Result<ImageNode, RpackAtlasError> {
if let Ok((atlas, image)) = self.get_atlas_data(key) {
Ok(ImageNode::from_atlas_image(image, atlas))
} else {
Err(RpackAtlasError::WrongKey)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

View File

@ -1,11 +1,13 @@
[package]
name = "rpack"
version = "0.1.0"
description = "GUI application for generating rpack atlases"
authors = ["Piotr Siuszko <siuszko@zoho.com>"]
edition = "2021"
rust-version = "1.81"
repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack"
license = "MIT OR Apache-2.0"
[dependencies]
egui = "0.30"

View File

@ -61,7 +61,12 @@ impl Default for Application {
impl Application {
pub fn rebuild_image_data(&mut self) {
let prefix = crate::helpers::get_common_prefix(&self.dropped_files);
let file_paths: Vec<String> = self
.dropped_files
.iter()
.map(|dropped_file| dropped_file.file_path())
.collect();
let prefix = rpack_cli::get_common_prefix(&file_paths);
self.image_data = self
.dropped_files
@ -254,7 +259,7 @@ impl eframe::App for Application {
.color(MY_ACCENT_COLOR32)
.strong();
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, HEADER_HEIGHT));
ui.add(egui::Label::new(text));
ui.add(egui::Label::new(text).selectable(false));
});
});
ctx.input(|i| {

View File

@ -1,33 +1,8 @@
use std::path::Path;
use egui::DroppedFile;
use image::DynamicImage;
use rpack_cli::ImageFile;
use texture_packer::importer::ImageImporter;
pub fn get_common_prefix(paths: &[DroppedFile]) -> String {
if paths.is_empty() {
return String::new();
}
let full_name = paths[0].file_path();
let path = Path::new(&full_name)
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
let mut prefix = full_name.strip_suffix(&path).unwrap_or_default().to_owned();
for s in paths.iter().skip(1) {
let s = s.file_path();
while !(s.starts_with(&prefix) || prefix.is_empty()) {
prefix.pop();
}
}
prefix
}
pub trait DroppedFileHelper {
fn file_path(&self) -> String;
fn create_image<P>(&self, prefix: P) -> Option<(String, ImageFile)>

View File

@ -1,7 +1,11 @@
[package]
name = "rpack_cli"
authors = ["Piotr Siuszko <siuszko@zoho.com>"]
license = "MIT"
description = "CLI application for generating rpack atlases"
repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack"
license = "MIT OR Apache-2.0"
version = "0.1.0"
edition = "2021"
[features]
@ -21,6 +25,6 @@ thiserror = "2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
clap = { version = "4", features = ["derive"], optional = true }
basis-universal = { version = "0.3.1", optional = true }
image_dds = { version = "0.6.2", optional = true }
image_dds = { version = "0.7", optional = true }
glob = { version = "0.3", optional = true }
anyhow = "1"

View File

@ -1,7 +1,7 @@
use std::{io::Write, path::Path};
use std::io::Write;
use clap::Subcommand;
use rpack_cli::{ImageFile, Spritesheet, TilemapGenerationConfig};
use rpack_cli::TilemapGenerationConfig;
use rpack_cli::SaveImageFormat;
@ -12,14 +12,21 @@ pub enum Commands {
/// Name of the tilemap to build, when no value is provided uses 'tilemap'
#[clap(action)]
name: Option<String>,
/// size of the tilemap, default: 512
/// size of the tilemap, default: 2048
#[arg(long)]
size: Option<u32>,
/// Image format
#[clap(short, long)]
format: Option<SaveImageFormat>,
/// Asset sources path, argument can be passed multiple times
#[clap(short, long)]
source_paths: Vec<String>,
/// Size of the padding between frames in pixel. Default value is `2`
texture_padding: Option<u32>,
/// Size of the padding on the outer edge of the packed image in pixel. Default value is `0`.
border_padding: Option<u32>,
},
/// Creates a tilemap generation config that can be used by this tool
/// Creates a tilemap generation config
ConfigCreate {
/// path of the config to create
#[clap(action)]
@ -27,7 +34,7 @@ pub enum Commands {
/// 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
/// size of the tilemap, default: 2048
#[arg(long)]
size: Option<u32>,
/// Image format, png by default
@ -36,6 +43,10 @@ pub enum Commands {
/// Asset sources path, argument can be passed multiple times
#[clap(short, long)]
source_paths: Vec<String>,
/// Size of the padding between frames in pixel. Default value is `2`
texture_padding: Option<u32>,
/// Size of the padding on the outer edge of the packed image in pixel. Default value is `0`.
border_padding: Option<u32>,
},
/// Generates a tilemap from config
GenerateFromConfig {
@ -47,14 +58,38 @@ pub enum Commands {
impl Commands {
pub(crate) fn run(&self) -> anyhow::Result<()> {
match self.clone() {
Commands::Generate { name, size, format } => Self::generate_tilemap(name, size, format),
Commands::Generate {
name,
size,
format,
source_paths,
texture_padding,
border_padding,
} => Self::generate_tilemap(
name,
size,
format,
source_paths,
texture_padding,
border_padding,
),
Commands::ConfigCreate {
config_path,
output_path,
size,
format,
source_paths,
} => Self::create_config(config_path, output_path, size, format, source_paths),
texture_padding,
border_padding,
} => Self::create_config(
config_path,
output_path,
size,
format,
source_paths,
texture_padding,
border_padding,
),
Commands::GenerateFromConfig { config_path } => {
Self::generate_tilemap_from_config(config_path)
}
@ -65,72 +100,48 @@ impl Commands {
name: Option<String>,
size: Option<u32>,
format: Option<SaveImageFormat>,
source_paths: Vec<String>,
texture_padding: Option<u32>,
border_padding: Option<u32>,
) -> 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 source_paths = if source_paths.is_empty() {
vec![".".to_owned()]
} else {
source_paths
};
let config = TilemapGenerationConfig {
asset_patterns: source_paths,
output_path: name,
format,
size,
texture_padding,
border_padding,
..Default::default()
};
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,
)?;
config.generate()
}
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>,
texture_padding: Option<u32>,
border_padding: Option<u32>,
) -> 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,
asset_patterns: source_paths,
output_path: name,
format,
texture_padding,
border_padding,
..Default::default()
};
let json = serde_json::to_string_pretty(&config)?;
@ -141,61 +152,7 @@ impl Commands {
}
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(())
let config = TilemapGenerationConfig::read_from_file(config_path)?;
config.generate()
}
}

View File

@ -2,7 +2,13 @@ use bevy_rpack::{AtlasFrame, SerializableRect};
use image::DynamicImage;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{fmt::Display, path::Path};
#[cfg(all(feature = "cli", not(target_arch = "wasm32")))]
use std::io::Write;
use std::{
ffi::OsStr,
fmt::Display,
path::{Path, PathBuf},
};
use texture_packer::{importer::ImageImporter, TexturePacker, TexturePackerConfig};
use thiserror::Error;
@ -36,6 +42,34 @@ impl ImageFile {
}
}
pub fn get_common_prefix<S>(paths: &[S]) -> String
where
S: AsRef<OsStr> + Sized,
{
if paths.is_empty() {
return String::new();
}
let path = Path::new(paths[0].as_ref())
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
let mut prefix = String::from(paths[0].as_ref().to_string_lossy())
.strip_suffix(&path)
.unwrap_or_default()
.to_owned();
for s in paths.iter().skip(1) {
let s = s.as_ref().to_string_lossy();
while !(s.starts_with(&prefix) || prefix.is_empty()) {
prefix.pop();
}
}
prefix
}
#[derive(Clone, Debug, Default, Copy, Serialize, Deserialize)]
#[cfg_attr(
all(feature = "cli", not(target_arch = "wasm32")),
@ -132,7 +166,7 @@ impl Spritesheet {
#[cfg(all(feature = "dds", not(target_arch = "wasm32")))]
pub fn save_as_dds<R>(&self, output_path: R)
where
R: AsRef<str>,
R: AsRef<Path>,
{
let rgba_image = self.image_data.to_rgba8();
@ -152,7 +186,7 @@ impl Spritesheet {
#[cfg(all(feature = "basis", not(target_arch = "wasm32")))]
pub fn save_as_basis<R>(&self, output_path: R)
where
R: AsRef<str>,
R: AsRef<Path>,
{
use basis_universal::{
BasisTextureFormat, Compressor, TranscodeParameters, Transcoder,
@ -268,10 +302,122 @@ impl Spritesheet {
}
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Default)]
pub struct TilemapGenerationConfig {
pub asset_paths: Vec<String>,
pub asset_patterns: Vec<String>,
pub output_path: String,
pub format: SaveImageFormat,
pub size: u32,
/// Image format, png by default
pub format: Option<SaveImageFormat>,
/// Size of the tilemap texture. Default value is `2048`.
pub size: Option<u32>,
/// Size of the padding between frames in pixel. Default value is `2`
pub texture_padding: Option<u32>,
/// Size of the padding on the outer edge of the packed image in pixel. Default value is `0`.
pub border_padding: Option<u32>,
#[serde(skip)]
pub working_dir: Option<PathBuf>,
}
#[cfg(all(feature = "cli", not(target_arch = "wasm32")))]
impl TilemapGenerationConfig {
pub fn read_from_file<P>(path: P) -> anyhow::Result<TilemapGenerationConfig>
where
P: AsRef<Path>,
{
let config_file = std::fs::read_to_string(path.as_ref())?;
let mut config: TilemapGenerationConfig = serde_json::from_str(&config_file)?;
config.working_dir = Path::new(path.as_ref()).parent().map(|p| p.to_path_buf());
Ok(config)
}
pub fn generate(&self) -> anyhow::Result<()> {
let dir = match &self.working_dir {
None => std::env::current_dir().expect("msg"),
Some(p) => {
if p.to_string_lossy().len() == 0 {
std::env::current_dir().expect("msg")
} else {
p.clone()
}
}
};
let working_dir = match std::path::absolute(dir) {
Ok(p) => p,
Err(e) => panic!("DUPA {:?}", e),
};
let mut file_paths: Vec<PathBuf> = self
.asset_patterns
.iter()
.flat_map(|pattern| {
let p = format!("{}/{}", working_dir.to_string_lossy(), pattern);
println!("{}", p);
glob::glob(&p).expect("Wrong pattern for assets").flatten()
})
.collect();
file_paths.sort();
let prefix = get_common_prefix(&file_paths);
let images: Vec<ImageFile> = file_paths
.iter()
.flat_map(|f| {
let id = f
.to_str()
.unwrap_or_default()
.strip_prefix(&prefix)
.unwrap_or_default();
ImageFile::at_path(f, id)
})
.collect();
let atlas_image_path = working_dir.join(format!(
"{}{}",
self.output_path,
self.format.unwrap_or_default()
));
let atlas_filename = Path::new(&atlas_image_path)
.file_name()
.expect("D")
.to_string_lossy()
.to_string();
let atlas_config_path = working_dir.join(format!("{}.rpack.json", self.output_path));
let spritesheet = Spritesheet::build(
texture_packer::TexturePackerConfig {
max_width: self.size.unwrap_or(2048),
max_height: self.size.unwrap_or(2048),
allow_rotation: false,
force_max_dimensions: true,
border_padding: self.border_padding.unwrap_or(0),
texture_padding: self.texture_padding.unwrap_or(2),
texture_extrusion: 0,
trim: false,
texture_outlines: false,
},
&images,
&atlas_filename,
)?;
if Path::new(&atlas_config_path).exists() {
std::fs::remove_file(&atlas_config_path).expect("Could not remove the old file");
}
if Path::new(&atlas_image_path).exists() {
std::fs::remove_file(&atlas_image_path).expect("Could not remove the old file");
}
match self.format.unwrap_or_default() {
SaveImageFormat::Dds => {
#[cfg(feature = "dds")]
spritesheet.save_as_dds(&atlas_image_path);
#[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_image_path, f.into())?;
}
}
let json = serde_json::to_string_pretty(&spritesheet.atlas_asset_json)?;
let mut file = std::fs::File::create(&atlas_config_path)?;
file.write_all(json.as_bytes())?;
Ok(())
}
}

View File

@ -1,16 +1,29 @@
use std::path::Path;
use clap::Parser;
use rpack_cli::TilemapGenerationConfig;
pub mod commands;
/// Build rpack tilemaps with ease
#[derive(Parser, Debug)]
#[command(version, about, long_about = "rpack CLI tool")]
#[command(version, about, long_about = "Build rpack tilemaps with ease")]
#[command(propagate_version = true)]
struct Args {
#[command(subcommand)]
command: crate::commands::Commands,
}
fn main() -> anyhow::Result<()> {
let args_os = std::env::args_os();
if args_os.len() == 2 {
let arg = format!("{}", args_os.last().expect("msg").to_string_lossy());
if Path::new(&arg).exists() && arg.ends_with("rpack_gen.json") {
let config = TilemapGenerationConfig::read_from_file(&arg)?;
config.generate()?;
return Ok(());
}
}
let args = Args::parse();
args.command.run()?;

2
justfile Normal file
View File

@ -0,0 +1,2 @@
check_wasm_all:
cargo check --all-features --lib --target wasm32-unknown-unknown