Overhaul of the program

This commit is contained in:
Piotr Siuszko 2025-01-07 17:31:55 +01:00
parent f0cfe7cb8a
commit 82419d165d
32 changed files with 2688 additions and 300 deletions

View File

@ -31,17 +31,21 @@ jobs:
- name: Rust Cache # cache the rust build artefacts
uses: Swatinem/rust-cache@v2
- name: Download and install Trunk binary
working-directory: crates/rpack
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
- name: Update file
working-directory: crates/rpack
run: sed -i '15d' index.html
- name: Build
working-directory: crates/rpack
run: ./trunk build --release
- name: Update result file
working-directory: crates/rpack
run: sed -i 's|/rpack|./rpack|g' dist/index.html
- name: Itch.io - Publish
uses: KikimoraGames/itch-publish@v0.0.3
with:
gameData: ./dist
gameData: ./crates/rpack/dist
itchUsername: mevlyshkin
itchGameId: rpack
buildChannel: wasm

View File

@ -31,8 +31,10 @@ jobs:
- name: Rust Cache # cache the rust build artefacts
uses: Swatinem/rust-cache@v2
- name: Download and install Trunk binary
working-directory: crates/rpack
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
- name: Build # build
working-directory: crates/rpack
# "${GITHUB_REPOSITORY#*/}" evaluates into the name of the repository
# using --public-url something will allow trunk to modify all the href paths like from favicon.ico to repo_name/favicon.ico .
# this is necessary for github pages where the site is deployed to username.github.io/repo_name and all files must be requested
@ -42,7 +44,7 @@ jobs:
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: dist
folder: crates/rpack/dist
# this option will not maintain any history of your previous pages deployment
# set to false if you want all page build to be committed to your gh-pages branch history
single-commit: true

View File

