Compare commits

...

6 commits

Author SHA1 Message Date
3c4ad68059 Refactor depencies, add clay to the repo in preparation to move from raygui to clay 2026-01-02 21:13:34 +02:00
d748f742f7 Cleanup unused imports 2026-01-02 19:05:24 +02:00
b4f7fb4171 Reorganize globals and introduce an Atlas struct 2026-01-02 19:03:06 +02:00
a51f5c6b57 More cleanup. Hooray! 2026-01-02 18:49:24 +02:00
faf42da522 Update dependencies, refresh README.md, add build scripts for linux(& macos?) 2026-01-02 15:06:34 +02:00
3f1c523ad9 Major refactor:
* Removed any hotreload functionality to simplify the code-base, may introduce it back again if there's a need to redesign the UI as it was used for dynamic prototyping.
* Split the code base into two packages: generator and frontend. The 'generator' package is reponsible for the functionality of making the atlases, etc... while the frontend is pure the immediate mode UI implemented with raygui and application globals
* New build scripts, windows only for now.
2024-08-30 18:05:56 +03:00
46 changed files with 1407 additions and 1760 deletions

15
.gitignore vendored
View file

@ -1,18 +1,11 @@
*.exe
*.sublime-workspace
*.pdb
*.raddbgi
*.dll
*.exp
*.lib
log.txt
*.bin
*.dylib
*.so
*.dSYM
*.a
linux
# gets automagically downloaded through scripts/setup.sh
vendors/clay-odin
vendors/clay-odin.zip
build/
build_generator/
ols.json

9
.gitmodules vendored
View file

@ -1,6 +1,3 @@
[submodule "src/aseprite"]
path = src/aseprite
url = https://github.com/bersK/odin-aseprite.git
[submodule "src/dialog/libtinyfiledialogs"]
path = src/dialog/libtinyfiledialogs
url = https://github.com/native-toolkit/libtinyfiledialogs.git
[submodule "vendors/odin-aseprite"]
path = vendors/odin-aseprite
url = git@github.com:blob1807/odin-aseprite.git

31
.vscode/launch.json vendored
View file

@ -1,31 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "cppvsdbg",
"request": "launch",
"preLaunchTask": "Build Debug",
"name": "Debug",
"program": "${workspaceFolder}/build/game_debug.exe",
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "cppvsdbg",
"request": "launch",
"preLaunchTask": "Build Release",
"name": "Release",
"program": "${workspaceFolder}/game_release.exe",
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "cppvsdbg",
"request": "launch",
"name": "Run File",
"program": "odin",
"args": ["run", "${fileBasename}", "-file"],
"cwd": "${workspaceFolder}"
}
]
}

View file

@ -1,6 +0,0 @@
{
"[odin]": {
"editor.formatOnSave": true,
"editor.tabSize": 4
}
}

92
.vscode/tasks.json vendored
View file

@ -1,92 +0,0 @@
{
"version": "2.0.0",
"command": "",
"args": [],
"tasks": [
{
"label": "Build Debug",
"type": "shell",
"windows": {
"command": "${workspaceFolder}/scripts/build_debug.bat"
},
"linux": {
"command": "${workspaceFolder}/scripts/build_debug.sh"
},
"osx": {
"command": "${workspaceFolder}/scripts/build_debug.sh"
},
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
},
{
"label": "Build Release",
"type": "shell",
"windows": {
"command": "${workspaceFolder}/scripts/build_release.bat"
},
"linux": {
"command": "${workspaceFolder}/scripts/build_release.sh"
},
"osx": {
"command": "${workspaceFolder}/scripts/build_release.sh"
},
"group": "build"
},
{
"label": "Clean build folder(s)",
"type": "shell",
"windows": {
"command": "cd ${workspaceFolder}\\build && rm game*; cd ${workspaceFolder} && rm aseprite_odin_generator*"
},
"group": "build"
},
{
"label": "Build Hot Reload",
"type": "shell",
"windows": {
"command": "${workspaceFolder}/scripts/build_hot_reload.bat; start game.exe"
},
"linux": {
"command": "${workspaceFolder}/scripts/build_hot_reload.sh"
},
"osx": {
"command": "${workspaceFolder}/scripts/build_hot_reload.sh"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": false,
"clear": true
},
"group": "build",
"problemMatcher": []
},
{
"label": "Build&Run Atlas Generator Test",
"type": "shell",
"windows": {
"command": "${workspaceFolder}/scripts/build_generator_debug.bat && build_generator\\aseprite_odin_generator.exe -input-files:value_of_custom_arg -h"
},
"options": {
"cwd": "${workspaceFolder}"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": false,
"clear": true
},
"group": {
"kind": "build",
"isDefault": false
}
}
]
}

View file

@ -1,18 +1,31 @@
# YAAP
Yet-Another-Atlas-Packer by Stefan Stefanov
Yet-Another-Atlas-Packer by bersK (Stefan Stefanov)
## Usage & requirements
> [!IMPORTANT]
> Pull this repo with `--recursive`, the `odin-aseprite` library is pulled in as a submodule.
At least odin compiler version `dev-2025-07:204edd0fc`
Since we need to download/compile some 3rd party dependencies in the vendors folder we need to call
`scripts/setup.[sh|bat]` to compile libtinydialog & download the already compiled `clay` binaries and bindings.
## Description
> [!NOTE]
> Quite frankly if you want automatic atlas packing I would suggest using `stb_rect_pack` directly & an aseprite plugin for exporting your assets in a proper format for packing. Using this atlas packer in an automated fashion is not quite possible and slower as a workflow. I would only use it for experiment projects or places where having a built in atlas packer is not practical.
Simple atlas packer using `stb_rect_pack` from the `stb` family of header libraries & `raylib` for rendering/ui. Here's a quick preview on [youtube](https://youtu.be/4_dKq7G57Lw) of the application.
Simple atlas packer for `.aseprite` files. Generates a packed png & metadata in the form of json and/or source files(cpp, odin, etc...).
> [!CAUTION]
> Does not handle palette files currently.
<a href="https://youtu.be/4_dKq7G57Lw">
<img src="https://raw.githubusercontent.com/bersK/yaap/master/repo_assets/image.png" />
<a/>
Uses `stb_rect_pack` for the image packing & `raylib` for the UI.
The goal of the tool is to take in multiple aseprite files and pack them into a single atlas, outputting some metadata in the process in the form of
json and/or source files for direct use in odin (maybe more languages too).
A quick preview on [youtube](https://youtu.be/4_dKq7G57Lw).
I'm using a library for marshalling the aseprite files found [here](https://github.com/blob1807/odin-aseprite) on github.
<img src="https://raw.githubusercontent.com/bersK/yaap/master/repo_assets/image.png" />
Project template provided by Karl Zylinski found [here](https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template) on github.
## Dependencies
* odin-aseprite [github](https://github.com/blob1807/odin-aseprite)
* raylib (`vendor/raylib`, [link](https://github.com/odin-lang/Odin/tree/master/vendor/raylib))
* stb_rect_pack (`vendor/stb/rect_pack`, [link](https://github.com/odin-lang/Odin/tree/master/vendor/stb/rect_pack))

View file

@ -1,42 +1,29 @@
package generator
package cli
import ase "../aseprite"
import ase "../../vendors/odin-aseprite"
import "core:encoding/json"
import "core:fmt"
import "core:mem"
import "core:os"
import fp "core:path/filepath"
import "core:slice"
import s "core:strings"
import "core:testing"
import rl "vendor:raylib"
import stbrp "vendor:stb/rect_pack"
import gen ".."
import utils "../utils"
import gen "../src/generator"
ATLAS_SIZE :: 512
IMPORT_PATH :: "./src/aseprite_odin_generator/big.aseprite"
EXPORT_PATH :: "./src/aseprite_odin_generator/atlas.png"
IMPORT_PATH :: "./example.aseprite"
EXPORT_PATH :: "./atlas.png"
main :: proc() {
args := utils.parse_arguments(os.args[1:])
fmt.println(args)
if ok := utils.CLIFlagType.Help in args; ok {
fmt.println("Help called!")
utils.print_help()
return
}
ase_file, ase_ok := os.read_entire_file(IMPORT_PATH)
if !ase_ok {
fmt.panicf("Couldn't load file!")
}
cwd := os.get_current_directory()
target_dir := s.concatenate({cwd, "\\src\\aseprite_odin_generator\\"})
target_dir := os.get_current_directory()
atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK)
atlas_entries: [dynamic]gen.AtlasEntry = make([dynamic]gen.AtlasEntry)
@ -45,13 +32,10 @@ main :: proc() {
metadata := gen.pack_atlas_entries(atlas_entries[:], &atlas, 10, 10)
json_bytes, jerr := json.marshal(metadata)
os.write_entire_file("src/aseprite_odin_generator/metadata.json", json_bytes)
os.write_entire_file("./metadata.json", json_bytes)
sb := gen.metadata_source_code_generate(metadata[:], gen.odin_source_generator_metadata)
odin_output_str := s.to_string(sb)
os.write_entire_file(
"src/aseprite_odin_generator/output.odino",
transmute([]byte)odin_output_str,
)
os.write_entire_file("./output.odin", transmute([]byte)odin_output_str)
rl.ExportImage(atlas, EXPORT_PATH)
}

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

5
scripts/build.sh Normal file
View file

@ -0,0 +1,5 @@
FLAGS="-define:RAYLIB_SHARED=true -out:build/yaap -debug"
SRC=src/frontend
mkdir -p build
odin build ${SRC} ${FLAGS}

2
scripts/build_atlas.bat Normal file
View file

@ -0,0 +1,2 @@
@echo off
odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap-debug.exe -debug

View file

@ -0,0 +1,2 @@
@echo off
odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap.exe -o:speed

2
scripts/build_cli.bat Normal file
View file

@ -0,0 +1,2 @@
@echo off
odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli-debug.exe -debug

View file

@ -0,0 +1,2 @@
@echo off
odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli.exe -o:speed

View file

@ -1,2 +0,0 @@
@echo off
odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_debug.exe -debug

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
odin build src/main_release -out:build/game_debug.bin -no-bounds-check -debug

View file

@ -1,2 +0,0 @@
@echo off
odin build src/aseprite_odin_generator -define:RAYLIB_SHARED=true -out:build_generator/aseprite_odin_generator.exe -debug

View file

@ -1,21 +0,0 @@
@echo off
rem Build game.dll
odin build src -show-timings -use-separate-modules -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game.dll -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug
IF %ERRORLEVEL% NEQ 0 exit /b 1
rem If game.exe already running: Then only compile game.dll and exit cleanly
QPROCESS "game.exe">NUL
IF %ERRORLEVEL% EQU 0 exit /b 1
rem build game.exe
odin build src/main_hot_reload -use-separate-modules -out:build/game.exe -strict-style -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug
IF %ERRORLEVEL% NEQ 0 exit /b 1
rem copy raylib.dll from odin folder to here
if not exist "raylib.dll" (
echo "Please copy raylib.dll from <your_odin_compiler>/vendor/raylib/windows/raylib.dll to the same directory as game.exe"
exit /b 1
)
exit /b 0

View file

@ -1,47 +0,0 @@
#!/usr/bin/env bash
VET="-strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon"
# NOTE: this is a recent addition to the Odin compiler, if you don't have this command
# you can change this to the path to the Odin folder that contains vendor, eg: "~/Odin".
ROOT=$(odin root)
if [ ! $? -eq 0 ]; then
echo "Your Odin compiler does not have the 'odin root' command, please update or hardcode it in the script."
exit 1
fi
set -eu
# Figure out the mess that is dynamic libraries.
case $(uname) in
"Darwin")
case $(uname -m) in
"arm64") LIB_PATH="macos-arm64" ;;
*) LIB_PATH="macos" ;;
esac
DLL_EXT=".dylib"
EXTRA_LINKER_FLAGS="-Wl,-rpath $ROOT/vendor/raylib/$LIB_PATH"
;;
*)
DLL_EXT=".so"
EXTRA_LINKER_FLAGS="'-Wl,-rpath=\$ORIGIN/linux'"
# Copy the linux libraries into the project automatically.
if [ ! -d "linux" ]; then
mkdir linux
cp -r $ROOT/vendor/raylib/linux/libraylib*.so* linux
fi
;;
esac
# Build the game.
odin build src -use-separate-modules -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -show-timings -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game_tmp$DLL_EXT -debug $VET
# Need to use a temp file on Linux because it first writes an empty `game.so`, which the game will load before it is actually fully written.
mv ./build/game_tmp$DLL_EXT ./build/game$DLL_EXT
# Do not build the game.bin if it is already running.
if ! pgrep game.bin > /dev/null; then
odin build src/main_hot_reload -use-separate-modules -out:build/game.bin $VET -debug
fi

View file

@ -1,2 +0,0 @@
@echo off
odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_release.exe -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -subsystem:windows

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
odin build src/main_release -out:build/game_release.bin -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon

20
scripts/setup.bat Normal file
View file

@ -0,0 +1,20 @@
@echo off
mkdir build
FOR /F "tokens=*" %%g IN ('odin root') do (SET ODIN_ROOT=%%g)
echo %ODIN_ROOT%
@REM If it fails to find your odin root folder, copy the raylib.dll manually into the build folder, it's a runtime requirement
copy %ODIN_ROOT%\vendor\raylib\windows\raylib.dll build\raylib.dll
pushd vendors\dialog
call .\build.bat
popd
pushd vendors
curl.exe --output clay.zip --url https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip
@REM Apparently available on Win10 since build 17063 - https://superuser.com/a/1473255
tar -xf .\clay.zip
popd

13
scripts/setup.sh Normal file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env sh
set -e
pushd vendors/dialog
sh build.sh
popd
pushd vendors
wget https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip
unzip clay-odin.zip
rm -rf ./__MACOSX # Yeah...
popd

@ -1 +0,0 @@
Subproject commit 628e655661d822fecae67cf238cbfa414912d943

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1 +0,0 @@
[{"name":"Edinica","location":[95,10],"size":[58,57]},{"name":"Dvoika_0","location":[234,10],"size":[55,31]},{"name":"Dvoika_1","location":[163,10],"size":[61,33]},{"name":"Troika","location":[10,10],"size":[75,75]}]

View file

@ -1,32 +0,0 @@
package tinyfiledialogs
import "core:c"
when ODIN_OS == .Windows {
foreign import lib {"tinyfiledialogs.lib", "system:comdlg32.lib", "system:Ole32.lib"}
} else when ODIN_OS == .Linux || ODIN_OS == .Darwin {
foreign import lib "libtinyfiledialogs.a"
}
foreign lib {
@(link_name = "tinyfd_notifyPopup")
notify_popup :: proc(title, message, icon_type: cstring) -> c.int ---
@(link_name = "tinyfd_messageBox")
message_box :: proc(title, message, dialog_type, icon_type: cstring, default_button: c.int) -> c.int ---
@(link_name = "tinyfd_inputBox")
input_box :: proc(title, message, default_input: cstring) -> [^]c.char ---
@(link_name = "tinyfd_saveFileDialog")
save_file_dialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring) -> [^]c.char ---
@(link_name = "tinyfd_openFileDialog")
open_file_dialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring, allow_multi: c.int) -> [^]c.char ---
@(link_name = "tinyfd_selectFolderDialog")
select_folder_dialog :: proc(title, default_path: cstring) -> [^]c.char ---
@(link_name = "tinyfd_colorChooser")
color_chooser :: proc(title, default_hex_rgb: cstring, default_rgb, result_rgb: [3]byte) -> [^]c.char ---
}

