diff --git a/.gitignore b/.gitignore index 362a987..5cb0a1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,17 @@ -*.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/ -ols.json \ No newline at end of file +build_generator/ +ols.json +libtinyfiledialogs.a +libtinyfiledialogs.obj +tinyfiledialogs.lib +tinyfiledialogs.obj + +.idea/ diff --git a/.gitmodules b/.gitmodules index 6282b93..6e5660b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "src/aseprite"] - path = src/aseprite - url = https://github.com/bersK/odin-aseprite.git +[submodule "vendors/odin-aseprite"] + path = vendors/odin-aseprite + url = git@github.com:blob1807/odin-aseprite.git diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 110830b..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "cppvsdbg", - "request": "launch", - "preLaunchTask": "Build Debug", - "name": "Debug", - "program": "${workspaceFolder}/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 deleted file mode 100644 index 9e476f8..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "workbench.colorCustomizations": { - "activityBar.background": "#322C2D", - "titleBar.activeBackground": "#463E3F", - "titleBar.activeForeground": "#FAFAFA" - } -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index b1eca51..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "version": "2.0.0", - "command": "", - "args": [], - "tasks": [ - { - "label": "Build Debug", - "type": "shell", - "windows": { - "command": "${workspaceFolder}/scripts/build_debug.bat", - }, - "linux": { - "command": "${workspaceFolder}/scripts/build_debug.sh", - }, - "osx": { - "command": "${workspaceFolder}/scripts/build_debug.sh", - }, - "group": "build" - }, - { - "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": "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": { - "kind": "build", - "isDefault": false - }, - }, - { - "label": "Build&Run Tile Generator Test", - "type": "shell", - "command": "odin run src/aseprite_odin_generator -out:build/aseprite_odin_generator.exe", - "options": { - "cwd": "${workspaceFolder}" - }, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "dedicated", - "showReuseMessage": false, - "clear": true - }, - "group": { - "kind": "build", - "isDefault": true - }, - } - ] -} \ No newline at end of file diff --git a/README.md b/README.md index a20f4ac..44c60e2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,31 @@ # YAAP -Yet-Another-Atlas-Packer by Stefan Stefanov +Yet-Another-Atlas-Packer by bersK (Stefan Stefanov) + +## Usage & requirements +> [!IMPORTANT] +> Pull this repo with `--recursive`, the `odin-aseprite` library is pulled in as a submodule. + +At least odin compiler version `dev-2025-07:204edd0fc` + +Since we need to download/compile some 3rd party dependencies in the vendors folder we need to call +`scripts/setup.[sh|bat]` to compile libtinydialog & download the already compiled `clay` binaries and bindings. ## Description +> [!NOTE] +> Quite frankly if you want automatic atlas packing I would suggest using `stb_rect_pack` directly & an aseprite plugin for exporting your assets in a proper format for packing. Using this atlas packer in an automated fashion is not quite possible and slower as a workflow. I would only use it for experiment projects or places where having a built in atlas packer is not practical. -Simple atlas packer using `stb_rect_pack` from the `stb` family of header libraries & `raylib` for rendering/ui. +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. -I'm using a custom marshall/unmarshall odin library for reading .aseprite files found [here](https://github.com/blob1807/odin-aseprite) +Uses `stb_rect_pack` for the image packing & `raylib` for the UI. -Project template provided by Karl Zylinski on github [here](https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template). +A quick preview on [youtube](https://youtu.be/4_dKq7G57Lw). + + + +## 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)) diff --git a/examples/aseprite_odin_generator.odin b/examples/aseprite_odin_generator.odin new file mode 100644 index 0000000..7180177 --- /dev/null +++ b/examples/aseprite_odin_generator.odin @@ -0,0 +1,41 @@ +package cli + +import ase "../../vendors/odin-aseprite" +import "core:encoding/json" +import "core:fmt" +import "core:os" +import "core:slice" +import s "core:strings" + +import rl "vendor:raylib" +import stbrp "vendor:stb/rect_pack" + +import gen "../src/generator" + +ATLAS_SIZE :: 512 +IMPORT_PATH :: "./example.aseprite" +EXPORT_PATH :: "./atlas.png" + +main :: proc() { + + 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() + + atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK) + atlas_entries: [dynamic]gen.AtlasEntry = make([dynamic]gen.AtlasEntry) + gen.unmarshall_aseprite_dir(target_dir, &atlas_entries) + + metadata := gen.pack_atlas_entries(atlas_entries[:], &atlas, 10, 10) + + json_bytes, jerr := json.marshal(metadata) + os.write_entire_file("./metadata.json", json_bytes) + sb := gen.metadata_source_code_generate(metadata[:], gen.odin_source_generator_metadata) + odin_output_str := s.to_string(sb) + os.write_entire_file("./output.odin", transmute([]byte)odin_output_str) + + rl.ExportImage(atlas, EXPORT_PATH) +} diff --git a/examples/sample.aseprite b/examples/sample.aseprite new file mode 100644 index 0000000..889aabc Binary files /dev/null and b/examples/sample.aseprite differ diff --git a/odinfmt.json b/odinfmt.json new file mode 100644 index 0000000..12c1d35 --- /dev/null +++ b/odinfmt.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json", + "character_width": 100, + "sort_imports": true, + "tabs": true +} \ No newline at end of file diff --git a/resources/repo_assets/image.png b/resources/repo_assets/image.png new file mode 100644 index 0000000..af4f14c Binary files /dev/null and b/resources/repo_assets/image.png differ diff --git a/styles/style_candy.rgs b/resources/styles/style_candy.rgs similarity index 100% rename from styles/style_candy.rgs rename to resources/styles/style_candy.rgs diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..1bf5423 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..d650386 --- /dev/null +++ b/scripts/build_atlas.bat @@ -0,0 +1,2 @@ +@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 new file mode 100644 index 0000000..9fb82e6 --- /dev/null +++ b/scripts/build_atlas_release.bat @@ -0,0 +1,2 @@ +@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 new file mode 100644 index 0000000..a13a781 --- /dev/null +++ b/scripts/build_cli.bat @@ -0,0 +1,2 @@ +@echo off +odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli-debug.exe -debug \ No newline at end of file diff --git a/scripts/build_cli_release.bat b/scripts/build_cli_release.bat new file mode 100644 index 0000000..5bd9255 --- /dev/null +++ b/scripts/build_cli_release.bat @@ -0,0 +1,2 @@ +@echo off +odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli.exe -o:speed \ No newline at end of file diff --git a/scripts/build_debug.bat b/scripts/build_debug.bat deleted file mode 100644 index ecaabef..0000000 --- a/scripts/build_debug.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -odin build src/main_release -define:RAYLIB_SHARED=false -out:build/game_debug.exe -no-bounds-check -subsystem:windows -debug diff --git a/scripts/build_debug.sh b/scripts/build_debug.sh deleted file mode 100644 index 7df52c7..0000000 --- a/scripts/build_debug.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -odin build src/main_release -out:build/game_debug.bin -no-bounds-check -debug diff --git a/scripts/build_hot_reload.bat b/scripts/build_hot_reload.bat deleted file mode 100644 index 3890995..0000000 --- a/scripts/build_hot_reload.bat +++ /dev/null @@ -1,21 +0,0 @@ -@echo off - -rem Build game.dll -odin build src -show-timings -use-separate-modules -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game.dll -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug -IF %ERRORLEVEL% NEQ 0 exit /b 1 - -rem If game.exe already running: Then only compile game.dll and exit cleanly -QPROCESS "game.exe">NUL -IF %ERRORLEVEL% EQU 0 exit /b 1 - -rem build game.exe -odin build src/main_hot_reload -use-separate-modules -out:build/game.exe -strict-style -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug -IF %ERRORLEVEL% NEQ 0 exit /b 1 - -rem copy raylib.dll from odin folder to here -if not exist "raylib.dll" ( - echo "Please copy raylib.dll from /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 deleted file mode 100644 index 99bbd1d..0000000 --- a/scripts/build_hot_reload.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -VET="-strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon" - -# NOTE: this is a recent addition to the Odin compiler, if you don't have this command -# you can change this to the path to the Odin folder that contains vendor, eg: "~/Odin". -ROOT=$(odin root) -if [ ! $? -eq 0 ]; then - echo "Your Odin compiler does not have the 'odin root' command, please update or hardcode it in the script." - exit 1 -fi - -set -eu - -# Figure out the mess that is dynamic libraries. -case $(uname) in -"Darwin") - case $(uname -m) in - "arm64") LIB_PATH="macos-arm64" ;; - *) LIB_PATH="macos" ;; - esac - - DLL_EXT=".dylib" - EXTRA_LINKER_FLAGS="-Wl,-rpath $ROOT/vendor/raylib/$LIB_PATH" - ;; -*) - DLL_EXT=".so" - EXTRA_LINKER_FLAGS="'-Wl,-rpath=\$ORIGIN/linux'" - - # Copy the linux libraries into the project automatically. - if [ ! -d "linux" ]; then - mkdir linux - cp -r $ROOT/vendor/raylib/linux/libraylib*.so* linux - fi - ;; -esac - -# Build the game. -odin build . -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 game_tmp$DLL_EXT 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 deleted file mode 100644 index aa3e8bd..0000000 --- a/scripts/build_release.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -odin build src/main_release -define:RAYLIB_SHARED=false -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 deleted file mode 100644 index 257abc0..0000000 --- a/scripts/build_release.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -odin build src/main_release -out:build/game_release.bin -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon diff --git a/scripts/setup.bat b/scripts/setup.bat new file mode 100644 index 0000000..cdd0a75 --- /dev/null +++ b/scripts/setup.bat @@ -0,0 +1,20 @@ +@echo off + +mkdir build + +FOR /F "tokens=*" %%g IN ('odin root') do (SET ODIN_ROOT=%%g) + +echo %ODIN_ROOT% + +@REM If it fails to find your odin root folder, copy the raylib.dll manually into the build folder, it's a runtime requirement +copy %ODIN_ROOT%\vendor\raylib\windows\raylib.dll build\raylib.dll + +pushd vendors\dialog +call .\build.bat +popd + +pushd vendors +curl.exe --output clay.zip --url https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip +@REM Apparently available on Win10 since build 17063 - https://superuser.com/a/1473255 +tar -xf .\clay.zip +popd diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..ed557b0 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +set -e + +pushd vendors/dialog +sh build.sh +popd + +pushd vendors +wget https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip +unzip clay-odin.zip +rm -rf ./__MACOSX # Yeah... +popd \ No newline at end of file diff --git a/src/animation.odin b/src/animation.odin deleted file mode 100644 index aeeaed5..0000000 --- a/src/animation.odin +++ /dev/null @@ -1,53 +0,0 @@ -// This implements simple animations using sprite sheets. The texture in the -// `Animation` struct is assumed to contain a horizontal strip of the frames -// in the animation. Call `animation_update` to update and then call -// `animation_rect` when you wish to know the source rect to use in the texture -// With the source rect you can run rl.DrawTextureRec to draw the current frame. - -package game - -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/aseprite b/src/aseprite deleted file mode 160000 index f21bed8..0000000 --- a/src/aseprite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f21bed838a6d1e6bc1178ea0876596eb14190192 diff --git a/src/aseprite_odin_generator/atlas.png b/src/aseprite_odin_generator/atlas.png deleted file mode 100644 index 02082b4..0000000 Binary files a/src/aseprite_odin_generator/atlas.png and /dev/null differ diff --git a/src/aseprite_odin_generator/big.aseprite b/src/aseprite_odin_generator/big.aseprite deleted file mode 100644 index bfc55aa..0000000 Binary files a/src/aseprite_odin_generator/big.aseprite and /dev/null differ diff --git a/src/aseprite_odin_generator/big.png b/src/aseprite_odin_generator/big.png deleted file mode 100644 index b2f4a73..0000000 Binary files a/src/aseprite_odin_generator/big.png and /dev/null differ diff --git a/src/aseprite_odin_generator/generator.odin b/src/aseprite_odin_generator/generator.odin deleted file mode 100644 index fd18903..0000000 --- a/src/aseprite_odin_generator/generator.odin +++ /dev/null @@ -1,82 +0,0 @@ -package generator - -import ase "../aseprite" -import "core:fmt" -import "core:mem" -import "core:os" -import fp "core:path/filepath" -import "core:slice" -import "core:strings" -import "core:testing" -import rl "vendor:raylib" - -ATLAS_SIZE :: 512 -EXPORT_PATH :: "E:/dev/odin-atlas-packer/src/aseprite_odin_generator/atlas.png" - -main :: proc() { - fmt.println("Hello!") - ase_file, ase_ok := os.read_entire_file( - "E:/dev/odin-atlas-packer/src/aseprite_odin_generator/big.aseprite", - ) - if !ase_ok { - fmt.panicf("Couldn't load file!") - } - - doc: ase.Document - read, um_err := ase.unmarshal_from_slice(ase_file, &doc) - if um_err != nil { - fmt.panicf("Couldn't unmarshall file!") - } else { - fmt.printfln("Read {0} bytes from file", read) - } - - fmt.println("Header:\n\t", doc.header) - // fmt.println("Frames:\n\t", doc.frames) - - images: [dynamic]rl.Image - atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK) - - for frame in doc.frames { - for chunk in frame.chunks { - cel_chunk, cok := chunk.(ase.Cel_Chunk) - if !cok { - continue - } - - cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel) - if !ci_ok { - continue - } - append( - &images, - rl.Image { - data = rawptr(&cel_img.pixel[0]), - width = auto_cast cel_img.width, - height = auto_cast cel_img.height, - format = .UNCOMPRESSED_R8G8B8A8, - }, - ) - } - } - curr_x, curr_y: i32 - for img, img_i in images { - fmt.printfln("Image_{0}: {1}", img_i, img) - rl.ImageDraw( - &atlas, - img, - {0, 0, auto_cast img.width, auto_cast img.height}, - {auto_cast curr_x, auto_cast curr_y, auto_cast img.width, auto_cast img.height}, - rl.WHITE, - ) - curr_x += img.width - curr_y += img.height - } - - // todo: pack the rectangles - - // todo: blit them to the atlas - - // todo: generate metadata (json, odin enums) - - rl.ExportImage(atlas, EXPORT_PATH) -} diff --git a/src/dialog/tinyfiledialog.odin b/src/dialog/tinyfiledialog.odin deleted file mode 100644 index ae6dd35..0000000 --- a/src/dialog/tinyfiledialog.odin +++ /dev/null @@ -1,34 +0,0 @@ -package tinyfiledialogs - -import "core:c" - -when ODIN_OS == .Windows { - foreign import lib {"tinyfiledialogs.lib", "system:comdlg32.lib", "system:Ole32.lib"} -} -when ODIN_OS == .Linux { - - 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 new file mode 100644 index 0000000..795a7c1 --- /dev/null +++ b/src/frontend/globals.odin @@ -0,0 +1,55 @@ +package frontend + +import generator "../generator" +import rl "vendor:raylib" + +PIXEL_WINDOW_HEIGHT :: 180 + +ui_scaling: f32 = 2 + +@(rodata) +ATLAS_RENDER_SIZES := []i32{256, 512, 1024, 2048, 4096} + +FileDialogType :: enum { + SourceFiles, + OutputFolder, +} + +WindowInformation :: struct { + w: f32, + h: f32, + width_scaled: f32, + height_scaled: f32, +} +window_info: WindowInformation + +MonitorInformation :: struct { + max_width: f32, + max_height: f32, +} +monitor_info: MonitorInformation + +PackerSettings :: struct { + atlas_size_x: i32, + atlas_size_y: i32, + pixel_padding_x_int: i32, + pixel_padding_y_int: i32, + padding_enabled: bool, + output_json: bool, + output_odin: bool, +} +packer_settings: PackerSettings + +// Where the output files will be written (atlas.png json output, etc) +output_folder_path: string +// If a folder was chosen as input - the path +source_files_to_pack: []string + +Atlas :: struct { + render_texture_target: rl.RenderTexture2D, + checked_background: rl.RenderTexture2D, + render_has_preview: bool, + render_size: i32, + metadata: [dynamic]generator.SpriteAtlasMetadata, +} +atlas: Atlas diff --git a/src/frontend/main.odin b/src/frontend/main.odin new file mode 100644 index 0000000..6f69642 --- /dev/null +++ b/src/frontend/main.odin @@ -0,0 +1,520 @@ +package frontend + +import "base:runtime" +import "core:c/libc" +import "core:log" +import "core:mem" +import "core:strings" +import rl "vendor:raylib" + +import diag "../../vendors/dialog" +import generator "../generator" + +should_pack_atlas_and_render: bool + +main :: proc() { + default_allocator := context.allocator + tracking_allocator: mem.Tracking_Allocator + mem.tracking_allocator_init(&tracking_allocator, default_allocator) + context.allocator = mem.tracking_allocator(&tracking_allocator) + + context.logger = log.create_console_logger() + { + init() + + defer cleanup() + + for !rl.WindowShouldClose() { + update() + draw() + + for b in tracking_allocator.bad_free_array { + log.errorf("Bad free at: %v", b.location) + } + + clear(&tracking_allocator.bad_free_array) + + free_all(context.temp_allocator) + } + } + log.destroy_console_logger(context.logger) + + for key, value in tracking_allocator.allocation_map { + log.errorf("%v: Leaked %v bytes\n", value.location, value.size) + } + mem.tracking_allocator_destroy(&tracking_allocator) +} + +init :: proc() { + atlas.render_size = ATLAS_RENDER_SIZES[0] + + rl.SetConfigFlags({.WINDOW_RESIZABLE}) + rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer") + rl.SetWindowMinSize(1400, 800) + rl.SetTraceLogCallback(rl_log) +} + +cleanup :: proc() { + delete(atlas.metadata) + delete(output_folder_path) + delete(source_files_to_pack) + log.info("Bye") + rl.CloseWindow() +} + +update :: proc() { + // Update the width/height + win_info := &window_info + win_info.w = f32(rl.GetScreenWidth()) + win_info.h = f32(rl.GetScreenHeight()) + win_info.height_scaled = win_info.h / ui_scaling + win_info.width_scaled = win_info.w / ui_scaling + + // Update the virtual mouse position (needed for GUI interaction to work properly for instance) + mouse_scale := 1 / ui_scaling + rl.SetMouseScale(mouse_scale, mouse_scale) +} + +draw :: proc() { + rl.BeginDrawing() + defer rl.EndDrawing() + + rl.ClearBackground(rl.BLACK) + + draw_screen_ui() + + if should_pack_atlas_and_render { + pack_atlas_and_render() + } + + free_all(context.temp_allocator) +} + +ui_camera :: proc() -> rl.Camera2D { + return {zoom = ui_scaling} +} + +draw_screen_ui :: proc() { + rl.BeginMode2D(ui_camera()) + + draw_settings_and_preview() + + rl.EndMode2D() +} + +save_atlas_to_file :: proc() { + generator.save_output(output_folder_path, atlas.metadata, atlas.render_texture_target) +} + +save_to_atlas_to_file :: proc() { + open_file_dialog(.OutputFolder) + save_atlas_to_file() +} + +pack_atlas_and_render :: proc() { + rl.BeginTextureMode(atlas.render_texture_target) + defer rl.EndTextureMode() + + atlas_entries: [dynamic]generator.AtlasEntry + defer { + for entry in atlas_entries { + delete(entry.cells) + delete(entry.layer_cell_count) + delete(entry.layer_names) + } + delete(atlas_entries) + } + + if len(source_files_to_pack) > 0 { + generator.unmarshall_aseprite_files(source_files_to_pack, &atlas_entries) + } else { + log.error("No source folder or files set! Can't pack the void!!!") + should_pack_atlas_and_render = false + return + } + + atlas_img: rl.Image = rl.GenImageColor(atlas.render_size, atlas.render_size, rl.BLANK) + + padding_x := packer_settings.pixel_padding_x_int if packer_settings.padding_enabled else 0 + padding_y := packer_settings.pixel_padding_y_int if packer_settings.padding_enabled else 0 + + delete(atlas.metadata) + atlas.metadata = generator.pack_atlas_entries( + atlas_entries[:], + &atlas_img, + padding_x, + padding_y, + ) + + // OpenGL's Y buffer is flipped + rl.ImageFlipVertical(&atlas_img) + // rl.UnloadTexture(atlas_render_target.texture) + log.info("Packed everything!") + atlas.render_texture_target.texture = rl.LoadTextureFromImage(atlas_img) + + should_pack_atlas_and_render = false + atlas.render_has_preview = true +} + +draw_settings_and_preview :: proc() { + left_half_rect := rl.Rectangle { + x = 0, + y = 0, + width = cast(f32)window_info.width_scaled / 3, + height = cast(f32)window_info.height_scaled, + } + right_half_rect := rl.Rectangle { + x = cast(f32)window_info.width_scaled / 3, + y = 0, + width = cast(f32)(window_info.width_scaled / 3) * 2, + height = cast(f32)window_info.height_scaled, + } + rl.DrawRectangleRec(left_half_rect, rl.WHITE) + rl.DrawRectangleRec(right_half_rect, rl.MAROON) + + @(static) spinner_edit_mode: bool + + small_offset := 10 * ui_scaling + big_offset := 30 * ui_scaling + elements_height: f32 = 0 + + rl.GuiPanel(left_half_rect, "Atlas Settings") + elements_height += small_offset / 2 + + @(static) settings_dropdown_box_active_idx: i32 + + elements_height += small_offset + 5 * ui_scaling + + rl.GuiLabel( + {x = small_offset, y = elements_height, width = left_half_rect.width}, + "Atlas Size", + ) + elements_height += small_offset / 2 + + @(static) dropdown_resolution_edit_mode: bool + @(static) dropdown_resolution_mode: i32 + + dropdown_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = left_half_rect.width - small_offset * 2, + height = small_offset, + } + // Because we want to render this ontop of everything else, we can just 'defer' it at the end of the draw function + defer { + if dropdown_resolution_edit_mode {rl.GuiLock()} + + if rl.GuiDropdownBox( + dropdown_rect, + "256x;512x;1024x;2048x;4096x", + &dropdown_resolution_mode, + dropdown_resolution_edit_mode, + ) { + dropdown_resolution_edit_mode = !dropdown_resolution_edit_mode + atlas.render_size = + ATLAS_RENDER_SIZES[max(i32(len(ATLAS_RENDER_SIZES) - 1), dropdown_resolution_mode)] + } + rl.GuiUnlock() + } + elements_height += small_offset * 2 + + // General Options + if settings_dropdown_box_active_idx == 0 { + padding_settings_y := elements_height + { + defer { + padding_settings_rect := rl.Rectangle { + x = small_offset / 2, + y = padding_settings_y, + width = left_half_rect.width - small_offset, + height = elements_height - padding_settings_y, + } + rl.GuiGroupBox(padding_settings_rect, "Padding Settings") + } + elements_height += small_offset + + enable_padding_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = small_offset, + height = small_offset, + } + rl.GuiCheckBox( + enable_padding_rect, + " Enable padding", + &packer_settings.padding_enabled, + ) + elements_height += small_offset * 2 + + // Padding X spinner and label + { + padding_x_spinner_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = big_offset * 2, + height = small_offset, + } + padding_x_spinner := rl.GuiSpinner( + padding_x_spinner_rect, + "", + &packer_settings.pixel_padding_x_int, + 0, + 10, + spinner_edit_mode, + ) + if (padding_x_spinner) > 0 { + spinner_edit_mode = !spinner_edit_mode + } + rl.GuiLabel( + { + x = (small_offset * 2) + big_offset * 2, + y = elements_height, + width = big_offset, + height = small_offset, + }, + "Padding X", + ) + } + elements_height += small_offset * 2 + + // Padding Y spinner and label + { + padding_y_spinner_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = big_offset * 2, + height = small_offset, + } + padding_y_spinner := rl.GuiSpinner( + padding_y_spinner_rect, + "", + &packer_settings.pixel_padding_y_int, + 0, + 10, + spinner_edit_mode, + ) + if (padding_y_spinner) > 0 { + spinner_edit_mode = !spinner_edit_mode + } + rl.GuiLabel( + { + x = (small_offset * 2) + big_offset * 2, + y = elements_height, + width = big_offset, + height = small_offset, + }, + "Padding Y", + ) + } + elements_height += small_offset * 2 + + } + elements_height += small_offset + + { + actions_label_y := elements_height + + defer { + actions_rect := rl.Rectangle { + x = small_offset / 2, + y = actions_label_y, + width = left_half_rect.width - small_offset, + height = elements_height - actions_label_y, + } + rl.GuiGroupBox(actions_rect, "Actions") + } + elements_height += small_offset + + pick_sources_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(pick_sources_rect, "Pick Source(s)") { + open_file_dialog(.SourceFiles) + } + + pick_output_rect := rl.Rectangle { + x = left_half_rect.width / 2, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(pick_output_rect, "Pick Output") { + open_file_dialog(.OutputFolder) + } + elements_height += small_offset * 2 + + pack_atlas_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(pack_atlas_rect, "Pack Atlas") { + should_pack_atlas_and_render = true + } + + clear_atlas_rect := rl.Rectangle { + x = left_half_rect.width / 2, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(clear_atlas_rect, "Clear Atlas") { + clear_atlas_data() + } + elements_height += small_offset * 2 + + save_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(save_rect, "Save") { + save_atlas_to_file() + } + + save_to_rect := rl.Rectangle { + x = left_half_rect.width / 2, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(save_to_rect, "Save To...") { + save_to_atlas_to_file() + } + elements_height += small_offset * 2 + } + + } + + // Packing Options + if settings_dropdown_box_active_idx == 1 { + + @(static) active_tab: i32 + tabs: []cstring = {"One", "Two", "Three"} + rl.GuiTabBar( + {x = small_offset, y = elements_height, width = 100, height = small_offset}, + &tabs[0], + auto_cast len(tabs), + &active_tab, + ) + } + + // Save Options + // if settings_dropdown_box_active_idx == 2 { + // } + + elements_height = 0 + rl.GuiPanel(right_half_rect, "Atlas Preview") + short_edge := min( + right_half_rect.height - big_offset * 1.5, + right_half_rect.width - big_offset * 1.5, + ) + preview_rect := rl.Rectangle { + x = (right_half_rect.width / 2 + right_half_rect.x) - (short_edge / 2), + y = (right_half_rect.height / 2 + right_half_rect.y) - (short_edge / 2), + width = short_edge, + height = short_edge, + } + if !atlas.render_has_preview { + rl.GuiDummyRec(preview_rect, "PREVIEW") + } else { + // rl.DrawRectangleRec(preview_rect, rl.WHITE) + bg_texture := atlas.checked_background.texture + rl.DrawTexturePro( + bg_texture, + {width = auto_cast bg_texture.width, height = auto_cast bg_texture.height}, + preview_rect, + {}, + 0, + rl.WHITE, + ) + atlas_texture := atlas.render_texture_target.texture + rl.DrawTexturePro( + atlas_texture, + {width = auto_cast atlas_texture.width, height = auto_cast -atlas_texture.height}, + preview_rect, + {0, 0}, + 0, + rl.WHITE, + ) + } +} + +open_file_dialog :: proc(dialog_type: FileDialogType) { + switch dialog_type { + case .SourceFiles: + // `open_file_dialog` returns a single cstring with one or more paths divided by a separator ('|'), + // https://github.com/native-toolkit/libtinyfiledialogs/blob/master/tinyfiledialogs.c#L2706 + source_files, ok := diag.open_file_dialog("Select source files", {}, 0, {}, "", 1) + if len(source_files) > 0 { + // todo(stefan): We're assuming the filepaths returned libtinydialog are valid... + source_files_to_pack = strings.split(source_files, "|") + + log.info(source_files_to_pack) + } else { + log.error("No files were selected!") + } + + case .OutputFolder: + file, ok := diag.select_folder_dialog("Select source folder", {}) + if len(file) > 0 && ok { + output_folder_path = file + log.info(file) + } else { + log.error("Got an empty path from the file dialog!") + } + } +} + +clear_atlas_data :: proc() { + delete(atlas.metadata) + atlas.render_has_preview = false +} + +logger: log.Logger +rl_log_buf: []byte +rl_log :: proc "c" (logLevel: rl.TraceLogLevel, text: cstring, args: ^libc.va_list) { + context = runtime.default_context() + context.logger = logger + + level: log.Level + switch logLevel { + case .TRACE, .DEBUG: + level = .Debug + case .ALL, .NONE, .INFO: + level = .Info + case .WARNING: + level = .Warning + case .ERROR: + level = .Error + case .FATAL: + level = .Fatal + } + + if level < logger.lowest_level { + return + } + + if rl_log_buf == nil { + rl_log_buf = make([]byte, 1024) + } + + defer mem.zero_slice(rl_log_buf) + + n: int + for { + va := args + n = int(libc.vsnprintf(raw_data(rl_log_buf), len(rl_log_buf), text, va)) + if n < len(rl_log_buf) do break + log.infof("Resizing raylib log buffer from %m to %m", len(rl_log_buf), len(rl_log_buf) * 2) + rl_log_buf, _ = mem.resize_bytes(rl_log_buf, len(rl_log_buf) * 2) + } + + formatted := string(rl_log_buf[:n]) + log.log(level, formatted) +} diff --git a/src/game.odin b/src/game.odin deleted file mode 100644 index f2f4057..0000000 --- a/src/game.odin +++ /dev/null @@ -1,468 +0,0 @@ -// This file is compiled as part of the `odin.dll` file. It contains the -// procs that `game.exe` will call, such as: -// -// game_init: Sets up the game state -// game_update: Run once per frame -// game_shutdown: Shuts down game and frees memory -// game_memory: Run just before a hot reload, so game.exe has a pointer to the -// game's memory. -// game_hot_reloaded: Run after a hot reload so that the `g_mem` global variable -// can be set to whatever pointer it was in the old DLL. - -package game - -import "core:fmt" -import "core:strings" -import rl "vendor:raylib" - -import diag "dialog" - -PixelWindowHeight :: 180 - -/* - `SourceFilesPicker` // Screen 1: Shows the file dialog box, meant for the user to choose the source files/folder - `OutputLocationPicker` // Screen 2: Shows the file dialog box, meant for the user to choose the output file name & location - `PackSettingsAndPreview` Screen 3: Shows settings about the packing operations, `save` & `save as` button - `SaveToOutputPicker` // Screen 4: After clicking the `save as` button on screen 3, ask the user for a new location & name and save the file -*/ - -AppScreen :: enum { - SourceFilesPicker, - OutputLocationPicker, - PackSettingsAndPreview, - SaveToOutputPicker, -} - -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, - Exit, -} - -PackerSettings :: struct { - pixel_padding_x_int: i32, - pixel_padding_x: f32, - pixel_padding_y_int: i32, - pixel_padding_y: f32, - padding_enabled: bool, - fix_pixel_bleeding: bool, - output_json: bool, - output_odin: bool, -} - -FILE_DIALOG_SIZE :: 1000 -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, - // atlas packer state - app_screen: AppScreen, - // Where the output files will be written (atlas.png, json output, etc) - output_path_set: bool, - output_folder_path: string, - // If files were chosen as input - their paths - input_path_set: bool, - source_location_to_pack: string, - // If a folder was chosen as input - the path - input_files_set: bool, - source_files_to_pack: []string, - // What type of file dialog to open - source_location_type: FileDialogType, - // Packer settings - packer_settings: PackerSettings, - atlas_render_texture_target: rl.RenderTexture2D, - atlas_render: bool, -} - -g_mem: ^GameMemory - -w, h: f32 - -game_camera :: proc() -> rl.Camera2D { - w = f32(rl.GetScreenWidth()) - h = f32(rl.GetScreenHeight()) - - return {zoom = h / PixelWindowHeight, target = {}, offset = {w / 2, h / 2}} -} - -scaling: f32 = 2 -ui_camera :: proc() -> rl.Camera2D { - return {zoom = scaling} -} - -input_box_loc: rl.Vector2 = {} -moving_input_box: bool -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) - - update_screen() -} - -draw :: proc() { - rl.BeginDrawing() - defer rl.EndDrawing() - - rl.ClearBackground(rl.BLACK) - - draw_screen_ui() - - if g_mem.atlas_render { - draw_screen_target() - } -} - -update_screen :: proc() { - if (g_mem.input_files_set || g_mem.input_path_set) { - if !g_mem.output_path_set { - g_mem.app_screen = .OutputLocationPicker - } else { - g_mem.app_screen = .PackSettingsAndPreview - } - } else { - g_mem.app_screen = .SourceFilesPicker - } - - switch g_mem.app_screen { - case .SourceFilesPicker: - fallthrough - case .OutputLocationPicker: - fallthrough - case .SaveToOutputPicker: - if g_mem.should_open_file_dialog { - open_file_dialog_and_store_output_paths() - } - case .PackSettingsAndPreview: - } -} - -draw_screen_ui :: proc() { - rl.BeginMode2D(ui_camera()) - defer rl.EndMode2D() - - switch g_mem.app_screen { - case .SourceFilesPicker: - fallthrough - case .OutputLocationPicker: - fallthrough - case .SaveToOutputPicker: - draw_and_handle_source_files_logic() - case .PackSettingsAndPreview: - draw_atlas_settings_and_preview() - } -} - -draw_screen_target :: proc() { - rl.BeginTextureMode(g_mem.atlas_render_texture_target) - defer rl.EndTextureMode() - - rl.ClearBackground(rl.WHITE) - rl.DrawCircle(100, 100, 50, rl.GREEN) - - g_mem.atlas_render = false -} - -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) - - small_offset := 10 * scaling - big_offset := 30 * scaling - elements_height: f32 = 0 - - rl.GuiPanel(left_half_rect, "Atlas Settings") - elements_height += 25 * scaling - - rl.GuiLine({y = elements_height, width = left_half_rect.width}, "General Settings") - elements_height += small_offset - - rl.GuiCheckBox( - {x = small_offset, y = elements_height, width = small_offset, height = small_offset}, - "Fix pixel bleed", - &g_mem.packer_settings.padding_enabled, - ) - elements_height += small_offset * 2 - - rl.GuiLine({y = elements_height, width = left_half_rect.width}, "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.fix_pixel_bleeding, - ) - elements_height += small_offset * 2 - - @(static) - spinner_edit_mode: bool - 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 - - rl.GuiLine({y = elements_height, width = left_half_rect.width}, "Actions") - elements_height += small_offset - - if rl.GuiButton( - { - x = small_offset, - y = elements_height, - width = left_half_rect.width / 2 - small_offset * 2, - height = small_offset, - }, - "Pack", - ) { - g_mem.atlas_render = true - } - elements_height += small_offset * 2 - - - if rl.GuiButton( - { - x = small_offset, - y = elements_height, - width = left_half_rect.width / 2 - small_offset * 2, - 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...", - ) { - - } - - 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, - } - rl.GuiDummyRec(preview_rect, "PREVIEW") - preview_rect.x += 10;preview_rect.y += 10;preview_rect.height -= 20;preview_rect.width -= 20 - texture := &g_mem.atlas_render_texture_target.texture - rl.DrawTexturePro( - texture^, - {width = auto_cast texture.width, height = auto_cast -texture.height}, - preview_rect, - {0, 0}, - 0, - rl.WHITE, - ) -} - -open_file_dialog_and_store_output_paths :: proc() { - if g_mem.source_location_type == .SourceFiles { - files := cstring( - diag.open_file_dialog( - "Select source files", - cstring(&g_mem.file_dialog_text_buffer[0]), - 0, - nil, - "", - 1, - ), - ) - - source_files_to_pack := strings.clone_from_cstring(files, context.allocator) - // File dialog returns an array of path(s), separated by a '|' - g_mem.source_files_to_pack = strings.split(source_files_to_pack, "|") - g_mem.input_files_set = (len(source_files_to_pack) > 0) - - fmt.println(g_mem.source_files_to_pack) - } - if g_mem.source_location_type == .SourceFolder { - file := cstring( - diag.select_folder_dialog( - "Select source folder", - cstring(&g_mem.file_dialog_text_buffer[0]), - ), - ) - g_mem.source_location_to_pack = strings.clone_from_cstring(file) - g_mem.input_path_set = (len(file) > 0) - fmt.println(g_mem.source_location_to_pack) - } - if g_mem.source_location_type == .OutputFolder { - file := cstring( - diag.select_folder_dialog( - "Select source folder", - cstring(&g_mem.file_dialog_text_buffer[0]), - ), - ) - g_mem.output_folder_path = strings.clone_from_cstring(file) - - g_mem.output_path_set = (len(file) > 0) - fmt.println(g_mem.output_folder_path) - } - - g_mem.should_open_file_dialog = false -} - -draw_and_handle_source_files_logic :: proc() { - #partial switch g_mem.app_screen { - case .SourceFilesPicker: - result := rl.GuiTextInputBox( - rl.Rectangle{width = (w / scaling), height = (h / scaling)}, - "Files", - "File input box", - "Open Source Files;Open Source Folder", - cstring(rawptr(&g_mem.file_dialog_text_buffer)), - FILE_DIALOG_SIZE, - nil, - ) - if result != -1 { - file_dialg_type: FileDialogType - if result == 1 || result == 2 { - file_dialg_type = .SourceFiles if result == 1 else .SourceFolder - } else if result == 0 { - file_dialg_type = .Exit - } - handle_source_file_logic(file_dialg_type) - fmt.println("result: ", result) - } - case .OutputLocationPicker: - result := rl.GuiTextInputBox( - rl.Rectangle{width = (w / scaling), height = (h / scaling)}, - "Files", - "Output Folder", - "Choose Output Folder", - cstring(rawptr(&g_mem.file_dialog_text_buffer)), - FILE_DIALOG_SIZE, - nil, - ) - if result != -1 { - file_dialg_type: FileDialogType = .OutputFolder if result == 1 else .Exit - handle_source_file_logic(file_dialg_type) - fmt.println("result: ", result) - } - case .SaveToOutputPicker: - result := rl.GuiTextInputBox( - rl.Rectangle{width = (w / scaling), height = (h / scaling)}, - "Files", - "Output Folder", - "Choose Output Folder", - cstring(rawptr(&g_mem.file_dialog_text_buffer)), - FILE_DIALOG_SIZE, - nil, - ) - if result != -1 { - file_dialg_type: FileDialogType = .SourceFolder if result == 1 else .Exit - handle_source_file_logic(file_dialg_type) - fmt.println("result: ", result) - } - } -} - -draw_packer_and_settings :: proc() { - -} - -handle_source_file_logic :: proc(picker_type: FileDialogType) { - switch picker_type { - case .Exit: - g_mem.should_open_file_dialog = false - rl.CloseWindow() - case .SourceFiles: - fallthrough - case .SourceFolder: - fallthrough - case .OutputFolder: - g_mem.source_location_type = picker_type - g_mem.should_open_file_dialog = true - } -} diff --git a/src/generator/cli.odin b/src/generator/cli.odin new file mode 100644 index 0000000..5a48ebe --- /dev/null +++ b/src/generator/cli.odin @@ -0,0 +1,117 @@ +package generator + +import "core:fmt" +import s "core:strings" + +// todo(stefan): Simplify this whole flags business, +// this can be implemented in a simpler fashion + +CLIFlagType :: enum { + Unknown, + InputFiles, + InputFolder, + OutputFolder, + ConfigPath, + EnableMetadataOutput, + MetadataJSONOutputPath, + SourceCodeOutputPathOutputPath, + Help, +} + +CLI_FLAG_STRINGS := [CLIFlagType][]string { + .Unknown = {""}, + .Help = {"h", "help"}, + .InputFiles = {"i", "input-files"}, + .InputFolder = {"d", "input-directory"}, + .OutputFolder = {"o", "out"}, + .EnableMetadataOutput = {"m", "export-metadata"}, + .ConfigPath = {"c", "config"}, + .MetadataJSONOutputPath = {"j", "json-path"}, + .SourceCodeOutputPathOutputPath = {"s", "source-path"}, +} + +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.", + .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", +} + +CLIOutputSettings :: struct { + // 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), +} + +CLIMetadataSettings :: struct { + json_path: Maybe(string), + source_code_path: Maybe(string), +} + +CLIPackerSettings :: struct { + output: Maybe(CLIOutputSettings), + metadata: Maybe(CLIMetadataSettings), + json_config_path: Maybe(string), +} + +CLIFlag :: struct { + flag: string, + value: string, + cli_type: CLIFlagType, +} + +categorize_arg :: proc(flag: string) -> (flag_type: CLIFlagType) { + flag_type = .Unknown + for flag_strings, enum_flag_type in CLI_FLAG_STRINGS { + for flag_string in flag_strings { + if flag == flag_string { + flag_type = enum_flag_type + return + } + } + } + + return +} + +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) + } +} + +parse_arguments :: proc(args: []string) -> (cliargs: map[CLIFlagType]CLIFlag) { + cliargs = make(map[CLIFlagType]CLIFlag) + + for arg in args { + arg_name_and_value, err := s.split(arg, ":") + if err != nil {continue} + name := arg_name_and_value[0] + + if name[0] == '-' { + name = name[1:] + value: string + flag_type := categorize_arg(name) + + if len(arg_name_and_value) > 1 { + value = arg_name_and_value[1] + } + + map_insert(&cliargs, flag_type, CLIFlag{name, value, flag_type}) + } + } + + return +} diff --git a/src/generator/generator.odin b/src/generator/generator.odin new file mode 100644 index 0000000..fce6a4c --- /dev/null +++ b/src/generator/generator.odin @@ -0,0 +1,598 @@ +package generator + +import ase "../../vendors/odin-aseprite" +import "core:encoding/json" +import "core:fmt" +import "core:log" +import "core:os" +import fp "core:path/filepath" +import "core:slice" +import "core:strings" +import rl "vendor:raylib" +import stbrp "vendor:stb/rect_pack" + +when ODIN_OS == .Windows { + OS_FILE_SEPARATOR :: "\\" +} else { + OS_FILE_SEPARATOR :: "/" +} + +CellData :: struct { + layer_index: u16, + opacity: u8, + frame_index: i32, + img: rl.Image, +} + +AtlasEntry :: struct { + path: string, + cells: [dynamic]CellData, + frames: i32, + layer_names: [dynamic]string, + layer_cell_count: [dynamic]i32, +} + +SpriteAtlasMetadata :: struct { + name: string, + location: [2]i32, + size: [2]i32, +} + +SourceCodeGeneratorMetadata :: struct { + file_defines: struct { + top: string, + bottom: string, + file_name: string, + file_extension: string, + }, + lanugage_settings: struct { + first_class_enum_arrays: bool, // for languages that support creating arrays that contain for each enum value an entry in the enum_data.entry_line: .EnumCase = {array entry} + }, + custom_data_type: struct { + name: string, + type_declaration: string, // contains one param: custom_data_type.name + the rest of the type declaration like braces of the syntax & the type members + }, + enum_data: struct { + name: string, + begin_line: string, // contains one params: enum_data.name + entry_line: string, + end_line: string, + }, + array_data: struct { + name: string, + type: string, + begin_line: string, // array begin line contains 2 params in the listed order: array.name, array.type + entry_line: string, // array entry contains 5 params in the listed order: cell.name, cell.location.x, cell.location.y, cell.size.x, cell.size.y, + end_line: string, + }, +} + +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", + bottom = "", + file_name = "metadata", + file_extension = ".odin", + }, + custom_data_type = { + name = "AtlasRect", + type_declaration = "%v :: struct {{ x, y, w, h: i32 }}\n\n", + }, + enum_data = { + name = "AtlasEnum", + begin_line = "%v :: enum {{\n", + entry_line = "\t%s,\n", + end_line = "}\n\n", + }, + array_data = { + name = "ATLAS_SPRITES", + type = "[AtlasSprite]AtlasRect", + begin_line = "%v := %v {{\n", + entry_line = "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n", + end_line = "}\n\n", + }, + lanugage_settings = {first_class_enum_arrays = true}, +} + + +cpp_source_generator_metadata := SourceCodeGeneratorMetadata { + file_defines = { + top = "#include \n\n", + bottom = "", + file_name = "metadata", + file_extension = ".hpp", + }, + custom_data_type = { + name = "AtlasRect", + type_declaration = "struct %v {{\n\tint x;\n\tint y;\n\tint w;\n\tint h;\n}};\n\n", + }, + enum_data = { + name = "AtlasEnum", + begin_line = "enum %v {{\n", + entry_line = "\t%s,\n", + end_line = "\n\tCOUNT\n}\n\n", + }, + array_data = { + name = "ATLAS_SPRITES", + type = "AtlasRect[size_t(AtlasEnum::COUNT)-1]", + begin_line = "{1} {0} = {{\n", + entry_line = "\t{{ {1}, {2}, {3}, {4} }},\n", + end_line = "}\n\n", + }, +} + +/* + 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, + alloc := context.allocator, +) -> strings.Builder { + sb := strings.builder_make(alloc) + strings.write_string(&sb, "package atlas_bindings\n\n") + + // Introduce the Rect type + strings.write_string(&sb, "AtlasRect :: struct { x, y, w, h: i32 }\n\n") + // start enum + strings.write_string(&sb, "AtlasSprite :: enum {\n") + { + for cell in metadata { + strings.write_string(&sb, fmt.aprintf("\t%s,\n", cell.name)) + } + } + // end enum + strings.write_string(&sb, "}\n\n") + + // 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") + { + entry: string + for cell in metadata { + entry = fmt.aprintf( + "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n", + cell.name, + cell.location.x, + cell.location.y, + cell.size.x, + cell.size.y, + ) + strings.write_string(&sb, entry) + } + } + // end offsets array + strings.write_string(&sb, "}\n\n") + + log.info("\n", strings.to_string(sb)) + + return sb +} + +metadata_source_code_generate :: proc( + metadata: []SpriteAtlasMetadata, + codegen: SourceCodeGeneratorMetadata, + alloc := context.allocator, +) -> strings.Builder { + + sb := strings.builder_make(alloc) + // strings.write_string(&sb, "package atlas_bindings\n\n") + strings.write_string(&sb, codegen.file_defines.top) + + // Introduce the Rect type + strings.write_string( + &sb, + fmt.aprintf(codegen.custom_data_type.type_declaration, codegen.custom_data_type.name), + ) + // start enum + 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(codegen.enum_data.entry_line, cell.name)) + } + } + // end enum + strings.write_string(&sb, codegen.enum_data.end_line) + + // start offsets array + strings.write_string( + &sb, + fmt.aprintf( + codegen.array_data.begin_line, + codegen.array_data.name, + codegen.array_data.type, + ), + ) + { + entry: string + for cell in metadata { + entry = fmt.aprintf( + codegen.array_data.entry_line, // "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n", + cell.name, + cell.location.x, + cell.location.y, + cell.size.x, + cell.size.y, + ) + strings.write_string(&sb, entry) + } + } + // end offsets array + + strings.write_string(&sb, codegen.array_data.end_line) + + strings.write_string(&sb, codegen.file_defines.bottom) + + log.info("\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!") + return + } + + image := rl.LoadImageFromTexture(render_texture_target.texture) + rl.ImageFlipVertical(&image) + + cstring_atlas_output_path := strings.clone_to_cstring( + strings.concatenate({output_path, OS_FILE_SEPARATOR, "atlas.png"}), + ) + + rl.ExportImage(image, cstring_atlas_output_path) + + if len(metadata) > 0 { + log.info("Building metadata...") + if json_metadata, jok := json.marshal(metadata); jok == nil { + os.write_entire_file( + strings.concatenate( + {output_path, OS_FILE_SEPARATOR, "metadata.json"}, + context.temp_allocator, + ), + json_metadata, + ) + delete(json_metadata) + } else { + log.error("Failed to marshall the atlas metadata to a json!") + } + + // TODO(stefan): Think of a more generic alternative to just straight output to a odin file + // maybe supply a config.json that defines the start, end, line by line entry and enum format strings + // this way you can essentially support any language + sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:]) + defer strings.builder_destroy(&sb) + odin_metadata := strings.to_string(sb) + ok := os.write_entire_file( + strings.concatenate( + {output_path, OS_FILE_SEPARATOR, "metadata.odin"}, + context.temp_allocator, + ), + transmute([]byte)odin_metadata, + ) + if !ok { + log.error("Failed to save 'metadata.odin'") + } + } else { + log.error("No metadata to export!") + } + +} + +save_metadata_simple :: proc( + output_path: string, + 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!") + return + } + + metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); if !ok { + log.error("No metadata to export!") + } + + log.info("Building metadata...") + if json_file_name_ok { + if json_metadata, jok := json.marshal(metadata); jok == nil { + json_output_path := strings.concatenate( + {output_path, OS_FILE_SEPARATOR, json_file_base_name}, + ) + if ok = os.write_entire_file(json_output_path, json_metadata); !ok { + log.errorf("Failed to write json to file: ", json_output_path) + } + } else { + log.error("Failed to marshall the atlas metadata to a json!") + } + } + + // note(stefan): Having source_file_name & source_gen_metadata is redundant but this is fine for now + if source_file_name_ok { + // 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, + 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) + } + } 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"}, + ) + + 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) + } + } + } +} + +save_metadata :: proc( + settings: CLIPackerSettings, + atlas_entries: []AtlasEntry, + atlas_metadata: []SpriteAtlasMetadata, +) { + metadata, ok := settings.metadata.(CLIMetadataSettings); if !ok do return + + if json_path, ok := metadata.json_path.(string); ok { + json_bytes, jerr := json.marshal(atlas_metadata) + if jerr == nil { + os.write_entire_file(json_path, json_bytes) + } else { + log.error("Failed to marshall metadata") + } + } + if source_code_path, ok := metadata.source_code_path.(string); ok { + sb := metadata_source_code_generate(atlas_metadata, odin_source_generator_metadata) + source_code_output_str := strings.to_string(sb) + os.write_entire_file(source_code_path, transmute([]byte)source_code_output_str) + } +} diff --git a/src/handle_array.odin b/src/handle_array.odin deleted file mode 100644 index 69d9709..0000000 --- a/src/handle_array.odin +++ /dev/null @@ -1,141 +0,0 @@ -// This handle-based array gives you a statically allocated array where you can -// use index based handles instead of pointers. The handles have a generation -// that makes sure you don't get bugs when slots are re-used. -// Read more about it here: https://floooh.github.io/2018/06/17/handles-vs-pointers.html */ - -package game - -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/helpers.odin b/src/helpers.odin deleted file mode 100644 index 190d044..0000000 --- a/src/helpers.odin +++ /dev/null @@ -1,41 +0,0 @@ -// generic odin helpers - -package game - -import "core:intrinsics" -import "core:reflect" -import "core:strings" - -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) -} diff --git a/src/main_hot_reload/main_hot_reload.odin b/src/main_hot_reload/main_hot_reload.odin deleted file mode 100644 index 99f4692..0000000 --- a/src/main_hot_reload/main_hot_reload.odin +++ /dev/null @@ -1,196 +0,0 @@ -// Development game exe. Loads game.dll and reloads it whenever it changes. - -package main - -import "core:c/libc" -import "core:dynlib" -import "core:fmt" -import "core:log" -import "core:mem" -import "core:os" - -when ODIN_OS == .Windows { - DLL_EXT :: ".dll" -} else when ODIN_OS == .Darwin { - DLL_EXT :: ".dylib" -} else { - DLL_EXT :: ".so" -} - -copy_dll :: proc(to: string) -> bool { - exit: i32 - when ODIN_OS == .Windows { - exit = libc.system(fmt.ctprintf("copy game.dll {0}", to)) - } else { - exit = libc.system(fmt.ctprintf("cp game" + DLL_EXT + " {0}", to)) - } - - if exit != 0 { - fmt.printfln("Failed to copy game" + DLL_EXT + " to {0}", to) - return false - } - - return true -} - -GameAPI :: struct { - lib: dynlib.Library, - init_window: proc(), - init: proc(), - update: proc() -> bool, - shutdown: proc(), - shutdown_window: proc(), - memory: proc() -> rawptr, - memory_size: proc() -> int, - hot_reloaded: proc(mem: rawptr), - force_reload: proc() -> bool, - force_restart: proc() -> bool, - modification_time: os.File_Time, - api_version: int, -} - -load_game_api :: proc(api_version: int) -> (api: GameAPI, ok: bool) { - mod_time, mod_time_error := os.last_write_time_by_name("game" + DLL_EXT) - if mod_time_error != os.ERROR_NONE { - fmt.printfln( - "Failed getting last write time of game" + DLL_EXT + ", error code: {1}", - mod_time_error, - ) - return - } - - // NOTE: this needs to be a relative path for Linux to work. - game_dll_name := fmt.tprintf( - "{0}game_{1}" + DLL_EXT, - "./" when ODIN_OS != .Windows else "", - api_version, - ) - copy_dll(game_dll_name) or_return - - _, ok = dynlib.initialize_symbols(&api, game_dll_name, "game_", "lib") - if !ok { - fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error()) - } - - api.api_version = api_version - api.modification_time = mod_time - ok = true - - return -} - -unload_game_api :: proc(api: ^GameAPI) { - if api.lib != nil { - if !dynlib.unload_library(api.lib) { - fmt.printfln("Failed unloading lib: {0}", dynlib.last_error()) - } - } - - if os.remove(fmt.tprintf("game_{0}" + DLL_EXT, api.api_version)) != 0 { - fmt.printfln("Failed to remove game_{0}" + DLL_EXT + " copy", api.api_version) - } -} - -main :: proc() { - context.logger = log.create_console_logger() - - default_allocator := context.allocator - tracking_allocator: mem.Tracking_Allocator - mem.tracking_allocator_init(&tracking_allocator, default_allocator) - context.allocator = mem.tracking_allocator(&tracking_allocator) - - reset_tracking_allocator :: proc(a: ^mem.Tracking_Allocator) -> bool { - err := false - - for _, value in a.allocation_map { - fmt.printf("%v: Leaked %v bytes\n", value.location, value.size) - err = true - } - - mem.tracking_allocator_clear(a) - return err - } - - game_api_version := 0 - game_api, game_api_ok := load_game_api(game_api_version) - - if !game_api_ok { - fmt.println("Failed to load Game API") - return - } - - game_api_version += 1 - game_api.init_window() - game_api.init() - - old_game_apis := make([dynamic]GameAPI, default_allocator) - - window_open := true - for window_open { - window_open = game_api.update() - force_reload := game_api.force_reload() - force_restart := game_api.force_restart() - reload := force_reload || force_restart - game_dll_mod, game_dll_mod_err := os.last_write_time_by_name("game" + DLL_EXT) - - if game_dll_mod_err == os.ERROR_NONE && game_api.modification_time != game_dll_mod { - reload = true - } - - if reload { - new_game_api, new_game_api_ok := load_game_api(game_api_version) - - if new_game_api_ok { - if game_api.memory_size() != new_game_api.memory_size() || force_restart { - game_api.shutdown() - reset_tracking_allocator(&tracking_allocator) - - for &g in old_game_apis { - unload_game_api(&g) - } - - clear(&old_game_apis) - unload_game_api(&game_api) - game_api = new_game_api - game_api.init() - } else { - append(&old_game_apis, game_api) - game_memory := game_api.memory() - game_api = new_game_api - game_api.hot_reloaded(game_memory) - } - - game_api_version += 1 - } - } - - for b in tracking_allocator.bad_free_array { - log.error("Bad free at: %v", b.location) - } - - clear(&tracking_allocator.bad_free_array) - free_all(context.temp_allocator) - } - - free_all(context.temp_allocator) - game_api.shutdown() - reset_tracking_allocator(&tracking_allocator) - - for &g in old_game_apis { - unload_game_api(&g) - } - - delete(old_game_apis) - - game_api.shutdown_window() - unload_game_api(&game_api) - mem.tracking_allocator_destroy(&tracking_allocator) -} - -// make game use good GPU on laptops etc - -@(export) -NvOptimusEnablement: u32 = 1 - -@(export) -AmdPowerXpressRequestHighPerformance: i32 = 1 diff --git a/src/main_release/main_release.odin b/src/main_release/main_release.odin deleted file mode 100644 index 1d335d1..0000000 --- a/src/main_release/main_release.odin +++ /dev/null @@ -1,77 +0,0 @@ -// For making a release exe that does not use hot reload. - -package main_release - -import "core:log" -import "core:os" - -import game ".." - -UseTrackingAllocator :: #config(UseTrackingAllocator, false) - -main :: proc() { - when UseTrackingAllocator { - default_allocator := context.allocator - tracking_allocator: Tracking_Allocator - tracking_allocator_init(&tracking_allocator, default_allocator) - context.allocator = allocator_from_tracking_allocator(&tracking_allocator) - } - - mode: int = 0 - when ODIN_OS == .Linux || ODIN_OS == .Darwin { - mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH - } - - logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode) - - if logh_err == os.ERROR_NONE { - os.stdout = logh - os.stderr = logh - } - - logger := - logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger() - context.logger = logger - - game.game_init_window() - game.game_init() - - window_open := true - for window_open { - window_open = game.game_update() - - when UseTrackingAllocator { - for b in tracking_allocator.bad_free_array { - log.error("Bad free at: %v", b.location) - } - - clear(&tracking_allocator.bad_free_array) - } - - free_all(context.temp_allocator) - } - - free_all(context.temp_allocator) - game.game_shutdown() - game.game_shutdown_window() - - if logh_err == os.ERROR_NONE { - log.destroy_file_logger(&logger) - } - - when UseTrackingAllocator { - for key, value in tracking_allocator.allocation_map { - log.error("%v: Leaked %v bytes\n", value.location, value.size) - } - - tracking_allocator_destroy(&tracking_allocator) - } -} - -// make game use good GPU on laptops etc - -@(export) -NvOptimusEnablement: u32 = 1 - -@(export) -AmdPowerXpressRequestHighPerformance: i32 = 1 diff --git a/src/math.odin b/src/math.odin deleted file mode 100644 index 4f6634c..0000000 --- a/src/math.odin +++ /dev/null @@ -1,8 +0,0 @@ -package game - -Vec2i :: [2]int -Vec2 :: [2]f32 - -vec2_from_vec2i :: proc(p: Vec2i) -> Vec2 { - return {f32(p.x), f32(p.y)} -} diff --git a/src/raylib_helpers.odin b/src/raylib_helpers.odin deleted file mode 100644 index d884fc4..0000000 --- a/src/raylib_helpers.odin +++ /dev/null @@ -1,70 +0,0 @@ -package game - -import "core:slice" -import rl "vendor:raylib" - -Texture :: rl.Texture -Color :: rl.Color - -texture_rect :: proc(tex: Texture, flip_x: bool) -> Rect { - return( - { - x = 0, - y = 0, - width = flip_x ? -f32(tex.width) : f32(tex.width), - height = f32(tex.height), - } \ - ) -} - -load_premultiplied_alpha_ttf_from_memory :: proc(file_data: []byte, font_size: int) -> rl.Font { - font := rl.Font { - baseSize = i32(font_size), - glyphCount = 95, - } - - font.glyphs = rl.LoadFontData( - &file_data[0], - i32(len(file_data)), - font.baseSize, - {}, - font.glyphCount, - .DEFAULT, - ) - - if font.glyphs != nil { - font.glyphPadding = 4 - - atlas := rl.GenImageFontAtlas( - font.glyphs, - &font.recs, - font.glyphCount, - font.baseSize, - font.glyphPadding, - 0, - ) - atlas_u8 := slice.from_ptr((^u8)(atlas.data), int(atlas.width * atlas.height * 2)) - - for i in 0 ..< atlas.width * atlas.height { - a := atlas_u8[i * 2 + 1] - v := atlas_u8[i * 2] - atlas_u8[i * 2] = u8(f32(v) * (f32(a) / 255)) - } - - font.texture = rl.LoadTextureFromImage(atlas) - rl.SetTextureFilter(font.texture, .BILINEAR) - - // Update glyphs[i].image to use alpha, required to be used on ImageDrawText() - for i in 0 ..< font.glyphCount { - rl.UnloadImage(font.glyphs[i].image) - font.glyphs[i].image = rl.ImageFromImage(atlas, font.recs[i]) - } - //TRACELOG(LOG_INFO, "FONT: Data loaded successfully (%i pixel size | %i glyphs)", font.baseSize, font.glyphCount); - - rl.UnloadImage(atlas) - } else { - font = rl.GetFontDefault() - } - - return font -} diff --git a/src/rect.odin b/src/rect.odin deleted file mode 100644 index 4291aa2..0000000 --- a/src/rect.odin +++ /dev/null @@ -1,66 +0,0 @@ -// procs for modifying and managing rects - -package game - -import rl "vendor:raylib" - -Rect :: rl.Rectangle - -RectEmpty :: Rect{} - -split_rect_top :: proc(r: Rect, y: f32, m: f32) -> (top, bottom: Rect) { - top = r - bottom = r - top.y += m - top.height = y - bottom.y += y + m - bottom.height -= y + m - return -} - -split_rect_left :: proc(r: Rect, x: f32, m: f32) -> (left, right: Rect) { - left = r - right = r - left.width = x - right.x += x + m - right.width -= x + m - return -} - -split_rect_bottom :: proc(r: rl.Rectangle, y: f32, m: f32) -> (top, bottom: rl.Rectangle) { - top = r - top.height -= y + m - bottom = r - bottom.y = top.y + top.height + m - bottom.height = y - return -} - -split_rect_right :: proc(r: Rect, x: f32, m: f32) -> (left, right: Rect) { - left = r - right = r - right.width = x - left.width -= x + m - right.x = left.x + left.width - return -} - -rect_middle :: proc(r: Rect) -> Vec2 { - return {r.x + f32(r.width) * 0.5, r.y + f32(r.height) * 0.5} -} - -inset_rect :: proc(r: Rect, x: f32, y: f32) -> Rect { - return {r.x + x, r.y + y, r.width - x * 2, r.height - y * 2} -} - -rect_add_pos :: proc(r: Rect, p: Vec2) -> Rect { - return {r.x + p.x, r.y + p.y, r.width, r.height} -} - -mouse_in_rect :: proc(r: Rect) -> bool { - return rl.CheckCollisionPointRec(rl.GetMousePosition(), r) -} - -mouse_in_world_rect :: proc(r: Rect, camera: rl.Camera2D) -> bool { - return rl.CheckCollisionPointRec(rl.GetScreenToWorld2D(rl.GetMousePosition(), camera), r) -} diff --git a/src/save_output.odin b/src/save_output.odin deleted file mode 100644 index e69f405..0000000 --- a/src/save_output.odin +++ /dev/null @@ -1,17 +0,0 @@ -package game - -import "core:strings" -import rl "vendor:raylib" - -save_output :: proc() { - image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture) - rl.ImageFlipVertical(&image) - when ODIN_OS == .Windows { - atlas_path :: "\\atlas.png" - } else { - atlas_path :: "/atlas.png" - } - output_path := strings.concatenate({g_mem.output_folder_path, atlas_path}) - cstring_output_path := strings.clone_to_cstring(output_path) - rl.ExportImage(image, cstring_output_path) -} diff --git a/src/symbol_exports.odin b/src/symbol_exports.odin deleted file mode 100644 index 628c968..0000000 --- a/src/symbol_exports.odin +++ /dev/null @@ -1,79 +0,0 @@ -package game - -import rl "vendor:raylib" - -@(export) -game_update :: proc() -> bool { - update() - draw() - return !rl.WindowShouldClose() -} - -@(export) -game_init_window :: proc() { - rl.SetConfigFlags({.WINDOW_RESIZABLE}) - rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer, Powered by Raylib & Odin") - rl.SetWindowPosition(200, 200) - rl.SetWindowMinSize(1400, 800) -} - -@(export) -game_init :: proc() { - g_mem = new(GameMemory) - - g_mem^ = GameMemory{} - - game_hot_reloaded(g_mem) - - 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(2048, 2048) - - 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/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin b/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin new file mode 100644 index 0000000..0f20238 --- /dev/null +++ b/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin @@ -0,0 +1,296 @@ +package main + +import clay "../clay-odin" +import "base:runtime" +import "core:math" +import "core:strings" +import "core:unicode/utf8" +import rl "vendor:raylib" + +Raylib_Font :: struct { + fontId: u16, + font: rl.Font, +} + +clay_color_to_rl_color :: proc(color: clay.Color) -> rl.Color { + return {u8(color.r), u8(color.g), u8(color.b), u8(color.a)} +} + +raylib_fonts := [dynamic]Raylib_Font{} + +// Alias for compatibility, default to ascii support +measure_text :: measure_text_ascii + +measure_text_unicode :: proc "c" ( + text: clay.StringSlice, + config: ^clay.TextElementConfig, + userData: rawptr, +) -> clay.Dimensions { + // Needed for grapheme_count + context = runtime.default_context() + + line_width: f32 = 0 + + font := raylib_fonts[config.fontId].font + text_str := string(text.chars[:text.length]) + + // This function seems somewhat expensive, if you notice performance issues, you could assume + // - 1 codepoint per visual character (no grapheme clusters), where you can get the length from the loop + // - 1 byte per visual character (ascii), where you can get the length with `text.length` + // see `measure_text_ascii` + grapheme_count, _, _ := utf8.grapheme_count(text_str) + + for letter, byte_idx in text_str { + glyph_index := rl.GetGlyphIndex(font, letter) + + glyph := font.glyphs[glyph_index] + + if glyph.advanceX != 0 { + line_width += f32(glyph.advanceX) + } else { + line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX) + } + } + + scaleFactor := f32(config.fontSize) / f32(font.baseSize) + + // Note: + // I'd expect this to be `grapheme_count - 1`, + // but that seems to be one letterSpacing too small + // maybe that's a raylib bug, maybe that's Clay? + total_spacing := f32(grapheme_count) * f32(config.letterSpacing) + + return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)} +} + +measure_text_ascii :: proc "c" ( + text: clay.StringSlice, + config: ^clay.TextElementConfig, + userData: rawptr, +) -> clay.Dimensions { + line_width: f32 = 0 + + font := raylib_fonts[config.fontId].font + text_str := string(text.chars[:text.length]) + + for i in 0 ..< len(text_str) { + glyph_index := text_str[i] - 32 + + glyph := font.glyphs[glyph_index] + + if glyph.advanceX != 0 { + line_width += f32(glyph.advanceX) + } else { + line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX) + } + } + + scaleFactor := f32(config.fontSize) / f32(font.baseSize) + + // Note: + // I'd expect this to be `len(text_str) - 1`, + // but that seems to be one letterSpacing too small + // maybe that's a raylib bug, maybe that's Clay? + total_spacing := f32(len(text_str)) * f32(config.letterSpacing) + + return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)} +} + +clay_raylib_render :: proc( + render_commands: ^clay.ClayArray(clay.RenderCommand), + allocator := context.temp_allocator, +) { + for i in 0 ..< render_commands.length { + render_command := clay.RenderCommandArray_Get(render_commands, i) + bounds := render_command.boundingBox + + switch render_command.commandType { + case .None: // None + case .Text: + config := render_command.renderData.text + + text := string(config.stringContents.chars[:config.stringContents.length]) + + // Raylib uses C strings instead of Odin strings, so we need to clone + // Assume this will be freed elsewhere since we default to the temp allocator + cstr_text := strings.clone_to_cstring(text, allocator) + + font := raylib_fonts[config.fontId].font + rl.DrawTextEx( + font, + cstr_text, + {bounds.x, bounds.y}, + f32(config.fontSize), + f32(config.letterSpacing), + clay_color_to_rl_color(config.textColor), + ) + case .Image: + config := render_command.renderData.image + tint := config.backgroundColor + if tint == 0 { + tint = {255, 255, 255, 255} + } + + imageTexture := (^rl.Texture2D)(config.imageData) + rl.DrawTextureEx( + imageTexture^, + {bounds.x, bounds.y}, + 0, + bounds.width / f32(imageTexture.width), + clay_color_to_rl_color(tint), + ) + case .ScissorStart: + rl.BeginScissorMode( + i32(math.round(bounds.x)), + i32(math.round(bounds.y)), + i32(math.round(bounds.width)), + i32(math.round(bounds.height)), + ) + case .ScissorEnd: + rl.EndScissorMode() + case .Rectangle: + config := render_command.renderData.rectangle + if config.cornerRadius.topLeft > 0 { + radius: f32 = (config.cornerRadius.topLeft * 2) / min(bounds.width, bounds.height) + draw_rect_rounded( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + radius, + config.backgroundColor, + ) + } else { + draw_rect(bounds.x, bounds.y, bounds.width, bounds.height, config.backgroundColor) + } + case .Border: + config := render_command.renderData.border + // Left border + if config.width.left > 0 { + draw_rect( + bounds.x, + bounds.y + config.cornerRadius.topLeft, + f32(config.width.left), + bounds.height - config.cornerRadius.topLeft - config.cornerRadius.bottomLeft, + config.color, + ) + } + // Right border + if config.width.right > 0 { + draw_rect( + bounds.x + bounds.width - f32(config.width.right), + bounds.y + config.cornerRadius.topRight, + f32(config.width.right), + bounds.height - config.cornerRadius.topRight - config.cornerRadius.bottomRight, + config.color, + ) + } + // Top border + if config.width.top > 0 { + draw_rect( + bounds.x + config.cornerRadius.topLeft, + bounds.y, + bounds.width - config.cornerRadius.topLeft - config.cornerRadius.topRight, + f32(config.width.top), + config.color, + ) + } + // Bottom border + if config.width.bottom > 0 { + draw_rect( + bounds.x + config.cornerRadius.bottomLeft, + bounds.y + bounds.height - f32(config.width.bottom), + bounds.width - + config.cornerRadius.bottomLeft - + config.cornerRadius.bottomRight, + f32(config.width.bottom), + config.color, + ) + } + + // Rounded Borders + if config.cornerRadius.topLeft > 0 { + draw_arc( + bounds.x + config.cornerRadius.topLeft, + bounds.y + config.cornerRadius.topLeft, + config.cornerRadius.topLeft - f32(config.width.top), + config.cornerRadius.topLeft, + 180, + 270, + config.color, + ) + } + if config.cornerRadius.topRight > 0 { + draw_arc( + bounds.x + bounds.width - config.cornerRadius.topRight, + bounds.y + config.cornerRadius.topRight, + config.cornerRadius.topRight - f32(config.width.top), + config.cornerRadius.topRight, + 270, + 360, + config.color, + ) + } + if config.cornerRadius.bottomLeft > 0 { + draw_arc( + bounds.x + config.cornerRadius.bottomLeft, + bounds.y + bounds.height - config.cornerRadius.bottomLeft, + config.cornerRadius.bottomLeft - f32(config.width.top), + config.cornerRadius.bottomLeft, + 90, + 180, + config.color, + ) + } + if config.cornerRadius.bottomRight > 0 { + draw_arc( + bounds.x + bounds.width - config.cornerRadius.bottomRight, + bounds.y + bounds.height - config.cornerRadius.bottomRight, + config.cornerRadius.bottomRight - f32(config.width.bottom), + config.cornerRadius.bottomRight, + 0.1, + 90, + config.color, + ) + } + case clay.RenderCommandType.Custom: + // Implement custom element rendering here + } + } +} + +// Helper procs, mainly for repeated conversions + +@(private = "file") +draw_arc :: proc( + x, y: f32, + inner_rad, outer_rad: f32, + start_angle, end_angle: f32, + color: clay.Color, +) { + rl.DrawRing( + {math.round(x), math.round(y)}, + math.round(inner_rad), + outer_rad, + start_angle, + end_angle, + 10, + clay_color_to_rl_color(color), + ) +} + +@(private = "file") +draw_rect :: proc(x, y, w, h: f32, color: clay.Color) { + rl.DrawRectangle( + i32(math.round(x)), + i32(math.round(y)), + i32(math.round(w)), + i32(math.round(h)), + clay_color_to_rl_color(color), + ) +} + +@(private = "file") +draw_rect_rounded :: proc(x, y, w, h: f32, radius: f32, color: clay.Color) { + rl.DrawRectangleRounded({x, y, w, h}, radius, 8, clay_color_to_rl_color(color)) +} diff --git a/vendors/dialog/build.bat b/vendors/dialog/build.bat new file mode 100644 index 0000000..6553a0f --- /dev/null +++ b/vendors/dialog/build.bat @@ -0,0 +1,5 @@ +@echo off + +cl /c libtinyfiledialogs\tinyfiledialogs.c + +lib tinyfiledialogs.obj /out:tinyfiledialogs.lib \ No newline at end of file diff --git a/vendors/dialog/build.sh b/vendors/dialog/build.sh new file mode 100644 index 0000000..43b3bbc --- /dev/null +++ b/vendors/dialog/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +set -e + +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/vendors/dialog/libtinyfiledialogs new file mode 160000 index 0000000..cc6b593 --- /dev/null +++ b/vendors/dialog/libtinyfiledialogs @@ -0,0 +1 @@ +Subproject commit cc6b593c029110af8045826ce691f540c85e850c diff --git a/vendors/dialog/tinyfiledialogs.odin b/vendors/dialog/tinyfiledialogs.odin new file mode 100644 index 0000000..ba43e6e --- /dev/null +++ b/vendors/dialog/tinyfiledialogs.odin @@ -0,0 +1,161 @@ +package tinyfiledialogs + +import "core:c" +import "core:mem" +import "core:strings" + +when ODIN_OS == .Windows { + foreign import lib {"tinyfiledialogs.lib", "system:comdlg32.lib", "system:Ole32.lib"} +} else when ODIN_OS == .Linux || ODIN_OS == .Darwin { + foreign import lib "libtinyfiledialogs.a" +} + +@(default_calling_convention = "c", link_prefix = "tinyfd_") +foreign lib { + notifyPopup :: proc(title, message, icon_type: cstring) -> c.int --- + + messageBox :: proc(title, message, dialog_type, icon_type: cstring, default_button: c.int) -> c.int --- + inputBox :: proc(title, message, default_input: cstring) -> cstring --- + + saveFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring) -> cstring --- + openFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring, allow_multi: c.int) -> cstring --- + + selectFolderDialog :: proc(title, default_path: cstring) -> cstring --- + + colorChooser :: proc(title, default_hex_rgb: cstring, default_rgb, result_rgb: [3]byte) -> cstring --- +} + +select_folder_dialog :: proc( + title, default_path: string, + alloc := context.allocator, + temp_alloc := context.temp_allocator, +) -> ( + path: string, + success: bool, +) { + ctitle: cstring = nil + cdefault_path: cstring = nil + err: mem.Allocator_Error + + if len(title) > 0 { + ctitle, err = strings.clone_to_cstring(title, temp_alloc) + if err != nil { + return {}, false + } + } + if len(default_path) > 0 { + cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc) + if err != nil { + return {}, false + } + } + res := selectFolderDialog(ctitle, cdefault_path) + path, err = strings.clone_from_cstring(res, alloc) + if err != nil { + return {}, false + } + return path, true +} + +save_file_dialog :: proc( + title, default_path: string, + pattern_count: i32, + patterns: []string, + file_desc: string, + alloc := context.allocator, + temp_alloc := context.temp_allocator, +) -> ( + path: string, + success: bool, +) { + ctitle: cstring = nil + cdefault_path: cstring = nil + cfile_desc: cstring = nil + cpatterns: [^]cstring = nil + err: mem.Allocator_Error + + if len(title) > 0 { + ctitle, err = strings.clone_to_cstring(title, temp_alloc) + if err != nil { + return {}, false + } + } + if len(default_path) > 0 { + cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc) + if err != nil { + return {}, false + } + } + if len(cfile_desc) > 0 { + cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc) + if err != nil { + return {}, false + } + } + + if pattern_count > 0 { + cpatterns = make([^]cstring, pattern_count + 1, temp_alloc) + for p, i in patterns { + cpatterns[i] = strings.clone_to_cstring(p) + } + cpatterns[pattern_count] = nil // null terminate the array + } + res := saveFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc) + path, err = strings.clone_from_cstring(res, alloc) + if err != nil { + return {}, false + } + return path, true +} + +open_file_dialog :: proc( + title, default_path: string, + pattern_count: i32, + patterns: []string, + file_desc: string, + allow_multi: i32, + alloc := context.allocator, + temp_alloc := context.temp_allocator, +) -> ( + path: string, + success: bool, +) { + ctitle: cstring = nil + cdefault_path: cstring = nil + cfile_desc: cstring = nil + cpatterns: [^]cstring = nil + err: mem.Allocator_Error + + if len(title) > 0 { + ctitle, err = strings.clone_to_cstring(title, temp_alloc) + if err != nil { + return {}, false + } + } + if len(default_path) > 0 { + cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc) + if err != nil { + return {}, false + } + } + if len(cfile_desc) > 0 { + cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc) + if err != nil { + return {}, false + } + } + + if pattern_count > 0 { + cpatterns = make([^]cstring, pattern_count + 1, temp_alloc) + for p, i in patterns { + cpatterns[i] = strings.clone_to_cstring(p) + } + cpatterns[pattern_count] = nil // null terminate the array + } + res := openFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc, allow_multi) + path, err = strings.clone_from_cstring(res, alloc) + if err != nil { + return {}, false + } + return path, true +} diff --git a/vendors/odin-aseprite b/vendors/odin-aseprite new file mode 160000 index 0000000..72ea2e8 --- /dev/null +++ b/vendors/odin-aseprite @@ -0,0 +1 @@ +Subproject commit 72ea2e8094a5f05074d4c4f2faafdba42e54673c