UI rebuild

This commit is contained in:
Piotr Siuszko 2025-09-18 21:11:00 +02:00
parent 83e46ee710
commit 57159784eb
8 changed files with 993 additions and 421 deletions

View File

@ -12,6 +12,8 @@ use std::{
use texture_packer::{TexturePacker, TexturePackerConfig, importer::ImageImporter};
use thiserror::Error;
pub mod packer;
#[derive(Clone)]
pub struct Spritesheet {
pub image_data: DynamicImage,
@ -79,7 +81,7 @@ where
prefix
}
#[derive(Clone, Debug, Default, Copy, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Copy, Serialize, Deserialize, PartialEq)]
#[cfg_attr(
all(feature = "cli", not(target_arch = "wasm32")),
derive(clap::ValueEnum)
@ -105,14 +107,14 @@ impl Display for SaveImageFormat {
/// Errors that can occur while building a `Spritesheet`.
#[non_exhaustive]
#[derive(Debug, Error)]
#[derive(Debug, Error, Clone)]
pub enum SpritesheetError {
#[error("Cannot pack image: {0}")]
CannotPackImage(String),
#[error("Failed to export tilemap image")]
FailedToExportImage,
#[error("could not parse asset: {0}")]
ParsingError(#[from] serde_json::Error),
ParsingError(String),
#[error("Failed to pack image into tilemap, tilemap to small")]
FailedToPackImage,
}
@ -138,7 +140,7 @@ impl From<TexturePackerConfig> for SpritesheetBuildConfig {
impl Spritesheet {
pub fn build<P>(
config: impl Into<SpritesheetBuildConfig>,
images: &[&ImageFile],
images: &[ImageFile],
filename: P,
) -> Result<Self, SpritesheetError>
where
@ -185,7 +187,8 @@ impl Spritesheet {
.collect(),
};
atlas_asset.frames.sort_by(|a, b| a.key.cmp(&b.key));
let atlas_asset_json = serde_json::to_value(&atlas_asset)?;
let atlas_asset_json = serde_json::to_value(&atlas_asset)
.map_err(|e| SpritesheetError::ParsingError(e.to_string()))?;
Ok(Spritesheet {
image_data,
@ -266,7 +269,7 @@ impl Spritesheet {
}
}
#[derive(Clone, Serialize, Deserialize, Default)]
#[derive(Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct TilemapGenerationConfig {
pub asset_patterns: Vec<String>,
pub output_path: String,
@ -372,7 +375,6 @@ 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,
@ -384,7 +386,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(self, &borrowed_images, &atlas_filename)?;
let spritesheet = Spritesheet::build(self, &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

@ -0,0 +1,177 @@
use std::cmp::max;
use texture_packer::{Rect, TexturePackerConfig};
struct Skyline {
pub x: u32,
pub y: u32,
pub w: u32,
}
impl Skyline {
#[inline(always)]
pub fn left(&self) -> u32 {
self.x
}
#[inline(always)]
pub fn right(&self) -> u32 {
self.x + self.w - 1
}
}
pub struct SkylinePacker {
config: TexturePackerConfig,
border: Rect,
// the skylines are sorted by their `x` position
skylines: Vec<Skyline>,
}
impl SkylinePacker {
pub fn new(config: TexturePackerConfig) -> Self {
let skylines = vec![Skyline {
x: 0,
y: 0,
w: config.max_width,
}];
SkylinePacker {
config,
border: Rect::new(0, 0, config.max_width, config.max_height),
skylines,
}
}
// return `rect` if rectangle (w, h) can fit the skyline started at `i`
pub fn can_put(&self, mut i: usize, w: u32, h: u32) -> Option<Rect> {
let mut rect = Rect::new(self.skylines[i].x, 0, w, h);
let mut width_left = rect.w;
loop {
rect.y = max(rect.y, self.skylines[i].y);
// the source rect is too large
if !self.border.contains(&rect) {
return None;
}
if self.skylines[i].w >= width_left {
return Some(rect);
}
width_left -= self.skylines[i].w;
i += 1;
assert!(i < self.skylines.len());
}
}
pub fn find_skyline(&self, w: u32, h: u32) -> Option<(usize, Rect)> {
let mut bottom = std::u32::MAX;
let mut width = std::u32::MAX;
let mut index = None;
let mut rect = Rect::new(0, 0, 0, 0);
// keep the `bottom` and `width` as small as possible
for i in 0..self.skylines.len() {
if let Some(r) = self.can_put(i, w, h) {
if r.bottom() < bottom || (r.bottom() == bottom && self.skylines[i].w < width) {
bottom = r.bottom();
width = self.skylines[i].w;
index = Some(i);
rect = r;
}
}
if self.config.allow_rotation {
if let Some(r) = self.can_put(i, h, w) {
if r.bottom() < bottom || (r.bottom() == bottom && self.skylines[i].w < width) {
bottom = r.bottom();
width = self.skylines[i].w;
index = Some(i);
rect = r;
}
}
}
}
index.map(|x| (x, rect))
}
pub fn split(&mut self, index: usize, rect: &Rect) {
let skyline = Skyline {
x: rect.left(),
y: rect.bottom() + 1,
w: rect.w,
};
assert!(skyline.right() <= self.border.right());
assert!(skyline.y <= self.border.bottom());
self.skylines.insert(index, skyline);
let i = index + 1;
while i < self.skylines.len() {
assert!(self.skylines[i - 1].left() <= self.skylines[i].left());
if self.skylines[i].left() <= self.skylines[i - 1].right() {
let shrink = self.skylines[i - 1].right() - self.skylines[i].left() + 1;
if self.skylines[i].w <= shrink {
self.skylines.remove(i);
} else {
self.skylines[i].x += shrink;
self.skylines[i].w -= shrink;
break;
}
} else {
break;
}
}
}
pub fn merge(&mut self) {
let mut i = 1;
while i < self.skylines.len() {
if self.skylines[i - 1].y == self.skylines[i].y {
self.skylines[i - 1].w += self.skylines[i].w;
self.skylines.remove(i);
i -= 1;
}
i += 1;
}
}
pub fn pack(&mut self, texture_rect: &Rect) -> Option<()> {
let mut width = texture_rect.w;
let mut height = texture_rect.h;
width += self.config.texture_padding + self.config.texture_extrusion * 2;
height += self.config.texture_padding + self.config.texture_extrusion * 2;
if let Some((i, mut rect)) = self.find_skyline(width, height) {
self.split(i, &rect);
self.merge();
// let rotated = width != rect.w;
rect.w -= self.config.texture_padding + self.config.texture_extrusion * 2;
rect.h -= self.config.texture_padding + self.config.texture_extrusion * 2;
Some(())
} else {
None
}
}
pub fn can_pack(&self, texture_rect: &Rect) -> bool {
if let Some((_, rect)) = self.find_skyline(
texture_rect.w + self.config.texture_padding + self.config.texture_extrusion * 2,
texture_rect.h + self.config.texture_padding + self.config.texture_extrusion * 2,
) {
let skyline = Skyline {
x: rect.left(),
y: rect.bottom() + 1,
w: rect.w,
};
return skyline.right() <= self.border.right() && skyline.y <= self.border.bottom();
}
false
}
}

View File

@ -8,9 +8,13 @@ repository = "https://github.com/Leinnan/rpack.git"
homepage = "https://github.com/Leinnan/rpack"
license = "MIT OR Apache-2.0"
[features]
profiler = ["dep:puffin", "dep:puffin_http", "dep:profiling"]
[dependencies]
egui = "0.32"
eframe = { version = "0.32", default-features = false, features = [
"persistence",
"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".
@ -27,9 +31,15 @@ egui_extras = { version = "0.32", features = ["all_loaders"] }
rfd = { version = "0.15", features = [] }
wasm-bindgen-futures = "0.4"
anyhow = "1"
crossbeam = "0.8"
once_cell = "1"
futures = "0.3.12"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
puffin = {version = "0.19", optional = true}
puffin_http = {version = "0.16", optional = true}
profiling = {version = "1.0.16", optional = true, default-features = false , features = ["profile-with-puffin"] }
env_logger = "0.11"
rpack_cli = { default-features = false, features = ["config_ext"], path = "../rpack_cli", version = "0.3" }

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,16 @@ impl DroppedFileHelper for std::fs::DirEntry {
ImageImporter::import_from_file(&self.path()).ok()
}
}
impl DroppedFileHelper for (Vec<u8>, String) {
fn file_path(&self) -> String {
self.1.clone()
}
fn dynamic_image(&self) -> Option<DynamicImage> {
ImageImporter::import_from_memory(&self.0).ok()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl DroppedFileHelper for PathBuf {
fn file_path(&self) -> String {
@ -70,19 +80,10 @@ impl DroppedFileHelper for DroppedFile {
}
fn dynamic_image(&self) -> Option<DynamicImage> {
#[cfg(target_arch = "wasm32")]
{
let bytes = self.bytes.as_ref().clone()?;
ImageImporter::import_from_memory(bytes)
.ok()
.map(|r| r.into())
}
#[cfg(not(target_arch = "wasm32"))]
{
let path = self.path.as_ref()?;
ImageImporter::import_from_file(path).ok()
}
}
}

View File

@ -4,3 +4,4 @@ mod app;
mod fonts;
mod helpers;
pub use app::Application;
pub use app::ICON_DATA;

View File

@ -1,10 +1,37 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#[cfg(all(not(target_arch = "wasm32"), feature = "profiler"))]
fn start_puffin_server() {
puffin::set_scopes_on(true); // tell puffin to collect data
match puffin_http::Server::new("127.0.0.1:8585") {
Ok(puffin_server) => {
log::info!("Run: cargo install puffin_viewer && puffin_viewer --url 127.0.0.1:8585");
std::process::Command::new("puffin_viewer")
.arg("--url")
.arg("127.0.0.1:8585")
.spawn()
.ok();
// We can store the server if we want, but in this case we just want
// it to keep running. Dropping it closes the server, so let's not drop it!
#[expect(clippy::mem_forget)]
std::mem::forget(puffin_server);
}
Err(err) => {
log::error!("Failed to start puffin server: {err}");
}
}
}
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
#[cfg(feature = "profiler")]
start_puffin_server();
let file_arg: Option<String> = if std::env::args().len() > 1 {
std::env::args().skip(1).next()
} else {
@ -13,10 +40,7 @@ fn main() -> eframe::Result<()> {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0])
.with_icon(
eframe::icon_data::from_png_bytes(include_bytes!("../static/base_icon.png"))
.unwrap_or_default(),
)
.with_icon(eframe::icon_data::from_png_bytes(rpack_egui::ICON_DATA).unwrap_or_default())
.with_min_inner_size([400.0, 300.0]),
..Default::default()
};

View File

@ -22,7 +22,7 @@
<string>Rpack gen config</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>rpack_gen.json</string>
<string>json</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>