55
src/frontend/globals.odin Normal file
View file

@ -0,0 +1,55 @@
package frontend
import generator "../generator"
import rl "vendor:raylib"
PIXEL_WINDOW_HEIGHT :: 180
ui_scaling: f32 = 2
@(rodata)
ATLAS_RENDER_SIZES := []i32{256, 512, 1024, 2048, 4096}
FileDialogType :: enum {
SourceFiles,
OutputFolder,
}
WindowInformation :: struct {
w: f32,
h: f32,
width_scaled: f32,
height_scaled: f32,
}
window_info: WindowInformation
MonitorInformation :: struct {
max_width: f32,
max_height: f32,
}
monitor_info: MonitorInformation
PackerSettings :: struct {
atlas_size_x: i32,
atlas_size_y: i32,
pixel_padding_x_int: i32,
pixel_padding_y_int: i32,
padding_enabled: bool,
output_json: bool,
output_odin: bool,
}
packer_settings: PackerSettings
// Where the output files will be written (atlas.png json output, etc)
output_folder_path: string
// If a folder was chosen as input - the path
source_files_to_pack: []string
Atlas :: struct {
render_texture_target: rl.RenderTexture2D,
checked_background: rl.RenderTexture2D,
render_has_preview: bool,
render_size: i32,
metadata: [dynamic]generator.SpriteAtlasMetadata,
}
atlas: Atlas

520
src/frontend/main.odin Normal file
View file

