From 0f9ab63a1fa6b51bf1a631a6d3482f3c5880f019 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Tue, 16 Sep 2025 18:14:32 +0200 Subject: [PATCH] Work on new version --- README.md | 25 +- crates/bevy_rpack/CHANGELOG.md | 12 +- crates/bevy_rpack/Cargo.toml | 2 +- crates/bevy_rpack/assets/tilemap.rpack.json | 5 + crates/bevy_rpack/src/lib.rs | 39 +++ crates/rpack_cli/Cargo.toml | 7 +- crates/rpack_cli/src/lib.rs | 131 ++++++-- crates/rpack_egui/Cargo.toml | 4 +- crates/rpack_egui/src/app.rs | 345 +++++++++++++------- crates/rpack_egui/src/fonts.rs | 77 ++--- crates/rpack_egui/src/helpers.rs | 13 + crates/rpack_egui/src/main.rs | 9 +- 12 files changed, 450 insertions(+), 219 deletions(-) diff --git a/README.md b/README.md index 39f9b27..aa20479 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Repository contains example how to use plugin in Bevy. [![Crates.io](https://img.shields.io/crates/v/rpack_cli)](https://crates.io/crates/rpack_cli) [![Documentation](https://docs.rs/rpack_cli/badge.svg)](https://docs.rs/rpack_cli) -Command line interface for generating tilemaps. +Command line interface for generating tilemaps. ```sh Build rpack tilemaps with ease @@ -74,17 +74,17 @@ rpack tools provides and work with two json based files. ### Atlas files -Tilemaps are using `.rpack.json` extension. +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 + - `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 + - `x`- x start pos of the image in the tilemap + - `y`- y start pos of the image in the tilemap Example: @@ -120,7 +120,7 @@ Example: ### Generation config files -Config files are using `.rpack_gen.json` extension. +Config files are using `.rpack_gen.json` extension. Fields: @@ -130,7 +130,7 @@ Fields: - `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 - +- `metadata`: optional, struct containing metadata about the program used to generate the tilemap and version number, stored for the future in case of future breaking changes Example: @@ -147,6 +147,15 @@ Example: "format": "Png", "size": 512, "texture_padding": 2, - "border_padding": 2 + "border_padding": 2, + "metadata": { + "app": "rpack", + "app_version": "0.3.0", + "format_version": 1 + }, + "size": [ + 512, + 512 + ] } ``` diff --git a/crates/bevy_rpack/CHANGELOG.md b/crates/bevy_rpack/CHANGELOG.md index 2a6446a..c3d08f0 100644 --- a/crates/bevy_rpack/CHANGELOG.md +++ b/crates/bevy_rpack/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.0] + +### Added + +- Introduced `metadata` field in tilemap format. + +### Changed + +- Updated to Bevy 0.17 + ## [0.2.0] - 2025-05-07 ### Added @@ -29,4 +39,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2025-01-14 -- initial version \ No newline at end of file +- initial version diff --git a/crates/bevy_rpack/Cargo.toml b/crates/bevy_rpack/Cargo.toml index e1883fd..8232dd1 100644 --- a/crates/bevy_rpack/Cargo.toml +++ b/crates/bevy_rpack/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bevy_rpack" description = "Bevy plugin with rpack atlas support" -version = "0.2.0" +version = "0.3.0" edition = "2024" repository = "https://github.com/Leinnan/rpack.git" homepage = "https://github.com/Leinnan/rpack" diff --git a/crates/bevy_rpack/assets/tilemap.rpack.json b/crates/bevy_rpack/assets/tilemap.rpack.json index 8c0a22d..3b3312e 100644 --- a/crates/bevy_rpack/assets/tilemap.rpack.json +++ b/crates/bevy_rpack/assets/tilemap.rpack.json @@ -200,6 +200,11 @@ "key": "ship/spaceBuilding_012" } ], + "metadata": { + "app": "rpack", + "app_version": "0.3.0", + "format_version": 1 + }, "size": [ 512, 512 diff --git a/crates/bevy_rpack/src/lib.rs b/crates/bevy_rpack/src/lib.rs index e5b1d2d..69e6227 100644 --- a/crates/bevy_rpack/src/lib.rs +++ b/crates/bevy_rpack/src/lib.rs @@ -1,4 +1,6 @@ #![doc = include_str!("../README.md")] +extern crate alloc; +use alloc::borrow::Cow; #[cfg(feature = "bevy")] /// Contains the Bevy plugin for handling `Rpack` assets and atlases. @@ -50,4 +52,41 @@ pub struct AtlasAsset { pub filename: String, /// A collection of frames contained within the texture atlas. pub frames: Vec, + /// Metadata about the atlas. + #[cfg_attr(feature = "bevy", reflect(default))] + #[serde(default, skip_serializing_if = "AtlasMetadata::skip_serialization")] + pub metadata: AtlasMetadata, +} + +/// Represents metadata associated with the texture atlas format. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[cfg_attr(feature = "bevy", derive(bevy::prelude::Reflect))] +pub struct AtlasMetadata { + /// The version of the texture atlas format. + pub format_version: u32, + /// The name of the application that created the atlas. + pub app: Cow<'static, str>, + /// The version of the application that created the atlas. + pub app_version: Cow<'static, str>, + /// Whether to skip serialization of the metadata. + #[serde(skip_serializing, default)] + pub skip_serialization: bool, +} + +impl AtlasMetadata { + /// Returns true if the metadata should be skipped during serialization. + pub fn skip_serialization(&self) -> bool { + self.skip_serialization + } +} + +impl Default for AtlasMetadata { + fn default() -> Self { + Self { + format_version: 1, + app: Cow::Borrowed("rpack"), + app_version: Cow::Borrowed(env!("CARGO_PKG_VERSION")), + skip_serialization: false, + } + } } diff --git a/crates/rpack_cli/Cargo.toml b/crates/rpack_cli/Cargo.toml index c84e18a..1c26397 100644 --- a/crates/rpack_cli/Cargo.toml +++ b/crates/rpack_cli/Cargo.toml @@ -5,17 +5,18 @@ 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.2.0" +version = "0.3.0" edition = "2024" [features] default = ["cli", "dds"] -cli = ["dep:clap", "dep:glob"] +cli = ["dep:clap", "dep:glob", "config_ext"] basis = ["dep:basis-universal"] dds = ["dep:image_dds"] +config_ext = ["dep:glob"] [dependencies] -bevy_rpack = { default-features = false, path = "../bevy_rpack", version = "0.2" } +bevy_rpack = { default-features = false, path = "../bevy_rpack", version = "0.3" } serde = { version = "1", features = ["derive"] } serde_json = "1" texture_packer = { workspace = true } diff --git a/crates/rpack_cli/src/lib.rs b/crates/rpack_cli/src/lib.rs index 24f117b..782ebb5 100644 --- a/crates/rpack_cli/src/lib.rs +++ b/crates/rpack_cli/src/lib.rs @@ -1,8 +1,8 @@ -use bevy_rpack::{AtlasFrame, SerializableRect}; +use bevy_rpack::{AtlasFrame, AtlasMetadata, SerializableRect}; use image::DynamicImage; use serde::{Deserialize, Serialize}; use serde_json::Value; -#[cfg(all(feature = "cli", not(target_arch = "wasm32")))] +#[cfg(all(feature = "config_ext", not(target_arch = "wasm32")))] use std::io::Write; use std::{ ffi::OsStr, @@ -19,7 +19,7 @@ pub struct Spritesheet { pub atlas_asset_json: Value, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct ImageFile { pub id: String, pub image: DynamicImage, @@ -67,6 +67,15 @@ where } } + // Ensure the prefix ends at a directory boundary + if !prefix.is_empty() && !prefix.ends_with('/') && !prefix.ends_with('\\') { + if let Some(last_slash) = prefix.rfind('/') { + prefix.truncate(last_slash + 1); + } else if let Some(last_backslash) = prefix.rfind('\\') { + prefix.truncate(last_backslash + 1); + } + } + prefix } @@ -108,21 +117,43 @@ pub enum SpritesheetError { FailedToPackImage, } +/// Configuration for building a `Spritesheet`. +#[derive(Debug, Clone, PartialEq)] +pub struct SpritesheetBuildConfig { + /// Configuration for the texture packer. + pub packer_config: TexturePackerConfig, + /// Whether to skip metadata serialization. + pub skip_metadata_serialization: bool, +} + +impl From for SpritesheetBuildConfig { + fn from(config: TexturePackerConfig) -> Self { + Self { + packer_config: config, + skip_metadata_serialization: false, + } + } +} + impl Spritesheet { pub fn build

( - config: TexturePackerConfig, - images: &[ImageFile], + config: impl Into, + images: &[&ImageFile], filename: P, ) -> Result where P: AsRef, { + let SpritesheetBuildConfig { + packer_config: config, + skip_metadata_serialization, + } = config.into(); let mut packer = TexturePacker::new_skyline(config); for image in images.iter() { if !packer.can_pack(&image.image) { return Err(SpritesheetError::CannotPackImage(image.id.clone())); } - if let Err(_err) = packer.pack_own(&image.id, image.image.clone()) { + if let Err(_err) = packer.pack_ref(&image.id, &image.image) { return Err(SpritesheetError::FailedToPackImage); } } @@ -131,6 +162,10 @@ impl Spritesheet { }; let mut atlas_asset = bevy_rpack::AtlasAsset { + metadata: AtlasMetadata { + skip_serialization: skip_metadata_serialization, + ..Default::default() + }, size: [image_data.width(), image_data.height()], filename: filename.as_ref().to_owned(), frames: packer @@ -247,11 +282,37 @@ pub struct TilemapGenerationConfig { /// Size of the padding on the outer edge of the packed image in pixel. Default value is `0`. #[serde(skip_serializing_if = "Option::is_none", default)] pub border_padding: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub skip_serializing_metadata: Option, #[serde(skip)] pub working_dir: Option, } -#[cfg(all(feature = "cli", not(target_arch = "wasm32")))] +impl From<&TilemapGenerationConfig> for TexturePackerConfig { + fn from(config: &TilemapGenerationConfig) -> Self { + texture_packer::TexturePackerConfig { + max_width: config.size.unwrap_or(2048), + max_height: config.size.unwrap_or(2048), + allow_rotation: false, + force_max_dimensions: true, + border_padding: config.border_padding.unwrap_or(0), + texture_padding: config.texture_padding.unwrap_or(2), + texture_extrusion: 0, + trim: false, + texture_outlines: false, + } + } +} +impl From<&TilemapGenerationConfig> for SpritesheetBuildConfig { + fn from(config: &TilemapGenerationConfig) -> Self { + SpritesheetBuildConfig { + packer_config: config.into(), + skip_metadata_serialization: config.skip_serializing_metadata.unwrap_or_default(), + } + } +} + +#[cfg(all(feature = "config_ext", not(target_arch = "wasm32")))] impl TilemapGenerationConfig { pub fn read_from_file

(path: P) -> anyhow::Result where @@ -263,7 +324,24 @@ impl TilemapGenerationConfig { Ok(config) } - pub fn generate(&self) -> anyhow::Result<()> { + pub fn get_file_paths_and_prefix(&self) -> (Vec, String) { + let working_dir = self.working_dir(); + let lossy_working_dir = working_dir.to_string_lossy(); + let mut file_paths: Vec = self + .asset_patterns + .iter() + .flat_map(|pattern| { + let p = format!("{}/{}", lossy_working_dir, pattern); + glob::glob(&p).expect("Wrong pattern for assets").flatten() + }) + .filter(|e| e.is_file()) + .collect(); + file_paths.sort(); + let prefix = get_common_prefix(&file_paths); + (file_paths, prefix) + } + + pub fn working_dir(&self) -> PathBuf { let dir = match &self.working_dir { None => std::env::current_dir().expect("msg"), Some(p) => { @@ -274,19 +352,15 @@ impl TilemapGenerationConfig { } } }; - let working_dir = std::path::absolute(dir)?; + let working_dir = std::path::absolute(dir).unwrap_or_default(); - let mut file_paths: Vec = self - .asset_patterns - .iter() - .flat_map(|pattern| { - let p = format!("{}/{}", working_dir.to_string_lossy(), pattern); - glob::glob(&p).expect("Wrong pattern for assets").flatten() - }) - .filter(|e| e.is_file()) - .collect(); - file_paths.sort(); - let prefix = get_common_prefix(&file_paths); + working_dir + } + + pub fn generate(&self) -> anyhow::Result<()> { + let working_dir = self.working_dir(); + + let (file_paths, prefix) = self.get_file_paths_and_prefix(); let images: Vec = file_paths .iter() .flat_map(|f| { @@ -298,6 +372,7 @@ impl TilemapGenerationConfig { ImageFile::at_path(f, id) }) .collect(); + let borrowed_images: Vec<&ImageFile> = images.iter().map(|s| s).collect(); let atlas_image_path = working_dir.join(format!( "{}{}", self.output_path, @@ -309,21 +384,7 @@ impl TilemapGenerationConfig { .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, - )?; + let spritesheet = Spritesheet::build(self, &borrowed_images, &atlas_filename)?; if Path::new(&atlas_config_path).exists() { std::fs::remove_file(&atlas_config_path).expect("Could not remove the old file"); diff --git a/crates/rpack_egui/Cargo.toml b/crates/rpack_egui/Cargo.toml index 4faef7d..367d5ef 100644 --- a/crates/rpack_egui/Cargo.toml +++ b/crates/rpack_egui/Cargo.toml @@ -14,11 +14,9 @@ eframe = { version = "0.32", 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.13" -rpack_cli = { default-features = false, path = "../rpack_cli", version = "0.2" } # You only need serde if you want app persistence: serde = { version = "1", features = ["derive"] } @@ -33,9 +31,11 @@ anyhow = "1" # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] env_logger = "0.11" +rpack_cli = { default-features = false, features = ["config_ext"], path = "../rpack_cli", version = "0.3" } # web: [target.'cfg(target_arch = "wasm32")'.dependencies] +rpack_cli = { default-features = false, path = "../rpack_cli", version = "0.3" } wasm-bindgen-futures = "0.4" wasm-bindgen = "0.2" web-sys = { version = "0.3", features = [ diff --git a/crates/rpack_egui/src/app.rs b/crates/rpack_egui/src/app.rs index 46b4a1a..bc9cbe1 100644 --- a/crates/rpack_egui/src/app.rs +++ b/crates/rpack_egui/src/app.rs @@ -1,5 +1,10 @@ -use egui::{CollapsingHeader, Color32, FontFamily, FontId, Grid, Image, Label, RichText}; -use rpack_cli::{ImageFile, Spritesheet, SpritesheetError}; +use std::path::PathBuf; + +use egui::{ + CollapsingHeader, Color32, FontFamily, FontId, Grid, Image, Label, Layout, RichText, + util::undoer::Undoer, +}; +use rpack_cli::{ImageFile, Spritesheet, SpritesheetBuildConfig, SpritesheetError}; use texture_packer::TexturePackerConfig; use crate::helpers::DroppedFileHelper; @@ -9,27 +14,58 @@ pub const HEADER_HEIGHT: f32 = 45.0; pub const TOP_BUTTON_WIDTH: f32 = 150.0; pub const GIT_HASH: &str = env!("GIT_HASH"); -/// 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 pub struct Application { - #[serde(skip)] - config: TexturePackerConfig, - #[serde(skip)] - output: Option, - #[serde(skip)] - name: String, - #[serde(skip)] - min_size: [u32; 2], - #[serde(skip)] - max_size: u32, - #[serde(skip)] - image_data: Vec, + data: ApplicationData, + output: Option, + last_error: Option, + undoer: Undoer, } -type SpriteSheetResult = Result; +#[derive(serde::Deserialize, serde::Serialize, Default, Clone)] +pub struct ApplicationData { + #[serde(skip, default)] + image_data: Vec, + #[serde(skip, default)] + config: TexturePackerConfig, + settings: Settings, +} +impl PartialEq for ApplicationData { + fn eq(&self, other: &Self) -> bool { + self.image_data == other.image_data + && self.config.allow_rotation == other.config.allow_rotation + && self.config.border_padding == other.config.border_padding + && self.config.force_max_dimensions == other.config.force_max_dimensions + && self.config.max_height == other.config.max_height + && self.config.max_width == other.config.max_width + && self.config.texture_extrusion == other.config.texture_extrusion + && self.config.texture_outlines == other.config.texture_outlines + && self.config.texture_padding == other.config.texture_padding + && self.config.trim == other.config.trim + && self.settings == other.settings + } +} -#[derive(Clone)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] +pub struct Settings { + pub filename: String, + pub size: u32, + #[serde(skip)] + min_size: [u32; 2], + pub skip_metadata_serialization: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + filename: String::from("Tilemap"), + size: 512, + min_size: [32, 32], + skip_metadata_serialization: false, + } + } +} + +#[derive(Clone, PartialEq)] pub struct AppImageData { pub width: u32, pub height: u32, @@ -54,27 +90,29 @@ impl AppImageData { impl Default for Application { fn default() -> Self { Self { - config: TexturePackerConfig { - max_width: 512, - max_height: 512, - allow_rotation: false, - border_padding: 2, - trim: false, - force_max_dimensions: true, - ..Default::default() - }, + data: Default::default(), + undoer: Default::default(), output: None, - max_size: 4096, - name: String::from("Tilemap"), - min_size: [32, 32], - image_data: Vec::new(), + last_error: None, } } } impl Application { + pub fn read_config(&mut self, config: rpack_cli::TilemapGenerationConfig) { + self.data.settings.size = config.size.unwrap_or(512); + self.data.config = (&config).into(); + + let (file_paths, prefix) = config.get_file_paths_and_prefix(); + self.data.image_data.clear(); + self.data + .image_data + .extend(file_paths.iter().flat_map(|f| f.create_image(&prefix))); + self.rebuild_image_data(); + } pub fn get_common_prefix(&self) -> String { let file_paths: Vec = self + .data .image_data .iter() .map(|image| image.path.clone()) @@ -83,65 +121,82 @@ impl Application { } pub fn rebuild_image_data(&mut self) { let prefix = self.get_common_prefix(); - self.image_data + self.data + .image_data .iter_mut() .for_each(|f| f.update_id(prefix.as_str())); self.update_min_size(); } pub fn update_min_size(&mut self) { - if let Some(file) = self.image_data.iter().max_by(|a, b| a.width.cmp(&b.width)) { - self.min_size[0] = file.width; - } else { - self.min_size[0] = 32; - } - if let Some(file) = self + self.data.settings.min_size[0] = self + .data + .image_data + .iter() + .max_by(|a, b| a.width.cmp(&b.width)) + .map_or(32, |s| s.width); + self.data.settings.min_size[1] = self + .data .image_data .iter() .max_by(|a, b| a.height.cmp(&b.height)) - { - self.min_size[1] = file.height; - } else { - self.min_size[1] = 32; + .map_or(32, |s| s.height); + for nr in [32, 64, 128, 256, 512, 1024, 2048, 4096] { + if nr >= self.data.settings.min_size[0] && nr >= self.data.settings.min_size[1] { + self.data.settings.min_size[0] = nr; + self.data.settings.min_size[1] = nr; + break; + } } } /// Called once before the first frame. - pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + pub fn new(cc: &eframe::CreationContext<'_>, config_file: Option) -> Self { crate::fonts::setup_custom_fonts(&cc.egui_ctx); // This is also where you can customize the look and feel of egui using // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. egui_extras::install_image_loaders(&cc.egui_ctx); - // Load previous app state (if any). - // Note that you must enable the `persistence` feature for this to work. - if let Some(storage) = cc.storage { - return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); + let mut app = Self::default(); + if let Some(config_file) = config_file { + if let Ok(config) = rpack_cli::TilemapGenerationConfig::read_from_file(&config_file) { + app.data.settings.filename = PathBuf::from(config_file) + .file_name() + .unwrap_or_default() + .to_string_lossy() + .replace(".rpack_gen.json", ""); + app.read_config(config); + } } - Default::default() + app } fn build_atlas(&mut self, ctx: &egui::Context) { + self.last_error = None; self.output = None; ctx.forget_image("bytes://output.png"); - let images: Vec = self - .image_data - .iter() - .map(|file| file.data.clone()) - .collect(); + if self.data.image_data.is_empty() { + return; + } + let images: Vec<&ImageFile> = self.data.image_data.iter().map(|file| &file.data).collect(); - for size in [32, 64, 128, 256, 512, 1024, 2048, 4096] { - if size < self.min_size[0] || size < self.min_size[1] { - continue; - } - if size > self.max_size { + for multiplier in 1..10 { + let size = multiplier * self.data.settings.min_size[0]; + if size > self.data.settings.size { break; } let config = TexturePackerConfig { max_width: size, max_height: size, - ..self.config + ..self.data.config }; - match Spritesheet::build(config, &images, format!("{}.png", &self.name)) { + match Spritesheet::build( + SpritesheetBuildConfig { + packer_config: config, + skip_metadata_serialization: self.data.settings.skip_metadata_serialization, + }, + &images, + format!("{}.png", &self.data.settings.filename), + ) { Ok(data) => { let mut out_vec = vec![]; data.image_data @@ -152,23 +207,26 @@ impl Application { .unwrap(); ctx.include_bytes("bytes://output.png", out_vec); - self.output = Some(Ok(data)); + self.output = Some(data); break; } Err(e) => { - self.output = Some(Err(e)); + self.last_error = Some(e); } } } + if self.output.is_some() { + self.last_error = None; + } ctx.request_repaint(); } fn save_json(&self) -> Result<(), String> { - let Some(Ok(spritesheet)) = &self.output else { + let Some(spritesheet) = &self.output else { return Err("Data is incorrect".to_owned()); }; let data = spritesheet.atlas_asset_json.to_string(); - let filename = format!("{}.rpack.json", self.name); + let filename = format!("{}.rpack.json", self.data.settings.filename); #[cfg(not(target_arch = "wasm32"))] { let path_buf = rfd::FileDialog::new() @@ -204,10 +262,10 @@ impl Application { } fn save_atlas(&self) -> Result<(), String> { - let Some(Ok(spritesheet)) = &self.output else { + let Some(spritesheet) = &self.output else { return Err("Data is incorrect".to_owned()); }; - let filename = format!("{}.png", self.name); + let filename = format!("{}.png", self.data.settings.filename); #[cfg(not(target_arch = "wasm32"))] { let path_buf = rfd::FileDialog::new() @@ -253,17 +311,10 @@ impl Application { } impl eframe::App for Application { - /// Called by the frame work to save state before shutdown. - fn save(&mut self, storage: &mut dyn eframe::Storage) { - eframe::set_value(storage, eframe::APP_KEY, self); - } - /// Called each time the UI needs repainting, which may be many times per second. fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - // if self.dropped_files.is_empty() && self.image.is_some() { - // self.image = None; - // self.data = None; - // } + self.undoer + .feed_state(ctx.input(|input| input.time), &self.data); egui::TopBottomPanel::top("topPanel") .frame(egui::Frame::canvas(&ctx.style())) .show(ctx, |ui| { @@ -277,48 +328,90 @@ impl eframe::App for Application { }); }); ctx.input(|i| { - if !i.raw.dropped_files.is_empty() { - for file in i.raw.dropped_files.iter() { - #[cfg(not(target_arch = "wasm32"))] - if let Some(path) = &file.path { - if path.is_dir() { - let Ok(dir) = path.read_dir() else { - continue; - }; - for entry in dir { - if let Ok(entry) = entry { - let Ok(metadata) = entry.metadata() else { + if i.raw.dropped_files.is_empty() { + return; + } + for file in i.raw.dropped_files.iter() { + #[cfg(not(target_arch = "wasm32"))] + if let Some(path) = &file.path { + if path.is_dir() { + let Ok(dir) = path.read_dir() else { + continue; + }; + for entry in dir { + if let Ok(entry) = entry { + let Ok(metadata) = entry.metadata() else { + continue; + }; + if metadata.is_file() { + let Some(dyn_image) = entry.create_image("") else { continue; }; - if metadata.is_file() { - let Some(dyn_image) = entry.create_image("") else { - continue; - }; - self.image_data.push(dyn_image); - } + self.data.image_data.push(dyn_image); } } } + } else { + let Some(path) = &file.path else { + continue; + }; + if path.to_string_lossy().ends_with(".rpack_gen.json") { + if let Ok(config) = + rpack_cli::TilemapGenerationConfig::read_from_file(&path) + { + self.data.settings.filename = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .replace(".rpack_gen.json", ""); + self.read_config(config); + break; + } + } } - let Some(dyn_image) = file.create_image("") else { - continue; - }; - self.image_data.push(dyn_image); } - self.output = None; - self.rebuild_image_data(); + let Some(dyn_image) = file.create_image("") else { + continue; + }; + self.data.image_data.push(dyn_image); } + self.output = None; + self.rebuild_image_data(); }); egui::TopBottomPanel::bottom("bottom_panel") .frame(egui::Frame::canvas(&ctx.style())) .show(ctx, |ui| { - powered_by_egui_and_eframe(ui); + ui.add_space(5.0); + ui.horizontal(|ui| { + ui.add_space(5.0); + ui.add_enabled_ui(self.undoer.has_undo(&self.data), |ui| { + if ui.button("⮪").on_hover_text("Go back").clicked() { + if let Some(action) = self.undoer.undo(&self.data) { + self.data = action.clone(); + self.rebuild_image_data(); + self.build_atlas(ui.ctx()); + } + } + }); + ui.add_enabled_ui(self.undoer.has_redo(&self.data), |ui| { + if ui.button("⮫").on_hover_text("Redo").clicked() { + if let Some(action) = self.undoer.redo(&self.data) { + self.data = action.clone(); + self.rebuild_image_data(); + self.build_atlas(ui.ctx()); + } + } + }); + ui.add_space(5.0); + powered_by_egui_and_eframe(ui); + }); + ui.add_space(5.0); }); egui::SidePanel::right("right") .min_width(200.0) .max_width(400.0) .frame(egui::Frame::canvas(&ctx.style())) - .show_animated(ctx, !self.image_data.is_empty(), |ui| { + .show_animated(ctx, !self.data.image_data.is_empty(), |ui| { egui::ScrollArea::vertical() .id_salt("rightPanel_scroll") .show(ui, |ui| { @@ -327,29 +420,31 @@ impl eframe::App for Application { .show(ui, |ui| { ui.vertical_centered_justified(|ui|{ let label = ui.label("Tilemap filename"); - ui.text_edit_singleline(&mut self.name).labelled_by(label.id); + ui.text_edit_singleline(&mut self.data.settings.filename).labelled_by(label.id); ui.add_space(10.0); ui.add( - egui::Slider::new(&mut self.max_size, self.min_size[0]..=4096) + egui::Slider::new(&mut self.data.settings.size, self.data.settings.min_size[0]..=4096) .step_by(32.0) .text("Max size"), ); ui.add( - egui::Slider::new(&mut self.config.border_padding, 0..=10) + egui::Slider::new(&mut self.data.config.border_padding, 0..=10) .text("Border Padding"), ); ui.add( - egui::Slider::new(&mut self.config.texture_padding, 0..=10) + egui::Slider::new(&mut self.data.config.texture_padding, 0..=10) .text("Texture Padding"), ); // ui.checkbox(&mut self.config.allow_rotation, "Allow Rotation") // .on_hover_text("True to allow rotation of the input images. Default value is `true`. Images rotated will be rotated 90 degrees clockwise."); - ui.checkbox(&mut self.config.texture_outlines, "Texture Outlines") + ui.checkbox(&mut self.data.config.texture_outlines, "Texture Outlines") .on_hover_text("Draw the red line on the edge of the each frames. Useful for debugging."); + ui.checkbox(&mut self.data.settings.skip_metadata_serialization, "Skip Metadata Serialization") + .on_hover_text("Skip metadata serialization."); // ui.checkbox(&mut self.config.trim, "Trim").on_hover_text("True to trim the empty pixels of the input images."); ui.add_space(10.0); - ui.add_enabled_ui(!self.image_data.is_empty(), |ui| { + ui.add_enabled_ui(!self.data.image_data.is_empty(), |ui| { if ui .add_sized([TOP_BUTTON_WIDTH, 30.0], egui::Button::new("Build atlas")) .clicked() @@ -367,8 +462,8 @@ impl eframe::App for Application { .show_unindented(ui, |ui| { ui.horizontal(|ui|{ - if !self.image_data.is_empty() && ui.button("clear list").clicked() { - self.image_data.clear(); + if !self.data.image_data.is_empty() && ui.button("clear list").clicked() { + self.data.image_data.clear(); self.output = None; self.update_min_size(); } @@ -379,7 +474,7 @@ impl eframe::App for Application { for file in files.iter() { let Ok(image) = texture_packer::importer::ImageImporter::import_from_file(file) else { continue }; let id = crate::helpers::id_from_path(&file.to_string_lossy()); - self.image_data.push(AppImageData { width: image.width(), height: image.height(), data: ImageFile { id: id, image }, path: file.to_string_lossy().to_string() }); + self.data.image_data.push(AppImageData { width: image.width(), height: image.height(), data: ImageFile { id: id, image }, path: file.to_string_lossy().to_string() }); } self.rebuild_image_data(); } @@ -393,7 +488,7 @@ impl eframe::App for Application { }; Grid::new("Image List").num_columns(columns).striped(true).spacing((10.0,10.0)).show(ui, |ui|{ - for (index, file) in self.image_data.iter().enumerate() { + for (index, file) in self.data.image_data.iter().enumerate() { if ui.button("x").clicked() { to_remove = Some(index); } @@ -405,7 +500,7 @@ impl eframe::App for Application { } }); if let Some(index) = to_remove { - self.image_data.remove(index); + self.data.image_data.remove(index); self.output = None; self.rebuild_image_data(); } @@ -413,18 +508,17 @@ impl eframe::App for Application { }); }); egui::CentralPanel::default().show(ctx, |ui| { + if let Some(error) = &self.last_error { + let text = egui::RichText::new(format!("Error: {}", &error)) + .font(FontId::new(20.0, FontFamily::Name("semibold".into()))) + .color(Color32::RED) + .strong(); + ui.add(egui::Label::new(text)); + } egui::ScrollArea::vertical() .id_salt("vertical_scroll") .show(ui, |ui| { - if let Some(Err(error)) = &self.output { - let text = egui::RichText::new(format!("Error: {}", &error)) - .font(FontId::new(20.0, FontFamily::Name("semibold".into()))) - .color(Color32::RED) - .strong(); - ui.add(egui::Label::new(text)); - return; - } - if self.image_data.is_empty() { + if self.data.image_data.is_empty() { ui.vertical_centered_justified(|ui| { ui.add_space(50.0); ui.add( @@ -437,7 +531,7 @@ impl eframe::App for Application { ); }); } - let Some(Ok(data)) = &self.output else { + let Some(data) = &self.output else { return; }; ui.vertical_centered_justified(|ui| { @@ -513,14 +607,17 @@ impl eframe::App for Application { } fn powered_by_egui_and_eframe(ui: &mut egui::Ui) { - ui.horizontal(|ui| { + ui.with_layout(Layout::right_to_left(egui::Align::Min), |ui| { + ui.add_space(10.0); ui.hyperlink_to(format!("Build: {}", GIT_HASH), env!("CARGO_PKG_HOMEPAGE")); egui::warn_if_debug_build(ui); ui.separator(); egui::widgets::global_theme_preference_switch(ui); ui.separator(); ui.spacing_mut().item_spacing.x = 0.0; - ui.label("Made by "); ui.hyperlink_to("Mev Lyshkin", "https://www.mevlyshkin.com/"); + ui.add_space(10.0); + ui.label("Made by "); + ui.add_space((ui.available_width() - 10.0).max(15.0)); }); } diff --git a/crates/rpack_egui/src/fonts.rs b/crates/rpack_egui/src/fonts.rs index 8d44cd1..0a6dc7b 100644 --- a/crates/rpack_egui/src/fonts.rs +++ b/crates/rpack_egui/src/fonts.rs @@ -1,62 +1,54 @@ pub fn setup_custom_fonts(ctx: &egui::Context) { // Start with the default fonts (we will be adding to them rather than replacing them). let mut fonts = egui::FontDefinitions::default(); - let Ok((regular, semibold)) = get_fonts() else { - return; - }; - fonts.font_data.insert( - "regular".to_owned(), - egui::FontData::from_owned(regular).into(), - ); - fonts.font_data.insert( - "semibold".to_owned(), - egui::FontData::from_owned(semibold).into(), - ); + if let Ok((regular, semibold)) = get_fonts() { + fonts.font_data.insert( + "regular".to_owned(), + egui::FontData::from_owned(regular).into(), + ); + fonts.font_data.insert( + "semibold".to_owned(), + egui::FontData::from_owned(semibold).into(), + ); - // Put my font first (highest priority) for proportional text: - fonts - .families - .entry(egui::FontFamily::Proportional) - .or_default() - .insert(0, "regular".to_owned()); - fonts - .families - .entry(egui::FontFamily::Name("semibold".into())) - .or_default() - .insert(0, "semibold".to_owned()); + // Put my font first (highest priority) for proportional text: + fonts + .families + .entry(egui::FontFamily::Proportional) + .or_default() + .insert(0, "regular".to_owned()); + fonts + .families + .entry(egui::FontFamily::Name("semibold".into())) + .or_default() + .insert(0, "semibold".to_owned()); - // Put my font as last fallback for monospace: - fonts - .families - .entry(egui::FontFamily::Monospace) - .or_default() - .push("regular".to_owned()); + // Put my font as last fallback for monospace: + fonts + .families + .entry(egui::FontFamily::Monospace) + .or_default() + .push("regular".to_owned()); - // Tell egui to use these fonts: - ctx.set_fonts(fonts); + // Tell egui to use these fonts: + ctx.set_fonts(fonts); + } - #[cfg(not(target_arch = "wasm32"))] - ctx.style_mut(|style| { + ctx.all_styles_mut(|style| { for font_id in style.text_styles.values_mut() { font_id.size *= 1.4; } }); } -#[cfg(all(not(target_os = "macos"), not(windows)))] +#[cfg(not(windows))] fn get_fonts() -> anyhow::Result<(Vec, Vec)> { - let regular = include_bytes!("../static/JetBrainsMonoNL-Regular.ttf").to_vec(); - let semibold = include_bytes!("../static/JetBrainsMono-SemiBold.ttf").to_vec(); + use std::fs; - Ok((regular, semibold)) -} - -#[cfg(target_os = "macos")] -fn get_fonts() -> anyhow::Result<(Vec, Vec)> { let font_path = std::path::Path::new("/System/Library/Fonts"); - let regular = std::fs::read(font_path.join("SFNSRounded.ttf"))?; - let semibold = std::fs::read(font_path.join("SFCompact.ttf"))?; + let regular = fs::read(font_path.join("SFNSRounded.ttf"))?; + let semibold = fs::read(font_path.join("SFCompact.ttf"))?; Ok((regular, semibold)) } @@ -64,7 +56,6 @@ fn get_fonts() -> anyhow::Result<(Vec, Vec)> { #[cfg(windows)] fn get_fonts() -> anyhow::Result<(Vec, Vec)> { use std::fs; - let app_data = std::env::var("APPDATA")?; let font_path = std::path::Path::new(&app_data); diff --git a/crates/rpack_egui/src/helpers.rs b/crates/rpack_egui/src/helpers.rs index 40d7aa6..c2ef59d 100644 --- a/crates/rpack_egui/src/helpers.rs +++ b/crates/rpack_egui/src/helpers.rs @@ -1,3 +1,6 @@ +#[cfg(not(target_arch = "wasm32"))] +use std::path::PathBuf; + use egui::DroppedFile; use image::DynamicImage; use rpack_cli::ImageFile; @@ -39,6 +42,16 @@ impl DroppedFileHelper for std::fs::DirEntry { ImageImporter::import_from_file(&self.path()).ok() } } +#[cfg(not(target_arch = "wasm32"))] +impl DroppedFileHelper for PathBuf { + fn file_path(&self) -> String { + self.display().to_string() + } + + fn dynamic_image(&self) -> Option { + ImageImporter::import_from_file(self.as_path()).ok() + } +} pub fn id_from_path(path: &str) -> String { match path.rfind('.') { diff --git a/crates/rpack_egui/src/main.rs b/crates/rpack_egui/src/main.rs index 4c9a029..46d7e3a 100644 --- a/crates/rpack_egui/src/main.rs +++ b/crates/rpack_egui/src/main.rs @@ -5,7 +5,12 @@ #[cfg(not(target_arch = "wasm32"))] fn main() -> eframe::Result<()> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). - + let args: Vec = std::env::args().collect(); + let file_arg: Option = if args.len() > 1 { + Some(args[1].clone()) + } else { + None + }; let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([400.0, 300.0]) @@ -15,7 +20,7 @@ fn main() -> eframe::Result<()> { eframe::run_native( "rPack", native_options, - Box::new(|cc| Ok(Box::new(rpack_egui::Application::new(cc)))), + Box::new(|cc| Ok(Box::new(rpack_egui::Application::new(cc, file_arg)))), ) }