diff --git a/.gitignore b/.gitignore
index 5cb0a1d..91dc2bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/.gitmodules b/.gitmodules
index 6e5660b..e4908a1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -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
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..af68a68
--- /dev/null
+++ b/.vscode/launch.json
@@ -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}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f7964b5
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+ "[odin]": {
+ "editor.formatOnSave": true,
+ "editor.tabSize": 4
+ }
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..784b8a9
--- /dev/null
+++ b/.vscode/tasks.json
@@ -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
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 44c60e2..3a0d31f 100644
--- a/README.md
+++ b/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 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).
-
+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.
diff --git a/resources/repo_assets/image.png b/repo_assets/image.png
similarity index 100%
rename from resources/repo_assets/image.png
rename to repo_assets/image.png
diff --git a/scripts/build.sh b/scripts/build.sh
deleted file mode 100644
index 1bf5423..0000000
--- a/scripts/build.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-FLAGS="-define:RAYLIB_SHARED=true -out:build/yaap -debug"
-SRC=src/frontend
-
-mkdir -p build
-odin build ${SRC} ${FLAGS}
\ No newline at end of file
diff --git a/scripts/build_atlas.bat b/scripts/build_atlas.bat
deleted file mode 100644
index d650386..0000000
--- a/scripts/build_atlas.bat
+++ /dev/null
@@ -1,2 +0,0 @@
-@echo off
-odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap-debug.exe -debug
\ No newline at end of file
diff --git a/scripts/build_atlas_release.bat b/scripts/build_atlas_release.bat
deleted file mode 100644
index 9fb82e6..0000000
--- a/scripts/build_atlas_release.bat
+++ /dev/null
@@ -1,2 +0,0 @@
-@echo off
-odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap.exe -o:speed
\ No newline at end of file
diff --git a/scripts/build_cli.bat b/scripts/build_cli.bat
deleted file mode 100644
index a13a781..0000000
--- a/scripts/build_cli.bat
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/scripts/build_cli_release.bat b/scripts/build_cli_release.bat
deleted file mode 100644
index 5bd9255..0000000
--- a/scripts/build_cli_release.bat
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/scripts/build_debug.bat b/scripts/build_debug.bat
new file mode 100644
index 0000000..d5ab9ff
--- /dev/null
+++ b/scripts/build_debug.bat
@@ -0,0 +1,2 @@
+@echo off
+odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_debug.exe -debug
diff --git a/scripts/build_debug.sh b/scripts/build_debug.sh
new file mode 100755
index 0000000..7df52c7
--- /dev/null
+++ b/scripts/build_debug.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+odin build src/main_release -out:build/game_debug.bin -no-bounds-check -debug
diff --git a/scripts/build_generator_debug.bat b/scripts/build_generator_debug.bat
new file mode 100644
index 0000000..2340f93
--- /dev/null
+++ b/scripts/build_generator_debug.bat
@@ -0,0 +1,2 @@
+@echo off
+odin build src/aseprite_odin_generator -define:RAYLIB_SHARED=true -out:build_generator/aseprite_odin_generator.exe -debug
diff --git a/scripts/build_hot_reload.bat b/scripts/build_hot_reload.bat
new file mode 100644
index 0000000..3890995
--- /dev/null
+++ b/scripts/build_hot_reload.bat
@@ -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 /vendor/raylib/windows/raylib.dll to the same directory as game.exe"
+ exit /b 1
+)
+
+exit /b 0
diff --git a/scripts/build_hot_reload.sh b/scripts/build_hot_reload.sh
new file mode 100755
index 0000000..996a14d
--- /dev/null
+++ b/scripts/build_hot_reload.sh
@@ -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
diff --git a/scripts/build_release.bat b/scripts/build_release.bat
new file mode 100644
index 0000000..0d1ac5b
--- /dev/null
+++ b/scripts/build_release.bat
@@ -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
diff --git a/scripts/build_release.sh b/scripts/build_release.sh
new file mode 100644
index 0000000..257abc0
--- /dev/null
+++ b/scripts/build_release.sh
@@ -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
diff --git a/scripts/setup.bat b/scripts/setup.bat
deleted file mode 100644
index cdd0a75..0000000
--- a/scripts/setup.bat
+++ /dev/null
@@ -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
diff --git a/scripts/setup.sh b/scripts/setup.sh
deleted file mode 100644
index ed557b0..0000000
--- a/scripts/setup.sh
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/src/aseprite b/src/aseprite
new file mode 160000
index 0000000..628e655
--- /dev/null
+++ b/src/aseprite
@@ -0,0 +1 @@
+Subproject commit 628e655661d822fecae67cf238cbfa414912d943
diff --git a/examples/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin
similarity index 51%
rename from examples/aseprite_odin_generator.odin
rename to src/aseprite_odin_generator/aseprite_odin_generator.odin
index 7180177..563452d 100644
--- a/examples/aseprite_odin_generator.odin
+++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin
@@ -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)
}
diff --git a/src/aseprite_odin_generator/atlas.png b/src/aseprite_odin_generator/atlas.png
new file mode 100644
index 0000000..94facee
Binary files /dev/null and b/src/aseprite_odin_generator/atlas.png differ
diff --git a/examples/sample.aseprite b/src/aseprite_odin_generator/big.aseprite
similarity index 100%
rename from examples/sample.aseprite
rename to src/aseprite_odin_generator/big.aseprite
diff --git a/src/aseprite_odin_generator/metadata.json b/src/aseprite_odin_generator/metadata.json
new file mode 100644
index 0000000..be61ff1
--- /dev/null
+++ b/src/aseprite_odin_generator/metadata.json
@@ -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]}]
\ No newline at end of file
diff --git a/vendors/dialog/build.bat b/src/dialog/build.bat
similarity index 100%
rename from vendors/dialog/build.bat
rename to src/dialog/build.bat
diff --git a/vendors/dialog/build.sh b/src/dialog/build.sh
old mode 100644
new mode 100755
similarity index 71%
rename from vendors/dialog/build.sh
rename to src/dialog/build.sh
index 43b3bbc..2eba996
--- a/vendors/dialog/build.sh
+++ b/src/dialog/build.sh
@@ -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
\ No newline at end of file
diff --git a/vendors/dialog/libtinyfiledialogs b/src/dialog/libtinyfiledialogs
similarity index 100%
rename from vendors/dialog/libtinyfiledialogs
rename to src/dialog/libtinyfiledialogs
diff --git a/src/dialog/tinyfiledialog.odin b/src/dialog/tinyfiledialog.odin
new file mode 100644
index 0000000..1c82058
--- /dev/null
+++ b/src/dialog/tinyfiledialog.odin
@@ -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 ---
+}
diff --git a/src/frontend/globals.odin b/src/frontend/globals.odin
deleted file mode 100644
index 795a7c1..0000000
--- a/src/frontend/globals.odin
+++ /dev/null
@@ -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
diff --git a/src/frontend/main.odin b/src/frontend/main.odin
deleted file mode 100644
index 6f69642..0000000
--- a/src/frontend/main.odin
+++ /dev/null
@@ -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)
-}
diff --git a/src/game.odin b/src/game.odin
new file mode 100644
index 0000000..b289b82
--- /dev/null
+++ b/src/game.odin
@@ -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
+}
diff --git a/src/generator/generator.odin b/src/generator.odin
similarity index 77%
rename from src/generator/generator.odin
rename to src/generator.odin
index fce6a4c..571cca2 100644
--- a/src/generator/generator.odin
+++ b/src/generator.odin
@@ -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",
@@ -342,9 +346,9 @@ cpp_source_generator_metadata := SourceCodeGeneratorMetadata {
}
/*
- Generates a barebones file with the package name "atlas_bindings",
- the file contains an array of offsets, indexed by an enum.
- The enum has unique names
+ Generates a barebones file with the package name "atlas_bindings",
+ the file contains an array of offsets, indexed by an enum.
+ The enum has unique names
*/
generate_odin_enums_and_atlas_offsets_file_sb :: proc(
metadata: []SpriteAtlasMetadata,
@@ -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 {
diff --git a/src/globals.odin b/src/globals.odin
new file mode 100644
index 0000000..7808893
--- /dev/null
+++ b/src/globals.odin
@@ -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),
+}
diff --git a/src/main_hot_reload/main_hot_reload.odin b/src/main_hot_reload/main_hot_reload.odin
new file mode 100644
index 0000000..99f4692
--- /dev/null
+++ b/src/main_hot_reload/main_hot_reload.odin
@@ -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
diff --git a/src/main_release/main_release.odin b/src/main_release/main_release.odin
new file mode 100644
index 0000000..1d335d1
--- /dev/null
+++ b/src/main_release/main_release.odin
@@ -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
diff --git a/src/symbol_exports.odin b/src/symbol_exports.odin
new file mode 100644
index 0000000..d5ada25
--- /dev/null
+++ b/src/symbol_exports.odin
@@ -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)
+}
diff --git a/src/utils/animation.odin b/src/utils/animation.odin
new file mode 100644
index 0000000..4a55c6e
--- /dev/null
+++ b/src/utils/animation.odin
@@ -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}
+}
diff --git a/src/generator/cli.odin b/src/utils/cli.odin
similarity index 66%
rename from src/generator/cli.odin
rename to src/utils/cli.odin
index 5a48ebe..8b9411f 100644
--- a/src/generator/cli.odin
+++ b/src/utils/cli.odin
@@ -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],
+ )
}
}
diff --git a/src/utils/handle_array.odin b/src/utils/handle_array.odin
new file mode 100644
index 0000000..b6b59d1
--- /dev/null
+++ b/src/utils/handle_array.odin
@@ -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
+}
diff --git a/src/utils/helpers.odin b/src/utils/helpers.odin
new file mode 100644
index 0000000..07a5a86
--- /dev/null
+++ b/src/utils/helpers.odin
@@ -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)}
+}
diff --git a/resources/styles/style_candy.rgs b/styles/style_candy.rgs
similarity index 100%
rename from resources/styles/style_candy.rgs
rename to styles/style_candy.rgs
diff --git a/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin b/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin
deleted file mode 100644
index 0f20238..0000000
--- a/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin
+++ /dev/null
@@ -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))
-}
diff --git a/vendors/dialog/tinyfiledialogs.odin b/vendors/dialog/tinyfiledialogs.odin
deleted file mode 100644
index ba43e6e..0000000
--- a/vendors/dialog/tinyfiledialogs.odin
+++ /dev/null
@@ -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
-}
diff --git a/vendors/odin-aseprite b/vendors/odin-aseprite
deleted file mode 160000
index 72ea2e8..0000000
--- a/vendors/odin-aseprite
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 72ea2e8094a5f05074d4c4f2faafdba42e54673c