mirror of https://github.com/Leinnan/rpack.git
Work on new version
This commit is contained in:
parent
61d4cd239e
commit
0f9ab63a1f
13
README.md
13
README.md
|
|
@ -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
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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('.') {
|
||||||
|
|
|
||||||
|
|
@ -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)))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue