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
- `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
]
}
```

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/),
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

View File

@ -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"

View File

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

View File

@ -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<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"
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 }

View File

@ -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<TexturePackerConfig> for SpritesheetBuildConfig {
fn from(config: TexturePackerConfig) -> Self {
Self {
packer_config: config,
skip_metadata_serialization: false,
}
}
}
impl Spritesheet {
pub fn build<P>(
config: TexturePackerConfig,
images: &[ImageFile],
config: impl Into<SpritesheetBuildConfig>,
images: &[&ImageFile],
filename: P,
) -> Result<Self, SpritesheetError>
where
P: AsRef<str>,
{
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<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub skip_serializing_metadata: Option<bool>,
#[serde(skip)]
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 {
pub fn read_from_file<P>(path: P) -> anyhow::Result<TilemapGenerationConfig>
where
@ -263,7 +324,24 @@ impl TilemapGenerationConfig {
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 {
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<PathBuf> = 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<ImageFile> = 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");

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.
"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 = [

View File

@ -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<SpriteSheetResult>,
#[serde(skip)]
name: String,
#[serde(skip)]
min_size: [u32; 2],
#[serde(skip)]
max_size: u32,
#[serde(skip)]
image_data: Vec<AppImageData>,
data: ApplicationData,
output: Option<Spritesheet>,
last_error: Option<SpritesheetError>,
undoer: Undoer<ApplicationData>,
}
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 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<String> = 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<String>) -> 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<ImageFile> = 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));
});
}

View File

@ -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<u8>, Vec<u8>)> {
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<u8>, Vec<u8>)> {
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<u8>, Vec<u8>)> {
#[cfg(windows)]
fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
use std::fs;
let app_data = std::env::var("APPDATA")?;
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 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<DynamicImage> {
ImageImporter::import_from_file(self.as_path()).ok()
}
}
pub fn id_from_path(path: &str) -> String {
match path.rfind('.') {

View File

@ -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<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 {
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)))),
)
}