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
|
||||
- `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
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -200,6 +200,11 @@
|
|||
"key": "ship/spaceBuilding_012"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"app": "rpack",
|
||||
"app_version": "0.3.0",
|
||||
"format_version": 1
|
||||
},
|
||||
"size": [
|
||||
512,
|
||||
512
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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('.') {
|
||||
|
|
|
|||
|
|
@ -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)))),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue