From 7510894c780d53193f762b4f0190d42eb8760ad5 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Sun, 21 Apr 2024 13:59:27 +0300 Subject: [PATCH 01/31] working packing generator example --- .vscode/tasks.json | 8 +- .../aseprite_odin_generator.odin | 44 ++++++ src/aseprite_odin_generator/big.aseprite | Bin 683 -> 1115 bytes src/aseprite_odin_generator/generator.odin | 82 ---------- src/generator.odin | 147 ++++++++++++++++++ 5 files changed, 198 insertions(+), 83 deletions(-) create mode 100644 src/aseprite_odin_generator/aseprite_odin_generator.odin delete mode 100644 src/aseprite_odin_generator/generator.odin create mode 100644 src/generator.odin diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b1eca51..edc8dcf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -59,7 +59,13 @@ { "label": "Build&Run Tile Generator Test", "type": "shell", - "command": "odin run src/aseprite_odin_generator -out:build/aseprite_odin_generator.exe", + "command": "odin run", + "args": [ + "src/aseprite_odin_generator", + "-define:RAYLIB_SHARED=true", + "-out:build_generator/aseprite_odin_generator.exe", + "-debug" + ], "options": { "cwd": "${workspaceFolder}" }, diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin new file mode 100644 index 0000000..b0012e9 --- /dev/null +++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin @@ -0,0 +1,44 @@ +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" + +import gen ".." + +ATLAS_SIZE :: 512 +IMPORT_PATH :: "./src/aseprite_odin_generator/big.aseprite" +EXPORT_PATH :: "./src/aseprite_odin_generator/atlas.png" + +main :: proc() { + fmt.println("Hello!") + ase_file, ase_ok := os.read_entire_file(IMPORT_PATH) + 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) + + atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK) + + atlas_entry := gen.atlas_entry_from_compressed_cells(doc) + // Packs the cells & blits them to the atlas + gen.pack_atlas_entries({atlas_entry}, &atlas) + + rl.ExportImage(atlas, EXPORT_PATH) +} diff --git a/src/aseprite_odin_generator/big.aseprite b/src/aseprite_odin_generator/big.aseprite index bfc55aa64ad69013a578098a6aac7774308fdcf1..e656ff87413c8ec49fd351b5c8b70bb0f2f782f5 100644 GIT binary patch delta 693 zcmZ3@dYgkOnq?wW9q(;s28LfBIT%tH7#KJ<_9`+?E?|_aH@*SnvMMkza5E?{{AU7k zz`%;ZlA&VG+e_YD4F&=%7iBK~j(3~cs_;lX>xxgfc#5#{O!aA(${zCMi z+&y*c>W}B_so%Dp!%uqt{P(I&pU<7SImc{Uy?%7rE1-d8S*gqYSdv`|gnA~UZeZ{Cv#tg9JKwK=zd0vc?9%>EZt2%J&uqVDF%jqyRs}`|QHF9P zkC`*bgFJTAkn4bgfJ#Vn0 zJ$rLh&Ao}WDYKP+Cw=b8TD(i-s#~e%qTZ@_wOQ{v%pTeGoLoG^X3@-!_n{{JU77#q zoELieHZD|V_s{?H62HkmKU+2HG1Ga5&~jBqkc*fY3K+s+E~*DQ(i;eYPP*fm*X#gv z(*6HJ`~3N)tX;fDDbit;702uk`;N?dd3;0Ck+)?&^X0!E|6F=ut^eg|rzhPHZoQt$F`#owV9Bzh->>`0CNsqpQ2aR(G)2avi_h^*`nQR@YoB7E7@;t6lT!&Bn-k=PU1|-7Nk*=XGV(gwz5v3nNCawZmfn?&>H%D@9!y>)))=W#Dx;6FC>*Yrty-HX8XY_sQn{V^p z+uxEkJ$>`P=Z{xk=N)_>d;RNO?Kjir-Lw{wUc1%wc*?rX)o-rG&8z;(TCq5v7XX$s BNSXiu delta 217 zcmcc3v6_`>HPb|ApR%(W{~Ce-|WdN_QqWQEqs5uv{H%ll(dP4ngI(* (atlas_entry: AtlasEntry) { + for frame in document.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( + &atlas_entry.cells, + rl.Image { + data = rawptr(&cel_img.pixel[0]), + width = auto_cast cel_img.width, + height = auto_cast cel_img.height, + format = .UNCOMPRESSED_R8G8B8A8, + }, + ) + } + } + return +} + +/* + Takes in a slice of entries, an output texture, the width & height +*/ +pack_atlas_entries :: proc( + entries: []AtlasEntry, + atlas: ^rl.Image, + offset_x: int = 0, + offset_y: int = 0, +) { + all_entries: [dynamic]rl.Image // it's fine to store it like this, rl.Image just stores a pointer to the data + // todo: set up the stb_rect_pack rectangles + { + for entry in entries { + append(&all_entries, ..entry.cells[:]) + } + } + + num_entries := len(all_entries) + nodes := make([]stbrp.Node, num_entries) + rects := make([]stbrp.Rect, num_entries) + + EntryAndCell :: struct { + entry: ^AtlasEntry, + cell_of_entry: ^rl.Image, + } + rect_idx_to_entry_and_cell: map[int]EntryAndCell + + // 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 + } + } + + for entry, entryIdx in all_entries { + entry_stb_rect := &rects[entryIdx] + entry_stb_rect.w = auto_cast entry.width + entry_stb_rect.h = auto_cast entry.height + } + + ctx: stbrp.Context + stbrp.init_target(&ctx, atlas.width, atlas.height, &nodes[0], auto_cast num_entries) + res := stbrp.pack_rects(&ctx, &rects[0], auto_cast num_entries) + if bool(res) { + 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 + // We're grabbing the whole cell (the image itself) + src_rect := rl.Rectangle { + x = 0, + y = 0, + width = auto_cast cell.width, + height = auto_cast cell.height, + } + // Placing it in the atlas in the calculated offsets (in the packing step) + dst_rect := rl.Rectangle { + auto_cast rect.x, + auto_cast rect.y, + auto_cast cell.width, + auto_cast cell.height, + } + + rl.ImageDraw(atlas, cell^, src_rect, dst_rect, rl.WHITE) + } +} From b839d631ddaf12c88822ed12a9803103e789417d Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Sun, 21 Apr 2024 14:00:09 +0300 Subject: [PATCH 02/31] resulting atlas --- src/aseprite_odin_generator/atlas.png | Bin 10342 -> 10897 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/aseprite_odin_generator/atlas.png b/src/aseprite_odin_generator/atlas.png index 02082b443dfd9eb206f7375267ffaa77d2d3983a..36bbf354ebf3a6e00211ffc6ee1a28c9efde7c5c 100644 GIT binary patch literal 10897 zcmeI2|5Fop8pl7o#JH6NLXjFki~+|3YFru8PdRj!UhOWXLh(pdx)IlN<)QI2*Lke(4NVg;eWB)7qQ6-ZkKEiCUlG*BExoQO)_T3&2>2`}VT8yE^R8W>S1vNmd|bhCD} zX@oi!IR~}eBCznwBk|iuN!eBIhN-(KOcbWeB04_3wOLv2a(q{QXb^$hZPpXgx2Te_OGc>Ks2XH`k3XVa;j zxr=2tcJRkZGBG!*%%oBCNEuz_*KRZboM+gUKptD|vgk9?XW-^W<&;>@1v4Z|pfYyK&0*r{?ElxNw|| z&8Gp~=w_F_>XpxxrCvlv3)`&`Xu_%A|1_RrWz`w}Aotx~k$7+g(rs{5OpdtipSszO zCEl$Jy@LQK$_1op_EG;f)Z ztbAw+n>3X;ypCDkWkzO4r%NSqN#X`2S4Vuo5l$P5?=R>peej3r@r1JSg1|-eJ0pKSDOumB zS+iNA-g29&lXwf)6ot|WpnU$ThIRka*v=4zUB0sQhUtoL12E>w8b^!^a~yhxY9-9q zo;I1MZv0$K@VHM?U>KeGh;!`XPpp9OIwoJlGmK#j`M?=IX4MSra$eZ^(IvLS7v@x} zv&rr>pr~*qy{T_`@t4@-!$ccF3lFpXySz@!m*UUI0=e~fNk^k*PZV)_R_zf3nz5*8 z?QnGc6J=$iH^WRl(-3_+_pd{e`<1S?6Hh9xtC9NDUMiO;Z1LrSVTh`gsD05P{YZLS zML0LhQhoDBVS;Z&;;k!y{jgK@bN}VDx?=Trzfehmmco0bxSdndcu}(aj?Zv!qOKvXt)Z0=l{ zc}<{hQ>zB<3*0<$Lw9I6c&<7VdHij1(uUyX`+-JR$hDWGxcEV&coQ%#ob;#Z>2o9m zbc+<2`MoJ`w8RvR&i#JoD%U{ID?O#+<-Y|KG>F7;h$XwwqG+9Uva9Cm#ntSJYu%R1 zvoBFMV*E4}$4EU%EqJJ}kH|VXZ;3U@$ku$q_WOFppus%OlF5?#US;KJzgDgw^W%B^ zJFA-Lh*yH?R;y?1S()^`_x)LWo)w;M?BuFFPM8-?YY_cDhWeE-4+M%}*nr_xtGChz zc2C=Z^yp|%I;gY8=Vk`7Z2k*RF$bxxdl0Nle1J>pdaH4a#iL&c4LUCeGp^pbpPDDE zWS0do;@NDl_nfe-wPL9M%f6UI+-;I+OeK#3Th>IQi#LbN%@d*r-%&1dvGKl~xV;zb z1ME#C{4&3Z`c@P;1@YBm zCBrbU4P}b=p9t9~j5tCl?>dyzJG;}0Xy>&}B)_jd)w1%2{NvcaVusM+j_!Dp2zpd{& R%Kg<8f;eo(GQ;h0nF))Va{_{R(t03+Il<&=f7^i!jvffXa5#gxA+h8~ zy@@*91{sj;AYaS0m~m5R1VW*q~-kMHNf{HmEn`F!b>K>`!}&Y80^MS&+!_ zW#y@V^1KfaQVYeI7I!~(3)a1<*bwtm{BE7itZ8nSFF`9Xo`nJux?9{QYeyGEeS_U!qJj&v?LrY2}eu9p;8ha cwr63OzgS(~VBO7$!1aR+p00i_>zopr0H?L}mjD0& From 81d9632396fc3d5f84159a61e8840db98e03164b Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Sun, 21 Apr 2024 14:31:12 +0300 Subject: [PATCH 03/31] added new batch build script for the test generator program & minor cleanup --- .gitignore | 1 + .vscode/tasks.json | 12 ++++-------- scripts/build_debug.bat | 2 +- scripts/build_generator_debug.bat | 2 ++ src/generator.odin | 3 +-- 5 files changed, 9 insertions(+), 11 deletions(-) create mode 100644 scripts/build_generator_debug.bat diff --git a/.gitignore b/.gitignore index 362a987..b467572 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ log.txt linux build/ +build_generator/ ols.json \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index edc8dcf..f7cc3f0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -57,15 +57,11 @@ }, }, { - "label": "Build&Run Tile Generator Test", + "label": "Build&Run Atlas Generator Test", "type": "shell", - "command": "odin run", - "args": [ - "src/aseprite_odin_generator", - "-define:RAYLIB_SHARED=true", - "-out:build_generator/aseprite_odin_generator.exe", - "-debug" - ], + "windows": { + "command": "${workspaceFolder}/scripts/build_generator_debug.bat && build_generator\\aseprite_odin_generator.exe", + }, "options": { "cwd": "${workspaceFolder}" }, diff --git a/scripts/build_debug.bat b/scripts/build_debug.bat index ecaabef..2340f93 100644 --- a/scripts/build_debug.bat +++ b/scripts/build_debug.bat @@ -1,2 +1,2 @@ @echo off -odin build src/main_release -define:RAYLIB_SHARED=false -out:build/game_debug.exe -no-bounds-check -subsystem:windows -debug +odin build src/aseprite_odin_generator -define:RAYLIB_SHARED=true -out:build_generator/aseprite_odin_generator.exe -debug diff --git a/scripts/build_generator_debug.bat b/scripts/build_generator_debug.bat new file mode 100644 index 0000000..ecaabef --- /dev/null +++ b/scripts/build_generator_debug.bat @@ -0,0 +1,2 @@ +@echo off +odin build src/main_release -define:RAYLIB_SHARED=false -out:build/game_debug.exe -no-bounds-check -subsystem:windows -debug diff --git a/src/generator.odin b/src/generator.odin index 3886bc3..f75e568 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -80,7 +80,6 @@ pack_atlas_entries :: proc( offset_y: int = 0, ) { all_entries: [dynamic]rl.Image // it's fine to store it like this, rl.Image just stores a pointer to the data - // todo: set up the stb_rect_pack rectangles { for entry in entries { append(&all_entries, ..entry.cells[:]) @@ -117,7 +116,7 @@ pack_atlas_entries :: proc( ctx: stbrp.Context stbrp.init_target(&ctx, atlas.width, atlas.height, &nodes[0], auto_cast num_entries) res := stbrp.pack_rects(&ctx, &rects[0], auto_cast num_entries) - if bool(res) { + if res == 1 { fmt.println("Packed everything successfully!") fmt.printfln("Rects: {0}", rects[:]) } else { From 973af0138095c4a6cbc2b25dd501f7446d7af9c9 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Sun, 21 Apr 2024 19:17:27 +0300 Subject: [PATCH 04/31] update the generator tool & added unmarshalling a whole directory --- .vscode/settings.json | 6 +-- .vscode/tasks.json | 14 ++++++ scripts/build_generator_debug.bat | 2 +- .../aseprite_odin_generator.odin | 19 +++---- src/aseprite_odin_generator/atlas.png | Bin 10897 -> 10891 bytes src/aseprite_odin_generator/big.png | Bin 515 -> 0 bytes src/generator.odin | 47 ++++++++++-------- 7 files changed, 49 insertions(+), 39 deletions(-) delete mode 100644 src/aseprite_odin_generator/big.png diff --git a/.vscode/settings.json b/.vscode/settings.json index 9e476f8..e820f0f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "workbench.colorCustomizations": { - "activityBar.background": "#322C2D", - "titleBar.activeBackground": "#463E3F", - "titleBar.activeForeground": "#FAFAFA" + "activityBar.background": "#322C2D", + "titleBar.activeBackground": "#463E3F", + "titleBar.activeForeground": "#FAFAFA" } } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f7cc3f0..9ad0a34 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -31,6 +31,20 @@ }, "group": "build" }, + { + "label": "Clean build folder(s)", + "type": "shell", + "windows": { + "command": "cd ${workspaceFolder}\\build && rm game*; cd ${workspaceFolder} && rm aseprite_odin_generator*", + }, + // "linux": { + // "command": "${workspaceFolder}/scripts/build_release.sh", + // }, + // "osx": { + // "command": "${workspaceFolder}/scripts/build_release.sh", + // }, + "group": "build" + }, { "label": "Build Hot Reload", "type": "shell", diff --git a/scripts/build_generator_debug.bat b/scripts/build_generator_debug.bat index ecaabef..2340f93 100644 --- a/scripts/build_generator_debug.bat +++ b/scripts/build_generator_debug.bat @@ -1,2 +1,2 @@ @echo off -odin build src/main_release -define:RAYLIB_SHARED=false -out:build/game_debug.exe -no-bounds-check -subsystem:windows -debug +odin build src/aseprite_odin_generator -define:RAYLIB_SHARED=true -out:build_generator/aseprite_odin_generator.exe -debug diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin index b0012e9..d897541 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin @@ -23,22 +23,15 @@ main :: proc() { 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) + cwd := os.get_current_directory() + target_dir := strings.concatenate({cwd, "\\src\\aseprite_odin_generator\\"}) atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK) + atlas_entries: [dynamic]gen.AtlasEntry + gen.unmarshall_aseprite_dir(target_dir, &atlas_entries) - atlas_entry := gen.atlas_entry_from_compressed_cells(doc) - // Packs the cells & blits them to the atlas - gen.pack_atlas_entries({atlas_entry}, &atlas) + + gen.pack_atlas_entries(atlas_entries[:], &atlas, 10, 10) rl.ExportImage(atlas, EXPORT_PATH) } diff --git a/src/aseprite_odin_generator/atlas.png b/src/aseprite_odin_generator/atlas.png index 36bbf354ebf3a6e00211ffc6ee1a28c9efde7c5c..afb2d717bfef7045898ca774d537aaa5911f7546 100644 GIT binary patch literal 10891 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 R%Kg<8GCg-HgEmVfX (atlas_entr } /* - Takes in a slice of entries, an output texture, the width & height + 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: int = 0, - offset_y: int = 0, -) { +pack_atlas_entries :: proc(entries: []AtlasEntry, atlas: ^rl.Image, offset_x: i32, offset_y: i32) { all_entries: [dynamic]rl.Image // it's fine to store it like this, rl.Image just stores a pointer to the data { for entry in entries { @@ -100,7 +102,7 @@ pack_atlas_entries :: proc( 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 + // 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 @@ -109,13 +111,13 @@ pack_atlas_entries :: proc( for entry, entryIdx in all_entries { entry_stb_rect := &rects[entryIdx] - entry_stb_rect.w = auto_cast entry.width - entry_stb_rect.h = auto_cast entry.height + entry_stb_rect.w = stbrp.Coord(entry.width + offset_x * 2) + entry_stb_rect.h = stbrp.Coord(entry.height + offset_y * 2) } ctx: stbrp.Context - stbrp.init_target(&ctx, atlas.width, atlas.height, &nodes[0], auto_cast num_entries) - res := stbrp.pack_rects(&ctx, &rects[0], auto_cast num_entries) + 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[:]) @@ -135,11 +137,12 @@ pack_atlas_entries :: proc( } // Placing it in the atlas in the calculated offsets (in the packing step) dst_rect := rl.Rectangle { - auto_cast rect.x, - auto_cast rect.y, + auto_cast rect.x + auto_cast offset_x, + auto_cast rect.y + auto_cast offset_y, auto_cast cell.width, auto_cast cell.height, } + fmt.printfln("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect) rl.ImageDraw(atlas, cell^, src_rect, dst_rect, rl.WHITE) } From 04a472874dfd4fa817af47a60fae482faa1c1e9c Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Sun, 21 Apr 2024 22:08:03 +0300 Subject: [PATCH 05/31] working packing --- .vscode/tasks.json | 4 +-- src/aseprite | 2 +- src/game.odin | 79 +++++++++++++++++++++++++++++++++++++++------- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9ad0a34..a13d0eb 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -67,7 +67,7 @@ }, "group": { "kind": "build", - "isDefault": false + "isDefault": true }, }, { @@ -89,7 +89,7 @@ }, "group": { "kind": "build", - "isDefault": true + "isDefault": false }, } ] diff --git a/src/aseprite b/src/aseprite index f21bed8..628e655 160000 --- a/src/aseprite +++ b/src/aseprite @@ -1 +1 @@ -Subproject commit f21bed838a6d1e6bc1178ea0876596eb14190192 +Subproject commit 628e655661d822fecae67cf238cbfa414912d943 diff --git a/src/game.odin b/src/game.odin index f2f4057..f660452 100644 --- a/src/game.odin +++ b/src/game.odin @@ -11,6 +11,7 @@ package game +import "core:math" import "core:fmt" import "core:strings" import rl "vendor:raylib" @@ -53,10 +54,10 @@ FileDialogType :: enum { } PackerSettings :: struct { + atlas_size_x: i32, + atlas_size_y: i32, 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, @@ -87,6 +88,8 @@ GameMemory :: struct { packer_settings: PackerSettings, atlas_render_texture_target: rl.RenderTexture2D, atlas_render: bool, + atlas_render_has_preview: bool, + atlas_render_size: i32, } g_mem: ^GameMemory @@ -134,6 +137,8 @@ draw :: proc() { if g_mem.atlas_render { draw_screen_target() } + + free_all(context.temp_allocator) } update_screen :: proc() { @@ -180,10 +185,31 @@ 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) + atlas_entries: [dynamic]AtlasEntry + if g_mem.input_path_set { + unmarshall_aseprite_dir(g_mem.output_folder_path, &atlas_entries) + } else if g_mem.input_files_set { + unmarshall_aseprite_files(g_mem.source_files_to_pack, &atlas_entries) + } else { + fmt.println("No source folder or files set! Can't pack the void!!!") + } + atlas: rl.Image = rl.GenImageColor(g_mem.atlas_render_size, g_mem.atlas_render_size, rl.BLANK) + pack_atlas_entries( + atlas_entries[:], + &atlas, + g_mem.packer_settings.pixel_padding_x_int, + g_mem.packer_settings.pixel_padding_y_int, + ) + delete(atlas_entries) + rl.ImageFlipVertical(&atlas) + rl.UnloadTexture(g_mem.atlas_render_texture_target.texture) + + g_mem.atlas_render_texture_target.texture = rl.LoadTextureFromImage(atlas) + + rl.UnloadImage(atlas) g_mem.atlas_render = false + g_mem.atlas_render_has_preview = true } draw_atlas_settings_and_preview :: proc() { @@ -202,6 +228,9 @@ draw_atlas_settings_and_preview :: proc() { 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 @@ -212,11 +241,35 @@ draw_atlas_settings_and_preview :: proc() { 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, - ) + @(static) + DropdownBox000EditMode: bool + @(static) + DropdownBox000Active: i32 + + + dropdown_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = big_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 rl.GuiLine({y = elements_height, width = left_half_rect.width}, "Padding Settings") @@ -229,8 +282,6 @@ draw_atlas_settings_and_preview :: proc() { ) elements_height += small_offset * 2 - @(static) - spinner_edit_mode: bool if (rl.GuiSpinner( { x = small_offset, @@ -333,7 +384,11 @@ draw_atlas_settings_and_preview :: proc() { width = short_edge, height = short_edge, } - rl.GuiDummyRec(preview_rect, "PREVIEW") + if !g_mem.atlas_render_has_preview { + rl.GuiDummyRec(preview_rect, "PREVIEW") + } else { + rl.DrawRectangleRec(preview_rect, rl.WHITE) + } 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( From 3dc7d72a505d32de1fb2d6a65e0461b8f0b55325 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Mon, 22 Apr 2024 10:07:02 +0300 Subject: [PATCH 06/31] added tinyfiledialogs as a submodule & updated the build scripts for linux/mac --- .gitignore | 3 ++- .gitmodules | 3 +++ scripts/build_debug.sh | 0 scripts/build_hot_reload.sh | 4 ++-- src/dialog/build.sh | 7 +++++++ src/dialog/tinyfiledialog.odin | 6 ++---- 6 files changed, 16 insertions(+), 7 deletions(-) mode change 100644 => 100755 scripts/build_debug.sh mode change 100644 => 100755 scripts/build_hot_reload.sh create mode 100755 src/dialog/build.sh diff --git a/.gitignore b/.gitignore index b467572..b1203b3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ linux build/ build_generator/ -ols.json \ No newline at end of file +ols.json +libtinyfiledialogs.a \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 6282b93..e4908a1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "src/aseprite"] path = src/aseprite url = https://github.com/bersK/odin-aseprite.git +[submodule "src/dialog/libtinyfiledialogs"] + path = src/dialog/libtinyfiledialogs + url = https://github.com/native-toolkit/libtinyfiledialogs.git diff --git a/scripts/build_debug.sh b/scripts/build_debug.sh old mode 100644 new mode 100755 diff --git a/scripts/build_hot_reload.sh b/scripts/build_hot_reload.sh old mode 100644 new mode 100755 index 99bbd1d..996a14d --- a/scripts/build_hot_reload.sh +++ b/scripts/build_hot_reload.sh @@ -36,10 +36,10 @@ case $(uname) in 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 +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 game_tmp$DLL_EXT game$DLL_EXT +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 diff --git a/src/dialog/build.sh b/src/dialog/build.sh new file mode 100755 index 0000000..2eba996 --- /dev/null +++ b/src/dialog/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +gcc ./libtinyfiledialogs/tinyfiledialogs.c -c -o libtinyfiledialogs.o + +ar rcs libtinyfiledialogs.a libtinyfiledialogs.o + +rm libtinyfiledialogs.o \ No newline at end of file diff --git a/src/dialog/tinyfiledialog.odin b/src/dialog/tinyfiledialog.odin index ae6dd35..1c82058 100644 --- a/src/dialog/tinyfiledialog.odin +++ b/src/dialog/tinyfiledialog.odin @@ -4,10 +4,8 @@ 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"} +} else when ODIN_OS == .Linux || ODIN_OS == .Darwin { + foreign import lib "libtinyfiledialogs.a" } foreign lib { From dd1f5f8e3bfe7886abb4dfbd20f38aec670281c1 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Mon, 22 Apr 2024 16:09:18 +0300 Subject: [PATCH 07/31] fmt change --- src/game.odin | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/game.odin b/src/game.odin index f660452..c4c04c5 100644 --- a/src/game.odin +++ b/src/game.odin @@ -11,8 +11,8 @@ package game -import "core:math" import "core:fmt" +import "core:math" import "core:strings" import rl "vendor:raylib" @@ -89,7 +89,7 @@ GameMemory :: struct { atlas_render_texture_target: rl.RenderTexture2D, atlas_render: bool, atlas_render_has_preview: bool, - atlas_render_size: i32, + atlas_render_size: i32, } g_mem: ^GameMemory @@ -138,7 +138,7 @@ draw :: proc() { draw_screen_target() } - free_all(context.temp_allocator) + free_all(context.temp_allocator) } update_screen :: proc() { @@ -202,11 +202,11 @@ draw_screen_target :: proc() { ) delete(atlas_entries) rl.ImageFlipVertical(&atlas) - rl.UnloadTexture(g_mem.atlas_render_texture_target.texture) + rl.UnloadTexture(g_mem.atlas_render_texture_target.texture) g_mem.atlas_render_texture_target.texture = rl.LoadTextureFromImage(atlas) - rl.UnloadImage(atlas) + rl.UnloadImage(atlas) g_mem.atlas_render = false g_mem.atlas_render_has_preview = true @@ -254,7 +254,7 @@ draw_atlas_settings_and_preview :: proc() { 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 + // 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()} @@ -265,8 +265,8 @@ draw_atlas_settings_and_preview :: proc() { DropdownBox000EditMode, ) { DropdownBox000EditMode = !DropdownBox000EditMode - fmt.println(DropdownBox000Active) - g_mem.atlas_render_size = 256 * auto_cast math.pow(2, f32(DropdownBox000Active)) + fmt.println(DropdownBox000Active) + g_mem.atlas_render_size = 256 * auto_cast math.pow(2, f32(DropdownBox000Active)) } rl.GuiUnlock() } From 686312438436ba5648de0c76a96c4e990a231e5e Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Mon, 22 Apr 2024 19:00:14 +0300 Subject: [PATCH 08/31] refactored the UI & fixed a bug that would crash everything --- .vscode/tasks.json | 5 +- scripts/build_debug.bat | 2 +- src/game.odin | 549 ++++++++++++++++++---------------------- src/generator.odin | 3 + src/globals.odin | 66 +++++ src/symbol_exports.odin | 7 +- 6 files changed, 322 insertions(+), 310 deletions(-) create mode 100644 src/globals.odin diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a13d0eb..66b9279 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -15,7 +15,10 @@ "osx": { "command": "${workspaceFolder}/scripts/build_debug.sh", }, - "group": "build" + "group": { + "kind": "build", + "isDefault": true + }, }, { "label": "Build Release", diff --git a/scripts/build_debug.bat b/scripts/build_debug.bat index 2340f93..d5ab9ff 100644 --- a/scripts/build_debug.bat +++ b/scripts/build_debug.bat @@ -1,2 +1,2 @@ @echo off -odin build src/aseprite_odin_generator -define:RAYLIB_SHARED=true -out:build_generator/aseprite_odin_generator.exe -debug +odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_debug.exe -debug diff --git a/src/game.odin b/src/game.odin index c4c04c5..ddaf516 100644 --- a/src/game.odin +++ b/src/game.odin @@ -18,84 +18,8 @@ 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 { - atlas_size_x: i32, - atlas_size_y: i32, - pixel_padding_x_int: i32, - pixel_padding_y_int: i32, - 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, - atlas_render_has_preview: bool, - atlas_render_size: i32, -} - g_mem: ^GameMemory -w, h: f32 - game_camera :: proc() -> rl.Camera2D { w = f32(rl.GetScreenWidth()) h = f32(rl.GetScreenHeight()) @@ -103,13 +27,10 @@ game_camera :: proc() -> rl.Camera2D { 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 @@ -123,7 +44,9 @@ update :: proc() { // Update the virtual mouse position (needed for GUI interaction to work properly for instance) rl.SetMouseScale(1 / scaling, 1 / scaling) - update_screen() + if g_mem.should_open_file_dialog { + open_file_dialog_and_store_output_paths() + } } draw :: proc() { @@ -134,81 +57,55 @@ draw :: proc() { draw_screen_ui() - if g_mem.atlas_render { + if g_mem.should_render_atlas { draw_screen_target() } free_all(context.temp_allocator) } -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_atlas_settings_and_preview() } draw_screen_target :: proc() { - rl.BeginTextureMode(g_mem.atlas_render_texture_target) + 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 g_mem.input_path_set { unmarshall_aseprite_dir(g_mem.output_folder_path, &atlas_entries) } else if g_mem.input_files_set { unmarshall_aseprite_files(g_mem.source_files_to_pack, &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) - pack_atlas_entries( - atlas_entries[:], - &atlas, - g_mem.packer_settings.pixel_padding_x_int, - g_mem.packer_settings.pixel_padding_y_int, - ) - delete(atlas_entries) + // 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 + pack_atlas_entries(atlas_entries[:], &atlas, padding_x, padding_y) + + // OpenGL's Y buffer is flipped rl.ImageFlipVertical(&atlas) - rl.UnloadTexture(g_mem.atlas_render_texture_target.texture) + // rl.UnloadTexture(atlas_render_target.texture) + fmt.println("Packed everything!") + atlas_render_target.texture = rl.LoadTextureFromImage(atlas) - g_mem.atlas_render_texture_target.texture = rl.LoadTextureFromImage(atlas) - - rl.UnloadImage(atlas) - - g_mem.atlas_render = false + g_mem.should_render_atlas = false g_mem.atlas_render_has_preview = true } @@ -236,21 +133,30 @@ draw_atlas_settings_and_preview :: proc() { elements_height: f32 = 0 rl.GuiPanel(left_half_rect, "Atlas Settings") - elements_height += 25 * scaling + elements_height += small_offset / 2 - rl.GuiLine({y = elements_height, width = left_half_rect.width}, "General Settings") - elements_height += small_offset + @(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 = big_offset * 2, + width = left_half_rect.width - small_offset * 2, height = small_offset, } @@ -272,103 +178,196 @@ draw_atlas_settings_and_preview :: proc() { } 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 + // 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 - 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 + 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 - 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 + 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 - rl.GuiLine({y = elements_height, width = left_half_rect.width}, "Actions") - elements_height += small_offset + 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", + ) { + g_mem.atlas_render_has_preview = false + } + 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...", + ) { + } + 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, - }, - "Pack", - ) { - g_mem.atlas_render = true } - elements_height += small_offset * 2 + // Packing Options + if SettingsDropdownBoxActive == 1 { - if rl.GuiButton( - { - x = small_offset, - y = elements_height, - width = left_half_rect.width / 2 - small_offset * 2, - height = small_offset, - }, - "Save", - ) { - save_output() + @(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, + ) } - 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...", - ) { + + // Save Options + if SettingsDropdownBoxActive == 2 { } @@ -387,18 +386,28 @@ draw_atlas_settings_and_preview :: proc() { if !g_mem.atlas_render_has_preview { rl.GuiDummyRec(preview_rect, "PREVIEW") } else { - rl.DrawRectangleRec(preview_rect, rl.WHITE) + // 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, + ) } - 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() { @@ -447,77 +456,3 @@ open_file_dialog_and_store_output_paths :: proc() { g_mem.should_open_file_dialog = false } - -draw_and_handle_source_files_logic :: proc() { - #partial switch g_mem.app_screen { - case .SourceFilesPicker: - result := rl.GuiTextInputBox( - rl.Rectangle{width = (w / scaling), height = (h / scaling)}, - "Files", - "File input box", - "Open Source Files;Open Source Folder", - cstring(rawptr(&g_mem.file_dialog_text_buffer)), - FILE_DIALOG_SIZE, - nil, - ) - if result != -1 { - file_dialg_type: FileDialogType - if result == 1 || result == 2 { - file_dialg_type = .SourceFiles if result == 1 else .SourceFolder - } else if result == 0 { - file_dialg_type = .Exit - } - handle_source_file_logic(file_dialg_type) - fmt.println("result: ", result) - } - case .OutputLocationPicker: - result := rl.GuiTextInputBox( - rl.Rectangle{width = (w / scaling), height = (h / scaling)}, - "Files", - "Output Folder", - "Choose Output Folder", - cstring(rawptr(&g_mem.file_dialog_text_buffer)), - FILE_DIALOG_SIZE, - nil, - ) - if result != -1 { - file_dialg_type: FileDialogType = .OutputFolder if result == 1 else .Exit - handle_source_file_logic(file_dialg_type) - fmt.println("result: ", result) - } - case .SaveToOutputPicker: - result := rl.GuiTextInputBox( - rl.Rectangle{width = (w / scaling), height = (h / scaling)}, - "Files", - "Output Folder", - "Choose Output Folder", - cstring(rawptr(&g_mem.file_dialog_text_buffer)), - FILE_DIALOG_SIZE, - nil, - ) - if result != -1 { - file_dialg_type: FileDialogType = .SourceFolder if result == 1 else .Exit - handle_source_file_logic(file_dialg_type) - fmt.println("result: ", result) - } - } -} - -draw_packer_and_settings :: proc() { - -} - -handle_source_file_logic :: proc(picker_type: FileDialogType) { - switch picker_type { - case .Exit: - g_mem.should_open_file_dialog = false - rl.CloseWindow() - case .SourceFiles: - fallthrough - case .SourceFolder: - fallthrough - case .OutputFolder: - g_mem.source_location_type = picker_type - g_mem.should_open_file_dialog = true - } -} diff --git a/src/generator.odin b/src/generator.odin index 4c7c7df..5ea7382 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -81,6 +81,9 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr 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) { + assert(atlas.width != 0, "This shouldn't be 0!") + assert(atlas.height != 0, "This shouldn't be 0!") + all_entries: [dynamic]rl.Image // it's fine to store it like this, rl.Image just stores a pointer to the data { for entry in entries { diff --git a/src/globals.odin b/src/globals.odin new file mode 100644 index 0000000..bfbbe10 --- /dev/null +++ b/src/globals.odin @@ -0,0 +1,66 @@ + +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, + Exit, +} + +PackerSettings :: struct { + atlas_size_x: i32, + atlas_size_y: i32, + pixel_padding_x_int: i32, + pixel_padding_y_int: i32, + padding_enabled: bool, + fix_pixel_bleeding: 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_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_checked_background: rl.RenderTexture2D, + should_render_atlas: bool, + atlas_render_has_preview: bool, + atlas_render_size: i32, +} diff --git a/src/symbol_exports.odin b/src/symbol_exports.odin index 628c968..ea97dae 100644 --- a/src/symbol_exports.odin +++ b/src/symbol_exports.odin @@ -36,7 +36,12 @@ game_init :: proc() { h = 720, } - g_mem.atlas_render_texture_target = rl.LoadRenderTexture(2048, 2048) + 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") From 1f1a0ee48d7ab4fbcee03c48413112f8d7b24af7 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Mon, 22 Apr 2024 20:09:42 +0300 Subject: [PATCH 09/31] working build & fixed the double padding --- scripts/build_release.bat | 2 +- src/game.odin | 2 +- src/generator.odin | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/build_release.bat b/scripts/build_release.bat index aa3e8bd..0d1ac5b 100644 --- a/scripts/build_release.bat +++ b/scripts/build_release.bat @@ -1,2 +1,2 @@ @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 +odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_release.exe -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -subsystem:windows diff --git a/src/game.odin b/src/game.odin index ddaf516..4af23b7 100644 --- a/src/game.odin +++ b/src/game.odin @@ -202,7 +202,7 @@ draw_atlas_settings_and_preview :: proc() { height = small_offset, }, " Enable padding", - &g_mem.packer_settings.fix_pixel_bleeding, + &g_mem.packer_settings.padding_enabled, ) elements_height += small_offset * 2 diff --git a/src/generator.odin b/src/generator.odin index 5ea7382..f1ad8e3 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -114,8 +114,8 @@ pack_atlas_entries :: proc(entries: []AtlasEntry, atlas: ^rl.Image, offset_x: i3 for entry, entryIdx in all_entries { entry_stb_rect := &rects[entryIdx] - entry_stb_rect.w = stbrp.Coord(entry.width + offset_x * 2) - entry_stb_rect.h = stbrp.Coord(entry.height + offset_y * 2) + entry_stb_rect.w = stbrp.Coord(entry.width + offset_x) + entry_stb_rect.h = stbrp.Coord(entry.height + offset_y) } ctx: stbrp.Context From 362f1a8165eb46cd6c25ad7a993b4e70364abc3d Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Tue, 23 Apr 2024 09:41:04 +0300 Subject: [PATCH 10/31] escape key doesn't close the application in release mode anymore --- src/symbol_exports.odin | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/symbol_exports.odin b/src/symbol_exports.odin index ea97dae..8774c81 100644 --- a/src/symbol_exports.odin +++ b/src/symbol_exports.odin @@ -12,7 +12,7 @@ game_update :: proc() -> bool { @(export) game_init_window :: proc() { rl.SetConfigFlags({.WINDOW_RESIZABLE}) - rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer, Powered by Raylib & Odin") + rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer") rl.SetWindowPosition(200, 200) rl.SetWindowMinSize(1400, 800) } @@ -25,6 +25,11 @@ game_init :: proc() { 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), @@ -37,11 +42,11 @@ game_init :: proc() { } g_mem.atlas_render_texture_target = rl.LoadRenderTexture(256, 256) - g_mem.atlas_render_size = 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) + 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") From 52ac2819c793b0bbe575cdc21b460d4cf654cdd0 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Tue, 23 Apr 2024 10:01:51 +0300 Subject: [PATCH 11/31] updated readme & fixed the submodule setup for tinyfiledialogs + added a build script for windows --- .gitignore | 5 ++++- README.md | 7 +++++-- src/dialog/build.bat | 5 +++++ src/dialog/libtinyfiledialogs | 1 + 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 src/dialog/build.bat create mode 160000 src/dialog/libtinyfiledialogs diff --git a/.gitignore b/.gitignore index b1203b3..66483a8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ linux build/ build_generator/ ols.json -libtinyfiledialogs.a \ No newline at end of file +libtinyfiledialogs.a +libtinyfiledialogs.obj +tinyfiledialogs.lib +tinyfiledialogs.obj \ No newline at end of file diff --git a/README.md b/README.md index a20f4ac..6cb3dae 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ Yet-Another-Atlas-Packer by Stefan Stefanov Simple atlas packer using `stb_rect_pack` from the `stb` family of header libraries & `raylib` for rendering/ui. -I'm using a custom marshall/unmarshall odin library for reading .aseprite files found [here](https://github.com/blob1807/odin-aseprite) +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). -Project template provided by Karl Zylinski on github [here](https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template). +I'm using a library for marhsalling the aseprite files found [here](https://github.com/blob1807/odin-aseprite) on github. + +Project template provided by Karl Zylinski found [here](https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template) on github. diff --git a/src/dialog/build.bat b/src/dialog/build.bat new file mode 100644 index 0000000..6553a0f --- /dev/null +++ b/src/dialog/build.bat @@ -0,0 +1,5 @@ +@echo off + +cl /c libtinyfiledialogs\tinyfiledialogs.c + +lib tinyfiledialogs.obj /out:tinyfiledialogs.lib \ No newline at end of file diff --git a/src/dialog/libtinyfiledialogs b/src/dialog/libtinyfiledialogs new file mode 160000 index 0000000..cc6b593 --- /dev/null +++ b/src/dialog/libtinyfiledialogs @@ -0,0 +1 @@ +Subproject commit cc6b593c029110af8045826ce691f540c85e850c From 16a4faaff7710a928174355f8d57eede4f214e93 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Tue, 23 Apr 2024 10:20:03 +0300 Subject: [PATCH 12/31] update readme with youtube link to a preview of the program --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cb3dae..d569d62 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Yet-Another-Atlas-Packer by Stefan Stefanov ## Description -Simple atlas packer using `stb_rect_pack` from the `stb` family of header libraries & `raylib` for rendering/ui. +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. 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). From 8c7a22ed8fdd60079081c84817cf3972d0760f7f Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Tue, 23 Apr 2024 10:26:32 +0300 Subject: [PATCH 13/31] add screenshot to repo & readme --- README.md | 4 ++++ repo_assets/image.png | Bin 0 -> 18730 bytes 2 files changed, 4 insertions(+) create mode 100644 repo_assets/image.png diff --git a/README.md b/README.md index d569d62..d71b72b 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Yet-Another-Atlas-Packer by Stefan Stefanov 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. + + + + 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). diff --git a/repo_assets/image.png b/repo_assets/image.png new file mode 100644 index 0000000000000000000000000000000000000000..af4f14c64eaacb9fb047312c7d75a7803a62df0a GIT binary patch literal 18730 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 literal 0 HcmV?d00001 From d68a2f99b540ea80e785446600ddae51b6665d4a Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Tue, 23 Apr 2024 10:29:22 +0300 Subject: [PATCH 14/31] fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d71b72b..3a0d31f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,6 @@ Simple atlas packer using `stb_rect_pack` from the `stb` family of header librar The goal of the tool is to take in multiple aseprite files and pack them into a single atlas, outputting some metadata in the process in the form of json and/or source files for direct use in odin (maybe more languages too). -I'm using a library for marhsalling the aseprite files found [here](https://github.com/blob1807/odin-aseprite) on github. +I'm using a library for marshalling the aseprite files found [here](https://github.com/blob1807/odin-aseprite) on github. Project template provided by Karl Zylinski found [here](https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template) on github. From 0faa687d058277fef1403e980ec76819d23e76bc Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Tue, 23 Apr 2024 12:39:32 +0300 Subject: [PATCH 15/31] refactor of the save method & replaced the path strings with a maybe(string) instead of a string+bool --- .vscode/tasks.json | 2 +- src/game.odin | 81 +++++++++++++++++++++++++++++++------------- src/globals.odin | 11 +++--- src/save_output.odin | 31 +++++++++++------ 4 files changed, 82 insertions(+), 43 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 66b9279..e054793 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -17,7 +17,7 @@ }, "group": { "kind": "build", - "isDefault": true + "isDefault": false }, }, { diff --git a/src/game.odin b/src/game.odin index 4af23b7..b1ff9ba 100644 --- a/src/game.odin +++ b/src/game.odin @@ -45,7 +45,7 @@ update :: proc() { rl.SetMouseScale(1 / scaling, 1 / scaling) if g_mem.should_open_file_dialog { - open_file_dialog_and_store_output_paths() + open_file_dialog() } } @@ -80,10 +80,8 @@ draw_screen_target :: proc() { atlas_entries: [dynamic]AtlasEntry delete(atlas_entries) - if g_mem.input_path_set { - unmarshall_aseprite_dir(g_mem.output_folder_path, &atlas_entries) - } else if g_mem.input_files_set { - unmarshall_aseprite_files(g_mem.source_files_to_pack, &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 @@ -102,7 +100,7 @@ draw_screen_target :: proc() { // OpenGL's Y buffer is flipped rl.ImageFlipVertical(&atlas) // rl.UnloadTexture(atlas_render_target.texture) - fmt.println("Packed everything!") + fmt.println("Packed everything!") atlas_render_target.texture = rl.LoadTextureFromImage(atlas) g_mem.should_render_atlas = false @@ -410,9 +408,12 @@ draw_atlas_settings_and_preview :: proc() { } } -open_file_dialog_and_store_output_paths :: proc() { - if g_mem.source_location_type == .SourceFiles { - files := cstring( +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]), @@ -422,37 +423,69 @@ open_file_dialog_and_store_output_paths :: proc() { 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, "|") - 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) + } else { + fmt.println("No files were selected!") + } - fmt.println(g_mem.source_files_to_pack) - } - if g_mem.source_location_type == .SourceFolder { + case .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 { + 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]), ), ) - g_mem.output_folder_path = strings.clone_from_cstring(file) + 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!") + } - g_mem.output_path_set = (len(file) > 0) - fmt.println(g_mem.output_folder_path) + 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, atlas_path}) + 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 } diff --git a/src/globals.odin b/src/globals.odin index bfbbe10..b2af01b 100644 --- a/src/globals.odin +++ b/src/globals.odin @@ -25,7 +25,7 @@ FileDialogType :: enum { SourceFiles, SourceFolder, OutputFolder, - Exit, + SaveFileAs, } PackerSettings :: struct { @@ -46,14 +46,11 @@ GameMemory :: struct { window_info: WindowInformation, monitor_info: MonitorInformation, // Where the output files will be written (atlas.png, json output, etc) - output_path_set: bool, - output_folder_path: string, + output_folder_path: Maybe(string), // If files were chosen as input - their paths - input_path_set: bool, - source_location_to_pack: string, + source_location_to_pack: Maybe(string), // If a folder was chosen as input - the path - input_files_set: bool, - source_files_to_pack: []string, + source_files_to_pack: Maybe([]string), // What type of file dialog to open source_location_type: FileDialogType, // Packer settings diff --git a/src/save_output.odin b/src/save_output.odin index e69f405..34d1914 100644 --- a/src/save_output.odin +++ b/src/save_output.odin @@ -1,17 +1,26 @@ package game import "core:strings" +import "core:fmt" 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) +when ODIN_OS == .Windows { + atlas_path :: "\\atlas.png" +} else { + atlas_path :: "/atlas.png" +} + +save_output :: proc() { + if output_path, ok := g_mem.output_folder_path.(string); ok { + if len(output_path) == 0 { + fmt.println("Output path is empty!") + return + } + + image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture) + rl.ImageFlipVertical(&image) + output_path := strings.concatenate({output_path, atlas_path}) + cstring_output_path := strings.clone_to_cstring(output_path) + rl.ExportImage(image, cstring_output_path) + } } From 194550335dd7c86022b119a4a0980b34cb18f448 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Tue, 23 Apr 2024 12:49:44 +0300 Subject: [PATCH 16/31] 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) --- src/game.odin | 1 - src/math.odin | 8 ---- src/raylib_helpers.odin | 70 ------------------------------- src/rect.odin | 66 ----------------------------- src/{ => utils}/animation.odin | 2 +- src/{ => utils}/handle_array.odin | 2 +- src/{ => utils}/helpers.odin | 9 +++- 7 files changed, 10 insertions(+), 148 deletions(-) delete mode 100644 src/math.odin delete mode 100644 src/raylib_helpers.odin delete mode 100644 src/rect.odin rename src/{ => utils}/animation.odin (98%) rename src/{ => utils}/handle_array.odin (99%) rename src/{ => utils}/helpers.odin (87%) diff --git a/src/game.odin b/src/game.odin index b1ff9ba..0df7608 100644 --- a/src/game.odin +++ b/src/game.odin @@ -448,7 +448,6 @@ open_file_dialog :: proc() { fmt.println("Got an empty path from the file dialog!") } - case .OutputFolder: file := cstring( diag.select_folder_dialog( diff --git a/src/math.odin b/src/math.odin deleted file mode 100644 index 4f6634c..0000000 --- a/src/math.odin +++ /dev/null @@ -1,8 +0,0 @@ -package game - -Vec2i :: [2]int -Vec2 :: [2]f32 - -vec2_from_vec2i :: proc(p: Vec2i) -> Vec2 { - return {f32(p.x), f32(p.y)} -} diff --git a/src/raylib_helpers.odin b/src/raylib_helpers.odin deleted file mode 100644 index d884fc4..0000000 --- a/src/raylib_helpers.odin +++ /dev/null @@ -1,70 +0,0 @@ -package game - -import "core:slice" -import rl "vendor:raylib" - -Texture :: rl.Texture -Color :: rl.Color - -texture_rect :: proc(tex: Texture, flip_x: bool) -> Rect { - return( - { - x = 0, - y = 0, - width = flip_x ? -f32(tex.width) : f32(tex.width), - height = f32(tex.height), - } \ - ) -} - -load_premultiplied_alpha_ttf_from_memory :: proc(file_data: []byte, font_size: int) -> rl.Font { - font := rl.Font { - baseSize = i32(font_size), - glyphCount = 95, - } - - font.glyphs = rl.LoadFontData( - &file_data[0], - i32(len(file_data)), - font.baseSize, - {}, - font.glyphCount, - .DEFAULT, - ) - - if font.glyphs != nil { - font.glyphPadding = 4 - - atlas := rl.GenImageFontAtlas( - font.glyphs, - &font.recs, - font.glyphCount, - font.baseSize, - font.glyphPadding, - 0, - ) - atlas_u8 := slice.from_ptr((^u8)(atlas.data), int(atlas.width * atlas.height * 2)) - - for i in 0 ..< atlas.width * atlas.height { - a := atlas_u8[i * 2 + 1] - v := atlas_u8[i * 2] - atlas_u8[i * 2] = u8(f32(v) * (f32(a) / 255)) - } - - font.texture = rl.LoadTextureFromImage(atlas) - rl.SetTextureFilter(font.texture, .BILINEAR) - - // Update glyphs[i].image to use alpha, required to be used on ImageDrawText() - for i in 0 ..< font.glyphCount { - rl.UnloadImage(font.glyphs[i].image) - font.glyphs[i].image = rl.ImageFromImage(atlas, font.recs[i]) - } - //TRACELOG(LOG_INFO, "FONT: Data loaded successfully (%i pixel size | %i glyphs)", font.baseSize, font.glyphCount); - - rl.UnloadImage(atlas) - } else { - font = rl.GetFontDefault() - } - - return font -} diff --git a/src/rect.odin b/src/rect.odin deleted file mode 100644 index 4291aa2..0000000 --- a/src/rect.odin +++ /dev/null @@ -1,66 +0,0 @@ -// procs for modifying and managing rects - -package game - -import rl "vendor:raylib" - -Rect :: rl.Rectangle - -RectEmpty :: Rect{} - -split_rect_top :: proc(r: Rect, y: f32, m: f32) -> (top, bottom: Rect) { - top = r - bottom = r - top.y += m - top.height = y - bottom.y += y + m - bottom.height -= y + m - return -} - -split_rect_left :: proc(r: Rect, x: f32, m: f32) -> (left, right: Rect) { - left = r - right = r - left.width = x - right.x += x + m - right.width -= x + m - return -} - -split_rect_bottom :: proc(r: rl.Rectangle, y: f32, m: f32) -> (top, bottom: rl.Rectangle) { - top = r - top.height -= y + m - bottom = r - bottom.y = top.y + top.height + m - bottom.height = y - return -} - -split_rect_right :: proc(r: Rect, x: f32, m: f32) -> (left, right: Rect) { - left = r - right = r - right.width = x - left.width -= x + m - right.x = left.x + left.width - return -} - -rect_middle :: proc(r: Rect) -> Vec2 { - return {r.x + f32(r.width) * 0.5, r.y + f32(r.height) * 0.5} -} - -inset_rect :: proc(r: Rect, x: f32, y: f32) -> Rect { - return {r.x + x, r.y + y, r.width - x * 2, r.height - y * 2} -} - -rect_add_pos :: proc(r: Rect, p: Vec2) -> Rect { - return {r.x + p.x, r.y + p.y, r.width, r.height} -} - -mouse_in_rect :: proc(r: Rect) -> bool { - return rl.CheckCollisionPointRec(rl.GetMousePosition(), r) -} - -mouse_in_world_rect :: proc(r: Rect, camera: rl.Camera2D) -> bool { - return rl.CheckCollisionPointRec(rl.GetScreenToWorld2D(rl.GetMousePosition(), camera), r) -} diff --git a/src/animation.odin b/src/utils/animation.odin similarity index 98% rename from src/animation.odin rename to src/utils/animation.odin index aeeaed5..4a55c6e 100644 --- a/src/animation.odin +++ b/src/utils/animation.odin @@ -4,7 +4,7 @@ // `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 +package utils import "core:log" diff --git a/src/handle_array.odin b/src/utils/handle_array.odin similarity index 99% rename from src/handle_array.odin rename to src/utils/handle_array.odin index 69d9709..b6b59d1 100644 --- a/src/handle_array.odin +++ b/src/utils/handle_array.odin @@ -3,7 +3,7 @@ // 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 +package utils Handle :: struct($T: typeid) { // idx 0 means unused. Note that slot 0 is a dummy slot, it can never be used. diff --git a/src/helpers.odin b/src/utils/helpers.odin similarity index 87% rename from src/helpers.odin rename to src/utils/helpers.odin index 190d044..6f07e8b 100644 --- a/src/helpers.odin +++ b/src/utils/helpers.odin @@ -1,6 +1,6 @@ // generic odin helpers -package game +package utils import "core:intrinsics" import "core:reflect" @@ -39,3 +39,10 @@ remap :: proc "contextless" ( } 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)} +} \ No newline at end of file From 67d7b3e2276c6a7048335ec34c22d9d41edae85f Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Tue, 23 Apr 2024 19:17:38 +0300 Subject: [PATCH 17/31] working demo of what the metdata exporter is supposed to do --- .vscode/tasks.json | 4 +- odinfmt.json | 6 + .../aseprite_odin_generator.odin | 12 +- src/aseprite_odin_generator/atlas.png | Bin 10891 -> 11027 bytes src/aseprite_odin_generator/big.aseprite | Bin 1115 -> 1333 bytes src/aseprite_odin_generator/metadata.json | 1 + src/generator.odin | 175 +++++++++++++----- src/globals.odin | 1 - src/save_output.odin | 4 +- src/symbol_exports.odin | 1 - 10 files changed, 150 insertions(+), 54 deletions(-) create mode 100644 odinfmt.json create mode 100644 src/aseprite_odin_generator/metadata.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e054793..a419bf5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -70,7 +70,7 @@ }, "group": { "kind": "build", - "isDefault": true + "isDefault": false }, }, { @@ -92,7 +92,7 @@ }, "group": { "kind": "build", - "isDefault": false + "isDefault": true }, } ] diff --git a/odinfmt.json b/odinfmt.json new file mode 100644 index 0000000..12c1d35 --- /dev/null +++ b/odinfmt.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json", + "character_width": 100, + "sort_imports": true, + "tabs": true +} \ No newline at end of file diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin index d897541..225578e 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin @@ -1,14 +1,17 @@ package generator import ase "../aseprite" +import "core:encoding/json" import "core:fmt" import "core:mem" import "core:os" import fp "core:path/filepath" import "core:slice" -import "core:strings" +import s "core:strings" import "core:testing" + import rl "vendor:raylib" +import stbrp "vendor:stb/rect_pack" import gen ".." @@ -24,14 +27,17 @@ main :: proc() { } cwd := os.get_current_directory() - target_dir := strings.concatenate({cwd, "\\src\\aseprite_odin_generator\\"}) + target_dir := s.concatenate({cwd, "\\src\\aseprite_odin_generator\\"}) atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK) atlas_entries: [dynamic]gen.AtlasEntry 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("src/aseprite_odin_generator/metadata.json", json_bytes) - gen.pack_atlas_entries(atlas_entries[:], &atlas, 10, 10) rl.ExportImage(atlas, EXPORT_PATH) } diff --git a/src/aseprite_odin_generator/atlas.png b/src/aseprite_odin_generator/atlas.png index afb2d717bfef7045898ca774d537aaa5911f7546..94faceeb56095bd4d1e706f0f473f24853e86a14 100644 GIT binary patch literal 11027 zcmeI2Yfw{X8pq!gLOftXf`WTUfDkE&T4;o*fMNoH6Y&BRD7Dzth@%^wdyzJryFNl(&Ci^A?g0tWDn=|Lj$(cMk z&+Y#_zc-V~o|GgR%bn{E0AMA~k52`F25)HqWx$t#E$u@9OzTXHk4@jQY$fJO2!veP z`+dn}A{Q@A>g-8swd$V)hnTN|yi@&GCHsiDbiUKy)UU6DsIKQ!>{Ap3ZG&P-oidA>Y* zjc5@*T@AgR6Bxoj4`gqpgPx(CWYZij?Xmzoy03q=PS)0}Ky?~}0%Z~$?2jKwBEwfm zt;7p!$A2s`Kmxe(2_9dTjs-DqCPyb0=gJ>qC9 zJMmTTi?=X=^5zS$85tAcSmjF9*`j_7!~5qO6zlm)53Ji&m>F(Q8(vViypm{lVdv{9 zCqJFj+2+^xR-eIuk9Eb#bLoIf1a9z>*AME?r}AQ+udlD*+mmvGE#bBXM$qx;s`f3~ zm(R%Qs?YBPM)l}4R}{yh?3H~el~*oul`rj`J>d`5LjSi5GiUjK=k60~hD|r;8+uU# zx~Mhq^q{_`pX>L8ANq@3415i$n+5PCSM~c^4()B(k-M80k!JH6H0<@UR34d(+~(ck zhZ(JA&463bvIPSkH*_<&!9;-t_PU3I`5~C#n6hE;&C~~ni=ax%H$+(%Y*-Tq`x5BX zOZX?t#Qj2X)TZAFZ>yqNoY_}s(@>`YbLpDjyub|UYv~5_p}qg=C5)XN(=DX8g>W^& z4}5R;eerpWuhK$D-?lHVBZvUvlUvZ##gY3KZfESfCGL$km^i^r85Zf$63M!Rga@Jv zIam2`h%+CKgvo>>VN#`P2DFXO#8J{V!uY2ArV`HE-+rMs+_w?7Y>q zx9Q4*mwJ!vm9^&BBZSt!aFWG=3w013S6X*X`K-7t&9ARJtJZwQC&ni%FSQg;yp|K=SA6|o#LRGPY8vJewJrAAON40iB+70;!G7joGgx3r zv$!i`sB~!wyMO`Cdbg)kudSZpC(NuUa!3;dcbW%!PYlV8$2|s_=*>qeie< zD)0ExC7<<@eW`f|u}1~bWFv$9QteGMzVZe4qO9bDJdznPy0(zo)dJyU4_e%E5E&gU zJiu~3gl|K2mrH?6HV%(~Q#q1^?G7wJnS%-W#CRZ^_-3r>wp*U5(>|iXukDNPxCBh5 zYtp3h`hNc6euwV%14EL9GPSTe7rE%By?#d&@U=j=kL|;Sv;MB5iU7Lij>4e0FFtT@ zc-e-FR4;%J;4U~15Lb4*Q#wYI9`zJ<0&MXsXXMKQgNW;E6w@VT;=2sSMY6%)K`KYW z6)>?^AFIh*R>j6)UJTn2Zh=9*j&9rU(zm0P1K6r0L|3(EY|VD|ReV5aJPNUqLUGO+ zLc&J=P6qfia005bs&b#->ZKV-uX&@Ys6*+n5SQ{WdD)zwHDKnpr}>ZOkCk zu6I;i^17%ng^$LQd}EW0*|x8s#xwSW|D3~9b!5p-^3E3yH;md4 zcX|C@-r}wir*;oCI6{az=DN(dsy=e$Y=O!ARUkZv=7_o-i#EJ13iP%|9(gcqb%%|; zMLte}Jz*Ip!I7<^5Ag`6h=vpoWba2cv9;!q^XApnuKri8fvr8n&F#){+EM_T&mcvW z3}oiIAzrKGo#e1M^}`zo`x4`VqE;n3<;;b7h9j>$t*vVV z3t6K9cXQCBEbe$BW{A|iv-4xzy#HU^sAB$S�DAEuj{B^gyD9ff@#C7~cC|XsA<- z0vQD|3S<<>C=ya6q)14SkRl;PLW+bG2`Lg%B&0}4k&q%GMM8>%6bUI3QY55E_2D)c^nh literal 10891 zcmeI2|5FoZ9>)TWxJ<3>+2b^G)wyyLP^r~sWIgA^9Ho4-1kj2c-#mfblO;>&pKzW1 z<;hN-eV)(f^ZvX)ubIrGMypXvMByR;fFvV5H3t9$J|X}OgdY1s!cGT;FP(9?SQu<1VLmFmJ?HsCo_HnxR7?j`?Z*>e%jQjc4D# zS$8-(>+kdjhZRCXrPG?gZQcHt|J?uO_6H?pp=5WG3&)&!6C%Nm8`-5*_1!vbubBGi zJ|n{o=12!Op8X_}W)itqr|atUxM!6|iV_n=cz$=i_Z{&ew1sM3OPWPgJ*r(X z8uRAAztx9q@ON0OBD=p?7PB$lHa(+OgPU6=bPS-=Mts+c%TX!;skV^Jt;VA@zq1SQ z{0Xx!_)kzHLxBAaxC_`c#Zeb$)pf0GEp0L+lV%(akRO4&P)Fm7#L%4j;4Oc~En*`R z2>JrSrCR2yVf<2?{)xd3qP(hNugLL41ej}>ZJ;~jfPGq-uj2xa1qt3`(xW7T<8nOz zS?-z>XO4Z)sUL}C&v_SHE=p+vY=MfuHO(`9hPy8gxLZ8>rwip`D%~NGPEptogZFs! z{cIEA(iefyxq!Y9QHE06=g+R8m?97JH7Aj3t&lhmFlJI-lGIpK~3YVXq za#ZgFlyolGvs?41y|pRlS>^0{mp1I8jeTXU#2E4oz-r$0q>s!Ed^+>jJll0esQcGX z`w!gFKUC(AS-f`A#t4b-@-^c-J?RA#YRyAsVj*dDy01#g?dPIn2i4vR2^A@lgHLAE z<7vvlxWR~|xqXV~i{5}!_F4eB>w#q=_9PX75i378zgO(%*q97UfZY%o6nShwZh}y$ zu_5_hw)11yhv?WXTvanBN6Q}(QZi{7ym68a`#me9Z$YaZErlj`Uz-$Prnm+wIVIL@ye40VGi5v{Pq#u|n`GYy6VycB9m) z$m?SU#qbYOu9g@Zx@6enIm@0I&Q9JNNf@ORjw>MEnd2Jk)z((!NWYS;gR^UWG$#mgnHw<0{J|2&5O{h>L zangEo01a|{Cm|q?dFYGYU>-KC}`NSLtBAv^*hF^m7D8PmN5!jhm4>*!EqCO{v=6gCJ3&iMGdmM}g;)MkNT`9S{F7$lhUn9K4bJxM zOzzz-+n1po`o32e)k(=`g|Db>`&YFs&+@0v&<=Nc8;727WFiO>&!I1+3m|01=$gG; z6)N2h*tRR{Dip&kuvziM78#a=>J>j<+7_1h;Hn&VJ&(?MjVqMRpdD5_9-TGr|56nz zto{(eE)EheO)vEV<&Z(mb(RUHJSQLJtA$($Sf(ctCiOMOX^?X5o=C<;dSv(GylYohWEz=?*m=~-Us{`@F?I( z$diyKAx}b{gggm(67nSENyw9sCm~Nlo`gIJc@pv@?Gyo=Pk|XMf(LN>4x1bde4KrwDfi delta 71 zcmdnWb(@1dnuUSk!BWPF?3IkSH#V{}vil@frWPp}PA*`SfU=E1Y!QgK@eKwB237?I Q25trghX0#yFg7v)0Q#R4GXMYp diff --git a/src/aseprite_odin_generator/metadata.json b/src/aseprite_odin_generator/metadata.json new file mode 100644 index 0000000..a07b92b --- /dev/null +++ b/src/aseprite_odin_generator/metadata.json @@ -0,0 +1 @@ +[{"name":"Edinica","location":[95,10],"size":[58,57]},{"name":"Dvoika_0","location":[234,10],"size":[55,31]},{"name":"Troika","location":[10,10],"size":[75,75]},{"name":"Dvoika_1","location":[163,10],"size":[61,33]}] \ No newline at end of file diff --git a/src/generator.odin b/src/generator.odin index f1ad8e3..62483a8 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -1,15 +1,41 @@ package game import ase "./aseprite" +import "core:encoding/json" import "core:fmt" +import "core:mem" import "core:os" import fp "core:path/filepath" +import "core:strings" import rl "vendor:raylib" import stbrp "vendor:stb/rect_pack" +CellData :: struct { + layer_index: u16, + opacity: u8, + frame_index: i32, + img: rl.Image, +} + AtlasEntry :: struct { - path: string, - cells: [dynamic]rl.Image, + path: string, + cells: [dynamic]CellData, + frames: i32, + layer_names: [dynamic]string, + layer_cell_count: [dynamic]i32, +} + +SingleFrameSprite :: distinct rl.Rectangle + +AnimatedSprite :: struct { + x, y: i32, + width, height: i32, +} + +SpriteAtlasMetadata :: struct { + name: string, + location: [2]i32, + size: [2]i32, } unmarshall_aseprite_dir :: proc(path: string, atlas_entries: ^[dynamic]AtlasEntry) { @@ -27,14 +53,22 @@ unmarshall_aseprite_files_file_info :: proc( files: []os.File_Info, atlas_entries: ^[dynamic]AtlasEntry, ) { - paths: [dynamic]string - for f in files { - append(&paths, f.fullpath) + if len(files) == 0 do return + + paths := make([]string, len(files)) + defer delete(paths) + + for f, fi in files { + paths[fi] = f.fullpath } + unmarshall_aseprite_files(paths[:], atlas_entries) + } unmarshall_aseprite_files :: proc(file_paths: []string, atlas_entries: ^[dynamic]AtlasEntry) { + if len(file_paths) == 0 do return + aseprite_document: ase.Document for file in file_paths { extension := fp.ext(file) @@ -43,6 +77,7 @@ unmarshall_aseprite_files :: proc(file_paths: []string, atlas_entries: ^[dynamic fmt.println("Unmarshalling file: ", file) ase.unmarshal_from_filename(file, &aseprite_document) atlas_entry := atlas_entry_from_compressed_cells(aseprite_document) + atlas_entry.path = file append(atlas_entries, atlas_entry) } @@ -52,26 +87,34 @@ unmarshall_aseprite_files :: proc(file_paths: []string, atlas_entries: ^[dynamic 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) { - for frame in document.frames { + atlas_entry.frames = auto_cast len(document.frames) + fmt.println("N Frames: ", len(document.frames)) + for frame, frameIdx in document.frames { + fmt.printfln("Frame_{0} Chunks: ", frameIdx, len(frame.chunks)) for chunk in frame.chunks { - cel_chunk, cok := chunk.(ase.Cel_Chunk) - if !cok { - continue - } + 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 - cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel) - if !ci_ok { - 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) } - append( - &atlas_entry.cells, - rl.Image { - data = rawptr(&cel_img.pixel[0]), - width = auto_cast cel_img.width, - height = auto_cast cel_img.height, - format = .UNCOMPRESSED_R8G8B8A8, - }, - ) } } return @@ -80,26 +123,33 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr /* 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) { - assert(atlas.width != 0, "This shouldn't be 0!") - assert(atlas.height != 0, "This shouldn't be 0!") +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_entries: [dynamic]rl.Image // it's fine to store it like this, rl.Image just stores a pointer to the data - { - for entry in entries { - append(&all_entries, ..entry.cells[:]) + 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_entries) - nodes := make([]stbrp.Node, num_entries) - rects := make([]stbrp.Rect, num_entries) + 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: ^rl.Image, + cell_of_entry: ^CellData, } - rect_idx_to_entry_and_cell: map[int]EntryAndCell + rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100, allocator) // Set the custom IDs cellIdx: int @@ -109,13 +159,15 @@ pack_atlas_entries :: proc(entries: []AtlasEntry, atlas: ^rl.Image, offset_x: i3 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 entry, entryIdx in all_entries { - entry_stb_rect := &rects[entryIdx] - entry_stb_rect.w = stbrp.Coord(entry.width + offset_x) - entry_stb_rect.h = stbrp.Coord(entry.height + offset_y) + 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 @@ -131,22 +183,53 @@ pack_atlas_entries :: proc(entries: []AtlasEntry, atlas: ^rl.Image, offset_x: i3 for rect, rectIdx in rects { entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx] cell := entry_and_cell.cell_of_entry - // We're grabbing the whole cell (the image itself) + src_rect := rl.Rectangle { x = 0, y = 0, - width = auto_cast cell.width, - height = auto_cast cell.height, + width = auto_cast cell.img.width, + height = auto_cast cell.img.height, } - // Placing it in the atlas in the calculated offsets (in the packing step) + dst_rect := rl.Rectangle { auto_cast rect.x + auto_cast offset_x, auto_cast rect.y + auto_cast offset_y, - auto_cast cell.width, - auto_cast cell.height, + auto_cast cell.img.width, + auto_cast cell.img.height, } - fmt.printfln("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect) - rl.ImageDraw(atlas, cell^, src_rect, dst_rect, rl.WHITE) + // 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 } diff --git a/src/globals.odin b/src/globals.odin index b2af01b..42a8b54 100644 --- a/src/globals.odin +++ b/src/globals.odin @@ -34,7 +34,6 @@ PackerSettings :: struct { pixel_padding_x_int: i32, pixel_padding_y_int: i32, padding_enabled: bool, - fix_pixel_bleeding: bool, output_json: bool, output_odin: bool, } diff --git a/src/save_output.odin b/src/save_output.odin index 34d1914..68363fe 100644 --- a/src/save_output.odin +++ b/src/save_output.odin @@ -22,5 +22,7 @@ save_output :: proc() { output_path := strings.concatenate({output_path, atlas_path}) cstring_output_path := strings.clone_to_cstring(output_path) rl.ExportImage(image, cstring_output_path) - } + } else { + fmt.println("Output path is empty!") + } } diff --git a/src/symbol_exports.odin b/src/symbol_exports.odin index 8774c81..d5ada25 100644 --- a/src/symbol_exports.odin +++ b/src/symbol_exports.odin @@ -13,7 +13,6 @@ game_update :: proc() -> bool { game_init_window :: proc() { rl.SetConfigFlags({.WINDOW_RESIZABLE}) rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer") - rl.SetWindowPosition(200, 200) rl.SetWindowMinSize(1400, 800) } From 52364f28b907b463f55b66984d780209dd3d4ad8 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Wed, 24 Apr 2024 21:08:36 +0300 Subject: [PATCH 18/31] working metdata output for json & odin in the aseprite_odin_generator tool --- .gitignore | 4 +- .../aseprite_odin_generator.odin | 4 +- src/aseprite_odin_generator/metadata.json | 2 +- src/generator.odin | 96 +++++++++++++++---- 4 files changed, 83 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 66483a8..91dc2bb 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,6 @@ ols.json libtinyfiledialogs.a libtinyfiledialogs.obj tinyfiledialogs.lib -tinyfiledialogs.obj \ No newline at end of file +tinyfiledialogs.obj + +.idea/ diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin index 225578e..d095938 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin @@ -37,7 +37,9 @@ main :: proc() { json_bytes, jerr := json.marshal(metadata) os.write_entire_file("src/aseprite_odin_generator/metadata.json", json_bytes) - + sb := gen.generate_odin_enums_and_atlas_offsets_file_sb(metadata[:]) + odin_output_str := s.to_string(sb) + os.write_entire_file("src/aseprite_odin_generator/output.odino", transmute([]byte)odin_output_str ) rl.ExportImage(atlas, EXPORT_PATH) } diff --git a/src/aseprite_odin_generator/metadata.json b/src/aseprite_odin_generator/metadata.json index a07b92b..be61ff1 100644 --- a/src/aseprite_odin_generator/metadata.json +++ b/src/aseprite_odin_generator/metadata.json @@ -1 +1 @@ -[{"name":"Edinica","location":[95,10],"size":[58,57]},{"name":"Dvoika_0","location":[234,10],"size":[55,31]},{"name":"Troika","location":[10,10],"size":[75,75]},{"name":"Dvoika_1","location":[163,10],"size":[61,33]}] \ No newline at end of file +[{"name":"Edinica","location":[95,10],"size":[58,57]},{"name":"Dvoika_0","location":[234,10],"size":[55,31]},{"name":"Dvoika_1","location":[163,10],"size":[61,33]},{"name":"Troika","location":[10,10],"size":[75,75]}] \ No newline at end of file diff --git a/src/generator.odin b/src/generator.odin index 62483a8..6ad9ebe 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -1,11 +1,10 @@ package game import ase "./aseprite" -import "core:encoding/json" import "core:fmt" -import "core:mem" import "core:os" import fp "core:path/filepath" +import "core:slice" import "core:strings" import rl "vendor:raylib" import stbrp "vendor:stb/rect_pack" @@ -25,13 +24,6 @@ AtlasEntry :: struct { layer_cell_count: [dynamic]i32, } -SingleFrameSprite :: distinct rl.Rectangle - -AnimatedSprite :: struct { - x, y: i32, - width, height: i32, -} - SpriteAtlasMetadata :: struct { name: string, location: [2]i32, @@ -89,6 +81,13 @@ unmarshall_aseprite_files :: proc(file_paths: []string, atlas_entries: ^[dynamic 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 { @@ -109,14 +108,21 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr 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 } @@ -210,17 +216,17 @@ pack_atlas_entries :: proc( 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_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 = { @@ -231,5 +237,55 @@ pack_atlas_entries :: proc( } append(&metadata, cell_metadata) } + return metadata } + +/* + 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 := []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") + + fmt.println("\n", strings.to_string(sb)) + + return sb +} From 99194e943ac4d9b539374a3ab37681e5b1990568 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Wed, 24 Apr 2024 22:39:10 +0300 Subject: [PATCH 19/31] working highly constrained metadata(ish) source code generator --- .../aseprite_odin_generator.odin | 26 ++- src/game.odin | 15 +- src/generator.odin | 182 ++++++++++++++++-- src/globals.odin | 1 + src/save_output.odin | 49 ++++- 5 files changed, 238 insertions(+), 35 deletions(-) diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin index d095938..a72f5ea 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin @@ -30,16 +30,32 @@ main :: proc() { target_dir := s.concatenate({cwd, "\\src\\aseprite_odin_generator\\"}) atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK) - atlas_entries: [dynamic]gen.AtlasEntry + 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("src/aseprite_odin_generator/metadata.json", json_bytes) - sb := gen.generate_odin_enums_and_atlas_offsets_file_sb(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("src/aseprite_odin_generator/metadata.json", json_bytes) + sb := gen.metadata_source_code_generate(metadata[:], gen.odin_source_generator_metadata) + odin_output_str := s.to_string(sb) + os.write_entire_file( + "src/aseprite_odin_generator/output.odino", + transmute([]byte)odin_output_str, + ) rl.ExportImage(atlas, EXPORT_PATH) + + // TestStruct :: struct { + // something: struct { + // name: string, + // age: int, + // }, + // } + // ts: TestStruct + // ts.something.name = "name" + + // jb, err := json.marshal(ts) + // sjb := transmute(string)jb + // fmt.println(sjb) } diff --git a/src/game.odin b/src/game.odin index 0df7608..237bfac 100644 --- a/src/game.odin +++ b/src/game.odin @@ -95,7 +95,8 @@ draw_screen_target :: proc() { 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 - pack_atlas_entries(atlas_entries[:], &atlas, padding_x, padding_y) + + g_mem.atlas_metadata = pack_atlas_entries(atlas_entries[:], &atlas, padding_x, padding_y) // OpenGL's Y buffer is flipped rl.ImageFlipVertical(&atlas) @@ -320,7 +321,7 @@ draw_atlas_settings_and_preview :: proc() { }, "Clear Atlas", ) { - g_mem.atlas_render_has_preview = false + clear_atlas_data() } elements_height += small_offset * 2 @@ -466,7 +467,7 @@ open_file_dialog :: proc() { file_path: cstring patterns: []cstring = {"*.png"} if default_path, ok := g_mem.output_folder_path.(string); ok { - default_path_filename := strings.concatenate({default_path, atlas_path}) + 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( @@ -488,3 +489,11 @@ open_file_dialog :: proc() { g_mem.should_open_file_dialog = false } + +clear_atlas_data :: proc() { + if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + delete(metadata) + // g_mem.atlas_metadata = nil + } + g_mem.atlas_render_has_preview = false +} diff --git a/src/generator.odin b/src/generator.odin index 6ad9ebe..4751d7b 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -30,11 +30,17 @@ SpriteAtlasMetadata :: struct { size: [2]i32, } -unmarshall_aseprite_dir :: proc(path: string, atlas_entries: ^[dynamic]AtlasEntry) { +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) + unmarshall_aseprite_files_file_info(fis, atlas_entries, alloc) } } else { fmt.println("Couldn't open folder: ", path) @@ -44,30 +50,35 @@ unmarshall_aseprite_dir :: proc(path: string, atlas_entries: ^[dynamic]AtlasEntr 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)) + paths := make([]string, len(files), alloc) defer delete(paths) for f, fi in files { paths[fi] = f.fullpath } - unmarshall_aseprite_files(paths[:], atlas_entries) + unmarshall_aseprite_files(paths[:], atlas_entries, alloc) } -unmarshall_aseprite_files :: proc(file_paths: []string, atlas_entries: ^[dynamic]AtlasEntry) { +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" {continue} + if extension != ".aseprite" do continue fmt.println("Unmarshalling file: ", file) - ase.unmarshal_from_filename(file, &aseprite_document) + ase.unmarshal_from_filename(file, &aseprite_document, alloc) atlas_entry := atlas_entry_from_compressed_cells(aseprite_document) atlas_entry.path = file @@ -81,13 +92,13 @@ unmarshall_aseprite_files :: proc(file_paths: []string, atlas_entries: ^[dynamic 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 + // 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 { @@ -119,9 +130,9 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr } } - slice.sort_by(atlas_entry.cells[:], proc(i, j: CellData) -> bool { - return i.layer_index < j.layer_index - }) + slice.sort_by(atlas_entry.cells[:], proc(i, j: CellData) -> bool { + return i.layer_index < j.layer_index + }) return } @@ -241,6 +252,73 @@ pack_atlas_entries :: proc( return metadata } +SourceCodeGeneratorMetadata :: struct { + file_defines: struct { + top: string, + bottom: string, + }, + 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, + }, +} + +odin_source_generator_metadata := SourceCodeGeneratorMetadata { + file_defines = {top = "package atlas_bindings\n\n", bottom = ""}, + 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 = "[]AtlasRect", + begin_line = "%v := %v {{\n", + entry_line = "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n", + end_line = "}\n\n", + }, +} + + +// cpp_source_generator_metadata := SourceCodeGeneratorMetadata { +// file_defines = {top = "package atlas_bindings\n\n", bottom = ""}, +// 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 = "[]AtlasRect", +// begin_line = "%v := %v {{\n", +// entry_line = "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\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. @@ -289,3 +367,73 @@ generate_odin_enums_and_atlas_offsets_file_sb :: proc( return sb } + +metadata_source_code_generate :: proc( + metadata: []SpriteAtlasMetadata, + code_generation_metadata: Maybe(SourceCodeGeneratorMetadata), + alloc := context.allocator, +) -> strings.Builder { + codegen, ok := code_generation_metadata.(SourceCodeGeneratorMetadata) + + if !ok { + return generate_odin_enums_and_atlas_offsets_file_sb(metadata, alloc) + } + + 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, "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( + 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, "}\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)) + + return sb + +} diff --git a/src/globals.odin b/src/globals.odin index 42a8b54..7808893 100644 --- a/src/globals.odin +++ b/src/globals.odin @@ -59,4 +59,5 @@ GameMemory :: struct { should_render_atlas: bool, atlas_render_has_preview: bool, atlas_render_size: i32, + atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata), } diff --git a/src/save_output.odin b/src/save_output.odin index 68363fe..e6498d5 100644 --- a/src/save_output.odin +++ b/src/save_output.odin @@ -1,28 +1,57 @@ package game -import "core:strings" import "core:fmt" +import "core:strings" +import "core:os" +import "core:encoding/json" import rl "vendor:raylib" + when ODIN_OS == .Windows { - atlas_path :: "\\atlas.png" + os_file_separator :: "\\" } else { - atlas_path :: "/atlas.png" + os_file_separator :: "/" } save_output :: proc() { if output_path, ok := g_mem.output_folder_path.(string); ok { - if len(output_path) == 0 { - fmt.println("Output path is empty!") - return - } + if len(output_path) == 0 { + fmt.println("Output path is empty!") + return + } image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture) rl.ImageFlipVertical(&image) - output_path := strings.concatenate({output_path, atlas_path}) + + output_path := strings.concatenate({output_path, os_file_separator, "atlas.png"}) cstring_output_path := strings.clone_to_cstring(output_path) + rl.ExportImage(image, cstring_output_path) + + if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + if json_metadata, jok := json.marshal(metadata); jok == nil { + os.write_entire_file( + strings.concatenate({output_path, os_file_separator, "metadata.json"}), + json_metadata, + ) + } else { + fmt.println("Failed to marshall the atlas metadata to a json!") + } + + // TODO(stefan): Think of a more generic alternative to just straight output to a odin file + // maybe supply a config.json that defines the start, end, line by line entry and enum format strings + // this way you can essentially support any language + sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:]) + odin_metadata := strings.to_string(sb) + os.write_entire_file( + strings.concatenate({output_path, os_file_separator, "metadata.odin"}), + transmute([]byte)odin_metadata, + ) + } else { + fmt.println("No metadata to export!") + } + } else { - fmt.println("Output path is empty!") - } + fmt.println("Output path is empty!") + } } From 043892cdd168c365dcb77ea0e05008666a6e0fa1 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Wed, 24 Apr 2024 22:52:24 +0300 Subject: [PATCH 20/31] added metagen check for first class enum arrays --- src/generator.odin | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/generator.odin b/src/generator.odin index 4751d7b..8841258 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -253,21 +253,24 @@ pack_atlas_entries :: proc( } SourceCodeGeneratorMetadata :: struct { - file_defines: struct { + file_defines: struct { top: string, bottom: string, }, - custom_data_type: struct { + 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 { + enum_data: struct { name: string, begin_line: string, // contains one params: enum_data.name entry_line: string, end_line: string, }, - array_data: struct { + array_data: struct { name: string, type: string, begin_line: string, // array begin line contains 2 params in the listed order: array.name, array.type @@ -295,6 +298,7 @@ odin_source_generator_metadata := SourceCodeGeneratorMetadata { entry_line = "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n", end_line = "}\n\n", }, + lanugage_settings = {first_class_enum_arrays = true}, } @@ -415,14 +419,24 @@ metadata_source_code_generate :: proc( { 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, - ) + if codegen.lanugage_settings.first_class_enum_arrays { + 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, + ) + } else { + entry = fmt.aprintf( + codegen.array_data.entry_line, // "\t{{ x = %v, y = %v, w = %v, h = %v }},\n", + cell.location.x, + cell.location.y, + cell.size.x, + cell.size.y, + ) + } strings.write_string(&sb, entry) } } From 44e88f7fd0e1e2eabdd512a22e07411f4900111f Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Wed, 24 Apr 2024 23:24:59 +0300 Subject: [PATCH 21/31] working cpp codegen --- .../aseprite_odin_generator.odin | 3 + src/generator.odin | 66 ++++++++----------- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin index a72f5ea..00549e7 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin @@ -46,6 +46,9 @@ main :: proc() { rl.ExportImage(atlas, EXPORT_PATH) + // something : string = "hello" + // fmt.printf("{1} {2} else", something, 10) + // TestStruct :: struct { // something: struct { // name: string, diff --git a/src/generator.odin b/src/generator.odin index 8841258..a8db468 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -302,26 +302,26 @@ odin_source_generator_metadata := SourceCodeGeneratorMetadata { } -// cpp_source_generator_metadata := SourceCodeGeneratorMetadata { -// file_defines = {top = "package atlas_bindings\n\n", bottom = ""}, -// 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 = "[]AtlasRect", -// begin_line = "%v := %v {{\n", -// entry_line = "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n", -// end_line = "}\n\n", -// }, -// } +cpp_source_generator_metadata := SourceCodeGeneratorMetadata { + file_defines = {top = "#include \n\n", bottom = ""}, + 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", @@ -419,24 +419,14 @@ metadata_source_code_generate :: proc( { entry: string for cell in metadata { - if codegen.lanugage_settings.first_class_enum_arrays { - 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, - ) - } else { - entry = fmt.aprintf( - codegen.array_data.entry_line, // "\t{{ x = %v, y = %v, w = %v, h = %v }},\n", - cell.location.x, - cell.location.y, - cell.size.x, - cell.size.y, - ) - } + 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) } } From 0ef07b299c64e72aca3f0d2fcab8a21cb625793b Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Fri, 26 Apr 2024 23:31:21 +0300 Subject: [PATCH 22/31] some preliminary work done on an argument parser & help menu/docs --- .vscode/settings.json | 13 ++- .vscode/tasks.json | 2 +- .../aseprite_odin_generator.odin | 30 +++--- src/utils/cli.odin | 96 +++++++++++++++++++ src/utils/helpers.odin | 9 +- 5 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 src/utils/cli.odin diff --git a/.vscode/settings.json b/.vscode/settings.json index e820f0f..6c4f812 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,10 @@ { - "workbench.colorCustomizations": { - "activityBar.background": "#322C2D", - "titleBar.activeBackground": "#463E3F", - "titleBar.activeForeground": "#FAFAFA" - } + "workbench.colorCustomizations": { + "activityBar.background": "#322C2D", + "titleBar.activeBackground": "#463E3F", + "titleBar.activeForeground": "#FAFAFA" + }, + "[odin]": { + "editor.formatOnSave": true + } } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a419bf5..cc072d6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -77,7 +77,7 @@ "label": "Build&Run Atlas Generator Test", "type": "shell", "windows": { - "command": "${workspaceFolder}/scripts/build_generator_debug.bat && build_generator\\aseprite_odin_generator.exe", + "command": "${workspaceFolder}/scripts/build_generator_debug.bat && build_generator\\aseprite_odin_generator.exe -input-files:value_of_custom_arg -h", }, "options": { "cwd": "${workspaceFolder}" diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin index 00549e7..4e8b976 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin @@ -14,13 +14,25 @@ import rl "vendor:raylib" import stbrp "vendor:stb/rect_pack" import gen ".." +import utils "../utils" ATLAS_SIZE :: 512 IMPORT_PATH :: "./src/aseprite_odin_generator/big.aseprite" EXPORT_PATH :: "./src/aseprite_odin_generator/atlas.png" main :: proc() { - fmt.println("Hello!") + 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 + } + // if help, ok: args[utils.CLIFlagType.Help]; ok { + // fmt.println("Help called!") + // } + ase_file, ase_ok := os.read_entire_file(IMPORT_PATH) if !ase_ok { fmt.panicf("Couldn't load file!") @@ -45,20 +57,4 @@ main :: proc() { ) rl.ExportImage(atlas, EXPORT_PATH) - - // something : string = "hello" - // fmt.printf("{1} {2} else", something, 10) - - // TestStruct :: struct { - // something: struct { - // name: string, - // age: int, - // }, - // } - // ts: TestStruct - // ts.something.name = "name" - - // jb, err := json.marshal(ts) - // sjb := transmute(string)jb - // fmt.println(sjb) } diff --git a/src/utils/cli.odin b/src/utils/cli.odin new file mode 100644 index 0000000..23ed179 --- /dev/null +++ b/src/utils/cli.odin @@ -0,0 +1,96 @@ +package utils + +import "core:fmt" +import s "core:strings" + +CLIFlagType :: enum { + Unknown, + InputFiles, + InputFolder, + OutputFolder, + ConfigPath, + EnableMetadataOutput, + MetadataJSONOutputPath, + SourceCodeOutputPathOutputPath, + Help, +} + +CLI_FLAG_STRINGS := [CLIFlagType][]string { + .Unknown = {""}, + .Help = {"h", "help"}, + .InputFiles = {"f", "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 = "(real) path the source files for the packer (realpaths only), for multiple files you can provide one string of concateneted paths, separated by a ';'", + .InputFolder = "(real) path to a folder full of source files. This is an alternative to the -i[,input-files] flag", + .OutputFolder = "(real) path to the output folder for all the resulting files to be saved to.", + .EnableMetadataOutput = "On by default. 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", +} + +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 + + fmt.printfln( + "Flag: -%v,%v \t -- %v", + CLI_FLAG_STRINGS[flag][0], + CLI_FLAG_STRINGS[flag][1], + CLI_FLAG_DESCRIPTIONS[flag], + ) + } +} + +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, ":") + name := arg_name_and_value[0] + + if name[0] == '-' { + name = name[1:] + value: string + flag_type := categorize_arg(name) + + if len(arg_name_and_value) > 1 { + value = arg_name_and_value[1] + } + + map_insert(&cliargs, flag_type, CLIFlag{name, value, flag_type}) + } + } + + return +} diff --git a/src/utils/helpers.odin b/src/utils/helpers.odin index 6f07e8b..07a5a86 100644 --- a/src/utils/helpers.odin +++ b/src/utils/helpers.odin @@ -5,6 +5,13 @@ 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 @@ -45,4 +52,4 @@ Vec2 :: [2]f32 vec2_from_vec2i :: proc(p: Vec2i) -> Vec2 { return {f32(p.x), f32(p.y)} -} \ No newline at end of file +} From 82ee56ef031ced58853833ff7687941f2101f285 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Mon, 29 Apr 2024 12:44:30 +0300 Subject: [PATCH 23/31] cli work, partial flags implementation --- .../aseprite_odin_generator.odin | 3 - src/generator.odin | 75 +++++++++++++++++++ src/save_output.odin | 57 -------------- src/utils/cli.odin | 22 +++++- 4 files changed, 96 insertions(+), 61 deletions(-) delete mode 100644 src/save_output.odin diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin index 4e8b976..563452d 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin @@ -29,9 +29,6 @@ main :: proc() { utils.print_help() return } - // if help, ok: args[utils.CLIFlagType.Help]; ok { - // fmt.println("Help called!") - // } ase_file, ase_ok := os.read_entire_file(IMPORT_PATH) if !ase_ok { diff --git a/src/generator.odin b/src/generator.odin index a8db468..d0f01d8 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -1,6 +1,7 @@ package game import ase "./aseprite" +import "core:encoding/json" import "core:fmt" import "core:os" import fp "core:path/filepath" @@ -9,6 +10,15 @@ import "core:strings" import rl "vendor:raylib" import stbrp "vendor:stb/rect_pack" +import utils "./utils" + +when ODIN_OS == .Windows { + os_file_separator :: "\\" +} else { + os_file_separator :: "/" +} + + CellData :: struct { layer_index: u16, opacity: u8, @@ -441,3 +451,68 @@ metadata_source_code_generate :: proc( return sb } + +save_metadata :: proc( + settings: utils.CLIPackerSettings, + atlas_entries: []AtlasEntry, + atlas_metadata: []SpriteAtlasMetadata, +) { + metadata, ok := settings.metadata.(utils.CLIMetadataSettings);if !ok do return + + if json_path, ok := metadata.json_path.(string); ok { + json_bytes, jerr := json.marshal(atlas_metadata) + if jerr == nil { + os.write_entire_file(json_path, json_bytes) + } else { + fmt.println("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) + } +} + +save_output :: proc() { + output_path, ok := g_mem.output_folder_path.(string) + if !ok { + fmt.println("Output path is empty!") + return + } else if output_path == "" { + fmt.println("Output path is empty!") + return + } + + image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture) + rl.ImageFlipVertical(&image) + + output_path = strings.concatenate({output_path, os_file_separator, "atlas.png"}) + cstring_output_path := strings.clone_to_cstring(output_path) + + rl.ExportImage(image, cstring_output_path) + + if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + if json_metadata, jok := json.marshal(metadata); jok == nil { + os.write_entire_file( + strings.concatenate({output_path, os_file_separator, "metadata.json"}), + json_metadata, + ) + } else { + fmt.println("Failed to marshall the atlas metadata to a json!") + } + + // TODO(stefan): Think of a more generic alternative to just straight output to a odin file + // maybe supply a config.json that defines the start, end, line by line entry and enum format strings + // this way you can essentially support any language + sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:]) + odin_metadata := strings.to_string(sb) + os.write_entire_file( + strings.concatenate({output_path, os_file_separator, "metadata.odin"}), + transmute([]byte)odin_metadata, + ) + } else { + fmt.println("No metadata to export!") + } + +} diff --git a/src/save_output.odin b/src/save_output.odin deleted file mode 100644 index e6498d5..0000000 --- a/src/save_output.odin +++ /dev/null @@ -1,57 +0,0 @@ -package game - -import "core:fmt" -import "core:strings" -import "core:os" -import "core:encoding/json" -import rl "vendor:raylib" - - -when ODIN_OS == .Windows { - os_file_separator :: "\\" -} else { - os_file_separator :: "/" -} - -save_output :: proc() { - if output_path, ok := g_mem.output_folder_path.(string); ok { - if len(output_path) == 0 { - fmt.println("Output path is empty!") - return - } - - image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture) - rl.ImageFlipVertical(&image) - - output_path := strings.concatenate({output_path, os_file_separator, "atlas.png"}) - cstring_output_path := strings.clone_to_cstring(output_path) - - rl.ExportImage(image, cstring_output_path) - - if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { - if json_metadata, jok := json.marshal(metadata); jok == nil { - os.write_entire_file( - strings.concatenate({output_path, os_file_separator, "metadata.json"}), - json_metadata, - ) - } else { - fmt.println("Failed to marshall the atlas metadata to a json!") - } - - // TODO(stefan): Think of a more generic alternative to just straight output to a odin file - // maybe supply a config.json that defines the start, end, line by line entry and enum format strings - // this way you can essentially support any language - sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:]) - odin_metadata := strings.to_string(sb) - os.write_entire_file( - strings.concatenate({output_path, os_file_separator, "metadata.odin"}), - transmute([]byte)odin_metadata, - ) - } else { - fmt.println("No metadata to export!") - } - - } else { - fmt.println("Output path is empty!") - } -} diff --git a/src/utils/cli.odin b/src/utils/cli.odin index 23ed179..d6aa08a 100644 --- a/src/utils/cli.odin +++ b/src/utils/cli.odin @@ -33,12 +33,32 @@ CLI_FLAG_DESCRIPTIONS := [CLIFlagType]string { .InputFiles = "(real) path the source files for the packer (realpaths only), for multiple files you can provide one string of concateneted paths, separated by a ';'", .InputFolder = "(real) path to a folder full of source files. This is an alternative to the -i[,input-files] flag", .OutputFolder = "(real) path to the output folder for all the resulting files to be saved to.", - .EnableMetadataOutput = "On by default. Whether or not to export metadata (JSON or source files with the offsets for the packer sprites in the atlas)", + .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", } +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, From 178b0d55258d136e561145c7ec2f5a9284fe78ef Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Fri, 30 Aug 2024 12:32:57 +0300 Subject: [PATCH 24/31] feat: Added methods for writing out the metadata about the atlas elements misc: Refactoring --- .vscode/launch.json | 2 +- .vscode/tasks.json | 167 ++++++++++++++++++------------------- src/game.odin | 21 +++-- src/generator.odin | 197 ++++++++++++++++++++++++++++++-------------- src/utils/cli.odin | 1 + 5 files changed, 233 insertions(+), 155 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 110830b..af68a68 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "request": "launch", "preLaunchTask": "Build Debug", "name": "Debug", - "program": "${workspaceFolder}/game_debug.exe", + "program": "${workspaceFolder}/build/game_debug.exe", "args": [], "cwd": "${workspaceFolder}" }, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index cc072d6..784b8a9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,97 +3,90 @@ "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": false - }, + { + "label": "Build Debug", + "type": "shell", + "windows": { + "command": "${workspaceFolder}/scripts/build_debug.bat" }, - { - "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" + "linux": { + "command": "${workspaceFolder}/scripts/build_debug.sh" }, - { - "label": "Clean build folder(s)", - "type": "shell", - "windows": { - "command": "cd ${workspaceFolder}\\build && rm game*; cd ${workspaceFolder} && rm aseprite_odin_generator*", - }, - // "linux": { - // "command": "${workspaceFolder}/scripts/build_release.sh", - // }, - // "osx": { - // "command": "${workspaceFolder}/scripts/build_release.sh", - // }, - "group": "build" + "osx": { + "command": "${workspaceFolder}/scripts/build_debug.sh" }, - { - "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 - }, + "group": { + "kind": "build", + "isDefault": true }, - { - "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": true - }, + "problemMatcher": [] + }, + { + "label": "Build Release", + "type": "shell", + "windows": { + "command": "${workspaceFolder}/scripts/build_release.bat" + }, + "linux": { + "command": "${workspaceFolder}/scripts/build_release.sh" + }, + "osx": { + "command": "${workspaceFolder}/scripts/build_release.sh" + }, + "group": "build" + }, + { + "label": "Clean build folder(s)", + "type": "shell", + "windows": { + "command": "cd ${workspaceFolder}\\build && rm game*; cd ${workspaceFolder} && rm aseprite_odin_generator*" + }, + "group": "build" + }, + { + "label": "Build Hot Reload", + "type": "shell", + "windows": { + "command": "${workspaceFolder}/scripts/build_hot_reload.bat; start game.exe" + }, + "linux": { + "command": "${workspaceFolder}/scripts/build_hot_reload.sh" + }, + "osx": { + "command": "${workspaceFolder}/scripts/build_hot_reload.sh" + }, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "dedicated", + "showReuseMessage": false, + "clear": true + }, + "group": "build", + "problemMatcher": [] + }, + { + "label": "Build&Run Atlas Generator Test", + "type": "shell", + "windows": { + "command": "${workspaceFolder}/scripts/build_generator_debug.bat && build_generator\\aseprite_odin_generator.exe -input-files:value_of_custom_arg -h" + }, + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "dedicated", + "showReuseMessage": false, + "clear": true + }, + "group": { + "kind": "build", + "isDefault": false } + } ] } \ No newline at end of file diff --git a/src/game.odin b/src/game.odin index 237bfac..b289b82 100644 --- a/src/game.odin +++ b/src/game.odin @@ -14,6 +14,7 @@ package game import "core:fmt" import "core:math" import "core:strings" +import "utils" import rl "vendor:raylib" import diag "dialog" @@ -261,8 +262,8 @@ draw_atlas_settings_and_preview :: proc() { // rl.GuiLine({y = elements_height, width = left_half_rect.width}, "Actions") // elements_height += small_offset - actions_label_y := elements_height { + actions_label_y := elements_height defer rl.GuiGroupBox( { x = small_offset / 2, @@ -286,6 +287,7 @@ draw_atlas_settings_and_preview :: proc() { g_mem.should_open_file_dialog = true g_mem.source_location_type = .SourceFiles } + if rl.GuiButton( { x = left_half_rect.width / 2, @@ -312,6 +314,7 @@ draw_atlas_settings_and_preview :: proc() { ) { g_mem.should_render_atlas = true } + if rl.GuiButton( { x = left_half_rect.width / 2, @@ -336,6 +339,7 @@ draw_atlas_settings_and_preview :: proc() { ) { save_output() } + if rl.GuiButton( { x = left_half_rect.width / 2, @@ -345,6 +349,9 @@ draw_atlas_settings_and_preview :: proc() { }, "Save To...", ) { + if output_folder, ok := g_mem.output_folder_path.(string); ok { + save_metadata_simple(output_folder) + } } elements_height += small_offset * 2 } @@ -467,7 +474,9 @@ open_file_dialog :: proc() { 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_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( @@ -491,9 +500,9 @@ open_file_dialog :: proc() { } clear_atlas_data :: proc() { - if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { - delete(metadata) - // g_mem.atlas_metadata = nil - } + if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + delete(metadata) + // g_mem.atlas_metadata = nil + } g_mem.atlas_render_has_preview = false } diff --git a/src/generator.odin b/src/generator.odin index d0f01d8..571cca2 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -102,7 +102,7 @@ unmarshall_aseprite_files :: proc( 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 + // 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. @@ -250,7 +250,7 @@ pack_atlas_entries :: proc( } cell_metadata := SpriteAtlasMetadata { name = cell_name, - location = { + location = { auto_cast rect.x + auto_cast offset_x, auto_cast rect.y + auto_cast offset_y, }, @@ -264,8 +264,10 @@ pack_atlas_entries :: proc( SourceCodeGeneratorMetadata :: struct { file_defines: struct { - top: string, - bottom: string, + 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} @@ -290,18 +292,23 @@ SourceCodeGeneratorMetadata :: struct { } odin_source_generator_metadata := SourceCodeGeneratorMetadata { - file_defines = {top = "package atlas_bindings\n\n", bottom = ""}, - custom_data_type = { + 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 = { + enum_data = { name = "AtlasEnum", begin_line = "%v :: enum {{\n", entry_line = "\t%s,\n", end_line = "}\n\n", }, - array_data = { + array_data = { name = "ATLAS_SPRITES", type = "[]AtlasRect", begin_line = "%v := %v {{\n", @@ -313,18 +320,23 @@ odin_source_generator_metadata := SourceCodeGeneratorMetadata { cpp_source_generator_metadata := SourceCodeGeneratorMetadata { - file_defines = {top = "#include \n\n", bottom = ""}, - custom_data_type = { + file_defines = { + top = "#include \n\n", + bottom = "", + file_name = "metadata", + file_extension = ".hpp", + }, + custom_data_type = { name = "AtlasRect", type_declaration = "struct %v {{\n\tint x;\n\tint y;\n\tint w;\n\tint h;\n}};\n\n", }, - enum_data = { + enum_data = { name = "AtlasEnum", begin_line = "enum %v {{\n", entry_line = "\t%s,\n", end_line = "\n\tCOUNT\n}\n\n", }, - array_data = { + array_data = { name = "ATLAS_SPRITES", type = "AtlasRect[size_t(AtlasEnum::COUNT)-1]", begin_line = "{1} {0} = {{\n", @@ -384,14 +396,9 @@ generate_odin_enums_and_atlas_offsets_file_sb :: proc( metadata_source_code_generate :: proc( metadata: []SpriteAtlasMetadata, - code_generation_metadata: Maybe(SourceCodeGeneratorMetadata), + codegen: SourceCodeGeneratorMetadata, alloc := context.allocator, ) -> strings.Builder { - codegen, ok := code_generation_metadata.(SourceCodeGeneratorMetadata) - - if !ok { - return generate_odin_enums_and_atlas_offsets_file_sb(metadata, alloc) - } sb := strings.builder_make(alloc) // strings.write_string(&sb, "package atlas_bindings\n\n") @@ -452,6 +459,117 @@ metadata_source_code_generate :: proc( } +save_output :: proc() { + output_path, ok := g_mem.output_folder_path.(string) + if !ok || output_path == "" { + fmt.println("Output path is empty!") + return + } + + image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture) + rl.ImageFlipVertical(&image) + + cstring_atlas_output_path := strings.clone_to_cstring( + strings.concatenate({output_path, os_file_separator, "atlas.png"}), + ) + + rl.ExportImage(image, cstring_atlas_output_path) + + if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + fmt.println("Building metadata...") + if json_metadata, jok := json.marshal(metadata); jok == nil { + os.write_entire_file( + strings.concatenate({output_path, os_file_separator, "metadata.json"}), + json_metadata, + ) + } else { + fmt.println("Failed to marshall the atlas metadata to a json!") + } + + // TODO(stefan): Think of a more generic alternative to just straight output to a odin file + // maybe supply a config.json that defines the start, end, line by line entry and enum format strings + // this way you can essentially support any language + sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:]) + odin_metadata := strings.to_string(sb) + ok := os.write_entire_file( + strings.concatenate({output_path, os_file_separator, "metadata.odin"}), + transmute([]byte)odin_metadata, + ) + if !ok { + fmt.println("Failed to save 'metadata.odin'") + } + } else { + fmt.println("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), +) { + 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!") + return + } + + metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata);if !ok { + fmt.println("No metadata to export!") + } + + fmt.println("Building metadata...") + if json_file_name_ok { + if json_metadata, jok := json.marshal(metadata); jok == nil { + json_output_path := strings.concatenate( + {output_path, os_file_separator, json_file_base_name}, + ) + if ok = os.write_entire_file(json_output_path, json_metadata); !ok { + fmt.println("Failed to write json to file: ", json_output_path) + } + } else { + fmt.println("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) + + 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 { + fmt.println("Failed to save source code to file:", source_output_path) + } + } else { + sb := metadata_source_code_generate(metadata[:], odin_source_generator_metadata) + 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 { + fmt.println("Failed to save source code to file:", source_output_path) + } + } + } +} + save_metadata :: proc( settings: utils.CLIPackerSettings, atlas_entries: []AtlasEntry, @@ -473,46 +591,3 @@ save_metadata :: proc( os.write_entire_file(source_code_path, transmute([]byte)source_code_output_str) } } - -save_output :: proc() { - output_path, ok := g_mem.output_folder_path.(string) - if !ok { - fmt.println("Output path is empty!") - return - } else if output_path == "" { - fmt.println("Output path is empty!") - return - } - - image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture) - rl.ImageFlipVertical(&image) - - output_path = strings.concatenate({output_path, os_file_separator, "atlas.png"}) - cstring_output_path := strings.clone_to_cstring(output_path) - - rl.ExportImage(image, cstring_output_path) - - if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { - if json_metadata, jok := json.marshal(metadata); jok == nil { - os.write_entire_file( - strings.concatenate({output_path, os_file_separator, "metadata.json"}), - json_metadata, - ) - } else { - fmt.println("Failed to marshall the atlas metadata to a json!") - } - - // TODO(stefan): Think of a more generic alternative to just straight output to a odin file - // maybe supply a config.json that defines the start, end, line by line entry and enum format strings - // this way you can essentially support any language - sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:]) - odin_metadata := strings.to_string(sb) - os.write_entire_file( - strings.concatenate({output_path, os_file_separator, "metadata.odin"}), - transmute([]byte)odin_metadata, - ) - } else { - fmt.println("No metadata to export!") - } - -} diff --git a/src/utils/cli.odin b/src/utils/cli.odin index d6aa08a..8b9411f 100644 --- a/src/utils/cli.odin +++ b/src/utils/cli.odin @@ -97,6 +97,7 @@ parse_arguments :: proc(args: []string) -> (cliargs: 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] == '-' { From 84db74586b4b24837d8754c2448033456c6120fc Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Fri, 30 Aug 2024 12:33:40 +0300 Subject: [PATCH 25/31] misc: settings update --- .vscode/settings.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c4f812..f7964b5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,6 @@ { - "workbench.colorCustomizations": { - "activityBar.background": "#322C2D", - "titleBar.activeBackground": "#463E3F", - "titleBar.activeForeground": "#FAFAFA" - }, "[odin]": { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "editor.tabSize": 4 } } \ No newline at end of file From 3f1c523ad97d30c2f79e54cbe71e1ff22779a8b9 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Fri, 30 Aug 2024 18:05:56 +0300 Subject: [PATCH 26/31] 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. --- .gitmodules | 6 - README.md | 8 +- .../aseprite_odin_generator.odin | 32 +- .../big.aseprite => examples/sample.aseprite | Bin .../repo_assets}/image.png | Bin {styles => resources/styles}/style_candy.rgs | Bin scripts/build_atlas.bat | 2 + scripts/build_atlas_release.bat | 2 + scripts/build_cli.bat | 2 + scripts/build_cli_release.bat | 2 + scripts/build_debug.bat | 2 - scripts/build_debug.sh | 3 - scripts/build_generator_debug.bat | 2 - scripts/build_hot_reload.bat | 21 - scripts/build_hot_reload.sh | 47 -- scripts/build_release.bat | 2 - scripts/build_release.sh | 3 - scripts/setup.bat | 14 + src/aseprite_odin_generator/atlas.png | Bin 11027 -> 0 bytes src/aseprite_odin_generator/metadata.json | 1 - src/{ => frontend}/globals.odin | 13 +- src/{game.odin => frontend/main.odin} | 432 ++++++++++-------- src/{utils => generator}/cli.odin | 28 +- src/{ => generator}/generator.odin | 188 ++++---- src/main_hot_reload/main_hot_reload.odin | 196 -------- src/main_release/main_release.odin | 77 ---- src/symbol_exports.odin | 88 ---- src/utils/animation.odin | 53 --- src/utils/handle_array.odin | 141 ------ src/utils/helpers.odin | 55 --- {src => vendors}/aseprite | 0 {src => vendors}/dialog/build.bat | 0 {src => vendors}/dialog/build.sh | 0 {src => vendors}/dialog/libtinyfiledialogs | 0 {src => vendors}/dialog/tinyfiledialog.odin | 0 35 files changed, 397 insertions(+), 1023 deletions(-) delete mode 100644 .gitmodules rename {src/aseprite_odin_generator => examples}/aseprite_odin_generator.odin (51%) rename src/aseprite_odin_generator/big.aseprite => examples/sample.aseprite (100%) rename {repo_assets => resources/repo_assets}/image.png (100%) rename {styles => resources/styles}/style_candy.rgs (100%) create mode 100644 scripts/build_atlas.bat create mode 100644 scripts/build_atlas_release.bat create mode 100644 scripts/build_cli.bat create mode 100644 scripts/build_cli_release.bat delete mode 100644 scripts/build_debug.bat delete mode 100755 scripts/build_debug.sh delete mode 100644 scripts/build_generator_debug.bat delete mode 100644 scripts/build_hot_reload.bat delete mode 100755 scripts/build_hot_reload.sh delete mode 100644 scripts/build_release.bat delete mode 100644 scripts/build_release.sh create mode 100644 scripts/setup.bat delete mode 100644 src/aseprite_odin_generator/atlas.png delete mode 100644 src/aseprite_odin_generator/metadata.json rename src/{ => frontend}/globals.odin (87%) rename src/{game.odin => frontend/main.odin} (51%) rename src/{utils => generator}/cli.odin (66%) rename src/{ => generator}/generator.odin (82%) delete mode 100644 src/main_hot_reload/main_hot_reload.odin delete mode 100644 src/main_release/main_release.odin delete mode 100644 src/symbol_exports.odin delete mode 100644 src/utils/animation.odin delete mode 100644 src/utils/handle_array.odin delete mode 100644 src/utils/helpers.odin rename {src => vendors}/aseprite (100%) rename {src => vendors}/dialog/build.bat (100%) rename {src => vendors}/dialog/build.sh (100%) mode change 100755 => 100644 rename {src => vendors}/dialog/libtinyfiledialogs (100%) rename {src => vendors}/dialog/tinyfiledialog.odin (100%) diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e4908a1..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "src/aseprite"] - path = src/aseprite - url = https://github.com/bersK/odin-aseprite.git -[submodule "src/dialog/libtinyfiledialogs"] - path = src/dialog/libtinyfiledialogs - url = https://github.com/native-toolkit/libtinyfiledialogs.git diff --git a/README.md b/README.md index 3a0d31f..5f53772 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,16 @@ Yet-Another-Atlas-Packer by Stefan Stefanov ## Description -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 metadata and potentially embeds in an output source file of your choosing. + +Uses `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. -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). +The goal of the tool is to take in multiple aseprite files and pack them into a single atlas, outputting metadata in the process in the form of +JSON and/or source files for direct use in odin (or other languages through a customization file). I'm using a library for marshalling the aseprite files found [here](https://github.com/blob1807/odin-aseprite) on github. diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/examples/aseprite_odin_generator.odin similarity index 51% rename from src/aseprite_odin_generator/aseprite_odin_generator.odin rename to examples/aseprite_odin_generator.odin index 563452d..d400cc7 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/examples/aseprite_odin_generator.odin @@ -1,42 +1,29 @@ -package generator +package cli -import ase "../aseprite" +import ase "../vendors/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) } diff --git a/src/aseprite_odin_generator/big.aseprite b/examples/sample.aseprite similarity index 100% rename from src/aseprite_odin_generator/big.aseprite rename to examples/sample.aseprite diff --git a/repo_assets/image.png b/resources/repo_assets/image.png similarity index 100% rename from repo_assets/image.png rename to resources/repo_assets/image.png diff --git a/styles/style_candy.rgs b/resources/styles/style_candy.rgs similarity index 100% rename from styles/style_candy.rgs rename to resources/styles/style_candy.rgs diff --git a/scripts/build_atlas.bat b/scripts/build_atlas.bat new file mode 100644 index 0000000..d650386 --- /dev/null +++ b/scripts/build_atlas.bat @@ -0,0 +1,2 @@ +@echo off +odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap-debug.exe -debug \ No newline at end of file diff --git a/scripts/build_atlas_release.bat b/scripts/build_atlas_release.bat new file mode 100644 index 0000000..9fb82e6 --- /dev/null +++ b/scripts/build_atlas_release.bat @@ -0,0 +1,2 @@ +@echo off +odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap.exe -o:speed \ No newline at end of file diff --git a/scripts/build_cli.bat b/scripts/build_cli.bat new file mode 100644 index 0000000..a13a781 --- /dev/null +++ b/scripts/build_cli.bat @@ -0,0 +1,2 @@ +@echo off +odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli-debug.exe -debug \ No newline at end of file diff --git a/scripts/build_cli_release.bat b/scripts/build_cli_release.bat new file mode 100644 index 0000000..5bd9255 --- /dev/null +++ b/scripts/build_cli_release.bat @@ -0,0 +1,2 @@ +@echo off +odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli.exe -o:speed \ No newline at end of file diff --git a/scripts/build_debug.bat b/scripts/build_debug.bat deleted file mode 100644 index d5ab9ff..0000000 --- a/scripts/build_debug.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_debug.exe -debug diff --git a/scripts/build_debug.sh b/scripts/build_debug.sh deleted file mode 100755 index 7df52c7..0000000 --- a/scripts/build_debug.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -odin build src/main_release -out:build/game_debug.bin -no-bounds-check -debug diff --git a/scripts/build_generator_debug.bat b/scripts/build_generator_debug.bat deleted file mode 100644 index 2340f93..0000000 --- a/scripts/build_generator_debug.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -odin build src/aseprite_odin_generator -define:RAYLIB_SHARED=true -out:build_generator/aseprite_odin_generator.exe -debug diff --git a/scripts/build_hot_reload.bat b/scripts/build_hot_reload.bat deleted file mode 100644 index 3890995..0000000 --- a/scripts/build_hot_reload.bat +++ /dev/null @@ -1,21 +0,0 @@ -@echo off - -rem Build game.dll -odin build src -show-timings -use-separate-modules -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game.dll -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug -IF %ERRORLEVEL% NEQ 0 exit /b 1 - -rem If game.exe already running: Then only compile game.dll and exit cleanly -QPROCESS "game.exe">NUL -IF %ERRORLEVEL% EQU 0 exit /b 1 - -rem build game.exe -odin build src/main_hot_reload -use-separate-modules -out:build/game.exe -strict-style -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug -IF %ERRORLEVEL% NEQ 0 exit /b 1 - -rem copy raylib.dll from odin folder to here -if not exist "raylib.dll" ( - echo "Please copy raylib.dll from /vendor/raylib/windows/raylib.dll to the same directory as game.exe" - exit /b 1 -) - -exit /b 0 diff --git a/scripts/build_hot_reload.sh b/scripts/build_hot_reload.sh deleted file mode 100755 index 996a14d..0000000 --- a/scripts/build_hot_reload.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -VET="-strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon" - -# NOTE: this is a recent addition to the Odin compiler, if you don't have this command -# you can change this to the path to the Odin folder that contains vendor, eg: "~/Odin". -ROOT=$(odin root) -if [ ! $? -eq 0 ]; then - echo "Your Odin compiler does not have the 'odin root' command, please update or hardcode it in the script." - exit 1 -fi - -set -eu - -# Figure out the mess that is dynamic libraries. -case $(uname) in -"Darwin") - case $(uname -m) in - "arm64") LIB_PATH="macos-arm64" ;; - *) LIB_PATH="macos" ;; - esac - - DLL_EXT=".dylib" - EXTRA_LINKER_FLAGS="-Wl,-rpath $ROOT/vendor/raylib/$LIB_PATH" - ;; -*) - DLL_EXT=".so" - EXTRA_LINKER_FLAGS="'-Wl,-rpath=\$ORIGIN/linux'" - - # Copy the linux libraries into the project automatically. - if [ ! -d "linux" ]; then - mkdir linux - cp -r $ROOT/vendor/raylib/linux/libraylib*.so* linux - fi - ;; -esac - -# Build the game. -odin build src -use-separate-modules -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -show-timings -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game_tmp$DLL_EXT -debug $VET - -# Need to use a temp file on Linux because it first writes an empty `game.so`, which the game will load before it is actually fully written. -mv ./build/game_tmp$DLL_EXT ./build/game$DLL_EXT - -# Do not build the game.bin if it is already running. -if ! pgrep game.bin > /dev/null; then - odin build src/main_hot_reload -use-separate-modules -out:build/game.bin $VET -debug -fi diff --git a/scripts/build_release.bat b/scripts/build_release.bat deleted file mode 100644 index 0d1ac5b..0000000 --- a/scripts/build_release.bat +++ /dev/null @@ -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 diff --git a/scripts/build_release.sh b/scripts/build_release.sh deleted file mode 100644 index 257abc0..0000000 --- a/scripts/build_release.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -odin build src/main_release -out:build/game_release.bin -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon diff --git a/scripts/setup.bat b/scripts/setup.bat new file mode 100644 index 0000000..abf078d --- /dev/null +++ b/scripts/setup.bat @@ -0,0 +1,14 @@ +@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 \ No newline at end of file diff --git a/src/aseprite_odin_generator/atlas.png b/src/aseprite_odin_generator/atlas.png deleted file mode 100644 index 94faceeb56095bd4d1e706f0f473f24853e86a14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11027 zcmeI2Yfw{X8pq!gLOftXf`WTUfDkE&T4;o*fMNoH6Y&BRD7Dzth@%^wdyzJryFNl(&Ci^A?g0tWDn=|Lj$(cMk z&+Y#_zc-V~o|GgR%bn{E0AMA~k52`F25)HqWx$t#E$u@9OzTXHk4@jQY$fJO2!veP z`+dn}A{Q@A>g-8swd$V)hnTN|yi@&GCHsiDbiUKy)UU6DsIKQ!>{Ap3ZG&P-oidA>Y* zjc5@*T@AgR6Bxoj4`gqpgPx(CWYZij?Xmzoy03q=PS)0}Ky?~}0%Z~$?2jKwBEwfm zt;7p!$A2s`Kmxe(2_9dTjs-DqCPyb0=gJ>qC9 zJMmTTi?=X=^5zS$85tAcSmjF9*`j_7!~5qO6zlm)53Ji&m>F(Q8(vViypm{lVdv{9 zCqJFj+2+^xR-eIuk9Eb#bLoIf1a9z>*AME?r}AQ+udlD*+mmvGE#bBXM$qx;s`f3~ zm(R%Qs?YBPM)l}4R}{yh?3H~el~*oul`rj`J>d`5LjSi5GiUjK=k60~hD|r;8+uU# zx~Mhq^q{_`pX>L8ANq@3415i$n+5PCSM~c^4()B(k-M80k!JH6H0<@UR34d(+~(ck zhZ(JA&463bvIPSkH*_<&!9;-t_PU3I`5~C#n6hE;&C~~ni=ax%H$+(%Y*-Tq`x5BX zOZX?t#Qj2X)TZAFZ>yqNoY_}s(@>`YbLpDjyub|UYv~5_p}qg=C5)XN(=DX8g>W^& z4}5R;eerpWuhK$D-?lHVBZvUvlUvZ##gY3KZfESfCGL$km^i^r85Zf$63M!Rga@Jv zIam2`h%+CKgvo>>VN#`P2DFXO#8J{V!uY2ArV`HE-+rMs+_w?7Y>q zx9Q4*mwJ!vm9^&BBZSt!aFWG=3w013S6X*X`K-7t&9ARJtJZwQC&ni%FSQg;yp|K=SA6|o#LRGPY8vJewJrAAON40iB+70;!G7joGgx3r zv$!i`sB~!wyMO`Cdbg)kudSZpC(NuUa!3;dcbW%!PYlV8$2|s_=*>qeie< zD)0ExC7<<@eW`f|u}1~bWFv$9QteGMzVZe4qO9bDJdznPy0(zo)dJyU4_e%E5E&gU zJiu~3gl|K2mrH?6HV%(~Q#q1^?G7wJnS%-W#CRZ^_-3r>wp*U5(>|iXukDNPxCBh5 zYtp3h`hNc6euwV%14EL9GPSTe7rE%By?#d&@U=j=kL|;Sv;MB5iU7Lij>4e0FFtT@ zc-e-FR4;%J;4U~15Lb4*Q#wYI9`zJ<0&MXsXXMKQgNW;E6w@VT;=2sSMY6%)K`KYW z6)>?^AFIh*R>j6)UJTn2Zh=9*j&9rU(zm0P1K6r0L|3(EY|VD|ReV5aJPNUqLUGO+ zLc&J=P6qfia005bs&b#->ZKV-uX&@Ys6*+n5SQ{WdD)zwHDKnpr}>ZOkCk zu6I;i^17%ng^$LQd}EW0*|x8s#xwSW|D3~9b!5p-^3E3yH;md4 zcX|C@-r}wir*;oCI6{az=DN(dsy=e$Y=O!ARUkZv=7_o-i#EJ13iP%|9(gcqb%%|; zMLte}Jz*Ip!I7<^5Ag`6h=vpoWba2cv9;!q^XApnuKri8fvr8n&F#){+EM_T&mcvW z3}oiIAzrKGo#e1M^}`zo`x4`VqE;n3<;;b7h9j>$t*vVV z3t6K9cXQCBEbe$BW{A|iv-4xzy#HU^sAB$S�DAEuj{B^gyD9ff@#C7~cC|XsA<- z0vQD|3S<<>C=ya6q)14SkRl;PLW+bG2`Lg%B&0}4k&q%GMM8>%6bUI3QY55E_2D)c^nh diff --git a/src/aseprite_odin_generator/metadata.json b/src/aseprite_odin_generator/metadata.json deleted file mode 100644 index be61ff1..0000000 --- a/src/aseprite_odin_generator/metadata.json +++ /dev/null @@ -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]}] \ No newline at end of file diff --git a/src/globals.odin b/src/frontend/globals.odin similarity index 87% rename from src/globals.odin rename to src/frontend/globals.odin index 7808893..c9aae32 100644 --- a/src/globals.odin +++ b/src/frontend/globals.odin @@ -1,13 +1,12 @@ +package frontend -package game - +import generator "../generator" import rl "vendor:raylib" -PixelWindowHeight :: 180 +PIXEL_WINDOW_HEIGHT :: 180 FILE_DIALOG_SIZE :: 1000 scaling: f32 = 2 -w, h: f32 WindowInformation :: struct { w: f32, @@ -38,7 +37,7 @@ PackerSettings :: struct { output_odin: bool, } -GameMemory :: struct { +ApplicationState :: struct { file_dialog_text_buffer: [FILE_DIALOG_SIZE + 1]u8, is_packing_whole_source_folder: bool, should_open_file_dialog: bool, @@ -59,5 +58,7 @@ GameMemory :: struct { should_render_atlas: bool, atlas_render_has_preview: bool, atlas_render_size: i32, - atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata), + atlas_metadata: Maybe([dynamic]generator.SpriteAtlasMetadata), } + +g_mem: ApplicationState diff --git a/src/game.odin b/src/frontend/main.odin similarity index 51% rename from src/game.odin rename to src/frontend/main.odin index b289b82..5962de0 100644 --- a/src/game.odin +++ b/src/frontend/main.odin @@ -1,35 +1,65 @@ -// 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 +package frontend import "core:fmt" +import "core:log" import "core:math" +import "core:mem" +import "core:mem/virtual" +import "core:os" import "core:strings" -import "utils" import rl "vendor:raylib" -import diag "dialog" +import diag "../../vendors/dialog" +import generator "../generator" -g_mem: ^GameMemory +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) -game_camera :: proc() -> rl.Camera2D { - w = f32(rl.GetScreenWidth()) - h = f32(rl.GetScreenHeight()) + mode: int = 0 + when ODIN_OS == .Linux || ODIN_OS == .Darwin { + mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH + } - return {zoom = h / PixelWindowHeight, target = {}, offset = {w / 2, h / 2}} -} + logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode) -ui_camera :: proc() -> rl.Camera2D { - return {zoom = scaling} + 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 + defer if logh_err == os.ERROR_NONE { + log.destroy_file_logger(logger) + } + + rl.SetConfigFlags({.WINDOW_RESIZABLE}) + rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer") + defer rl.CloseWindow() + rl.SetWindowMinSize(1400, 800) + + for !rl.WindowShouldClose() { + update() + draw() + + 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) + } + + for key, value in tracking_allocator.allocation_map { + log.error("%v: Leaked %v bytes\n", value.location, value.size) + } + + mem.tracking_allocator_destroy(&tracking_allocator) } update :: proc() { @@ -39,11 +69,10 @@ update :: proc() { 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) + mouse_scale := 1 / scaling + rl.SetMouseScale(mouse_scale, mouse_scale) if g_mem.should_open_file_dialog { open_file_dialog() @@ -65,6 +94,10 @@ draw :: proc() { free_all(context.temp_allocator) } +ui_camera :: proc() -> rl.Camera2D { + return {zoom = scaling} +} + draw_screen_ui :: proc() { rl.BeginMode2D(ui_camera()) defer rl.EndMode2D() @@ -72,19 +105,47 @@ draw_screen_ui :: proc() { draw_atlas_settings_and_preview() } +pick_sources :: proc() { + g_mem.should_open_file_dialog = true + g_mem.source_location_type = .SourceFiles +} + +pick_output :: proc() { + g_mem.should_open_file_dialog = true + g_mem.source_location_type = .OutputFolder +} + +pack_atlas :: proc() { + g_mem.should_render_atlas = true +} + +save :: proc() { + generator.save_output( + g_mem.output_folder_path, + g_mem.atlas_metadata, + g_mem.atlas_render_texture_target, + ) +} + +save_to :: proc() { + // if output_folder, ok := g_mem.output_folder_path.(string); ok { + // generator.save_metadata_simple(output_folder, g_mem.atlas_metadata, nil, nil, nil) + // } +} + 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 + atlas_entries: [dynamic]generator.AtlasEntry delete(atlas_entries) if files, ok := g_mem.source_files_to_pack.([]string); ok { - unmarshall_aseprite_files(files, &atlas_entries) + generator.unmarshall_aseprite_files(files, &atlas_entries) } else { - fmt.println("No source folder or files set! Can't pack the void!!!") + log.error("No source folder or files set! Can't pack the void!!!") g_mem.should_render_atlas = false return } @@ -97,12 +158,17 @@ draw_screen_target :: proc() { 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) + g_mem.atlas_metadata = generator.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!") + log.info("Packed everything!") atlas_render_target.texture = rl.LoadTextureFromImage(atlas) g_mem.should_render_atlas = false @@ -113,14 +179,14 @@ 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, + width = cast(f32)g_mem.window_info.width_scaled / 3, + height = cast(f32)g_mem.window_info.height_scaled, } right_half_rect := rl.Rectangle { - x = auto_cast g_mem.window_info.width_scaled / 3, + x = cast(f32)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, + width = cast(f32)(g_mem.window_info.width_scaled / 3) * 2, + height = cast(f32)g_mem.window_info.height_scaled, } rl.DrawRectangleRec(left_half_rect, rl.WHITE) rl.DrawRectangleRec(right_half_rect, rl.MAROON) @@ -149,9 +215,9 @@ draw_atlas_settings_and_preview :: proc() { elements_height += small_offset / 2 @(static) - DropdownBox000EditMode: bool + dropdown_resolution_edit_mode: bool @(static) - DropdownBox000Active: i32 + dropdown_resolution_mode: i32 dropdown_rect := rl.Rectangle { x = small_offset, @@ -159,20 +225,18 @@ draw_atlas_settings_and_preview :: proc() { 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 dropdown_resolution_edit_mode {rl.GuiLock()} if rl.GuiDropdownBox( dropdown_rect, "256x;512x;1024x;2048x;4096x", - &DropdownBox000Active, - DropdownBox000EditMode, + &dropdown_resolution_mode, + dropdown_resolution_edit_mode, ) { - DropdownBox000EditMode = !DropdownBox000EditMode - fmt.println(DropdownBox000Active) - g_mem.atlas_render_size = 256 * auto_cast math.pow(2, f32(DropdownBox000Active)) + dropdown_resolution_edit_mode = !dropdown_resolution_edit_mode + g_mem.atlas_render_size = 256 * auto_cast math.pow(2, f32(dropdown_resolution_mode)) } rl.GuiUnlock() } @@ -183,77 +247,90 @@ draw_atlas_settings_and_preview :: proc() { 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, + 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, - }, - "Padding Settings", - ) + } + 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( - { - x = small_offset, - y = elements_height, - width = small_offset, - height = small_offset, - }, + enable_padding_rect, " 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, + // 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", - ) + } + padding_x_spinner := rl.GuiSpinner( + padding_x_spinner_rect, + "", + &g_mem.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 - 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, + // 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", - ) + } + padding_y_spinner := rl.GuiSpinner( + padding_y_spinner_rect, + "", + &g_mem.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 } @@ -264,94 +341,78 @@ draw_atlas_settings_and_preview :: proc() { { actions_label_y := elements_height - defer rl.GuiGroupBox( - { - x = small_offset / 2, - y = actions_label_y, - width = left_half_rect.width - small_offset, + + 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, - }, - "Actions", - ) + } + rl.GuiGroupBox(actions_rect, "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 + 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)") { + pick_sources() } - 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 + 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") { + pick_output() } 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 + 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") { + pack_atlas() } - 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_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 - if rl.GuiButton( - { - x = small_offset, - y = elements_height, - width = left_half_rect.width / 2 - small_offset, - height = small_offset, - }, - "Save", - ) { - save_output() + 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() } - 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) - } + 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() } elements_height += small_offset * 2 } @@ -437,9 +498,9 @@ open_file_dialog :: proc() { 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) + log.info(g_mem.source_files_to_pack) } else { - fmt.println("No files were selected!") + log.error("No files were selected!") } case .SourceFolder: @@ -451,9 +512,9 @@ open_file_dialog :: proc() { ) if len(file) > 0 { g_mem.source_location_to_pack = strings.clone_from_cstring(file) - fmt.println(g_mem.source_location_to_pack) + log.info(g_mem.source_location_to_pack) } else { - fmt.println("Got an empty path from the file dialog!") + log.error("Got an empty path from the file dialog!") } case .OutputFolder: @@ -465,9 +526,9 @@ open_file_dialog :: proc() { ) if len(file) > 0 { g_mem.output_folder_path = strings.clone_from_cstring(file) - fmt.println(g_mem.output_folder_path) + log.info(g_mem.output_folder_path) } else { - fmt.println("Got an empty path from the file dialog!") + log.error("Got an empty path from the file dialog!") } case .SaveFileAs: @@ -475,7 +536,7 @@ open_file_dialog :: proc() { 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, generator.OS_FILE_SEPARATOR, "atlas.png"}, ) default_path_to_save: cstring = strings.clone_to_cstring(default_path_filename) file_path = cstring( @@ -491,7 +552,11 @@ open_file_dialog :: proc() { file_path = cstring(diag.save_file_dialog("Save as...", "", 1, &patterns[0], "Atlas")) } if file_path != nil { - save_output() + generator.save_output( + g_mem.output_folder_path, + g_mem.atlas_metadata, + g_mem.atlas_render_texture_target, + ) } } @@ -500,9 +565,8 @@ open_file_dialog :: proc() { } clear_atlas_data :: proc() { - if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + if metadata, ok := g_mem.atlas_metadata.([dynamic]generator.SpriteAtlasMetadata); ok { delete(metadata) - // g_mem.atlas_metadata = nil } g_mem.atlas_render_has_preview = false } diff --git a/src/utils/cli.odin b/src/generator/cli.odin similarity index 66% rename from src/utils/cli.odin rename to src/generator/cli.odin index 8b9411f..5a48ebe 100644 --- a/src/utils/cli.odin +++ b/src/generator/cli.odin @@ -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) } } diff --git a/src/generator.odin b/src/generator/generator.odin similarity index 82% rename from src/generator.odin rename to src/generator/generator.odin index 571cca2..5e78041 100644 --- a/src/generator.odin +++ b/src/generator/generator.odin @@ -1,8 +1,9 @@ -package game +package generator -import ase "./aseprite" +import ase "../../vendors/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,6 +38,35 @@ SpriteAtlasMetadata :: struct { 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, @@ -53,7 +80,7 @@ unmarshall_aseprite_dir :: proc( unmarshall_aseprite_files_file_info(fis, atlas_entries, alloc) } } else { - fmt.println("Couldn't open folder: ", path) + log.errorf("Couldn't open folder: ", path) } } @@ -87,7 +114,7 @@ unmarshall_aseprite_files :: proc( extension := fp.ext(file) if extension != ".aseprite" do continue - fmt.println("Unmarshalling file: ", file) + log.infof("Unmarshalling file: ", file) ase.unmarshal_from_filename(file, &aseprite_document, alloc) atlas_entry := atlas_entry_from_compressed_cells(aseprite_document) atlas_entry.path = file @@ -97,11 +124,11 @@ unmarshall_aseprite_files :: proc( } /* - Goes through all the chunks in an aseprite document & copies the `Com_Image_Cel` cells in a separate image + 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)) + 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 @@ -110,13 +137,13 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr // 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)) + log.infof("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) + log.info(cel_chunk.layer_index) cell := CellData { img = rl.Image { @@ -134,7 +161,7 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr } if layer_chunk, ok := chunk.(ase.Layer_Chunk); ok { - fmt.println("Layer chunk: ", layer_chunk) + log.infof("Layer chunk: ", layer_chunk) append(&atlas_entry.layer_names, layer_chunk.name) } } @@ -148,7 +175,7 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr } /* - Takes in a slice of entries, an output texture and offsets (offset_x/y) + Takes in a slice of entries, an output texture and offsets (offset_x/y) */ pack_atlas_entries :: proc( entries: []AtlasEntry, @@ -201,10 +228,10 @@ pack_atlas_entries :: proc( 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[:]) + log.info("Packed everything successfully!") + log.infof("Rects: {0}", rects[:]) } else { - fmt.println("Failed to pack everything!") + log.error("Failed to pack everything!") } for rect, rectIdx in rects { @@ -228,7 +255,7 @@ pack_atlas_entries :: proc( // 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) + log.infof("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect) } metadata := make([dynamic]SpriteAtlasMetadata, allocator) @@ -250,7 +277,7 @@ pack_atlas_entries :: proc( } cell_metadata := SpriteAtlasMetadata { name = cell_name, - location = { + location = { auto_cast rect.x + auto_cast offset_x, auto_cast rect.y + auto_cast offset_y, }, @@ -262,53 +289,24 @@ pack_atlas_entries :: proc( return metadata } -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, - }, -} - odin_source_generator_metadata := SourceCodeGeneratorMetadata { - file_defines = { + file_defines = { top = "package atlas_bindings\n\n", bottom = "", file_name = "metadata", file_extension = ".odin", }, - custom_data_type = { + custom_data_type = { name = "AtlasRect", type_declaration = "%v :: struct {{ x, y, w, h: i32 }}\n\n", }, - enum_data = { + enum_data = { name = "AtlasEnum", begin_line = "%v :: enum {{\n", entry_line = "\t%s,\n", end_line = "}\n\n", }, - array_data = { + array_data = { name = "ATLAS_SPRITES", type = "[]AtlasRect", begin_line = "%v := %v {{\n", @@ -320,23 +318,23 @@ odin_source_generator_metadata := SourceCodeGeneratorMetadata { cpp_source_generator_metadata := SourceCodeGeneratorMetadata { - file_defines = { + file_defines = { top = "#include \n\n", bottom = "", file_name = "metadata", file_extension = ".hpp", }, - custom_data_type = { + 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 = { + enum_data = { name = "AtlasEnum", begin_line = "enum %v {{\n", entry_line = "\t%s,\n", end_line = "\n\tCOUNT\n}\n\n", }, - array_data = { + array_data = { name = "ATLAS_SPRITES", type = "AtlasRect[size_t(AtlasEnum::COUNT)-1]", begin_line = "{1} {0} = {{\n", @@ -346,9 +344,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, @@ -389,7 +387,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 +403,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,42 +441,46 @@ 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) +save_output :: proc( + output_folder_path: Maybe(string), + atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata), + atlas_render_texture_target: rl.RenderTexture2D, +) { + output_path, ok := output_folder_path.(string) if !ok || output_path == "" { - fmt.println("Output path is empty!") + log.error("Output path is empty!") return } - image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture) + image := rl.LoadImageFromTexture(atlas_render_texture_target.texture) rl.ImageFlipVertical(&image) cstring_atlas_output_path := strings.clone_to_cstring( - strings.concatenate({output_path, os_file_separator, "atlas.png"}), + strings.concatenate({output_path, OS_FILE_SEPARATOR, "atlas.png"}), ) rl.ExportImage(image, cstring_atlas_output_path) - if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { - fmt.println("Building metadata...") + if metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + 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"}), 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 @@ -492,14 +489,14 @@ save_output :: proc() { sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:]) 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"}), 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 +506,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!") } } @@ -544,45 +542,45 @@ save_metadata_simple :: proc( 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) 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 { diff --git a/src/main_hot_reload/main_hot_reload.odin b/src/main_hot_reload/main_hot_reload.odin deleted file mode 100644 index 99f4692..0000000 --- a/src/main_hot_reload/main_hot_reload.odin +++ /dev/null @@ -1,196 +0,0 @@ -// Development game exe. Loads game.dll and reloads it whenever it changes. - -package main - -import "core:c/libc" -import "core:dynlib" -import "core:fmt" -import "core:log" -import "core:mem" -import "core:os" - -when ODIN_OS == .Windows { - DLL_EXT :: ".dll" -} else when ODIN_OS == .Darwin { - DLL_EXT :: ".dylib" -} else { - DLL_EXT :: ".so" -} - -copy_dll :: proc(to: string) -> bool { - exit: i32 - when ODIN_OS == .Windows { - exit = libc.system(fmt.ctprintf("copy game.dll {0}", to)) - } else { - exit = libc.system(fmt.ctprintf("cp game" + DLL_EXT + " {0}", to)) - } - - if exit != 0 { - fmt.printfln("Failed to copy game" + DLL_EXT + " to {0}", to) - return false - } - - return true -} - -GameAPI :: struct { - lib: dynlib.Library, - init_window: proc(), - init: proc(), - update: proc() -> bool, - shutdown: proc(), - shutdown_window: proc(), - memory: proc() -> rawptr, - memory_size: proc() -> int, - hot_reloaded: proc(mem: rawptr), - force_reload: proc() -> bool, - force_restart: proc() -> bool, - modification_time: os.File_Time, - api_version: int, -} - -load_game_api :: proc(api_version: int) -> (api: GameAPI, ok: bool) { - mod_time, mod_time_error := os.last_write_time_by_name("game" + DLL_EXT) - if mod_time_error != os.ERROR_NONE { - fmt.printfln( - "Failed getting last write time of game" + DLL_EXT + ", error code: {1}", - mod_time_error, - ) - return - } - - // NOTE: this needs to be a relative path for Linux to work. - game_dll_name := fmt.tprintf( - "{0}game_{1}" + DLL_EXT, - "./" when ODIN_OS != .Windows else "", - api_version, - ) - copy_dll(game_dll_name) or_return - - _, ok = dynlib.initialize_symbols(&api, game_dll_name, "game_", "lib") - if !ok { - fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error()) - } - - api.api_version = api_version - api.modification_time = mod_time - ok = true - - return -} - -unload_game_api :: proc(api: ^GameAPI) { - if api.lib != nil { - if !dynlib.unload_library(api.lib) { - fmt.printfln("Failed unloading lib: {0}", dynlib.last_error()) - } - } - - if os.remove(fmt.tprintf("game_{0}" + DLL_EXT, api.api_version)) != 0 { - fmt.printfln("Failed to remove game_{0}" + DLL_EXT + " copy", api.api_version) - } -} - -main :: proc() { - context.logger = log.create_console_logger() - - default_allocator := context.allocator - tracking_allocator: mem.Tracking_Allocator - mem.tracking_allocator_init(&tracking_allocator, default_allocator) - context.allocator = mem.tracking_allocator(&tracking_allocator) - - reset_tracking_allocator :: proc(a: ^mem.Tracking_Allocator) -> bool { - err := false - - for _, value in a.allocation_map { - fmt.printf("%v: Leaked %v bytes\n", value.location, value.size) - err = true - } - - mem.tracking_allocator_clear(a) - return err - } - - game_api_version := 0 - game_api, game_api_ok := load_game_api(game_api_version) - - if !game_api_ok { - fmt.println("Failed to load Game API") - return - } - - game_api_version += 1 - game_api.init_window() - game_api.init() - - old_game_apis := make([dynamic]GameAPI, default_allocator) - - window_open := true - for window_open { - window_open = game_api.update() - force_reload := game_api.force_reload() - force_restart := game_api.force_restart() - reload := force_reload || force_restart - game_dll_mod, game_dll_mod_err := os.last_write_time_by_name("game" + DLL_EXT) - - if game_dll_mod_err == os.ERROR_NONE && game_api.modification_time != game_dll_mod { - reload = true - } - - if reload { - new_game_api, new_game_api_ok := load_game_api(game_api_version) - - if new_game_api_ok { - if game_api.memory_size() != new_game_api.memory_size() || force_restart { - game_api.shutdown() - reset_tracking_allocator(&tracking_allocator) - - for &g in old_game_apis { - unload_game_api(&g) - } - - clear(&old_game_apis) - unload_game_api(&game_api) - game_api = new_game_api - game_api.init() - } else { - append(&old_game_apis, game_api) - game_memory := game_api.memory() - game_api = new_game_api - game_api.hot_reloaded(game_memory) - } - - game_api_version += 1 - } - } - - for b in tracking_allocator.bad_free_array { - log.error("Bad free at: %v", b.location) - } - - clear(&tracking_allocator.bad_free_array) - free_all(context.temp_allocator) - } - - free_all(context.temp_allocator) - game_api.shutdown() - reset_tracking_allocator(&tracking_allocator) - - for &g in old_game_apis { - unload_game_api(&g) - } - - delete(old_game_apis) - - game_api.shutdown_window() - unload_game_api(&game_api) - mem.tracking_allocator_destroy(&tracking_allocator) -} - -// make game use good GPU on laptops etc - -@(export) -NvOptimusEnablement: u32 = 1 - -@(export) -AmdPowerXpressRequestHighPerformance: i32 = 1 diff --git a/src/main_release/main_release.odin b/src/main_release/main_release.odin deleted file mode 100644 index 1d335d1..0000000 --- a/src/main_release/main_release.odin +++ /dev/null @@ -1,77 +0,0 @@ -// For making a release exe that does not use hot reload. - -package main_release - -import "core:log" -import "core:os" - -import game ".." - -UseTrackingAllocator :: #config(UseTrackingAllocator, false) - -main :: proc() { - when UseTrackingAllocator { - default_allocator := context.allocator - tracking_allocator: Tracking_Allocator - tracking_allocator_init(&tracking_allocator, default_allocator) - context.allocator = allocator_from_tracking_allocator(&tracking_allocator) - } - - mode: int = 0 - when ODIN_OS == .Linux || ODIN_OS == .Darwin { - mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH - } - - logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode) - - if logh_err == os.ERROR_NONE { - os.stdout = logh - os.stderr = logh - } - - logger := - logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger() - context.logger = logger - - game.game_init_window() - game.game_init() - - window_open := true - for window_open { - window_open = game.game_update() - - when UseTrackingAllocator { - for b in tracking_allocator.bad_free_array { - log.error("Bad free at: %v", b.location) - } - - clear(&tracking_allocator.bad_free_array) - } - - free_all(context.temp_allocator) - } - - free_all(context.temp_allocator) - game.game_shutdown() - game.game_shutdown_window() - - if logh_err == os.ERROR_NONE { - log.destroy_file_logger(&logger) - } - - when UseTrackingAllocator { - for key, value in tracking_allocator.allocation_map { - log.error("%v: Leaked %v bytes\n", value.location, value.size) - } - - tracking_allocator_destroy(&tracking_allocator) - } -} - -// make game use good GPU on laptops etc - -@(export) -NvOptimusEnablement: u32 = 1 - -@(export) -AmdPowerXpressRequestHighPerformance: i32 = 1 diff --git a/src/symbol_exports.odin b/src/symbol_exports.odin deleted file mode 100644 index d5ada25..0000000 --- a/src/symbol_exports.odin +++ /dev/null @@ -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) -} diff --git a/src/utils/animation.odin b/src/utils/animation.odin deleted file mode 100644 index 4a55c6e..0000000 --- a/src/utils/animation.odin +++ /dev/null @@ -1,53 +0,0 @@ -// This implements simple animations using sprite sheets. The texture in the -// `Animation` struct is assumed to contain a horizontal strip of the frames -// in the animation. Call `animation_update` to update and then call -// `animation_rect` when you wish to know the source rect to use in the texture -// With the source rect you can run rl.DrawTextureRec to draw the current frame. - -package utils - -import "core:log" - -Animation :: struct { - texture: Texture, - num_frames: int, - current_frame: int, - frame_timer: f32, - frame_length: f32, -} - -animation_create :: proc(tex: Texture, num_frames: int, frame_length: f32) -> Animation { - return( - Animation { - texture = tex, - num_frames = num_frames, - frame_length = frame_length, - frame_timer = frame_length, - } \ - ) -} - -animation_update :: proc(a: ^Animation, dt: f32) { - a.frame_timer -= dt - - if a.frame_timer <= 0 { - a.frame_timer = a.frame_length + a.frame_timer - a.current_frame += 1 - - if a.current_frame >= a.num_frames { - a.current_frame = 0 - } - } -} - -animation_rect :: proc(a: Animation) -> Rect { - if a.num_frames == 0 { - log.error("Animation has zero frames") - return RectEmpty - } - - w := f32(a.texture.width) / f32(a.num_frames) - h := f32(a.texture.height) - - return {x = f32(a.current_frame) * w, y = 0, width = w, height = h} -} diff --git a/src/utils/handle_array.odin b/src/utils/handle_array.odin deleted file mode 100644 index b6b59d1..0000000 --- a/src/utils/handle_array.odin +++ /dev/null @@ -1,141 +0,0 @@ -// This handle-based array gives you a statically allocated array where you can -// use index based handles instead of pointers. The handles have a generation -// that makes sure you don't get bugs when slots are re-used. -// Read more about it here: https://floooh.github.io/2018/06/17/handles-vs-pointers.html */ - -package utils - -Handle :: struct($T: typeid) { - // idx 0 means unused. Note that slot 0 is a dummy slot, it can never be used. - idx: u32, - gen: u32, -} - -HandleArrayItem :: struct($T: typeid) { - item: T, - handle: Handle(T), -} - -// TODO: Add a freelist that uses some kind of bit array... We should be able to -// check 64 item slots at a time that way, but without any dynamic array. -HandleArray :: struct($T: typeid, $N: int) { - items: #soa[N]HandleArrayItem(T), - num_items: u32, -} - -ha_add :: proc(a: ^HandleArray($T, $N), v: T) -> (Handle(T), bool) #optional_ok { - for idx in 1 ..< a.num_items { - i := &a.items[idx] - - if idx != 0 && i.handle.idx == 0 { - i.handle.idx = u32(idx) - i.item = v - return i.handle, true - } - } - - // Index 0 is dummy - if a.num_items == 0 { - a.num_items += 1 - } - - if a.num_items == len(a.items) { - return {}, false - } - - idx := a.num_items - i := &a.items[a.num_items] - a.num_items += 1 - i.handle.idx = idx - i.handle.gen = 1 - i.item = v - return i.handle, true -} - -ha_get :: proc(a: HandleArray($T, $N), h: Handle(T)) -> (T, bool) { - if h.idx == 0 { - return {}, false - } - - if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h { - return a.items[h.idx].item, true - } - - return {}, false -} - -ha_get_ptr :: proc(a: HandleArray($T, $N), h: Handle(T)) -> ^T { - if h.idx == 0 { - return nil - } - - if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h { - return &ha.items[h.idx].item - } - - return nil -} - -ha_remove :: proc(a: ^HandleArray($T, $N), h: Handle(T)) { - if h.idx == 0 { - return - } - - if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h { - a.items[h.idx].handle.idx = 0 - a.items[h.idx].handle.gen += 1 - } -} - -ha_valid :: proc(a: HandleArray($T, $N), h: Handle(T)) -> bool { - if h.idx == 0 { - return false - } - - return int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h -} - -HandleArrayIter :: struct($T: typeid, $N: int) { - a: ^HandleArray(T, N), - index: int, -} - -ha_make_iter :: proc(a: ^HandleArray($T, $N)) -> HandleArrayIter(T, N) { - return HandleArrayIter(T, N){a = a} -} - -ha_iter :: proc(it: ^HandleArrayIter($T, $N)) -> (val: T, h: Handle(T), cond: bool) { - cond = it.index < int(it.a.num_items) - - for ; cond; cond = it.index < int(it.a.num_items) { - if it.a.items[it.index].handle.idx == 0 { - it.index += 1 - continue - } - - val = it.a.items[it.index].item - h = it.a.items[it.index].handle - it.index += 1 - break - } - - return -} - -ha_iter_ptr :: proc(it: ^HandleArrayIter($T, $N)) -> (val: ^T, h: Handle(T), cond: bool) { - cond = it.index < int(it.a.num_items) - - for ; cond; cond = it.index < int(it.a.num_items) { - if it.a.items[it.index].handle.idx == 0 { - it.index += 1 - continue - } - - val = &it.a.items[it.index].item - h = it.a.items[it.index].handle - it.index += 1 - break - } - - return -} diff --git a/src/utils/helpers.odin b/src/utils/helpers.odin deleted file mode 100644 index 07a5a86..0000000 --- a/src/utils/helpers.odin +++ /dev/null @@ -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)} -} diff --git a/src/aseprite b/vendors/aseprite similarity index 100% rename from src/aseprite rename to vendors/aseprite diff --git a/src/dialog/build.bat b/vendors/dialog/build.bat similarity index 100% rename from src/dialog/build.bat rename to vendors/dialog/build.bat diff --git a/src/dialog/build.sh b/vendors/dialog/build.sh old mode 100755 new mode 100644 similarity index 100% rename from src/dialog/build.sh rename to vendors/dialog/build.sh diff --git a/src/dialog/libtinyfiledialogs b/vendors/dialog/libtinyfiledialogs similarity index 100% rename from src/dialog/libtinyfiledialogs rename to vendors/dialog/libtinyfiledialogs diff --git a/src/dialog/tinyfiledialog.odin b/vendors/dialog/tinyfiledialog.odin similarity index 100% rename from src/dialog/tinyfiledialog.odin rename to vendors/dialog/tinyfiledialog.odin From faf42da5223902075945159a05159d16d7cc15b9 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Fri, 2 Jan 2026 15:06:34 +0200 Subject: [PATCH 27/31] Update dependencies, refresh README.md, add build scripts for linux(& macos?) --- .gitmodules | 3 + .vscode/launch.json | 31 --------- .vscode/settings.json | 6 -- .vscode/tasks.json | 92 --------------------------- README.md | 31 +++++---- examples/aseprite_odin_generator.odin | 2 +- scripts/build.sh | 5 ++ scripts/setup.sh | 7 ++ src/generator/generator.odin | 30 ++++----- vendors/aseprite | 1 - vendors/dialog/build.sh | 2 +- vendors/odin-aseprite | 1 + 12 files changed, 53 insertions(+), 158 deletions(-) create mode 100644 .gitmodules delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json delete mode 100644 .vscode/tasks.json create mode 100644 scripts/build.sh create mode 100644 scripts/setup.sh delete mode 160000 vendors/aseprite create mode 160000 vendors/odin-aseprite diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6e5660b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendors/odin-aseprite"] + path = vendors/odin-aseprite + url = git@github.com:blob1807/odin-aseprite.git diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index af68a68..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "cppvsdbg", - "request": "launch", - "preLaunchTask": "Build Debug", - "name": "Debug", - "program": "${workspaceFolder}/build/game_debug.exe", - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "cppvsdbg", - "request": "launch", - "preLaunchTask": "Build Release", - "name": "Release", - "program": "${workspaceFolder}/game_release.exe", - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "cppvsdbg", - "request": "launch", - "name": "Run File", - "program": "odin", - "args": ["run", "${fileBasename}", "-file"], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f7964b5..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "[odin]": { - "editor.formatOnSave": true, - "editor.tabSize": 4 - } -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 784b8a9..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -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 - } - } - ] -} \ No newline at end of file diff --git a/README.md b/README.md index 5f53772..faeb113 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,29 @@ # 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` +* Build the `libtinyfiledialog` library in the `vendors/dialog` fold ## Description +> [!NOTE] +> Quite frankly if you want automatic atlas packing I would suggest using `stb_rect_pack` directly & an aseprite plugin for exporting your assets in a proper format for packing. Using this atlas packer in an automated fashion is not quite possible and slower as a workflow. I would only use it for experiment projects or places where having a built in atlas packer is not practical. -Simple atlas packer for .aseprite files. Generates metadata and potentially embeds in an output source file of your choosing. +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. -Uses `stb_rect_pack` from the `stb` family of header libraries & `raylib` for rendering/UI. Here's a quick preview on [youtube](https://youtu.be/4_dKq7G57Lw) of the application. +Uses `stb_rect_pack` for the image packing & `raylib` for the UI. - - - +A quick preview on [youtube](https://youtu.be/4_dKq7G57Lw). -The goal of the tool is to take in multiple aseprite files and pack them into a single atlas, outputting metadata in the process in the form of -JSON and/or source files for direct use in odin (or other languages through a customization file). + -I'm using a library for marshalling the aseprite files found [here](https://github.com/blob1807/odin-aseprite) on github. - -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)) diff --git a/examples/aseprite_odin_generator.odin b/examples/aseprite_odin_generator.odin index d400cc7..7180177 100644 --- a/examples/aseprite_odin_generator.odin +++ b/examples/aseprite_odin_generator.odin @@ -1,6 +1,6 @@ package cli -import ase "../vendors/aseprite" +import ase "../../vendors/odin-aseprite" import "core:encoding/json" import "core:fmt" import "core:os" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..1bf5423 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,5 @@ +FLAGS="-define:RAYLIB_SHARED=true -out:build/yaap -debug" +SRC=src/frontend + +mkdir -p build +odin build ${SRC} ${FLAGS} \ No newline at end of file diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..81af715 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +set -e + +pushd vendors/dialog +sh build.sh +popd \ No newline at end of file diff --git a/src/generator/generator.odin b/src/generator/generator.odin index 5e78041..34d6ab5 100644 --- a/src/generator/generator.odin +++ b/src/generator/generator.odin @@ -1,6 +1,6 @@ package generator -import ase "../../vendors/aseprite" +import ase "../../vendors/odin-aseprite" import "core:encoding/json" import "core:fmt" import "core:log" @@ -115,7 +115,7 @@ unmarshall_aseprite_files :: proc( if extension != ".aseprite" do continue log.infof("Unmarshalling file: ", file) - ase.unmarshal_from_filename(file, &aseprite_document, alloc) + ase.unmarshal_from_filename(&aseprite_document, file) atlas_entry := atlas_entry_from_compressed_cells(aseprite_document) atlas_entry.path = file @@ -147,7 +147,7 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr cell := CellData { img = rl.Image { - data = rawptr(&cel_img.pixel[0]), + data = rawptr(&cel_img.pixels[0]), width = auto_cast cel_img.width, height = auto_cast cel_img.height, format = .UNCOMPRESSED_R8G8B8A8, @@ -277,7 +277,7 @@ pack_atlas_entries :: proc( } cell_metadata := SpriteAtlasMetadata { name = cell_name, - location = { + location = { auto_cast rect.x + auto_cast offset_x, auto_cast rect.y + auto_cast offset_y, }, @@ -290,23 +290,23 @@ pack_atlas_entries :: proc( } odin_source_generator_metadata := SourceCodeGeneratorMetadata { - file_defines = { + file_defines = { top = "package atlas_bindings\n\n", bottom = "", file_name = "metadata", file_extension = ".odin", }, - custom_data_type = { + custom_data_type = { name = "AtlasRect", type_declaration = "%v :: struct {{ x, y, w, h: i32 }}\n\n", }, - enum_data = { + enum_data = { name = "AtlasEnum", begin_line = "%v :: enum {{\n", entry_line = "\t%s,\n", end_line = "}\n\n", }, - array_data = { + array_data = { name = "ATLAS_SPRITES", type = "[]AtlasRect", begin_line = "%v := %v {{\n", @@ -318,23 +318,23 @@ odin_source_generator_metadata := SourceCodeGeneratorMetadata { cpp_source_generator_metadata := SourceCodeGeneratorMetadata { - file_defines = { + file_defines = { top = "#include \n\n", bottom = "", file_name = "metadata", file_extension = ".hpp", }, - custom_data_type = { + 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 = { + enum_data = { name = "AtlasEnum", begin_line = "enum %v {{\n", entry_line = "\t%s,\n", end_line = "\n\tCOUNT\n}\n\n", }, - array_data = { + array_data = { name = "ATLAS_SPRITES", type = "AtlasRect[size_t(AtlasEnum::COUNT)-1]", begin_line = "{1} {0} = {{\n", @@ -516,7 +516,7 @@ save_metadata_simple :: proc( return } - metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata);if !ok { + metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); if !ok { log.error("No metadata to export!") } @@ -542,7 +542,7 @@ save_metadata_simple :: proc( source_metadata := strings.to_string(sb) source_output_path := strings.concatenate( - { + { output_path, OS_FILE_SEPARATOR, codegen.file_defines.file_name, @@ -573,7 +573,7 @@ save_metadata :: proc( atlas_entries: []AtlasEntry, atlas_metadata: []SpriteAtlasMetadata, ) { - metadata, ok := settings.metadata.(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) diff --git a/vendors/aseprite b/vendors/aseprite deleted file mode 160000 index 628e655..0000000 --- a/vendors/aseprite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 628e655661d822fecae67cf238cbfa414912d943 diff --git a/vendors/dialog/build.sh b/vendors/dialog/build.sh index 2eba996..bea45c2 100644 --- a/vendors/dialog/build.sh +++ b/vendors/dialog/build.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh gcc ./libtinyfiledialogs/tinyfiledialogs.c -c -o libtinyfiledialogs.o diff --git a/vendors/odin-aseprite b/vendors/odin-aseprite new file mode 160000 index 0000000..72ea2e8 --- /dev/null +++ b/vendors/odin-aseprite @@ -0,0 +1 @@ +Subproject commit 72ea2e8094a5f05074d4c4f2faafdba42e54673c From a51f5c6b5760d0e1f0233f4531686add4ccd4b0e Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Fri, 2 Jan 2026 18:49:24 +0200 Subject: [PATCH 28/31] More cleanup. Hooray! --- src/frontend/globals.odin | 44 ++--- src/frontend/main.odin | 347 +++++++++++++++-------------------- src/generator/generator.odin | 36 ++-- vendors/dialog/build.sh | 4 +- 4 files changed, 180 insertions(+), 251 deletions(-) diff --git a/src/frontend/globals.odin b/src/frontend/globals.odin index c9aae32..4f54a6b 100644 --- a/src/frontend/globals.odin +++ b/src/frontend/globals.odin @@ -4,10 +4,12 @@ import generator "../generator" import rl "vendor:raylib" PIXEL_WINDOW_HEIGHT :: 180 -FILE_DIALOG_SIZE :: 1000 scaling: f32 = 2 +@(rodata) +ATLAS_RENDER_SIZES := []i32{256, 512, 1024, 2048, 4096} + WindowInformation :: struct { w: f32, h: f32, @@ -22,9 +24,7 @@ MonitorInformation :: struct { FileDialogType :: enum { SourceFiles, - SourceFolder, OutputFolder, - SaveFileAs, } PackerSettings :: struct { @@ -37,28 +37,16 @@ PackerSettings :: struct { output_odin: bool, } -ApplicationState :: 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]generator.SpriteAtlasMetadata), -} - -g_mem: ApplicationState +window_info: WindowInformation +monitor_info: MonitorInformation +// Where the output files will be written (atlas.png json output, etc) +output_folder_path: Maybe(string) +// If a folder was chosen as input - the path +source_files_to_pack: Maybe([]string) +// Packer settings +packer_settings: PackerSettings +atlas_render_texture_target: rl.RenderTexture2D +atlas_checked_background: rl.RenderTexture2D +atlas_render_has_preview: bool +atlas_render_size: i32 +atlas_metadata: Maybe([dynamic]generator.SpriteAtlasMetadata) diff --git a/src/frontend/main.odin b/src/frontend/main.odin index 5962de0..4efe66c 100644 --- a/src/frontend/main.odin +++ b/src/frontend/main.odin @@ -1,5 +1,7 @@ package frontend +import "base:runtime" +import "core:c/libc" import "core:fmt" import "core:log" import "core:math" @@ -12,59 +14,58 @@ 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) - mode: int = 0 - when ODIN_OS == .Linux || ODIN_OS == .Darwin { - mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH - } + context.logger = log.create_console_logger() + { + init() - logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode) + defer cleanup() - if logh_err == os.ERROR_NONE { - os.stdout = logh - os.stderr = logh - } + for !rl.WindowShouldClose() { + update() + draw() - logger := - logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger() - context.logger = logger - defer if logh_err == os.ERROR_NONE { - log.destroy_file_logger(logger) + 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") - defer rl.CloseWindow() rl.SetWindowMinSize(1400, 800) + rl.SetTraceLogCallback(rl_log) +} - for !rl.WindowShouldClose() { - update() - draw() - - 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) - } - - for key, value in tracking_allocator.allocation_map { - log.error("%v: Leaked %v bytes\n", value.location, value.size) - } - - mem.tracking_allocator_destroy(&tracking_allocator) +cleanup :: proc() { + log.info("Bye") + rl.CloseWindow() } update :: proc() { // Update the width/height - win_info := &g_mem.window_info + win_info := &window_info win_info.w = f32(rl.GetScreenWidth()) win_info.h = f32(rl.GetScreenHeight()) win_info.height_scaled = win_info.h / scaling @@ -73,10 +74,6 @@ update :: proc() { // Update the virtual mouse position (needed for GUI interaction to work properly for instance) mouse_scale := 1 / scaling rl.SetMouseScale(mouse_scale, mouse_scale) - - if g_mem.should_open_file_dialog { - open_file_dialog() - } } draw :: proc() { @@ -87,8 +84,8 @@ draw :: proc() { draw_screen_ui() - if g_mem.should_render_atlas { - draw_screen_target() + if should_pack_atlas_and_render { + pack_atlas_and_render() } free_all(context.temp_allocator) @@ -100,41 +97,23 @@ ui_camera :: proc() -> rl.Camera2D { draw_screen_ui :: proc() { rl.BeginMode2D(ui_camera()) - defer rl.EndMode2D() - draw_atlas_settings_and_preview() + draw_settings_and_preview() + + rl.EndMode2D() } -pick_sources :: proc() { - g_mem.should_open_file_dialog = true - g_mem.source_location_type = .SourceFiles +save_atlas_to_file :: proc() { + generator.save_output(output_folder_path, atlas_metadata, atlas_render_texture_target) } -pick_output :: proc() { - g_mem.should_open_file_dialog = true - g_mem.source_location_type = .OutputFolder +save_to_atlas_to_file :: proc() { + open_file_dialog(.OutputFolder) + save_atlas_to_file() } -pack_atlas :: proc() { - g_mem.should_render_atlas = true -} - -save :: proc() { - generator.save_output( - g_mem.output_folder_path, - g_mem.atlas_metadata, - g_mem.atlas_render_texture_target, - ) -} - -save_to :: proc() { - // if output_folder, ok := g_mem.output_folder_path.(string); ok { - // generator.save_metadata_simple(output_folder, g_mem.atlas_metadata, nil, nil, nil) - // } -} - -draw_screen_target :: proc() { - atlas_render_target := &g_mem.atlas_render_texture_target +pack_atlas_and_render :: proc() { + atlas_render_target := &atlas_render_texture_target rl.BeginTextureMode(atlas_render_target^) defer rl.EndTextureMode() @@ -142,28 +121,21 @@ draw_screen_target :: proc() { atlas_entries: [dynamic]generator.AtlasEntry delete(atlas_entries) - if files, ok := g_mem.source_files_to_pack.([]string); ok { + if files, ok := source_files_to_pack.([]string); ok { generator.unmarshall_aseprite_files(files, &atlas_entries) } else { log.error("No source folder or files set! Can't pack the void!!!") - g_mem.should_render_atlas = false + should_pack_atlas_and_render = false return } - atlas: rl.Image = rl.GenImageColor(g_mem.atlas_render_size, g_mem.atlas_render_size, rl.BLANK) + atlas: rl.Image = rl.GenImageColor(atlas_render_size, 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 + 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 - g_mem.atlas_metadata = generator.pack_atlas_entries( - atlas_entries[:], - &atlas, - padding_x, - padding_y, - ) + atlas_metadata = generator.pack_atlas_entries(atlas_entries[:], &atlas, padding_x, padding_y) // OpenGL's Y buffer is flipped rl.ImageFlipVertical(&atlas) @@ -171,28 +143,27 @@ draw_screen_target :: proc() { log.info("Packed everything!") atlas_render_target.texture = rl.LoadTextureFromImage(atlas) - g_mem.should_render_atlas = false - g_mem.atlas_render_has_preview = true + should_pack_atlas_and_render = false + atlas_render_has_preview = true } -draw_atlas_settings_and_preview :: proc() { +draw_settings_and_preview :: proc() { left_half_rect := rl.Rectangle { x = 0, y = 0, - width = cast(f32)g_mem.window_info.width_scaled / 3, - height = cast(f32)g_mem.window_info.height_scaled, + width = cast(f32)window_info.width_scaled / 3, + height = cast(f32)window_info.height_scaled, } right_half_rect := rl.Rectangle { - x = cast(f32)g_mem.window_info.width_scaled / 3, + x = cast(f32)window_info.width_scaled / 3, y = 0, - width = cast(f32)(g_mem.window_info.width_scaled / 3) * 2, - height = cast(f32)g_mem.window_info.height_scaled, + 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 + @(static) spinner_edit_mode: bool small_offset := 10 * scaling big_offset := 30 * scaling @@ -201,10 +172,7 @@ draw_atlas_settings_and_preview :: proc() { rl.GuiPanel(left_half_rect, "Atlas Settings") elements_height += small_offset / 2 - @(static) - SettingsDropBoxEditMode: bool - @(static) - SettingsDropdownBoxActive: i32 + @(static) settings_dropdown_box_active_idx: i32 elements_height += small_offset + 5 * scaling @@ -214,10 +182,8 @@ draw_atlas_settings_and_preview :: proc() { ) elements_height += small_offset / 2 - @(static) - dropdown_resolution_edit_mode: bool - @(static) - dropdown_resolution_mode: i32 + @(static) dropdown_resolution_edit_mode: bool + @(static) dropdown_resolution_mode: i32 dropdown_rect := rl.Rectangle { x = small_offset, @@ -230,21 +196,21 @@ draw_atlas_settings_and_preview :: proc() { 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_rect, + "256x;512x;1024x;2048x;4096x", + &dropdown_resolution_mode, + dropdown_resolution_edit_mode, + ) { dropdown_resolution_edit_mode = !dropdown_resolution_edit_mode - g_mem.atlas_render_size = 256 * auto_cast math.pow(2, f32(dropdown_resolution_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 SettingsDropdownBoxActive == 0 { + if settings_dropdown_box_active_idx == 0 { padding_settings_y := elements_height { defer { @@ -267,7 +233,7 @@ draw_atlas_settings_and_preview :: proc() { rl.GuiCheckBox( enable_padding_rect, " Enable padding", - &g_mem.packer_settings.padding_enabled, + &packer_settings.padding_enabled, ) elements_height += small_offset * 2 @@ -282,7 +248,7 @@ draw_atlas_settings_and_preview :: proc() { padding_x_spinner := rl.GuiSpinner( padding_x_spinner_rect, "", - &g_mem.packer_settings.pixel_padding_x_int, + &packer_settings.pixel_padding_x_int, 0, 10, spinner_edit_mode, @@ -291,7 +257,7 @@ draw_atlas_settings_and_preview :: proc() { spinner_edit_mode = !spinner_edit_mode } rl.GuiLabel( - { + { x = (small_offset * 2) + big_offset * 2, y = elements_height, width = big_offset, @@ -313,7 +279,7 @@ draw_atlas_settings_and_preview :: proc() { padding_y_spinner := rl.GuiSpinner( padding_y_spinner_rect, "", - &g_mem.packer_settings.pixel_padding_y_int, + &packer_settings.pixel_padding_y_int, 0, 10, spinner_edit_mode, @@ -322,7 +288,7 @@ draw_atlas_settings_and_preview :: proc() { spinner_edit_mode = !spinner_edit_mode } rl.GuiLabel( - { + { x = (small_offset * 2) + big_offset * 2, y = elements_height, width = big_offset, @@ -336,9 +302,6 @@ draw_atlas_settings_and_preview :: proc() { } elements_height += small_offset - // rl.GuiLine({y = elements_height, width = left_half_rect.width}, "Actions") - // elements_height += small_offset - { actions_label_y := elements_height @@ -360,7 +323,7 @@ draw_atlas_settings_and_preview :: proc() { height = small_offset, } if rl.GuiButton(pick_sources_rect, "Pick Source(s)") { - pick_sources() + open_file_dialog(.SourceFiles) } pick_output_rect := rl.Rectangle { @@ -370,7 +333,7 @@ draw_atlas_settings_and_preview :: proc() { height = small_offset, } if rl.GuiButton(pick_output_rect, "Pick Output") { - pick_output() + open_file_dialog(.OutputFolder) } elements_height += small_offset * 2 @@ -381,7 +344,7 @@ draw_atlas_settings_and_preview :: proc() { height = small_offset, } if rl.GuiButton(pack_atlas_rect, "Pack Atlas") { - pack_atlas() + should_pack_atlas_and_render = true } clear_atlas_rect := rl.Rectangle { @@ -402,7 +365,7 @@ draw_atlas_settings_and_preview :: proc() { height = small_offset, } if rl.GuiButton(save_rect, "Save") { - save() + save_atlas_to_file() } save_to_rect := rl.Rectangle { @@ -412,7 +375,7 @@ draw_atlas_settings_and_preview :: proc() { height = small_offset, } if rl.GuiButton(save_to_rect, "Save To...") { - save_to() + save_to_atlas_to_file() } elements_height += small_offset * 2 } @@ -420,10 +383,9 @@ draw_atlas_settings_and_preview :: proc() { } // Packing Options - if SettingsDropdownBoxActive == 1 { + if settings_dropdown_box_active_idx == 1 { - @(static) - active_tab: i32 + @(static) active_tab: i32 tabs: []cstring = {"One", "Two", "Three"} rl.GuiTabBar( {x = small_offset, y = elements_height, width = 100, height = small_offset}, @@ -434,9 +396,8 @@ draw_atlas_settings_and_preview :: proc() { } // Save Options - if SettingsDropdownBoxActive == 2 { - - } + // if settings_dropdown_box_active_idx == 2 { + // } elements_height = 0 rl.GuiPanel(right_half_rect, "Atlas Preview") @@ -450,11 +411,11 @@ draw_atlas_settings_and_preview :: proc() { width = short_edge, height = short_edge, } - if !g_mem.atlas_render_has_preview { + if !atlas_render_has_preview { rl.GuiDummyRec(preview_rect, "PREVIEW") } else { // rl.DrawRectangleRec(preview_rect, rl.WHITE) - bg_texture := g_mem.atlas_checked_background.texture + bg_texture := atlas_checked_background.texture rl.DrawTexturePro( bg_texture, {width = auto_cast bg_texture.width, height = auto_cast bg_texture.height}, @@ -463,9 +424,7 @@ draw_atlas_settings_and_preview :: proc() { 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 + atlas_texture := atlas_render_texture_target.texture rl.DrawTexturePro( atlas_texture, {width = auto_cast atlas_texture.width, height = auto_cast -atlas_texture.height}, @@ -477,96 +436,80 @@ draw_atlas_settings_and_preview :: proc() { } } -open_file_dialog :: proc() { - switch g_mem.source_location_type { +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 ('|'), + // `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, - ), + diag.open_file_dialog("Select source files", nil, 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, "|") + // todo(stefan): We're assuming the filepaths returned libtinydialog are valid... + source_files := strings.clone_from_cstring(file_paths_conc, context.allocator) + source_files_to_pack = strings.split(source_files, "|") - log.info(g_mem.source_files_to_pack) + log.info(source_files_to_pack) } else { log.error("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) - log.info(g_mem.source_location_to_pack) - } else { - log.error("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]), - ), - ) + file := cstring(diag.select_folder_dialog("Select source folder", nil)) if len(file) > 0 { - g_mem.output_folder_path = strings.clone_from_cstring(file) - log.info(g_mem.output_folder_path) + output_folder_path = strings.clone_from_cstring(file) + log.info(output_folder_path) } else { log.error("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, generator.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 { - generator.save_output( - g_mem.output_folder_path, - g_mem.atlas_metadata, - g_mem.atlas_render_texture_target, - ) - } } - - - g_mem.should_open_file_dialog = false } clear_atlas_data :: proc() { - if metadata, ok := g_mem.atlas_metadata.([dynamic]generator.SpriteAtlasMetadata); ok { + if metadata, ok := atlas_metadata.([dynamic]generator.SpriteAtlasMetadata); ok { delete(metadata) } - g_mem.atlas_render_has_preview = false + atlas_render_has_preview = false +} +logger: log.Logger +rl_log_buf: []byte +rl_log :: proc "c" (logLevel: rl.TraceLogLevel, text: cstring, args: ^libc.va_list) { + context = runtime.default_context() + context.logger = logger + + level: log.Level + switch logLevel { + case .TRACE, .DEBUG: + level = .Debug + case .ALL, .NONE, .INFO: + level = .Info + case .WARNING: + level = .Warning + case .ERROR: + level = .Error + case .FATAL: + level = .Fatal + } + + if level < logger.lowest_level { + return + } + + if rl_log_buf == nil { + rl_log_buf = make([]byte, 1024) + } + + defer mem.zero_slice(rl_log_buf) + + n: int + for { + va := args + n = int(libc.vsnprintf(raw_data(rl_log_buf), len(rl_log_buf), text, va)) + if n < len(rl_log_buf) do break + log.infof("Resizing raylib log buffer from %m to %m", len(rl_log_buf), len(rl_log_buf) * 2) + rl_log_buf, _ = mem.resize_bytes(rl_log_buf, len(rl_log_buf) * 2) + } + + formatted := string(rl_log_buf[:n]) + log.log(level, formatted) } diff --git a/src/generator/generator.odin b/src/generator/generator.odin index 34d6ab5..a3745af 100644 --- a/src/generator/generator.odin +++ b/src/generator/generator.odin @@ -182,28 +182,27 @@ pack_atlas_entries :: proc( 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 + all_cell_images := make([dynamic]rl.Image) // 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) + entry.layer_cell_count = make([dynamic]i32, len(entry.cells)) } num_entries := len(all_cell_images) - nodes := make([]stbrp.Node, num_entries, allocator) - rects := make([]stbrp.Rect, num_entries, allocator) + nodes := make([]stbrp.Node, num_entries) + rects := make([]stbrp.Rect, num_entries) EntryAndCell :: struct { entry: ^AtlasEntry, cell_of_entry: ^CellData, } - rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100, allocator) + rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100) // Set the custom IDs cellIdx: int @@ -241,15 +240,15 @@ pack_atlas_entries :: proc( src_rect := rl.Rectangle { x = 0, y = 0, - width = auto_cast cell.img.width, - height = auto_cast cell.img.height, + width = f32(cell.img.width), + height = f32(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, + 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 @@ -258,7 +257,7 @@ pack_atlas_entries :: proc( log.infof("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect) } - metadata := make([dynamic]SpriteAtlasMetadata, allocator) + 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 @@ -267,10 +266,9 @@ pack_atlas_entries :: proc( cell_name: string if entry.layer_cell_count[cell.layer_index] > 1 { cell_name = fmt.aprintf( - "{0}_%d", + "{0}_{1}", entry.layer_names[cell.layer_index], cell.frame_index, - allocator, ) } else { cell_name = entry.layer_names[cell.layer_index] @@ -278,8 +276,8 @@ pack_atlas_entries :: proc( cell_metadata := SpriteAtlasMetadata { name = cell_name, location = { - auto_cast rect.x + auto_cast offset_x, - auto_cast rect.y + auto_cast offset_y, + i32(rect.x) + offset_x, + i32(rect.y) + offset_y, }, size = {auto_cast cell.img.width, auto_cast cell.img.height}, } @@ -308,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", @@ -369,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 { diff --git a/vendors/dialog/build.sh b/vendors/dialog/build.sh index bea45c2..43b3bbc 100644 --- a/vendors/dialog/build.sh +++ b/vendors/dialog/build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh +set -e + gcc ./libtinyfiledialogs/tinyfiledialogs.c -c -o libtinyfiledialogs.o - ar rcs libtinyfiledialogs.a libtinyfiledialogs.o - rm libtinyfiledialogs.o \ No newline at end of file From b4f7fb4171d46461b8b43dcb9ac3f4445d2dfde6 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Fri, 2 Jan 2026 19:03:06 +0200 Subject: [PATCH 29/31] Reorganize globals and introduce an `Atlas` struct --- src/frontend/globals.odin | 37 ++++++++++++++++------------- src/frontend/main.odin | 50 ++++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/src/frontend/globals.odin b/src/frontend/globals.odin index 4f54a6b..c3c3a94 100644 --- a/src/frontend/globals.odin +++ b/src/frontend/globals.odin @@ -5,27 +5,29 @@ import rl "vendor:raylib" PIXEL_WINDOW_HEIGHT :: 180 -scaling: f32 = 2 +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, } - -FileDialogType :: enum { - SourceFiles, - OutputFolder, -} +monitor_info: MonitorInformation PackerSettings :: struct { atlas_size_x: i32, @@ -36,17 +38,18 @@ PackerSettings :: struct { output_json: bool, output_odin: bool, } +packer_settings: PackerSettings -window_info: WindowInformation -monitor_info: MonitorInformation // Where the output files will be written (atlas.png json output, etc) -output_folder_path: Maybe(string) +output_folder_path: Maybe(string) // If a folder was chosen as input - the path -source_files_to_pack: Maybe([]string) -// Packer settings -packer_settings: PackerSettings -atlas_render_texture_target: rl.RenderTexture2D -atlas_checked_background: rl.RenderTexture2D -atlas_render_has_preview: bool -atlas_render_size: i32 -atlas_metadata: Maybe([dynamic]generator.SpriteAtlasMetadata) +source_files_to_pack: Maybe([]string) + +Atlas :: struct { + render_texture_target: rl.RenderTexture2D, + checked_background: rl.RenderTexture2D, + render_has_preview: bool, + render_size: i32, + metadata: Maybe([dynamic]generator.SpriteAtlasMetadata), +} +atlas: Atlas diff --git a/src/frontend/main.odin b/src/frontend/main.odin index 4efe66c..fc0d84d 100644 --- a/src/frontend/main.odin +++ b/src/frontend/main.odin @@ -50,7 +50,7 @@ main :: proc() { } init :: proc() { - atlas_render_size = ATLAS_RENDER_SIZES[0] + atlas.render_size = ATLAS_RENDER_SIZES[0] rl.SetConfigFlags({.WINDOW_RESIZABLE}) rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer") @@ -68,11 +68,11 @@ update :: proc() { win_info := &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 + 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 / scaling + mouse_scale := 1 / ui_scaling rl.SetMouseScale(mouse_scale, mouse_scale) } @@ -92,7 +92,7 @@ draw :: proc() { } ui_camera :: proc() -> rl.Camera2D { - return {zoom = scaling} + return {zoom = ui_scaling} } draw_screen_ui :: proc() { @@ -104,7 +104,7 @@ draw_screen_ui :: proc() { } save_atlas_to_file :: proc() { - generator.save_output(output_folder_path, atlas_metadata, atlas_render_texture_target) + generator.save_output(output_folder_path, atlas.metadata, atlas.render_texture_target) } save_to_atlas_to_file :: proc() { @@ -113,9 +113,7 @@ save_to_atlas_to_file :: proc() { } pack_atlas_and_render :: proc() { - atlas_render_target := &atlas_render_texture_target - - rl.BeginTextureMode(atlas_render_target^) + rl.BeginTextureMode(atlas.render_texture_target) defer rl.EndTextureMode() atlas_entries: [dynamic]generator.AtlasEntry @@ -129,22 +127,26 @@ pack_atlas_and_render :: proc() { return } - atlas: rl.Image = rl.GenImageColor(atlas_render_size, atlas_render_size, rl.BLANK) - // defer rl.UnloadImage(atlas) + 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 - atlas_metadata = generator.pack_atlas_entries(atlas_entries[:], &atlas, padding_x, padding_y) + atlas.metadata = generator.pack_atlas_entries( + atlas_entries[:], + &atlas_img, + padding_x, + padding_y, + ) // OpenGL's Y buffer is flipped - rl.ImageFlipVertical(&atlas) + rl.ImageFlipVertical(&atlas_img) // rl.UnloadTexture(atlas_render_target.texture) log.info("Packed everything!") - atlas_render_target.texture = rl.LoadTextureFromImage(atlas) + atlas.render_texture_target.texture = rl.LoadTextureFromImage(atlas_img) should_pack_atlas_and_render = false - atlas_render_has_preview = true + atlas.render_has_preview = true } draw_settings_and_preview :: proc() { @@ -165,8 +167,8 @@ draw_settings_and_preview :: proc() { @(static) spinner_edit_mode: bool - small_offset := 10 * scaling - big_offset := 30 * scaling + small_offset := 10 * ui_scaling + big_offset := 30 * ui_scaling elements_height: f32 = 0 rl.GuiPanel(left_half_rect, "Atlas Settings") @@ -174,7 +176,7 @@ draw_settings_and_preview :: proc() { @(static) settings_dropdown_box_active_idx: i32 - elements_height += small_offset + 5 * scaling + elements_height += small_offset + 5 * ui_scaling rl.GuiLabel( {x = small_offset, y = elements_height, width = left_half_rect.width}, @@ -202,7 +204,7 @@ draw_settings_and_preview :: proc() { dropdown_resolution_edit_mode, ) { dropdown_resolution_edit_mode = !dropdown_resolution_edit_mode - atlas_render_size = + atlas.render_size = ATLAS_RENDER_SIZES[max(i32(len(ATLAS_RENDER_SIZES) - 1), dropdown_resolution_mode)] } rl.GuiUnlock() @@ -411,11 +413,11 @@ draw_settings_and_preview :: proc() { width = short_edge, height = short_edge, } - if !atlas_render_has_preview { + if !atlas.render_has_preview { rl.GuiDummyRec(preview_rect, "PREVIEW") } else { // rl.DrawRectangleRec(preview_rect, rl.WHITE) - bg_texture := atlas_checked_background.texture + bg_texture := atlas.checked_background.texture rl.DrawTexturePro( bg_texture, {width = auto_cast bg_texture.width, height = auto_cast bg_texture.height}, @@ -424,7 +426,7 @@ draw_settings_and_preview :: proc() { 0, rl.WHITE, ) - atlas_texture := atlas_render_texture_target.texture + atlas_texture := atlas.render_texture_target.texture rl.DrawTexturePro( atlas_texture, {width = auto_cast atlas_texture.width, height = auto_cast -atlas_texture.height}, @@ -466,10 +468,10 @@ open_file_dialog :: proc(dialog_type: FileDialogType) { } clear_atlas_data :: proc() { - if metadata, ok := atlas_metadata.([dynamic]generator.SpriteAtlasMetadata); ok { + if metadata, ok := atlas.metadata.([dynamic]generator.SpriteAtlasMetadata); ok { delete(metadata) } - atlas_render_has_preview = false + atlas.render_has_preview = false } logger: log.Logger rl_log_buf: []byte From d748f742f7e2d8300b43e3372633064a345b727b Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Fri, 2 Jan 2026 19:05:24 +0200 Subject: [PATCH 30/31] Cleanup unused imports --- src/frontend/main.odin | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/frontend/main.odin b/src/frontend/main.odin index fc0d84d..1cf4860 100644 --- a/src/frontend/main.odin +++ b/src/frontend/main.odin @@ -2,12 +2,8 @@ package frontend import "base:runtime" import "core:c/libc" -import "core:fmt" import "core:log" -import "core:math" import "core:mem" -import "core:mem/virtual" -import "core:os" import "core:strings" import rl "vendor:raylib" @@ -473,6 +469,7 @@ clear_atlas_data :: proc() { } 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) { From 3c4ad6805909404e4aebc9c729ec3a4c8db974a9 Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Fri, 2 Jan 2026 21:13:34 +0200 Subject: [PATCH 31/31] Refactor depencies, add clay to the repo in preparation to move from raygui to clay --- .gitignore | 15 +- README.md | 6 +- scripts/setup.bat | 8 +- scripts/setup.sh | 6 + src/frontend/globals.odin | 6 +- src/frontend/main.odin | 36 ++- src/generator/generator.odin | 43 ++- .../clay_renderer_raylib.odin | 296 ++++++++++++++++++ vendors/dialog/tinyfiledialog.odin | 32 -- vendors/dialog/tinyfiledialogs.odin | 161 ++++++++++ 10 files changed, 528 insertions(+), 81 deletions(-) create mode 100644 vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin delete mode 100644 vendors/dialog/tinyfiledialog.odin create mode 100644 vendors/dialog/tinyfiledialogs.odin diff --git a/.gitignore b/.gitignore index 91dc2bb..5cb0a1d 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index faeb113..44c60e2 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@ Yet-Another-Atlas-Packer by bersK (Stefan Stefanov) > [!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` -* Build the `libtinyfiledialog` library in the `vendors/dialog` fold +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] diff --git a/scripts/setup.bat b/scripts/setup.bat index abf078d..cdd0a75 100644 --- a/scripts/setup.bat +++ b/scripts/setup.bat @@ -11,4 +11,10 @@ copy %ODIN_ROOT%\vendor\raylib\windows\raylib.dll build\raylib.dll pushd vendors\dialog call .\build.bat -popd \ No newline at end of file +popd + +pushd vendors +curl.exe --output clay.zip --url https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip +@REM Apparently available on Win10 since build 17063 - https://superuser.com/a/1473255 +tar -xf .\clay.zip +popd diff --git a/scripts/setup.sh b/scripts/setup.sh index 81af715..ed557b0 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -4,4 +4,10 @@ set -e pushd vendors/dialog sh build.sh +popd + +pushd vendors +wget https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip +unzip clay-odin.zip +rm -rf ./__MACOSX # Yeah... popd \ No newline at end of file diff --git a/src/frontend/globals.odin b/src/frontend/globals.odin index c3c3a94..795a7c1 100644 --- a/src/frontend/globals.odin +++ b/src/frontend/globals.odin @@ -41,15 +41,15 @@ PackerSettings :: struct { packer_settings: PackerSettings // Where the output files will be written (atlas.png json output, etc) -output_folder_path: Maybe(string) +output_folder_path: string // If a folder was chosen as input - the path -source_files_to_pack: Maybe([]string) +source_files_to_pack: []string Atlas :: struct { render_texture_target: rl.RenderTexture2D, checked_background: rl.RenderTexture2D, render_has_preview: bool, render_size: i32, - metadata: Maybe([dynamic]generator.SpriteAtlasMetadata), + metadata: [dynamic]generator.SpriteAtlasMetadata, } atlas: Atlas diff --git a/src/frontend/main.odin b/src/frontend/main.odin index 1cf4860..6f69642 100644 --- a/src/frontend/main.odin +++ b/src/frontend/main.odin @@ -55,6 +55,9 @@ init :: proc() { } cleanup :: proc() { + delete(atlas.metadata) + delete(output_folder_path) + delete(source_files_to_pack) log.info("Bye") rl.CloseWindow() } @@ -113,10 +116,17 @@ pack_atlas_and_render :: proc() { defer rl.EndTextureMode() atlas_entries: [dynamic]generator.AtlasEntry - delete(atlas_entries) + defer { + for entry in atlas_entries { + delete(entry.cells) + delete(entry.layer_cell_count) + delete(entry.layer_names) + } + delete(atlas_entries) + } - if files, ok := source_files_to_pack.([]string); ok { - generator.unmarshall_aseprite_files(files, &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 @@ -128,6 +138,7 @@ pack_atlas_and_render :: proc() { 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, @@ -439,12 +450,9 @@ open_file_dialog :: proc(dialog_type: FileDialogType) { 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", nil, 0, nil, "", 1), - ) - if len(file_paths_conc) > 0 { + 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 := strings.clone_from_cstring(file_paths_conc, context.allocator) source_files_to_pack = strings.split(source_files, "|") log.info(source_files_to_pack) @@ -453,10 +461,10 @@ open_file_dialog :: proc(dialog_type: FileDialogType) { } case .OutputFolder: - file := cstring(diag.select_folder_dialog("Select source folder", nil)) - if len(file) > 0 { - output_folder_path = strings.clone_from_cstring(file) - log.info(output_folder_path) + 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!") } @@ -464,9 +472,7 @@ open_file_dialog :: proc(dialog_type: FileDialogType) { } clear_atlas_data :: proc() { - if metadata, ok := atlas.metadata.([dynamic]generator.SpriteAtlasMetadata); ok { - delete(metadata) - } + delete(atlas.metadata) atlas.render_has_preview = false } diff --git a/src/generator/generator.odin b/src/generator/generator.odin index a3745af..fce6a4c 100644 --- a/src/generator/generator.odin +++ b/src/generator/generator.odin @@ -99,7 +99,6 @@ unmarshall_aseprite_files_file_info :: proc( } unmarshall_aseprite_files(paths[:], atlas_entries, alloc) - } unmarshall_aseprite_files :: proc( @@ -114,7 +113,7 @@ unmarshall_aseprite_files :: proc( extension := fp.ext(file) if extension != ".aseprite" do continue - log.infof("Unmarshalling file: ", file) + 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 @@ -137,7 +136,7 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr // 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: ", frameIdx, len(frame.chunks)) + 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) @@ -161,7 +160,7 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr } if layer_chunk, ok := chunk.(ase.Layer_Chunk); ok { - log.infof("Layer chunk: ", layer_chunk) + log.info("Layer chunk:", layer_chunk) append(&atlas_entry.layer_names, layer_chunk.name) } } @@ -187,6 +186,7 @@ pack_atlas_entries :: proc( 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) @@ -197,12 +197,15 @@ pack_atlas_entries :: proc( 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 @@ -275,10 +278,7 @@ pack_atlas_entries :: proc( } cell_metadata := SpriteAtlasMetadata { name = cell_name, - location = { - i32(rect.x) + offset_x, - i32(rect.y) + offset_y, - }, + 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) @@ -451,17 +451,16 @@ metadata_source_code_generate :: proc( } save_output :: proc( - output_folder_path: Maybe(string), - atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata), - atlas_render_texture_target: rl.RenderTexture2D, + output_path: string, + metadata: [dynamic]SpriteAtlasMetadata, + render_texture_target: rl.RenderTexture2D, ) { - output_path, ok := output_folder_path.(string) - if !ok || output_path == "" { + if len(output_path) == 0 { log.error("Output path is empty!") return } - image := rl.LoadImageFromTexture(atlas_render_texture_target.texture) + image := rl.LoadImageFromTexture(render_texture_target.texture) rl.ImageFlipVertical(&image) cstring_atlas_output_path := strings.clone_to_cstring( @@ -470,13 +469,17 @@ save_output :: proc( rl.ExportImage(image, cstring_atlas_output_path) - if metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + 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 { log.error("Failed to marshall the atlas metadata to a json!") } @@ -485,9 +488,13 @@ save_output :: proc( // 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 { @@ -537,6 +544,7 @@ 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( @@ -553,6 +561,7 @@ save_metadata_simple :: proc( } } 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"}, diff --git a/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin b/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin new file mode 100644 index 0000000..0f20238 --- /dev/null +++ b/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin @@ -0,0 +1,296 @@ +package main + +import clay "../clay-odin" +import "base:runtime" +import "core:math" +import "core:strings" +import "core:unicode/utf8" +import rl "vendor:raylib" + +Raylib_Font :: struct { + fontId: u16, + font: rl.Font, +} + +clay_color_to_rl_color :: proc(color: clay.Color) -> rl.Color { + return {u8(color.r), u8(color.g), u8(color.b), u8(color.a)} +} + +raylib_fonts := [dynamic]Raylib_Font{} + +// Alias for compatibility, default to ascii support +measure_text :: measure_text_ascii + +measure_text_unicode :: proc "c" ( + text: clay.StringSlice, + config: ^clay.TextElementConfig, + userData: rawptr, +) -> clay.Dimensions { + // Needed for grapheme_count + context = runtime.default_context() + + line_width: f32 = 0 + + font := raylib_fonts[config.fontId].font + text_str := string(text.chars[:text.length]) + + // This function seems somewhat expensive, if you notice performance issues, you could assume + // - 1 codepoint per visual character (no grapheme clusters), where you can get the length from the loop + // - 1 byte per visual character (ascii), where you can get the length with `text.length` + // see `measure_text_ascii` + grapheme_count, _, _ := utf8.grapheme_count(text_str) + + for letter, byte_idx in text_str { + glyph_index := rl.GetGlyphIndex(font, letter) + + glyph := font.glyphs[glyph_index] + + if glyph.advanceX != 0 { + line_width += f32(glyph.advanceX) + } else { + line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX) + } + } + + scaleFactor := f32(config.fontSize) / f32(font.baseSize) + + // Note: + // I'd expect this to be `grapheme_count - 1`, + // but that seems to be one letterSpacing too small + // maybe that's a raylib bug, maybe that's Clay? + total_spacing := f32(grapheme_count) * f32(config.letterSpacing) + + return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)} +} + +measure_text_ascii :: proc "c" ( + text: clay.StringSlice, + config: ^clay.TextElementConfig, + userData: rawptr, +) -> clay.Dimensions { + line_width: f32 = 0 + + font := raylib_fonts[config.fontId].font + text_str := string(text.chars[:text.length]) + + for i in 0 ..< len(text_str) { + glyph_index := text_str[i] - 32 + + glyph := font.glyphs[glyph_index] + + if glyph.advanceX != 0 { + line_width += f32(glyph.advanceX) + } else { + line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX) + } + } + + scaleFactor := f32(config.fontSize) / f32(font.baseSize) + + // Note: + // I'd expect this to be `len(text_str) - 1`, + // but that seems to be one letterSpacing too small + // maybe that's a raylib bug, maybe that's Clay? + total_spacing := f32(len(text_str)) * f32(config.letterSpacing) + + return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)} +} + +clay_raylib_render :: proc( + render_commands: ^clay.ClayArray(clay.RenderCommand), + allocator := context.temp_allocator, +) { + for i in 0 ..< render_commands.length { + render_command := clay.RenderCommandArray_Get(render_commands, i) + bounds := render_command.boundingBox + + switch render_command.commandType { + case .None: // None + case .Text: + config := render_command.renderData.text + + text := string(config.stringContents.chars[:config.stringContents.length]) + + // Raylib uses C strings instead of Odin strings, so we need to clone + // Assume this will be freed elsewhere since we default to the temp allocator + cstr_text := strings.clone_to_cstring(text, allocator) + + font := raylib_fonts[config.fontId].font + rl.DrawTextEx( + font, + cstr_text, + {bounds.x, bounds.y}, + f32(config.fontSize), + f32(config.letterSpacing), + clay_color_to_rl_color(config.textColor), + ) + case .Image: + config := render_command.renderData.image + tint := config.backgroundColor + if tint == 0 { + tint = {255, 255, 255, 255} + } + + imageTexture := (^rl.Texture2D)(config.imageData) + rl.DrawTextureEx( + imageTexture^, + {bounds.x, bounds.y}, + 0, + bounds.width / f32(imageTexture.width), + clay_color_to_rl_color(tint), + ) + case .ScissorStart: + rl.BeginScissorMode( + i32(math.round(bounds.x)), + i32(math.round(bounds.y)), + i32(math.round(bounds.width)), + i32(math.round(bounds.height)), + ) + case .ScissorEnd: + rl.EndScissorMode() + case .Rectangle: + config := render_command.renderData.rectangle + if config.cornerRadius.topLeft > 0 { + radius: f32 = (config.cornerRadius.topLeft * 2) / min(bounds.width, bounds.height) + draw_rect_rounded( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + radius, + config.backgroundColor, + ) + } else { + draw_rect(bounds.x, bounds.y, bounds.width, bounds.height, config.backgroundColor) + } + case .Border: + config := render_command.renderData.border + // Left border + if config.width.left > 0 { + draw_rect( + bounds.x, + bounds.y + config.cornerRadius.topLeft, + f32(config.width.left), + bounds.height - config.cornerRadius.topLeft - config.cornerRadius.bottomLeft, + config.color, + ) + } + // Right border + if config.width.right > 0 { + draw_rect( + bounds.x + bounds.width - f32(config.width.right), + bounds.y + config.cornerRadius.topRight, + f32(config.width.right), + bounds.height - config.cornerRadius.topRight - config.cornerRadius.bottomRight, + config.color, + ) + } + // Top border + if config.width.top > 0 { + draw_rect( + bounds.x + config.cornerRadius.topLeft, + bounds.y, + bounds.width - config.cornerRadius.topLeft - config.cornerRadius.topRight, + f32(config.width.top), + config.color, + ) + } + // Bottom border + if config.width.bottom > 0 { + draw_rect( + bounds.x + config.cornerRadius.bottomLeft, + bounds.y + bounds.height - f32(config.width.bottom), + bounds.width - + config.cornerRadius.bottomLeft - + config.cornerRadius.bottomRight, + f32(config.width.bottom), + config.color, + ) + } + + // Rounded Borders + if config.cornerRadius.topLeft > 0 { + draw_arc( + bounds.x + config.cornerRadius.topLeft, + bounds.y + config.cornerRadius.topLeft, + config.cornerRadius.topLeft - f32(config.width.top), + config.cornerRadius.topLeft, + 180, + 270, + config.color, + ) + } + if config.cornerRadius.topRight > 0 { + draw_arc( + bounds.x + bounds.width - config.cornerRadius.topRight, + bounds.y + config.cornerRadius.topRight, + config.cornerRadius.topRight - f32(config.width.top), + config.cornerRadius.topRight, + 270, + 360, + config.color, + ) + } + if config.cornerRadius.bottomLeft > 0 { + draw_arc( + bounds.x + config.cornerRadius.bottomLeft, + bounds.y + bounds.height - config.cornerRadius.bottomLeft, + config.cornerRadius.bottomLeft - f32(config.width.top), + config.cornerRadius.bottomLeft, + 90, + 180, + config.color, + ) + } + if config.cornerRadius.bottomRight > 0 { + draw_arc( + bounds.x + bounds.width - config.cornerRadius.bottomRight, + bounds.y + bounds.height - config.cornerRadius.bottomRight, + config.cornerRadius.bottomRight - f32(config.width.bottom), + config.cornerRadius.bottomRight, + 0.1, + 90, + config.color, + ) + } + case clay.RenderCommandType.Custom: + // Implement custom element rendering here + } + } +} + +// Helper procs, mainly for repeated conversions + +@(private = "file") +draw_arc :: proc( + x, y: f32, + inner_rad, outer_rad: f32, + start_angle, end_angle: f32, + color: clay.Color, +) { + rl.DrawRing( + {math.round(x), math.round(y)}, + math.round(inner_rad), + outer_rad, + start_angle, + end_angle, + 10, + clay_color_to_rl_color(color), + ) +} + +@(private = "file") +draw_rect :: proc(x, y, w, h: f32, color: clay.Color) { + rl.DrawRectangle( + i32(math.round(x)), + i32(math.round(y)), + i32(math.round(w)), + i32(math.round(h)), + clay_color_to_rl_color(color), + ) +} + +@(private = "file") +draw_rect_rounded :: proc(x, y, w, h: f32, radius: f32, color: clay.Color) { + rl.DrawRectangleRounded({x, y, w, h}, radius, 8, clay_color_to_rl_color(color)) +} diff --git a/vendors/dialog/tinyfiledialog.odin b/vendors/dialog/tinyfiledialog.odin deleted file mode 100644 index 1c82058..0000000 --- a/vendors/dialog/tinyfiledialog.odin +++ /dev/null @@ -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 --- -} diff --git a/vendors/dialog/tinyfiledialogs.odin b/vendors/dialog/tinyfiledialogs.odin new file mode 100644 index 0000000..ba43e6e --- /dev/null +++ b/vendors/dialog/tinyfiledialogs.odin @@ -0,0 +1,161 @@ +package tinyfiledialogs + +import "core:c" +import "core:mem" +import "core:strings" + +when ODIN_OS == .Windows { + foreign import lib {"tinyfiledialogs.lib", "system:comdlg32.lib", "system:Ole32.lib"} +} else when ODIN_OS == .Linux || ODIN_OS == .Darwin { + foreign import lib "libtinyfiledialogs.a" +} + +@(default_calling_convention = "c", link_prefix = "tinyfd_") +foreign lib { + notifyPopup :: proc(title, message, icon_type: cstring) -> c.int --- + + messageBox :: proc(title, message, dialog_type, icon_type: cstring, default_button: c.int) -> c.int --- + inputBox :: proc(title, message, default_input: cstring) -> cstring --- + + saveFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring) -> cstring --- + openFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring, allow_multi: c.int) -> cstring --- + + selectFolderDialog :: proc(title, default_path: cstring) -> cstring --- + + colorChooser :: proc(title, default_hex_rgb: cstring, default_rgb, result_rgb: [3]byte) -> cstring --- +} + +select_folder_dialog :: proc( + title, default_path: string, + alloc := context.allocator, + temp_alloc := context.temp_allocator, +) -> ( + path: string, + success: bool, +) { + ctitle: cstring = nil + cdefault_path: cstring = nil + err: mem.Allocator_Error + + if len(title) > 0 { + ctitle, err = strings.clone_to_cstring(title, temp_alloc) + if err != nil { + return {}, false + } + } + if len(default_path) > 0 { + cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc) + if err != nil { + return {}, false + } + } + res := selectFolderDialog(ctitle, cdefault_path) + path, err = strings.clone_from_cstring(res, alloc) + if err != nil { + return {}, false + } + return path, true +} + +save_file_dialog :: proc( + title, default_path: string, + pattern_count: i32, + patterns: []string, + file_desc: string, + alloc := context.allocator, + temp_alloc := context.temp_allocator, +) -> ( + path: string, + success: bool, +) { + ctitle: cstring = nil + cdefault_path: cstring = nil + cfile_desc: cstring = nil + cpatterns: [^]cstring = nil + err: mem.Allocator_Error + + if len(title) > 0 { + ctitle, err = strings.clone_to_cstring(title, temp_alloc) + if err != nil { + return {}, false + } + } + if len(default_path) > 0 { + cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc) + if err != nil { + return {}, false + } + } + if len(cfile_desc) > 0 { + cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc) + if err != nil { + return {}, false + } + } + + if pattern_count > 0 { + cpatterns = make([^]cstring, pattern_count + 1, temp_alloc) + for p, i in patterns { + cpatterns[i] = strings.clone_to_cstring(p) + } + cpatterns[pattern_count] = nil // null terminate the array + } + res := saveFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc) + path, err = strings.clone_from_cstring(res, alloc) + if err != nil { + return {}, false + } + return path, true +} + +open_file_dialog :: proc( + title, default_path: string, + pattern_count: i32, + patterns: []string, + file_desc: string, + allow_multi: i32, + alloc := context.allocator, + temp_alloc := context.temp_allocator, +) -> ( + path: string, + success: bool, +) { + ctitle: cstring = nil + cdefault_path: cstring = nil + cfile_desc: cstring = nil + cpatterns: [^]cstring = nil + err: mem.Allocator_Error + + if len(title) > 0 { + ctitle, err = strings.clone_to_cstring(title, temp_alloc) + if err != nil { + return {}, false + } + } + if len(default_path) > 0 { + cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc) + if err != nil { + return {}, false + } + } + if len(cfile_desc) > 0 { + cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc) + if err != nil { + return {}, false + } + } + + if pattern_count > 0 { + cpatterns = make([^]cstring, pattern_count + 1, temp_alloc) + for p, i in patterns { + cpatterns[i] = strings.clone_to_cstring(p) + } + cpatterns[pattern_count] = nil // null terminate the array + } + res := openFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc, allow_multi) + path, err = strings.clone_from_cstring(res, alloc) + if err != nil { + return {}, false + } + return path, true +}