mirror of https://github.com/Leinnan/rpack.git
UI refactor, some doc tests
This commit is contained in:
parent
b81d3f9c3a
commit
35ae42d2bb
|
|
@ -23,7 +23,7 @@ jobs:
|
||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: check
|
command: check
|
||||||
args: --all-features
|
args: --all-features --lib
|
||||||
|
|
||||||
check_wasm:
|
check_wasm:
|
||||||
name: Check wasm32
|
name: Check wasm32
|
||||||
|
|
@ -40,23 +40,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
command: check
|
command: check
|
||||||
args: --all-features --lib --target wasm32-unknown-unknown
|
args: --all-features --lib --target wasm32-unknown-unknown
|
||||||
|
|
||||||
test:
|
|
||||||
name: Test Suite
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
- run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --lib
|
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
name: Rustfmt
|
name: Rustfmt
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -73,22 +56,6 @@ jobs:
|
||||||
command: fmt
|
command: fmt
|
||||||
args: --all -- --check
|
args: --all -- --check
|
||||||
|
|
||||||
clippy:
|
|
||||||
name: Clippy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
components: clippy
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
args: -- -D warnings
|
|
||||||
|
|
||||||
trunk:
|
trunk:
|
||||||
name: trunk
|
name: trunk
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,17 @@ impl RpackAtlasAsset {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Plugin that provides support for rpack atlases.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// use bevy::prelude::*;
|
||||||
|
/// use bevy_rpack::prelude::*;
|
||||||
|
///
|
||||||
|
/// App::new()
|
||||||
|
/// .add_plugins((DefaultPlugins,RpackAssetPlugin))
|
||||||
|
/// .run();
|
||||||
|
/// ```
|
||||||
pub struct RpackAssetPlugin;
|
pub struct RpackAssetPlugin;
|
||||||
|
|
||||||
impl Plugin for RpackAssetPlugin {
|
impl Plugin for RpackAssetPlugin {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
#[cfg(feature = "bevy")]
|
#[cfg(feature = "bevy")]
|
||||||
pub mod bevy;
|
mod bevy;
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::{AtlasAsset,SerializableRect, AtlasFrame};
|
||||||
|
#[cfg(feature = "bevy")]
|
||||||
|
pub use super::bevy::{RpackAssetPlugin, RpackAtlasAsset, RpackAtlasAssetError, RpackAtlasAssetLoader};
|
||||||
|
}
|
||||||
|
|
||||||
/// Defines a rectangle in pixels with the origin at the top-left of the texture atlas.
|
/// Defines a rectangle in pixels with the origin at the top-left of the texture atlas.
|
||||||
#[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use egui::{CollapsingHeader, Color32, DroppedFile, FontFamily, FontId, Image, RichText, Vec2};
|
use std::{collections::HashMap, path::Path};
|
||||||
use image::DynamicImage;
|
|
||||||
|
use egui::{CollapsingHeader, Color32, DroppedFile, FontFamily, FontId, Image, RichText};
|
||||||
|
use image::{DynamicImage, GenericImageView};
|
||||||
use rpack_cli::{ImageFile, Spritesheet};
|
use rpack_cli::{ImageFile, Spritesheet};
|
||||||
use texture_packer::{importer::ImageImporter, TexturePackerConfig};
|
use texture_packer::{importer::ImageImporter, TexturePackerConfig};
|
||||||
pub const MY_ACCENT_COLOR32: Color32 = Color32::from_rgb(230, 102, 1);
|
pub const MY_ACCENT_COLOR32: Color32 = Color32::from_rgb(230, 102, 1);
|
||||||
|
|
@ -24,8 +26,13 @@ pub struct TemplateApp {
|
||||||
counter: i32,
|
counter: i32,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
data: Option<Result<Spritesheet, String>>,
|
data: Option<Result<Spritesheet, String>>,
|
||||||
|
#[serde(skip)]
|
||||||
|
min_size: [u32; 2],
|
||||||
|
#[serde(skip)]
|
||||||
|
max_size: u32,
|
||||||
|
#[serde(skip)]
|
||||||
|
image_data: HashMap<String, ImageFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TemplateApp {
|
impl Default for TemplateApp {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -36,17 +43,50 @@ impl Default for TemplateApp {
|
||||||
allow_rotation: false,
|
allow_rotation: false,
|
||||||
border_padding: 2,
|
border_padding: 2,
|
||||||
trim: false,
|
trim: false,
|
||||||
|
force_max_dimensions: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
counter: 0,
|
counter: 0,
|
||||||
image: None,
|
image: None,
|
||||||
data: None,
|
data: None,
|
||||||
|
max_size: 4096,
|
||||||
name: String::from("Tilemap"),
|
name: String::from("Tilemap"),
|
||||||
|
min_size: [32, 32],
|
||||||
|
image_data: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemplateApp {
|
impl TemplateApp {
|
||||||
|
pub fn rebuild_image_data(&mut self) {
|
||||||
|
let prefix = Self::get_common_prefix(&self.dropped_files);
|
||||||
|
self.image_data = self
|
||||||
|
.dropped_files
|
||||||
|
.iter()
|
||||||
|
.flat_map(|f| Self::image_from_dropped_file(f, &prefix))
|
||||||
|
.collect();
|
||||||
|
self.update_min_size();
|
||||||
|
}
|
||||||
|
pub fn update_min_size(&mut self) {
|
||||||
|
if let Some(file) = self
|
||||||
|
.image_data
|
||||||
|
.values()
|
||||||
|
.max_by(|a, b| a.image.width().cmp(&b.image.width()))
|
||||||
|
{
|
||||||
|
self.min_size[0] = file.image.width();
|
||||||
|
} else {
|
||||||
|
self.min_size[0] = 32;
|
||||||
|
}
|
||||||
|
if let Some(file) = self
|
||||||
|
.image_data
|
||||||
|
.values()
|
||||||
|
.max_by(|a, b| a.image.height().cmp(&b.image.height()))
|
||||||
|
{
|
||||||
|
self.min_size[1] = file.image.height();
|
||||||
|
} else {
|
||||||
|
self.min_size[1] = 32;
|
||||||
|
}
|
||||||
|
}
|
||||||
/// 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<'_>) -> Self {
|
||||||
setup_custom_fonts(&cc.egui_ctx);
|
setup_custom_fonts(&cc.egui_ctx);
|
||||||
|
|
@ -65,11 +105,19 @@ impl TemplateApp {
|
||||||
fn get_common_prefix(paths: &[DroppedFile]) -> String {
|
fn get_common_prefix(paths: &[DroppedFile]) -> String {
|
||||||
if paths.is_empty() {
|
if paths.is_empty() {
|
||||||
return String::new();
|
return String::new();
|
||||||
|
} else if paths.len() == 1 {
|
||||||
|
let full_name = paths[0].file_path();
|
||||||
|
let path = Path::new(&full_name)
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or_default();
|
||||||
|
return full_name.strip_suffix(&path).unwrap_or_default().to_owned();
|
||||||
}
|
}
|
||||||
let mut prefix = file_path(&paths[0]);
|
let mut prefix = paths[0].file_path();
|
||||||
|
|
||||||
for s in paths.iter().skip(1) {
|
for s in paths.iter().skip(1) {
|
||||||
let s = file_path(s);
|
let s = s.file_path();
|
||||||
while !s.starts_with(&prefix) {
|
while !s.starts_with(&prefix) {
|
||||||
prefix.pop(); // Remove the last character of the prefix
|
prefix.pop(); // Remove the last character of the prefix
|
||||||
if prefix.is_empty() {
|
if prefix.is_empty() {
|
||||||
|
|
@ -80,21 +128,12 @@ impl TemplateApp {
|
||||||
|
|
||||||
prefix
|
prefix
|
||||||
}
|
}
|
||||||
pub fn image_from_dropped_file<P>(file: &DroppedFile, prefix: P) -> Option<ImageFile>
|
pub fn image_from_dropped_file<P>(file: &DroppedFile, prefix: P) -> Option<(String, ImageFile)>
|
||||||
where
|
where
|
||||||
P: AsRef<str>,
|
P: AsRef<str>,
|
||||||
{
|
{
|
||||||
let id;
|
let path = file.file_path();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
let base_id = path.replace(".png", "");
|
||||||
{
|
|
||||||
let path = file.path.as_ref().unwrap().clone();
|
|
||||||
id = path.to_str().unwrap().to_owned();
|
|
||||||
}
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
{
|
|
||||||
id = file.name.clone();
|
|
||||||
}
|
|
||||||
let base_id = id.replace(".png", "");
|
|
||||||
|
|
||||||
let id = base_id
|
let id = base_id
|
||||||
.strip_prefix(prefix.as_ref())
|
.strip_prefix(prefix.as_ref())
|
||||||
|
|
@ -102,83 +141,94 @@ impl TemplateApp {
|
||||||
.to_owned()
|
.to_owned()
|
||||||
.replace("\\", "/");
|
.replace("\\", "/");
|
||||||
|
|
||||||
let image = dynamic_image_from_file(file)?;
|
let image: DynamicImage = dynamic_image_from_file(file)?;
|
||||||
Some(ImageFile { id, image })
|
Some((path, ImageFile { id, image }))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_atlas(&mut self, ctx: &egui::Context) {
|
fn build_atlas(&mut self, ctx: &egui::Context) {
|
||||||
let prefix = Self::get_common_prefix(&self.dropped_files);
|
self.data = None;
|
||||||
println!("Prefix: {}", prefix);
|
self.image = None;
|
||||||
let images: Vec<ImageFile> = self
|
let images: Vec<ImageFile> = self.image_data.values().cloned().collect();
|
||||||
.dropped_files
|
|
||||||
.iter()
|
|
||||||
.flat_map(|f| Self::image_from_dropped_file(f, &prefix))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
self.data = Some(Spritesheet::build(self.config, &images, "name"));
|
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 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let config = TexturePackerConfig {
|
||||||
|
max_width: size,
|
||||||
|
max_height: size,
|
||||||
|
..self.config
|
||||||
|
};
|
||||||
|
self.data = Some(Spritesheet::build(
|
||||||
|
config,
|
||||||
|
&images,
|
||||||
|
format!("{}.png", &self.name),
|
||||||
|
));
|
||||||
if let Some(Ok(data)) = &self.data {
|
if let Some(Ok(data)) = &self.data {
|
||||||
let mut out_vec = vec![];
|
let mut out_vec = vec![];
|
||||||
let mut img =
|
data.image_data
|
||||||
image::DynamicImage::new_rgba8(data.atlas_asset.size[0], data.atlas_asset.size[1]);
|
.write_to(
|
||||||
image::imageops::overlay(&mut img, &data.image_data, 0, 0);
|
|
||||||
|
|
||||||
img.write_to(
|
|
||||||
&mut std::io::Cursor::new(&mut out_vec),
|
&mut std::io::Cursor::new(&mut out_vec),
|
||||||
image::ImageFormat::Png,
|
image::ImageFormat::Png,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
ctx.include_bytes("bytes://output.png", out_vec);
|
ctx.include_bytes("bytes://output.png", out_vec);
|
||||||
self.image =
|
self.image = Some(Image::from_uri("bytes://output.png"));
|
||||||
Some(Image::from_uri("bytes://output.png").max_size(Vec2::new(256.0, 256.0)));
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_atlas(&mut self) {
|
fn save_atlas(&self) -> Result<(), String> {
|
||||||
let Some(Ok(data)) = &self.data else {
|
let Some(Ok(spritesheet)) = &self.data else {
|
||||||
return;
|
return Err("Data is incorrect".to_owned());
|
||||||
};
|
};
|
||||||
let data = data.image_data.as_bytes().to_vec();
|
|
||||||
let filename = format!("{}.png", self.name);
|
let filename = format!("{}.png", self.name);
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
use std::io::Write;
|
|
||||||
let path_buf = rfd::FileDialog::new()
|
let path_buf = rfd::FileDialog::new()
|
||||||
.set_directory(".")
|
.set_directory(".")
|
||||||
.add_filter("Image", &["png"])
|
.add_filter("png", &["png"])
|
||||||
.set_file_name(filename)
|
.set_file_name(filename)
|
||||||
.save_file();
|
.save_file();
|
||||||
if let Some(path) = path_buf {
|
if let Some(path) = path_buf {
|
||||||
let mut file = std::fs::File::create(path).unwrap();
|
let write_result = spritesheet
|
||||||
let write_result = file.write_all(&data);
|
.image_data
|
||||||
|
.save_with_format(path, image::ImageFormat::Png);
|
||||||
if write_result.is_err() {
|
if write_result.is_err() {
|
||||||
self.data = Some(Err(format!(
|
return Err(format!(
|
||||||
"Could not make atlas, error: {:?}",
|
"Could not make atlas, error: {:?}",
|
||||||
write_result.unwrap_err()
|
write_result.unwrap_err()
|
||||||
)));
|
));
|
||||||
} else {
|
|
||||||
println!("Output texture stored in {:?}", file);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
|
let mut data = vec![];
|
||||||
|
let Ok(()) = spritesheet.image_data.write_to(
|
||||||
|
&mut std::io::Cursor::new(&mut data),
|
||||||
|
image::ImageFormat::Png,
|
||||||
|
) else {
|
||||||
|
return Err("Failed to copy data".to_owned());
|
||||||
|
};
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
let file = rfd::AsyncFileDialog::new()
|
let Some(file) = rfd::AsyncFileDialog::new()
|
||||||
.set_directory(".")
|
.set_directory(".")
|
||||||
.set_file_name(filename)
|
.set_file_name(filename)
|
||||||
.save_file()
|
.save_file()
|
||||||
.await;
|
.await
|
||||||
match file {
|
else {
|
||||||
None => (),
|
return;
|
||||||
Some(file) => {
|
};
|
||||||
// let module = serde_yaml::to_string(&module).unwrap();
|
|
||||||
// TODO: error handling
|
|
||||||
file.write(&data).await.unwrap();
|
file.write(&data).await.unwrap();
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,49 +285,21 @@ impl eframe::App for TemplateApp {
|
||||||
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| {
|
||||||
ui.with_layout(
|
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||||
egui::Layout::left_to_right(egui::Align::Center)
|
|
||||||
.with_cross_align(eframe::emath::Align::Center),
|
|
||||||
|ui| {
|
|
||||||
let text = egui::RichText::new("rPack")
|
let text = egui::RichText::new("rPack")
|
||||||
.font(FontId::new(26.0, FontFamily::Name("semibold".into())))
|
.font(FontId::new(26.0, FontFamily::Name("semibold".into())))
|
||||||
.color(MY_ACCENT_COLOR32)
|
.color(MY_ACCENT_COLOR32)
|
||||||
.strong();
|
.strong();
|
||||||
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, HEADER_HEIGHT));
|
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, HEADER_HEIGHT));
|
||||||
ui.add(egui::Label::new(text));
|
ui.add(egui::Label::new(text));
|
||||||
let available_width =
|
|
||||||
ui.available_width() - ((TOP_BUTTON_WIDTH - TOP_SIDE_MARGIN) * 3.0);
|
|
||||||
ui.allocate_space(egui::vec2(available_width, HEADER_HEIGHT));
|
|
||||||
ui.add_enabled_ui(self.data.is_some(), |ui| {
|
|
||||||
if ui
|
|
||||||
.add_sized([TOP_BUTTON_WIDTH, 30.0], egui::Button::new("Save"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.save_atlas();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
ui.add_enabled_ui(!self.dropped_files.is_empty(), |ui| {
|
|
||||||
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, 10.0));
|
|
||||||
if ui
|
|
||||||
.add_sized(
|
|
||||||
[TOP_BUTTON_WIDTH, 30.0],
|
|
||||||
egui::Button::new("Build atlas"),
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.image = None;
|
|
||||||
ctx.forget_image("bytes://output.png");
|
|
||||||
self.build_atlas(ctx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, 10.0));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
ctx.input(|i| {
|
ctx.input(|i| {
|
||||||
if !i.raw.dropped_files.is_empty() {
|
if !i.raw.dropped_files.is_empty() {
|
||||||
let mut extra = i.raw.dropped_files.clone();
|
let mut extra = i.raw.dropped_files.clone();
|
||||||
self.dropped_files.append(&mut extra);
|
self.dropped_files.append(&mut extra);
|
||||||
|
self.data = None;
|
||||||
|
self.rebuild_image_data();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
egui::TopBottomPanel::bottom("bottom_panel")
|
egui::TopBottomPanel::bottom("bottom_panel")
|
||||||
|
|
@ -285,90 +307,101 @@ impl eframe::App for TemplateApp {
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
powered_by_egui_and_eframe(ui);
|
powered_by_egui_and_eframe(ui);
|
||||||
});
|
});
|
||||||
|
egui::SidePanel::right("leftPanel")
|
||||||
|
.min_width(200.0)
|
||||||
|
.frame(egui::Frame::canvas(&ctx.style()))
|
||||||
|
.show_animated(ctx, !self.image_data.is_empty(), |ui| {
|
||||||
|
CollapsingHeader::new("Settings")
|
||||||
|
.default_open(true)
|
||||||
|
.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.add_space(10.0);
|
||||||
|
ui.add(
|
||||||
|
egui::Slider::new(&mut self.max_size, self.min_size[0]..=4096)
|
||||||
|
.step_by(32.0)
|
||||||
|
.text("Max size"),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
egui::Slider::new(&mut self.config.border_padding, 0..=10)
|
||||||
|
.text("Border Padding"),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
egui::Slider::new(&mut self.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")
|
||||||
|
.on_hover_text("Draw the red line on the edge of the each frames. Useful for debugging.");
|
||||||
|
// 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.dropped_files.is_empty(), |ui| {
|
||||||
|
if ui
|
||||||
|
.add_sized([TOP_BUTTON_WIDTH, 30.0], egui::Button::new("Build atlas"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.image = None;
|
||||||
|
ctx.forget_image("bytes://output.png");
|
||||||
|
self.build_atlas(ctx);
|
||||||
|
}
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
CollapsingHeader::new("Image list")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
if !self.image_data.is_empty() && ui.button("clear list").clicked() {
|
||||||
|
self.image_data.clear();
|
||||||
|
self.dropped_files.clear();
|
||||||
|
self.data = None;
|
||||||
|
self.update_min_size();
|
||||||
|
}
|
||||||
|
let mut to_remove: Option<String> = None;
|
||||||
|
for (id, file) in self.image_data.iter() {
|
||||||
|
ui.horizontal_top(|ui| {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
if ui.button("x").clicked() {
|
||||||
|
to_remove = Some(id.clone());
|
||||||
|
}
|
||||||
|
ui.add_space(10.0);
|
||||||
|
let (x, y) = file.image.dimensions();
|
||||||
|
ui.label(&file.id)
|
||||||
|
.on_hover_text(format!("Dimensions: {}x{}", x, y));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(index) = to_remove {
|
||||||
|
if let Some(i) = self
|
||||||
|
.dropped_files
|
||||||
|
.iter()
|
||||||
|
.position(|e| e.file_path().eq(&index))
|
||||||
|
{
|
||||||
|
self.dropped_files.remove(i);
|
||||||
|
self.image_data.remove(&index);
|
||||||
|
self.data = None;
|
||||||
|
self.rebuild_image_data();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.id_salt("vertical_scroll")
|
||||||
|
.show(ui, |ui| {
|
||||||
if let Some(Err(error)) = &self.data {
|
if let Some(Err(error)) = &self.data {
|
||||||
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.dropped_files.is_empty() {
|
if self.dropped_files.is_empty() {
|
||||||
ui.horizontal_top(|ui|{
|
|
||||||
if let Some(image) = &self.image {
|
|
||||||
ui.add(image.clone());
|
|
||||||
}
|
|
||||||
CollapsingHeader::new("Settings")
|
|
||||||
.default_open(false)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label("Tilemap id");
|
|
||||||
ui.text_edit_singleline(&mut self.name);
|
|
||||||
ui.add(
|
|
||||||
egui::Slider::new(&mut self.config.max_width, 64..=4096).text("Width"),
|
|
||||||
);
|
|
||||||
ui.add(
|
|
||||||
egui::Slider::new(&mut self.config.max_height, 64..=4096).text("Height"),
|
|
||||||
);
|
|
||||||
ui.add(
|
|
||||||
egui::Slider::new(&mut self.config.border_padding, 0..=10).text("Border Padding"),
|
|
||||||
);
|
|
||||||
ui.add(
|
|
||||||
egui::Slider::new(&mut self.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")
|
|
||||||
.on_hover_text("True to draw the red line on the edge of the each frames. Useful for debugging.");
|
|
||||||
ui.checkbox(&mut self.config.trim, "Trim").on_hover_text("True to trim the empty pixels of the input images.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui|{
|
|
||||||
|
|
||||||
egui::ScrollArea::vertical().auto_shrink(false).show(ui, |ui| {
|
|
||||||
if let Some(Ok(data)) = &self.data {
|
|
||||||
ui.horizontal_top(|ui|{
|
|
||||||
ui.label(format!("{} frames, size: {}x{}",data.atlas_asset.frames.len(),data.atlas_asset.size[0],data.atlas_asset.size[1]));
|
|
||||||
});
|
|
||||||
ui.label(RichText::new("Frames JSON").strong());
|
|
||||||
egui_json_tree::JsonTree::new("simple-tree", &data.atlas_asset_json).show(ui);
|
|
||||||
if ui
|
|
||||||
.add(egui::Button::new("Copy JSON to Clipboard"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
ui.output_mut(|o| o.copied_text = data.atlas_asset_json.to_string());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
ui.separator();
|
|
||||||
let mut index_to_remove : Option<usize> = None;
|
|
||||||
for (i, file) in self.dropped_files.iter().enumerate() {
|
|
||||||
let mut info = if let Some(path) = &file.path {
|
|
||||||
path.display().to_string()
|
|
||||||
} else if !file.name.is_empty() {
|
|
||||||
file.name.clone()
|
|
||||||
} else {
|
|
||||||
"???".to_owned()
|
|
||||||
};
|
|
||||||
if let Some(bytes) = &file.bytes {
|
|
||||||
info += &format!(" ({} bytes)", bytes.len());
|
|
||||||
}
|
|
||||||
ui.horizontal_top(|ui|{
|
|
||||||
if ui.button("x").clicked(){
|
|
||||||
index_to_remove = Some(i);
|
|
||||||
}
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.label(info);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(index) = index_to_remove{
|
|
||||||
self.dropped_files.remove(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if ui.button("clear list").clicked() {
|
|
||||||
self.dropped_files.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
ui.add_space(50.0);
|
ui.add_space(50.0);
|
||||||
ui.label(
|
ui.label(
|
||||||
|
|
@ -376,26 +409,96 @@ impl eframe::App for TemplateApp {
|
||||||
.heading()
|
.heading()
|
||||||
.color(MY_ACCENT_COLOR32),
|
.color(MY_ACCENT_COLOR32),
|
||||||
);
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
let Some(image) = &self.image else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(Ok(data)) = &self.data else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
egui::Frame::canvas(&ctx.style()).show(ui, |ui| {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.heading(
|
||||||
|
egui::RichText::new("Crated atlas").color(MY_ACCENT_COLOR32),
|
||||||
|
);
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(format!(
|
||||||
|
"{} sprites\nsize: {}x{}",
|
||||||
|
data.atlas_asset.frames.len(),
|
||||||
|
data.atlas_asset.size[0],
|
||||||
|
data.atlas_asset.size[1]
|
||||||
|
));
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.add_enabled_ui(self.data.is_some(), |ui| {
|
||||||
|
if ui
|
||||||
|
.add_sized(
|
||||||
|
[TOP_BUTTON_WIDTH, 30.0],
|
||||||
|
egui::Button::new("Save atlas image"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if let Err(error) = self.save_atlas() {
|
||||||
|
eprintln!("ERROR: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(10.0);
|
||||||
|
CollapsingHeader::new("Atlas JSON")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
if ui
|
||||||
|
.add(egui::Button::new("Copy JSON to Clipboard"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
ui.output_mut(|o| {
|
||||||
|
o.copied_text = data.atlas_asset_json.to_string()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(RichText::new("Frames JSON").strong());
|
||||||
|
ui.add_space(10.0);
|
||||||
|
egui_json_tree::JsonTree::new(
|
||||||
|
"simple-tree",
|
||||||
|
&data.atlas_asset_json,
|
||||||
|
)
|
||||||
|
.show(ui);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.separator();
|
||||||
|
ui.add(image.clone());
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.add_space(10.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_path(file: &DroppedFile) -> String {
|
trait FilePath {
|
||||||
|
fn file_path(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilePath for DroppedFile {
|
||||||
|
fn file_path(&self) -> String {
|
||||||
let id;
|
let id;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
let path = file.path.as_ref().unwrap().clone();
|
let path = self.path.as_ref().unwrap().clone();
|
||||||
id = path.to_str().unwrap().to_owned();
|
id = path.to_str().unwrap().to_owned();
|
||||||
}
|
}
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
id = file.name.clone();
|
id = self.name.clone();
|
||||||
}
|
}
|
||||||
id.replace(".png", "")
|
id.replace(".png", "")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn dynamic_image_from_file(file: &DroppedFile) -> Option<DynamicImage> {
|
fn dynamic_image_from_file(file: &DroppedFile) -> Option<DynamicImage> {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue