Work on new version

This commit is contained in:
Piotr Siuszko 2025-09-16 18:14:32 +02:00
parent 61d4cd239e
commit 0f9ab63a1f
12 changed files with 450 additions and 219 deletions

View File

@ -130,7 +130,7 @@ Fields:
- `size`: optional(defaults to `2048`), size of the tilemap image - `size`: optional(defaults to `2048`), size of the tilemap image
- `texture_padding`: optional(defaults to `2`), size of the padding between frames in pixel - `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 - `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: Example:
@ -147,6 +147,15 @@ Example:
"format": "Png", "format": "Png",
"size": 512, "size": 512,
"texture_padding": 2, "texture_padding": 2,
"border_padding": 2 "border_padding": 2,
"metadata": {
"app": "rpack",
"app_version": "0.3.0",
"format_version": 1
},
"size": [
512,
512
]
} }
``` ```

View File

@ -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/), 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). 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 ## [0.2.0] - 2025-05-07
### Added ### Added

View File

@ -1,7 +1,7 @@
[package] [package]
name = "bevy_rpack" name = "bevy_rpack"
description = "Bevy plugin with rpack atlas support" description = "Bevy plugin with rpack atlas support"
version = "0.2.0" version = "0.3.0"
edition = "2024" edition = "2024"
repository = "https://github.com/Leinnan/rpack.git" repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack" homepage = "https://github.com/Leinnan/rpack"

View File

@ -200,6 +200,11 @@
"key": "ship/spaceBuilding_012" "key": "ship/spaceBuilding_012"
} }
], ],
"metadata": {
"app": "rpack",
"app_version": "0.3.0",
"format_version": 1
},
"size": [ "size": [
512, 512,
512 512

View File

@ -1,4 +1,6 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
extern crate alloc;
use alloc::borrow::Cow;
#[cfg(feature = "bevy")] #[cfg(feature = "bevy")]
/// Contains the Bevy plugin for handling `Rpack` assets and atlases. /// Contains the Bevy plugin for handling `Rpack` assets and atlases.
@ -50,4 +52,41 @@ pub struct AtlasAsset {
pub filename: String, pub filename: String,
/// A collection of frames contained within the texture atlas. /// A collection of frames contained within the texture atlas.
pub frames: Vec<AtlasFrame>, pub frames: Vec<AtlasFrame>,
/// 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,
}
}
} }

View File