@ -0,0 +1,520 @@
package frontend
import "base:runtime"
import "core:c/libc"
import "core:log"
import "core:mem"
import "core:strings"
import rl "vendor:raylib"
import diag "../../vendors/dialog"
import generator "../generator"
should_pack_atlas_and_render: bool
main :: proc() {
default_allocator := context.allocator
tracking_allocator: mem.Tracking_Allocator
mem.tracking_allocator_init(&tracking_allocator, default_allocator)
context.allocator = mem.tracking_allocator(&tracking_allocator)
context.logger = log.create_console_logger()
{
init()
defer cleanup()
for !rl.WindowShouldClose() {
update()
draw()
for b in tracking_allocator.bad_free_array {
log.errorf("Bad free at: %v", b.location)
}
clear(&tracking_allocator.bad_free_array)
free_all(context.temp_allocator)
}
}
log.destroy_console_logger(context.logger)
for key, value in tracking_allocator.allocation_map {
log.errorf("%v: Leaked %v bytes\n", value.location, value.size)
}
mem.tracking_allocator_destroy(&tracking_allocator)
}
init :: proc() {
atlas.render_size = ATLAS_RENDER_SIZES[0]
rl.SetConfigFlags({.WINDOW_RESIZABLE})
rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer")
rl.SetWindowMinSize(1400, 800)
rl.SetTraceLogCallback(rl_log)
}
cleanup :: proc() {
delete(atlas.metadata)
delete(output_folder_path)
delete(source_files_to_pack)
log.info("Bye")
rl.CloseWindow()
}
update :: proc() {
// Update the width/height
win_info := &window_info
win_info.w = f32(rl.GetScreenWidth())
win_info.h = f32(rl.GetScreenHeight())
win_info.height_scaled = win_info.h / ui_scaling
win_info.width_scaled = win_info.w / ui_scaling
// Update the virtual mouse position (needed for GUI interaction to work properly for instance)
mouse_scale := 1 / ui_scaling
rl.SetMouseScale(mouse_scale, mouse_scale)
}
draw :: proc() {
rl.BeginDrawing()
defer rl.EndDrawing()
rl.ClearBackground(rl.BLACK)
draw_screen_ui()
if should_pack_atlas_and_render {
pack_atlas_and_render()
}
free_all(context.temp_allocator)
}
ui_camera :: proc() -> rl.Camera2D {
return {zoom = ui_scaling}
}
draw_screen_ui :: proc() {
rl.BeginMode2D(ui_camera())
draw_settings_and_preview()
rl.EndMode2D()
}
save_atlas_to_file :: proc() {
generator.save_output(output_folder_path, atlas.metadata, atlas.render_texture_target)
}
save_to_atlas_to_file :: proc() {
open_file_dialog(.OutputFolder)
save_atlas_to_file()
}
pack_atlas_and_render :: proc() {
rl.BeginTextureMode(atlas.render_texture_target)
defer rl.EndTextureMode()
atlas_entries: [dynamic]generator.AtlasEntry
defer {
for entry in atlas_entries {
delete(entry.cells)
delete(entry.layer_cell_count)
delete(entry.layer_names)
}
delete(atlas_entries)
}
if len(source_files_to_pack) > 0 {
generator.unmarshall_aseprite_files(source_files_to_pack, &atlas_entries)
} else {
log.error("No source folder or files set! Can't pack the void!!!")
should_pack_atlas_and_render = false
return
}
atlas_img: rl.Image = rl.GenImageColor(atlas.render_size, atlas.render_size, rl.BLANK)
padding_x := packer_settings.pixel_padding_x_int if packer_settings.padding_enabled else 0
padding_y := packer_settings.pixel_padding_y_int if packer_settings.padding_enabled else 0
delete(atlas.metadata)
atlas.metadata = generator.pack_atlas_entries(
atlas_entries[:],
&atlas_img,
padding_x,
padding_y,
)
// OpenGL's Y buffer is flipped
rl.ImageFlipVertical(&atlas_img)
// rl.UnloadTexture(atlas_render_target.texture)
log.info("Packed everything!")
atlas.render_texture_target.texture = rl.LoadTextureFromImage(atlas_img)
should_pack_atlas_and_render = false
atlas.render_has_preview = true
}
draw_settings_and_preview :: proc() {
left_half_rect := rl.Rectangle {
x = 0,
y = 0,
width = cast(f32)window_info.width_scaled / 3,
height = cast(f32)window_info.height_scaled,
}
right_half_rect := rl.Rectangle {
x = cast(f32)window_info.width_scaled / 3,
y = 0,
width = cast(f32)(window_info.width_scaled / 3) * 2,
height = cast(f32)window_info.height_scaled,
}
rl.DrawRectangleRec(left_half_rect, rl.WHITE)
rl.DrawRectangleRec(right_half_rect, rl.MAROON)
@(static) spinner_edit_mode: bool
small_offset := 10 * ui_scaling
big_offset := 30 * ui_scaling
elements_height: f32 = 0
rl.GuiPanel(left_half_rect, "Atlas Settings")
elements_height += small_offset / 2
@(static) settings_dropdown_box_active_idx: i32
elements_height += small_offset + 5 * ui_scaling
rl.GuiLabel(
{x = small_offset, y = elements_height, width = left_half_rect.width},
"Atlas Size",
)
elements_height += small_offset / 2
@(static) dropdown_resolution_edit_mode: bool
@(static) dropdown_resolution_mode: i32
dropdown_rect := rl.Rectangle {
x = small_offset,
y = elements_height,
width = left_half_rect.width - small_offset * 2,
height = small_offset,
}
// Because we want to render this ontop of everything else, we can just 'defer' it at the end of the draw function
defer {
if dropdown_resolution_edit_mode {rl.GuiLock()}
if rl.GuiDropdownBox(
dropdown_rect,
"256x;512x;1024x;2048x;4096x",
&dropdown_resolution_mode,
dropdown_resolution_edit_mode,
) {
dropdown_resolution_edit_mode = !dropdown_resolution_edit_mode
atlas.render_size =
ATLAS_RENDER_SIZES[max(i32(len(ATLAS_RENDER_SIZES) - 1), dropdown_resolution_mode)]
}
rl.GuiUnlock()
}
elements_height += small_offset * 2
// General Options
if settings_dropdown_box_active_idx == 0 {
padding_settings_y := elements_height
{
defer {
padding_settings_rect := rl.Rectangle {
x = small_offset / 2,
y = padding_settings_y,
width = left_half_rect.width - small_offset,
height = elements_height - padding_settings_y,
}
rl.GuiGroupBox(padding_settings_rect, "Padding Settings")
}
elements_height += small_offset
enable_padding_rect := rl.Rectangle {
x = small_offset,
y = elements_height,
width = small_offset,
height = small_offset,
}
rl.GuiCheckBox(
enable_padding_rect,
" Enable padding",
&packer_settings.padding_enabled,
)
elements_height += small_offset * 2
// Padding X spinner and label
{
padding_x_spinner_rect := rl.Rectangle {
x = small_offset,
y = elements_height,
width = big_offset * 2,
height = small_offset,
}
padding_x_spinner := rl.GuiSpinner(
padding_x_spinner_rect,
"",
&packer_settings.pixel_padding_x_int,
0,
10,
spinner_edit_mode,
)
if (padding_x_spinner) > 0 {
spinner_edit_mode = !spinner_edit_mode
}
rl.GuiLabel(
{
x = (small_offset * 2) + big_offset * 2,
y = elements_height,
width = big_offset,
height = small_offset,
},
"Padding X",
)
}
elements_height += small_offset * 2
// Padding Y spinner and label
{
padding_y_spinner_rect := rl.Rectangle {
x = small_offset,
y = elements_height,
width = big_offset * 2,
height = small_offset,
}
padding_y_spinner := rl.GuiSpinner(
padding_y_spinner_rect,
"",
&packer_settings.pixel_padding_y_int,
0,
10,
spinner_edit_mode,
)
if (padding_y_spinner) > 0 {
spinner_edit_mode = !spinner_edit_mode
}
rl.GuiLabel(
{
x = (small_offset * 2) + big_offset * 2,
y = elements_height,
width = big_offset,
height = small_offset,
},
"Padding Y",
)
}
elements_height += small_offset * 2
}
elements_height += small_offset
{
actions_label_y := elements_height
defer {
actions_rect := rl.Rectangle {
x = small_offset / 2,
y = actions_label_y,
width = left_half_rect.width - small_offset,
height = elements_height - actions_label_y,
}
rl.GuiGroupBox(actions_rect, "Actions")
}
elements_height += small_offset
pick_sources_rect := rl.Rectangle {
x = small_offset,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
}
if rl.GuiButton(pick_sources_rect, "Pick Source(s)") {
open_file_dialog(.SourceFiles)
}
pick_output_rect := rl.Rectangle {
x = left_half_rect.width / 2,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
}
if rl.GuiButton(pick_output_rect, "Pick Output") {
open_file_dialog(.OutputFolder)
}
elements_height += small_offset * 2
pack_atlas_rect := rl.Rectangle {
x = small_offset,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
}
if rl.GuiButton(pack_atlas_rect, "Pack Atlas") {
should_pack_atlas_and_render = true
}
clear_atlas_rect := rl.Rectangle {
x = left_half_rect.width / 2,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
}
if rl.GuiButton(clear_atlas_rect, "Clear Atlas") {
clear_atlas_data()
}
elements_height += small_offset * 2
save_rect := rl.Rectangle {
x = small_offset,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
}
if rl.GuiButton(save_rect, "Save") {
save_atlas_to_file()
}
save_to_rect := rl.Rectangle {
x = left_half_rect.width / 2,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
}
if rl.GuiButton(save_to_rect, "Save To...") {
save_to_atlas_to_file()
}
elements_height += small_offset * 2
}
}
// Packing Options
if settings_dropdown_box_active_idx == 1 {
@(static) active_tab: i32
tabs: []cstring = {"One", "Two", "Three"}
rl.GuiTabBar(
{x = small_offset, y = elements_height, width = 100, height = small_offset},
&tabs[0],
auto_cast len(tabs),
&active_tab,
)
}
// Save Options
// if settings_dropdown_box_active_idx == 2 {
// }
elements_height = 0
rl.GuiPanel(right_half_rect, "Atlas Preview")
short_edge := min(
right_half_rect.height - big_offset * 1.5,
right_half_rect.width - big_offset * 1.5,
)
preview_rect := rl.Rectangle {
x = (right_half_rect.width / 2 + right_half_rect.x) - (short_edge / 2),
y = (right_half_rect.height / 2 + right_half_rect.y) - (short_edge / 2),
width = short_edge,
height = short_edge,
}
if !atlas.render_has_preview {
rl.GuiDummyRec(preview_rect, "PREVIEW")
} else {
// rl.DrawRectangleRec(preview_rect, rl.WHITE)
bg_texture := atlas.checked_background.texture
rl.DrawTexturePro(
bg_texture,
{width = auto_cast bg_texture.width, height = auto_cast bg_texture.height},
preview_rect,
{},
0,
rl.WHITE,
)
atlas_texture := atlas.render_texture_target.texture
rl.DrawTexturePro(
atlas_texture,
{width = auto_cast atlas_texture.width, height = auto_cast -atlas_texture.height},
preview_rect,
{0, 0},
0,
rl.WHITE,
)
}
}
open_file_dialog :: proc(dialog_type: FileDialogType) {
switch dialog_type {
case .SourceFiles:
// `open_file_dialog` returns a single cstring with one or more paths divided by a separator ('|'),
// https://github.com/native-toolkit/libtinyfiledialogs/blob/master/tinyfiledialogs.c#L2706
source_files, ok := diag.open_file_dialog("Select source files", {}, 0, {}, "", 1)
if len(source_files) > 0 {
// todo(stefan): We're assuming the filepaths returned libtinydialog are valid...
source_files_to_pack = strings.split(source_files, "|")
log.info(source_files_to_pack)
} else {
log.error("No files were selected!")
}
case .OutputFolder:
file, ok := diag.select_folder_dialog("Select source folder", {})
if len(file) > 0 && ok {
output_folder_path = file
log.info(file)
} else {
log.error("Got an empty path from the file dialog!")
}
}
}
clear_atlas_data :: proc() {
delete(atlas.metadata)
atlas.render_has_preview = false
}
logger: log.Logger
rl_log_buf: []byte
rl_log :: proc "c" (logLevel: rl.TraceLogLevel, text: cstring, args: ^libc.va_list) {
context = runtime.default_context()
context.logger = logger
level: log.Level
switch logLevel {
case .TRACE, .DEBUG:
level = .Debug
case .ALL, .NONE, .INFO:
level = .Info
case .WARNING:
level = .Warning
case .ERROR:
level = .Error
case .FATAL:
level = .Fatal
}
if level < logger.lowest_level {
return
}
if rl_log_buf == nil {
rl_log_buf = make([]byte, 1024)
}
defer mem.zero_slice(rl_log_buf)
n: int
for {
va := args
n = int(libc.vsnprintf(raw_data(rl_log_buf), len(rl_log_buf), text, va))
if n < len(rl_log_buf) do break
log.infof("Resizing raylib log buffer from %m to %m", len(rl_log_buf), len(rl_log_buf) * 2)
rl_log_buf, _ = mem.resize_bytes(rl_log_buf, len(rl_log_buf) * 2)
}
formatted := string(rl_log_buf[:n])
log.log(level, formatted)
}

View file

