Compare commits
No commits in common. "3c4ad6805909404e4aebc9c729ec3a4c8db974a9" and "84db74586b4b24837d8754c2448033456c6120fc" have entirely different histories.
3c4ad68059
...
84db74586b
46 changed files with 1760 additions and 1407 deletions
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -1,11 +1,18 @@
|
|||
*.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,3 +1,6 @@
|
|||
[submodule "vendors/odin-aseprite"]
|
||||
path = vendors/odin-aseprite
|
||||
url = git@github.com:blob1807/odin-aseprite.git
|
||||
[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
|
||||
|
|
|
|||
31
.vscode/launch.json
vendored
Normal file
31
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"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
Normal file
6
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"[odin]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 4
|
||||
}
|
||||
}
|
||||
92
.vscode/tasks.json
vendored
Normal file
92
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"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,31 +1,18 @@
|
|||
|
||||
# YAAP
|
||||
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.
|
||||
Yet-Another-Atlas-Packer by Stefan Stefanov
|
||||
|
||||
## 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 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.
|
||||
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.
|
||||
|
||||
Uses `stb_rect_pack` for the image packing & `raylib` for the UI.
|
||||
<a href="https://youtu.be/4_dKq7G57Lw">
|
||||
<img src="https://raw.githubusercontent.com/bersK/yaap/master/repo_assets/image.png" />
|
||||
<a/>
|
||||
|
||||
A quick preview on [youtube](https://youtu.be/4_dKq7G57Lw).
|
||||
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).
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bersK/yaap/master/repo_assets/image.png" />
|
||||
I'm using a library for marshalling the aseprite files found [here](https://github.com/blob1807/odin-aseprite) 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))
|
||||
Project template provided by Karl Zylinski found [here](https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template) on github.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
|
@ -1,5 +0,0 @@
|
|||
FLAGS="-define:RAYLIB_SHARED=true -out:build/yaap -debug"
|
||||
SRC=src/frontend
|
||||
|
||||
mkdir -p build
|
||||
odin build ${SRC} ${FLAGS}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@echo off
|
||||
odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap-debug.exe -debug
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@echo off
|
||||
odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap.exe -o:speed
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@echo off
|
||||
odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli-debug.exe -debug
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@echo off
|
||||
odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli.exe -o:speed
|
||||
2
scripts/build_debug.bat
Normal file
2
scripts/build_debug.bat
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_debug.exe -debug
|
||||
3
scripts/build_debug.sh
Executable file
3
scripts/build_debug.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
odin build src/main_release -out:build/game_debug.bin -no-bounds-check -debug
|
||||
2
scripts/build_generator_debug.bat
Normal file
2
scripts/build_generator_debug.bat
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
odin build src/aseprite_odin_generator -define:RAYLIB_SHARED=true -out:build_generator/aseprite_odin_generator.exe -debug
|
||||
21
scripts/build_hot_reload.bat
Normal file
21
scripts/build_hot_reload.bat
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@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
|
||||
47
scripts/build_hot_reload.sh
Executable file
47
scripts/build_hot_reload.sh
Executable file
|
|
@ -0,0 +1,47 @@
|
|||
#!/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
|
||||
2
scripts/build_release.bat
Normal file
2
scripts/build_release.bat
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@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
|
||||
3
scripts/build_release.sh
Normal file
3
scripts/build_release.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/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
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
@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
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
#!/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
src/aseprite
Submodule
1
src/aseprite
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 628e655661d822fecae67cf238cbfa414912d943
|
||||
|
|
@ -1,29 +1,42 @@
|
|||
package cli
|
||||
package generator
|
||||
|
||||
import ase "../../vendors/odin-aseprite"
|
||||
import ase "../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 "../src/generator"
|
||||
import gen ".."
|
||||
import utils "../utils"
|
||||
|
||||
ATLAS_SIZE :: 512
|
||||
IMPORT_PATH :: "./example.aseprite"
|
||||
EXPORT_PATH :: "./atlas.png"
|
||||
IMPORT_PATH :: "./src/aseprite_odin_generator/big.aseprite"
|
||||
EXPORT_PATH :: "./src/aseprite_odin_generator/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!")
|
||||
}
|
||||
|
||||
target_dir := os.get_current_directory()
|
||||
cwd := os.get_current_directory()
|
||||
target_dir := s.concatenate({cwd, "\\src\\aseprite_odin_generator\\"})
|
||||
|
||||
atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK)
|
||||
atlas_entries: [dynamic]gen.AtlasEntry = make([dynamic]gen.AtlasEntry)
|
||||
|
|
@ -32,10 +45,13 @@ main :: proc() {
|
|||
metadata := gen.pack_atlas_entries(atlas_entries[:], &atlas, 10, 10)
|
||||
|
||||
json_bytes, jerr := json.marshal(metadata)
|
||||
os.write_entire_file("./metadata.json", json_bytes)
|
||||
os.write_entire_file("src/aseprite_odin_generator/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("./output.odin", transmute([]byte)odin_output_str)
|
||||
os.write_entire_file(
|
||||
"src/aseprite_odin_generator/output.odino",
|
||||
transmute([]byte)odin_output_str,
|
||||
)
|
||||
|
||||
rl.ExportImage(atlas, EXPORT_PATH)
|
||||
}
|
||||
BIN
src/aseprite_odin_generator/atlas.png
Normal file
BIN
src/aseprite_odin_generator/atlas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
1
src/aseprite_odin_generator/metadata.json
Normal file
1
src/aseprite_odin_generator/metadata.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
[{"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]}]
|
||||
6
vendors/dialog/build.sh → src/dialog/build.sh
Normal file → Executable file
6
vendors/dialog/build.sh → src/dialog/build.sh
Normal file → Executable file
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
set -e
|
||||
#!/usr/bin/env bash
|
||||
|
||||
gcc ./libtinyfiledialogs/tinyfiledialogs.c -c -o libtinyfiledialogs.o
|
||||
|
||||
ar rcs libtinyfiledialogs.a libtinyfiledialogs.o
|
||||
|
||||
rm libtinyfiledialogs.o
|
||||
32
src/dialog/tinyfiledialog.odin
Normal file
32
src/dialog/tinyfiledialog.odin
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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 ---
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,520 +0,0 @@
|
|||
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
Normal file
508
src/game.odin
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
// 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,9 +1,8 @@
|
|||
package generator
|
||||
package game
|
||||
|
||||
import ase "../../vendors/odin-aseprite"
|
||||
import ase "./aseprite"
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:log"
|
||||
import "core:os"
|
||||
import fp "core:path/filepath"
|
||||
import "core:slice"
|
||||
|
|
@ -11,12 +10,15 @@ 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,
|
||||
|
|
@ -38,6 +40,228 @@ 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,
|
||||
|
|
@ -67,226 +291,6 @@ 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",
|
||||
|
|
@ -306,7 +310,7 @@ odin_source_generator_metadata := SourceCodeGeneratorMetadata {
|
|||
},
|
||||
array_data = {
|
||||
name = "ATLAS_SPRITES",
|
||||
type = "[AtlasSprite]AtlasRect",
|
||||
type = "[]AtlasRect",
|
||||
begin_line = "%v := %v {{\n",
|
||||
entry_line = "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n",
|
||||
end_line = "}\n\n",
|
||||
|
|
@ -367,7 +371,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 := [AtlasSprite]AtlasRect {\n")
|
||||
strings.write_string(&sb, "ATLAS_SPRITES := []AtlasRect {\n")
|
||||
{
|
||||
entry: string
|
||||
for cell in metadata {
|
||||
|
|
@ -385,7 +389,7 @@ generate_odin_enums_and_atlas_offsets_file_sb :: proc(
|
|||
// end offsets array
|
||||
strings.write_string(&sb, "}\n\n")
|
||||
|
||||
log.info("\n", strings.to_string(sb))
|
||||
fmt.println("\n", strings.to_string(sb))
|
||||
|
||||
return sb
|
||||
}
|
||||
|
|
@ -401,21 +405,26 @@ 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(
|
||||
|
|
@ -439,69 +448,58 @@ 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)
|
||||
|
||||
log.info("\n", strings.to_string(sb))
|
||||
fmt.println("\n", strings.to_string(sb))
|
||||
|
||||
return sb
|
||||
|
||||
}
|
||||
|
||||
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!")
|
||||
save_output :: proc() {
|
||||
output_path, ok := g_mem.output_folder_path.(string)
|
||||
if !ok || output_path == "" {
|
||||
fmt.println("Output path is empty!")
|
||||
return
|
||||
}
|
||||
|
||||
image := rl.LoadImageFromTexture(render_texture_target.texture)
|
||||
image := rl.LoadImageFromTexture(g_mem.atlas_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 len(metadata) > 0 {
|
||||
log.info("Building metadata...")
|
||||
if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok {
|
||||
fmt.println("Building metadata...")
|
||||
if json_metadata, jok := json.marshal(metadata); jok == nil {
|
||||
os.write_entire_file(
|
||||
strings.concatenate(
|
||||
{output_path, OS_FILE_SEPARATOR, "metadata.json"},
|
||||
context.temp_allocator,
|
||||
),
|
||||
strings.concatenate({output_path, os_file_separator, "metadata.json"}),
|
||||
json_metadata,
|
||||
)
|
||||
delete(json_metadata)
|
||||
} else {
|
||||
log.error("Failed to marshall the atlas metadata to a json!")
|
||||
fmt.println("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"},
|
||||
context.temp_allocator,
|
||||
),
|
||||
strings.concatenate({output_path, os_file_separator, "metadata.odin"}),
|
||||
transmute([]byte)odin_metadata,
|
||||
)
|
||||
if !ok {
|
||||
log.error("Failed to save 'metadata.odin'")
|
||||
fmt.println("Failed to save 'metadata.odin'")
|
||||
}
|
||||
} else {
|
||||
log.error("No metadata to export!")
|
||||
fmt.println("No metadata to export!")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -511,31 +509,30 @@ 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 {
|
||||
log.error("Neither a json file name or a source code filename has been provided!")
|
||||
fmt.println("Neither a json file name or a source code filename has been provided!")
|
||||
return
|
||||
}
|
||||
|
||||
metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); if !ok {
|
||||
log.error("No metadata to export!")
|
||||
metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata);if !ok {
|
||||
fmt.println("No metadata to export!")
|
||||
}
|
||||
|
||||
log.info("Building metadata...")
|
||||
fmt.println("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 {
|
||||
log.errorf("Failed to write json to file: ", json_output_path)
|
||||
fmt.println("Failed to write json to file: ", json_output_path)
|
||||
}
|
||||
} else {
|
||||
log.error("Failed to marshall the atlas metadata to a json!")
|
||||
fmt.println("Failed to marshall the atlas metadata to a json!")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -544,50 +541,48 @@ 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 {
|
||||
log.errorf("Failed to save source code to file:", source_output_path)
|
||||
fmt.println("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 {
|
||||
log.errorf("Failed to save source code to file:", source_output_path)
|
||||
fmt.println("Failed to save source code to file:", source_output_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
save_metadata :: proc(
|
||||
settings: CLIPackerSettings,
|
||||
settings: utils.CLIPackerSettings,
|
||||
atlas_entries: []AtlasEntry,
|
||||
atlas_metadata: []SpriteAtlasMetadata,
|
||||
) {
|
||||
metadata, ok := settings.metadata.(CLIMetadataSettings); if !ok do return
|
||||
metadata, ok := settings.metadata.(utils.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 {
|
||||
log.error("Failed to marshall metadata")
|
||||
fmt.println("Failed to marshall metadata")
|
||||
}
|
||||
}
|
||||
if source_code_path, ok := metadata.source_code_path.(string); ok {
|
||||
63
src/globals.odin
Normal file
63
src/globals.odin
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
|
||||
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),
|
||||
}
|
||||
196
src/main_hot_reload/main_hot_reload.odin
Normal file
196
src/main_hot_reload/main_hot_reload.odin
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// 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
|
||||
77
src/main_release/main_release.odin
Normal file
77
src/main_release/main_release.odin
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// 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
|
||||
88
src/symbol_exports.odin
Normal file
88
src/symbol_exports.odin
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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)
|
||||
}
|
||||
53
src/utils/animation.odin
Normal file
53
src/utils/animation.odin
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// 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,11 +1,8 @@
|
|||
package generator
|
||||
package utils
|
||||
|
||||
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,
|
||||
|
|
@ -21,7 +18,7 @@ CLIFlagType :: enum {
|
|||
CLI_FLAG_STRINGS := [CLIFlagType][]string {
|
||||
.Unknown = {""},
|
||||
.Help = {"h", "help"},
|
||||
.InputFiles = {"i", "input-files"},
|
||||
.InputFiles = {"f", "input-files"},
|
||||
.InputFolder = {"d", "input-directory"},
|
||||
.OutputFolder = {"o", "out"},
|
||||
.EnableMetadataOutput = {"m", "export-metadata"},
|
||||
|
|
@ -33,13 +30,13 @@ CLI_FLAG_STRINGS := [CLIFlagType][]string {
|
|||
CLI_FLAG_DESCRIPTIONS := [CLIFlagType]string {
|
||||
.Unknown = "Invalid flag",
|
||||
.Help = "Prints the help message... hello!",
|
||||
.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.",
|
||||
.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.",
|
||||
.EnableMetadataOutput = "Whether or not to export metadata (JSON or source files with the offsets for the packer sprites in the atlas)",
|
||||
.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",
|
||||
.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",
|
||||
}
|
||||
|
||||
CLIOutputSettings :: struct {
|
||||
|
|
@ -86,9 +83,12 @@ print_help :: proc() {
|
|||
for flag in CLIFlagType {
|
||||
if flag == .Unknown do continue
|
||||
|
||||
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)
|
||||
fmt.printfln(
|
||||
"Flag: -%v,%v \t -- %v",
|
||||
CLI_FLAG_STRINGS[flag][0],
|
||||
CLI_FLAG_STRINGS[flag][1],
|
||||
CLI_FLAG_DESCRIPTIONS[flag],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
141
src/utils/handle_array.odin
Normal file
141
src/utils/handle_array.odin
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// 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
|
||||
}
|
||||
55
src/utils/helpers.odin
Normal file
55
src/utils/helpers.odin
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// 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)}
|
||||
}
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
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))
|
||||
}
|
||||
161
vendors/dialog/tinyfiledialogs.odin
vendored
161
vendors/dialog/tinyfiledialogs.odin
vendored
|
|
@ -1,161 +0,0 @@
|
|||
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
1
vendors/odin-aseprite
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 72ea2e8094a5f05074d4c4f2faafdba42e54673c
|
||||
Loading…
Add table
Add a link
Reference in a new issue