@ -5,17 +5,18 @@ description = "CLI application for generating rpack atlases"
repository = "https://github.com/Leinnan/rpack.git" repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack" homepage = "https://github.com/Leinnan/rpack"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
version = "0.2.0" version = "0.3.0"
edition = "2024" edition = "2024"
[features] [features]
default = ["cli", "dds"] default = ["cli", "dds"]
cli = ["dep:clap", "dep:glob"] cli = ["dep:clap", "dep:glob", "config_ext"]
basis = ["dep:basis-universal"] basis = ["dep:basis-universal"]
dds = ["dep:image_dds"] dds = ["dep:image_dds"]
config_ext = ["dep:glob"]
[dependencies] [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 = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
texture_packer = { workspace = true } texture_packer = { workspace = true }

View File

@ -1,8 +1,8 @@
use bevy_rpack::{AtlasFrame, SerializableRect}; use bevy_rpack::{AtlasFrame, AtlasMetadata, SerializableRect};
use image::DynamicImage; use image::DynamicImage;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; 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::io::Write;
use std::{ use std::{
ffi::OsStr, ffi::OsStr,
@ -19,7 +19,7 @@ pub struct Spritesheet {
pub atlas_asset_json: Value, pub atlas_asset_json: Value,
} }
#[derive(Clone)] #[derive(Clone, PartialEq)]
pub struct ImageFile { pub struct ImageFile {
pub id: String, pub id: String,
pub image: DynamicImage, 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 prefix
} }
@ -108,21 +117,43 @@ pub enum SpritesheetError {
FailedToPackImage, 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<TexturePackerConfig> for SpritesheetBuildConfig {
fn from(config: TexturePackerConfig) -> Self {
Self {
packer_config: config,
skip_metadata_serialization: false,
}
}
}
impl Spritesheet { impl Spritesheet {
pub fn build<P>( pub fn build<P>(
config: TexturePackerConfig, config: impl Into<SpritesheetBuildConfig>,
images: &[ImageFile], images: &[&ImageFile],
filename: P, filename: P,
) -> Result<Self, SpritesheetError> ) -> Result<Self, SpritesheetError>
where where
P: AsRef<str>, P: AsRef<str>,
{ {
let SpritesheetBuildConfig {
packer_config: config,
skip_metadata_serialization,
} = config.into();
let mut packer = TexturePacker::new_skyline(config); let mut packer = TexturePacker::new_skyline(config);
for image in images.iter() { for image in images.iter() {
if !packer.can_pack(&image.image) { if !packer.can_pack(&image.image) {
return Err(SpritesheetError::CannotPackImage(image.id.clone())); 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); return Err(SpritesheetError::FailedToPackImage);
} }
} }
@ -131,6 +162,10 @@ impl Spritesheet {
}; };
let mut atlas_asset = bevy_rpack::AtlasAsset { let mut atlas_asset = bevy_rpack::AtlasAsset {
metadata: AtlasMetadata {
skip_serialization: skip_metadata_serialization,
..Default::default()
},
size: [image_data.width(), image_data.height()], size: [image_data.width(), image_data.height()],
filename: filename.as_ref().to_owned(), filename: filename.as_ref().to_owned(),
frames: packer 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`. /// 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)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub border_padding: Option<u32>, pub border_padding: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub skip_serializing_metadata: Option<bool>,
#[serde(skip)] #[serde(skip)]
pub working_dir: Option<PathBuf>, pub working_dir: Option<PathBuf>,
} }
#[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 { impl TilemapGenerationConfig {
pub fn read_from_file<P>(path: P) -> anyhow::Result<TilemapGenerationConfig> pub fn read_from_file<P>(path: P) -> anyhow::Result<TilemapGenerationConfig>
where where
@ -263,7 +324,24 @@ impl TilemapGenerationConfig {
Ok(config) Ok(config)
} }
pub fn generate(&self) -> anyhow::Result<()> { pub fn get_file_paths_and_prefix(&self) -> (Vec<PathBuf>, String) {
let working_dir = self.working_dir();
let lossy_working_dir = working_dir.to_string_lossy();
let mut file_paths: Vec<PathBuf> = 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 { let dir = match &self.working_dir {
None => std::env::current_dir().expect("msg"), None => std::env::current_dir().expect("msg"),
Some(p) => { 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<PathBuf> = self working_dir
.asset_patterns }
.iter()
.flat_map(|pattern| { pub fn generate(&self) -> anyhow::Result<()> {
let p = format!("{}/{}", working_dir.to_string_lossy(), pattern); let working_dir = self.working_dir();
glob::glob(&p).expect("Wrong pattern for assets").flatten()
}) let (file_paths, prefix) = self.get_file_paths_and_prefix();
.filter(|e| e.is_file())
.collect();
file_paths.sort();
let prefix = get_common_prefix(&file_paths);
let images: Vec<ImageFile> = file_paths let images: Vec<ImageFile> = file_paths
.iter() .iter()
.flat_map(|f| { .flat_map(|f| {
@ -298,6 +372,7 @@ impl TilemapGenerationConfig {
ImageFile::at_path(f, id) ImageFile::at_path(f, id)
}) })
.collect(); .collect();
let borrowed_images: Vec<&ImageFile> = images.iter().map(|s| s).collect();
let atlas_image_path = working_dir.join(format!( let atlas_image_path = working_dir.join(format!(
"{}{}", "{}{}",
self.output_path, self.output_path,
@ -309,21 +384,7 @@ impl TilemapGenerationConfig {
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
let atlas_config_path = working_dir.join(format!("{}.rpack.json", self.output_path)); let atlas_config_path = working_dir.join(format!("{}.rpack.json", self.output_path));
let spritesheet = Spritesheet::build( let spritesheet = Spritesheet::build(self, &borrowed_images, &atlas_filename)?;
texture_packer::TexturePackerConfig {
max_width: self.size.unwrap_or(2048),
max_height: self.size.unwrap_or(2048),
allow_rotation: false,
force_max_dimensions: true,
border_padding: self.border_padding.unwrap_or(0),
texture_padding: self.texture_padding.unwrap_or(2),
texture_extrusion: 0,
trim: false,
texture_outlines: false,
},
&images,
&atlas_filename,
)?;
if Path::new(&atlas_config_path).exists() { if Path::new(&atlas_config_path).exists() {
std::fs::remove_file(&atlas_config_path).expect("Could not remove the old file"); std::fs::remove_file(&atlas_config_path).expect("Could not remove the old file");

View File

@ -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. "accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies.
"default_fonts", # Embed the default egui fonts. "default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu". "glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
] } ] }
log = "0.4" log = "0.4"
egui_json_tree = "0.13" 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: # You only need serde if you want app persistence:
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@ -33,9 +31,11 @@ anyhow = "1"
# native: # native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
env_logger = "0.11" env_logger = "0.11"
rpack_cli = { default-features = false, features = ["config_ext"], path = "../rpack_cli", version = "0.3" }
# web: # web:
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
rpack_cli = { default-features = false, path = "../rpack_cli", version = "0.3" }
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = [ web-sys = { version = "0.3", features = [

View File

@ -1,5 +1,10 @@
use egui::{CollapsingHeader, Color32, FontFamily, FontId, Grid, Image, Label, RichText}; use std::path::PathBuf;
use rpack_cli::{ImageFile, Spritesheet, SpritesheetError};
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 texture_packer::TexturePackerConfig;
use crate::helpers::DroppedFileHelper; 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 TOP_BUTTON_WIDTH: f32 = 150.0;
pub const GIT_HASH: &str = env!("GIT_HASH"); 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 { pub struct Application {
#[serde(skip)] data: ApplicationData,
config: TexturePackerConfig, output: Option<Spritesheet>,
#[serde(skip)] last_error: Option<SpritesheetError>,
output: Option<SpriteSheetResult>, undoer: Undoer<ApplicationData>,
#[serde(skip)]
name: String,
#[serde(skip)]
min_size: [u32; 2],
#[serde(skip)]
max_size: u32,
#[serde(skip)]
image_data: Vec<AppImageData>,
} }
type SpriteSheetResult = Result<Spritesheet, SpritesheetError>; #[derive(serde::Deserialize, serde::Serialize, Default, Clone)]
pub struct ApplicationData {
#[serde(skip, default)]
image_data: Vec<AppImageData>,
#[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 struct AppImageData {
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
@ -54,27 +90,29 @@ impl AppImageData {
impl Default for Application { impl Default for Application {
fn default() -> Self { fn default() -> Self {
Self { Self {
config: TexturePackerConfig { data: Default::default(),
max_width: 512, undoer: Default::default(),
max_height: 512,
allow_rotation: false,
border_padding: 2,
trim: false,
force_max_dimensions: true,
..Default::default()
},
output: None, output: None,
max_size: 4096, last_error: None,
name: String::from("Tilemap"),
min_size: [32, 32],
image_data: Vec::new(),
} }
} }
} }
impl Application { 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 { pub fn get_common_prefix(&self) -> String {
let file_paths: Vec<String> = self let file_paths: Vec<String> = self
.data
.image_data .image_data
.iter() .iter()
.map(|image| image.path.clone()) .map(|image| image.path.clone())
@ -83,65 +121,82 @@ impl Application {
} }
pub fn rebuild_image_data(&mut self) { pub fn rebuild_image_data(&mut self) {
let prefix = self.get_common_prefix(); let prefix = self.get_common_prefix();
self.image_data self.data
.image_data
.iter_mut() .iter_mut()
.for_each(|f| f.update_id(prefix.as_str())); .for_each(|f| f.update_id(prefix.as_str()));
self.update_min_size(); self.update_min_size();
} }
pub fn update_min_size(&mut self) { 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.data.settings.min_size[0] = self
self.min_size[0] = file.width; .data
} else { .image_data
self.min_size[0] = 32; .iter()
} .max_by(|a, b| a.width.cmp(&b.width))
if let Some(file) = self .map_or(32, |s| s.width);
self.data.settings.min_size[1] = self
.data
.image_data .image_data
.iter() .iter()
.max_by(|a, b| a.height.cmp(&b.height)) .max_by(|a, b| a.height.cmp(&b.height))
{ .map_or(32, |s| s.height);
self.min_size[1] = file.height; for nr in [32, 64, 128, 256, 512, 1024, 2048, 4096] {
} else { if nr >= self.data.settings.min_size[0] && nr >= self.data.settings.min_size[1] {
self.min_size[1] = 32; self.data.settings.min_size[0] = nr;
self.data.settings.min_size[1] = nr;
break;
}
} }
} }
/// Called once before the first frame. /// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self { pub fn new(cc: &eframe::CreationContext<'_>, config_file: Option<String>) -> Self {
crate::fonts::setup_custom_fonts(&cc.egui_ctx); crate::fonts::setup_custom_fonts(&cc.egui_ctx);
// This is also where you can customize the look and feel of egui using // 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`. // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
egui_extras::install_image_loaders(&cc.egui_ctx); egui_extras::install_image_loaders(&cc.egui_ctx);
// Load previous app state (if any). let mut app = Self::default();
// Note that you must enable the `persistence` feature for this to work. if let Some(config_file) = config_file {
if let Some(storage) = cc.storage { if let Ok(config) = rpack_cli::TilemapGenerationConfig::read_from_file(&config_file) {
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); 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) { fn build_atlas(&mut self, ctx: &egui::Context) {
self.last_error = None;
self.output = None; self.output = None;
ctx.forget_image("bytes://output.png"); ctx.forget_image("bytes://output.png");
let images: Vec<ImageFile> = self if self.data.image_data.is_empty() {
.image_data return;
.iter()
.map(|file| file.data.clone())
.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 { let images: Vec<&ImageFile> = self.data.image_data.iter().map(|file| &file.data).collect();
for multiplier in 1..10 {
let size = multiplier * self.data.settings.min_size[0];
if size > self.data.settings.size {
break; break;
} }
let config = TexturePackerConfig { let config = TexturePackerConfig {
max_width: size, max_width: size,
max_height: 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) => { Ok(data) => {
let mut out_vec = vec![]; let mut out_vec = vec![];
data.image_data data.image_data
@ -152,23 +207,26 @@ impl Application {
.unwrap(); .unwrap();
ctx.include_bytes("bytes://output.png", out_vec); ctx.include_bytes("bytes://output.png", out_vec);
self.output = Some(Ok(data)); self.output = Some(data);
break; break;
} }
Err(e) => { Err(e) => {
self.output = Some(Err(e)); self.last_error = Some(e);
} }
} }
} }
if self.output.is_some() {
self.last_error = None;
}
ctx.request_repaint(); ctx.request_repaint();
} }
fn save_json(&self) -> Result<(), String> { 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()); return Err("Data is incorrect".to_owned());
}; };
let data = spritesheet.atlas_asset_json.to_string(); 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"))] #[cfg(not(target_arch = "wasm32"))]
{ {
let path_buf = rfd::FileDialog::new() let path_buf = rfd::FileDialog::new()
@ -204,10 +262,10 @@ impl Application {
} }
fn save_atlas(&self) -> Result<(), String> { 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()); 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"))] #[cfg(not(target_arch = "wasm32"))]
{ {
let path_buf = rfd::FileDialog::new() let path_buf = rfd::FileDialog::new()
@ -253,17 +311,10 @@ impl Application {
} }
impl eframe::App for 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. /// 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) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// if self.dropped_files.is_empty() && self.image.is_some() { self.undoer
// self.image = None; .feed_state(ctx.input(|input| input.time), &self.data);
// self.data = None;
// }
egui::TopBottomPanel::top("topPanel") egui::TopBottomPanel::top("topPanel")
.frame(egui::Frame::canvas(&ctx.style())) .frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| { .show(ctx, |ui| {
@ -277,7 +328,9 @@ impl eframe::App for Application {
}); });
}); });
ctx.input(|i| { ctx.input(|i| {
if !i.raw.dropped_files.is_empty() { if i.raw.dropped_files.is_empty() {
return;
}
for file in i.raw.dropped_files.iter() { for file in i.raw.dropped_files.iter() {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
if let Some(path) = &file.path { if let Some(path) = &file.path {
@ -294,31 +347,71 @@ impl eframe::App for Application {
let Some(dyn_image) = entry.create_image("") else { let Some(dyn_image) = entry.create_image("") else {
continue; 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 { let Some(dyn_image) = file.create_image("") else {
continue; continue;
}; };
self.image_data.push(dyn_image); self.data.image_data.push(dyn_image);
} }
self.output = None; self.output = None;
self.rebuild_image_data(); self.rebuild_image_data();
}
}); });
egui::TopBottomPanel::bottom("bottom_panel") egui::TopBottomPanel::bottom("bottom_panel")
.frame(egui::Frame::canvas(&ctx.style())) .frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| { .show(ctx, |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); powered_by_egui_and_eframe(ui);
}); });
ui.add_space(5.0);
});
egui::SidePanel::right("right") egui::SidePanel::right("right")
.min_width(200.0) .min_width(200.0)
.max_width(400.0) .max_width(400.0)
.frame(egui::Frame::canvas(&ctx.style())) .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() egui::ScrollArea::vertical()
.id_salt("rightPanel_scroll") .id_salt("rightPanel_scroll")
.show(ui, |ui| { .show(ui, |ui| {
@ -327,29 +420,31 @@ impl eframe::App for Application {
.show(ui, |ui| { .show(ui, |ui| {
ui.vertical_centered_justified(|ui|{ ui.vertical_centered_justified(|ui|{
let label = ui.label("Tilemap filename"); 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_space(10.0);
ui.add( 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) .step_by(32.0)
.text("Max size"), .text("Max size"),
); );
ui.add( 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"), .text("Border Padding"),
); );
ui.add( 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"), .text("Texture Padding"),
); );
// ui.checkbox(&mut self.config.allow_rotation, "Allow Rotation") // 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."); // .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."); .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.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_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 if ui
.add_sized([TOP_BUTTON_WIDTH, 30.0], egui::Button::new("Build atlas")) .add_sized([TOP_BUTTON_WIDTH, 30.0], egui::Button::new("Build atlas"))
.clicked() .clicked()
@ -367,8 +462,8 @@ impl eframe::App for Application {
.show_unindented(ui, |ui| { .show_unindented(ui, |ui| {
ui.horizontal(|ui|{ ui.horizontal(|ui|{
if !self.image_data.is_empty() && ui.button("clear list").clicked() { if !self.data.image_data.is_empty() && ui.button("clear list").clicked() {
self.image_data.clear(); self.data.image_data.clear();
self.output = None; self.output = None;
self.update_min_size(); self.update_min_size();
} }
@ -379,7 +474,7 @@ impl eframe::App for Application {
for file in files.iter() { for file in files.iter() {
let Ok(image) = texture_packer::importer::ImageImporter::import_from_file(file) else { continue }; let Ok(image) = texture_packer::importer::ImageImporter::import_from_file(file) else { continue };
let id = crate::helpers::id_from_path(&file.to_string_lossy()); 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(); 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|{ 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() { if ui.button("x").clicked() {
to_remove = Some(index); to_remove = Some(index);
} }
@ -405,7 +500,7 @@ impl eframe::App for Application {
} }
}); });
if let Some(index) = to_remove { if let Some(index) = to_remove {
self.image_data.remove(index); self.data.image_data.remove(index);
self.output = None; self.output = None;
self.rebuild_image_data(); self.rebuild_image_data();
} }
@ -413,18 +508,17 @@ impl eframe::App for Application {
}); });
}); });
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical() if let Some(error) = &self.last_error {
.id_salt("vertical_scroll")
.show(ui, |ui| {
if let Some(Err(error)) = &self.output {
let text = egui::RichText::new(format!("Error: {}", &error)) let text = egui::RichText::new(format!("Error: {}", &error))
.font(FontId::new(20.0, FontFamily::Name("semibold".into()))) .font(FontId::new(20.0, FontFamily::Name("semibold".into())))
.color(Color32::RED) .color(Color32::RED)
.strong(); .strong();
ui.add(egui::Label::new(text)); ui.add(egui::Label::new(text));
return;
} }
if self.image_data.is_empty() { egui::ScrollArea::vertical()
.id_salt("vertical_scroll")
.show(ui, |ui| {
if self.data.image_data.is_empty() {
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
ui.add_space(50.0); ui.add_space(50.0);
ui.add( 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; return;
}; };
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
@ -513,14 +607,17 @@ impl eframe::App for Application {
} }
fn powered_by_egui_and_eframe(ui: &mut egui::Ui) { 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")); ui.hyperlink_to(format!("Build: {}", GIT_HASH), env!("CARGO_PKG_HOMEPAGE"));
egui::warn_if_debug_build(ui); egui::warn_if_debug_build(ui);
ui.separator(); ui.separator();
egui::widgets::global_theme_preference_switch(ui); egui::widgets::global_theme_preference_switch(ui);
ui.separator(); ui.separator();
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.label("Made by ");
ui.hyperlink_to("Mev Lyshkin", "https://www.mevlyshkin.com/"); 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));
}); });
} }

View File

@ -1,9 +1,7 @@
pub fn setup_custom_fonts(ctx: &egui::Context) { pub fn setup_custom_fonts(ctx: &egui::Context) {
// Start with the default fonts (we will be adding to them rather than replacing them). // Start with the default fonts (we will be adding to them rather than replacing them).
let mut fonts = egui::FontDefinitions::default(); let mut fonts = egui::FontDefinitions::default();
let Ok((regular, semibold)) = get_fonts() else { if let Ok((regular, semibold)) = get_fonts() {
return;
};
fonts.font_data.insert( fonts.font_data.insert(
"regular".to_owned(), "regular".to_owned(),
egui::FontData::from_owned(regular).into(), egui::FontData::from_owned(regular).into(),
@ -34,29 +32,23 @@ pub fn setup_custom_fonts(ctx: &egui::Context) {
// Tell egui to use these fonts: // Tell egui to use these fonts:
ctx.set_fonts(fonts); ctx.set_fonts(fonts);
}
#[cfg(not(target_arch = "wasm32"))] ctx.all_styles_mut(|style| {
ctx.style_mut(|style| {
for font_id in style.text_styles.values_mut() { for font_id in style.text_styles.values_mut() {
font_id.size *= 1.4; font_id.size *= 1.4;
} }
}); });
} }
#[cfg(all(not(target_os = "macos"), not(windows)))] #[cfg(not(windows))]
fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> { fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
let regular = include_bytes!("../static/JetBrainsMonoNL-Regular.ttf").to_vec(); use std::fs;
let semibold = include_bytes!("../static/JetBrainsMono-SemiBold.ttf").to_vec();
Ok((regular, semibold))
}
#[cfg(target_os = "macos")]
fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
let font_path = std::path::Path::new("/System/Library/Fonts"); let font_path = std::path::Path::new("/System/Library/Fonts");
let regular = std::fs::read(font_path.join("SFNSRounded.ttf"))?; let regular = fs::read(font_path.join("SFNSRounded.ttf"))?;
let semibold = std::fs::read(font_path.join("SFCompact.ttf"))?; let semibold = fs::read(font_path.join("SFCompact.ttf"))?;
Ok((regular, semibold)) Ok((regular, semibold))
} }
@ -64,7 +56,6 @@ fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
#[cfg(windows)] #[cfg(windows)]
fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> { fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
use std::fs; use std::fs;
let app_data = std::env::var("APPDATA")?; let app_data = std::env::var("APPDATA")?;
let font_path = std::path::Path::new(&app_data); let font_path = std::path::Path::new(&app_data);

View File

@ -1,3 +1,6 @@
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
use egui::DroppedFile; use egui::DroppedFile;
use image::DynamicImage; use image::DynamicImage;
use rpack_cli::ImageFile; use rpack_cli::ImageFile;
@ -39,6 +42,16 @@ impl DroppedFileHelper for std::fs::DirEntry {
ImageImporter::import_from_file(&self.path()).ok() 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<DynamicImage> {
ImageImporter::import_from_file(self.as_path()).ok()
}
}
pub fn id_from_path(path: &str) -> String { pub fn id_from_path(path: &str) -> String {
match path.rfind('.') { match path.rfind('.') {

View File

@ -5,7 +5,12 @@
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> { fn main() -> eframe::Result<()> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let args: Vec<String> = std::env::args().collect();
let file_arg: Option<String> = if args.len() > 1 {
Some(args[1].clone())
} else {
None
};
let native_options = eframe::NativeOptions { let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0]) .with_inner_size([400.0, 300.0])
@ -15,7 +20,7 @@ fn main() -> eframe::Result<()> {
eframe::run_native( eframe::run_native(
"rPack", "rPack",
native_options, 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)))),
) )
} }