@ -1,508 +0,0 @@
// This file is compiled as part of the `odin.dll` file. It contains the
// procs that `game.exe` will call, such as:
//
// game_init: Sets up the game state
// game_update: Run once per frame
// game_shutdown: Shuts down game and frees memory
// game_memory: Run just before a hot reload, so game.exe has a pointer to the
// game's memory.
// game_hot_reloaded: Run after a hot reload so that the `g_mem` global variable
// can be set to whatever pointer it was in the old DLL.
package game
import "core:fmt"
import "core:math"
import "core:strings"
import "utils"
import rl "vendor:raylib"
import diag "dialog"
g_mem: ^GameMemory
game_camera :: proc() -> rl.Camera2D {
w = f32(rl.GetScreenWidth())
h = f32(rl.GetScreenHeight())
return {zoom = h / PixelWindowHeight, target = {}, offset = {w / 2, h / 2}}
}
ui_camera :: proc() -> rl.Camera2D {
return {zoom = scaling}
}
update :: proc() {
// Update the width/height
win_info := &g_mem.window_info
win_info.w = f32(rl.GetScreenWidth())
win_info.h = f32(rl.GetScreenHeight())
win_info.height_scaled = win_info.h / scaling
win_info.width_scaled = win_info.w / scaling
w = win_info.w
h = win_info.h
// Update the virtual mouse position (needed for GUI interaction to work properly for instance)
rl.SetMouseScale(1 / scaling, 1 / scaling)
if g_mem.should_open_file_dialog {
open_file_dialog()
}
}
draw :: proc() {
rl.BeginDrawing()
defer rl.EndDrawing()
rl.ClearBackground(rl.BLACK)
draw_screen_ui()
if g_mem.should_render_atlas {
draw_screen_target()
}
free_all(context.temp_allocator)
}
draw_screen_ui :: proc() {
rl.BeginMode2D(ui_camera())
defer rl.EndMode2D()
draw_atlas_settings_and_preview()
}
draw_screen_target :: proc() {
atlas_render_target := &g_mem.atlas_render_texture_target
rl.BeginTextureMode(atlas_render_target^)
defer rl.EndTextureMode()
atlas_entries: [dynamic]AtlasEntry
delete(atlas_entries)
if files, ok := g_mem.source_files_to_pack.([]string); ok {
unmarshall_aseprite_files(files, &atlas_entries)
} else {
fmt.println("No source folder or files set! Can't pack the void!!!")
g_mem.should_render_atlas = false
return
}
atlas: rl.Image = rl.GenImageColor(g_mem.atlas_render_size, g_mem.atlas_render_size, rl.BLANK)
// defer rl.UnloadImage(atlas)
padding_x :=
g_mem.packer_settings.pixel_padding_x_int if g_mem.packer_settings.padding_enabled else 0
padding_y :=
g_mem.packer_settings.pixel_padding_y_int if g_mem.packer_settings.padding_enabled else 0
g_mem.atlas_metadata = pack_atlas_entries(atlas_entries[:], &atlas, padding_x, padding_y)
// OpenGL's Y buffer is flipped
rl.ImageFlipVertical(&atlas)
// rl.UnloadTexture(atlas_render_target.texture)
fmt.println("Packed everything!")
atlas_render_target.texture = rl.LoadTextureFromImage(atlas)
g_mem.should_render_atlas = false
g_mem.atlas_render_has_preview = true
}
draw_atlas_settings_and_preview :: proc() {
left_half_rect := rl.Rectangle {
x = 0,
y = 0,
width = auto_cast g_mem.window_info.width_scaled / 3,
height = auto_cast g_mem.window_info.height_scaled,
}
right_half_rect := rl.Rectangle {
x = auto_cast g_mem.window_info.width_scaled / 3,
y = 0,
width = auto_cast (g_mem.window_info.width_scaled / 3) * 2,
height = auto_cast g_mem.window_info.height_scaled,
}
rl.DrawRectangleRec(left_half_rect, rl.WHITE)
rl.DrawRectangleRec(right_half_rect, rl.MAROON)
@(static)
spinner_edit_mode: bool
small_offset := 10 * scaling
big_offset := 30 * scaling
elements_height: f32 = 0
rl.GuiPanel(left_half_rect, "Atlas Settings")
elements_height += small_offset / 2
@(static)
SettingsDropBoxEditMode: bool
@(static)
SettingsDropdownBoxActive: i32
elements_height += small_offset + 5 * scaling
rl.GuiLabel(
{x = small_offset, y = elements_height, width = left_half_rect.width},
"Atlas Size",
)
elements_height += small_offset / 2
@(static)
DropdownBox000EditMode: bool
@(static)
DropdownBox000Active: i32
dropdown_rect := rl.Rectangle {
x = small_offset,
y = elements_height,
width = left_half_rect.width - small_offset * 2,
height = small_offset,
}
// Because we want to render this ontop of everything else, we can just 'defer' it at the end of the draw function
defer {
if DropdownBox000EditMode {rl.GuiLock()}
if rl.GuiDropdownBox(
dropdown_rect,
"256x;512x;1024x;2048x;4096x",
&DropdownBox000Active,
DropdownBox000EditMode,
) {
DropdownBox000EditMode = !DropdownBox000EditMode
fmt.println(DropdownBox000Active)
g_mem.atlas_render_size = 256 * auto_cast math.pow(2, f32(DropdownBox000Active))
}
rl.GuiUnlock()
}
elements_height += small_offset * 2
// General Options
if SettingsDropdownBoxActive == 0 {
padding_settings_y := elements_height
{
defer rl.GuiGroupBox(
{
x = small_offset / 2,
y = padding_settings_y,
width = left_half_rect.width - small_offset,
height = elements_height - padding_settings_y,
},
"Padding Settings",
)
elements_height += small_offset
rl.GuiCheckBox(
{
x = small_offset,
y = elements_height,
width = small_offset,
height = small_offset,
},
" Enable padding",
&g_mem.packer_settings.padding_enabled,
)
elements_height += small_offset * 2
if (rl.GuiSpinner(
{
x = small_offset,
y = elements_height,
width = big_offset * 2,
height = small_offset,
},
"",
&g_mem.packer_settings.pixel_padding_x_int,
0,
10,
spinner_edit_mode,
)) >
0 {spinner_edit_mode = !spinner_edit_mode}
rl.GuiLabel(
{
x = (small_offset * 2) + big_offset * 2,
y = elements_height,
width = big_offset,
height = small_offset,
},
"Padding X",
)
elements_height += small_offset * 2
if (rl.GuiSpinner(
{
x = small_offset,
y = elements_height,
width = big_offset * 2,
height = small_offset,
},
"",
&g_mem.packer_settings.pixel_padding_y_int,
0,
10,
spinner_edit_mode,
)) >
0 {spinner_edit_mode = !spinner_edit_mode}
rl.GuiLabel(
{
x = (small_offset * 2) + big_offset * 2,
y = elements_height,
width = big_offset,
height = small_offset,
},
"Padding Y",
)
elements_height += small_offset * 2
}
elements_height += small_offset
// rl.GuiLine({y = elements_height, width = left_half_rect.width}, "Actions")
// elements_height += small_offset
{
actions_label_y := elements_height
defer rl.GuiGroupBox(
{
x = small_offset / 2,
y = actions_label_y,
width = left_half_rect.width - small_offset,
height = elements_height - actions_label_y,
},
"Actions",
)
elements_height += small_offset
if rl.GuiButton(
{
x = small_offset,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
},
"Pick Source(s)",
) {
g_mem.should_open_file_dialog = true
g_mem.source_location_type = .SourceFiles
}
if rl.GuiButton(
{
x = left_half_rect.width / 2,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
},
"Pick Output",
) {
g_mem.should_open_file_dialog = true
g_mem.source_location_type = .OutputFolder
}
elements_height += small_offset * 2
if rl.GuiButton(
{
x = small_offset,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
},
"Pack Atlas",
) {
g_mem.should_render_atlas = true
}
if rl.GuiButton(
{
x = left_half_rect.width / 2,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
},
"Clear Atlas",
) {
clear_atlas_data()
}
elements_height += small_offset * 2
if rl.GuiButton(
{
x = small_offset,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
},
"Save",
) {
save_output()
}
if rl.GuiButton(
{
x = left_half_rect.width / 2,
y = elements_height,
width = left_half_rect.width / 2 - small_offset,
height = small_offset,
},
"Save To...",
) {
if output_folder, ok := g_mem.output_folder_path.(string); ok {
save_metadata_simple(output_folder)
}
}
elements_height += small_offset * 2
}
}
// Packing Options
if SettingsDropdownBoxActive == 1 {
@(static)
active_tab: i32
tabs: []cstring = {"One", "Two", "Three"}
rl.GuiTabBar(
{x = small_offset, y = elements_height, width = 100, height = small_offset},
&tabs[0],
auto_cast len(tabs),
&active_tab,
)
}
// Save Options
if SettingsDropdownBoxActive == 2 {
}
elements_height = 0
rl.GuiPanel(right_half_rect, "Atlas Preview")
short_edge := min(
right_half_rect.height - big_offset * 1.5,
right_half_rect.width - big_offset * 1.5,
)
preview_rect := rl.Rectangle {
x = (right_half_rect.width / 2 + right_half_rect.x) - (short_edge / 2),
y = (right_half_rect.height / 2 + right_half_rect.y) - (short_edge / 2),
width = short_edge,
height = short_edge,
}
if !g_mem.atlas_render_has_preview {
rl.GuiDummyRec(preview_rect, "PREVIEW")
} else {
// rl.DrawRectangleRec(preview_rect, rl.WHITE)
bg_texture := g_mem.atlas_checked_background.texture
rl.DrawTexturePro(
bg_texture,
{width = auto_cast bg_texture.width, height = auto_cast bg_texture.height},
preview_rect,
{},
0,
rl.WHITE,
)
// preview_rect.x +=
// 10;preview_rect.y += 10;preview_rect.height -= 20;preview_rect.width -= 20
atlas_texture := g_mem.atlas_render_texture_target.texture
rl.DrawTexturePro(
atlas_texture,
{width = auto_cast atlas_texture.width, height = auto_cast -atlas_texture.height},
preview_rect,
{0, 0},
0,
rl.WHITE,
)
}
}
open_file_dialog :: proc() {
switch g_mem.source_location_type {
case .SourceFiles:
// `open_file_dialog` returns a single cstring with one or more paths, divided by a separator ('|'),
// https://github.com/native-toolkit/libtinyfiledialogs/blob/master/tinyfiledialogs.c#L2706
file_paths_conc := cstring(
diag.open_file_dialog(
"Select source files",
cstring(&g_mem.file_dialog_text_buffer[0]),
0,
nil,
"",
1,
),
)
if len(file_paths_conc) > 0 {
// todo(stefan): Currently we're not doing any checks if the filepaths are valid at all,
// this should be fine because it's returned by the OS' file picker but who knows...
source_files_to_pack := strings.clone_from_cstring(file_paths_conc, context.allocator)
g_mem.source_files_to_pack = strings.split(source_files_to_pack, "|")
fmt.println(g_mem.source_files_to_pack)
} else {
fmt.println("No files were selected!")
}
case .SourceFolder:
file := cstring(
diag.select_folder_dialog(
"Select source folder",
cstring(&g_mem.file_dialog_text_buffer[0]),
),
)
if len(file) > 0 {
g_mem.source_location_to_pack = strings.clone_from_cstring(file)
fmt.println(g_mem.source_location_to_pack)
} else {
fmt.println("Got an empty path from the file dialog!")
}
case .OutputFolder:
file := cstring(
diag.select_folder_dialog(
"Select source folder",
cstring(&g_mem.file_dialog_text_buffer[0]),
),
)
if len(file) > 0 {
g_mem.output_folder_path = strings.clone_from_cstring(file)
fmt.println(g_mem.output_folder_path)
} else {
fmt.println("Got an empty path from the file dialog!")
}
case .SaveFileAs:
file_path: cstring
patterns: []cstring = {"*.png"}
if default_path, ok := g_mem.output_folder_path.(string); ok {
default_path_filename := strings.concatenate(
{default_path, os_file_separator, "atlas.png"},
)
default_path_to_save: cstring = strings.clone_to_cstring(default_path_filename)
file_path = cstring(
diag.save_file_dialog(
"Save as...",
default_path_to_save,
1,
&patterns[0],
"Atlas",
),
)
} else {
file_path = cstring(diag.save_file_dialog("Save as...", "", 1, &patterns[0], "Atlas"))
}
if file_path != nil {
save_output()
}
}
g_mem.should_open_file_dialog = false
}
clear_atlas_data :: proc() {
if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok {
delete(metadata)
// g_mem.atlas_metadata = nil
}
g_mem.atlas_render_has_preview = false
}

View file

