mirror of https://github.com/Leinnan/rpack.git
Reinit
This commit is contained in:
commit
7e62334e54
|
|
@ -0,0 +1,6 @@
|
||||||
|
# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work
|
||||||
|
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
|
||||||
|
# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility
|
||||||
|
# we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93
|
||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
rustflags = ["--cfg=web_sys_unstable_apis"]
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: Github Pages
|
||||||
|
|
||||||
|
# By default, runs if you push to master. keeps your deployed app in sync with master branch.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
# to only run when you do a new github release, comment out above part and uncomment the below trigger.
|
||||||
|
# on:
|
||||||
|
# release:
|
||||||
|
# types:
|
||||||
|
# - published
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # for committing to gh-pages branch.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-github-pages:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2 # repo checkout
|
||||||
|
- uses: actions-rs/toolchain@v1 # get rust toolchain for wasm
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
target: wasm32-unknown-unknown
|
||||||
|
override: true
|
||||||
|
- name: Rust Cache # cache the rust build artefacts
|
||||||
|
uses: Swatinem/rust-cache@v1
|
||||||
|
- name: Download and install Trunk binary
|
||||||
|
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
|
||||||
|
- name: Build # build
|
||||||
|
# "${GITHUB_REPOSITORY#*/}" evaluates into the name of the repository
|
||||||
|
# using --public-url something will allow trunk to modify all the href paths like from favicon.ico to repo_name/favicon.ico .
|
||||||
|
# this is necessary for github pages where the site is deployed to username.github.io/repo_name and all files must be requested
|
||||||
|
# relatively as eframe_template/favicon.ico. if we skip public-url option, the href paths will instead request username.github.io/favicon.ico which
|
||||||
|
# will obviously return error 404 not found.
|
||||||
|
run: ./trunk build --release
|
||||||
|
- name: Deploy
|
||||||
|
uses: JamesIves/github-pages-deploy-action@v4
|
||||||
|
with:
|
||||||
|
folder: dist
|
||||||
|
# this option will not maintain any history of your previous pages deployment
|
||||||
|
# set to false if you want all page build to be committed to your gh-pages branch history
|
||||||
|
single-commit: true
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
env:
|
||||||
|
# This is required to enable the web_sys clipboard API which egui_web uses
|
||||||
|
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
|
||||||
|
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
|
||||||
|
RUSTFLAGS: --cfg=web_sys_unstable_apis
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
args: --all-features
|
||||||
|
|
||||||
|
check_wasm:
|
||||||
|
name: Check wasm32
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
target: wasm32-unknown-unknown
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
args: --all-features --lib --target wasm32-unknown-unknown
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test Suite
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- 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:
|
||||||
|
name: Rustfmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
components: rustfmt
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
name: Clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- 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:
|
||||||
|
name: trunk
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: 1.72.0
|
||||||
|
target: wasm32-unknown-unknown
|
||||||
|
override: true
|
||||||
|
- name: Download and install Trunk binary
|
||||||
|
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
|
||||||
|
- name: Build
|
||||||
|
run: ./trunk build
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
/dist
|
||||||
|
skyline-packer-output.png
|
||||||
|
result.png
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,55 @@
|
||||||
|
[package]
|
||||||
|
name = "rpack"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Piotr Siuszko <siuszko@zoho.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.72"
|
||||||
|
repository = "https://github.com/Leinnan/rpack.git"
|
||||||
|
homepage = "https://github.com/Leinnan/rpack"
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
egui = "0.24.1"
|
||||||
|
eframe = { version = "0.24.1", 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"
|
||||||
|
|
||||||
|
# You only need serde if you want app persistence:
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
texture_packer = {version="0.25.0", features = ["common"]}
|
||||||
|
image = { version = "0.24", features = ["jpeg", "png"] }
|
||||||
|
egui_extras = { version = "*", features = ["all_loaders"] }
|
||||||
|
|
||||||
|
# native:
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
env_logger = "0.10"
|
||||||
|
|
||||||
|
# web:
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
web-sys = {version = "0.3", features=["Url","HtmlAnchorElement","Blob", "BlobPropertyBag"]}
|
||||||
|
js-sys = "0.3"
|
||||||
|
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 2 # fast and small wasm
|
||||||
|
|
||||||
|
# Optimize all dependencies even in debug builds:
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
|
||||||
|
# If you want to use the bleeding edge version of egui and eframe:
|
||||||
|
# egui = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||||
|
# eframe = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||||
|
|
||||||
|
# If you fork https://github.com/emilk/egui you can test with:
|
||||||
|
# egui = { path = "../egui/crates/egui" }
|
||||||
|
# eframe = { path = "../egui/crates/eframe" }
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# rpack
|
||||||
|
|
||||||
|
[](https://github.com/Leinnan/rpack/actions?workflow=CI)
|
||||||
|
|
||||||
|
Attempt to build texture atlas packer GUI.
|
||||||
|
|
||||||
|
### Testing locally
|
||||||
|
|
||||||
|
Make sure you are using the latest version of stable rust by running `rustup update`.
|
||||||
|
|
||||||
|
`cargo run --release`
|
||||||
|
|
||||||
|
On Linux you need to first run:
|
||||||
|
|
||||||
|
`sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
|
||||||
|
|
||||||
|
On Fedora Rawhide you need to run:
|
||||||
|
|
||||||
|
`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel gtk3-devel atk fontconfig-devel`
|
||||||
|
|
||||||
|
### Web Locally
|
||||||
|
|
||||||
|
You can compile your app to [WASM](https://en.wikipedia.org/wiki/WebAssembly) and publish it as a web page.
|
||||||
|
|
||||||
|
We use [Trunk](https://trunkrs.dev/) to build for web target.
|
||||||
|
1. Install the required target with `rustup target add wasm32-unknown-unknown`.
|
||||||
|
2. Install Trunk with `cargo install --locked trunk`.
|
||||||
|
3. Run `trunk serve` to build and serve on `http://127.0.0.1:8080`. Trunk will rebuild automatically if you edit the project.
|
||||||
|
4. Open `http://127.0.0.1:8080/index.html#dev` in a browser. See the warning below.
|
||||||
|
|
||||||
|
> `assets/sw.js` script will try to cache our app, and loads the cached version when it cannot connect to server allowing your app to work offline (like PWA).
|
||||||
|
> appending `#dev` to `index.html` will skip this caching, allowing us to load the latest builds during development.
|
||||||
|
|
||||||
|
### Web Deploy
|
||||||
|
1. Just run `trunk build --release`.
|
||||||
|
2. It will generate a `dist` directory as a "static html" website
|
||||||
|
3. Upload the `dist` directory to any of the numerous free hosting websites including [GitHub Pages](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site).
|
||||||
|
4. we already provide a workflow that auto-deploys our app to GitHub pages if you enable it.
|
||||||
|
> To enable Github Pages, you need to go to Repository -> Settings -> Pages -> Source -> set to `gh-pages` branch and `/` (root).
|
||||||
|
>
|
||||||
|
> If `gh-pages` is not available in `Source`, just create and push a branch called `gh-pages` and it should be available.
|
||||||
|
|
||||||
|
You can test the template app at <https://emilk.github.io/eframe_template/>.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[build]
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 314 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "egui Template PWA",
|
||||||
|
"short_name": "egui-template-pwa",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "./icon-256.png",
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "./maskable_icon_x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "./icon-1024.png",
|
||||||
|
"sizes": "1024x1024",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lang": "en-US",
|
||||||
|
"id": "/index.html",
|
||||||
|
"start_url": "./index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "white",
|
||||||
|
"theme_color": "white"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
|
|
@ -0,0 +1,25 @@
|
||||||
|
var cacheName = 'egui-template-pwa';
|
||||||
|
var filesToCache = [
|
||||||
|
'./',
|
||||||
|
'./index.html',
|
||||||
|
'./rpack.js',
|
||||||
|
'./rpack_bg.wasm',
|
||||||
|
];
|
||||||
|
|
||||||
|
/* Start the service worker and cache all of the app's content */
|
||||||
|
self.addEventListener('install', function (e) {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.open(cacheName).then(function (cache) {
|
||||||
|
return cache.addAll(filesToCache);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Serve cached content when offline */
|
||||||
|
self.addEventListener('fetch', function (e) {
|
||||||
|
e.respondWith(
|
||||||
|
caches.match(e.request).then(function (response) {
|
||||||
|
return response || fetch(e.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// note: add error checking yourself.
|
||||||
|
{
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(["rev-parse", "HEAD"])
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let git_hash = String::from_utf8(output.stdout).unwrap();
|
||||||
|
println!("cargo:rustc-env=GIT_HASH={}", &git_hash[..7]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(["log", "-1", "--date=format:%Y/%m/%d %T", "--format=%ad"])
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let git_hash = String::from_utf8(output.stdout).unwrap();
|
||||||
|
println!("cargo:rustc-env=GIT_DATE={}", git_hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# This scripts runs various CI-like checks in a convenient way.
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
cargo check --workspace --all-targets
|
||||||
|
cargo check --workspace --all-features --lib --target wasm32-unknown-unknown
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::all
|
||||||
|
cargo test --workspace --all-targets --all-features
|
||||||
|
cargo test --workspace --doc
|
||||||
|
trunk build
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
|
||||||
|
<!-- Disable zooming: -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!-- change this to your project name -->
|
||||||
|
<title>rPack</title>
|
||||||
|
|
||||||
|
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
|
||||||
|
<link data-trunk rel="rust" data-wasm-opt="2" />
|
||||||
|
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
|
||||||
|
<base data-trunk-public-url />
|
||||||
|
|
||||||
|
<link data-trunk rel="icon" href="assets/favicon.ico">
|
||||||
|
|
||||||
|
|
||||||
|
<link data-trunk rel="copy-file" href="assets/sw.js" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/manifest.json" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/icon-1024.png" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/icon-256.png" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/icon_ios_touch_192.png" />
|
||||||
|
<link data-trunk rel="copy-file" href="assets/maskable_icon_x512.png" />
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<link rel="apple-touch-icon" href="icon_ios_touch_192.png">
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
/* Remove touch delay: */
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* Light mode background color for what is not covered by the egui canvas,
|
||||||
|
or where the egui canvas is translucent. */
|
||||||
|
background: #909090;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
/* Dark mode background color for what is not covered by the egui canvas,
|
||||||
|
or where the egui canvas is translucent. */
|
||||||
|
background: #404040;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow canvas to fill entire web page: */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position canvas in center-top: */
|
||||||
|
canvas {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: Ubuntu-Light, Helvetica, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------- */
|
||||||
|
/* Loading animation from https://loading.io/css/ */
|
||||||
|
.lds-dual-ring {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lds-dual-ring:after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
border-color: #fff transparent #fff transparent;
|
||||||
|
animation: lds-dual-ring 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lds-dual-ring {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- The WASM code will resize the canvas dynamically -->
|
||||||
|
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
|
||||||
|
<canvas id="the_canvas_id"></canvas>
|
||||||
|
|
||||||
|
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
|
||||||
|
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
|
||||||
|
<script>
|
||||||
|
// We disable caching during development so that we always view the latest version.
|
||||||
|
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
navigator.serviceWorker.register('sw.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<!-- Powered by egui: https://github.com/emilk/egui/ -->
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# If you see this, run "rustup self update" to get rustup 1.23 or newer.
|
||||||
|
|
||||||
|
# NOTE: above comment is for older `rustup` (before TOML support was added),
|
||||||
|
# which will treat the first line as the toolchain name, and therefore show it
|
||||||
|
# to the user in the error, instead of "error: invalid channel name '[toolchain]'".
|
||||||
|
|
||||||
|
[toolchain]
|
||||||
|
channel = "1.72.0"
|
||||||
|
components = [ "rustfmt", "clippy" ]
|
||||||
|
targets = [ "wasm32-unknown-unknown" ]
|
||||||
|
|
@ -0,0 +1,420 @@
|
||||||
|
use std::{collections::HashMap, io::Cursor};
|
||||||
|
|
||||||
|
use egui::{CollapsingHeader, Color32, DroppedFile, FontFamily, FontId, Image, RichText};
|
||||||
|
use image::DynamicImage;
|
||||||
|
|
||||||
|
use texture_packer::{
|
||||||
|
importer::ImageImporter, texture::Texture, TexturePacker, TexturePackerConfig,
|
||||||
|
};
|
||||||
|
pub const MY_ACCENT_COLOR32: Color32 = Color32::from_rgb(230, 102, 1);
|
||||||
|
pub const TOP_SIDE_MARGIN: f32 = 10.0;
|
||||||
|
pub const HEADER_HEIGHT: f32 = 45.0;
|
||||||
|
pub const TOP_BUTTON_WIDTH: f32 = 150.0;
|
||||||
|
pub const GIT_HASH: &str = env!("GIT_HASH");
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Spritesheet {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub frames: HashMap<String, texture_packer::Frame<String>>,
|
||||||
|
pub size: (u32, u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 TemplateApp {
|
||||||
|
// Example stuff:
|
||||||
|
label: String,
|
||||||
|
|
||||||
|
#[serde(skip)] // This how you opt-out of serialization of a field
|
||||||
|
value: f32,
|
||||||
|
#[serde(skip)]
|
||||||
|
dropped_files: Vec<DroppedFile>,
|
||||||
|
#[serde(skip)]
|
||||||
|
config: TexturePackerConfig,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
image: Option<Image<'static>>,
|
||||||
|
#[serde(skip)]
|
||||||
|
counter: i32,
|
||||||
|
#[serde(skip)]
|
||||||
|
data: Option<Spritesheet>,
|
||||||
|
#[serde(skip)]
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TemplateApp {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
// Example stuff:
|
||||||
|
label: "Hello World!".to_owned(),
|
||||||
|
value: 2.7,
|
||||||
|
dropped_files: vec![],
|
||||||
|
config: TexturePackerConfig {
|
||||||
|
max_width: 2048,
|
||||||
|
max_height: 2048,
|
||||||
|
allow_rotation: false,
|
||||||
|
border_padding: 2,
|
||||||
|
trim: false,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
counter: 0,
|
||||||
|
image: None,
|
||||||
|
data: None,
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateApp {
|
||||||
|
/// Called once before the first frame.
|
||||||
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
fn build_atlas(&mut self, ctx: &egui::Context) {
|
||||||
|
self.error = None;
|
||||||
|
let mut packer = TexturePacker::new_skyline(self.config);
|
||||||
|
|
||||||
|
for file in &self.dropped_files {
|
||||||
|
let id = id_for_file(file);
|
||||||
|
let texture = dynamic_image_from_file(file);
|
||||||
|
let can_pack = packer.can_pack(&texture);
|
||||||
|
|
||||||
|
if can_pack {
|
||||||
|
packer.pack_own(id, texture).unwrap();
|
||||||
|
} else {
|
||||||
|
self.error = Some(format!(
|
||||||
|
"Consider making atlas bigger. Could not make atlas, failed on: {}",
|
||||||
|
id
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (name, frame) in packer.get_frames() {
|
||||||
|
println!(" {:7} : {:?}", name, frame.frame);
|
||||||
|
}
|
||||||
|
let mut out_vec = vec![];
|
||||||
|
let exporter = texture_packer::exporter::ImageExporter::export(&packer).unwrap();
|
||||||
|
exporter
|
||||||
|
.write_to(&mut Cursor::new(&mut out_vec), image::ImageFormat::Png)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.data = Some(Spritesheet {
|
||||||
|
data: out_vec.clone(),
|
||||||
|
frames: packer.get_frames().clone(),
|
||||||
|
size: (packer.width(), packer.height()),
|
||||||
|
});
|
||||||
|
let id = format!("bytes://output_{}.png", self.counter);
|
||||||
|
self.image = None;
|
||||||
|
ctx.forget_image(&id);
|
||||||
|
self.counter += 1;
|
||||||
|
|
||||||
|
let id = format!("bytes://output_{}.png", self.counter);
|
||||||
|
ctx.include_bytes(id.clone(), out_vec.clone());
|
||||||
|
println!("LENGTH OF {}: {}", id.clone(), out_vec.len());
|
||||||
|
self.image = Some(Image::from_uri(id.clone()));
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_atlas(&mut self) {
|
||||||
|
if self.data.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = self.data.clone().unwrap().data;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
use std::io::Write;
|
||||||
|
let mut file = std::fs::File::create("result.png").unwrap();
|
||||||
|
let write_result = file.write_all(&data);
|
||||||
|
if write_result.is_err() {
|
||||||
|
self.error = Some(format!(
|
||||||
|
"Could not make atlas, error: {:?}",
|
||||||
|
write_result.unwrap_err()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
println!("Output texture stored in {:?}", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
save_blob_on_wasm(&data, "result.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn save_blob_on_wasm(buf: &[u8], id: &str) {
|
||||||
|
use wasm_bindgen::*;
|
||||||
|
use web_sys::*;
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
let doc = window.document().unwrap();
|
||||||
|
let arr: js_sys::Array = buf
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.flat_map(|n| n.to_be_bytes().into_iter().map(JsValue::from))
|
||||||
|
.collect();
|
||||||
|
let blob = Blob::new_with_u8_array_sequence_and_options(
|
||||||
|
&arr,
|
||||||
|
web_sys::BlobPropertyBag::new().type_("data:image/png;base64"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let blob_url = web_sys::Url::create_object_url_with_blob(&blob).unwrap();
|
||||||
|
let download_link = doc.create_element("a").unwrap();
|
||||||
|
let download_link: HtmlAnchorElement = download_link.unchecked_into();
|
||||||
|
download_link.set_href(&blob_url);
|
||||||
|
download_link.set_download(id);
|
||||||
|
doc.body().unwrap().append_child(&download_link).unwrap();
|
||||||
|
download_link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Install my own font (maybe supporting non-latin characters).
|
||||||
|
// .ttf and .otf files supported.
|
||||||
|
fonts.font_data.insert(
|
||||||
|
"regular".to_owned(),
|
||||||
|
egui::FontData::from_static(include_bytes!("../static/JetBrainsMonoNL-Regular.ttf")),
|
||||||
|
);
|
||||||
|
fonts.font_data.insert(
|
||||||
|
"semibold".to_owned(),
|
||||||
|
egui::FontData::from_static(include_bytes!("../static/JetBrainsMono-SemiBold.ttf")),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
// Tell egui to use these fonts:
|
||||||
|
ctx.set_fonts(fonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for TemplateApp {
|
||||||
|
/// 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) {
|
||||||
|
egui::TopBottomPanel::top("topPanel")
|
||||||
|
.frame(egui::Frame::canvas(&ctx.style()))
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.with_layout(
|
||||||
|
egui::Layout::left_to_right(egui::Align::Center)
|
||||||
|
.with_cross_align(eframe::emath::Align::Center),
|
||||||
|
|ui| {
|
||||||
|
let text = egui::RichText::new("rPack")
|
||||||
|
.font(FontId::new(26.0, FontFamily::Name("semibold".into())))
|
||||||
|
.color(MY_ACCENT_COLOR32)
|
||||||
|
.strong();
|
||||||
|
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, HEADER_HEIGHT));
|
||||||
|
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.build_atlas(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, 10.0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ctx.input(|i| {
|
||||||
|
if !i.raw.dropped_files.is_empty() {
|
||||||
|
self.dropped_files = i.raw.dropped_files.clone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
if let Some(error) = &self.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));
|
||||||
|
}
|
||||||
|
if !self.dropped_files.is_empty() {
|
||||||
|
CollapsingHeader::new("Settings")
|
||||||
|
.default_open(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::Slider::new(&mut self.config.max_width, 64..=4096).text("Max width"),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
egui::Slider::new(&mut self.config.max_height, 64..=4096).text("Max 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|{
|
||||||
|
|
||||||
|
if let Some(image) = &self.image {
|
||||||
|
ui.horizontal_top(|ui|{
|
||||||
|
let data = &self.data.clone().unwrap();
|
||||||
|
ui.label(format!("{} frames, size: {}x{}",data.frames.len(),data.size.0,data.size.1));
|
||||||
|
});
|
||||||
|
CollapsingHeader::new("Preview")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(image.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
egui::ScrollArea::vertical().auto_shrink(false).show(ui, |ui| {
|
||||||
|
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.add_space(50.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Drop files here")
|
||||||
|
.heading()
|
||||||
|
.color(MY_ACCENT_COLOR32),
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
egui::TopBottomPanel::bottom("bottom_panel")
|
||||||
|
.frame(egui::Frame::canvas(&ctx.style()))
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
powered_by_egui_and_eframe(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id_for_file(file: &DroppedFile) -> String {
|
||||||
|
let id;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let path = file.path.as_ref().unwrap().clone();
|
||||||
|
id = path
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
id = file.name.clone();
|
||||||
|
}
|
||||||
|
id.replace(".png", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dynamic_image_from_file(file: &DroppedFile) -> DynamicImage {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let bytes = file.bytes.as_ref().clone();
|
||||||
|
|
||||||
|
ImageImporter::import_from_memory(&bytes.unwrap())
|
||||||
|
.expect("Unable to import file. Run this example with --features=\"png\"")
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let path = file.path.as_ref().unwrap().clone();
|
||||||
|
|
||||||
|
ImageImporter::import_from_file(&path)
|
||||||
|
.expect("Unable to import file. Run this example with --features=\"png\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn powered_by_egui_and_eframe(ui: &mut egui::Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.hyperlink_to(format!("Build: {}", GIT_HASH), env!("CARGO_PKG_HOMEPAGE"));
|
||||||
|
egui::warn_if_debug_build(ui);
|
||||||
|
ui.separator();
|
||||||
|
egui::widgets::global_dark_light_mode_buttons(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.label(". ");
|
||||||
|
ui.label("Powered by ");
|
||||||
|
ui.hyperlink_to("egui", "https://github.com/emilk/egui");
|
||||||
|
ui.label(" and ");
|
||||||
|
ui.hyperlink_to(
|
||||||
|
"eframe",
|
||||||
|
"https://github.com/emilk/egui/tree/master/crates/eframe",
|
||||||
|
);
|
||||||
|
ui.label(".");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
#![warn(clippy::all, rust_2018_idioms)]
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
pub use app::TemplateApp;
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
#![warn(clippy::all, rust_2018_idioms)]
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
|
|
||||||
|
// 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`).
|
||||||
|
|
||||||
|
let native_options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default()
|
||||||
|
.with_inner_size([400.0, 300.0])
|
||||||
|
.with_min_inner_size([300.0, 220.0]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
eframe::run_native(
|
||||||
|
"rPack",
|
||||||
|
native_options,
|
||||||
|
Box::new(|cc| Box::new(rpack::TemplateApp::new(cc))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When compiling to web using trunk:
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn main() {
|
||||||
|
// Redirect `log` message to `console.log` and friends:
|
||||||
|
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
|
||||||
|
|
||||||
|
let web_options = eframe::WebOptions::default();
|
||||||
|
|
||||||
|
wasm_bindgen_futures::spawn_local(async {
|
||||||
|
eframe::WebRunner::new()
|
||||||
|
.start(
|
||||||
|
"the_canvas_id", // hardcode it
|
||||||
|
web_options,
|
||||||
|
Box::new(|cc| Box::new(rpack::TemplateApp::new(cc))),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to start eframe");
|
||||||
|
});
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue