Overhaul of the program
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
/target
|
||||
/crates/*/target
|
||||
/crates/rpack/dist
|
||||
/dist
|
||||
skyline-packer-output.png
|
||||
result.png
|
||||
|
|
|
|||
70
Cargo.toml
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
BIN
output.png
|
Before Width: | Height: | Size: 31 KiB |