@ -1,8 +1,11 @@
package utils
package generator
import "core:fmt"
import s "core:strings"
// todo(stefan): Simplify this whole flags business,
// this can be implemented in a simpler fashion
CLIFlagType :: enum {
Unknown,
InputFiles,
@ -18,7 +21,7 @@ CLIFlagType :: enum {
CLI_FLAG_STRINGS := [CLIFlagType][]string {
.Unknown = {""},
.Help = {"h", "help"},
.InputFiles = {"f", "input-files"},
.InputFiles = {"i", "input-files"},
.InputFolder = {"d", "input-directory"},
.OutputFolder = {"o", "out"},
.EnableMetadataOutput = {"m", "export-metadata"},
@ -30,13 +33,13 @@ CLI_FLAG_STRINGS := [CLIFlagType][]string {
CLI_FLAG_DESCRIPTIONS := [CLIFlagType]string {
.Unknown = "Invalid flag",
.Help = "Prints the help message... hello!",
.InputFiles = "(real) path the source files for the packer (realpaths only), for multiple files you can provide one string of concateneted paths, separated by a ';'",
.InputFolder = "(real) path to a folder full of source files. This is an alternative to the -i[,input-files] flag",
.OutputFolder = "(real) path to the output folder for all the resulting files to be saved to.",
.InputFiles = "Full path to the source files for the packer, for multiple files you can provide one string of concateneted paths, separated by a ';'",
.InputFolder = "Full path to a folder full of source files. This is an alternative to the -i[,input-files] flag",
.OutputFolder = "Full path to the output folder for all the resulting files to be saved to.",
.EnableMetadataOutput = "Whether or not to export metadata (JSON or source files with the offsets for the packer sprites in the atlas)",
.ConfigPath = "(real) path to a config file (json) that contains string definitions for exporting custom source files. More on this in the docs.",
.MetadataJSONOutputPath = "(real) path for the resulting JSON that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag",
.SourceCodeOutputPathOutputPath = "(real) path for the resulting source code file that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag",
.ConfigPath = "Full path to a config file (json) that contains string definitions for exporting custom source files. More on this in the docs.",
.MetadataJSONOutputPath = "Full path for the resulting JSON that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag",
.SourceCodeOutputPathOutputPath = "Full path for the resulting source code file that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag",
}
CLIOutputSettings :: struct {
@ -83,12 +86,9 @@ print_help :: proc() {
for flag in CLIFlagType {
if flag == .Unknown do continue
fmt.printfln(
"Flag: -%v,%v \t -- %v",
CLI_FLAG_STRINGS[flag][0],
CLI_FLAG_STRINGS[flag][1],
CLI_FLAG_DESCRIPTIONS[flag],
)
flag_info := CLI_FLAG_STRINGS[flag]
flag_desc := CLI_FLAG_DESCRIPTIONS[flag]
fmt.printfln("Flag: -%v,%v \t -- %v", flag_info[0], flag_info[1], flag_desc)
}
}

View file

@ -1,8 +1,9 @@
package game
package generator
import ase "./aseprite"
import ase "../../vendors/odin-aseprite"
import "core:encoding/json"
import "core:fmt"
import "core:log"
import "core:os"
import fp "core:path/filepath"
import "core:slice"
@ -10,15 +11,12 @@ import "core:strings"
import rl "vendor:raylib"
import stbrp "vendor:stb/rect_pack"
import utils "./utils"
when ODIN_OS == .Windows {
os_file_separator :: "\\"
OS_FILE_SEPARATOR :: "\\"
} else {
os_file_separator :: "/"
OS_FILE_SEPARATOR :: "/"
}
CellData :: struct {
layer_index: u16,
opacity: u8,
@ -40,228 +38,6 @@ SpriteAtlasMetadata :: struct {
size: [2]i32,
}
unmarshall_aseprite_dir :: proc(
path: string,
atlas_entries: ^[dynamic]AtlasEntry,
alloc := context.allocator,
) {
if len(path) == 0 do return
if dir_fd, err := os.open(path, os.O_RDONLY); err == os.ERROR_NONE {
fis: []os.File_Info
if fis, err = os.read_dir(dir_fd, -1); err == os.ERROR_NONE {
unmarshall_aseprite_files_file_info(fis, atlas_entries, alloc)
}
} else {
fmt.println("Couldn't open folder: ", path)
}
}
unmarshall_aseprite_files_file_info :: proc(
files: []os.File_Info,
atlas_entries: ^[dynamic]AtlasEntry,
alloc := context.allocator,
) {
if len(files) == 0 do return
paths := make([]string, len(files), alloc)
defer delete(paths)
for f, fi in files {
paths[fi] = f.fullpath
}
unmarshall_aseprite_files(paths[:], atlas_entries, alloc)
}
unmarshall_aseprite_files :: proc(
file_paths: []string,
atlas_entries: ^[dynamic]AtlasEntry,
alloc := context.allocator,
) {
if len(file_paths) == 0 do return
aseprite_document: ase.Document
for file in file_paths {
extension := fp.ext(file)
if extension != ".aseprite" do continue
fmt.println("Unmarshalling file: ", file)
ase.unmarshal_from_filename(file, &aseprite_document, alloc)
atlas_entry := atlas_entry_from_compressed_cells(aseprite_document)
atlas_entry.path = file
append(atlas_entries, atlas_entry)
}
}
/*
Goes through all the chunks in an aseprite document & copies the `Com_Image_Cel` cells in a separate image
*/
atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entry: AtlasEntry) {
atlas_entry.frames = auto_cast len(document.frames)
fmt.println("N Frames: ", len(document.frames))
// NOTE(stefan): Since the expected input for the program is multiple files containing a single sprite
// it's probably a safe assumption most of the files will be a single layer with 1 or more frames
// which means we can first prod the file for information about how many frames are there and
// allocate a slice that is going to be [Frames X Layers]CellData.
// which would allow us to gain an already sorted list of sprites if we iterate all frames of a single layer
// instead of iterating all layers for each frame
// might be even quicker to first get that information an allocate at once the amount of cells we need
for frame, frameIdx in document.frames {
fmt.printfln("Frame_{0} Chunks: ", frameIdx, len(frame.chunks))
for chunk in frame.chunks {
if cel_chunk, ok := chunk.(ase.Cel_Chunk); ok {
cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel)
if !ci_ok do continue
fmt.println(cel_chunk.layer_index)
cell := CellData {
img = rl.Image {
data = rawptr(&cel_img.pixel[0]),
width = auto_cast cel_img.width,
height = auto_cast cel_img.height,
format = .UNCOMPRESSED_R8G8B8A8,
},
frame_index = auto_cast frameIdx,
opacity = cel_chunk.opacity_level,
layer_index = cel_chunk.layer_index,
}
append(&atlas_entry.cells, cell)
}
if layer_chunk, ok := chunk.(ase.Layer_Chunk); ok {
fmt.println("Layer chunk: ", layer_chunk)
append(&atlas_entry.layer_names, layer_chunk.name)
}
}
}
slice.sort_by(atlas_entry.cells[:], proc(i, j: CellData) -> bool {
return i.layer_index < j.layer_index
})
return
}
/*
Takes in a slice of entries, an output texture and offsets (offset_x/y)
*/
pack_atlas_entries :: proc(
entries: []AtlasEntry,
atlas: ^rl.Image,
offset_x: i32,
offset_y: i32,
allocator := context.allocator,
) -> [dynamic]SpriteAtlasMetadata {
assert(atlas.width != 0, "Atlas width shouldn't be 0!")
assert(atlas.height != 0, "Atlas height shouldn't be 0!")
all_cell_images := make([dynamic]rl.Image, allocator) // it's fine to store it like this, rl.Image just stores a pointer to the data
for &entry in entries {
for cell in entry.cells {
append(&all_cell_images, cell.img)
}
entry.layer_cell_count = make([dynamic]i32, len(entry.cells), allocator)
}
num_entries := len(all_cell_images)
nodes := make([]stbrp.Node, num_entries, allocator)
rects := make([]stbrp.Rect, num_entries, allocator)
EntryAndCell :: struct {
entry: ^AtlasEntry,
cell_of_entry: ^CellData,
}
rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100, allocator)
// Set the custom IDs
cellIdx: int
for &entry, entryIdx in entries {
for &cell in entry.cells {
// I can probably infer this information with just the id of the rect but I'm being lazy right now
map_insert(&rect_idx_to_entry_and_cell, cellIdx, EntryAndCell{&entry, &cell})
rects[cellIdx].id = auto_cast entryIdx
cellIdx += 1
entry.layer_cell_count[cell.layer_index] += 1
}
}
for cell_image, cell_index in all_cell_images {
entry_stb_rect := &rects[cell_index]
entry_stb_rect.w = stbrp.Coord(cell_image.width + offset_x)
entry_stb_rect.h = stbrp.Coord(cell_image.height + offset_y)
}
ctx: stbrp.Context
stbrp.init_target(&ctx, atlas.width, atlas.height, &nodes[0], i32(num_entries))
res := stbrp.pack_rects(&ctx, &rects[0], i32(num_entries))
if res == 1 {
fmt.println("Packed everything successfully!")
fmt.printfln("Rects: {0}", rects[:])
} else {
fmt.println("Failed to pack everything!")
}
for rect, rectIdx in rects {
entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx]
cell := entry_and_cell.cell_of_entry
src_rect := rl.Rectangle {
x = 0,
y = 0,
width = auto_cast cell.img.width,
height = auto_cast cell.img.height,
}
dst_rect := rl.Rectangle {
auto_cast rect.x + auto_cast offset_x,
auto_cast rect.y + auto_cast offset_y,
auto_cast cell.img.width,
auto_cast cell.img.height,
}
// note(stefan): drawing the sprite in the atlas in the packed coordinates
rl.ImageDraw(atlas, cell.img, src_rect, dst_rect, rl.WHITE)
fmt.printfln("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect)
}
metadata := make([dynamic]SpriteAtlasMetadata, allocator)
for rect, rectIdx in rects {
entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx]
entry := entry_and_cell.entry
cell := entry_and_cell.cell_of_entry
cell_name: string
if entry.layer_cell_count[cell.layer_index] > 1 {
cell_name = fmt.aprintf(
"{0}_%d",
entry.layer_names[cell.layer_index],
cell.frame_index,
allocator,
)
} else {
cell_name = entry.layer_names[cell.layer_index]
}
cell_metadata := SpriteAtlasMetadata {
name = cell_name,
location = {
auto_cast rect.x + auto_cast offset_x,
auto_cast rect.y + auto_cast offset_y,
},
size = {auto_cast cell.img.width, auto_cast cell.img.height},
}
append(&metadata, cell_metadata)
}
return metadata
}
SourceCodeGeneratorMetadata :: struct {
file_defines: struct {
top: string,
@ -291,6 +67,226 @@ SourceCodeGeneratorMetadata :: struct {
},
}
unmarshall_aseprite_dir :: proc(
path: string,
atlas_entries: ^[dynamic]AtlasEntry,
alloc := context.allocator,
) {
if len(path) == 0 do return
if dir_fd, err := os.open(path, os.O_RDONLY); err == os.ERROR_NONE {
fis: []os.File_Info
if fis, err = os.read_dir(dir_fd, -1); err == os.ERROR_NONE {
unmarshall_aseprite_files_file_info(fis, atlas_entries, alloc)
}
} else {
log.errorf("Couldn't open folder: ", path)
}
}
unmarshall_aseprite_files_file_info :: proc(
files: []os.File_Info,
atlas_entries: ^[dynamic]AtlasEntry,
alloc := context.allocator,
) {
if len(files) == 0 do return
paths := make([]string, len(files), alloc)
defer delete(paths)
for f, fi in files {
paths[fi] = f.fullpath
}
unmarshall_aseprite_files(paths[:], atlas_entries, alloc)
}
unmarshall_aseprite_files :: proc(
file_paths: []string,
atlas_entries: ^[dynamic]AtlasEntry,
alloc := context.allocator,
) {
if len(file_paths) == 0 do return
aseprite_document: ase.Document
for file in file_paths {
extension := fp.ext(file)
if extension != ".aseprite" do continue
log.info("Unmarshalling file:", file)
ase.unmarshal_from_filename(&aseprite_document, file)
atlas_entry := atlas_entry_from_compressed_cells(aseprite_document)
atlas_entry.path = file
append(atlas_entries, atlas_entry)
}
}
/*
Goes through all the chunks in an aseprite document & copies the `Com_Image_Cel` cells in a separate image
*/
atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entry: AtlasEntry) {
atlas_entry.frames = auto_cast len(document.frames)
log.infof("N Frames: ", len(document.frames))
// NOTE(stefan): Since the expected input for the program is multiple files containing a single sprite
// it's probably a safe assumption most of the files will be a single layer with 1 or more frames
// which means we can first prod the file for information about how many frames are there and
// allocate a slice that is going to be [Frames X Layers]CellData.
// which would allow us to gain an already sorted list of sprites if we iterate all frames of a single layer
// instead of iterating all layers for each frame
// might be even quicker to first get that information an allocate at once the amount of cells we need
for frame, frameIdx in document.frames {
log.infof("Frame_{0} Chunks: {1}", frameIdx, len(frame.chunks))
for chunk in frame.chunks {
if cel_chunk, ok := chunk.(ase.Cel_Chunk); ok {
cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel)
if !ci_ok do continue
log.info(cel_chunk.layer_index)
cell := CellData {
img = rl.Image {
data = rawptr(&cel_img.pixels[0]),
width = auto_cast cel_img.width,
height = auto_cast cel_img.height,
format = .UNCOMPRESSED_R8G8B8A8,
},
frame_index = auto_cast frameIdx,
opacity = cel_chunk.opacity_level,
layer_index = cel_chunk.layer_index,
}
append(&atlas_entry.cells, cell)
}
if layer_chunk, ok := chunk.(ase.Layer_Chunk); ok {
log.info("Layer chunk:", layer_chunk)
append(&atlas_entry.layer_names, layer_chunk.name)
}
}
}
slice.sort_by(atlas_entry.cells[:], proc(i, j: CellData) -> bool {
return i.layer_index < j.layer_index
})
return
}
/*
Takes in a slice of entries, an output texture and offsets (offset_x/y)
*/
pack_atlas_entries :: proc(
entries: []AtlasEntry,
atlas: ^rl.Image,
offset_x: i32,
offset_y: i32,
) -> [dynamic]SpriteAtlasMetadata {
assert(atlas.width != 0, "Atlas width shouldn't be 0!")
assert(atlas.height != 0, "Atlas height shouldn't be 0!")
all_cell_images := make([dynamic]rl.Image) // it's fine to store it like this, rl.Image just stores a pointer to the data
defer delete(all_cell_images)
for &entry in entries {
for cell in entry.cells {
append(&all_cell_images, cell.img)
}
entry.layer_cell_count = make([dynamic]i32, len(entry.cells))
}
num_entries := len(all_cell_images)
nodes := make([]stbrp.Node, num_entries)
rects := make([]stbrp.Rect, num_entries)
defer delete(nodes)
defer delete(rects)
EntryAndCell :: struct {
entry: ^AtlasEntry,
cell_of_entry: ^CellData,
}
rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100)
defer delete(rect_idx_to_entry_and_cell)
// Set the custom IDs
cellIdx: int
for &entry, entryIdx in entries {
for &cell in entry.cells {
// I can probably infer this information with just the id of the rect but I'm being lazy right now
map_insert(&rect_idx_to_entry_and_cell, cellIdx, EntryAndCell{&entry, &cell})
rects[cellIdx].id = auto_cast entryIdx
cellIdx += 1
entry.layer_cell_count[cell.layer_index] += 1
}
}
for cell_image, cell_index in all_cell_images {
entry_stb_rect := &rects[cell_index]
entry_stb_rect.w = stbrp.Coord(cell_image.width + offset_x)
entry_stb_rect.h = stbrp.Coord(cell_image.height + offset_y)
}
ctx: stbrp.Context
stbrp.init_target(&ctx, atlas.width, atlas.height, &nodes[0], i32(num_entries))
res := stbrp.pack_rects(&ctx, &rects[0], i32(num_entries))
if res == 1 {
log.info("Packed everything successfully!")
log.infof("Rects: {0}", rects[:])
} else {
log.error("Failed to pack everything!")
}
for rect, rectIdx in rects {
entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx]
cell := entry_and_cell.cell_of_entry
src_rect := rl.Rectangle {
x = 0,
y = 0,
width = f32(cell.img.width),
height = f32(cell.img.height),
}
dst_rect := rl.Rectangle {
f32(i32(rect.x) + offset_x),
f32(i32(rect.y) + offset_y),
f32(cell.img.width),
f32(cell.img.height),
}
// note(stefan): drawing the sprite in the atlas in the packed coordinates
rl.ImageDraw(atlas, cell.img, src_rect, dst_rect, rl.WHITE)
log.infof("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect)
}
metadata := make([dynamic]SpriteAtlasMetadata)
for rect, rectIdx in rects {
entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx]
entry := entry_and_cell.entry
cell := entry_and_cell.cell_of_entry
cell_name: string
if entry.layer_cell_count[cell.layer_index] > 1 {
cell_name = fmt.aprintf(
"{0}_{1}",
entry.layer_names[cell.layer_index],
cell.frame_index,
)
} else {
cell_name = entry.layer_names[cell.layer_index]
}
cell_metadata := SpriteAtlasMetadata {
name = cell_name,
location = {i32(rect.x) + offset_x, i32(rect.y) + offset_y},
size = {auto_cast cell.img.width, auto_cast cell.img.height},
}
append(&metadata, cell_metadata)
}
return metadata
}
odin_source_generator_metadata := SourceCodeGeneratorMetadata {
file_defines = {
top = "package atlas_bindings\n\n",
@ -310,7 +306,7 @@ odin_source_generator_metadata := SourceCodeGeneratorMetadata {
},
array_data = {
name = "ATLAS_SPRITES",
type = "[]AtlasRect",
type = "[AtlasSprite]AtlasRect",
begin_line = "%v := %v {{\n",
entry_line = "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n",
end_line = "}\n\n",
@ -346,9 +342,9 @@ cpp_source_generator_metadata := SourceCodeGeneratorMetadata {
}
/*
Generates a barebones file with the package name "atlas_bindings",
the file contains an array of offsets, indexed by an enum.
The enum has unique names
Generates a barebones file with the package name "atlas_bindings",
the file contains an array of offsets, indexed by an enum.
The enum has unique names
*/
generate_odin_enums_and_atlas_offsets_file_sb :: proc(
metadata: []SpriteAtlasMetadata,
@ -371,7 +367,7 @@ generate_odin_enums_and_atlas_offsets_file_sb :: proc(
// start offsets array
// todo(stefan): the name of the array can be based on the output name?
strings.write_string(&sb, "ATLAS_SPRITES := []AtlasRect {\n")
strings.write_string(&sb, "ATLAS_SPRITES := [AtlasSprite]AtlasRect {\n")
{
entry: string
for cell in metadata {
@ -389,7 +385,7 @@ generate_odin_enums_and_atlas_offsets_file_sb :: proc(
// end offsets array
strings.write_string(&sb, "}\n\n")
fmt.println("\n", strings.to_string(sb))
log.info("\n", strings.to_string(sb))
return sb
}
@ -405,26 +401,21 @@ metadata_source_code_generate :: proc(
strings.write_string(&sb, codegen.file_defines.top)
// Introduce the Rect type
// strings.write_string(&sb, "AtlasRect :: struct { x, y, w, h: i32 }\n\n")
strings.write_string(
&sb,
fmt.aprintf(codegen.custom_data_type.type_declaration, codegen.custom_data_type.name),
)
// start enum
// strings.write_string(&sb, "AtlasSprite :: enum {\n")
strings.write_string(&sb, fmt.aprintf(codegen.enum_data.begin_line, codegen.enum_data.name))
{
for cell in metadata {
// strings.write_string(&sb, fmt.aprintf("\t%s,\n", cell.name))
strings.write_string(&sb, fmt.aprintf(codegen.enum_data.entry_line, cell.name))
}
}
// end enum
// strings.write_string(&sb, "}\n\n")
strings.write_string(&sb, codegen.enum_data.end_line)
// start offsets array
// strings.write_string(&sb, "ATLAS_SPRITES := []AtlasRect {\n")
strings.write_string(
&sb,
fmt.aprintf(
@ -448,58 +439,69 @@ metadata_source_code_generate :: proc(
}
}
// end offsets array
// strings.write_string(&sb, "}\n\n")
strings.write_string(&sb, codegen.array_data.end_line)
strings.write_string(&sb, codegen.file_defines.bottom)
fmt.println("\n", strings.to_string(sb))
log.info("\n", strings.to_string(sb))
return sb
}
save_output :: proc() {
output_path, ok := g_mem.output_folder_path.(string)
if !ok || output_path == "" {
fmt.println("Output path is empty!")
save_output :: proc(
output_path: string,
metadata: [dynamic]SpriteAtlasMetadata,
render_texture_target: rl.RenderTexture2D,
) {
if len(output_path) == 0 {
log.error("Output path is empty!")
return
}
image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture)
image := rl.LoadImageFromTexture(render_texture_target.texture)
rl.ImageFlipVertical(&image)
cstring_atlas_output_path := strings.clone_to_cstring(
strings.concatenate({output_path, os_file_separator, "atlas.png"}),
strings.concatenate({output_path, OS_FILE_SEPARATOR, "atlas.png"}),
)
rl.ExportImage(image, cstring_atlas_output_path)
if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok {
fmt.println("Building metadata...")
if len(metadata) > 0 {
log.info("Building metadata...")
if json_metadata, jok := json.marshal(metadata); jok == nil {
os.write_entire_file(
strings.concatenate({output_path, os_file_separator, "metadata.json"}),
strings.concatenate(
{output_path, OS_FILE_SEPARATOR, "metadata.json"},
context.temp_allocator,
),
json_metadata,
)
delete(json_metadata)
} else {
fmt.println("Failed to marshall the atlas metadata to a json!")
log.error("Failed to marshall the atlas metadata to a json!")
}
// TODO(stefan): Think of a more generic alternative to just straight output to a odin file
// maybe supply a config.json that defines the start, end, line by line entry and enum format strings
// this way you can essentially support any language
sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:])
defer strings.builder_destroy(&sb)
odin_metadata := strings.to_string(sb)
ok := os.write_entire_file(
strings.concatenate({output_path, os_file_separator, "metadata.odin"}),
strings.concatenate(
{output_path, OS_FILE_SEPARATOR, "metadata.odin"},
context.temp_allocator,
),
transmute([]byte)odin_metadata,
)
if !ok {
fmt.println("Failed to save 'metadata.odin'")
log.error("Failed to save 'metadata.odin'")
}
} else {
fmt.println("No metadata to export!")
log.error("No metadata to export!")
}
}
@ -509,30 +511,31 @@ save_metadata_simple :: proc(
json_file_name: Maybe(string),
source_file_name: Maybe(string),
source_gen_metadata: Maybe(SourceCodeGeneratorMetadata),
atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata),
) {
json_file_base_name, json_file_name_ok := json_file_name.(string)
source_file_base_name, source_file_name_ok := source_file_name.(string)
if !json_file_name_ok && !source_file_name_ok {
fmt.println("Neither a json file name or a source code filename has been provided!")
log.error("Neither a json file name or a source code filename has been provided!")
return
}
metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata);if !ok {
fmt.println("No metadata to export!")
metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); if !ok {
log.error("No metadata to export!")
}
fmt.println("Building metadata...")
log.info("Building metadata...")
if json_file_name_ok {
if json_metadata, jok := json.marshal(metadata); jok == nil {
json_output_path := strings.concatenate(
{output_path, os_file_separator, json_file_base_name},
{output_path, OS_FILE_SEPARATOR, json_file_base_name},
)
if ok = os.write_entire_file(json_output_path, json_metadata); !ok {
fmt.println("Failed to write json to file: ", json_output_path)
log.errorf("Failed to write json to file: ", json_output_path)
}
} else {
fmt.println("Failed to marshall the atlas metadata to a json!")
log.error("Failed to marshall the atlas metadata to a json!")
}
}
@ -541,48 +544,50 @@ save_metadata_simple :: proc(
// if src_gen_metadata
if codegen, cok := source_gen_metadata.(SourceCodeGeneratorMetadata); cok {
sb := metadata_source_code_generate(metadata[:], codegen)
defer strings.builder_destroy(&sb)
source_metadata := strings.to_string(sb)
source_output_path := strings.concatenate(
{
output_path,
os_file_separator,
OS_FILE_SEPARATOR,
codegen.file_defines.file_name,
codegen.file_defines.file_extension,
},
)
ok := os.write_entire_file(source_output_path, transmute([]byte)source_metadata)
if !ok {
fmt.println("Failed to save source code to file:", source_output_path)
log.errorf("Failed to save source code to file:", source_output_path)
}
} else {
sb := metadata_source_code_generate(metadata[:], odin_source_generator_metadata)
defer strings.builder_destroy(&sb)
odin_metadata := strings.to_string(sb)
source_output_path := strings.concatenate(
{output_path, os_file_separator, "metadata.odin"},
{output_path, OS_FILE_SEPARATOR, "metadata.odin"},
)
ok := os.write_entire_file(source_output_path, transmute([]byte)odin_metadata)
if !ok {
fmt.println("Failed to save source code to file:", source_output_path)
log.errorf("Failed to save source code to file:", source_output_path)
}
}
}
}
save_metadata :: proc(
settings: utils.CLIPackerSettings,
settings: CLIPackerSettings,
atlas_entries: []AtlasEntry,
atlas_metadata: []SpriteAtlasMetadata,
) {
metadata, ok := settings.metadata.(utils.CLIMetadataSettings);if !ok do return
metadata, ok := settings.metadata.(CLIMetadataSettings); if !ok do return
if json_path, ok := metadata.json_path.(string); ok {
json_bytes, jerr := json.marshal(atlas_metadata)
if jerr == nil {
os.write_entire_file(json_path, json_bytes)
} else {
fmt.println("Failed to marshall metadata")
log.error("Failed to marshall metadata")
}
}
if source_code_path, ok := metadata.source_code_path.(string); ok {

View file

@ -1,63 +0,0 @@
package game
import rl "vendor:raylib"
PixelWindowHeight :: 180
FILE_DIALOG_SIZE :: 1000
scaling: f32 = 2
w, h: f32
WindowInformation :: struct {
w: f32,
h: f32,
width_scaled: f32,
height_scaled: f32,
}
MonitorInformation :: struct {
max_width: f32,
max_height: f32,
}
FileDialogType :: enum {
SourceFiles,
SourceFolder,
OutputFolder,
SaveFileAs,
}
PackerSettings :: struct {
atlas_size_x: i32,
atlas_size_y: i32,
pixel_padding_x_int: i32,
pixel_padding_y_int: i32,
padding_enabled: bool,
output_json: bool,
output_odin: bool,
}
GameMemory :: struct {
file_dialog_text_buffer: [FILE_DIALOG_SIZE + 1]u8,
is_packing_whole_source_folder: bool,
should_open_file_dialog: bool,
window_info: WindowInformation,
monitor_info: MonitorInformation,
// Where the output files will be written (atlas.png, json output, etc)
output_folder_path: Maybe(string),
// If files were chosen as input - their paths
source_location_to_pack: Maybe(string),
// If a folder was chosen as input - the path
source_files_to_pack: Maybe([]string),
// What type of file dialog to open
source_location_type: FileDialogType,
// Packer settings
packer_settings: PackerSettings,
atlas_render_texture_target: rl.RenderTexture2D,
atlas_checked_background: rl.RenderTexture2D,
should_render_atlas: bool,
atlas_render_has_preview: bool,
atlas_render_size: i32,
atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata),
}

View file

@ -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

View file

@ -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

View file

@ -1,88 +0,0 @@
package game
import rl "vendor:raylib"
@(export)
game_update :: proc() -> bool {
update()
draw()
return !rl.WindowShouldClose()
}
@(export)
game_init_window :: proc() {
rl.SetConfigFlags({.WINDOW_RESIZABLE})
rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer")
rl.SetWindowMinSize(1400, 800)
}
@(export)
game_init :: proc() {
g_mem = new(GameMemory)
g_mem^ = GameMemory{}
game_hot_reloaded(g_mem)
when !ODIN_DEBUG {
rl.SetExitKey(nil)
}
current_monitor := rl.GetCurrentMonitor()
g_mem.monitor_info = MonitorInformation {
max_width = auto_cast rl.GetMonitorWidth(current_monitor),
max_height = auto_cast rl.GetMonitorHeight(current_monitor),
}
g_mem.window_info = WindowInformation {
w = 1280,
h = 720,
}
g_mem.atlas_render_texture_target = rl.LoadRenderTexture(256, 256)
g_mem.atlas_render_size = 256
checkered_img := rl.GenImageChecked(256, 256, 256 / 4, 256 / 4, rl.GRAY, rl.DARKGRAY)
defer rl.UnloadImage(checkered_img)
g_mem.atlas_checked_background.texture = rl.LoadTextureFromImage(checkered_img)
rl.SetTargetFPS(rl.GetMonitorRefreshRate(current_monitor))
rl.GuiLoadStyle("./styles/style_candy.rgs")
}
@(export)
game_shutdown :: proc() {
free(g_mem)
}
@(export)
game_shutdown_window :: proc() {
rl.CloseWindow()
}
@(export)
game_memory :: proc() -> rawptr {
return g_mem
}
@(export)
game_memory_size :: proc() -> int {
return size_of(GameMemory)
}
@(export)
game_hot_reloaded :: proc(mem: rawptr) {
g_mem = (^GameMemory)(mem)
rl.GuiLoadStyle("./styles/style_candy.rgs")
}
@(export)
game_force_reload :: proc() -> bool {
return rl.IsKeyPressed(.F5)
}
@(export)
game_force_restart :: proc() -> bool {
return rl.IsKeyPressed(.F6)
}

View file

@ -1,53 +0,0 @@
// This implements simple animations using sprite sheets. The texture in the
// `Animation` struct is assumed to contain a horizontal strip of the frames
// in the animation. Call `animation_update` to update and then call
// `animation_rect` when you wish to know the source rect to use in the texture
// With the source rect you can run rl.DrawTextureRec to draw the current frame.
package utils
import "core:log"
Animation :: struct {
texture: Texture,
num_frames: int,
current_frame: int,
frame_timer: f32,
frame_length: f32,
}
animation_create :: proc(tex: Texture, num_frames: int, frame_length: f32) -> Animation {
return(
Animation {
texture = tex,
num_frames = num_frames,
frame_length = frame_length,
frame_timer = frame_length,
} \
)
}
animation_update :: proc(a: ^Animation, dt: f32) {
a.frame_timer -= dt
if a.frame_timer <= 0 {
a.frame_timer = a.frame_length + a.frame_timer
a.current_frame += 1
if a.current_frame >= a.num_frames {
a.current_frame = 0
}
}
}
animation_rect :: proc(a: Animation) -> Rect {
if a.num_frames == 0 {
log.error("Animation has zero frames")
return RectEmpty
}
w := f32(a.texture.width) / f32(a.num_frames)
h := f32(a.texture.height)
return {x = f32(a.current_frame) * w, y = 0, width = w, height = h}
}

View file

@ -1,141 +0,0 @@
// This handle-based array gives you a statically allocated array where you can
// use index based handles instead of pointers. The handles have a generation
// that makes sure you don't get bugs when slots are re-used.
// Read more about it here: https://floooh.github.io/2018/06/17/handles-vs-pointers.html */
package utils
Handle :: struct($T: typeid) {
// idx 0 means unused. Note that slot 0 is a dummy slot, it can never be used.
idx: u32,
gen: u32,
}
HandleArrayItem :: struct($T: typeid) {
item: T,
handle: Handle(T),
}
// TODO: Add a freelist that uses some kind of bit array... We should be able to
// check 64 item slots at a time that way, but without any dynamic array.
HandleArray :: struct($T: typeid, $N: int) {
items: #soa[N]HandleArrayItem(T),
num_items: u32,
}
ha_add :: proc(a: ^HandleArray($T, $N), v: T) -> (Handle(T), bool) #optional_ok {
for idx in 1 ..< a.num_items {
i := &a.items[idx]
if idx != 0 && i.handle.idx == 0 {
i.handle.idx = u32(idx)
i.item = v
return i.handle, true
}
}
// Index 0 is dummy
if a.num_items == 0 {
a.num_items += 1
}
if a.num_items == len(a.items) {
return {}, false
}
idx := a.num_items
i := &a.items[a.num_items]
a.num_items += 1
i.handle.idx = idx
i.handle.gen = 1
i.item = v
return i.handle, true
}
ha_get :: proc(a: HandleArray($T, $N), h: Handle(T)) -> (T, bool) {
if h.idx == 0 {
return {}, false
}
if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h {
return a.items[h.idx].item, true
}
return {}, false
}
ha_get_ptr :: proc(a: HandleArray($T, $N), h: Handle(T)) -> ^T {
if h.idx == 0 {
return nil
}
if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h {
return &ha.items[h.idx].item
}
return nil
}
ha_remove :: proc(a: ^HandleArray($T, $N), h: Handle(T)) {
if h.idx == 0 {
return
}
if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h {
a.items[h.idx].handle.idx = 0
a.items[h.idx].handle.gen += 1
}
}
ha_valid :: proc(a: HandleArray($T, $N), h: Handle(T)) -> bool {
if h.idx == 0 {
return false
}
return int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h
}
HandleArrayIter :: struct($T: typeid, $N: int) {
a: ^HandleArray(T, N),
index: int,
}
ha_make_iter :: proc(a: ^HandleArray($T, $N)) -> HandleArrayIter(T, N) {
return HandleArrayIter(T, N){a = a}
}
ha_iter :: proc(it: ^HandleArrayIter($T, $N)) -> (val: T, h: Handle(T), cond: bool) {
cond = it.index < int(it.a.num_items)
for ; cond; cond = it.index < int(it.a.num_items) {
if it.a.items[it.index].handle.idx == 0 {
it.index += 1
continue
}
val = it.a.items[it.index].item
h = it.a.items[it.index].handle
it.index += 1
break
}
return
}
ha_iter_ptr :: proc(it: ^HandleArrayIter($T, $N)) -> (val: ^T, h: Handle(T), cond: bool) {
cond = it.index < int(it.a.num_items)
for ; cond; cond = it.index < int(it.a.num_items) {
if it.a.items[it.index].handle.idx == 0 {
it.index += 1
continue
}
val = &it.a.items[it.index].item
h = it.a.items[it.index].handle
it.index += 1
break
}
return
}

View file

@ -1,55 +0,0 @@
// generic odin helpers
package utils
import "core:intrinsics"
import "core:reflect"
import "core:strings"
import rl "vendor:raylib"
Texture :: rl.Texture
Color :: rl.Color
Rect :: rl.Rectangle
RectEmpty :: Rect{}
increase_or_wrap_enum :: proc(e: $T) -> T {
ei := int(e) + 1
if ei >= len(T) {
ei = 0
}
return T(ei)
}
union_type :: proc(a: any) -> typeid {
return reflect.union_variant_typeid(a)
}
temp_cstring :: proc(s: string) -> cstring {
return strings.clone_to_cstring(s, context.temp_allocator)
}
// There is a remap in core:math but it doesn't clamp in the new range, which I
// always want.
remap :: proc "contextless" (
old_value, old_min, old_max, new_min, new_max: $T,
) -> (
x: T,
) where intrinsics.type_is_numeric(T),
!intrinsics.type_is_array(T) {
old_range := old_max - old_min
new_range := new_max - new_min
if old_range == 0 {
return new_range / 2
}
return clamp(((old_value - old_min) / old_range) * new_range + new_min, new_min, new_max)
}
Vec2i :: [2]int
Vec2 :: [2]f32
vec2_from_vec2i :: proc(p: Vec2i) -> Vec2 {
return {f32(p.x), f32(p.y)}
}

View file

@ -0,0 +1,296 @@
package main
import clay "../clay-odin"
import "base:runtime"
import "core:math"
import "core:strings"
import "core:unicode/utf8"
import rl "vendor:raylib"
Raylib_Font :: struct {
fontId: u16,
font: rl.Font,
}
clay_color_to_rl_color :: proc(color: clay.Color) -> rl.Color {
return {u8(color.r), u8(color.g), u8(color.b), u8(color.a)}
}
raylib_fonts := [dynamic]Raylib_Font{}
// Alias for compatibility, default to ascii support
measure_text :: measure_text_ascii
measure_text_unicode :: proc "c" (
text: clay.StringSlice,
config: ^clay.TextElementConfig,
userData: rawptr,
) -> clay.Dimensions {
// Needed for grapheme_count
context = runtime.default_context()
line_width: f32 = 0
font := raylib_fonts[config.fontId].font
text_str := string(text.chars[:text.length])
// This function seems somewhat expensive, if you notice performance issues, you could assume
// - 1 codepoint per visual character (no grapheme clusters), where you can get the length from the loop
// - 1 byte per visual character (ascii), where you can get the length with `text.length`
// see `measure_text_ascii`
grapheme_count, _, _ := utf8.grapheme_count(text_str)
for letter, byte_idx in text_str {
glyph_index := rl.GetGlyphIndex(font, letter)
glyph := font.glyphs[glyph_index]
if glyph.advanceX != 0 {
line_width += f32(glyph.advanceX)
} else {
line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX)
}
}
scaleFactor := f32(config.fontSize) / f32(font.baseSize)
// Note:
// I'd expect this to be `grapheme_count - 1`,
// but that seems to be one letterSpacing too small
// maybe that's a raylib bug, maybe that's Clay?
total_spacing := f32(grapheme_count) * f32(config.letterSpacing)
return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)}
}
measure_text_ascii :: proc "c" (
text: clay.StringSlice,
config: ^clay.TextElementConfig,
userData: rawptr,
) -> clay.Dimensions {
line_width: f32 = 0
font := raylib_fonts[config.fontId].font
text_str := string(text.chars[:text.length])
for i in 0 ..< len(text_str) {
glyph_index := text_str[i] - 32
glyph := font.glyphs[glyph_index]
if glyph.advanceX != 0 {
line_width += f32(glyph.advanceX)
} else {
line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX)
}
}
scaleFactor := f32(config.fontSize) / f32(font.baseSize)
// Note:
// I'd expect this to be `len(text_str) - 1`,
// but that seems to be one letterSpacing too small
// maybe that's a raylib bug, maybe that's Clay?
total_spacing := f32(len(text_str)) * f32(config.letterSpacing)
return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)}
}
clay_raylib_render :: proc(
render_commands: ^clay.ClayArray(clay.RenderCommand),
allocator := context.temp_allocator,
) {
for i in 0 ..< render_commands.length {
render_command := clay.RenderCommandArray_Get(render_commands, i)
bounds := render_command.boundingBox
switch render_command.commandType {
case .None: // None
case .Text:
config := render_command.renderData.text
text := string(config.stringContents.chars[:config.stringContents.length])
// Raylib uses C strings instead of Odin strings, so we need to clone
// Assume this will be freed elsewhere since we default to the temp allocator
cstr_text := strings.clone_to_cstring(text, allocator)
font := raylib_fonts[config.fontId].font
rl.DrawTextEx(
font,
cstr_text,
{bounds.x, bounds.y},
f32(config.fontSize),
f32(config.letterSpacing),
clay_color_to_rl_color(config.textColor),
)
case .Image:
config := render_command.renderData.image
tint := config.backgroundColor
if tint == 0 {
tint = {255, 255, 255, 255}
}
imageTexture := (^rl.Texture2D)(config.imageData)
rl.DrawTextureEx(
imageTexture^,
{bounds.x, bounds.y},
0,
bounds.width / f32(imageTexture.width),
clay_color_to_rl_color(tint),
)
case .ScissorStart:
rl.BeginScissorMode(
i32(math.round(bounds.x)),
i32(math.round(bounds.y)),
i32(math.round(bounds.width)),
i32(math.round(bounds.height)),
)
case .ScissorEnd:
rl.EndScissorMode()
case .Rectangle:
config := render_command.renderData.rectangle
if config.cornerRadius.topLeft > 0 {
radius: f32 = (config.cornerRadius.topLeft * 2) / min(bounds.width, bounds.height)
draw_rect_rounded(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
radius,
config.backgroundColor,
)
} else {
draw_rect(bounds.x, bounds.y, bounds.width, bounds.height, config.backgroundColor)
}
case .Border:
config := render_command.renderData.border
// Left border
if config.width.left > 0 {
draw_rect(
bounds.x,
bounds.y + config.cornerRadius.topLeft,
f32(config.width.left),
bounds.height - config.cornerRadius.topLeft - config.cornerRadius.bottomLeft,
config.color,
)
}
// Right border
if config.width.right > 0 {
draw_rect(
bounds.x + bounds.width - f32(config.width.right),
bounds.y + config.cornerRadius.topRight,
f32(config.width.right),
bounds.height - config.cornerRadius.topRight - config.cornerRadius.bottomRight,
config.color,
)
}
// Top border
if config.width.top > 0 {
draw_rect(
bounds.x + config.cornerRadius.topLeft,
bounds.y,
bounds.width - config.cornerRadius.topLeft - config.cornerRadius.topRight,
f32(config.width.top),
config.color,
)
}
// Bottom border
if config.width.bottom > 0 {
draw_rect(
bounds.x + config.cornerRadius.bottomLeft,
bounds.y + bounds.height - f32(config.width.bottom),
bounds.width -
config.cornerRadius.bottomLeft -
config.cornerRadius.bottomRight,
f32(config.width.bottom),
config.color,
)
}
// Rounded Borders
if config.cornerRadius.topLeft > 0 {
draw_arc(
bounds.x + config.cornerRadius.topLeft,
bounds.y + config.cornerRadius.topLeft,
config.cornerRadius.topLeft - f32(config.width.top),
config.cornerRadius.topLeft,
180,
270,
config.color,
)
}
if config.cornerRadius.topRight > 0 {
draw_arc(
bounds.x + bounds.width - config.cornerRadius.topRight,
bounds.y + config.cornerRadius.topRight,
config.cornerRadius.topRight - f32(config.width.top),
config.cornerRadius.topRight,
270,
360,
config.color,
)
}
if config.cornerRadius.bottomLeft > 0 {
draw_arc(
bounds.x + config.cornerRadius.bottomLeft,
bounds.y + bounds.height - config.cornerRadius.bottomLeft,
config.cornerRadius.bottomLeft - f32(config.width.top),
config.cornerRadius.bottomLeft,
90,
180,
config.color,
)
}
if config.cornerRadius.bottomRight > 0 {
draw_arc(
bounds.x + bounds.width - config.cornerRadius.bottomRight,
bounds.y + bounds.height - config.cornerRadius.bottomRight,
config.cornerRadius.bottomRight - f32(config.width.bottom),
config.cornerRadius.bottomRight,
0.1,
90,
config.color,
)
}
case clay.RenderCommandType.Custom:
// Implement custom element rendering here
}
}
}
// Helper procs, mainly for repeated conversions
@(private = "file")
draw_arc :: proc(
x, y: f32,
inner_rad, outer_rad: f32,
start_angle, end_angle: f32,
color: clay.Color,
) {
rl.DrawRing(
{math.round(x), math.round(y)},
math.round(inner_rad),
outer_rad,
start_angle,
end_angle,
10,
clay_color_to_rl_color(color),
)
}
@(private = "file")
draw_rect :: proc(x, y, w, h: f32, color: clay.Color) {
rl.DrawRectangle(
i32(math.round(x)),
i32(math.round(y)),
i32(math.round(w)),
i32(math.round(h)),
clay_color_to_rl_color(color),
)
}
@(private = "file")
draw_rect_rounded :: proc(x, y, w, h: f32, radius: f32, color: clay.Color) {
rl.DrawRectangleRounded({x, y, w, h}, radius, 8, clay_color_to_rl_color(color))
}

