Configs, docs, CLI
|
|
@ -4,4 +4,7 @@
|
|||
/dist
|
||||
skyline-packer-output.png
|
||||
result.png
|
||||
/Cargo.lock
|
||||
/.idea
|
||||
/.vscode
|
||||
Cargo.lock
|
||||
|
|
|
|||
107
README.md
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 59 KiB |
|
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
build_atlas:
|
||||
rpack_cli example_config.rpack_gen.json
|
||||
|
||||
prepare:
|
||||
cargo install --path ../rpack_cli
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 640 B |
|
After Width: | Height: | Size: 733 B |
|
After Width: | Height: | Size: 665 B |
|
After Width: | Height: | Size: 831 B |
|
After Width: | Height: | Size: 923 B |
|
After Width: | Height: | Size: 516 B |
|
After Width: | Height: | Size: 434 B |
|
After Width: | Height: | Size: 372 B |
|
After Width: | Height: | Size: 747 B |
|
After Width: | Height: | Size: 478 B |
|
After Width: | Height: | Size: 423 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 944 B |
|
After Width: | Height: | Size: 400 B |
|
After Width: | Height: | Size: 320 B |
|
After Width: | Height: | Size: 583 B |
|
After Width: | Height: | Size: 395 B |
|
After Width: | Height: | Size: 679 B |
|
After Width: | Height: | Size: 290 B |
|
After Width: | Height: | Size: 726 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 791 B |
|
After Width: | Height: | Size: 788 B |
|
After Width: | Height: | Size: 280 B |
|
After Width: | Height: | Size: 231 B |
|
After Width: | Height: | Size: 560 B |
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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)>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||