Compare commits
6 commits
84db74586b
...
3c4ad68059
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c4ad68059 | |||
| d748f742f7 | |||
| b4f7fb4171 | |||
| a51f5c6b57 | |||
| faf42da522 | |||
| 3f1c523ad9 |
46 changed files with 1407 additions and 1760 deletions
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -1,18 +1,11 @@
|
|||
*.exe
|
||||
*.sublime-workspace
|
||||
*.pdb
|
||||
*.raddbgi
|
||||
*.dll
|
||||
*.exp
|
||||
*.lib
|
||||
log.txt
|
||||
*.bin
|
||||
*.dylib
|
||||
*.so
|
||||
*.dSYM
|
||||
*.a
|
||||
linux
|
||||
|
||||
# gets automagically downloaded through scripts/setup.sh
|
||||
vendors/clay-odin
|
||||
vendors/clay-odin.zip
|
||||
|
||||
build/
|
||||
build_generator/
|
||||
ols.json
|
||||
|
|
|
|||
9
.gitmodules
vendored
9
.gitmodules
vendored
|
|
@ -1,6 +1,3 @@
|
|||
[submodule "src/aseprite"]
|
||||
path = src/aseprite
|
||||
url = https://github.com/bersK/odin-aseprite.git
|
||||
[submodule "src/dialog/libtinyfiledialogs"]
|
||||
path = src/dialog/libtinyfiledialogs
|
||||
url = https://github.com/native-toolkit/libtinyfiledialogs.git
|
||||
[submodule "vendors/odin-aseprite"]
|
||||
path = vendors/odin-aseprite
|
||||
url = git@github.com:blob1807/odin-aseprite.git
|
||||
|
|
|
|||
31
.vscode/launch.json
vendored
31
.vscode/launch.json
vendored
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "Build Debug",
|
||||
"name": "Debug",
|
||||
"program": "${workspaceFolder}/build/game_debug.exe",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "Build Release",
|
||||
"name": "Release",
|
||||
"program": "${workspaceFolder}/game_release.exe",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"name": "Run File",
|
||||
"program": "odin",
|
||||
"args": ["run", "${fileBasename}", "-file"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"[odin]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 4
|
||||
}
|
||||
}
|
||||
92
.vscode/tasks.json
vendored
92
.vscode/tasks.json
vendored
|
|
@ -1,92 +0,0 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"command": "",
|
||||
"args": [],
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build Debug",
|
||||
"type": "shell",
|
||||
"windows": {
|
||||
"command": "${workspaceFolder}/scripts/build_debug.bat"
|
||||
},
|
||||
"linux": {
|
||||
"command": "${workspaceFolder}/scripts/build_debug.sh"
|
||||
},
|
||||
"osx": {
|
||||
"command": "${workspaceFolder}/scripts/build_debug.sh"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build Release",
|
||||
"type": "shell",
|
||||
"windows": {
|
||||
"command": "${workspaceFolder}/scripts/build_release.bat"
|
||||
},
|
||||
"linux": {
|
||||
"command": "${workspaceFolder}/scripts/build_release.sh"
|
||||
},
|
||||
"osx": {
|
||||
"command": "${workspaceFolder}/scripts/build_release.sh"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Clean build folder(s)",
|
||||
"type": "shell",
|
||||
"windows": {
|
||||
"command": "cd ${workspaceFolder}\\build && rm game*; cd ${workspaceFolder} && rm aseprite_odin_generator*"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Build Hot Reload",
|
||||
"type": "shell",
|
||||
"windows": {
|
||||
"command": "${workspaceFolder}/scripts/build_hot_reload.bat; start game.exe"
|
||||
},
|
||||
"linux": {
|
||||
"command": "${workspaceFolder}/scripts/build_hot_reload.sh"
|
||||
},
|
||||
"osx": {
|
||||
"command": "${workspaceFolder}/scripts/build_hot_reload.sh"
|
||||
},
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
},
|
||||
"group": "build",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build&Run Atlas Generator Test",
|
||||
"type": "shell",
|
||||
"windows": {
|
||||
"command": "${workspaceFolder}/scripts/build_generator_debug.bat && build_generator\\aseprite_odin_generator.exe -input-files:value_of_custom_arg -h"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": false,
|
||||
"clear": true
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
31
README.md
31
README.md
|
|
@ -1,18 +1,31 @@
|
|||
|
||||
# YAAP
|
||||
Yet-Another-Atlas-Packer by Stefan Stefanov
|
||||
Yet-Another-Atlas-Packer by bersK (Stefan Stefanov)
|
||||
|
||||
## Usage & requirements
|
||||
> [!IMPORTANT]
|
||||
> Pull this repo with `--recursive`, the `odin-aseprite` library is pulled in as a submodule.
|
||||
|
||||
At least odin compiler version `dev-2025-07:204edd0fc`
|
||||
|
||||
Since we need to download/compile some 3rd party dependencies in the vendors folder we need to call
|
||||
`scripts/setup.[sh|bat]` to compile libtinydialog & download the already compiled `clay` binaries and bindings.
|
||||
|
||||
## Description
|
||||
> [!NOTE]
|
||||
> Quite frankly if you want automatic atlas packing I would suggest using `stb_rect_pack` directly & an aseprite plugin for exporting your assets in a proper format for packing. Using this atlas packer in an automated fashion is not quite possible and slower as a workflow. I would only use it for experiment projects or places where having a built in atlas packer is not practical.
|
||||
|
||||
Simple atlas packer using `stb_rect_pack` from the `stb` family of header libraries & `raylib` for rendering/ui. Here's a quick preview on [youtube](https://youtu.be/4_dKq7G57Lw) of the application.
|
||||
Simple atlas packer for `.aseprite` files. Generates a packed png & metadata in the form of json and/or source files(cpp, odin, etc...).
|
||||
> [!CAUTION]
|
||||
> Does not handle palette files currently.
|
||||
|
||||
<a href="https://youtu.be/4_dKq7G57Lw">
|
||||
<img src="https://raw.githubusercontent.com/bersK/yaap/master/repo_assets/image.png" />
|
||||
<a/>
|
||||
Uses `stb_rect_pack` for the image packing & `raylib` for the UI.
|
||||
|
||||
The goal of the tool is to take in multiple aseprite files and pack them into a single atlas, outputting some metadata in the process in the form of
|
||||
json and/or source files for direct use in odin (maybe more languages too).
|
||||
A quick preview on [youtube](https://youtu.be/4_dKq7G57Lw).
|
||||
|
||||
I'm using a library for marshalling the aseprite files found [here](https://github.com/blob1807/odin-aseprite) on github.
|
||||
<img src="https://raw.githubusercontent.com/bersK/yaap/master/repo_assets/image.png" />
|
||||
|
||||
Project template provided by Karl Zylinski found [here](https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template) on github.
|
||||
## Dependencies
|
||||
* odin-aseprite [github](https://github.com/blob1807/odin-aseprite)
|
||||
* raylib (`vendor/raylib`, [link](https://github.com/odin-lang/Odin/tree/master/vendor/raylib))
|
||||
* stb_rect_pack (`vendor/stb/rect_pack`, [link](https://github.com/odin-lang/Odin/tree/master/vendor/stb/rect_pack))
|
||||
|
|
|
|||
|
|
@ -1,42 +1,29 @@
|
|||
package generator
|
||||
package cli
|
||||
|
||||
import ase "../aseprite"
|
||||
import ase "../../vendors/odin-aseprite"
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import fp "core:path/filepath"
|
||||
import "core:slice"
|
||||
import s "core:strings"
|
||||
import "core:testing"
|
||||
|
||||
import rl "vendor:raylib"
|
||||
import stbrp "vendor:stb/rect_pack"
|
||||
|
||||
import gen ".."
|
||||
import utils "../utils"
|
||||
import gen "../src/generator"
|
||||
|
||||
ATLAS_SIZE :: 512
|
||||
IMPORT_PATH :: "./src/aseprite_odin_generator/big.aseprite"
|
||||
EXPORT_PATH :: "./src/aseprite_odin_generator/atlas.png"
|
||||
IMPORT_PATH :: "./example.aseprite"
|
||||
EXPORT_PATH :: "./atlas.png"
|
||||
|
||||
main :: proc() {
|
||||
args := utils.parse_arguments(os.args[1:])
|
||||
fmt.println(args)
|
||||
|
||||
if ok := utils.CLIFlagType.Help in args; ok {
|
||||
fmt.println("Help called!")
|
||||
utils.print_help()
|
||||
return
|
||||
}
|
||||
|
||||
ase_file, ase_ok := os.read_entire_file(IMPORT_PATH)
|
||||
if !ase_ok {
|
||||
fmt.panicf("Couldn't load file!")
|
||||
}
|
||||
|
||||
cwd := os.get_current_directory()
|
||||
target_dir := s.concatenate({cwd, "\\src\\aseprite_odin_generator\\"})
|
||||
target_dir := os.get_current_directory()
|
||||
|
||||
atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK)
|
||||
atlas_entries: [dynamic]gen.AtlasEntry = make([dynamic]gen.AtlasEntry)
|
||||
|
|
@ -45,13 +32,10 @@ main :: proc() {
|
|||
metadata := gen.pack_atlas_entries(atlas_entries[:], &atlas, 10, 10)
|
||||
|
||||
json_bytes, jerr := json.marshal(metadata)
|
||||
os.write_entire_file("src/aseprite_odin_generator/metadata.json", json_bytes)
|
||||
os.write_entire_file("./metadata.json", json_bytes)
|
||||
sb := gen.metadata_source_code_generate(metadata[:], gen.odin_source_generator_metadata)
|
||||
odin_output_str := s.to_string(sb)
|
||||
os.write_entire_file(
|
||||
"src/aseprite_odin_generator/output.odino",
|
||||
transmute([]byte)odin_output_str,
|
||||
)
|
||||
os.write_entire_file("./output.odin", transmute([]byte)odin_output_str)
|
||||
|
||||
rl.ExportImage(atlas, EXPORT_PATH)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
5
scripts/build.sh
Normal file
5
scripts/build.sh
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
FLAGS="-define:RAYLIB_SHARED=true -out:build/yaap -debug"
|
||||
SRC=src/frontend
|
||||
|
||||
mkdir -p build
|
||||
odin build ${SRC} ${FLAGS}
|
||||
2
scripts/build_atlas.bat
Normal file
2
scripts/build_atlas.bat
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap-debug.exe -debug
|
||||
2
scripts/build_atlas_release.bat
Normal file
2
scripts/build_atlas_release.bat
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap.exe -o:speed
|
||||
2
scripts/build_cli.bat
Normal file
2
scripts/build_cli.bat
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli-debug.exe -debug
|
||||
2
scripts/build_cli_release.bat
Normal file
2
scripts/build_cli_release.bat
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli.exe -o:speed
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@echo off
|
||||
odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_debug.exe -debug
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
odin build src/main_release -out:build/game_debug.bin -no-bounds-check -debug
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@echo off
|
||||
odin build src/aseprite_odin_generator -define:RAYLIB_SHARED=true -out:build_generator/aseprite_odin_generator.exe -debug
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
@echo off
|
||||
|
||||
rem Build game.dll
|
||||
odin build src -show-timings -use-separate-modules -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game.dll -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug
|
||||
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||
|
||||
rem If game.exe already running: Then only compile game.dll and exit cleanly
|
||||
QPROCESS "game.exe">NUL
|
||||
IF %ERRORLEVEL% EQU 0 exit /b 1
|
||||
|
||||
rem build game.exe
|
||||
odin build src/main_hot_reload -use-separate-modules -out:build/game.exe -strict-style -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug
|
||||
IF %ERRORLEVEL% NEQ 0 exit /b 1
|
||||
|
||||
rem copy raylib.dll from odin folder to here
|
||||
if not exist "raylib.dll" (
|
||||
echo "Please copy raylib.dll from <your_odin_compiler>/vendor/raylib/windows/raylib.dll to the same directory as game.exe"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
exit /b 0
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
VET="-strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon"
|
||||
|
||||
# NOTE: this is a recent addition to the Odin compiler, if you don't have this command
|
||||
# you can change this to the path to the Odin folder that contains vendor, eg: "~/Odin".
|
||||
ROOT=$(odin root)
|
||||
if [ ! $? -eq 0 ]; then
|
||||
echo "Your Odin compiler does not have the 'odin root' command, please update or hardcode it in the script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -eu
|
||||
|
||||
# Figure out the mess that is dynamic libraries.
|
||||
case $(uname) in
|
||||
"Darwin")
|
||||
case $(uname -m) in
|
||||
"arm64") LIB_PATH="macos-arm64" ;;
|
||||
*) LIB_PATH="macos" ;;
|
||||
esac
|
||||
|
||||
DLL_EXT=".dylib"
|
||||
EXTRA_LINKER_FLAGS="-Wl,-rpath $ROOT/vendor/raylib/$LIB_PATH"
|
||||
;;
|
||||
*)
|
||||
DLL_EXT=".so"
|
||||
EXTRA_LINKER_FLAGS="'-Wl,-rpath=\$ORIGIN/linux'"
|
||||
|
||||
# Copy the linux libraries into the project automatically.
|
||||
if [ ! -d "linux" ]; then
|
||||
mkdir linux
|
||||
cp -r $ROOT/vendor/raylib/linux/libraylib*.so* linux
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Build the game.
|
||||
odin build src -use-separate-modules -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -show-timings -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game_tmp$DLL_EXT -debug $VET
|
||||
|
||||
# Need to use a temp file on Linux because it first writes an empty `game.so`, which the game will load before it is actually fully written.
|
||||
mv ./build/game_tmp$DLL_EXT ./build/game$DLL_EXT
|
||||
|
||||
# Do not build the game.bin if it is already running.
|
||||
if ! pgrep game.bin > /dev/null; then
|
||||
odin build src/main_hot_reload -use-separate-modules -out:build/game.bin $VET -debug
|
||||
fi
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@echo off
|
||||
odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_release.exe -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -subsystem:windows
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
odin build src/main_release -out:build/game_release.bin -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon
|
||||
20
scripts/setup.bat
Normal file
20
scripts/setup.bat
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
@echo off
|
||||
|
||||
mkdir build
|
||||
|
||||
FOR /F "tokens=*" %%g IN ('odin root') do (SET ODIN_ROOT=%%g)
|
||||
|
||||
echo %ODIN_ROOT%
|
||||
|
||||
@REM If it fails to find your odin root folder, copy the raylib.dll manually into the build folder, it's a runtime requirement
|
||||
copy %ODIN_ROOT%\vendor\raylib\windows\raylib.dll build\raylib.dll
|
||||
|
||||
pushd vendors\dialog
|
||||
call .\build.bat
|
||||
popd
|
||||
|
||||
pushd vendors
|
||||
curl.exe --output clay.zip --url https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip
|
||||
@REM Apparently available on Win10 since build 17063 - https://superuser.com/a/1473255
|
||||
tar -xf .\clay.zip
|
||||
popd
|
||||
13
scripts/setup.sh
Normal file
13
scripts/setup.sh
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
set -e
|
||||
|
||||
pushd vendors/dialog
|
||||
sh build.sh
|
||||
popd
|
||||
|
||||
pushd vendors
|
||||
wget https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip
|
||||
unzip clay-odin.zip
|
||||
rm -rf ./__MACOSX # Yeah...
|
||||
popd
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 628e655661d822fecae67cf238cbfa414912d943
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
|
|
@ -1 +0,0 @@
|
|||
[{"name":"Edinica","location":[95,10],"size":[58,57]},{"name":"Dvoika_0","location":[234,10],"size":[55,31]},{"name":"Dvoika_1","location":[163,10],"size":[61,33]},{"name":"Troika","location":[10,10],"size":[75,75]}]
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package tinyfiledialogs
|
||||
|
||||
import "core:c"
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
foreign import lib {"tinyfiledialogs.lib", "system:comdlg32.lib", "system:Ole32.lib"}
|
||||
} else when ODIN_OS == .Linux || ODIN_OS == .Darwin {
|
||||
foreign import lib "libtinyfiledialogs.a"
|
||||
}
|
||||
|
||||
foreign lib {
|
||||
@(link_name = "tinyfd_notifyPopup")
|
||||
notify_popup :: proc(title, message, icon_type: cstring) -> c.int ---
|
||||
|
||||
@(link_name = "tinyfd_messageBox")
|
||||
message_box :: proc(title, message, dialog_type, icon_type: cstring, default_button: c.int) -> c.int ---
|
||||
|
||||
@(link_name = "tinyfd_inputBox")
|
||||
input_box :: proc(title, message, default_input: cstring) -> [^]c.char ---
|
||||
|
||||
@(link_name = "tinyfd_saveFileDialog")
|
||||
save_file_dialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring) -> [^]c.char ---
|
||||
|
||||
@(link_name = "tinyfd_openFileDialog")
|
||||
open_file_dialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring, allow_multi: c.int) -> [^]c.char ---
|
||||
|
||||
@(link_name = "tinyfd_selectFolderDialog")
|
||||
select_folder_dialog :: proc(title, default_path: cstring) -> [^]c.char ---
|
||||
|
||||
@(link_name = "tinyfd_colorChooser")
|
||||
color_chooser :: proc(title, default_hex_rgb: cstring, default_rgb, result_rgb: [3]byte) -> [^]c.char ---
|
||||
}
|
||||
55
src/frontend/globals.odin
Normal file
55
src/frontend/globals.odin
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package frontend
|
||||
|
||||
import generator "../generator"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
PIXEL_WINDOW_HEIGHT :: 180
|
||||
|
||||
ui_scaling: f32 = 2
|
||||
|
||||
@(rodata)
|
||||
ATLAS_RENDER_SIZES := []i32{256, 512, 1024, 2048, 4096}
|
||||
|
||||
FileDialogType :: enum {
|
||||
SourceFiles,
|
||||
OutputFolder,
|
||||
}
|
||||
|
||||
WindowInformation :: struct {
|
||||
w: f32,
|
||||
h: f32,
|
||||
width_scaled: f32,
|
||||
height_scaled: f32,
|
||||
}
|
||||
window_info: WindowInformation
|
||||
|
||||
MonitorInformation :: struct {
|
||||
max_width: f32,
|
||||
max_height: f32,
|
||||
}
|
||||
monitor_info: MonitorInformation
|
||||
|
||||
PackerSettings :: struct {
|
||||
atlas_size_x: i32,
|
||||
atlas_size_y: i32,
|
||||
pixel_padding_x_int: i32,
|
||||
pixel_padding_y_int: i32,
|
||||
padding_enabled: bool,
|
||||
output_json: bool,
|
||||
output_odin: bool,
|
||||
}
|
||||
packer_settings: PackerSettings
|
||||
|
||||
// Where the output files will be written (atlas.png json output, etc)
|
||||
output_folder_path: string
|
||||
// If a folder was chosen as input - the path
|
||||
source_files_to_pack: []string
|
||||
|
||||
Atlas :: struct {
|
||||
render_texture_target: rl.RenderTexture2D,
|
||||
checked_background: rl.RenderTexture2D,
|
||||
render_has_preview: bool,
|
||||
render_size: i32,
|
||||
metadata: [dynamic]generator.SpriteAtlasMetadata,
|
||||
}
|
||||
atlas: Atlas
|
||||
520
src/frontend/main.odin
Normal file
520
src/frontend/main.odin
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
package frontend
|
||||
|
||||
import "base:runtime"
|
||||
import "core:c/libc"
|
||||
import "core:log"
|
||||
import "core:mem"
|
||||
import "core:strings"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
import diag "../../vendors/dialog"
|
||||
import generator "../generator"
|
||||
|
||||
should_pack_atlas_and_render: bool
|
||||
|
||||
main :: proc() {
|
||||
default_allocator := context.allocator
|
||||
tracking_allocator: mem.Tracking_Allocator
|
||||
mem.tracking_allocator_init(&tracking_allocator, default_allocator)
|
||||
context.allocator = mem.tracking_allocator(&tracking_allocator)
|
||||
|
||||
context.logger = log.create_console_logger()
|
||||
{
|
||||
init()
|
||||
|
||||
defer cleanup()
|
||||
|
||||
for !rl.WindowShouldClose() {
|
||||
update()
|
||||
draw()
|
||||
|
||||
for b in tracking_allocator.bad_free_array {
|
||||
log.errorf("Bad free at: %v", b.location)
|
||||
}
|
||||
|
||||
clear(&tracking_allocator.bad_free_array)
|
||||
|
||||
free_all(context.temp_allocator)
|
||||
}
|
||||
}
|
||||
log.destroy_console_logger(context.logger)
|
||||
|
||||
for key, value in tracking_allocator.allocation_map {
|
||||
log.errorf("%v: Leaked %v bytes\n", value.location, value.size)
|
||||
}
|
||||
mem.tracking_allocator_destroy(&tracking_allocator)
|
||||
}
|
||||
|
||||
init :: proc() {
|
||||
atlas.render_size = ATLAS_RENDER_SIZES[0]
|
||||
|
||||
rl.SetConfigFlags({.WINDOW_RESIZABLE})
|
||||
rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer")
|
||||
rl.SetWindowMinSize(1400, 800)
|
||||
rl.SetTraceLogCallback(rl_log)
|
||||
}
|
||||
|
||||
cleanup :: proc() {
|
||||
delete(atlas.metadata)
|
||||
delete(output_folder_path)
|
||||
delete(source_files_to_pack)
|
||||
log.info("Bye")
|
||||
rl.CloseWindow()
|
||||
}
|
||||
|
||||
update :: proc() {
|
||||
// Update the width/height
|
||||
win_info := &window_info
|
||||
win_info.w = f32(rl.GetScreenWidth())
|
||||
win_info.h = f32(rl.GetScreenHeight())
|
||||
win_info.height_scaled = win_info.h / ui_scaling
|
||||
win_info.width_scaled = win_info.w / ui_scaling
|
||||
|
||||
// Update the virtual mouse position (needed for GUI interaction to work properly for instance)
|
||||
mouse_scale := 1 / ui_scaling
|
||||
rl.SetMouseScale(mouse_scale, mouse_scale)
|
||||
}
|
||||
|
||||
draw :: proc() {
|
||||
rl.BeginDrawing()
|
||||
defer rl.EndDrawing()
|
||||
|
||||
rl.ClearBackground(rl.BLACK)
|
||||
|
||||
draw_screen_ui()
|
||||
|
||||
if should_pack_atlas_and_render {
|
||||
pack_atlas_and_render()
|
||||
}
|
||||
|
||||
free_all(context.temp_allocator)
|
||||
}
|
||||
|
||||
ui_camera :: proc() -> rl.Camera2D {
|
||||
return {zoom = ui_scaling}
|
||||
}
|
||||
|
||||
draw_screen_ui :: proc() {
|
||||
rl.BeginMode2D(ui_camera())
|
||||
|
||||
draw_settings_and_preview()
|
||||
|
||||
rl.EndMode2D()
|
||||
}
|
||||
|
||||
save_atlas_to_file :: proc() {
|
||||
generator.save_output(output_folder_path, atlas.metadata, atlas.render_texture_target)
|
||||
}
|
||||
|
||||
save_to_atlas_to_file :: proc() {
|
||||
open_file_dialog(.OutputFolder)
|
||||
save_atlas_to_file()
|
||||
}
|
||||
|
||||
pack_atlas_and_render :: proc() {
|
||||
rl.BeginTextureMode(atlas.render_texture_target)
|
||||
defer rl.EndTextureMode()
|
||||
|
||||
atlas_entries: [dynamic]generator.AtlasEntry
|
||||
defer {
|
||||
for entry in atlas_entries {
|
||||
delete(entry.cells)
|
||||
delete(entry.layer_cell_count)
|
||||
delete(entry.layer_names)
|
||||
}
|
||||
delete(atlas_entries)
|
||||
}
|
||||
|
||||
if len(source_files_to_pack) > 0 {
|
||||
generator.unmarshall_aseprite_files(source_files_to_pack, &atlas_entries)
|
||||
} else {
|
||||
log.error("No source folder or files set! Can't pack the void!!!")
|
||||
should_pack_atlas_and_render = false
|
||||
return
|
||||
}
|
||||
|
||||
atlas_img: rl.Image = rl.GenImageColor(atlas.render_size, atlas.render_size, rl.BLANK)
|
||||
|
||||
padding_x := packer_settings.pixel_padding_x_int if packer_settings.padding_enabled else 0
|
||||
padding_y := packer_settings.pixel_padding_y_int if packer_settings.padding_enabled else 0
|
||||
|
||||
delete(atlas.metadata)
|
||||
atlas.metadata = generator.pack_atlas_entries(
|
||||
atlas_entries[:],
|
||||
&atlas_img,
|
||||
padding_x,
|
||||
padding_y,
|
||||
)
|
||||
|
||||
// OpenGL's Y buffer is flipped
|
||||
rl.ImageFlipVertical(&atlas_img)
|
||||
// rl.UnloadTexture(atlas_render_target.texture)
|
||||
log.info("Packed everything!")
|
||||
atlas.render_texture_target.texture = rl.LoadTextureFromImage(atlas_img)
|
||||
|
||||
should_pack_atlas_and_render = false
|
||||
atlas.render_has_preview = true
|
||||
}
|
||||
|
||||
draw_settings_and_preview :: proc() {
|
||||
left_half_rect := rl.Rectangle {
|
||||
x = 0,
|
||||
y = 0,
|
||||
width = cast(f32)window_info.width_scaled / 3,
|
||||
height = cast(f32)window_info.height_scaled,
|
||||
}
|
||||
right_half_rect := rl.Rectangle {
|
||||
x = cast(f32)window_info.width_scaled / 3,
|
||||
y = 0,
|
||||
width = cast(f32)(window_info.width_scaled / 3) * 2,
|
||||
height = cast(f32)window_info.height_scaled,
|
||||
}
|
||||
rl.DrawRectangleRec(left_half_rect, rl.WHITE)
|
||||
rl.DrawRectangleRec(right_half_rect, rl.MAROON)
|
||||
|
||||
@(static) spinner_edit_mode: bool
|
||||
|
||||
small_offset := 10 * ui_scaling
|
||||
big_offset := 30 * ui_scaling
|
||||
elements_height: f32 = 0
|
||||
|
||||
rl.GuiPanel(left_half_rect, "Atlas Settings")
|
||||
elements_height += small_offset / 2
|
||||
|
||||
@(static) settings_dropdown_box_active_idx: i32
|
||||
|
||||
elements_height += small_offset + 5 * ui_scaling
|
||||
|
||||
rl.GuiLabel(
|
||||
{x = small_offset, y = elements_height, width = left_half_rect.width},
|
||||
"Atlas Size",
|
||||
)
|
||||
elements_height += small_offset / 2
|
||||
|
||||
@(static) dropdown_resolution_edit_mode: bool
|
||||
@(static) dropdown_resolution_mode: i32
|
||||
|
||||
dropdown_rect := rl.Rectangle {
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width - small_offset * 2,
|
||||
height = small_offset,
|
||||
}
|
||||
// Because we want to render this ontop of everything else, we can just 'defer' it at the end of the draw function
|
||||
defer {
|
||||
if dropdown_resolution_edit_mode {rl.GuiLock()}
|
||||
|
||||
if rl.GuiDropdownBox(
|
||||
dropdown_rect,
|
||||
"256x;512x;1024x;2048x;4096x",
|
||||
&dropdown_resolution_mode,
|
||||
dropdown_resolution_edit_mode,
|
||||
) {
|
||||
dropdown_resolution_edit_mode = !dropdown_resolution_edit_mode
|
||||
atlas.render_size =
|
||||
ATLAS_RENDER_SIZES[max(i32(len(ATLAS_RENDER_SIZES) - 1), dropdown_resolution_mode)]
|
||||
}
|
||||
rl.GuiUnlock()
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
|
||||
// General Options
|
||||
if settings_dropdown_box_active_idx == 0 {
|
||||
padding_settings_y := elements_height
|
||||
{
|
||||
defer {
|
||||
padding_settings_rect := rl.Rectangle {
|
||||
x = small_offset / 2,
|
||||
y = padding_settings_y,
|
||||
width = left_half_rect.width - small_offset,
|
||||
height = elements_height - padding_settings_y,
|
||||
}
|
||||
rl.GuiGroupBox(padding_settings_rect, "Padding Settings")
|
||||
}
|
||||
elements_height += small_offset
|
||||
|
||||
enable_padding_rect := rl.Rectangle {
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = small_offset,
|
||||
height = small_offset,
|
||||
}
|
||||
rl.GuiCheckBox(
|
||||
enable_padding_rect,
|
||||
" Enable padding",
|
||||
&packer_settings.padding_enabled,
|
||||
)
|
||||
elements_height += small_offset * 2
|
||||
|
||||
// Padding X spinner and label
|
||||
{
|
||||
padding_x_spinner_rect := rl.Rectangle {
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = big_offset * 2,
|
||||
height = small_offset,
|
||||
}
|
||||
padding_x_spinner := rl.GuiSpinner(
|
||||
padding_x_spinner_rect,
|
||||
"",
|
||||
&packer_settings.pixel_padding_x_int,
|
||||
0,
|
||||
10,
|
||||
spinner_edit_mode,
|
||||
)
|
||||
if (padding_x_spinner) > 0 {
|
||||
spinner_edit_mode = !spinner_edit_mode
|
||||
}
|
||||
rl.GuiLabel(
|
||||
{
|
||||
x = (small_offset * 2) + big_offset * 2,
|
||||
y = elements_height,
|
||||
width = big_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Padding X",
|
||||
)
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
|
||||
// Padding Y spinner and label
|
||||
{
|
||||
padding_y_spinner_rect := rl.Rectangle {
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = big_offset * 2,
|
||||
height = small_offset,
|
||||
}
|
||||
padding_y_spinner := rl.GuiSpinner(
|
||||
padding_y_spinner_rect,
|
||||
"",
|
||||
&packer_settings.pixel_padding_y_int,
|
||||
0,
|
||||
10,
|
||||
spinner_edit_mode,
|
||||
)
|
||||
if (padding_y_spinner) > 0 {
|
||||
spinner_edit_mode = !spinner_edit_mode
|
||||
}
|
||||
rl.GuiLabel(
|
||||
{
|
||||
x = (small_offset * 2) + big_offset * 2,
|
||||
y = elements_height,
|
||||
width = big_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Padding Y",
|
||||
)
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
|
||||
}
|
||||
elements_height += small_offset
|
||||
|
||||
{
|
||||
actions_label_y := elements_height
|
||||
|
||||
defer {
|
||||
actions_rect := rl.Rectangle {
|
||||
x = small_offset / 2,
|
||||
y = actions_label_y,
|
||||
width = left_half_rect.width - small_offset,
|
||||
height = elements_height - actions_label_y,
|
||||
}
|
||||
rl.GuiGroupBox(actions_rect, "Actions")
|
||||
}
|
||||
elements_height += small_offset
|
||||
|
||||
pick_sources_rect := rl.Rectangle {
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
}
|
||||
if rl.GuiButton(pick_sources_rect, "Pick Source(s)") {
|
||||
open_file_dialog(.SourceFiles)
|
||||
}
|
||||
|
||||
pick_output_rect := rl.Rectangle {
|
||||
x = left_half_rect.width / 2,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
}
|
||||
if rl.GuiButton(pick_output_rect, "Pick Output") {
|
||||
open_file_dialog(.OutputFolder)
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
|
||||
pack_atlas_rect := rl.Rectangle {
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
}
|
||||
if rl.GuiButton(pack_atlas_rect, "Pack Atlas") {
|
||||
should_pack_atlas_and_render = true
|
||||
}
|
||||
|
||||
clear_atlas_rect := rl.Rectangle {
|
||||
x = left_half_rect.width / 2,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
}
|
||||
if rl.GuiButton(clear_atlas_rect, "Clear Atlas") {
|
||||
clear_atlas_data()
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
|
||||
save_rect := rl.Rectangle {
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
}
|
||||
if rl.GuiButton(save_rect, "Save") {
|
||||
save_atlas_to_file()
|
||||
}
|
||||
|
||||
save_to_rect := rl.Rectangle {
|
||||
x = left_half_rect.width / 2,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
}
|
||||
if rl.GuiButton(save_to_rect, "Save To...") {
|
||||
save_to_atlas_to_file()
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Packing Options
|
||||
if settings_dropdown_box_active_idx == 1 {
|
||||
|
||||
@(static) active_tab: i32
|
||||
tabs: []cstring = {"One", "Two", "Three"}
|
||||
rl.GuiTabBar(
|
||||
{x = small_offset, y = elements_height, width = 100, height = small_offset},
|
||||
&tabs[0],
|
||||
auto_cast len(tabs),
|
||||
&active_tab,
|
||||
)
|
||||
}
|
||||
|
||||
// Save Options
|
||||
// if settings_dropdown_box_active_idx == 2 {
|
||||
// }
|
||||
|
||||
elements_height = 0
|
||||
rl.GuiPanel(right_half_rect, "Atlas Preview")
|
||||
short_edge := min(
|
||||
right_half_rect.height - big_offset * 1.5,
|
||||
right_half_rect.width - big_offset * 1.5,
|
||||
)
|
||||
preview_rect := rl.Rectangle {
|
||||
x = (right_half_rect.width / 2 + right_half_rect.x) - (short_edge / 2),
|
||||
y = (right_half_rect.height / 2 + right_half_rect.y) - (short_edge / 2),
|
||||
width = short_edge,
|
||||
height = short_edge,
|
||||
}
|
||||
if !atlas.render_has_preview {
|
||||
rl.GuiDummyRec(preview_rect, "PREVIEW")
|
||||
} else {
|
||||
// rl.DrawRectangleRec(preview_rect, rl.WHITE)
|
||||
bg_texture := atlas.checked_background.texture
|
||||
rl.DrawTexturePro(
|
||||
bg_texture,
|
||||
{width = auto_cast bg_texture.width, height = auto_cast bg_texture.height},
|
||||
preview_rect,
|
||||
{},
|
||||
0,
|
||||
rl.WHITE,
|
||||
)
|
||||
atlas_texture := atlas.render_texture_target.texture
|
||||
rl.DrawTexturePro(
|
||||
atlas_texture,
|
||||
{width = auto_cast atlas_texture.width, height = auto_cast -atlas_texture.height},
|
||||
preview_rect,
|
||||
{0, 0},
|
||||
0,
|
||||
rl.WHITE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open_file_dialog :: proc(dialog_type: FileDialogType) {
|
||||
switch dialog_type {
|
||||
case .SourceFiles:
|
||||
// `open_file_dialog` returns a single cstring with one or more paths divided by a separator ('|'),
|
||||
// https://github.com/native-toolkit/libtinyfiledialogs/blob/master/tinyfiledialogs.c#L2706
|
||||
source_files, ok := diag.open_file_dialog("Select source files", {}, 0, {}, "", 1)
|
||||
if len(source_files) > 0 {
|
||||
// todo(stefan): We're assuming the filepaths returned libtinydialog are valid...
|
||||
source_files_to_pack = strings.split(source_files, "|")
|
||||
|
||||
log.info(source_files_to_pack)
|
||||
} else {
|
||||
log.error("No files were selected!")
|
||||
}
|
||||
|
||||
case .OutputFolder:
|
||||
file, ok := diag.select_folder_dialog("Select source folder", {})
|
||||
if len(file) > 0 && ok {
|
||||
output_folder_path = file
|
||||
log.info(file)
|
||||
} else {
|
||||
log.error("Got an empty path from the file dialog!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear_atlas_data :: proc() {
|
||||
delete(atlas.metadata)
|
||||
atlas.render_has_preview = false
|
||||
}
|
||||
|
||||
logger: log.Logger
|
||||
rl_log_buf: []byte
|
||||
rl_log :: proc "c" (logLevel: rl.TraceLogLevel, text: cstring, args: ^libc.va_list) {
|
||||
context = runtime.default_context()
|
||||
context.logger = logger
|
||||
|
||||
level: log.Level
|
||||
switch logLevel {
|
||||
case .TRACE, .DEBUG:
|
||||
level = .Debug
|
||||
case .ALL, .NONE, .INFO:
|
||||
level = .Info
|
||||
case .WARNING:
|
||||
level = .Warning
|
||||
case .ERROR:
|
||||
level = .Error
|
||||
case .FATAL:
|
||||
level = .Fatal
|
||||
}
|
||||
|
||||
if level < logger.lowest_level {
|
||||
return
|
||||
}
|
||||
|
||||
if rl_log_buf == nil {
|
||||
rl_log_buf = make([]byte, 1024)
|
||||
}
|
||||
|
||||
defer mem.zero_slice(rl_log_buf)
|
||||
|
||||
n: int
|
||||
for {
|
||||
va := args
|
||||
n = int(libc.vsnprintf(raw_data(rl_log_buf), len(rl_log_buf), text, va))
|
||||
if n < len(rl_log_buf) do break
|
||||
log.infof("Resizing raylib log buffer from %m to %m", len(rl_log_buf), len(rl_log_buf) * 2)
|
||||
rl_log_buf, _ = mem.resize_bytes(rl_log_buf, len(rl_log_buf) * 2)
|
||||
}
|
||||
|
||||
formatted := string(rl_log_buf[:n])
|
||||
log.log(level, formatted)
|
||||
}
|
||||
508
src/game.odin
508
src/game.odin
|
|
@ -1,508 +0,0 @@
|
|||
// This file is compiled as part of the `odin.dll` file. It contains the
|
||||
// procs that `game.exe` will call, such as:
|
||||
//
|
||||
// game_init: Sets up the game state
|
||||
// game_update: Run once per frame
|
||||
// game_shutdown: Shuts down game and frees memory
|
||||
// game_memory: Run just before a hot reload, so game.exe has a pointer to the
|
||||
// game's memory.
|
||||
// game_hot_reloaded: Run after a hot reload so that the `g_mem` global variable
|
||||
// can be set to whatever pointer it was in the old DLL.
|
||||
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import "core:math"
|
||||
import "core:strings"
|
||||
import "utils"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
import diag "dialog"
|
||||
|
||||
g_mem: ^GameMemory
|
||||
|
||||
game_camera :: proc() -> rl.Camera2D {
|
||||
w = f32(rl.GetScreenWidth())
|
||||
h = f32(rl.GetScreenHeight())
|
||||
|
||||
return {zoom = h / PixelWindowHeight, target = {}, offset = {w / 2, h / 2}}
|
||||
}
|
||||
|
||||
ui_camera :: proc() -> rl.Camera2D {
|
||||
return {zoom = scaling}
|
||||
}
|
||||
|
||||
update :: proc() {
|
||||
// Update the width/height
|
||||
win_info := &g_mem.window_info
|
||||
win_info.w = f32(rl.GetScreenWidth())
|
||||
win_info.h = f32(rl.GetScreenHeight())
|
||||
win_info.height_scaled = win_info.h / scaling
|
||||
win_info.width_scaled = win_info.w / scaling
|
||||
w = win_info.w
|
||||
h = win_info.h
|
||||
|
||||
// Update the virtual mouse position (needed for GUI interaction to work properly for instance)
|
||||
rl.SetMouseScale(1 / scaling, 1 / scaling)
|
||||
|
||||
if g_mem.should_open_file_dialog {
|
||||
open_file_dialog()
|
||||
}
|
||||
}
|
||||
|
||||
draw :: proc() {
|
||||
rl.BeginDrawing()
|
||||
defer rl.EndDrawing()
|
||||
|
||||
rl.ClearBackground(rl.BLACK)
|
||||
|
||||
draw_screen_ui()
|
||||
|
||||
if g_mem.should_render_atlas {
|
||||
draw_screen_target()
|
||||
}
|
||||
|
||||
free_all(context.temp_allocator)
|
||||
}
|
||||
|
||||
draw_screen_ui :: proc() {
|
||||
rl.BeginMode2D(ui_camera())
|
||||
defer rl.EndMode2D()
|
||||
|
||||
draw_atlas_settings_and_preview()
|
||||
}
|
||||
|
||||
draw_screen_target :: proc() {
|
||||
atlas_render_target := &g_mem.atlas_render_texture_target
|
||||
|
||||
rl.BeginTextureMode(atlas_render_target^)
|
||||
defer rl.EndTextureMode()
|
||||
|
||||
atlas_entries: [dynamic]AtlasEntry
|
||||
delete(atlas_entries)
|
||||
|
||||
if files, ok := g_mem.source_files_to_pack.([]string); ok {
|
||||
unmarshall_aseprite_files(files, &atlas_entries)
|
||||
} else {
|
||||
fmt.println("No source folder or files set! Can't pack the void!!!")
|
||||
g_mem.should_render_atlas = false
|
||||
return
|
||||
}
|
||||
|
||||
atlas: rl.Image = rl.GenImageColor(g_mem.atlas_render_size, g_mem.atlas_render_size, rl.BLANK)
|
||||
// defer rl.UnloadImage(atlas)
|
||||
|
||||
padding_x :=
|
||||
g_mem.packer_settings.pixel_padding_x_int if g_mem.packer_settings.padding_enabled else 0
|
||||
padding_y :=
|
||||
g_mem.packer_settings.pixel_padding_y_int if g_mem.packer_settings.padding_enabled else 0
|
||||
|
||||
g_mem.atlas_metadata = pack_atlas_entries(atlas_entries[:], &atlas, padding_x, padding_y)
|
||||
|
||||
// OpenGL's Y buffer is flipped
|
||||
rl.ImageFlipVertical(&atlas)
|
||||
// rl.UnloadTexture(atlas_render_target.texture)
|
||||
fmt.println("Packed everything!")
|
||||
atlas_render_target.texture = rl.LoadTextureFromImage(atlas)
|
||||
|
||||
g_mem.should_render_atlas = false
|
||||
g_mem.atlas_render_has_preview = true
|
||||
}
|
||||
|
||||
draw_atlas_settings_and_preview :: proc() {
|
||||
left_half_rect := rl.Rectangle {
|
||||
x = 0,
|
||||
y = 0,
|
||||
width = auto_cast g_mem.window_info.width_scaled / 3,
|
||||
height = auto_cast g_mem.window_info.height_scaled,
|
||||
}
|
||||
right_half_rect := rl.Rectangle {
|
||||
x = auto_cast g_mem.window_info.width_scaled / 3,
|
||||
y = 0,
|
||||
width = auto_cast (g_mem.window_info.width_scaled / 3) * 2,
|
||||
height = auto_cast g_mem.window_info.height_scaled,
|
||||
}
|
||||
rl.DrawRectangleRec(left_half_rect, rl.WHITE)
|
||||
rl.DrawRectangleRec(right_half_rect, rl.MAROON)
|
||||
|
||||
@(static)
|
||||
spinner_edit_mode: bool
|
||||
|
||||
small_offset := 10 * scaling
|
||||
big_offset := 30 * scaling
|
||||
elements_height: f32 = 0
|
||||
|
||||
rl.GuiPanel(left_half_rect, "Atlas Settings")
|
||||
elements_height += small_offset / 2
|
||||
|
||||
@(static)
|
||||
SettingsDropBoxEditMode: bool
|
||||
@(static)
|
||||
SettingsDropdownBoxActive: i32
|
||||
|
||||
elements_height += small_offset + 5 * scaling
|
||||
|
||||
rl.GuiLabel(
|
||||
{x = small_offset, y = elements_height, width = left_half_rect.width},
|
||||
"Atlas Size",
|
||||
)
|
||||
elements_height += small_offset / 2
|
||||
|
||||
@(static)
|
||||
DropdownBox000EditMode: bool
|
||||
@(static)
|
||||
DropdownBox000Active: i32
|
||||
|
||||
dropdown_rect := rl.Rectangle {
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width - small_offset * 2,
|
||||
height = small_offset,
|
||||
}
|
||||
|
||||
// Because we want to render this ontop of everything else, we can just 'defer' it at the end of the draw function
|
||||
defer {
|
||||
if DropdownBox000EditMode {rl.GuiLock()}
|
||||
|
||||
if rl.GuiDropdownBox(
|
||||
dropdown_rect,
|
||||
"256x;512x;1024x;2048x;4096x",
|
||||
&DropdownBox000Active,
|
||||
DropdownBox000EditMode,
|
||||
) {
|
||||
DropdownBox000EditMode = !DropdownBox000EditMode
|
||||
fmt.println(DropdownBox000Active)
|
||||
g_mem.atlas_render_size = 256 * auto_cast math.pow(2, f32(DropdownBox000Active))
|
||||
}
|
||||
rl.GuiUnlock()
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
|
||||
|
||||
// General Options
|
||||
if SettingsDropdownBoxActive == 0 {
|
||||
padding_settings_y := elements_height
|
||||
{
|
||||
defer rl.GuiGroupBox(
|
||||
{
|
||||
x = small_offset / 2,
|
||||
y = padding_settings_y,
|
||||
width = left_half_rect.width - small_offset,
|
||||
height = elements_height - padding_settings_y,
|
||||
},
|
||||
"Padding Settings",
|
||||
)
|
||||
elements_height += small_offset
|
||||
|
||||
rl.GuiCheckBox(
|
||||
{
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = small_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
" Enable padding",
|
||||
&g_mem.packer_settings.padding_enabled,
|
||||
)
|
||||
elements_height += small_offset * 2
|
||||
|
||||
if (rl.GuiSpinner(
|
||||
{
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = big_offset * 2,
|
||||
height = small_offset,
|
||||
},
|
||||
"",
|
||||
&g_mem.packer_settings.pixel_padding_x_int,
|
||||
0,
|
||||
10,
|
||||
spinner_edit_mode,
|
||||
)) >
|
||||
0 {spinner_edit_mode = !spinner_edit_mode}
|
||||
rl.GuiLabel(
|
||||
{
|
||||
x = (small_offset * 2) + big_offset * 2,
|
||||
y = elements_height,
|
||||
width = big_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Padding X",
|
||||
)
|
||||
elements_height += small_offset * 2
|
||||
|
||||
if (rl.GuiSpinner(
|
||||
{
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = big_offset * 2,
|
||||
height = small_offset,
|
||||
},
|
||||
"",
|
||||
&g_mem.packer_settings.pixel_padding_y_int,
|
||||
0,
|
||||
10,
|
||||
spinner_edit_mode,
|
||||
)) >
|
||||
0 {spinner_edit_mode = !spinner_edit_mode}
|
||||
rl.GuiLabel(
|
||||
{
|
||||
x = (small_offset * 2) + big_offset * 2,
|
||||
y = elements_height,
|
||||
width = big_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Padding Y",
|
||||
)
|
||||
elements_height += small_offset * 2
|
||||
|
||||
}
|
||||
elements_height += small_offset
|
||||
|
||||
// rl.GuiLine({y = elements_height, width = left_half_rect.width}, "Actions")
|
||||
// elements_height += small_offset
|
||||
|
||||
{
|
||||
actions_label_y := elements_height
|
||||
defer rl.GuiGroupBox(
|
||||
{
|
||||
x = small_offset / 2,
|
||||
y = actions_label_y,
|
||||
width = left_half_rect.width - small_offset,
|
||||
height = elements_height - actions_label_y,
|
||||
},
|
||||
"Actions",
|
||||
)
|
||||
elements_height += small_offset
|
||||
|
||||
if rl.GuiButton(
|
||||
{
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Pick Source(s)",
|
||||
) {
|
||||
g_mem.should_open_file_dialog = true
|
||||
g_mem.source_location_type = .SourceFiles
|
||||
}
|
||||
|
||||
if rl.GuiButton(
|
||||
{
|
||||
x = left_half_rect.width / 2,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Pick Output",
|
||||
) {
|
||||
g_mem.should_open_file_dialog = true
|
||||
g_mem.source_location_type = .OutputFolder
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
|
||||
|
||||
if rl.GuiButton(
|
||||
{
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Pack Atlas",
|
||||
) {
|
||||
g_mem.should_render_atlas = true
|
||||
}
|
||||
|
||||
if rl.GuiButton(
|
||||
{
|
||||
x = left_half_rect.width / 2,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Clear Atlas",
|
||||
) {
|
||||
clear_atlas_data()
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
|
||||
if rl.GuiButton(
|
||||
{
|
||||
x = small_offset,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Save",
|
||||
) {
|
||||
save_output()
|
||||
}
|
||||
|
||||
if rl.GuiButton(
|
||||
{
|
||||
x = left_half_rect.width / 2,
|
||||
y = elements_height,
|
||||
width = left_half_rect.width / 2 - small_offset,
|
||||
height = small_offset,
|
||||
},
|
||||
"Save To...",
|
||||
) {
|
||||
if output_folder, ok := g_mem.output_folder_path.(string); ok {
|
||||
save_metadata_simple(output_folder)
|
||||
}
|
||||
}
|
||||
elements_height += small_offset * 2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Packing Options
|
||||
if SettingsDropdownBoxActive == 1 {
|
||||
|
||||
@(static)
|
||||
active_tab: i32
|
||||
tabs: []cstring = {"One", "Two", "Three"}
|
||||
rl.GuiTabBar(
|
||||
{x = small_offset, y = elements_height, width = 100, height = small_offset},
|
||||
&tabs[0],
|
||||
auto_cast len(tabs),
|
||||
&active_tab,
|
||||
)
|
||||
}
|
||||
|
||||
// Save Options
|
||||
if SettingsDropdownBoxActive == 2 {
|
||||
|
||||
}
|
||||
|
||||
elements_height = 0
|
||||
rl.GuiPanel(right_half_rect, "Atlas Preview")
|
||||
short_edge := min(
|
||||
right_half_rect.height - big_offset * 1.5,
|
||||
right_half_rect.width - big_offset * 1.5,
|
||||
)
|
||||
preview_rect := rl.Rectangle {
|
||||
x = (right_half_rect.width / 2 + right_half_rect.x) - (short_edge / 2),
|
||||
y = (right_half_rect.height / 2 + right_half_rect.y) - (short_edge / 2),
|
||||
width = short_edge,
|
||||
height = short_edge,
|
||||
}
|
||||
if !g_mem.atlas_render_has_preview {
|
||||
rl.GuiDummyRec(preview_rect, "PREVIEW")
|
||||
} else {
|
||||
// rl.DrawRectangleRec(preview_rect, rl.WHITE)
|
||||
bg_texture := g_mem.atlas_checked_background.texture
|
||||
rl.DrawTexturePro(
|
||||
bg_texture,
|
||||
{width = auto_cast bg_texture.width, height = auto_cast bg_texture.height},
|
||||
preview_rect,
|
||||
{},
|
||||
0,
|
||||
rl.WHITE,
|
||||
)
|
||||
// preview_rect.x +=
|
||||
// 10;preview_rect.y += 10;preview_rect.height -= 20;preview_rect.width -= 20
|
||||
atlas_texture := g_mem.atlas_render_texture_target.texture
|
||||
rl.DrawTexturePro(
|
||||
atlas_texture,
|
||||
{width = auto_cast atlas_texture.width, height = auto_cast -atlas_texture.height},
|
||||
preview_rect,
|
||||
{0, 0},
|
||||
0,
|
||||
rl.WHITE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open_file_dialog :: proc() {
|
||||
switch g_mem.source_location_type {
|
||||
case .SourceFiles:
|
||||
// `open_file_dialog` returns a single cstring with one or more paths, divided by a separator ('|'),
|
||||
// https://github.com/native-toolkit/libtinyfiledialogs/blob/master/tinyfiledialogs.c#L2706
|
||||
file_paths_conc := cstring(
|
||||
diag.open_file_dialog(
|
||||
"Select source files",
|
||||
cstring(&g_mem.file_dialog_text_buffer[0]),
|
||||
0,
|
||||
nil,
|
||||
"",
|
||||
1,
|
||||
),
|
||||
)
|
||||
if len(file_paths_conc) > 0 {
|
||||
// todo(stefan): Currently we're not doing any checks if the filepaths are valid at all,
|
||||
// this should be fine because it's returned by the OS' file picker but who knows...
|
||||
source_files_to_pack := strings.clone_from_cstring(file_paths_conc, context.allocator)
|
||||
g_mem.source_files_to_pack = strings.split(source_files_to_pack, "|")
|
||||
|
||||
fmt.println(g_mem.source_files_to_pack)
|
||||
} else {
|
||||
fmt.println("No files were selected!")
|
||||
}
|
||||
|
||||
case .SourceFolder:
|
||||
file := cstring(
|
||||
diag.select_folder_dialog(
|
||||
"Select source folder",
|
||||
cstring(&g_mem.file_dialog_text_buffer[0]),
|
||||
),
|
||||
)
|
||||
if len(file) > 0 {
|
||||
g_mem.source_location_to_pack = strings.clone_from_cstring(file)
|
||||
fmt.println(g_mem.source_location_to_pack)
|
||||
} else {
|
||||
fmt.println("Got an empty path from the file dialog!")
|
||||
}
|
||||
|
||||
case .OutputFolder:
|
||||
file := cstring(
|
||||
diag.select_folder_dialog(
|
||||
"Select source folder",
|
||||
cstring(&g_mem.file_dialog_text_buffer[0]),
|
||||
),
|
||||
)
|
||||
if len(file) > 0 {
|
||||
g_mem.output_folder_path = strings.clone_from_cstring(file)
|
||||
fmt.println(g_mem.output_folder_path)
|
||||
} else {
|
||||
fmt.println("Got an empty path from the file dialog!")
|
||||
}
|
||||
|
||||
case .SaveFileAs:
|
||||
file_path: cstring
|
||||
patterns: []cstring = {"*.png"}
|
||||
if default_path, ok := g_mem.output_folder_path.(string); ok {
|
||||
default_path_filename := strings.concatenate(
|
||||
{default_path, os_file_separator, "atlas.png"},
|
||||
)
|
||||
default_path_to_save: cstring = strings.clone_to_cstring(default_path_filename)
|
||||
file_path = cstring(
|
||||
diag.save_file_dialog(
|
||||
"Save as...",
|
||||
default_path_to_save,
|
||||
1,
|
||||
&patterns[0],
|
||||
"Atlas",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
file_path = cstring(diag.save_file_dialog("Save as...", "", 1, &patterns[0], "Atlas"))
|
||||
}
|
||||
if file_path != nil {
|
||||
save_output()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
g_mem.should_open_file_dialog = false
|
||||
}
|
||||
|
||||
clear_atlas_data :: proc() {
|
||||
if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok {
|
||||
delete(metadata)
|
||||
// g_mem.atlas_metadata = nil
|
||||
}
|
||||
g_mem.atlas_render_has_preview = false
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
package utils
|
||||
package generator
|
||||
|
||||
import "core:fmt"
|
||||
import s "core:strings"
|
||||
|
||||
// todo(stefan): Simplify this whole flags business,
|
||||
// this can be implemented in a simpler fashion
|
||||
|
||||
CLIFlagType :: enum {
|
||||
Unknown,
|
||||
InputFiles,
|
||||
|
|
@ -18,7 +21,7 @@ CLIFlagType :: enum {
|
|||
CLI_FLAG_STRINGS := [CLIFlagType][]string {
|
||||
.Unknown = {""},
|
||||
.Help = {"h", "help"},
|
||||
.InputFiles = {"f", "input-files"},
|
||||
.InputFiles = {"i", "input-files"},
|
||||
.InputFolder = {"d", "input-directory"},
|
||||
.OutputFolder = {"o", "out"},
|
||||
.EnableMetadataOutput = {"m", "export-metadata"},
|
||||
|
|
@ -30,13 +33,13 @@ CLI_FLAG_STRINGS := [CLIFlagType][]string {
|
|||
CLI_FLAG_DESCRIPTIONS := [CLIFlagType]string {
|
||||
.Unknown = "Invalid flag",
|
||||
.Help = "Prints the help message... hello!",
|
||||
.InputFiles = "(real) path the source files for the packer (realpaths only), for multiple files you can provide one string of concateneted paths, separated by a ';'",
|
||||
.InputFolder = "(real) path to a folder full of source files. This is an alternative to the -i[,input-files] flag",
|
||||
.OutputFolder = "(real) path to the output folder for all the resulting files to be saved to.",
|
||||
.InputFiles = "Full path to the source files for the packer, for multiple files you can provide one string of concateneted paths, separated by a ';'",
|
||||
.InputFolder = "Full path to a folder full of source files. This is an alternative to the -i[,input-files] flag",
|
||||
.OutputFolder = "Full path to the output folder for all the resulting files to be saved to.",
|
||||
.EnableMetadataOutput = "Whether or not to export metadata (JSON or source files with the offsets for the packer sprites in the atlas)",
|
||||
.ConfigPath = "(real) path to a config file (json) that contains string definitions for exporting custom source files. More on this in the docs.",
|
||||
.MetadataJSONOutputPath = "(real) path for the resulting JSON that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag",
|
||||
.SourceCodeOutputPathOutputPath = "(real) path for the resulting source code file that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag",
|
||||
.ConfigPath = "Full path to a config file (json) that contains string definitions for exporting custom source files. More on this in the docs.",
|
||||
.MetadataJSONOutputPath = "Full path for the resulting JSON that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag",
|
||||
.SourceCodeOutputPathOutputPath = "Full path for the resulting source code file that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag",
|
||||
}
|
||||
|
||||
CLIOutputSettings :: struct {
|
||||
|
|
@ -83,12 +86,9 @@ print_help :: proc() {
|
|||
for flag in CLIFlagType {
|
||||
if flag == .Unknown do continue
|
||||
|
||||
fmt.printfln(
|
||||
"Flag: -%v,%v \t -- %v",
|
||||
CLI_FLAG_STRINGS[flag][0],
|
||||
CLI_FLAG_STRINGS[flag][1],
|
||||
CLI_FLAG_DESCRIPTIONS[flag],
|
||||
)
|
||||
flag_info := CLI_FLAG_STRINGS[flag]
|
||||
flag_desc := CLI_FLAG_DESCRIPTIONS[flag]
|
||||
fmt.printfln("Flag: -%v,%v \t -- %v", flag_info[0], flag_info[1], flag_desc)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package game
|
||||
package generator
|
||||
|
||||
import ase "./aseprite"
|
||||
import ase "../../vendors/odin-aseprite"
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:log"
|
||||
import "core:os"
|
||||
import fp "core:path/filepath"
|
||||
import "core:slice"
|
||||
|
|
@ -10,15 +11,12 @@ import "core:strings"
|
|||
import rl "vendor:raylib"
|
||||
import stbrp "vendor:stb/rect_pack"
|
||||
|
||||
import utils "./utils"
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
os_file_separator :: "\\"
|
||||
OS_FILE_SEPARATOR :: "\\"
|
||||
} else {
|
||||
os_file_separator :: "/"
|
||||
OS_FILE_SEPARATOR :: "/"
|
||||
}
|
||||
|
||||
|
||||
CellData :: struct {
|
||||
layer_index: u16,
|
||||
opacity: u8,
|
||||
|
|
@ -40,228 +38,6 @@ SpriteAtlasMetadata :: struct {
|
|||
size: [2]i32,
|
||||
}
|
||||
|
||||
unmarshall_aseprite_dir :: proc(
|
||||
path: string,
|
||||
atlas_entries: ^[dynamic]AtlasEntry,
|
||||
alloc := context.allocator,
|
||||
) {
|
||||
if len(path) == 0 do return
|
||||
|
||||
if dir_fd, err := os.open(path, os.O_RDONLY); err == os.ERROR_NONE {
|
||||
fis: []os.File_Info
|
||||
if fis, err = os.read_dir(dir_fd, -1); err == os.ERROR_NONE {
|
||||
unmarshall_aseprite_files_file_info(fis, atlas_entries, alloc)
|
||||
}
|
||||
} else {
|
||||
fmt.println("Couldn't open folder: ", path)
|
||||
}
|
||||
}
|
||||
|
||||
unmarshall_aseprite_files_file_info :: proc(
|
||||
files: []os.File_Info,
|
||||
atlas_entries: ^[dynamic]AtlasEntry,
|
||||
alloc := context.allocator,
|
||||
) {
|
||||
if len(files) == 0 do return
|
||||
|
||||
paths := make([]string, len(files), alloc)
|
||||
defer delete(paths)
|
||||
|
||||
for f, fi in files {
|
||||
paths[fi] = f.fullpath
|
||||
}
|
||||
|
||||
unmarshall_aseprite_files(paths[:], atlas_entries, alloc)
|
||||
|
||||
}
|
||||
|
||||
unmarshall_aseprite_files :: proc(
|
||||
file_paths: []string,
|
||||
atlas_entries: ^[dynamic]AtlasEntry,
|
||||
alloc := context.allocator,
|
||||
) {
|
||||
if len(file_paths) == 0 do return
|
||||
|
||||
aseprite_document: ase.Document
|
||||
for file in file_paths {
|
||||
extension := fp.ext(file)
|
||||
if extension != ".aseprite" do continue
|
||||
|
||||
fmt.println("Unmarshalling file: ", file)
|
||||
ase.unmarshal_from_filename(file, &aseprite_document, alloc)
|
||||
atlas_entry := atlas_entry_from_compressed_cells(aseprite_document)
|
||||
atlas_entry.path = file
|
||||
|
||||
append(atlas_entries, atlas_entry)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Goes through all the chunks in an aseprite document & copies the `Com_Image_Cel` cells in a separate image
|
||||
*/
|
||||
atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entry: AtlasEntry) {
|
||||
atlas_entry.frames = auto_cast len(document.frames)
|
||||
fmt.println("N Frames: ", len(document.frames))
|
||||
// NOTE(stefan): Since the expected input for the program is multiple files containing a single sprite
|
||||
// it's probably a safe assumption most of the files will be a single layer with 1 or more frames
|
||||
// which means we can first prod the file for information about how many frames are there and
|
||||
// allocate a slice that is going to be [Frames X Layers]CellData.
|
||||
// which would allow us to gain an already sorted list of sprites if we iterate all frames of a single layer
|
||||
// instead of iterating all layers for each frame
|
||||
// might be even quicker to first get that information an allocate at once the amount of cells we need
|
||||
for frame, frameIdx in document.frames {
|
||||
fmt.printfln("Frame_{0} Chunks: ", frameIdx, len(frame.chunks))
|
||||
for chunk in frame.chunks {
|
||||
if cel_chunk, ok := chunk.(ase.Cel_Chunk); ok {
|
||||
cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel)
|
||||
if !ci_ok do continue
|
||||
|
||||
fmt.println(cel_chunk.layer_index)
|
||||
|
||||
cell := CellData {
|
||||
img = rl.Image {
|
||||
data = rawptr(&cel_img.pixel[0]),
|
||||
width = auto_cast cel_img.width,
|
||||
height = auto_cast cel_img.height,
|
||||
format = .UNCOMPRESSED_R8G8B8A8,
|
||||
},
|
||||
frame_index = auto_cast frameIdx,
|
||||
opacity = cel_chunk.opacity_level,
|
||||
layer_index = cel_chunk.layer_index,
|
||||
}
|
||||
|
||||
append(&atlas_entry.cells, cell)
|
||||
}
|
||||
|
||||
if layer_chunk, ok := chunk.(ase.Layer_Chunk); ok {
|
||||
fmt.println("Layer chunk: ", layer_chunk)
|
||||
append(&atlas_entry.layer_names, layer_chunk.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slice.sort_by(atlas_entry.cells[:], proc(i, j: CellData) -> bool {
|
||||
return i.layer_index < j.layer_index
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Takes in a slice of entries, an output texture and offsets (offset_x/y)
|
||||
*/
|
||||
pack_atlas_entries :: proc(
|
||||
entries: []AtlasEntry,
|
||||
atlas: ^rl.Image,
|
||||
offset_x: i32,
|
||||
offset_y: i32,
|
||||
allocator := context.allocator,
|
||||
) -> [dynamic]SpriteAtlasMetadata {
|
||||
assert(atlas.width != 0, "Atlas width shouldn't be 0!")
|
||||
assert(atlas.height != 0, "Atlas height shouldn't be 0!")
|
||||
|
||||
all_cell_images := make([dynamic]rl.Image, allocator) // it's fine to store it like this, rl.Image just stores a pointer to the data
|
||||
for &entry in entries {
|
||||
for cell in entry.cells {
|
||||
append(&all_cell_images, cell.img)
|
||||
}
|
||||
entry.layer_cell_count = make([dynamic]i32, len(entry.cells), allocator)
|
||||
}
|
||||
|
||||
num_entries := len(all_cell_images)
|
||||
nodes := make([]stbrp.Node, num_entries, allocator)
|
||||
rects := make([]stbrp.Rect, num_entries, allocator)
|
||||
|
||||
EntryAndCell :: struct {
|
||||
entry: ^AtlasEntry,
|
||||
cell_of_entry: ^CellData,
|
||||
}
|
||||
rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100, allocator)
|
||||
|
||||
// Set the custom IDs
|
||||
cellIdx: int
|
||||
for &entry, entryIdx in entries {
|
||||
for &cell in entry.cells {
|
||||
// I can probably infer this information with just the id of the rect but I'm being lazy right now
|
||||
map_insert(&rect_idx_to_entry_and_cell, cellIdx, EntryAndCell{&entry, &cell})
|
||||
rects[cellIdx].id = auto_cast entryIdx
|
||||
cellIdx += 1
|
||||
|
||||
entry.layer_cell_count[cell.layer_index] += 1
|
||||
}
|
||||
}
|
||||
|
||||
for cell_image, cell_index in all_cell_images {
|
||||
entry_stb_rect := &rects[cell_index]
|
||||
entry_stb_rect.w = stbrp.Coord(cell_image.width + offset_x)
|
||||
entry_stb_rect.h = stbrp.Coord(cell_image.height + offset_y)
|
||||
}
|
||||
|
||||
ctx: stbrp.Context
|
||||
stbrp.init_target(&ctx, atlas.width, atlas.height, &nodes[0], i32(num_entries))
|
||||
res := stbrp.pack_rects(&ctx, &rects[0], i32(num_entries))
|
||||
if res == 1 {
|
||||
fmt.println("Packed everything successfully!")
|
||||
fmt.printfln("Rects: {0}", rects[:])
|
||||
} else {
|
||||
fmt.println("Failed to pack everything!")
|
||||
}
|
||||
|
||||
for rect, rectIdx in rects {
|
||||
entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx]
|
||||
cell := entry_and_cell.cell_of_entry
|
||||
|
||||
src_rect := rl.Rectangle {
|
||||
x = 0,
|
||||
y = 0,
|
||||
width = auto_cast cell.img.width,
|
||||
height = auto_cast cell.img.height,
|
||||
}
|
||||
|
||||
dst_rect := rl.Rectangle {
|
||||
auto_cast rect.x + auto_cast offset_x,
|
||||
auto_cast rect.y + auto_cast offset_y,
|
||||
auto_cast cell.img.width,
|
||||
auto_cast cell.img.height,
|
||||
}
|
||||
|
||||
// note(stefan): drawing the sprite in the atlas in the packed coordinates
|
||||
rl.ImageDraw(atlas, cell.img, src_rect, dst_rect, rl.WHITE)
|
||||
|
||||
fmt.printfln("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect)
|
||||
}
|
||||
|
||||
metadata := make([dynamic]SpriteAtlasMetadata, allocator)
|
||||
for rect, rectIdx in rects {
|
||||
entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx]
|
||||
entry := entry_and_cell.entry
|
||||
cell := entry_and_cell.cell_of_entry
|
||||
|
||||
cell_name: string
|
||||
if entry.layer_cell_count[cell.layer_index] > 1 {
|
||||
cell_name = fmt.aprintf(
|
||||
"{0}_%d",
|
||||
entry.layer_names[cell.layer_index],
|
||||
cell.frame_index,
|
||||
allocator,
|
||||
)
|
||||
} else {
|
||||
cell_name = entry.layer_names[cell.layer_index]
|
||||
}
|
||||
cell_metadata := SpriteAtlasMetadata {
|
||||
name = cell_name,
|
||||
location = {
|
||||
auto_cast rect.x + auto_cast offset_x,
|
||||
auto_cast rect.y + auto_cast offset_y,
|
||||
},
|
||||
size = {auto_cast cell.img.width, auto_cast cell.img.height},
|
||||
}
|
||||
append(&metadata, cell_metadata)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
SourceCodeGeneratorMetadata :: struct {
|
||||
file_defines: struct {
|
||||
top: string,
|
||||
|
|
@ -291,6 +67,226 @@ SourceCodeGeneratorMetadata :: struct {
|
|||
},
|
||||
}
|
||||
|
||||
unmarshall_aseprite_dir :: proc(
|
||||
path: string,
|
||||
atlas_entries: ^[dynamic]AtlasEntry,
|
||||
alloc := context.allocator,
|
||||
) {
|
||||
if len(path) == 0 do return
|
||||
|
||||
if dir_fd, err := os.open(path, os.O_RDONLY); err == os.ERROR_NONE {
|
||||
fis: []os.File_Info
|
||||
if fis, err = os.read_dir(dir_fd, -1); err == os.ERROR_NONE {
|
||||
unmarshall_aseprite_files_file_info(fis, atlas_entries, alloc)
|
||||
}
|
||||
} else {
|
||||
log.errorf("Couldn't open folder: ", path)
|
||||
}
|
||||
}
|
||||
|
||||
unmarshall_aseprite_files_file_info :: proc(
|
||||
files: []os.File_Info,
|
||||
atlas_entries: ^[dynamic]AtlasEntry,
|
||||
alloc := context.allocator,
|
||||
) {
|
||||
if len(files) == 0 do return
|
||||
|
||||
paths := make([]string, len(files), alloc)
|
||||
defer delete(paths)
|
||||
|
||||
for f, fi in files {
|
||||
paths[fi] = f.fullpath
|
||||
}
|
||||
|
||||
unmarshall_aseprite_files(paths[:], atlas_entries, alloc)
|
||||
}
|
||||
|
||||
unmarshall_aseprite_files :: proc(
|
||||
file_paths: []string,
|
||||
atlas_entries: ^[dynamic]AtlasEntry,
|
||||
alloc := context.allocator,
|
||||
) {
|
||||
if len(file_paths) == 0 do return
|
||||
|
||||
aseprite_document: ase.Document
|
||||
for file in file_paths {
|
||||
extension := fp.ext(file)
|
||||
if extension != ".aseprite" do continue
|
||||
|
||||
log.info("Unmarshalling file:", file)
|
||||
ase.unmarshal_from_filename(&aseprite_document, file)
|
||||
atlas_entry := atlas_entry_from_compressed_cells(aseprite_document)
|
||||
atlas_entry.path = file
|
||||
|
||||
append(atlas_entries, atlas_entry)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Goes through all the chunks in an aseprite document & copies the `Com_Image_Cel` cells in a separate image
|
||||
*/
|
||||
atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entry: AtlasEntry) {
|
||||
atlas_entry.frames = auto_cast len(document.frames)
|
||||
log.infof("N Frames: ", len(document.frames))
|
||||
// NOTE(stefan): Since the expected input for the program is multiple files containing a single sprite
|
||||
// it's probably a safe assumption most of the files will be a single layer with 1 or more frames
|
||||
// which means we can first prod the file for information about how many frames are there and
|
||||
// allocate a slice that is going to be [Frames X Layers]CellData.
|
||||
// which would allow us to gain an already sorted list of sprites if we iterate all frames of a single layer
|
||||
// instead of iterating all layers for each frame
|
||||
// might be even quicker to first get that information an allocate at once the amount of cells we need
|
||||
for frame, frameIdx in document.frames {
|
||||
log.infof("Frame_{0} Chunks: {1}", frameIdx, len(frame.chunks))
|
||||
for chunk in frame.chunks {
|
||||
if cel_chunk, ok := chunk.(ase.Cel_Chunk); ok {
|
||||
cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel)
|
||||
if !ci_ok do continue
|
||||
|
||||
log.info(cel_chunk.layer_index)
|
||||
|
||||
cell := CellData {
|
||||
img = rl.Image {
|
||||
data = rawptr(&cel_img.pixels[0]),
|
||||
width = auto_cast cel_img.width,
|
||||
height = auto_cast cel_img.height,
|
||||
format = .UNCOMPRESSED_R8G8B8A8,
|
||||
},
|
||||
frame_index = auto_cast frameIdx,
|
||||
opacity = cel_chunk.opacity_level,
|
||||
layer_index = cel_chunk.layer_index,
|
||||
}
|
||||
|
||||
append(&atlas_entry.cells, cell)
|
||||
}
|
||||
|
||||
if layer_chunk, ok := chunk.(ase.Layer_Chunk); ok {
|
||||
log.info("Layer chunk:", layer_chunk)
|
||||
append(&atlas_entry.layer_names, layer_chunk.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slice.sort_by(atlas_entry.cells[:], proc(i, j: CellData) -> bool {
|
||||
return i.layer_index < j.layer_index
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Takes in a slice of entries, an output texture and offsets (offset_x/y)
|
||||
*/
|
||||
pack_atlas_entries :: proc(
|
||||
entries: []AtlasEntry,
|
||||
atlas: ^rl.Image,
|
||||
offset_x: i32,
|
||||
offset_y: i32,
|
||||
) -> [dynamic]SpriteAtlasMetadata {
|
||||
assert(atlas.width != 0, "Atlas width shouldn't be 0!")
|
||||
assert(atlas.height != 0, "Atlas height shouldn't be 0!")
|
||||
|
||||
all_cell_images := make([dynamic]rl.Image) // it's fine to store it like this, rl.Image just stores a pointer to the data
|
||||
defer delete(all_cell_images)
|
||||
for &entry in entries {
|
||||
for cell in entry.cells {
|
||||
append(&all_cell_images, cell.img)
|
||||
}
|
||||
entry.layer_cell_count = make([dynamic]i32, len(entry.cells))
|
||||
}
|
||||
|
||||
num_entries := len(all_cell_images)
|
||||
nodes := make([]stbrp.Node, num_entries)
|
||||
rects := make([]stbrp.Rect, num_entries)
|
||||
defer delete(nodes)
|
||||
defer delete(rects)
|
||||
|
||||
EntryAndCell :: struct {
|
||||
entry: ^AtlasEntry,
|
||||
cell_of_entry: ^CellData,
|
||||
}
|
||||
rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100)
|
||||
defer delete(rect_idx_to_entry_and_cell)
|
||||
|
||||
// Set the custom IDs
|
||||
cellIdx: int
|
||||
for &entry, entryIdx in entries {
|
||||
for &cell in entry.cells {
|
||||
// I can probably infer this information with just the id of the rect but I'm being lazy right now
|
||||
map_insert(&rect_idx_to_entry_and_cell, cellIdx, EntryAndCell{&entry, &cell})
|
||||
rects[cellIdx].id = auto_cast entryIdx
|
||||
cellIdx += 1
|
||||
|
||||
entry.layer_cell_count[cell.layer_index] += 1
|
||||
}
|
||||
}
|
||||
|
||||
for cell_image, cell_index in all_cell_images {
|
||||
entry_stb_rect := &rects[cell_index]
|
||||
entry_stb_rect.w = stbrp.Coord(cell_image.width + offset_x)
|
||||
entry_stb_rect.h = stbrp.Coord(cell_image.height + offset_y)
|
||||
}
|
||||
|
||||
ctx: stbrp.Context
|
||||
stbrp.init_target(&ctx, atlas.width, atlas.height, &nodes[0], i32(num_entries))
|
||||
res := stbrp.pack_rects(&ctx, &rects[0], i32(num_entries))
|
||||
if res == 1 {
|
||||
log.info("Packed everything successfully!")
|
||||
log.infof("Rects: {0}", rects[:])
|
||||
} else {
|
||||
log.error("Failed to pack everything!")
|
||||
}
|
||||
|
||||
for rect, rectIdx in rects {
|
||||
entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx]
|
||||
cell := entry_and_cell.cell_of_entry
|
||||
|
||||
src_rect := rl.Rectangle {
|
||||
x = 0,
|
||||
y = 0,
|
||||
width = f32(cell.img.width),
|
||||
height = f32(cell.img.height),
|
||||
}
|
||||
|
||||
dst_rect := rl.Rectangle {
|
||||
f32(i32(rect.x) + offset_x),
|
||||
f32(i32(rect.y) + offset_y),
|
||||
f32(cell.img.width),
|
||||
f32(cell.img.height),
|
||||
}
|
||||
|
||||
// note(stefan): drawing the sprite in the atlas in the packed coordinates
|
||||
rl.ImageDraw(atlas, cell.img, src_rect, dst_rect, rl.WHITE)
|
||||
|
||||
log.infof("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect)
|
||||
}
|
||||
|
||||
metadata := make([dynamic]SpriteAtlasMetadata)
|
||||
for rect, rectIdx in rects {
|
||||
entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx]
|
||||
entry := entry_and_cell.entry
|
||||
cell := entry_and_cell.cell_of_entry
|
||||
|
||||
cell_name: string
|
||||
if entry.layer_cell_count[cell.layer_index] > 1 {
|
||||
cell_name = fmt.aprintf(
|
||||
"{0}_{1}",
|
||||
entry.layer_names[cell.layer_index],
|
||||
cell.frame_index,
|
||||
)
|
||||
} else {
|
||||
cell_name = entry.layer_names[cell.layer_index]
|
||||
}
|
||||
cell_metadata := SpriteAtlasMetadata {
|
||||
name = cell_name,
|
||||
location = {i32(rect.x) + offset_x, i32(rect.y) + offset_y},
|
||||
size = {auto_cast cell.img.width, auto_cast cell.img.height},
|
||||
}
|
||||
append(&metadata, cell_metadata)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
odin_source_generator_metadata := SourceCodeGeneratorMetadata {
|
||||
file_defines = {
|
||||
top = "package atlas_bindings\n\n",
|
||||
|
|
@ -310,7 +306,7 @@ odin_source_generator_metadata := SourceCodeGeneratorMetadata {
|
|||
},
|
||||
array_data = {
|
||||
name = "ATLAS_SPRITES",
|
||||
type = "[]AtlasRect",
|
||||
type = "[AtlasSprite]AtlasRect",
|
||||
begin_line = "%v := %v {{\n",
|
||||
entry_line = "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n",
|
||||
end_line = "}\n\n",
|
||||
|
|
@ -346,9 +342,9 @@ cpp_source_generator_metadata := SourceCodeGeneratorMetadata {
|
|||
}
|
||||
|
||||
/*
|
||||
Generates a barebones file with the package name "atlas_bindings",
|
||||
the file contains an array of offsets, indexed by an enum.
|
||||
The enum has unique names
|
||||
Generates a barebones file with the package name "atlas_bindings",
|
||||
the file contains an array of offsets, indexed by an enum.
|
||||
The enum has unique names
|
||||
*/
|
||||
generate_odin_enums_and_atlas_offsets_file_sb :: proc(
|
||||
metadata: []SpriteAtlasMetadata,
|
||||
|
|
@ -371,7 +367,7 @@ generate_odin_enums_and_atlas_offsets_file_sb :: proc(
|
|||
|
||||
// start offsets array
|
||||
// todo(stefan): the name of the array can be based on the output name?
|
||||
strings.write_string(&sb, "ATLAS_SPRITES := []AtlasRect {\n")
|
||||
strings.write_string(&sb, "ATLAS_SPRITES := [AtlasSprite]AtlasRect {\n")
|
||||
{
|
||||
entry: string
|
||||
for cell in metadata {
|
||||
|
|
@ -389,7 +385,7 @@ generate_odin_enums_and_atlas_offsets_file_sb :: proc(
|
|||
// end offsets array
|
||||
strings.write_string(&sb, "}\n\n")
|
||||
|
||||
fmt.println("\n", strings.to_string(sb))
|
||||
log.info("\n", strings.to_string(sb))
|
||||
|
||||
return sb
|
||||
}
|
||||
|
|
@ -405,26 +401,21 @@ metadata_source_code_generate :: proc(
|
|||
strings.write_string(&sb, codegen.file_defines.top)
|
||||
|
||||
// Introduce the Rect type
|
||||
// strings.write_string(&sb, "AtlasRect :: struct { x, y, w, h: i32 }\n\n")
|
||||
strings.write_string(
|
||||
&sb,
|
||||
fmt.aprintf(codegen.custom_data_type.type_declaration, codegen.custom_data_type.name),
|
||||
)
|
||||
// start enum
|
||||
// strings.write_string(&sb, "AtlasSprite :: enum {\n")
|
||||
strings.write_string(&sb, fmt.aprintf(codegen.enum_data.begin_line, codegen.enum_data.name))
|
||||
{
|
||||
for cell in metadata {
|
||||
// strings.write_string(&sb, fmt.aprintf("\t%s,\n", cell.name))
|
||||
strings.write_string(&sb, fmt.aprintf(codegen.enum_data.entry_line, cell.name))
|
||||
}
|
||||
}
|
||||
// end enum
|
||||
// strings.write_string(&sb, "}\n\n")
|
||||
strings.write_string(&sb, codegen.enum_data.end_line)
|
||||
|
||||
// start offsets array
|
||||
// strings.write_string(&sb, "ATLAS_SPRITES := []AtlasRect {\n")
|
||||
strings.write_string(
|
||||
&sb,
|
||||
fmt.aprintf(
|
||||
|
|
@ -448,58 +439,69 @@ metadata_source_code_generate :: proc(
|
|||
}
|
||||
}
|
||||
// end offsets array
|
||||
// strings.write_string(&sb, "}\n\n")
|
||||
|
||||
strings.write_string(&sb, codegen.array_data.end_line)
|
||||
|
||||
strings.write_string(&sb, codegen.file_defines.bottom)
|
||||
|
||||
fmt.println("\n", strings.to_string(sb))
|
||||
log.info("\n", strings.to_string(sb))
|
||||
|
||||
return sb
|
||||
|
||||
}
|
||||
|
||||
save_output :: proc() {
|
||||
output_path, ok := g_mem.output_folder_path.(string)
|
||||
if !ok || output_path == "" {
|
||||
fmt.println("Output path is empty!")
|
||||
save_output :: proc(
|
||||
output_path: string,
|
||||
metadata: [dynamic]SpriteAtlasMetadata,
|
||||
render_texture_target: rl.RenderTexture2D,
|
||||
) {
|
||||
if len(output_path) == 0 {
|
||||
log.error("Output path is empty!")
|
||||
return
|
||||
}
|
||||
|
||||
image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture)
|
||||
image := rl.LoadImageFromTexture(render_texture_target.texture)
|
||||
rl.ImageFlipVertical(&image)
|
||||
|
||||
cstring_atlas_output_path := strings.clone_to_cstring(
|
||||
strings.concatenate({output_path, os_file_separator, "atlas.png"}),
|
||||
strings.concatenate({output_path, OS_FILE_SEPARATOR, "atlas.png"}),
|
||||
)
|
||||
|
||||
rl.ExportImage(image, cstring_atlas_output_path)
|
||||
|
||||
if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok {
|
||||
fmt.println("Building metadata...")
|
||||
if len(metadata) > 0 {
|
||||
log.info("Building metadata...")
|
||||
if json_metadata, jok := json.marshal(metadata); jok == nil {
|
||||
os.write_entire_file(
|
||||
strings.concatenate({output_path, os_file_separator, "metadata.json"}),
|
||||
strings.concatenate(
|
||||
{output_path, OS_FILE_SEPARATOR, "metadata.json"},
|
||||
context.temp_allocator,
|
||||
),
|
||||
json_metadata,
|
||||
)
|
||||
delete(json_metadata)
|
||||
} else {
|
||||
fmt.println("Failed to marshall the atlas metadata to a json!")
|
||||
log.error("Failed to marshall the atlas metadata to a json!")
|
||||
}
|
||||
|
||||
// TODO(stefan): Think of a more generic alternative to just straight output to a odin file
|
||||
// maybe supply a config.json that defines the start, end, line by line entry and enum format strings
|
||||
// this way you can essentially support any language
|
||||
sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:])
|
||||
defer strings.builder_destroy(&sb)
|
||||
odin_metadata := strings.to_string(sb)
|
||||
ok := os.write_entire_file(
|
||||
strings.concatenate({output_path, os_file_separator, "metadata.odin"}),
|
||||
strings.concatenate(
|
||||
{output_path, OS_FILE_SEPARATOR, "metadata.odin"},
|
||||
context.temp_allocator,
|
||||
),
|
||||
transmute([]byte)odin_metadata,
|
||||
)
|
||||
if !ok {
|
||||
fmt.println("Failed to save 'metadata.odin'")
|
||||
log.error("Failed to save 'metadata.odin'")
|
||||
}
|
||||
} else {
|
||||
fmt.println("No metadata to export!")
|
||||
log.error("No metadata to export!")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -509,30 +511,31 @@ save_metadata_simple :: proc(
|
|||
json_file_name: Maybe(string),
|
||||
source_file_name: Maybe(string),
|
||||
source_gen_metadata: Maybe(SourceCodeGeneratorMetadata),
|
||||
atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata),
|
||||
) {
|
||||
json_file_base_name, json_file_name_ok := json_file_name.(string)
|
||||
source_file_base_name, source_file_name_ok := source_file_name.(string)
|
||||
|
||||
if !json_file_name_ok && !source_file_name_ok {
|
||||
fmt.println("Neither a json file name or a source code filename has been provided!")
|
||||
log.error("Neither a json file name or a source code filename has been provided!")
|
||||
return
|
||||
}
|
||||
|
||||
metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata);if !ok {
|
||||
fmt.println("No metadata to export!")
|
||||
metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); if !ok {
|
||||
log.error("No metadata to export!")
|
||||
}
|
||||
|
||||
fmt.println("Building metadata...")
|
||||
log.info("Building metadata...")
|
||||
if json_file_name_ok {
|
||||
if json_metadata, jok := json.marshal(metadata); jok == nil {
|
||||
json_output_path := strings.concatenate(
|
||||
{output_path, os_file_separator, json_file_base_name},
|
||||
{output_path, OS_FILE_SEPARATOR, json_file_base_name},
|
||||
)
|
||||
if ok = os.write_entire_file(json_output_path, json_metadata); !ok {
|
||||
fmt.println("Failed to write json to file: ", json_output_path)
|
||||
log.errorf("Failed to write json to file: ", json_output_path)
|
||||
}
|
||||
} else {
|
||||
fmt.println("Failed to marshall the atlas metadata to a json!")
|
||||
log.error("Failed to marshall the atlas metadata to a json!")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -541,48 +544,50 @@ save_metadata_simple :: proc(
|
|||
// if src_gen_metadata
|
||||
if codegen, cok := source_gen_metadata.(SourceCodeGeneratorMetadata); cok {
|
||||
sb := metadata_source_code_generate(metadata[:], codegen)
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
source_metadata := strings.to_string(sb)
|
||||
source_output_path := strings.concatenate(
|
||||
{
|
||||
output_path,
|
||||
os_file_separator,
|
||||
OS_FILE_SEPARATOR,
|
||||
codegen.file_defines.file_name,
|
||||
codegen.file_defines.file_extension,
|
||||
},
|
||||
)
|
||||
ok := os.write_entire_file(source_output_path, transmute([]byte)source_metadata)
|
||||
if !ok {
|
||||
fmt.println("Failed to save source code to file:", source_output_path)
|
||||
log.errorf("Failed to save source code to file:", source_output_path)
|
||||
}
|
||||
} else {
|
||||
sb := metadata_source_code_generate(metadata[:], odin_source_generator_metadata)
|
||||
defer strings.builder_destroy(&sb)
|
||||
odin_metadata := strings.to_string(sb)
|
||||
source_output_path := strings.concatenate(
|
||||
{output_path, os_file_separator, "metadata.odin"},
|
||||
{output_path, OS_FILE_SEPARATOR, "metadata.odin"},
|
||||
)
|
||||
|
||||
ok := os.write_entire_file(source_output_path, transmute([]byte)odin_metadata)
|
||||
if !ok {
|
||||
fmt.println("Failed to save source code to file:", source_output_path)
|
||||
log.errorf("Failed to save source code to file:", source_output_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
save_metadata :: proc(
|
||||
settings: utils.CLIPackerSettings,
|
||||
settings: CLIPackerSettings,
|
||||
atlas_entries: []AtlasEntry,
|
||||
atlas_metadata: []SpriteAtlasMetadata,
|
||||
) {
|
||||
metadata, ok := settings.metadata.(utils.CLIMetadataSettings);if !ok do return
|
||||
metadata, ok := settings.metadata.(CLIMetadataSettings); if !ok do return
|
||||
|
||||
if json_path, ok := metadata.json_path.(string); ok {
|
||||
json_bytes, jerr := json.marshal(atlas_metadata)
|
||||
if jerr == nil {
|
||||
os.write_entire_file(json_path, json_bytes)
|
||||
} else {
|
||||
fmt.println("Failed to marshall metadata")
|
||||
log.error("Failed to marshall metadata")
|
||||
}
|
||||
}
|
||||
if source_code_path, ok := metadata.source_code_path.(string); ok {
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
|
||||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
PixelWindowHeight :: 180
|
||||
FILE_DIALOG_SIZE :: 1000
|
||||
|
||||
scaling: f32 = 2
|
||||
w, h: f32
|
||||
|
||||
WindowInformation :: struct {
|
||||
w: f32,
|
||||
h: f32,
|
||||
width_scaled: f32,
|
||||
height_scaled: f32,
|
||||
}
|
||||
|
||||
MonitorInformation :: struct {
|
||||
max_width: f32,
|
||||
max_height: f32,
|
||||
}
|
||||
|
||||
FileDialogType :: enum {
|
||||
SourceFiles,
|
||||
SourceFolder,
|
||||
OutputFolder,
|
||||
SaveFileAs,
|
||||
}
|
||||
|
||||
PackerSettings :: struct {
|
||||
atlas_size_x: i32,
|
||||
atlas_size_y: i32,
|
||||
pixel_padding_x_int: i32,
|
||||
pixel_padding_y_int: i32,
|
||||
padding_enabled: bool,
|
||||
output_json: bool,
|
||||
output_odin: bool,
|
||||
}
|
||||
|
||||
GameMemory :: struct {
|
||||
file_dialog_text_buffer: [FILE_DIALOG_SIZE + 1]u8,
|
||||
is_packing_whole_source_folder: bool,
|
||||
should_open_file_dialog: bool,
|
||||
window_info: WindowInformation,
|
||||
monitor_info: MonitorInformation,
|
||||
// Where the output files will be written (atlas.png, json output, etc)
|
||||
output_folder_path: Maybe(string),
|
||||
// If files were chosen as input - their paths
|
||||
source_location_to_pack: Maybe(string),
|
||||
// If a folder was chosen as input - the path
|
||||
source_files_to_pack: Maybe([]string),
|
||||
// What type of file dialog to open
|
||||
source_location_type: FileDialogType,
|
||||
// Packer settings
|
||||
packer_settings: PackerSettings,
|
||||
atlas_render_texture_target: rl.RenderTexture2D,
|
||||
atlas_checked_background: rl.RenderTexture2D,
|
||||
should_render_atlas: bool,
|
||||
atlas_render_has_preview: bool,
|
||||
atlas_render_size: i32,
|
||||
atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata),
|
||||
}
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
// Development game exe. Loads game.dll and reloads it whenever it changes.
|
||||
|
||||
package main
|
||||
|
||||
import "core:c/libc"
|
||||
import "core:dynlib"
|
||||
import "core:fmt"
|
||||
import "core:log"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
DLL_EXT :: ".dll"
|
||||
} else when ODIN_OS == .Darwin {
|
||||
DLL_EXT :: ".dylib"
|
||||
} else {
|
||||
DLL_EXT :: ".so"
|
||||
}
|
||||
|
||||
copy_dll :: proc(to: string) -> bool {
|
||||
exit: i32
|
||||
when ODIN_OS == .Windows {
|
||||
exit = libc.system(fmt.ctprintf("copy game.dll {0}", to))
|
||||
} else {
|
||||
exit = libc.system(fmt.ctprintf("cp game" + DLL_EXT + " {0}", to))
|
||||
}
|
||||
|
||||
if exit != 0 {
|
||||
fmt.printfln("Failed to copy game" + DLL_EXT + " to {0}", to)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
GameAPI :: struct {
|
||||
lib: dynlib.Library,
|
||||
init_window: proc(),
|
||||
init: proc(),
|
||||
update: proc() -> bool,
|
||||
shutdown: proc(),
|
||||
shutdown_window: proc(),
|
||||
memory: proc() -> rawptr,
|
||||
memory_size: proc() -> int,
|
||||
hot_reloaded: proc(mem: rawptr),
|
||||
force_reload: proc() -> bool,
|
||||
force_restart: proc() -> bool,
|
||||
modification_time: os.File_Time,
|
||||
api_version: int,
|
||||
}
|
||||
|
||||
load_game_api :: proc(api_version: int) -> (api: GameAPI, ok: bool) {
|
||||
mod_time, mod_time_error := os.last_write_time_by_name("game" + DLL_EXT)
|
||||
if mod_time_error != os.ERROR_NONE {
|
||||
fmt.printfln(
|
||||
"Failed getting last write time of game" + DLL_EXT + ", error code: {1}",
|
||||
mod_time_error,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: this needs to be a relative path for Linux to work.
|
||||
game_dll_name := fmt.tprintf(
|
||||
"{0}game_{1}" + DLL_EXT,
|
||||
"./" when ODIN_OS != .Windows else "",
|
||||
api_version,
|
||||
)
|
||||
copy_dll(game_dll_name) or_return
|
||||
|
||||
_, ok = dynlib.initialize_symbols(&api, game_dll_name, "game_", "lib")
|
||||
if !ok {
|
||||
fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error())
|
||||
}
|
||||
|
||||
api.api_version = api_version
|
||||
api.modification_time = mod_time
|
||||
ok = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
unload_game_api :: proc(api: ^GameAPI) {
|
||||
if api.lib != nil {
|
||||
if !dynlib.unload_library(api.lib) {
|
||||
fmt.printfln("Failed unloading lib: {0}", dynlib.last_error())
|
||||
}
|
||||
}
|
||||
|
||||
if os.remove(fmt.tprintf("game_{0}" + DLL_EXT, api.api_version)) != 0 {
|
||||
fmt.printfln("Failed to remove game_{0}" + DLL_EXT + " copy", api.api_version)
|
||||
}
|
||||
}
|
||||
|
||||
main :: proc() {
|
||||
context.logger = log.create_console_logger()
|
||||
|
||||
default_allocator := context.allocator
|
||||
tracking_allocator: mem.Tracking_Allocator
|
||||
mem.tracking_allocator_init(&tracking_allocator, default_allocator)
|
||||
context.allocator = mem.tracking_allocator(&tracking_allocator)
|
||||
|
||||
reset_tracking_allocator :: proc(a: ^mem.Tracking_Allocator) -> bool {
|
||||
err := false
|
||||
|
||||
for _, value in a.allocation_map {
|
||||
fmt.printf("%v: Leaked %v bytes\n", value.location, value.size)
|
||||
err = true
|
||||
}
|
||||
|
||||
mem.tracking_allocator_clear(a)
|
||||
return err
|
||||
}
|
||||
|
||||
game_api_version := 0
|
||||
game_api, game_api_ok := load_game_api(game_api_version)
|
||||
|
||||
if !game_api_ok {
|
||||
fmt.println("Failed to load Game API")
|
||||
return
|
||||
}
|
||||
|
||||
game_api_version += 1
|
||||
game_api.init_window()
|
||||
game_api.init()
|
||||
|
||||
old_game_apis := make([dynamic]GameAPI, default_allocator)
|
||||
|
||||
window_open := true
|
||||
for window_open {
|
||||
window_open = game_api.update()
|
||||
force_reload := game_api.force_reload()
|
||||
force_restart := game_api.force_restart()
|
||||
reload := force_reload || force_restart
|
||||
game_dll_mod, game_dll_mod_err := os.last_write_time_by_name("game" + DLL_EXT)
|
||||
|
||||
if game_dll_mod_err == os.ERROR_NONE && game_api.modification_time != game_dll_mod {
|
||||
reload = true
|
||||
}
|
||||
|
||||
if reload {
|
||||
new_game_api, new_game_api_ok := load_game_api(game_api_version)
|
||||
|
||||
if new_game_api_ok {
|
||||
if game_api.memory_size() != new_game_api.memory_size() || force_restart {
|
||||
game_api.shutdown()
|
||||
reset_tracking_allocator(&tracking_allocator)
|
||||
|
||||
for &g in old_game_apis {
|
||||
unload_game_api(&g)
|
||||
}
|
||||
|
||||
clear(&old_game_apis)
|
||||
unload_game_api(&game_api)
|
||||
game_api = new_game_api
|
||||
game_api.init()
|
||||
} else {
|
||||
append(&old_game_apis, game_api)
|
||||
game_memory := game_api.memory()
|
||||
game_api = new_game_api
|
||||
game_api.hot_reloaded(game_memory)
|
||||
}
|
||||
|
||||
game_api_version += 1
|
||||
}
|
||||
}
|
||||
|
||||
for b in tracking_allocator.bad_free_array {
|
||||
log.error("Bad free at: %v", b.location)
|
||||
}
|
||||
|
||||
clear(&tracking_allocator.bad_free_array)
|
||||
free_all(context.temp_allocator)
|
||||
}
|
||||
|
||||
free_all(context.temp_allocator)
|
||||
game_api.shutdown()
|
||||
reset_tracking_allocator(&tracking_allocator)
|
||||
|
||||
for &g in old_game_apis {
|
||||
unload_game_api(&g)
|
||||
}
|
||||
|
||||
delete(old_game_apis)
|
||||
|
||||
game_api.shutdown_window()
|
||||
unload_game_api(&game_api)
|
||||
mem.tracking_allocator_destroy(&tracking_allocator)
|
||||
}
|
||||
|
||||
// make game use good GPU on laptops etc
|
||||
|
||||
@(export)
|
||||
NvOptimusEnablement: u32 = 1
|
||||
|
||||
@(export)
|
||||
AmdPowerXpressRequestHighPerformance: i32 = 1
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// For making a release exe that does not use hot reload.
|
||||
|
||||
package main_release
|
||||
|
||||
import "core:log"
|
||||
import "core:os"
|
||||
|
||||
import game ".."
|
||||
|
||||
UseTrackingAllocator :: #config(UseTrackingAllocator, false)
|
||||
|
||||
main :: proc() {
|
||||
when UseTrackingAllocator {
|
||||
default_allocator := context.allocator
|
||||
tracking_allocator: Tracking_Allocator
|
||||
tracking_allocator_init(&tracking_allocator, default_allocator)
|
||||
context.allocator = allocator_from_tracking_allocator(&tracking_allocator)
|
||||
}
|
||||
|
||||
mode: int = 0
|
||||
when ODIN_OS == .Linux || ODIN_OS == .Darwin {
|
||||
mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH
|
||||
}
|
||||
|
||||
logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode)
|
||||
|
||||
if logh_err == os.ERROR_NONE {
|
||||
os.stdout = logh
|
||||
os.stderr = logh
|
||||
}
|
||||
|
||||
logger :=
|
||||
logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger()
|
||||
context.logger = logger
|
||||
|
||||
game.game_init_window()
|
||||
game.game_init()
|
||||
|
||||
window_open := true
|
||||
for window_open {
|
||||
window_open = game.game_update()
|
||||
|
||||
when UseTrackingAllocator {
|
||||
for b in tracking_allocator.bad_free_array {
|
||||
log.error("Bad free at: %v", b.location)
|
||||
}
|
||||
|
||||
clear(&tracking_allocator.bad_free_array)
|
||||
}
|
||||
|
||||
free_all(context.temp_allocator)
|
||||
}
|
||||
|
||||
free_all(context.temp_allocator)
|
||||
game.game_shutdown()
|
||||
game.game_shutdown_window()
|
||||
|
||||
if logh_err == os.ERROR_NONE {
|
||||
log.destroy_file_logger(&logger)
|
||||
}
|
||||
|
||||
when UseTrackingAllocator {
|
||||
for key, value in tracking_allocator.allocation_map {
|
||||
log.error("%v: Leaked %v bytes\n", value.location, value.size)
|
||||
}
|
||||
|
||||
tracking_allocator_destroy(&tracking_allocator)
|
||||
}
|
||||
}
|
||||
|
||||
// make game use good GPU on laptops etc
|
||||
|
||||
@(export)
|
||||
NvOptimusEnablement: u32 = 1
|
||||
|
||||
@(export)
|
||||
AmdPowerXpressRequestHighPerformance: i32 = 1
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
@(export)
|
||||
game_update :: proc() -> bool {
|
||||
update()
|
||||
draw()
|
||||
return !rl.WindowShouldClose()
|
||||
}
|
||||
|
||||
@(export)
|
||||
game_init_window :: proc() {
|
||||
rl.SetConfigFlags({.WINDOW_RESIZABLE})
|
||||
rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer")
|
||||
rl.SetWindowMinSize(1400, 800)
|
||||
}
|
||||
|
||||
@(export)
|
||||
game_init :: proc() {
|
||||
g_mem = new(GameMemory)
|
||||
|
||||
g_mem^ = GameMemory{}
|
||||
|
||||
game_hot_reloaded(g_mem)
|
||||
|
||||
|
||||
when !ODIN_DEBUG {
|
||||
rl.SetExitKey(nil)
|
||||
}
|
||||
|
||||
current_monitor := rl.GetCurrentMonitor()
|
||||
g_mem.monitor_info = MonitorInformation {
|
||||
max_width = auto_cast rl.GetMonitorWidth(current_monitor),
|
||||
max_height = auto_cast rl.GetMonitorHeight(current_monitor),
|
||||
}
|
||||
|
||||
g_mem.window_info = WindowInformation {
|
||||
w = 1280,
|
||||
h = 720,
|
||||
}
|
||||
|
||||
g_mem.atlas_render_texture_target = rl.LoadRenderTexture(256, 256)
|
||||
g_mem.atlas_render_size = 256
|
||||
|
||||
checkered_img := rl.GenImageChecked(256, 256, 256 / 4, 256 / 4, rl.GRAY, rl.DARKGRAY)
|
||||
defer rl.UnloadImage(checkered_img)
|
||||
g_mem.atlas_checked_background.texture = rl.LoadTextureFromImage(checkered_img)
|
||||
|
||||
rl.SetTargetFPS(rl.GetMonitorRefreshRate(current_monitor))
|
||||
rl.GuiLoadStyle("./styles/style_candy.rgs")
|
||||
}
|
||||
|
||||
@(export)
|
||||
game_shutdown :: proc() {
|
||||
free(g_mem)
|
||||
}
|
||||
|
||||
@(export)
|
||||
game_shutdown_window :: proc() {
|
||||
rl.CloseWindow()
|
||||
}
|
||||
|
||||
@(export)
|
||||
game_memory :: proc() -> rawptr {
|
||||
return g_mem
|
||||
}
|
||||
|
||||
@(export)
|
||||
game_memory_size :: proc() -> int {
|
||||
return size_of(GameMemory)
|
||||
}
|
||||
|
||||
@(export)
|
||||
game_hot_reloaded :: proc(mem: rawptr) {
|
||||
g_mem = (^GameMemory)(mem)
|
||||
rl.GuiLoadStyle("./styles/style_candy.rgs")
|
||||
}
|
||||
|
||||
@(export)
|
||||
game_force_reload :: proc() -> bool {
|
||||
return rl.IsKeyPressed(.F5)
|
||||
}
|
||||
|
||||
@(export)
|
||||
game_force_restart :: proc() -> bool {
|
||||
return rl.IsKeyPressed(.F6)
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
// This implements simple animations using sprite sheets. The texture in the
|
||||
// `Animation` struct is assumed to contain a horizontal strip of the frames
|
||||
// in the animation. Call `animation_update` to update and then call
|
||||
// `animation_rect` when you wish to know the source rect to use in the texture
|
||||
// With the source rect you can run rl.DrawTextureRec to draw the current frame.
|
||||
|
||||
package utils
|
||||
|
||||
import "core:log"
|
||||
|
||||
Animation :: struct {
|
||||
texture: Texture,
|
||||
num_frames: int,
|
||||
current_frame: int,
|
||||
frame_timer: f32,
|
||||
frame_length: f32,
|
||||
}
|
||||
|
||||
animation_create :: proc(tex: Texture, num_frames: int, frame_length: f32) -> Animation {
|
||||
return(
|
||||
Animation {
|
||||
texture = tex,
|
||||
num_frames = num_frames,
|
||||
frame_length = frame_length,
|
||||
frame_timer = frame_length,
|
||||
} \
|
||||
)
|
||||
}
|
||||
|
||||
animation_update :: proc(a: ^Animation, dt: f32) {
|
||||
a.frame_timer -= dt
|
||||
|
||||
if a.frame_timer <= 0 {
|
||||
a.frame_timer = a.frame_length + a.frame_timer
|
||||
a.current_frame += 1
|
||||
|
||||
if a.current_frame >= a.num_frames {
|
||||
a.current_frame = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animation_rect :: proc(a: Animation) -> Rect {
|
||||
if a.num_frames == 0 {
|
||||
log.error("Animation has zero frames")
|
||||
return RectEmpty
|
||||
}
|
||||
|
||||
w := f32(a.texture.width) / f32(a.num_frames)
|
||||
h := f32(a.texture.height)
|
||||
|
||||
return {x = f32(a.current_frame) * w, y = 0, width = w, height = h}
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
// This handle-based array gives you a statically allocated array where you can
|
||||
// use index based handles instead of pointers. The handles have a generation
|
||||
// that makes sure you don't get bugs when slots are re-used.
|
||||
// Read more about it here: https://floooh.github.io/2018/06/17/handles-vs-pointers.html */
|
||||
|
||||
package utils
|
||||
|
||||
Handle :: struct($T: typeid) {
|
||||
// idx 0 means unused. Note that slot 0 is a dummy slot, it can never be used.
|
||||
idx: u32,
|
||||
gen: u32,
|
||||
}
|
||||
|
||||
HandleArrayItem :: struct($T: typeid) {
|
||||
item: T,
|
||||
handle: Handle(T),
|
||||
}
|
||||
|
||||
// TODO: Add a freelist that uses some kind of bit array... We should be able to
|
||||
// check 64 item slots at a time that way, but without any dynamic array.
|
||||
HandleArray :: struct($T: typeid, $N: int) {
|
||||
items: #soa[N]HandleArrayItem(T),
|
||||
num_items: u32,
|
||||
}
|
||||
|
||||
ha_add :: proc(a: ^HandleArray($T, $N), v: T) -> (Handle(T), bool) #optional_ok {
|
||||
for idx in 1 ..< a.num_items {
|
||||
i := &a.items[idx]
|
||||
|
||||
if idx != 0 && i.handle.idx == 0 {
|
||||
i.handle.idx = u32(idx)
|
||||
i.item = v
|
||||
return i.handle, true
|
||||
}
|
||||
}
|
||||
|
||||
// Index 0 is dummy
|
||||
if a.num_items == 0 {
|
||||
a.num_items += 1
|
||||
}
|
||||
|
||||
if a.num_items == len(a.items) {
|
||||
return {}, false
|
||||
}
|
||||
|
||||
idx := a.num_items
|
||||
i := &a.items[a.num_items]
|
||||
a.num_items += 1
|
||||
i.handle.idx = idx
|
||||
i.handle.gen = 1
|
||||
i.item = v
|
||||
return i.handle, true
|
||||
}
|
||||
|
||||
ha_get :: proc(a: HandleArray($T, $N), h: Handle(T)) -> (T, bool) {
|
||||
if h.idx == 0 {
|
||||
return {}, false
|
||||
}
|
||||
|
||||
if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h {
|
||||
return a.items[h.idx].item, true
|
||||
}
|
||||
|
||||
return {}, false
|
||||
}
|
||||
|
||||
ha_get_ptr :: proc(a: HandleArray($T, $N), h: Handle(T)) -> ^T {
|
||||
if h.idx == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h {
|
||||
return &ha.items[h.idx].item
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
ha_remove :: proc(a: ^HandleArray($T, $N), h: Handle(T)) {
|
||||
if h.idx == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h {
|
||||
a.items[h.idx].handle.idx = 0
|
||||
a.items[h.idx].handle.gen += 1
|
||||
}
|
||||
}
|
||||
|
||||
ha_valid :: proc(a: HandleArray($T, $N), h: Handle(T)) -> bool {
|
||||
if h.idx == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h
|
||||
}
|
||||
|
||||
HandleArrayIter :: struct($T: typeid, $N: int) {
|
||||
a: ^HandleArray(T, N),
|
||||
index: int,
|
||||
}
|
||||
|
||||
ha_make_iter :: proc(a: ^HandleArray($T, $N)) -> HandleArrayIter(T, N) {
|
||||
return HandleArrayIter(T, N){a = a}
|
||||
}
|
||||
|
||||
ha_iter :: proc(it: ^HandleArrayIter($T, $N)) -> (val: T, h: Handle(T), cond: bool) {
|
||||
cond = it.index < int(it.a.num_items)
|
||||
|
||||
for ; cond; cond = it.index < int(it.a.num_items) {
|
||||
if it.a.items[it.index].handle.idx == 0 {
|
||||
it.index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
val = it.a.items[it.index].item
|
||||
h = it.a.items[it.index].handle
|
||||
it.index += 1
|
||||
break
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ha_iter_ptr :: proc(it: ^HandleArrayIter($T, $N)) -> (val: ^T, h: Handle(T), cond: bool) {
|
||||
cond = it.index < int(it.a.num_items)
|
||||
|
||||
for ; cond; cond = it.index < int(it.a.num_items) {
|
||||
if it.a.items[it.index].handle.idx == 0 {
|
||||
it.index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
val = &it.a.items[it.index].item
|
||||
h = it.a.items[it.index].handle
|
||||
it.index += 1
|
||||
break
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// generic odin helpers
|
||||
|
||||
package utils
|
||||
|
||||
import "core:intrinsics"
|
||||
import "core:reflect"
|
||||
import "core:strings"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Texture :: rl.Texture
|
||||
Color :: rl.Color
|
||||
|
||||
Rect :: rl.Rectangle
|
||||
RectEmpty :: Rect{}
|
||||
|
||||
increase_or_wrap_enum :: proc(e: $T) -> T {
|
||||
ei := int(e) + 1
|
||||
|
||||
if ei >= len(T) {
|
||||
ei = 0
|
||||
}
|
||||
|
||||
return T(ei)
|
||||
}
|
||||
|
||||
union_type :: proc(a: any) -> typeid {
|
||||
return reflect.union_variant_typeid(a)
|
||||
}
|
||||
|
||||
temp_cstring :: proc(s: string) -> cstring {
|
||||
return strings.clone_to_cstring(s, context.temp_allocator)
|
||||
}
|
||||
|
||||
// There is a remap in core:math but it doesn't clamp in the new range, which I
|
||||
// always want.
|
||||
remap :: proc "contextless" (
|
||||
old_value, old_min, old_max, new_min, new_max: $T,
|
||||
) -> (
|
||||
x: T,
|
||||
) where intrinsics.type_is_numeric(T),
|
||||
!intrinsics.type_is_array(T) {
|
||||
old_range := old_max - old_min
|
||||
new_range := new_max - new_min
|
||||
if old_range == 0 {
|
||||
return new_range / 2
|
||||
}
|
||||
return clamp(((old_value - old_min) / old_range) * new_range + new_min, new_min, new_max)
|
||||
}
|
||||
|
||||
Vec2i :: [2]int
|
||||
Vec2 :: [2]f32
|
||||
|
||||
vec2_from_vec2i :: proc(p: Vec2i) -> Vec2 {
|
||||
return {f32(p.x), f32(p.y)}
|
||||
}
|
||||
296
vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin
vendored
Normal file
296
vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin
vendored
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
package main
|
||||
|
||||
import clay "../clay-odin"
|
||||
import "base:runtime"
|
||||
import "core:math"
|
||||
import "core:strings"
|
||||
import "core:unicode/utf8"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Raylib_Font :: struct {
|
||||
fontId: u16,
|
||||
font: rl.Font,
|
||||
}
|
||||
|
||||
clay_color_to_rl_color :: proc(color: clay.Color) -> rl.Color {
|
||||
return {u8(color.r), u8(color.g), u8(color.b), u8(color.a)}
|
||||
}
|
||||
|
||||
raylib_fonts := [dynamic]Raylib_Font{}
|
||||
|
||||
// Alias for compatibility, default to ascii support
|
||||
measure_text :: measure_text_ascii
|
||||
|
||||
measure_text_unicode :: proc "c" (
|
||||
text: clay.StringSlice,
|
||||
config: ^clay.TextElementConfig,
|
||||
userData: rawptr,
|
||||
) -> clay.Dimensions {
|
||||
// Needed for grapheme_count
|
||||
context = runtime.default_context()
|
||||
|
||||
line_width: f32 = 0
|
||||
|
||||
font := raylib_fonts[config.fontId].font
|
||||
text_str := string(text.chars[:text.length])
|
||||
|
||||
// This function seems somewhat expensive, if you notice performance issues, you could assume
|
||||
// - 1 codepoint per visual character (no grapheme clusters), where you can get the length from the loop
|
||||
// - 1 byte per visual character (ascii), where you can get the length with `text.length`
|
||||
// see `measure_text_ascii`
|
||||
grapheme_count, _, _ := utf8.grapheme_count(text_str)
|
||||
|
||||
for letter, byte_idx in text_str {
|
||||
glyph_index := rl.GetGlyphIndex(font, letter)
|
||||
|
||||
glyph := font.glyphs[glyph_index]
|
||||
|
||||
if glyph.advanceX != 0 {
|
||||
line_width += f32(glyph.advanceX)
|
||||
} else {
|
||||
line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX)
|
||||
}
|
||||
}
|
||||
|
||||
scaleFactor := f32(config.fontSize) / f32(font.baseSize)
|
||||
|
||||
// Note:
|
||||
// I'd expect this to be `grapheme_count - 1`,
|
||||
// but that seems to be one letterSpacing too small
|
||||
// maybe that's a raylib bug, maybe that's Clay?
|
||||
total_spacing := f32(grapheme_count) * f32(config.letterSpacing)
|
||||
|
||||
return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)}
|
||||
}
|
||||
|
||||
measure_text_ascii :: proc "c" (
|
||||
text: clay.StringSlice,
|
||||
config: ^clay.TextElementConfig,
|
||||
userData: rawptr,
|
||||
) -> clay.Dimensions {
|
||||
line_width: f32 = 0
|
||||
|
||||
font := raylib_fonts[config.fontId].font
|
||||
text_str := string(text.chars[:text.length])
|
||||
|
||||
for i in 0 ..< len(text_str) {
|
||||
glyph_index := text_str[i] - 32
|
||||
|
||||
glyph := font.glyphs[glyph_index]
|
||||
|
||||
if glyph.advanceX != 0 {
|
||||
line_width += f32(glyph.advanceX)
|
||||
} else {
|
||||
line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX)
|
||||
}
|
||||
}
|
||||
|
||||
scaleFactor := f32(config.fontSize) / f32(font.baseSize)
|
||||
|
||||
// Note:
|
||||
// I'd expect this to be `len(text_str) - 1`,
|
||||
// but that seems to be one letterSpacing too small
|
||||
// maybe that's a raylib bug, maybe that's Clay?
|
||||
total_spacing := f32(len(text_str)) * f32(config.letterSpacing)
|
||||
|
||||
return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)}
|
||||
}
|
||||
|
||||
clay_raylib_render :: proc(
|
||||
render_commands: ^clay.ClayArray(clay.RenderCommand),
|
||||
allocator := context.temp_allocator,
|
||||
) {
|
||||
for i in 0 ..< render_commands.length {
|
||||
render_command := clay.RenderCommandArray_Get(render_commands, i)
|
||||
bounds := render_command.boundingBox
|
||||
|
||||
switch render_command.commandType {
|
||||
case .None: // None
|
||||
case .Text:
|
||||
config := render_command.renderData.text
|
||||
|
||||
text := string(config.stringContents.chars[:config.stringContents.length])
|
||||
|
||||
// Raylib uses C strings instead of Odin strings, so we need to clone
|
||||
// Assume this will be freed elsewhere since we default to the temp allocator
|
||||
cstr_text := strings.clone_to_cstring(text, allocator)
|
||||
|
||||
font := raylib_fonts[config.fontId].font
|
||||
rl.DrawTextEx(
|
||||
font,
|
||||
cstr_text,
|
||||
{bounds.x, bounds.y},
|
||||
f32(config.fontSize),
|
||||
f32(config.letterSpacing),
|
||||
clay_color_to_rl_color(config.textColor),
|
||||
)
|
||||
case .Image:
|
||||
config := render_command.renderData.image
|
||||
tint := config.backgroundColor
|
||||
if tint == 0 {
|
||||
tint = {255, 255, 255, 255}
|
||||
}
|
||||
|
||||
imageTexture := (^rl.Texture2D)(config.imageData)
|
||||
rl.DrawTextureEx(
|
||||
imageTexture^,
|
||||
{bounds.x, bounds.y},
|
||||
0,
|
||||
bounds.width / f32(imageTexture.width),
|
||||
clay_color_to_rl_color(tint),
|
||||
)
|
||||
case .ScissorStart:
|
||||
rl.BeginScissorMode(
|
||||
i32(math.round(bounds.x)),
|
||||
i32(math.round(bounds.y)),
|
||||
i32(math.round(bounds.width)),
|
||||
i32(math.round(bounds.height)),
|
||||
)
|
||||
case .ScissorEnd:
|
||||
rl.EndScissorMode()
|
||||
case .Rectangle:
|
||||
config := render_command.renderData.rectangle
|
||||
if config.cornerRadius.topLeft > 0 {
|
||||
radius: f32 = (config.cornerRadius.topLeft * 2) / min(bounds.width, bounds.height)
|
||||
draw_rect_rounded(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
radius,
|
||||
config.backgroundColor,
|
||||
)
|
||||
} else {
|
||||
draw_rect(bounds.x, bounds.y, bounds.width, bounds.height, config.backgroundColor)
|
||||
}
|
||||
case .Border:
|
||||
config := render_command.renderData.border
|
||||
// Left border
|
||||
if config.width.left > 0 {
|
||||
draw_rect(
|
||||
bounds.x,
|
||||
bounds.y + config.cornerRadius.topLeft,
|
||||
f32(config.width.left),
|
||||
bounds.height - config.cornerRadius.topLeft - config.cornerRadius.bottomLeft,
|
||||
config.color,
|
||||
)
|
||||
}
|
||||
// Right border
|
||||
if config.width.right > 0 {
|
||||
draw_rect(
|
||||
bounds.x + bounds.width - f32(config.width.right),
|
||||
bounds.y + config.cornerRadius.topRight,
|
||||
f32(config.width.right),
|
||||
bounds.height - config.cornerRadius.topRight - config.cornerRadius.bottomRight,
|
||||
config.color,
|
||||
)
|
||||
}
|
||||
// Top border
|
||||
if config.width.top > 0 {
|
||||
draw_rect(
|
||||
bounds.x + config.cornerRadius.topLeft,
|
||||
bounds.y,
|
||||
bounds.width - config.cornerRadius.topLeft - config.cornerRadius.topRight,
|
||||
f32(config.width.top),
|
||||
config.color,
|
||||
)
|
||||
}
|
||||
// Bottom border
|
||||
if config.width.bottom > 0 {
|
||||
draw_rect(
|
||||
bounds.x + config.cornerRadius.bottomLeft,
|
||||
bounds.y + bounds.height - f32(config.width.bottom),
|
||||
bounds.width -
|
||||
config.cornerRadius.bottomLeft -
|
||||
config.cornerRadius.bottomRight,
|
||||
f32(config.width.bottom),
|
||||
config.color,
|
||||
)
|
||||
}
|
||||
|
||||
// Rounded Borders
|
||||
if config.cornerRadius.topLeft > 0 {
|
||||
draw_arc(
|
||||
bounds.x + config.cornerRadius.topLeft,
|
||||
bounds.y + config.cornerRadius.topLeft,
|
||||
config.cornerRadius.topLeft - f32(config.width.top),
|
||||
config.cornerRadius.topLeft,
|
||||
180,
|
||||
270,
|
||||
config.color,
|
||||
)
|
||||
}
|
||||
if config.cornerRadius.topRight > 0 {
|
||||
draw_arc(
|
||||
bounds.x + bounds.width - config.cornerRadius.topRight,
|
||||
bounds.y + config.cornerRadius.topRight,
|
||||
config.cornerRadius.topRight - f32(config.width.top),
|
||||
config.cornerRadius.topRight,
|
||||
270,
|
||||
360,
|
||||
config.color,
|
||||
)
|
||||
}
|
||||
if config.cornerRadius.bottomLeft > 0 {
|
||||
draw_arc(
|
||||
bounds.x + config.cornerRadius.bottomLeft,
|
||||
bounds.y + bounds.height - config.cornerRadius.bottomLeft,
|
||||
config.cornerRadius.bottomLeft - f32(config.width.top),
|
||||
config.cornerRadius.bottomLeft,
|
||||
90,
|
||||
180,
|
||||
config.color,
|
||||
)
|
||||
}
|
||||
if config.cornerRadius.bottomRight > 0 {
|
||||
draw_arc(
|
||||
bounds.x + bounds.width - config.cornerRadius.bottomRight,
|
||||
bounds.y + bounds.height - config.cornerRadius.bottomRight,
|
||||
config.cornerRadius.bottomRight - f32(config.width.bottom),
|
||||
config.cornerRadius.bottomRight,
|
||||
0.1,
|
||||
90,
|
||||
config.color,
|
||||
)
|
||||
}
|
||||
case clay.RenderCommandType.Custom:
|
||||
// Implement custom element rendering here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper procs, mainly for repeated conversions
|
||||
|
||||
@(private = "file")
|
||||
draw_arc :: proc(
|
||||
x, y: f32,
|
||||
inner_rad, outer_rad: f32,
|
||||
start_angle, end_angle: f32,
|
||||
color: clay.Color,
|
||||
) {
|
||||
rl.DrawRing(
|
||||
{math.round(x), math.round(y)},
|
||||
math.round(inner_rad),
|
||||
outer_rad,
|
||||
start_angle,
|
||||
end_angle,
|
||||
10,
|
||||
clay_color_to_rl_color(color),
|
||||
)
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
draw_rect :: proc(x, y, w, h: f32, color: clay.Color) {
|
||||
rl.DrawRectangle(
|
||||
i32(math.round(x)),
|
||||
i32(math.round(y)),
|
||||
i32(math.round(w)),
|
||||
i32(math.round(h)),
|
||||
clay_color_to_rl_color(color),
|
||||
)
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
draw_rect_rounded :: proc(x, y, w, h: f32, radius: f32, color: clay.Color) {
|
||||
rl.DrawRectangleRounded({x, y, w, h}, radius, 8, clay_color_to_rl_color(color))
|
||||
}
|
||||
6
src/dialog/build.sh → vendors/dialog/build.sh
vendored
Executable file → Normal file
6
src/dialog/build.sh → vendors/dialog/build.sh
vendored
Executable file → Normal file
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -e
|
||||
|
||||
gcc ./libtinyfiledialogs/tinyfiledialogs.c -c -o libtinyfiledialogs.o
|
||||
|
||||
ar rcs libtinyfiledialogs.a libtinyfiledialogs.o
|
||||
|
||||
rm libtinyfiledialogs.o
|
||||
161
vendors/dialog/tinyfiledialogs.odin
vendored
Normal file
161
vendors/dialog/tinyfiledialogs.odin
vendored
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package tinyfiledialogs
|
||||
|
||||
import "core:c"
|
||||
import "core:mem"
|
||||
import "core:strings"
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
foreign import lib {"tinyfiledialogs.lib", "system:comdlg32.lib", "system:Ole32.lib"}
|
||||
} else when ODIN_OS == .Linux || ODIN_OS == .Darwin {
|
||||
foreign import lib "libtinyfiledialogs.a"
|
||||
}
|
||||
|
||||
@(default_calling_convention = "c", link_prefix = "tinyfd_")
|
||||
foreign lib {
|
||||
notifyPopup :: proc(title, message, icon_type: cstring) -> c.int ---
|
||||
|
||||
messageBox :: proc(title, message, dialog_type, icon_type: cstring, default_button: c.int) -> c.int ---
|
||||
inputBox :: proc(title, message, default_input: cstring) -> cstring ---
|
||||
|
||||
saveFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring) -> cstring ---
|
||||
openFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring, allow_multi: c.int) -> cstring ---
|
||||
|
||||
selectFolderDialog :: proc(title, default_path: cstring) -> cstring ---
|
||||
|
||||
colorChooser :: proc(title, default_hex_rgb: cstring, default_rgb, result_rgb: [3]byte) -> cstring ---
|
||||
}
|
||||
|
||||
select_folder_dialog :: proc(
|
||||
title, default_path: string,
|
||||
alloc := context.allocator,
|
||||
temp_alloc := context.temp_allocator,
|
||||
) -> (
|
||||
path: string,
|
||||
success: bool,
|
||||
) {
|
||||
ctitle: cstring = nil
|
||||
cdefault_path: cstring = nil
|
||||
err: mem.Allocator_Error
|
||||
|
||||
if len(title) > 0 {
|
||||
ctitle, err = strings.clone_to_cstring(title, temp_alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
}
|
||||
if len(default_path) > 0 {
|
||||
cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
}
|
||||
res := selectFolderDialog(ctitle, cdefault_path)
|
||||
path, err = strings.clone_from_cstring(res, alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
save_file_dialog :: proc(
|
||||
title, default_path: string,
|
||||
pattern_count: i32,
|
||||
patterns: []string,
|
||||
file_desc: string,
|
||||
alloc := context.allocator,
|
||||
temp_alloc := context.temp_allocator,
|
||||
) -> (
|
||||
path: string,
|
||||
success: bool,
|
||||
) {
|
||||
ctitle: cstring = nil
|
||||
cdefault_path: cstring = nil
|
||||
cfile_desc: cstring = nil
|
||||
cpatterns: [^]cstring = nil
|
||||
err: mem.Allocator_Error
|
||||
|
||||
if len(title) > 0 {
|
||||
ctitle, err = strings.clone_to_cstring(title, temp_alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
}
|
||||
if len(default_path) > 0 {
|
||||
cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
}
|
||||
if len(cfile_desc) > 0 {
|
||||
cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
}
|
||||
|
||||
if pattern_count > 0 {
|
||||
cpatterns = make([^]cstring, pattern_count + 1, temp_alloc)
|
||||
for p, i in patterns {
|
||||
cpatterns[i] = strings.clone_to_cstring(p)
|
||||
}
|
||||
cpatterns[pattern_count] = nil // null terminate the array
|
||||
}
|
||||
res := saveFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc)
|
||||
path, err = strings.clone_from_cstring(res, alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
open_file_dialog :: proc(
|
||||
title, default_path: string,
|
||||
pattern_count: i32,
|
||||
patterns: []string,
|
||||
file_desc: string,
|
||||
allow_multi: i32,
|
||||
alloc := context.allocator,
|
||||
temp_alloc := context.temp_allocator,
|
||||
) -> (
|
||||
path: string,
|
||||
success: bool,
|
||||
) {
|
||||
ctitle: cstring = nil
|
||||
cdefault_path: cstring = nil
|
||||
cfile_desc: cstring = nil
|
||||
cpatterns: [^]cstring = nil
|
||||
err: mem.Allocator_Error
|
||||
|
||||
if len(title) > 0 {
|
||||
ctitle, err = strings.clone_to_cstring(title, temp_alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
}
|
||||
if len(default_path) > 0 {
|
||||
cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
}
|
||||
if len(cfile_desc) > 0 {
|
||||
cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
}
|
||||
|
||||
if pattern_count > 0 {
|
||||
cpatterns = make([^]cstring, pattern_count + 1, temp_alloc)
|
||||
for p, i in patterns {
|
||||
cpatterns[i] = strings.clone_to_cstring(p)
|
||||
}
|
||||
cpatterns[pattern_count] = nil // null terminate the array
|
||||
}
|
||||
res := openFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc, allow_multi)
|
||||
path, err = strings.clone_from_cstring(res, alloc)
|
||||
if err != nil {
|
||||
return {}, false
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
1
vendors/odin-aseprite
vendored
Submodule
1
vendors/odin-aseprite
vendored
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 72ea2e8094a5f05074d4c4f2faafdba42e54673c
|
||||
Loading…
Add table
Add a link
Reference in a new issue