6
src/dialog/build.sh → vendors/dialog/build.sh vendored Executable file → Normal file
View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
set -e
gcc ./libtinyfiledialogs/tinyfiledialogs.c -c -o libtinyfiledialogs.o
ar rcs libtinyfiledialogs.a libtinyfiledialogs.o
rm libtinyfiledialogs.o

161
vendors/dialog/tinyfiledialogs.odin vendored Normal file
View file

@ -0,0 +1,161 @@
package tinyfiledialogs
import "core:c"
import "core:mem"
import "core:strings"
when ODIN_OS == .Windows {
foreign import lib {"tinyfiledialogs.lib", "system:comdlg32.lib", "system:Ole32.lib"}
} else when ODIN_OS == .Linux || ODIN_OS == .Darwin {
foreign import lib "libtinyfiledialogs.a"
}
@(default_calling_convention = "c", link_prefix = "tinyfd_")
foreign lib {
notifyPopup :: proc(title, message, icon_type: cstring) -> c.int ---
messageBox :: proc(title, message, dialog_type, icon_type: cstring, default_button: c.int) -> c.int ---
inputBox :: proc(title, message, default_input: cstring) -> cstring ---
saveFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring) -> cstring ---
openFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring, allow_multi: c.int) -> cstring ---
selectFolderDialog :: proc(title, default_path: cstring) -> cstring ---
colorChooser :: proc(title, default_hex_rgb: cstring, default_rgb, result_rgb: [3]byte) -> cstring ---
}
select_folder_dialog :: proc(
title, default_path: string,
alloc := context.allocator,
temp_alloc := context.temp_allocator,
) -> (
path: string,
success: bool,
) {
ctitle: cstring = nil
cdefault_path: cstring = nil
err: mem.Allocator_Error
if len(title) > 0 {
ctitle, err = strings.clone_to_cstring(title, temp_alloc)
if err != nil {
return {}, false
}
}
if len(default_path) > 0 {
cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc)
if err != nil {
return {}, false
}
}
res := selectFolderDialog(ctitle, cdefault_path)
path, err = strings.clone_from_cstring(res, alloc)
if err != nil {
return {}, false
}
return path, true
}
save_file_dialog :: proc(
title, default_path: string,
pattern_count: i32,
patterns: []string,
file_desc: string,
alloc := context.allocator,
temp_alloc := context.temp_allocator,
) -> (
path: string,
success: bool,
) {
ctitle: cstring = nil
cdefault_path: cstring = nil
cfile_desc: cstring = nil
cpatterns: [^]cstring = nil
err: mem.Allocator_Error
if len(title) > 0 {
ctitle, err = strings.clone_to_cstring(title, temp_alloc)
if err != nil {
return {}, false
}
}
if len(default_path) > 0 {
cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc)
if err != nil {
return {}, false
}
}
if len(cfile_desc) > 0 {
cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc)
if err != nil {
return {}, false
}
}
if pattern_count > 0 {
cpatterns = make([^]cstring, pattern_count + 1, temp_alloc)
for p, i in patterns {
cpatterns[i] = strings.clone_to_cstring(p)
}
cpatterns[pattern_count] = nil // null terminate the array
}
res := saveFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc)
path, err = strings.clone_from_cstring(res, alloc)
if err != nil {
return {}, false
}
return path, true
}
open_file_dialog :: proc(
title, default_path: string,
pattern_count: i32,
patterns: []string,
file_desc: string,
allow_multi: i32,
alloc := context.allocator,
temp_alloc := context.temp_allocator,
) -> (
path: string,
success: bool,
) {
ctitle: cstring = nil
cdefault_path: cstring = nil
cfile_desc: cstring = nil
cpatterns: [^]cstring = nil
err: mem.Allocator_Error
if len(title) > 0 {
ctitle, err = strings.clone_to_cstring(title, temp_alloc)
if err != nil {
return {}, false
}
}
if len(default_path) > 0 {
cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc)
if err != nil {
return {}, false
}
}
if len(cfile_desc) > 0 {
cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc)
if err != nil {
return {}, false
}
}
if pattern_count > 0 {
cpatterns = make([^]cstring, pattern_count + 1, temp_alloc)
for p, i in patterns {
cpatterns[i] = strings.clone_to_cstring(p)
}
cpatterns[pattern_count] = nil // null terminate the array
}
res := openFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc, allow_multi)
path, err = strings.clone_from_cstring(res, alloc)
if err != nil {
return {}, false
}
return path, true
}

1
vendors/odin-aseprite vendored Submodule

@ -0,0 +1 @@
Subproject commit 72ea2e8094a5f05074d4c4f2faafdba42e54673c