Compare commits

...

32 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
84db74586b misc: settings update 2024-08-30 12:33:40 +03:00
178b0d5525 feat: Added methods for writing out the metadata about the atlas elements
misc: Refactoring
2024-08-30 12:32:57 +03:00
82ee56ef03 cli work, partial flags implementation 2024-04-29 12:44:30 +03:00
0ef07b299c some preliminary work done on an argument parser & help menu/docs 2024-04-26 23:31:21 +03:00
44e88f7fd0 working cpp codegen 2024-04-24 23:24:59 +03:00
043892cdd1 added metagen check for first class enum arrays 2024-04-24 22:52:24 +03:00
99194e943a working highly constrained metadata(ish) source code generator 2024-04-24 22:39:10 +03:00
52364f28b9 working metdata output for json & odin in the aseprite_odin_generator tool 2024-04-24 21:08:36 +03:00
67d7b3e227 working demo of what the metdata exporter is supposed to do 2024-04-23 19:17:38 +03:00
194550335d project restructure (removed some files that are not needed for this project from the template & moved the files containing utils like features to the utils folder) 2024-04-23 12:49:44 +03:00
0faa687d05 refactor of the save method & replaced the path strings with a maybe(string) instead of a string+bool 2024-04-23 12:39:32 +03:00
d68a2f99b5 fixed typo 2024-04-23 10:29:22 +03:00
8c7a22ed8f add screenshot to repo & readme 2024-04-23 10:26:32 +03:00
16a4faaff7 update readme with youtube link to a preview of the program 2024-04-23 10:20:03 +03:00
52ac2819c7 updated readme & fixed the submodule setup for tinyfiledialogs + added a build script for windows 2024-04-23 10:01:51 +03:00
362f1a8165 escape key doesn't close the application in release mode anymore 2024-04-23 09:41:04 +03:00
1f1a0ee48d working build & fixed the double padding 2024-04-22 20:09:42 +03:00
6863124384 refactored the UI & fixed a bug that would crash everything 2024-04-22 19:00:14 +03:00
73202f5dfc Merge branch 'master' of stefanstefanov.eu:bersk/yaap 2024-04-22 16:09:24 +03:00
dd1f5f8e3b fmt change 2024-04-22 16:09:18 +03:00
3dc7d72a50 added tinyfiledialogs as a submodule & updated the build scripts for linux/mac 2024-04-22 10:07:02 +03:00
04a472874d working packing 2024-04-21 22:08:03 +03:00
973af01380 update the generator tool & added unmarshalling a whole directory 2024-04-21 19:17:27 +03:00
81d9632396 added new batch build script for the test generator program & minor cleanup 2024-04-21 14:31:12 +03:00
b839d631dd resulting atlas 2024-04-21 14:00:09 +03:00
7510894c78 working packing generator example 2024-04-21 13:59:27 +03:00
51 changed files with 1893 additions and 1548 deletions

24
.gitignore vendored
View file

@ -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
build_generator/
ols.json
libtinyfiledialogs.a
libtinyfiledialogs.obj
tinyfiledialogs.lib
tinyfiledialogs.obj
.idea/

6
.gitmodules vendored
View file

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

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}/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,7 +0,0 @@
{
"workbench.colorCustomizations": {
"activityBar.background": "#322C2D",
"titleBar.activeBackground": "#463E3F",
"titleBar.activeForeground": "#FAFAFA"
}
}

80
.vscode/tasks.json vendored
View file

@ -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
},
}
]
}

View file

@ -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).
<img src="https://raw.githubusercontent.com/bersK/yaap/master/repo_assets/image.png" />
## 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

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

BIN
examples/sample.aseprite Normal file

Binary file not shown.

6
odinfmt.json Normal file
View file

@ -0,0 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json",
"character_width": 100,
"sort_imports": true,
"tabs": true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

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=false -out:build/game_debug.exe -no-bounds-check -subsystem:windows -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,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 . -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

View file

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

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

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

@ -1 +0,0 @@
Subproject commit f21bed838a6d1e6bc1178ea0876596eb14190192

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 B

View file

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

View file

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

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,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
}
}

117
src/generator/cli.odin Normal file
View file

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

View file

@ -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 <iostream>\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)
}
}

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

View file

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

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,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)}
}

View file

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

View file

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

View file

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

View file

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

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))
}

5
vendors/dialog/build.bat vendored Normal file
View file

@ -0,0 +1,5 @@
@echo off
cl /c libtinyfiledialogs\tinyfiledialogs.c
lib tinyfiledialogs.obj /out:tinyfiledialogs.lib

7
vendors/dialog/build.sh vendored Normal file
View file

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

1
vendors/dialog/libtinyfiledialogs vendored Submodule

@ -0,0 +1 @@
Subproject commit cc6b593c029110af8045826ce691f540c85e850c

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