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