@ -101,6 +101,8 @@ jobs:
target: wasm32-unknown-unknown
override: true
- name: Download and install Trunk binary
working-directory: crates/rpack
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
- name: Build
working-directory: crates/rpack
run: ./trunk build

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
/target
/crates/*/target
/crates/rpack/dist
/dist
skyline-packer-output.png
result.png

2218
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,18 @@
[package]
name = "rpack"
version = "0.1.0"
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"
[dependencies]
egui = "0.30"
eframe = { version = "0.30", default-features = false, features = [
"accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies.
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
] }
log = "0.4"
serde_json = "1"
egui_json_tree = "0.10"
# You only need serde if you want app persistence:
serde = { version = "1", features = ["derive"] }
texture_packer = { version = "0.27.0", features = ["common"] }
image = { version = "0.24", features = ["jpeg", "png"] }
egui_extras = { version = "*", features = ["all_loaders"] }
rfd = { version = "0.15", features = [] }
wasm-bindgen-futures = "0.4"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
env_logger = "0.11"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = [
"Url",
"HtmlAnchorElement",
"Blob",
"BlobPropertyBag",
] }
js-sys = "0.3"
[workspace]
resolver = "2"
members = [
"crates/bevy_rpack",
"crates/rpack",
"crates/rpack_cli",
]
[profile.release]
opt-level = 2 # fast and small wasm
opt-level = 'z'
panic = 'abort'
lto = true
strip = true
# Optimize all dependencies even in debug builds:
[profile.dev.package."*"]
opt-level = 2
[patch.crates-io]
# If you want to use the bleeding edge version of egui and eframe:
# egui = { git = "https://github.com/emilk/egui", branch = "master" }
# eframe = { git = "https://github.com/emilk/egui", branch = "master" }
# If you fork https://github.com/emilk/egui you can test with:
# egui = { path = "../egui/crates/egui" }
# eframe = { path = "../egui/crates/eframe" }

View File

@ -0,0 +1,23 @@
[package]
name = "bevy_rpack"
description = "Bevy plugin with rpack atlas support"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/Leinnan/rpack"
authors = ["Piotr Siuszko <siuszko@zoho.com>"]
license = "MIT OR Apache-2.0"
keywords = ["bevy", "2d", "plugin"]
[features]
default = ["bevy"]
bevy = ["dep:bevy"]
[dependencies]
bevy = { version = "0.15", optional = true, default-features = false, features = [
"bevy_asset",
"bevy_sprite",
"bevy_image",
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"

View File

@ -0,0 +1,151 @@
use crate::{AtlasAsset, SerializableRect};
use bevy::asset::{AssetLoader, AsyncReadExt};
use bevy::image::ImageSampler;
use bevy::{prelude::*, utils::HashMap};
use thiserror::Error;
/// This is an asset containing the texture atlas image, the texture atlas layout, and a map of the original file names to their corresponding indices in the texture atlas.
#[derive(Asset, Debug, Reflect)]
pub struct RpackAtlasAsset {
/// The texture atlas image.
pub image: Handle<Image>,
/// The texture atlas layout.
pub atlas: Handle<TextureAtlasLayout>,
/// The map of the original file names to indices of the texture atlas.
pub files: HashMap<String, usize>,
}
impl From<SerializableRect> for URect {
fn from(val: SerializableRect) -> Self {
URect {
min: UVec2 { x: val.x, y: val.y },
max: UVec2 {
x: val.x + val.w,
y: val.y + val.h,
},
}
}
}
impl RpackAtlasAsset {
// When atlas contains the given key returns a copy of TextureAtlas and Image
pub fn get_atlas_data<T: AsRef<str>>(&self, key: T) -> Option<(TextureAtlas, Handle<Image>)> {
self.files.get(key.as_ref()).map(|s| {
(
TextureAtlas {
index: *s,
layout: self.atlas.clone(),
},
self.image.clone(),
)
})
}
// When atlas contains the given key creates a Sprite component
pub fn make_sprite<T: AsRef<str>>(&self, key: T) -> Option<Sprite> {
if let Some((atlas, image)) = self.get_atlas_data(key) {
Some(Sprite {
image,
texture_atlas: Some(atlas),
..Default::default()
})
} else {
None
}
}
}
pub struct RpackAssetPlugin;
impl Plugin for RpackAssetPlugin {
fn build(&self, app: &mut App) {
app.register_type::<RpackAtlasAsset>();
app.init_asset::<RpackAtlasAsset>();
app.init_asset_loader::<RpackAtlasAssetLoader>();
}
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum RpackAtlasAssetError {
/// An [IO](std::io) Error that occured
/// during parsing of a `.rpack.json` file.
#[error("could not load asset: {0}")]
Io(#[from] std::io::Error),
#[error("could not parse asset: {0}")]
ParsinError(#[from] serde_json::Error),
/// A Bevy [`LoadDirectError`](bevy::asset::LoadDirectError) that occured
/// while loading a [`RpackAtlasAsset::image`](crate::RpackAtlasAsset::image).
#[error("could not load asset: {0}")]
LoadDirect(Box<bevy::asset::LoadDirectError>),
/// An error that can occur if there is
/// trouble loading the image asset of
/// an atlas.
#[error("missing image asset: {0}")]
LoadingImageAsset(String),
}
impl From<bevy::asset::LoadDirectError> for RpackAtlasAssetError {
fn from(value: bevy::asset::LoadDirectError) -> Self {
Self::LoadDirect(Box::new(value))
}
}
#[derive(Default)]
pub struct RpackAtlasAssetLoader;
impl AssetLoader for RpackAtlasAssetLoader {
type Asset = RpackAtlasAsset;
type Settings = ();
type Error = RpackAtlasAssetError;
fn extensions(&self) -> &[&str] {
&["rpack.json"]
}
async fn load(
&self,
reader: &mut dyn bevy::asset::io::Reader,
_settings: &(),
load_context: &mut bevy::asset::LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut file = String::new();
reader.read_to_string(&mut file).await?;
let asset: AtlasAsset = serde_json::from_str(&file)?;
let path = load_context
.asset_path()
.path()
.parent()
.unwrap_or(&std::path::Path::new(""))
.join(asset.name);
let mut image: Image = load_context
.loader()
.immediate()
.with_unknown_type()
.load(path)
.await?
.take()
.ok_or(RpackAtlasAssetError::LoadingImageAsset(
"failed to load image asset, does it exist".to_string(),
))?;
image.sampler = ImageSampler::nearest();
let mut layout = TextureAtlasLayout::new_empty(UVec2::new(asset.size[0], asset.size[1]));
let mut files = HashMap::new();
for frame in asset.frames {
let id = layout.add_texture(frame.frame.into());
files.insert(frame.key, id);
}
let atlas = load_context.add_labeled_asset("atlas_layout".into(), layout);
let image = load_context.add_labeled_asset("atlas_texture".into(), image);
Ok(RpackAtlasAsset {
image,
atlas,
files,
})
}
}

View File

@ -0,0 +1,28 @@
#[cfg(feature = "bevy")]
pub mod bevy;
/// Defines a rectangle in pixels with the origin at the top-left of the texture atlas.
#[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct SerializableRect {
/// Horizontal position the rectangle begins at.
pub x: u32,
/// Vertical position the rectangle begins at.
pub y: u32,
/// Width of the rectangle.
pub w: u32,
/// Height of the rectangle.
pub h: u32,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct AtlasFrame {
pub key: String,
pub frame: SerializableRect,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct AtlasAsset {
pub size: [u32; 2],
pub name: String,
pub frames: Vec<AtlasFrame>,
}

45
crates/rpack/Cargo.toml Normal file
View File

@ -0,0 +1,45 @@
[package]
name = "rpack"
version = "0.1.0"
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"
[dependencies]
egui = "0.30"
eframe = { version = "0.30", default-features = false, features = [
"accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies.
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
] }
log = "0.4"
egui_json_tree = "0.10"
rpack_cli = { default-features = false, path = "../rpack_cli" }
# You only need serde if you want app persistence:
serde = { version = "1", features = ["derive"] }
serde_json = "1"
texture_packer = { version = "0.29", features = ["common"] }
image = { version = "0.25", features = ["jpeg", "png"] }
egui_extras = { version = "*", features = ["all_loaders"] }
rfd = { version = "0.15", features = [] }
wasm-bindgen-futures = "0.4"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
env_logger = "0.11"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = [
"Url",
"HtmlAnchorElement",
"Blob",
"BlobPropertyBag",
] }
js-sys = "0.3"

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 314 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

@ -1,103 +1,13 @@
use std::{collections::HashMap, io::Cursor};
use egui::{CollapsingHeader, Color32, DroppedFile, FontFamily, FontId, Image, RichText, Vec2};
use image::DynamicImage;
use serde_json::Value;
use texture_packer::{importer::ImageImporter, TexturePacker, TexturePackerConfig};
use rpack_cli::{ImageFile, Spritesheet};
use texture_packer::{importer::ImageImporter, TexturePackerConfig};
pub const MY_ACCENT_COLOR32: Color32 = Color32::from_rgb(230, 102, 1);
pub const TOP_SIDE_MARGIN: f32 = 10.0;
pub const HEADER_HEIGHT: f32 = 45.0;
pub const TOP_BUTTON_WIDTH: f32 = 150.0;
pub const GIT_HASH: &str = env!("GIT_HASH");
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct AtlasFrame {
pub key: String,
pub frame: SerializableRect,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct AtlasAsset {
pub size: [u32; 2],
pub name: String,
pub frames: Vec<AtlasFrame>,
}
#[derive(Clone)]
pub struct Spritesheet {
pub data: Vec<u8>,
pub frames: HashMap<String, texture_packer::Frame<String>>,
pub atlas_asset_json: Value,
pub size: (u32, u32),
}
/// Boundaries and properties of a packed texture.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct SerializableFrame {
/// Key used to uniquely identify this frame.
pub key: String,
/// Rectangle describing the texture coordinates and size.
pub frame: SerializableRect,
/// True if the texture was rotated during packing.
/// If it was rotated, it was rotated 90 degrees clockwise.
pub rotated: bool,
/// True if the texture was trimmed during packing.
pub trimmed: bool,
// (x, y) is the trimmed frame position at original image
// (w, h) is original image size
//
// w
// +--------------+
// | (x, y) |
// | ^ |
// | | |
// | ********* |
// | * * | h
// | * * |
// | ********* |
// | |
// +--------------+
/// Source texture size before any trimming.
pub source: SerializableRect,
}
impl From<texture_packer::Frame<String>> for SerializableFrame {
fn from(value: texture_packer::Frame<String>) -> Self {
SerializableFrame {
key: value.key,
frame: value.frame.into(),
rotated: value.rotated,
trimmed: value.trimmed,
source: value.source.into(),
}
}
}
/// Defines a rectangle in pixels with the origin at the top-left of the texture atlas.
#[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct SerializableRect {
/// Horizontal position the rectangle begins at.
pub x: u32,
/// Vertical position the rectangle begins at.
pub y: u32,
/// Width of the rectangle.
pub w: u32,
/// Height of the rectangle.
pub h: u32,
}
impl From<texture_packer::Rect> for SerializableRect {
fn from(value: texture_packer::Rect) -> Self {
SerializableRect {
h: value.h,
w: value.w,
x: value.x,
y: value.y,
}
}
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] // if we add new fields, give them default values when deserializing old state
@ -106,15 +16,14 @@ pub struct TemplateApp {
dropped_files: Vec<DroppedFile>,
#[serde(skip)]
config: TexturePackerConfig,
#[serde(skip)]
image: Option<Image<'static>>,
#[serde(skip)]
name: String,
#[serde(skip)]
counter: i32,
#[serde(skip)]
data: Option<Spritesheet>,
#[serde(skip)]
error: Option<String>,
data: Option<Result<Spritesheet, String>>,
}
impl Default for TemplateApp {
@ -132,7 +41,7 @@ impl Default for TemplateApp {
counter: 0,
image: None,
data: None,
error: None,
name: String::from("Tilemap"),
}
}
}
@ -171,103 +80,78 @@ impl TemplateApp {
prefix
}
pub fn image_from_dropped_file<P>(file: &DroppedFile, prefix: P) -> Option<ImageFile>
where
P: AsRef<str>,
{
let id;
#[cfg(not(target_arch = "wasm32"))]
{
let path = file.path.as_ref().unwrap().clone();
id = path.to_str().unwrap().to_owned();
}
#[cfg(target_arch = "wasm32")]
{
id = file.name.clone();
}
let base_id = id.replace(".png", "");
fn build_atlas(&mut self, ctx: &egui::Context) {
self.error = None;
let mut packer = TexturePacker::new_skyline(self.config);
let prefix = Self::get_common_prefix(&self.dropped_files);
println!("Prefix: {}", prefix);
for file in &self.dropped_files {
let base_id = file_path(file);
let id = base_id
.strip_prefix(&prefix)
.strip_prefix(prefix.as_ref())
.unwrap_or(&base_id)
.to_owned()
.replace("\\", "/");
println!("Base id: {}, ID: {}", &base_id, &id);
let texture = dynamic_image_from_file(file);
let can_pack = packer.can_pack(&texture);
if can_pack {
packer.pack_own(id, texture).unwrap();
} else {
self.error = Some(format!(
"Consider making atlas bigger. Could not make atlas, failed on: {}",
id
));
return;
}
}
for (name, frame) in packer.get_frames() {
println!(" {:7} : {:?}", name, frame.frame);
}
let mut out_vec = vec![];
let exported_image = texture_packer::exporter::ImageExporter::export(&packer).unwrap();
let mut img = image::DynamicImage::new_rgba8(self.config.max_width, self.config.max_height);
image::imageops::overlay(&mut img, &exported_image, 0, 0);
img.write_to(&mut Cursor::new(&mut out_vec), image::ImageFormat::Png)
.unwrap();
let atlas = AtlasAsset {
size: [img.width(), img.height()],
name: "Atlas".to_owned(),
frames: packer
.get_frames()
.values()
.map(|v| -> AtlasFrame {
AtlasFrame {
key: v.key.clone(),
frame: SerializableRect {
x: v.frame.x,
y: v.frame.y,
w: v.frame.w,
h: v.frame.h,
},
}
})
.collect(),
let Some(image) = dynamic_image_from_file(file) else {
return None;
};
let frames_string = serde_json::to_string_pretty(&atlas).unwrap();
Some(ImageFile { id, image })
}
let atlas_asset_json = serde_json::from_str(&frames_string).unwrap();
self.data = Some(Spritesheet {
data: out_vec.clone(),
frames: packer.get_frames().clone(),
size: (img.width(), img.height()),
atlas_asset_json,
});
let id = format!("bytes://output_{}.png", self.counter);
self.image = None;
ctx.forget_image(&id);
self.counter += 1;
fn build_atlas(&mut self, ctx: &egui::Context) {
let prefix = Self::get_common_prefix(&self.dropped_files);
println!("Prefix: {}", prefix);
let images: Vec<ImageFile> = self
.dropped_files
.iter()
.flat_map(|f| Self::image_from_dropped_file(f, &prefix))
.collect();
let id = format!("bytes://output_{}.png", self.counter);
ctx.include_bytes(id.clone(), out_vec.clone());
self.image = Some(Image::from_uri(id.clone()).max_size(Vec2::new(256.0, 256.0)));
self.data = Some(Spritesheet::build(
self.config,
&images,
"name".to_owned(),
));
if let Some(Ok(data)) = &self.data {
ctx.include_bytes("bytes://output.png", data.image_data.clone());
self.image =
Some(Image::from_uri("bytes://output.png").max_size(Vec2::new(256.0, 256.0)));
}
ctx.request_repaint();
}
fn save_atlas(&mut self) {
if self.data.is_none() {
let Some(Ok(data)) = &self.data else {
return;
}
};
let data = data.image_data.clone();
let filename = format!("{}.png", self.name);
#[cfg(not(target_arch = "wasm32"))]
{
let data = self.data.clone().unwrap().data;
use std::io::Write;
let path_buf = rfd::FileDialog::new()
.set_directory(".")
.add_filter("Image", &["png"])
.set_file_name("output.png")
.set_file_name(filename)
.save_file();
if let Some(path) = path_buf {
let mut file = std::fs::File::create(path).unwrap();
let write_result = file.write_all(&data);
if write_result.is_err() {
self.error = Some(format!(
self.data = Some(Err(format!(
"Could not make atlas, error: {:?}",
write_result.unwrap_err()
));
)));
} else {
println!("Output texture stored in {:?}", file);
}
@ -275,11 +159,10 @@ impl TemplateApp {
}
#[cfg(target_arch = "wasm32")]
{
let data = self.data.clone().unwrap().data;
wasm_bindgen_futures::spawn_local(async move {
let file = rfd::AsyncFileDialog::new()
.set_directory(".")
.set_file_name("output.png")
.set_file_name(filename)
.save_file()
.await;
match file {
@ -378,6 +261,8 @@ impl eframe::App for TemplateApp {
)
.clicked()
{
self.image = None;
ctx.forget_image("bytes://output.png");
self.build_atlas(ctx);
}
});
@ -398,7 +283,7 @@ impl eframe::App for TemplateApp {
});
egui::CentralPanel::default().show(ctx, |ui| {
if let Some(error) = &self.error {
if let Some(Err(error)) = &self.data {
let text = egui::RichText::new(format!("Error: {}",&error))
.font(FontId::new(20.0, FontFamily::Name("semibold".into())))
.color(Color32::RED)
@ -413,6 +298,8 @@ impl eframe::App for TemplateApp {
CollapsingHeader::new("Settings")
.default_open(false)
.show(ui, |ui| {
ui.label("Tilemap id");
ui.text_edit_singleline(&mut self.name);
ui.add(
egui::Slider::new(&mut self.config.max_width, 64..=4096).text("Width"),
);
@ -435,9 +322,9 @@ impl eframe::App for TemplateApp {
ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui|{
egui::ScrollArea::vertical().auto_shrink(false).show(ui, |ui| {
if let Some(data) = &self.data {
if let Some(Ok(data)) = &self.data {
ui.horizontal_top(|ui|{
ui.label(format!("{} frames, size: {}x{}",data.frames.len(),data.size.0,data.size.1));
ui.label(format!("{} frames, size: {}x{}",data.atlas_asset.frames.len(),data.atlas_asset.size[0],data.atlas_asset.size[1]));
});
ui.label(RichText::new("Frames JSON").strong());
egui_json_tree::JsonTree::new("simple-tree", &data.atlas_asset_json).show(ui);
@ -506,20 +393,30 @@ fn file_path(file: &DroppedFile) -> String {
id.replace(".png", "")
}
fn dynamic_image_from_file(file: &DroppedFile) -> DynamicImage {
fn dynamic_image_from_file(file: &DroppedFile) -> Option<DynamicImage> {
#[cfg(target_arch = "wasm32")]
{
let bytes = file.bytes.as_ref().clone();
let Some(bytes) = file.bytes.as_ref().clone() else {
return None;
};
ImageImporter::import_from_memory(&bytes.unwrap())
.expect("Unable to import file. Run this example with --features=\"png\"")
if let Ok(r) = ImageImporter::import_from_memory(&bytes.unwrap()) {
Some(r.into())
} else {
None
}
}
#[cfg(not(target_arch = "wasm32"))]
{
let path = file.path.as_ref().unwrap().clone();
let Some(path) = file.path.as_ref() else {
return None;
};
ImageImporter::import_from_file(&path)
.expect("Unable to import file. Run this example with --features=\"png\"")
if let Ok(r) = ImageImporter::import_from_file(path) {
Some(r)
} else {
None
}
}
}

View File

@ -0,0 +1,18 @@
[package]
name = "rpack_cli"
authors = ["Piotr Siuszko <siuszko@zoho.com>"]
license = "MIT"
edition = "2021"
[features]
default = ["cli"]
cli = ["dep:clap", "dep:glob"]
[dependencies]
clap = { version = "4", features = ["derive"], optional = true}
bevy_rpack = { default-features = false, path = "../bevy_rpack" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
texture_packer = { version = "0.29", features = ["common"] }
image = { version = "0.25", features = ["jpeg", "png"] }
glob = {version = "0.3", optional = true}

View File

@ -0,0 +1,94 @@
use bevy_rpack::{AtlasFrame, SerializableRect};
use image::DynamicImage;
use serde_json::Value;
use std::{io::Cursor, path::PathBuf};
use texture_packer::{importer::ImageImporter, TexturePacker, TexturePackerConfig};
#[derive(Clone)]
pub struct Spritesheet {
pub image_data: Vec<u8>,
pub atlas_asset: bevy_rpack::AtlasAsset,
pub atlas_asset_json: Value,
}
#[derive(Clone)]
pub struct ImageFile {
pub id: String,
pub image: DynamicImage,
}
impl ImageFile {
pub fn at_path<P>(path: &PathBuf, id: P) -> Option<ImageFile>
where
P: AsRef<str>,
{
if let Ok(image) = ImageImporter::import_from_file(&path) {
Some(ImageFile {
image,
id: id.as_ref().to_owned().replace("\\", "/"),
})
} else {
None
}
}
}
impl Spritesheet {
pub fn build(
config: TexturePackerConfig,
images: &[ImageFile],
name: String,
) -> Result<Self, String> {
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
));
}
if let Err(err) = packer.pack_own(&image.id, image.image.clone()) {
return Err(format!(
"Could not make atlas, failed on: {}, {:?}",
image.id, err
));
}
}
let mut out_vec = vec![];
let exported_image =
texture_packer::exporter::ImageExporter::export(&packer, None).unwrap();
let mut img = image::DynamicImage::new_rgba8(config.max_width, config.max_height);
image::imageops::overlay(&mut img, &exported_image, 0, 0);
img.write_to(&mut Cursor::new(&mut out_vec), image::ImageFormat::Png)
.unwrap();
let atlas_asset = bevy_rpack::AtlasAsset {
size: [img.width(), img.height()],
name,
frames: packer
.get_frames()
.values()
.map(|v| -> AtlasFrame {
AtlasFrame {
key: v.key.clone(),
frame: SerializableRect {
x: v.frame.x,
y: v.frame.y,
w: v.frame.w,
h: v.frame.h,
},
}
})
.collect(),
};
let Ok(atlas_asset_json) = serde_json::to_value(&atlas_asset) else {
return Err("Failed to deserialize".to_owned());
};
Ok(Spritesheet {
image_data: out_vec.clone(),
atlas_asset,
atlas_asset_json,
})
}
}

View File

@ -0,0 +1,66 @@
use std::io::Write;
use clap::Parser;
use rpack_cli::{ImageFile, Spritesheet};
/// Build rpack tilemaps with ease
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// Name of the tilemap to build, when no value is provided uses 'tilemap'
#[arg(short, long)]
name: Option<String>,
/// size of the tilemap, default: 512
#[arg(long)]
size: Option<u32>,
}
fn main() {
let args = Args::parse();
let name = args.name.unwrap_or("tilemap".to_owned());
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,
name.clone(),
)
.expect("Failed to build spritesheet");
let mut file = std::fs::File::create(format!("{}.png", name)).unwrap();
let write_result = file.write_all(&spritesheet.image_data);
if write_result.is_err() {
eprintln!(
"Could not make atlas, error: {:?}",
write_result.unwrap_err()
);
} else {
println!("Output texture stored in {:?}", file);
}
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);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB