From 9ce6e4accd97a72fc1c3ca1196a504a9d1d83710 Mon Sep 17 00:00:00 2001 From: Peaceultime Date: Tue, 28 Apr 2026 21:05:43 +0200 Subject: [PATCH] Reorganized the code structure, fix a lot of issues. --- CLAUDE.md | 11 +- README.md | 30 +- src/api.h | 2 + src/camera.h | 57 ++ src/generated/shape.h | 22 +- src/generated/sprite.h | 57 +- src/history.h | 55 +- src/main.c | 1986 +++++++++++++++++++-------------------- src/rand.h | 15 +- src/render.h | 72 ++ src/shaders/shape.wgsl | 4 +- src/shaders/sprite.wgsl | 16 - src/shape.h | 250 +---- src/spatial.h | 9 +- 14 files changed, 1213 insertions(+), 1373 deletions(-) create mode 100644 src/camera.h create mode 100644 src/render.h diff --git a/CLAUDE.md b/CLAUDE.md index 11b0737..49bbc25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,7 @@ A browser-based world map creation tool (like Wonderdraft/Inkarnate). C99 compil - **UI:** Dear ImGui via cimgui — `lib/imgui/` - **Math:** cglm (types are C arrays: `vec2` = `float[2]`, `mat4` = `float[4][4]` column-major) — `lib/cglm/` - **Shaders:** WGSL in `src/shaders/`, compiled to C headers via `xxd -i` into `src/generated/` +- **Shapes:** Line-strip based vector shapes (circle, star) with procedural vertex generation ### Build - `make` (release) / `make debug` — outputs `app.html` @@ -16,9 +17,13 @@ A browser-based world map creation tool (like Wonderdraft/Inkarnate). C99 compil - Include paths: `lib/sokol`, `lib/imgui`, `lib/imgui/imgui`, `lib/util`, `lib/cglm/include` ### Key files -- `src/main.c` — entry point, sokol init, render loop, input (zoom/pan/drag) -- `src/api.h` — central include hub, backend defines -- `src/sprite.h` — sprite batching, texture manager, file import stubs +- `src/main.c` — entry point, sokol init, render loop, all input handling, overlay geometry, UI panels, and debug stats +- `src/api.h` — central include hub, backend defines, ALLOC/FREE macros (wired to smemtrack) +- `src/camera.h` — viewport state (zoom/pan), MVP matrix computation, screen↔world coordinate transforms +- `src/render.h` — shape pipeline init/shutdown, per-shape draw calls (shader uniform binding) +- `src/shape.h` — shape geometry types, procedural generation (circle/star), transform building, hit testing, buffer management +- `src/spatial.h` — spatial hash grid for accelerating hit tests and rect-selection queries +- `src/history.h` — undo/redo stack with property-level tracking (position/scale/rotation/color), edit session capture, batch operations - `src/util.h` — `vector_t` (dynamic array) and `mem_pool_t` (free-list pool), both stripe-based - `src/rand.h` — xorshift32 PRNG diff --git a/README.md b/README.md index 6d2ef06..f8e488d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,14 @@ # Cartograph -A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. Uses shape-based terrain generation with procedural detail. +A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. Uses line-strip vector shapes with procedural geometry. -## Features (planned) +## Features -- **Shapes** — the fundamental building block. Each shape has: - - **Height** — additive (raise terrain) or subtractive (lower terrain) - - **Biome** — determines the sampled texture applied to the shape - - **Intensity** — blending weight of the biome texture - - **Roughness** — edge noise amount (voronoi-like jagged edges at maximum) -- **Shape editing** — select, move, rotate, scale individual shapes -- **Groups** — combine shapes into groups for batch operations -- **Shake landmass** — procedurally decompose a shape into a more complex set of shapes -- **Viewport** — zoom and pan across the map canvas +- **Shapes** — procedural circles and stars with per-instance transforms (position, scale, rotation) +- **Shape editing** — select, move, rotate, scale individual shapes; rect-select multiple shapes +- **Undo/redo** — property-level history stack (position, scale, rotation, color) with edit session capture and batch operations +- **Spatial index** — hash grid for fast hit testing and rect-selection queries on large shape counts +- **Viewport** — zoom and pan with screen↔world coordinate transforms ## Tech stack @@ -44,12 +40,16 @@ Output is `app.html`, served by Emscripten's built-in web server or any static s ``` src/ - main.c Entry point, sokol init, render loop, input handling - api.h Central include — all library headers and project-wide defines - sprite.h Sprite batching, texture management, file import + main.c Entry point, sokol init, render loop, input handling, UI panels, debug stats + api.h Central include — all library headers, ALLOC/FREE macros + camera.h Viewport state, zoom/pan, MVP matrix, screen↔world transforms + render.h Shape pipeline init/shutdown, per-shape draw calls + shape.h Shape geometry types, procedural generation, hit testing, buffer management + spatial.h Spatial hash grid for accelerated hit tests and rect-select queries + history.h Undo/redo stack with property-level tracking and batch operations util.h Vector (dynamic array) and memory pool data structures rand.h Xorshift32 PRNG utilities - shaders/ WGSL shader sources + shaders/ WGSL shader sources (shape, sprite) generated/ xxd-generated C headers from shaders lib/ sokol/ Sokol single-file headers (gfx, app, glue, log) diff --git a/src/api.h b/src/api.h index fa3bdf7..1520c40 100644 --- a/src/api.h +++ b/src/api.h @@ -23,12 +23,14 @@ #include "cglm/cglm.h" #include "rand.h" +#include "camera.h" #include "generated/sprite.h" #include "generated/shape.h" #include "util.h" #include "shape.h" +#include "render.h" #include "spatial.h" #include "history.h" diff --git a/src/camera.h b/src/camera.h new file mode 100644 index 0000000..0df038e --- /dev/null +++ b/src/camera.h @@ -0,0 +1,57 @@ +#ifndef CAMERA_H +#define CAMERA_H + +#include "api.h" + +typedef struct { + bool dragging; + float origin_x, origin_y; +} pan_state_t; + +typedef struct { + int width, height; + float half_width, half_height; + vec2 pan; + float zoom; + float hover_tol; + pan_state_t pan_state; +} camera_t; + +static void compute_mvp(camera_t *cam, mat4 *mvp) +{ + const float w = (float)cam->width; + const float h = (float)cam->height; + const float z = cam->zoom; + const float px = cam->pan[0]; + const float py = cam->pan[1]; + + (*mvp)[0][0] = (2.0f / w) * z; + (*mvp)[0][1] = 0.0f; + (*mvp)[0][2] = 0.0f; + (*mvp)[0][3] = 0.0f; + + (*mvp)[1][0] = 0.0f; + (*mvp)[1][1] = (2.0f / h) * z; + (*mvp)[1][2] = 0.0f; + (*mvp)[1][3] = 0.0f; + + (*mvp)[2][0] = 0.0f; + (*mvp)[2][1] = 0.0f; + (*mvp)[2][2] = 0.0f; + (*mvp)[2][3] = 0.0f; + + (*mvp)[3][0] = (2.0f / w) * px; + (*mvp)[3][1] = (2.0f / h) * py; + (*mvp)[3][2] = 0.0f; + (*mvp)[3][3] = 1.0f; +} + +static void screen_to_world(camera_t *cam, float mx, float my, float *wx, float *wy) +{ + const float sx = mx - cam->half_width; + const float sy = cam->half_height - my; + *wx = (sx - cam->pan[0]) / cam->zoom; + *wy = (sy - cam->pan[1]) / cam->zoom; +} + +#endif diff --git a/src/generated/shape.h b/src/generated/shape.h index 11b3f6a..a0a0ac7 100644 --- a/src/generated/shape.h +++ b/src/generated/shape.h @@ -44,17 +44,17 @@ unsigned char src_shaders_shape_wgsl[] = { 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, 0x72, 0x6c, - 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, - 0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69, 0x6e, 0x70, 0x75, - 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x79, - 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x29, 0x20, - 0x2a, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, - 0x6f, 0x72, 0x6d, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, - 0x6d, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, - 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x77, 0x6f, 0x72, 0x6c, - 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x2a, 0x20, 0x76, 0x73, 0x5f, 0x75, - 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, 0x3b, + 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, 0x70, + 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x2a, 0x20, 0x76, 0x65, + 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69, 0x6e, + 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30, + 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x73, 0x5f, 0x75, + 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, 0x20, + 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, 0x20, diff --git a/src/generated/sprite.h b/src/generated/sprite.h index d7ac77d..912fb68 100644 --- a/src/generated/sprite.h +++ b/src/generated/sprite.h @@ -74,60 +74,7 @@ unsigned char src_shaders_sprite_wgsl[] = { 0x70, 0x75, 0x74, 0x2e, 0x75, 0x76, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x75, 0x76, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, - 0x70, 0x75, 0x74, 0x3b, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x2f, - 0x2f, 0x20, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x20, 0x61, 0x20, - 0x33, 0x32, 0x62, 0x69, 0x74, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x63, - 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x28, 0x68, 0x65, 0x78, 0x20, 0x72, 0x65, - 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x29, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x6e, 0x6f, 0x72, - 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x76, 0x65, 0x63, 0x34, - 0x66, 0x0d, 0x0a, 0x2f, 0x2a, 0x66, 0x6e, 0x20, 0x63, 0x6f, 0x6e, 0x76, - 0x65, 0x72, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x28, 0x69, 0x6e, 0x70, - 0x75, 0x74, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x29, 0x20, 0x2d, 0x3e, 0x20, - 0x76, 0x65, 0x63, 0x34, 0x66, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, - 0x20, 0x6c, 0x65, 0x74, 0x20, 0x72, 0x3a, 0x20, 0x66, 0x33, 0x32, 0x20, - 0x3d, 0x20, 0x66, 0x33, 0x32, 0x28, 0x28, 0x28, 0x69, 0x6e, 0x70, 0x75, - 0x74, 0x20, 0x3e, 0x3e, 0x20, 0x20, 0x30, 0x29, 0x20, 0x26, 0x20, 0x30, - 0x78, 0x66, 0x66, 0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, - 0x6c, 0x65, 0x74, 0x20, 0x67, 0x3a, 0x20, 0x66, 0x33, 0x32, 0x20, 0x3d, - 0x20, 0x66, 0x33, 0x32, 0x28, 0x28, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, - 0x20, 0x3e, 0x3e, 0x20, 0x20, 0x38, 0x29, 0x20, 0x26, 0x20, 0x30, 0x78, - 0x66, 0x66, 0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, - 0x65, 0x74, 0x20, 0x62, 0x3a, 0x20, 0x66, 0x33, 0x32, 0x20, 0x3d, 0x20, - 0x66, 0x33, 0x32, 0x28, 0x28, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, - 0x3e, 0x3e, 0x20, 0x31, 0x36, 0x29, 0x20, 0x26, 0x20, 0x30, 0x78, 0x66, - 0x66, 0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, - 0x74, 0x20, 0x61, 0x3a, 0x20, 0x66, 0x33, 0x32, 0x20, 0x3d, 0x20, 0x66, - 0x33, 0x32, 0x28, 0x28, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x3e, - 0x3e, 0x20, 0x32, 0x34, 0x29, 0x20, 0x26, 0x20, 0x30, 0x78, 0x66, 0x66, - 0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, - 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, - 0x72, 0x20, 0x2f, 0x20, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x67, 0x20, 0x2f, - 0x20, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x62, 0x20, 0x2f, 0x20, 0x32, 0x35, - 0x35, 0x2c, 0x20, 0x61, 0x20, 0x2f, 0x20, 0x32, 0x35, 0x35, 0x29, 0x3b, - 0x0d, 0x0a, 0x7d, 0x2a, 0x2f, 0x0d, 0x0a, 0x2f, 0x2f, 0x20, 0x47, 0x65, - 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x65, 0x78, 0x74, 0x75, 0x72, - 0x65, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79, 0x20, 0x69, 0x6e, 0x64, 0x65, - 0x78, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x55, - 0x56, 0x0d, 0x0a, 0x2f, 0x2a, 0x66, 0x6e, 0x20, 0x69, 0x6e, 0x64, 0x65, - 0x78, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x28, 0x75, - 0x76, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66, 0x2c, 0x20, 0x77, 0x69, - 0x64, 0x74, 0x68, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c, 0x20, 0x68, 0x65, - 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x29, 0x20, 0x2d, - 0x3e, 0x20, 0x75, 0x33, 0x32, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, - 0x20, 0x6c, 0x65, 0x74, 0x20, 0x78, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x20, - 0x3d, 0x20, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x28, 0x66, 0x6c, 0x6f, 0x6f, - 0x72, 0x28, 0x75, 0x76, 0x2e, 0x78, 0x20, 0x2a, 0x20, 0x66, 0x33, 0x32, - 0x28, 0x77, 0x69, 0x64, 0x74, 0x68, 0x29, 0x29, 0x2c, 0x20, 0x30, 0x2c, - 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, - 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x79, 0x3a, 0x20, 0x75, 0x33, 0x32, - 0x20, 0x3d, 0x20, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x28, 0x66, 0x6c, 0x6f, - 0x6f, 0x72, 0x28, 0x75, 0x76, 0x2e, 0x79, 0x20, 0x2a, 0x20, 0x66, 0x33, - 0x32, 0x28, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x29, 0x29, 0x2c, 0x20, - 0x30, 0x2c, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x29, 0x3b, 0x0d, - 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, - 0x79, 0x20, 0x2a, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x20, 0x2b, 0x20, - 0x78, 0x3b, 0x0d, 0x0a, 0x7d, 0x2a, 0x2f, 0x0d, 0x0a, 0x0d, 0x0a, 0x40, + 0x70, 0x75, 0x74, 0x3b, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x40, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20, 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, 0x3e, @@ -142,4 +89,4 @@ unsigned char src_shaders_sprite_wgsl[] = { 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0d, 0x0a, 0x7d }; -unsigned int src_shaders_sprite_wgsl_len = 1695; +unsigned int src_shaders_sprite_wgsl_len = 1059; diff --git a/src/history.h b/src/history.h index 43bacf5..85a2823 100644 --- a/src/history.h +++ b/src/history.h @@ -75,7 +75,6 @@ static void hist_apply_prop(shape_t *s, hist_prop_t prop, const float val[4]) { } } -// -- history API -- /** * Zero-initialize the history stack. Call once during app init. @@ -206,7 +205,37 @@ static void history_end_edit(history_t *h, vector_t *shapes) { * @param shapes the shapes vector to modify * @param forward true to use new_val (redo), false to use old_val (undo) */ -static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forward) { +// -- batch API for multi-shape operations (move, rotate, resize) -- + +typedef struct { + hist_change_t *changes; + int count; + int capacity; +} hist_batch_t; + +static void history_batch_init(hist_batch_t *batch, int count) { + batch->changes = (hist_change_t*) ALLOC((size_t)count * sizeof(hist_change_t)); + batch->count = 0; + batch->capacity = count; +} + +static void history_batch_add(hist_batch_t *batch, int shape_index, hist_prop_t prop, + const float old_val[4], const float new_val[4]) { + hist_change_t *c = &batch->changes[batch->count++]; + c->shape_index = shape_index; + c->prop = prop; + memcpy(c->old_val, old_val, sizeof(float[4])); + memcpy(c->new_val, new_val, sizeof(float[4])); +} + +static void history_batch_commit(hist_batch_t *batch, history_t *h) { + hist_entry_t entry = { .changes = batch->changes, .count = batch->count }; + history_push_entry(h, entry); +} + +/** + * Apply every change in an entry to the shapes vector and regenerate buffers. + */static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forward) { for (int i = 0; i < entry->count; i++) { hist_change_t *c = &entry->changes[i]; if (c->shape_index >= shapes->count) continue; @@ -217,37 +246,19 @@ static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forw } } -/** - * Undo the most recent history entry. - * - * @param h history stack - * @param shapes the shapes vector to revert - * @param selected_count out-parameter for updated selection count (currently passed through) - * @return true if state was changed, false if nothing to undo - */ -static bool history_undo(history_t *h, vector_t *shapes, int *selected_count) { +static bool history_undo(history_t *h, vector_t *shapes) { if (h->current < 0) return false; history_apply_entry(&h->entries[h->current], shapes, false); h->current--; - (void)selected_count; return true; } -/** - * Redo the next history entry. - * - * @param h history stack - * @param shapes the shapes vector to advance - * @param selected_count out-parameter (currently passed through) - * @return true if state was changed, false if nothing to redo - */ -static bool history_redo(history_t *h, vector_t *shapes, int *selected_count) { +static bool history_redo(history_t *h, vector_t *shapes) { if (h->current + 1 >= h->count) return false; h->current++; history_apply_entry(&h->entries[h->current], shapes, true); - (void)selected_count; return true; } diff --git a/src/main.c b/src/main.c index 3611dd1..57807c5 100644 --- a/src/main.c +++ b/src/main.c @@ -11,21 +11,16 @@ typedef struct log_entry_t { uint32_t level; } log_entry_t; -typedef struct vs_uniform_t { +typedef struct { mat4 mvp; } uniform_t; typedef struct renderer_t { - sg_pipeline pipeline; //Configured sprite pipeline - sg_pass_action clear_pass; //Render pass - Clear screen - uniform_t uniform; //Uniform data + sg_pipeline pipeline; + sg_pass_action clear_pass; + uniform_t uniform; } renderer_t; -typedef struct dragger_t { - bool dragging; - float origin_x, origin_y; -} dragger_t; - typedef struct { int idx; float init_sx, init_sy, init_cx, init_cy; @@ -33,18 +28,9 @@ typedef struct { float lpi_x, lpi_y; } resize_init_t; -typedef struct userdata_t { - int width, height; - float half_width, half_height; - vec2 pan; - float zoom; - dragger_t dragger; - renderer_t renderer; - vector_t shapes; - spatial_grid_t spatial_grid; +typedef struct { int selected_count; int hovered_shape; - float hover_tol; bool selecting; float sel_sx, sel_sy; @@ -52,8 +38,6 @@ typedef struct userdata_t { bool sel_dragging; int sel_clicked_shape; - float panel_w; - bool move_dragging; float move_start_wx, move_start_wy; float move_total_dx, move_total_dy; @@ -73,111 +57,69 @@ typedef struct userdata_t { resize_init_t *resize_init; int resize_init_count; - sg_buffer rect_vbuf, rect_ibuf; - sg_buffer handle_vbuf, handle_ibuf; - sg_buffer corner_vbuf, corner_ibuf; - - history_t history; - - bool overlay_upload_needed; - float cached_aabb[4]; bool aabb_cached; +} interact_state_t; +typedef struct { float fps_immediate; float fps_average; float frame_times[60]; int frame_time_head; int frame_time_count; float frame_time_sum; +} debug_stats_t; +typedef struct { + float panel_w; log_entry_t log_ring[LOG_RING_SIZE]; int log_head; int log_count; bool log_show; +} ui_state_t; + +typedef struct userdata_t { + camera_t camera; + renderer_t renderer; + vector_t shapes; + spatial_grid_t spatial_grid; + interact_state_t interact; + history_t history; + debug_stats_t debug; + ui_state_t ui; + bool overlay_upload_needed; + sg_buffer rect_vbuf, rect_ibuf; + sg_buffer handle_vbuf, handle_ibuf; + sg_buffer corner_vbuf, corner_ibuf; } userdata_t; -/** - * Format a string into a static buffer. Only valid until the next call. - * - * @param format printf-style format string - * @param ... variadic arguments - * @return pointer to internal static buffer (single-use) - */ -const char* format(const char* format, ...) -{ - static char buffer[_SLOG_LINE_LENGTH]; +// -- forward declarations for the extraction helpers -- - va_list va; - va_start(va, format); +static void selected_aabb(userdata_t *ud, float *min_x, float *min_y, + float *max_x, float *max_y); +static void meter_fps(userdata_t *ud); +static void compute_overlay_geometry(userdata_t *ud, + shape_vertex_t overlay_verts[5], + float *sel_cx, float *sel_cy, float *sel_hw, float *sel_hh, float *sel_angle, + bool *has_overlay, bool *show_handle); +static void upload_overlay_buffers(userdata_t *ud, + const shape_vertex_t overlay_verts[5], + float sel_cx, float sel_cy, float sel_hw, float sel_hh, float sel_angle, + bool has_overlay, bool show_handle); +static void draw_shapes(userdata_t *ud); +static void draw_overlay_and_handles(userdata_t *ud, + bool has_overlay, bool show_handle); +static void draw_properties_panel(userdata_t *ud); +static void draw_log_panel(userdata_t *ud); +static bool handle_key_down(userdata_t *ud, const sapp_event *event); +static void handle_resize(userdata_t *ud, const sapp_event *event); +static void handle_mouse_down(userdata_t *ud, const sapp_event *event); +static void handle_mouse_up(userdata_t *ud, const sapp_event *event); +static void handle_mouse_move(userdata_t *ud, const sapp_event *event); +static void handle_scroll_zoom(userdata_t *ud, const sapp_event *event); - int size = vsnprintf(buffer, _SLOG_LINE_LENGTH, format, va); +// -- aabb helper -- - va_end(va); - - return buffer; -} -/** - * Recompute the Model-View-Projection matrix from current pan and zoom. - * - * @param userdata application state - */ -void compute_mvp(userdata_t *userdata) -{ - const float w = (float)userdata->width; - const float h = (float)userdata->height; - const float z = userdata->zoom; - const float px = userdata->pan[0]; - const float py = userdata->pan[1]; - mat4 *m = &userdata->renderer.uniform.mvp; - - (*m)[0][0] = (2.0f / w) * z; - (*m)[0][1] = 0.0f; - (*m)[0][2] = 0.0f; - (*m)[0][3] = (2.0f / w) * px; - - (*m)[1][0] = 0.0f; - (*m)[1][1] = (2.0f / h) * z; - (*m)[1][2] = 0.0f; - (*m)[1][3] = (2.0f / h) * py; - - (*m)[2][0] = 0.0f; - (*m)[2][1] = 0.0f; - (*m)[2][2] = 0.0f; - (*m)[2][3] = 0.0f; - - (*m)[3][0] = 0.0f; - (*m)[3][1] = 0.0f; - (*m)[3][2] = 0.0f; - (*m)[3][3] = 1.0f; -} -/** - * Convert screen-space mouse coordinates to world-space coordinates. - * - * @param ud application state - * @param mx mouse X in screen pixels - * @param my mouse Y in screen pixels - * @param wx receives world X - * @param wy receives world Y - */ -static void screen_to_world(userdata_t *ud, float mx, float my, float *wx, float *wy) -{ - const float sx = mx - ud->half_width; - const float sy = ud->half_height - my; - *wx = (sx - ud->pan[0]) / ud->zoom; - *wy = (sy - ud->pan[1]) / ud->zoom; -} - -/** - * Compute the axis-aligned bounding box of all selected shapes. - * If no shapes are selected, min/max are unchanged. - * - * @param ud application state - * @param min_x receives minimum X in world space - * @param min_y receives minimum Y in world space - * @param max_x receives maximum X in world space - * @param max_y receives maximum Y in world space - */ static void selected_aabb(userdata_t *ud, float *min_x, float *min_y, float *max_x, float *max_y) { @@ -202,18 +144,8 @@ static void selected_aabb(userdata_t *ud, float *min_x, float *min_y, } } -/** - * Sokol log callback. Formats log entries into the in-app ring buffer and - * prints them to stderr. Errors and panics auto-open the log panel. - * - * @param tag log tag - * @param log_level 0=panic, 1=error, 2=warn, 3=info - * @param log_item log item id - * @param message log message string - * @param line_nr source line number - * @param filename source file name - * @param user_data pointer to userdata_t - */ +// -- log capture -- + static void log_capture(const char* tag, uint32_t log_level, uint32_t log_item, const char* message, uint32_t line_nr, const char* filename, void* user_data) @@ -237,60 +169,51 @@ static void log_capture(const char* tag, uint32_t log_level, uint32_t log_item, snprintf(buf + n, sizeof(buf) - n, " %s", message); } - int idx = ud->log_head; - strncpy(ud->log_ring[idx].text, buf, 255); - ud->log_ring[idx].text[255] = 0; - ud->log_ring[idx].level = log_level; - ud->log_head = (idx + 1) % LOG_RING_SIZE; - if (ud->log_count < LOG_RING_SIZE) ud->log_count++; + int idx = ud->ui.log_head; + strncpy(ud->ui.log_ring[idx].text, buf, 255); + ud->ui.log_ring[idx].text[255] = 0; + ud->ui.log_ring[idx].level = log_level; + ud->ui.log_head = (idx + 1) % LOG_RING_SIZE; + if (ud->ui.log_count < LOG_RING_SIZE) ud->ui.log_count++; fprintf(stderr, "%s\n", buf); - if (log_level <= 1) ud->log_show = true; + if (log_level <= 1) ud->ui.log_show = true; } -/** - * Per-frame render callback. Draws all shapes, the selection overlay, and the - * ImGui UI (properties panel and log panel). - * - * @param _userdata pointer to userdata_t - */ -static void frame(void* _userdata) +// -- frame helpers -- + +static void meter_fps(userdata_t *ud) { - userdata_t* userdata = (userdata_t*) _userdata; - shape_begin_frame(); + float dt = (float)sapp_frame_duration(); + float instant_fps = dt > 0.0001f ? 1.0f / dt : 0.0f; + ud->debug.fps_immediate += (instant_fps - ud->debug.fps_immediate) * 0.1f; - { - float dt = (float)sapp_frame_duration(); - float instant_fps = dt > 0.0001f ? 1.0f / dt : 0.0f; - userdata->fps_immediate += (instant_fps - userdata->fps_immediate) * 0.1f; - - int idx = userdata->frame_time_head; - if (userdata->frame_time_count == 60) { - userdata->frame_time_sum -= userdata->frame_times[idx]; - } else { - userdata->frame_time_count++; - } - userdata->frame_times[idx] = dt; - userdata->frame_time_sum += dt; - userdata->frame_time_head = (idx + 1) % 60; - userdata->fps_average = userdata->frame_time_sum > 0.0001f - ? (float)userdata->frame_time_count / userdata->frame_time_sum : 0.0f; + int idx = ud->debug.frame_time_head; + if (ud->debug.frame_time_count == 60) { + ud->debug.frame_time_sum -= ud->debug.frame_times[idx]; + } else { + ud->debug.frame_time_count++; } + ud->debug.frame_times[idx] = dt; + ud->debug.frame_time_sum += dt; + ud->debug.frame_time_head = (idx + 1) % 60; + ud->debug.fps_average = ud->debug.frame_time_sum > 0.0001f + ? (float)ud->debug.frame_time_count / ud->debug.frame_time_sum : 0.0f; +} - spatial_rebuild(&userdata->spatial_grid, &userdata->shapes); +static void compute_overlay_geometry(userdata_t *ud, + shape_vertex_t overlay_verts[5], + float *sel_cx, float *sel_cy, float *sel_hw, float *sel_hh, float *sel_angle, + bool *has_overlay, bool *show_handle) +{ + *has_overlay = false; + *sel_cx = *sel_cy = *sel_angle = 0; + *sel_hw = *sel_hh = 0; - // -- pre-pass: compute overlay/handle data and update GPU buffers -- - - bool has_overlay = false; - shape_vertex_t overlay_verts[5]; - - float sel_cx = 0, sel_cy = 0, sel_angle = 0; - float sel_hw = 0, sel_hh = 0; - - if (userdata->selecting && userdata->sel_dragging) { + if (ud->interact.selecting && ud->interact.sel_dragging) { float wx1, wy1, wx2, wy2; - screen_to_world(userdata, userdata->sel_sx, userdata->sel_sy, &wx1, &wy1); - screen_to_world(userdata, userdata->sel_cx, userdata->sel_cy, &wx2, &wy2); + screen_to_world(&ud->camera, ud->interact.sel_sx, ud->interact.sel_sy, &wx1, &wy1); + screen_to_world(&ud->camera, ud->interact.sel_cx, ud->interact.sel_cy, &wx2, &wy2); float x1 = fminf(wx1, wx2), y1 = fminf(wy1, wy2); float x2 = fmaxf(wx1, wx2), y2 = fmaxf(wy1, wy2); overlay_verts[0] = (shape_vertex_t){x1, y1}; @@ -298,20 +221,20 @@ static void frame(void* _userdata) overlay_verts[2] = (shape_vertex_t){x2, y2}; overlay_verts[3] = (shape_vertex_t){x1, y2}; overlay_verts[4] = (shape_vertex_t){x1, y1}; - has_overlay = true; - } else if (userdata->selected_count >= 1) { - if (userdata->selected_count == 1) { - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + *has_overlay = true; + } else if (ud->interact.selected_count >= 1) { + if (ud->interact.selected_count == 1) { + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); if (!s->selected) continue; - sel_cx = s->cx; sel_cy = s->cy; - sel_hw = s->sx; sel_hh = s->sy; - sel_angle = s->rotation; + *sel_cx = s->cx; *sel_cy = s->cy; + *sel_hw = s->sx; *sel_hh = s->sy; + *sel_angle = s->rotation; float x1, y1, x2, y2; - selected_aabb(userdata, &x1, &y1, &x2, &y2); - userdata->cached_aabb[0] = x1; userdata->cached_aabb[1] = y1; - userdata->cached_aabb[2] = x2; userdata->cached_aabb[3] = y2; - userdata->aabb_cached = true; + selected_aabb(ud, &x1, &y1, &x2, &y2); + ud->interact.cached_aabb[0] = x1; ud->interact.cached_aabb[1] = y1; + ud->interact.cached_aabb[2] = x2; ud->interact.cached_aabb[3] = y2; + ud->interact.aabb_cached = true; overlay_verts[0] = (shape_vertex_t){x1, y1}; overlay_verts[1] = (shape_vertex_t){x2, y1}; overlay_verts[2] = (shape_vertex_t){x2, y2}; @@ -322,24 +245,24 @@ static void frame(void* _userdata) } else { float omin[2], omax[2]; float sum_sin = 0, sum_cos = 0; - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); if (!s->selected) continue; sum_sin += sinf(s->rotation); sum_cos += cosf(s->rotation); } - selected_aabb(userdata, &omin[0], &omin[1], &omax[0], &omax[1]); - userdata->cached_aabb[0] = omin[0]; userdata->cached_aabb[1] = omin[1]; - userdata->cached_aabb[2] = omax[0]; userdata->cached_aabb[3] = omax[1]; - userdata->aabb_cached = true; - float pad = 8.0f / userdata->zoom; + selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); + ud->interact.cached_aabb[0] = omin[0]; ud->interact.cached_aabb[1] = omin[1]; + ud->interact.cached_aabb[2] = omax[0]; ud->interact.cached_aabb[3] = omax[1]; + ud->interact.aabb_cached = true; + float pad = 8.0f / ud->camera.zoom; omin[0] -= pad; omin[1] -= pad; omax[0] += pad; omax[1] += pad; - sel_cx = (omin[0] + omax[0]) * 0.5f; - sel_cy = (omin[1] + omax[1]) * 0.5f; - sel_hw = (omax[0] - omin[0]) * 0.5f; - sel_hh = (omax[1] - omin[1]) * 0.5f; - sel_angle = atan2f(sum_sin, sum_cos); + *sel_cx = (omin[0] + omax[0]) * 0.5f; + *sel_cy = (omin[1] + omax[1]) * 0.5f; + *sel_hw = (omax[0] - omin[0]) * 0.5f; + *sel_hh = (omax[1] - omin[1]) * 0.5f; + *sel_angle = atan2f(sum_sin, sum_cos); overlay_verts[0] = (shape_vertex_t){omin[0], omin[1]}; overlay_verts[1] = (shape_vertex_t){omax[0], omin[1]}; @@ -347,25 +270,32 @@ static void frame(void* _userdata) overlay_verts[3] = (shape_vertex_t){omin[0], omax[1]}; overlay_verts[4] = overlay_verts[0]; } - has_overlay = true; + *has_overlay = true; } - bool need_upload = userdata->overlay_upload_needed || - userdata->move_dragging || userdata->rotate_dragging || - userdata->resize_dragging || userdata->selecting; + *show_handle = ud->interact.selected_count > 0 && !ud->interact.selecting; +} + +static void upload_overlay_buffers(userdata_t *ud, + const shape_vertex_t overlay_verts[5], + float sel_cx, float sel_cy, float sel_hw, float sel_hh, float sel_angle, + bool has_overlay, bool show_handle) +{ + bool need_upload = ud->overlay_upload_needed || + ud->interact.move_dragging || ud->interact.rotate_dragging || + ud->interact.resize_dragging || ud->interact.selecting; if (has_overlay && need_upload) { - sg_update_buffer(userdata->rect_vbuf, &(sg_range){overlay_verts, sizeof(overlay_verts)}); + sg_update_buffer(ud->rect_vbuf, &(sg_range){overlay_verts, (size_t)5 * sizeof(shape_vertex_t)}); } - bool show_handle = userdata->selected_count > 0 && !userdata->selecting; if (show_handle) { - float pad = HANDLE_OFFSET_PX / userdata->zoom; + float pad = HANDLE_OFFSET_PX / ud->camera.zoom; float radius = sqrtf(sel_hw * sel_hw + sel_hh * sel_hh) + pad; - userdata->rotate_center_x = sel_cx; - userdata->rotate_center_y = sel_cy; - userdata->handle_radius = radius; + ud->interact.rotate_center_x = sel_cx; + ud->interact.rotate_center_y = sel_cy; + ud->interact.handle_radius = radius; const int n = HANDLE_CIRCLE_SEGMENTS + 1; shape_vertex_t hv[HANDLE_CIRCLE_SEGMENTS + 1]; @@ -374,22 +304,21 @@ static void frame(void* _userdata) hv[i] = (shape_vertex_t){sel_cx + cosf(a) * radius, sel_cy + sinf(a) * radius}; } if (need_upload) - sg_update_buffer(userdata->handle_vbuf, &(sg_range){hv, sizeof(hv)}); + sg_update_buffer(ud->handle_vbuf, &(sg_range){hv, sizeof(hv)}); - // resize handles: 4 corners + 4 edge midpoints { - float hs = CORNER_SIZE_PX / userdata->zoom * 0.5f; + float hs = CORNER_SIZE_PX / ud->camera.zoom * 0.5f; float mid_x = (overlay_verts[0].x + overlay_verts[1].x) * 0.5f; float mid_y = (overlay_verts[0].y + overlay_verts[2].y) * 0.5f; float handles[8][2] = { - {overlay_verts[0].x, overlay_verts[0].y}, // bottom-left corner - {mid_x, overlay_verts[0].y}, // bottom edge - {overlay_verts[1].x, overlay_verts[1].y}, // bottom-right corner - {overlay_verts[1].x, mid_y }, // right edge - {overlay_verts[2].x, overlay_verts[2].y}, // top-right corner - {mid_x, overlay_verts[2].y}, // top edge - {overlay_verts[3].x, overlay_verts[3].y}, // top-left corner - {overlay_verts[3].x, mid_y }, // left edge + {overlay_verts[0].x, overlay_verts[0].y}, + {mid_x, overlay_verts[0].y}, + {overlay_verts[1].x, overlay_verts[1].y}, + {overlay_verts[1].x, mid_y }, + {overlay_verts[2].x, overlay_verts[2].y}, + {mid_x, overlay_verts[2].y}, + {overlay_verts[3].x, overlay_verts[3].y}, + {overlay_verts[3].x, mid_y }, }; shape_vertex_t cv[40]; for (int h = 0; h < 8; h++) { @@ -401,23 +330,23 @@ static void frame(void* _userdata) cv[h*5+4] = (shape_vertex_t){cx - hs, cy - hs}; } if (need_upload) - sg_update_buffer(userdata->corner_vbuf, &(sg_range){cv, sizeof(cv)}); + sg_update_buffer(ud->corner_vbuf, &(sg_range){cv, sizeof(cv)}); } } - userdata->overlay_upload_needed = false; - // -- render pass -- - - sg_begin_pass(&(sg_pass){ - .action = userdata->renderer.clear_pass, - .swapchain = sglue_swapchain(), - }); + ud->overlay_upload_needed = false; +} +static void draw_shapes(userdata_t *ud) +{ sg_apply_pipeline(shape_pipeline); - for (int i = 0; i < userdata->shapes.count; i++) { - shape_draw((shape_t*) vec_get(&userdata->shapes, i), &userdata->renderer.uniform.mvp); + for (int i = 0; i < ud->shapes.count; i++) { + shape_draw((shape_t*) vec_get(&ud->shapes, i), &ud->renderer.uniform.mvp); } +} +static void draw_overlay_and_handles(userdata_t *ud, bool has_overlay, bool show_handle) +{ if (has_overlay) { shape_uniform_t u; glm_mat4_identity(u.transform); @@ -426,11 +355,11 @@ static void frame(void* _userdata) u.state = 0; memset(u._pad, 0, sizeof(u._pad)); - sg_apply_uniforms(0, &SG_RANGE(userdata->renderer.uniform.mvp)); + sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp)); sg_apply_uniforms(1, &SG_RANGE(u)); sg_apply_bindings(&(sg_bindings){ - .vertex_buffers[0] = userdata->rect_vbuf, - .index_buffer = userdata->rect_ibuf, + .vertex_buffers[0] = ud->rect_vbuf, + .index_buffer = ud->rect_ibuf, }); sg_draw(0, 5, 1); } @@ -443,15 +372,14 @@ static void frame(void* _userdata) hu.state = 0; memset(hu._pad, 0, sizeof(hu._pad)); - sg_apply_uniforms(0, &SG_RANGE(userdata->renderer.uniform.mvp)); + sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp)); sg_apply_uniforms(1, &SG_RANGE(hu)); sg_apply_bindings(&(sg_bindings){ - .vertex_buffers[0] = userdata->handle_vbuf, - .index_buffer = userdata->handle_ibuf, + .vertex_buffers[0] = ud->handle_vbuf, + .index_buffer = ud->handle_ibuf, }); sg_draw(0, HANDLE_CIRCLE_SEGMENTS + 1, 1); - // corner resize handles { shape_uniform_t cu; glm_mat4_identity(cu.transform); @@ -460,15 +388,176 @@ static void frame(void* _userdata) cu.state = 0; memset(cu._pad, 0, sizeof(cu._pad)); - sg_apply_uniforms(0, &SG_RANGE(userdata->renderer.uniform.mvp)); + sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp)); sg_apply_uniforms(1, &SG_RANGE(cu)); sg_apply_bindings(&(sg_bindings){ - .vertex_buffers[0] = userdata->corner_vbuf, - .index_buffer = userdata->corner_ibuf, + .vertex_buffers[0] = ud->corner_vbuf, + .index_buffer = ud->corner_ibuf, }); for (int h = 0; h < 8; h++) sg_draw(h * 5, 5, 1); } } +} + +static void draw_properties_panel(userdata_t *ud) +{ + igSetNextWindowPos((ImVec2){ud->camera.width - ud->ui.panel_w, 20.0f}, ImGuiCond_Always, (ImVec2){0, 0}); + igSetNextWindowSize((ImVec2){ud->ui.panel_w, ud->camera.height - 20.0f}, ImGuiCond_Always); + igBegin("Properties", NULL, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + if (ud->interact.selected_count == 0) { + igText("No shape selected"); + } else if (ud->interact.selected_count > 1) { + igText("%d shapes selected", ud->interact.selected_count); + } else { + int idx = 0; + while (idx < ud->shapes.count) { + shape_t *tmp = (shape_t*) vec_get(&ud->shapes, idx); + if (tmp->selected) break; + idx++; + } + shape_t *s = (shape_t*) vec_get(&ud->shapes, idx); + bool changed = false; + + changed |= igDragFloat2("Position", &s->cx, 1.0f, 0, 0, "%.1f", 0); + if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_POSITION); + changed |= igDragFloat2("Scale", &s->sx, 1.0f, 0.1f, 0, "%.1f", 0); + if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_SCALE); + changed |= igDragFloat("Rotation", &s->rotation, 0.01f, 0, 0, "%.3f", 0); + if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_ROTATION); + if (igColorEdit4("Color", s->uniform.base_color, 0)) + changed = true; + if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_COLOR); + + if (changed) { shape_regenerate(s); spatial_mark_dirty(&ud->spatial_grid); ud->overlay_upload_needed = true; } + + igSeparator(); + { + mat4 *m = &s->uniform.transform; + float sc = cosf(s->rotation), ss = sinf(s->rotation); + float lx0 = s->verts[0].x * s->sx; + float ly0 = s->verts[0].y * s->sy; + float wx = s->cx + lx0 * sc - ly0 * ss; + float wy = s->cy + lx0 * ss + ly0 * sc; + igText("Debug - Transform Matrix:"); + igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[0][0], (*m)[0][1], (*m)[0][2], (*m)[0][3]); + igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[1][0], (*m)[1][1], (*m)[1][2], (*m)[1][3]); + igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[2][0], (*m)[2][1], (*m)[2][2], (*m)[2][3]); + igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[3][0], (*m)[3][1], (*m)[3][2], (*m)[3][3]); + igSpacing(); + igText("Local vert[0]: (%.1f, %.1f)", s->verts[0].x, s->verts[0].y); + igText("World vert[0]: (%.1f, %.1f)", wx, wy); + igText("cx=%.1f cy=%.1f sx=%.1f sy=%.1f rot=%.3f", s->cx, s->cy, s->sx, s->sy, s->rotation); + char dbg[512]; + snprintf(dbg, sizeof(dbg), + "Transform Matrix:\n" + "[%+.3f %+.3f %+.3f %+.3f]\n" + "[%+.3f %+.3f %+.3f %+.3f]\n" + "[%+.3f %+.3f %+.3f %+.3f]\n" + "[%+.3f %+.3f %+.3f %+.3f]\n" + "\nLocal vert[0]: (%.1f, %.1f)\n" + "World vert[0]: (%.1f, %.1f)\n" + "cx=%.1f cy=%.1f sx=%.1f sy=%.1f rot=%.3f", + (*m)[0][0], (*m)[0][1], (*m)[0][2], (*m)[0][3], + (*m)[1][0], (*m)[1][1], (*m)[1][2], (*m)[1][3], + (*m)[2][0], (*m)[2][1], (*m)[2][2], (*m)[2][3], + (*m)[3][0], (*m)[3][1], (*m)[3][2], (*m)[3][3], + s->verts[0].x, s->verts[0].y, wx, wy, + s->cx, s->cy, s->sx, s->sy, s->rotation); + if (igButton("Copy Debug", (ImVec2){0, 0})) + sapp_set_clipboard_string(dbg); + } + } + + igEnd(); + + if (ud->history.capturing && !igIsAnyItemActive()) { + history_end_edit(&ud->history, &ud->shapes); + } +} + +static void draw_log_panel(userdata_t *ud) +{ + if (!ud->ui.log_show) return; + + igSetNextWindowPos((ImVec2){10.0f, ud->camera.height - 200.0f}, ImGuiCond_FirstUseEver, (ImVec2){0, 0}); + igSetNextWindowSize((ImVec2){400.0f, 180.0f}, ImGuiCond_FirstUseEver); + igBegin("Log", &ud->ui.log_show, 0); + if (igButton("Clear", (ImVec2){0, 0})) { + ud->ui.log_head = 0; + ud->ui.log_count = 0; + } + igSameLine(0.0f, 10.0f); + if (igButton("Copy", (ImVec2){0, 0})) { + int total = ud->ui.log_count < LOG_RING_SIZE ? ud->ui.log_count : LOG_RING_SIZE; + int start = ud->ui.log_count < LOG_RING_SIZE ? 0 : ud->ui.log_head; + int cap = total * 260; + char *buf = (char*) ALLOC((size_t)cap); + int off = 0; + for (int i = 0; i < total; i++) { + int idx = (start + i) % LOG_RING_SIZE; + off += snprintf(buf + off, (size_t)(cap - off), "%s\n", ud->ui.log_ring[idx].text); + } + igSetClipboardText(buf); + FREE(buf); + } + igSameLine(0.0f, 10.0f); + igText("%d entries", ud->ui.log_count); + igSameLine(0.0f, 10.0f); + igText("FPS: %.0f (avg: %.0f)", ud->debug.fps_immediate, ud->debug.fps_average); + igSameLine(0.0f, 10.0f); + igText("%.3fms", sapp_frame_duration() * 1000); + igSeparator(); + + igBeginChild_Str("LogScroll", (ImVec2){0, 0}, false, 0); + int total = ud->ui.log_count < LOG_RING_SIZE ? ud->ui.log_count : LOG_RING_SIZE; + int start = ud->ui.log_count < LOG_RING_SIZE ? 0 : ud->ui.log_head; + for (int i = 0; i < total; i++) { + int idx = (start + i) % LOG_RING_SIZE; + log_entry_t *e = &ud->ui.log_ring[idx]; + ImVec4 color; + switch (e->level) { + case 0: color = (ImVec4){1.0f, 0.3f, 0.3f, 1.0f}; break; + case 1: color = (ImVec4){1.0f, 0.5f, 0.3f, 1.0f}; break; + case 2: color = (ImVec4){1.0f, 0.9f, 0.3f, 1.0f}; break; + default:color = (ImVec4){0.7f, 0.7f, 0.7f, 1.0f}; break; + } + igPushStyleColor_Vec4(ImGuiCol_Text, color); + igTextUnformatted(e->text, NULL); + igPopStyleColor(1); + } + if (total > 0) igSetScrollHereY(1.0f); + igEndChild(); + igEnd(); +} + +// -- frame -- + +static void frame(void* _userdata) +{ + userdata_t* ud = (userdata_t*) _userdata; + shape_begin_frame(); + + meter_fps(ud); + spatial_rebuild(&ud->spatial_grid, &ud->shapes); + + float sel_cx, sel_cy, sel_hw, sel_hh, sel_angle; + shape_vertex_t overlay_verts[5]; + bool has_overlay, show_handle; + compute_overlay_geometry(ud, overlay_verts, &sel_cx, &sel_cy, + &sel_hw, &sel_hh, &sel_angle, &has_overlay, &show_handle); + + upload_overlay_buffers(ud, overlay_verts, sel_cx, sel_cy, + sel_hw, sel_hh, sel_angle, has_overlay, show_handle); + + sg_begin_pass(&(sg_pass){ + .action = ud->renderer.clear_pass, + .swapchain = sglue_swapchain(), + }); + + draw_shapes(ud); + draw_overlay_and_handles(ud, has_overlay, show_handle); simgui_new_frame(&(simgui_frame_desc_t){ .width = sapp_width(), @@ -477,175 +566,26 @@ static void frame(void* _userdata) .dpi_scale = sapp_dpi_scale(), }); - // Properties panel - { - igSetNextWindowPos((ImVec2){userdata->width - userdata->panel_w, 20.0f}, ImGuiCond_Always, (ImVec2){0, 0}); - igSetNextWindowSize((ImVec2){userdata->panel_w, userdata->height - 20.0f}, ImGuiCond_Always); - igBegin("Properties", NULL, - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - - if (userdata->selected_count == 0) { - igText("No shape selected"); - } else if (userdata->selected_count > 1) { - igText("%d shapes selected", userdata->selected_count); - } else { - int idx = 0; - while (idx < userdata->shapes.count) { - shape_t *tmp = (shape_t*) vec_get(&userdata->shapes, idx); - if (tmp->selected) break; - idx++; - } - shape_t *s = (shape_t*) vec_get(&userdata->shapes, idx); - bool changed = false; - - changed |= igDragFloat2("Position", &s->cx, 1.0f, 0, 0, "%.1f", 0); - if (igIsItemActivated()) history_begin_edit(&userdata->history, &userdata->shapes, idx, HIST_POSITION); - changed |= igDragFloat2("Scale", &s->sx, 1.0f, 0.1f, 0, "%.1f", 0); - if (igIsItemActivated()) history_begin_edit(&userdata->history, &userdata->shapes, idx, HIST_SCALE); - changed |= igDragFloat("Rotation", &s->rotation, 0.01f, 0, 0, "%.3f", 0); - if (igIsItemActivated()) history_begin_edit(&userdata->history, &userdata->shapes, idx, HIST_ROTATION); - if (igColorEdit4("Color", s->uniform.base_color, 0)) - changed = true; - if (igIsItemActivated()) history_begin_edit(&userdata->history, &userdata->shapes, idx, HIST_COLOR); - - if (changed) { shape_regenerate(s); spatial_mark_dirty(&userdata->spatial_grid); userdata->overlay_upload_needed = true; } - - igSeparator(); - { - mat4 *m = &s->uniform.transform; - float sc = cosf(s->rotation), ss = sinf(s->rotation); - float lx0 = s->verts[0].x * s->sx; - float ly0 = s->verts[0].y * s->sy; - float wx = s->cx + lx0 * sc - ly0 * ss; - float wy = s->cy + lx0 * ss + ly0 * sc; - igText("Debug - Transform Matrix:"); - igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[0][0], (*m)[0][1], (*m)[0][2], (*m)[0][3]); - igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[1][0], (*m)[1][1], (*m)[1][2], (*m)[1][3]); - igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[2][0], (*m)[2][1], (*m)[2][2], (*m)[2][3]); - igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[3][0], (*m)[3][1], (*m)[3][2], (*m)[3][3]); - igSpacing(); - igText("Local vert[0]: (%.1f, %.1f)", s->verts[0].x, s->verts[0].y); - igText("World vert[0]: (%.1f, %.1f)", wx, wy); - igText("cx=%.1f cy=%.1f sx=%.1f sy=%.1f rot=%.3f", s->cx, s->cy, s->sx, s->sy, s->rotation); - char dbg[512]; - snprintf(dbg, sizeof(dbg), - "Transform Matrix:\n" - "[%+.3f %+.3f %+.3f %+.3f]\n" - "[%+.3f %+.3f %+.3f %+.3f]\n" - "[%+.3f %+.3f %+.3f %+.3f]\n" - "[%+.3f %+.3f %+.3f %+.3f]\n" - "\nLocal vert[0]: (%.1f, %.1f)\n" - "World vert[0]: (%.1f, %.1f)\n" - "cx=%.1f cy=%.1f sx=%.1f sy=%.1f rot=%.3f", - (*m)[0][0], (*m)[0][1], (*m)[0][2], (*m)[0][3], - (*m)[1][0], (*m)[1][1], (*m)[1][2], (*m)[1][3], - (*m)[2][0], (*m)[2][1], (*m)[2][2], (*m)[2][3], - (*m)[3][0], (*m)[3][1], (*m)[3][2], (*m)[3][3], - s->verts[0].x, s->verts[0].y, wx, wy, - s->cx, s->cy, s->sx, s->sy, s->rotation); - if (igButton("Copy Debug", (ImVec2){0, 0})) - sapp_set_clipboard_string(dbg); - } - - igSeparator(); - - igBeginDisabled(true); - { - float dummy = 0; - igDragFloat("Height", &dummy, 1.0f, 0, 0, "%.1f", 0); - igDragFloat("Biome", &dummy, 1.0f, 0, 0, "%.1f", 0); - igDragFloat("Roughness", &dummy, 1.0f, 0, 0, "%.1f", 0); - igDragFloat("Intensity", &dummy, 1.0f, 0, 0, "%.0f", 0); - } - igEndDisabled(); - - igSeparator(); - - if (igButton("Shake Landmass", (ImVec2){0, 0})) { - // dummy for now - } - } - - igEnd(); - - if (userdata->history.capturing && !igIsAnyItemActive()) { - history_end_edit(&userdata->history, &userdata->shapes); - } - } - - // Log panel - if (userdata->log_show) { - igSetNextWindowPos((ImVec2){10.0f, userdata->height - 200.0f}, ImGuiCond_FirstUseEver, (ImVec2){0, 0}); - igSetNextWindowSize((ImVec2){400.0f, 180.0f}, ImGuiCond_FirstUseEver); - igBegin("Log", &userdata->log_show, 0); - if (igButton("Clear", (ImVec2){0, 0})) { - userdata->log_head = 0; - userdata->log_count = 0; - } - igSameLine(0.0f, 10.0f); - if (igButton("Copy", (ImVec2){0, 0})) { - int total = userdata->log_count < LOG_RING_SIZE ? userdata->log_count : LOG_RING_SIZE; - int start = userdata->log_count < LOG_RING_SIZE ? 0 : userdata->log_head; - int cap = total * 260; - char *buf = (char*) ALLOC((size_t)cap); - int off = 0; - for (int i = 0; i < total; i++) { - int idx = (start + i) % LOG_RING_SIZE; - off += snprintf(buf + off, (size_t)(cap - off), "%s\n", userdata->log_ring[idx].text); - } - igSetClipboardText(buf); - FREE(buf); - } - igSameLine(0.0f, 10.0f); - igText("%d entries", userdata->log_count); - igSameLine(0.0f, 10.0f); - igText("FPS: %.0f (avg: %.0f)", userdata->fps_immediate, userdata->fps_average); - igSeparator(); - - igBeginChild_Str("LogScroll", (ImVec2){0, 0}, false, 0); - int total = userdata->log_count < LOG_RING_SIZE ? userdata->log_count : LOG_RING_SIZE; - int start = userdata->log_count < LOG_RING_SIZE ? 0 : userdata->log_head; - for (int i = 0; i < total; i++) { - int idx = (start + i) % LOG_RING_SIZE; - log_entry_t *e = &userdata->log_ring[idx]; - ImVec4 color; - switch (e->level) { - case 0: color = (ImVec4){1.0f, 0.3f, 0.3f, 1.0f}; break; // panic: red - case 1: color = (ImVec4){1.0f, 0.5f, 0.3f, 1.0f}; break; // error: orange - case 2: color = (ImVec4){1.0f, 0.9f, 0.3f, 1.0f}; break; // warn: yellow - default:color = (ImVec4){0.7f, 0.7f, 0.7f, 1.0f}; break; // info: gray - } - igPushStyleColor_Vec4(ImGuiCol_Text, color); - igTextUnformatted(e->text, NULL); - igPopStyleColor(1); - } - if (total > 0) igSetScrollHereY(1.0f); - igEndChild(); - igEnd(); - } + draw_properties_panel(ud); + draw_log_panel(ud); simgui_render(); - sg_end_pass(); sg_commit(); } -/** - * One-time app initialization. Sets up sokol_gfx, simgui, shaders, pipelines, - * creates the initial shapes, and initializes the history stack. - * - * @param _userdata pointer to userdata_t - */ +// -- init -- + static void init(void* _userdata) { rand_seed(1); - userdata_t* userdata = (userdata_t*) _userdata; + userdata_t* ud = (userdata_t*) _userdata; sg_desc sgdesc = { .environment = sglue_environment(), .logger.func = log_capture, - .logger.user_data = userdata, + .logger.user_data = ud, }; sg_setup(&sgdesc); if (!sg_isvalid()) { @@ -655,28 +595,28 @@ static void init(void* _userdata) simgui_setup(&(simgui_desc_t){0}); const vec2 quad[4] = { - {-2.0f, 2.0f}, // bottom left - {2.0f, 2.0f}, // bottom right - {2.0f, -2.0f}, // top right - {-2.0f, -2.0f}, // top left + {-2.0f, 2.0f}, + {2.0f, 2.0f}, + {2.0f, -2.0f}, + {-2.0f, -2.0f}, }; const vec2 uv[4] = { - {0.0f, 1.0f}, // bottom left - {1.0f, 1.0f}, // bottom right - {1.0f, 0.0f}, // top right - {0.0f, 0.0f}, // top left + {0.0f, 1.0f}, + {1.0f, 1.0f}, + {1.0f, 0.0f}, + {0.0f, 0.0f}, }; const uint16_t indices[] = { 0, 1, 2, 0, 2, 3, }; - - userdata->width = sapp_width(); - userdata->height = sapp_height(); - userdata->half_width = userdata->width * 0.5f; - userdata->half_height = userdata->height * 0.5f; - glm_vec2_zero(userdata->pan); - userdata->zoom = 0.5f; - userdata->hover_tol = SHAPE_HOVER_PX / userdata->zoom; + + ud->camera.width = sapp_width(); + ud->camera.height = sapp_height(); + ud->camera.half_width = ud->camera.width * 0.5f; + ud->camera.half_height = ud->camera.height * 0.5f; + glm_vec2_zero(ud->camera.pan); + ud->camera.zoom = 0.5f; + ud->camera.hover_tol = SHAPE_HOVER_PX / ud->camera.zoom; sg_shader sprite_shader = sg_make_shader(&(sg_shader_desc) { .vertex_func = { @@ -731,11 +671,11 @@ static void init(void* _userdata) .label = "Sprite shader", }); - userdata->renderer = (renderer_t) { + ud->renderer = (renderer_t) { .clear_pass = (sg_pass_action) { .colors[0] = { .clear_value = { 0.0f, 0.0f, 0.0f, 1.0f }, .load_action = SG_LOADACTION_CLEAR } }, - .pipeline = sg_make_pipeline(&(sg_pipeline_desc) { + .pipeline = sg_make_pipeline(&(sg_pipeline_desc) { .shader = sprite_shader, .index_type = SG_INDEXTYPE_UINT16, .layout.attrs = { @@ -745,43 +685,43 @@ static void init(void* _userdata) .label = "Sprite pipeline", }), .uniform = (uniform_t) { - .mvp = { - 1.0f, 0.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, 0.0f, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f + .mvp = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f }, } }; shape_init_pipeline(); - vec_init(&userdata->shapes, sizeof(shape_t)); - spatial_init(&userdata->spatial_grid); - userdata->selected_count = 0; - userdata->hovered_shape = -1; - userdata->selecting = false; - userdata->sel_dragging = false; - userdata->panel_w = 300; - userdata->move_dragging = false; - userdata->rotate_dragging = false; - userdata->resize_dragging = false; - userdata->resize_angle = 0.0f; - userdata->resize_init = NULL; - userdata->overlay_upload_needed = true; - userdata->resize_init_count = 0; - userdata->log_head = 0; - userdata->log_count = 0; - userdata->log_show = false; + vec_init(&ud->shapes, sizeof(shape_t)); + spatial_init(&ud->spatial_grid); + ud->interact.selected_count = 0; + ud->interact.hovered_shape = -1; + ud->interact.selecting = false; + ud->interact.sel_dragging = false; + ud->ui.panel_w = 300; + ud->interact.move_dragging = false; + ud->interact.rotate_dragging = false; + ud->interact.resize_dragging = false; + ud->interact.resize_angle = 0.0f; + ud->interact.resize_init = NULL; + ud->overlay_upload_needed = true; + ud->interact.resize_init_count = 0; + ud->ui.log_head = 0; + ud->ui.log_count = 0; + ud->ui.log_show = true; { - userdata->rect_vbuf = sg_make_buffer(&(sg_buffer_desc){ + ud->rect_vbuf = sg_make_buffer(&(sg_buffer_desc){ .size = 5 * sizeof(shape_vertex_t), .usage = { .stream_update = true }, .label = "Sel rect verts", }); uint16_t rect_idx[5] = {0, 1, 2, 3, 4}; - userdata->rect_ibuf = sg_make_buffer(&(sg_buffer_desc){ + ud->rect_ibuf = sg_make_buffer(&(sg_buffer_desc){ .usage = {.index_buffer = true}, .data = {rect_idx, sizeof(rect_idx)}, .label = "Sel rect indices", @@ -792,12 +732,12 @@ static void init(void* _userdata) const int n = HANDLE_CIRCLE_SEGMENTS + 1; uint16_t handle_idx[HANDLE_CIRCLE_SEGMENTS + 1]; for (int i = 0; i < n; i++) handle_idx[i] = (uint16_t)i; - userdata->handle_vbuf = sg_make_buffer(&(sg_buffer_desc){ + ud->handle_vbuf = sg_make_buffer(&(sg_buffer_desc){ .size = (size_t)n * sizeof(shape_vertex_t), .usage = { .stream_update = true }, .label = "Handle verts", }); - userdata->handle_ibuf = sg_make_buffer(&(sg_buffer_desc){ + ud->handle_ibuf = sg_make_buffer(&(sg_buffer_desc){ .usage = {.index_buffer = true}, .data = {handle_idx, sizeof(handle_idx)}, .label = "Handle indices", @@ -805,27 +745,27 @@ static void init(void* _userdata) } { - userdata->corner_vbuf = sg_make_buffer(&(sg_buffer_desc){ + ud->corner_vbuf = sg_make_buffer(&(sg_buffer_desc){ .size = 40 * sizeof(shape_vertex_t), .usage = { .stream_update = true }, .label = "Corner verts", }); uint16_t ci[40]; for (int i = 0; i < 40; i++) ci[i] = (uint16_t)i; - userdata->corner_ibuf = sg_make_buffer(&(sg_buffer_desc){ + ud->corner_ibuf = sg_make_buffer(&(sg_buffer_desc){ .usage = {.index_buffer = true}, .data = {ci, sizeof(ci)}, .label = "Corner indices", }); } - *((shape_t*) vec_push(&userdata->shapes)) = shape_star(0.0f, 0.0f, 200.0f, 80.0f, 7, + *((shape_t*) vec_push(&ud->shapes)) = shape_star(0.0f, 0.0f, 200.0f, 80.0f, 7, (float[4]){ 0.0f, 0.94f, 1.0f, 1.0f }); - *((shape_t*) vec_push(&userdata->shapes)) = shape_circle(300.0f, 0.0f, 120.0f, + *((shape_t*) vec_push(&ud->shapes)) = shape_circle(300.0f, 0.0f, 120.0f, (float[4]){ 1.0f, 0.47f, 0.0f, 1.0f }); - history_init(&userdata->history); + history_init(&ud->history); EM_ASM({ window.addEventListener('keydown', function(e) { @@ -837,602 +777,658 @@ static void init(void* _userdata) }, true); }); - compute_mvp(userdata); + compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); } -/** - * App shutdown callback. Frees all shapes, GPU buffers, the history stack, - * and tears down simgui and sokol. - * - * @param _userdata pointer to userdata_t - */ +// -- cleanup -- + static void cleanup(void* _userdata) { - userdata_t* userdata = (userdata_t*) _userdata; + userdata_t* ud = (userdata_t*) _userdata; - for (int i = 0; i < userdata->shapes.count; i++) { - shape_shutdown((shape_t*) vec_get(&userdata->shapes, i)); + for (int i = 0; i < ud->shapes.count; i++) { + shape_shutdown((shape_t*) vec_get(&ud->shapes, i)); } - spatial_destroy(&userdata->spatial_grid); - vec_free(&userdata->shapes); - history_destroy(&userdata->history); - if (userdata->resize_init) FREE(userdata->resize_init); - sg_destroy_buffer(userdata->rect_vbuf); - sg_destroy_buffer(userdata->rect_ibuf); - sg_destroy_buffer(userdata->handle_vbuf); - sg_destroy_buffer(userdata->handle_ibuf); - sg_destroy_buffer(userdata->corner_vbuf); - sg_destroy_buffer(userdata->corner_ibuf); + spatial_destroy(&ud->spatial_grid); + vec_free(&ud->shapes); + history_destroy(&ud->history); + if (ud->interact.resize_init) FREE(ud->interact.resize_init); + sg_destroy_buffer(ud->rect_vbuf); + sg_destroy_buffer(ud->rect_ibuf); + sg_destroy_buffer(ud->handle_vbuf); + sg_destroy_buffer(ud->handle_ibuf); + sg_destroy_buffer(ud->corner_vbuf); + sg_destroy_buffer(ud->corner_ibuf); shape_shutdown_pipeline(); - FREE(userdata); + FREE(ud); simgui_shutdown(); sg_shutdown(); } -/** - * Input event handler. Processes keyboard shortcuts (Ctrl+Z/Y for undo/redo), - * then delegates to simgui. Handles mouse down/up/move/scroll for pan, zoom, - * single-click selection, marquee selection, and Ctrl+click toggle. - * - * @param event sokol event descriptor - * @param _userdata pointer to userdata_t - */ +// -- event helpers -- + +static bool handle_key_down(userdata_t *ud, const sapp_event *event) +{ + if (event->modifiers & SAPP_MODIFIER_CTRL) { + if (event->key_code == SAPP_KEYCODE_Z || event->key_code == SAPP_KEYCODE_W) { + if (history_undo(&ud->history, &ud->shapes)) { + ud->interact.hovered_shape = -1; + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; + } + return true; + } + if (event->key_code == SAPP_KEYCODE_Y) { + if (history_redo(&ud->history, &ud->shapes)) { + ud->interact.hovered_shape = -1; + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; + } + return true; + } + } + if (event->key_code == SAPP_KEYCODE_GRAVE_ACCENT) { + ud->ui.log_show = !ud->ui.log_show; + return true; + } + return false; +} + +static void handle_resize(userdata_t *ud, const sapp_event *event) +{ + (void)event; + ud->camera.width = sapp_width(); + ud->camera.height = sapp_height(); + ud->camera.half_width = ud->camera.width * 0.5f; + ud->camera.half_height = ud->camera.height * 0.5f; + compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); +} + +static void update_shape_states(userdata_t *ud) +{ + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + shape_set_state(s, s->hovered, s->selected); + } +} + +static void handle_left_down_ctrl_click(userdata_t *ud, float wx, float wy, float tol) +{ + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (shape_hit_test(s, wx, wy, tol)) { + s->selected = !s->selected; + ud->interact.selected_count += s->selected ? 1 : -1; + ud->overlay_upload_needed = true; + break; + } + } +} + +static int hit_test_resize_handles(userdata_t *ud, float wx, float wy, float tol) +{ + if (ud->interact.selected_count <= 0) return -1; + float omin[2], omax[2]; + if (ud->interact.aabb_cached) { + omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1]; + omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3]; + } else { + selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); + } + float hs = CORNER_SIZE_PX / ud->camera.zoom * 0.5f + tol; + float mid_x = (omin[0] + omax[0]) * 0.5f; + float mid_y = (omin[1] + omax[1]) * 0.5f; + float hx[8] = {omin[0], mid_x, omax[0], omax[0], omax[0], mid_x, omin[0], omin[0]}; + float hy[8] = {omin[1], omin[1], omin[1], mid_y, omax[1], omax[1], omax[1], mid_y}; + for (int h = 0; h < 8; h++) { + if (fabsf(wx - hx[h]) <= hs && fabsf(wy - hy[h]) <= hs) + return h; + } + return -1; +} + +static void handle_left_down_resize_begin(userdata_t *ud, float wx, float wy, int resize_hit) +{ + float omin[2], omax[2]; + if (ud->interact.aabb_cached) { + omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1]; + omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3]; + } else { + selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); + } + float mid_x = (omin[0] + omax[0]) * 0.5f; + float mid_y = (omin[1] + omax[1]) * 0.5f; + float px[8] = {omax[0], mid_x, omin[0], omin[0], omin[0], mid_x, omax[0], omax[0]}; + float py[8] = {omax[1], omax[1], omax[1], mid_y, omin[1], omin[1], omin[1], mid_y}; + ud->interact.resize_pivot_x = px[resize_hit]; + ud->interact.resize_pivot_y = py[resize_hit]; + ud->interact.resize_start_wx = wx; + ud->interact.resize_start_wy = wy; + ud->interact.resize_total_scale_x = 1.0f; + ud->interact.resize_total_scale_y = 1.0f; + ud->interact.resize_mask_x = (resize_hit == 3 || resize_hit == 7 || (resize_hit & 1) == 0) ? 1.0f : 0.0f; + ud->interact.resize_mask_y = (resize_hit == 1 || resize_hit == 5 || (resize_hit & 1) == 0) ? 1.0f : 0.0f; + ud->interact.resize_dragging = true; + + float sum_sin = 0, sum_cos = 0; + int sel_n = 0; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { sum_sin += sinf(s->rotation); sum_cos += cosf(s->rotation); sel_n++; } + } + ud->interact.resize_angle = atan2f(sum_sin, sum_cos); + + ud->interact.resize_init = (resize_init_t*) ALLOC((size_t)sel_n * sizeof(resize_init_t)); + ud->interact.resize_init_count = sel_n; + int j = 0; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + float sc = cosf(s->rotation), ss = sinf(s->rotation); + float hlx = (ud->interact.resize_start_wx - s->cx) * sc + (ud->interact.resize_start_wy - s->cy) * ss; + float hly = -(ud->interact.resize_start_wx - s->cx) * ss + (ud->interact.resize_start_wy - s->cy) * sc; + float plx = (ud->interact.resize_pivot_x - s->cx) * sc + (ud->interact.resize_pivot_y - s->cy) * ss; + float ply = -(ud->interact.resize_pivot_x - s->cx) * ss + (ud->interact.resize_pivot_y - s->cy) * sc; + + ud->interact.resize_init[j].idx = i; + ud->interact.resize_init[j].init_sx = s->sx; + ud->interact.resize_init[j].init_sy = s->sy; + ud->interact.resize_init[j].init_cx = s->cx; + ud->interact.resize_init[j].init_cy = s->cy; + ud->interact.resize_init[j].ext_x = hlx - plx; + ud->interact.resize_init[j].ext_y = hly - ply; + ud->interact.resize_init[j].lpi_x = plx; + ud->interact.resize_init[j].lpi_y = ply; + j++; + } + } +} + +static void handle_left_down_rotate_begin(userdata_t *ud, float wx, float wy) +{ + ud->interact.rotate_dragging = true; + ud->interact.rotate_start_angle = atan2f( + wy - ud->interact.rotate_center_y, + wx - ud->interact.rotate_center_x); + ud->interact.rotate_total_delta = 0.0f; +} + +static void handle_left_down_move_begin(userdata_t *ud, float wx, float wy) +{ + ud->interact.move_dragging = true; + ud->interact.move_start_wx = wx; + ud->interact.move_start_wy = wy; + ud->interact.move_total_dx = 0; + ud->interact.move_total_dy = 0; +} + +static void handle_left_down_select_or_marquee(userdata_t *ud, const sapp_event *event, float wx, float wy, float tol) +{ + ud->interact.selecting = true; + ud->interact.sel_dragging = false; + ud->interact.sel_sx = event->mouse_x; + ud->interact.sel_sy = event->mouse_y; + ud->interact.sel_cx = event->mouse_x; + ud->interact.sel_cy = event->mouse_y; + + ud->interact.sel_clicked_shape = -1; + for (int i = 0; i < ud->shapes.count; i++) { + if (shape_hit_test((shape_t*) vec_get(&ud->shapes, i), wx, wy, tol)) { + ud->interact.sel_clicked_shape = i; + break; + } + } +} + +static void handle_right_down_pan_begin(userdata_t *ud, const sapp_event *event) +{ + ud->camera.pan_state.dragging = true; + ud->camera.pan_state.origin_x = event->mouse_x; + ud->camera.pan_state.origin_y = event->mouse_y; +} + +static void handle_mouse_down(userdata_t *ud, const sapp_event *event) +{ + if (event->mouse_button == SAPP_MOUSEBUTTON_LEFT) { + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + const float tol = 4.0f / ud->camera.zoom; + + if (event->modifiers & SAPP_MODIFIER_CTRL) { + handle_left_down_ctrl_click(ud, wx, wy, tol); + } else { + int resize_hit = hit_test_resize_handles(ud, wx, wy, tol); + if (resize_hit >= 0) { + handle_left_down_resize_begin(ud, wx, wy, resize_hit); + } else { + float grip = HANDLE_RADIUS_PX / ud->camera.zoom + tol; + float dcx = wx - ud->interact.rotate_center_x; + float dcy = wy - ud->interact.rotate_center_y; + float dist = sqrtf(dcx * dcx + dcy * dcy); + bool on_handle = (ud->interact.selected_count > 0) && + (fabsf(dist - ud->interact.handle_radius) <= grip); + + if (on_handle) { + handle_left_down_rotate_begin(ud, wx, wy); + } else { + int clicked_selected = -1; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected && shape_hit_test(s, wx, wy, tol)) { + clicked_selected = i; + break; + } + } + + bool in_aabb = false; + if (clicked_selected < 0 && ud->interact.selected_count >= 2) { + float omin[2], omax[2]; + if (ud->interact.aabb_cached) { + omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1]; + omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3]; + } else { + selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); + } + float pad = 8.0f / ud->camera.zoom; + omin[0] -= pad; omin[1] -= pad; + omax[0] += pad; omax[1] += pad; + in_aabb = (wx >= omin[0] && wx <= omax[0] && wy >= omin[1] && wy <= omax[1]); + } + + if (clicked_selected >= 0 || in_aabb) { + handle_left_down_move_begin(ud, wx, wy); + } else { + handle_left_down_select_or_marquee(ud, event, wx, wy, tol); + } + } + } + } + + update_shape_states(ud); + } else if (event->modifiers & SAPP_MODIFIER_RMB) { + handle_right_down_pan_begin(ud, event); + } +} + +static void handle_resize_end(userdata_t *ud) +{ + int n = ud->interact.resize_init_count; + bool changed = false; + for (int j = 0; j < n; j++) { + resize_init_t *ini = &ud->interact.resize_init[j]; + shape_t *s = (shape_t*) vec_get(&ud->shapes, ini->idx); + if (s->sx != ini->init_sx || s->sy != ini->init_sy || + s->cx != ini->init_cx || s->cy != ini->init_cy) + changed = true; + } + if (changed) { + hist_batch_t batch; + history_batch_init(&batch, n * 2); + + for (int j = 0; j < n; j++) { + resize_init_t *ini = &ud->interact.resize_init[j]; + shape_t *s = (shape_t*) vec_get(&ud->shapes, ini->idx); + history_batch_add(&batch, ini->idx, HIST_POSITION, + (float[4]){ ini->init_cx, ini->init_cy }, + (float[4]){ s->cx, s->cy }); + history_batch_add(&batch, ini->idx, HIST_SCALE, + (float[4]){ ini->init_sx, ini->init_sy }, + (float[4]){ s->sx, s->sy }); + } + + history_batch_commit(&batch, &ud->history); + } + + FREE(ud->interact.resize_init); + ud->interact.resize_init = NULL; + ud->interact.resize_init_count = 0; + ud->interact.resize_dragging = false; + + update_shape_states(ud); + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; +} + +static void handle_rotate_end(userdata_t *ud) +{ + if (ud->interact.rotate_total_delta != 0.0f) { + int sel_count = 0; + for (int i = 0; i < ud->shapes.count; i++) { + if (((shape_t*) vec_get(&ud->shapes, i))->selected) sel_count++; + } + + float cos_b = cosf(-ud->interact.rotate_total_delta); + float sin_b = sinf(-ud->interact.rotate_total_delta); + float cx = ud->interact.rotate_center_x; + float cy = ud->interact.rotate_center_y; + + hist_batch_t batch; + history_batch_init(&batch, sel_count * 2); + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + float dx = s->cx - cx; + float dy = s->cy - cy; + float old_cx = cx + dx * cos_b - dy * sin_b; + float old_cy = cy + dx * sin_b + dy * cos_b; + + history_batch_add(&batch, i, HIST_POSITION, + (float[4]){ old_cx, old_cy }, + (float[4]){ s->cx, s->cy }); + history_batch_add(&batch, i, HIST_ROTATION, + (float[4]){ s->rotation - ud->interact.rotate_total_delta }, + (float[4]){ s->rotation }); + } + } + + history_batch_commit(&batch, &ud->history); + } + + ud->interact.rotate_dragging = false; + update_shape_states(ud); + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; +} + +static void handle_move_end(userdata_t *ud) +{ + if (ud->interact.move_total_dx != 0.0f || ud->interact.move_total_dy != 0.0f) { + int sel_count = 0; + for (int i = 0; i < ud->shapes.count; i++) { + if (((shape_t*) vec_get(&ud->shapes, i))->selected) sel_count++; + } + + hist_batch_t batch; + history_batch_init(&batch, sel_count); + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + history_batch_add(&batch, i, HIST_POSITION, + (float[4]){ s->cx - ud->interact.move_total_dx, s->cy - ud->interact.move_total_dy }, + (float[4]){ s->cx, s->cy }); + } + } + + history_batch_commit(&batch, &ud->history); + } + + ud->interact.move_dragging = false; + update_shape_states(ud); + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; +} + +static void handle_select_end(userdata_t *ud) +{ + if (!ud->interact.sel_dragging) { + if (ud->interact.sel_clicked_shape >= 0) { + for (int i = 0; i < ud->shapes.count; i++) { + ((shape_t*) vec_get(&ud->shapes, i))->selected = false; + } + ((shape_t*) vec_get(&ud->shapes, ud->interact.sel_clicked_shape))->selected = true; + ud->interact.selected_count = 1; + } else { + for (int i = 0; i < ud->shapes.count; i++) { + ((shape_t*) vec_get(&ud->shapes, i))->selected = false; + } + ud->interact.selected_count = 0; + } + } + + ud->interact.selecting = false; + ud->interact.sel_dragging = false; + update_shape_states(ud); + ud->overlay_upload_needed = true; +} + +static void handle_mouse_up(userdata_t *ud, const sapp_event *event) +{ + (void)event; + + if (ud->interact.resize_dragging) { + handle_resize_end(ud); + } else if (ud->interact.rotate_dragging) { + handle_rotate_end(ud); + } else if (ud->interact.move_dragging) { + handle_move_end(ud); + } else if (ud->interact.selecting) { + handle_select_end(ud); + } + + ud->camera.pan_state.dragging = false; +} + +static void handle_pan_drag(userdata_t *ud, const sapp_event *event) +{ + ud->camera.pan[0] += event->mouse_dx; + ud->camera.pan[1] -= event->mouse_dy; + compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); +} + +static void handle_resize_drag(userdata_t *ud, const sapp_event *event) +{ + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + + float sx_total = 1.0f, sy_total = 1.0f; + + for (int j = 0; j < ud->interact.resize_init_count; j++) { + resize_init_t *ini = &ud->interact.resize_init[j]; + shape_t *s = (shape_t*) vec_get(&ud->shapes, ini->idx); + + float sc = cosf(s->rotation), ss = sinf(s->rotation); + float mlx = (wx - ini->init_cx) * sc + (wy - ini->init_cy) * ss; + float mly = -(wx - ini->init_cx) * ss + (wy - ini->init_cy) * sc; + + float cex = mlx - ini->lpi_x; + float cey = mly - ini->lpi_y; + + float scale_x = 1.0f, scale_y = 1.0f; + if (ud->interact.resize_mask_x && fabsf(ini->ext_x) >= 0.0001f) + scale_x = fabsf(cex / ini->ext_x); + if (ud->interact.resize_mask_y && fabsf(ini->ext_y) >= 0.0001f) + scale_y = fabsf(cey / ini->ext_y); + + s->sx = ini->init_sx * scale_x; + s->sy = ini->init_sy * scale_y; + s->cx = ini->init_cx - ini->lpi_x * (scale_x - 1.0f) * sc + ini->lpi_y * (scale_y - 1.0f) * ss; + s->cy = ini->init_cy - ini->lpi_x * (scale_x - 1.0f) * ss - ini->lpi_y * (scale_y - 1.0f) * sc; + + shape_regenerate(s); + shape_set_state(s, false, true); + + sx_total = scale_x; + sy_total = scale_y; + } + + ud->interact.resize_total_scale_x = sx_total; + ud->interact.resize_total_scale_y = sy_total; +} + +static void handle_rotate_drag(userdata_t *ud, const sapp_event *event) +{ + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + float angle = atan2f(wy - ud->interact.rotate_center_y, + wx - ud->interact.rotate_center_x); + float delta = angle - ud->interact.rotate_start_angle; + if (delta > GLM_PIf) delta -= 2.0f * GLM_PIf; + else if (delta < -GLM_PIf) delta += 2.0f * GLM_PIf; + float inc = delta - ud->interact.rotate_total_delta; + + float cos_a = cosf(inc); + float sin_a = sinf(inc); + float cx = ud->interact.rotate_center_x; + float cy = ud->interact.rotate_center_y; + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + float dx = s->cx - cx; + float dy = s->cy - cy; + s->cx = cx + dx * cos_a - dy * sin_a; + s->cy = cy + dx * sin_a + dy * cos_a; + s->rotation += inc; + shape_build_transform(s); + shape_set_state(s, false, true); + } + } + + ud->interact.rotate_total_delta = delta; +} + +static void handle_move_drag(userdata_t *ud, const sapp_event *event) +{ + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + float dx = wx - ud->interact.move_start_wx; + float dy = wy - ud->interact.move_start_wy; + float delta_x = dx - ud->interact.move_total_dx; + float delta_y = dy - ud->interact.move_total_dy; + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + s->cx += delta_x; + s->cy += delta_y; + shape_build_transform(s); + shape_set_state(s, false, true); + } + } + + ud->interact.move_total_dx = dx; + ud->interact.move_total_dy = dy; +} + +static void handle_marquee_drag(userdata_t *ud, const sapp_event *event) +{ + ud->interact.sel_cx = event->mouse_x; + ud->interact.sel_cy = event->mouse_y; + float dx = ud->interact.sel_cx - ud->interact.sel_sx; + float dy = ud->interact.sel_cy - ud->interact.sel_sy; + if (dx * dx + dy * dy > 9.0f) { + ud->interact.sel_dragging = true; + } + + if (ud->interact.sel_dragging) { + float wx1, wy1, wx2, wy2; + screen_to_world(&ud->camera, ud->interact.sel_sx, ud->interact.sel_sy, &wx1, &wy1); + screen_to_world(&ud->camera, ud->interact.sel_cx, ud->interact.sel_cy, &wx2, &wy2); + float min_x = fminf(wx1, wx2), min_y = fminf(wy1, wy2); + float max_x = fmaxf(wx1, wx2), max_y = fmaxf(wy1, wy2); + + ud->interact.selected_count = spatial_query_rect_select( + &ud->spatial_grid, &ud->shapes, + min_x, min_y, max_x, max_y); + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + shape_set_state(s, false, s->selected); + } + } +} + +static void handle_hover(userdata_t *ud, const sapp_event *event) +{ + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + const float tol = ud->camera.hover_tol; + + int hovered = spatial_query_point(&ud->spatial_grid, + &ud->shapes, wx, wy, tol); + if (hovered != ud->interact.hovered_shape) { + ud->interact.hovered_shape = hovered; + EM_ASM({ document.querySelector('canvas').style.cursor = $0 ? 'pointer' : 'default'; }, hovered >= 0); + } + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + shape_set_state(s, i == hovered, s->selected); + } +} + +static void handle_mouse_move(userdata_t *ud, const sapp_event *event) +{ + if (ud->camera.pan_state.dragging) { + handle_pan_drag(ud, event); + } else if (ud->interact.resize_dragging) { + handle_resize_drag(ud, event); + } else if (ud->interact.rotate_dragging) { + handle_rotate_drag(ud, event); + } else if (ud->interact.move_dragging) { + handle_move_drag(ud, event); + } else if (ud->interact.selecting) { + handle_marquee_drag(ud, event); + } else { + handle_hover(ud, event); + } +} + +static void handle_scroll_zoom(userdata_t *ud, const sapp_event *event) +{ + if ((ud->camera.zoom >= 6.0f && event->scroll_y > 0.0f) || + (ud->camera.zoom <= 0.1f && event->scroll_y < 0.0f)) + return; + + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + + const float diff = expf(event->scroll_y * 0.1f); + ud->camera.zoom = _sg_clamp(ud->camera.zoom * diff, 0.1f, 6.0f); + ud->camera.hover_tol = SHAPE_HOVER_PX / ud->camera.zoom; + + const float sx = event->mouse_x - ud->camera.half_width; + const float sy = ud->camera.half_height - event->mouse_y; + ud->camera.pan[0] = sx - wx * ud->camera.zoom; + ud->camera.pan[1] = sy - wy * ud->camera.zoom; + + ud->overlay_upload_needed = true; + compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); +} + +// -- event -- + static void event(const sapp_event* event, void* _userdata) { - userdata_t* userdata = (userdata_t*) _userdata; + userdata_t* ud = (userdata_t*) _userdata; if (event->type == SAPP_EVENTTYPE_KEY_DOWN) { - if (event->modifiers & SAPP_MODIFIER_CTRL) { - if (event->key_code == SAPP_KEYCODE_Z || event->key_code == SAPP_KEYCODE_W) { - if (history_undo(&userdata->history, &userdata->shapes, &userdata->selected_count)) { - userdata->hovered_shape = -1; - spatial_mark_dirty(&userdata->spatial_grid); - userdata->aabb_cached = false; - userdata->overlay_upload_needed = true; - } - return; - } - if (event->key_code == SAPP_KEYCODE_Y) { - if (history_redo(&userdata->history, &userdata->shapes, &userdata->selected_count)) { - userdata->hovered_shape = -1; - spatial_mark_dirty(&userdata->spatial_grid); - userdata->aabb_cached = false; - userdata->overlay_upload_needed = true; - } - return; - } - } - if (event->key_code == SAPP_KEYCODE_GRAVE_ACCENT) { - userdata->log_show = !userdata->log_show; - return; - } + if (handle_key_down(ud, event)) return; } if (simgui_handle_event(event)) return; - switch(event->type) - { + switch (event->type) { case SAPP_EVENTTYPE_RESIZED: - userdata->width = sapp_width(); - userdata->height = sapp_height(); - userdata->half_width = userdata->width * 0.5f; - userdata->half_height = userdata->height * 0.5f; - - compute_mvp(userdata); - + handle_resize(ud, event); break; case SAPP_EVENTTYPE_MOUSE_DOWN: - if (event->mouse_button == SAPP_MOUSEBUTTON_LEFT) - { - float wx, wy; - screen_to_world(userdata, event->mouse_x, event->mouse_y, &wx, &wy); - const float tol = 4.0f / userdata->zoom; - - if (event->modifiers & SAPP_MODIFIER_CTRL) { - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - if (shape_hit_test(s, wx, wy, tol)) { - s->selected = !s->selected; - userdata->selected_count += s->selected ? 1 : -1; - userdata->overlay_upload_needed = true; - break; - } - } - } else { - int resize_hit = -1; - if (userdata->selected_count > 0) { - float omin[2], omax[2]; - if (userdata->aabb_cached) { - omin[0] = userdata->cached_aabb[0]; omin[1] = userdata->cached_aabb[1]; - omax[0] = userdata->cached_aabb[2]; omax[1] = userdata->cached_aabb[3]; - } else { - selected_aabb(userdata, &omin[0], &omin[1], &omax[0], &omax[1]); - } - float hs = CORNER_SIZE_PX / userdata->zoom * 0.5f + tol; - float mid_x = (omin[0] + omax[0]) * 0.5f; - float mid_y = (omin[1] + omax[1]) * 0.5f; - float hx[8] = {omin[0], mid_x, omax[0], omax[0], omax[0], mid_x, omin[0], omin[0]}; - float hy[8] = {omin[1], omin[1], omin[1], mid_y, omax[1], omax[1], omax[1], mid_y}; - for (int h = 0; h < 8; h++) { - if (fabsf(wx - hx[h]) <= hs && fabsf(wy - hy[h]) <= hs) { - resize_hit = h; - break; - } - } - if (resize_hit >= 0) { - float px[8] = {omax[0], mid_x, omin[0], omin[0], omin[0], mid_x, omax[0], omax[0]}; - float py[8] = {omax[1], omax[1], omax[1], mid_y, omin[1], omin[1], omin[1], mid_y}; - userdata->resize_pivot_x = px[resize_hit]; - userdata->resize_pivot_y = py[resize_hit]; - userdata->resize_start_wx = wx; - userdata->resize_start_wy = wy; - userdata->resize_total_scale_x = 1.0f; - userdata->resize_total_scale_y = 1.0f; - userdata->resize_mask_x = (resize_hit == 3 || resize_hit == 7 || (resize_hit & 1) == 0) ? 1.0f : 0.0f; - userdata->resize_mask_y = (resize_hit == 1 || resize_hit == 5 || (resize_hit & 1) == 0) ? 1.0f : 0.0f; - userdata->resize_dragging = true; - - float sum_sin = 0, sum_cos = 0; - int sel_n = 0; - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - if (s->selected) { sum_sin += sinf(s->rotation); sum_cos += cosf(s->rotation); sel_n++; } - } - userdata->resize_angle = atan2f(sum_sin, sum_cos); - - userdata->resize_init = (resize_init_t*) ALLOC((size_t)sel_n * sizeof(resize_init_t)); - userdata->resize_init_count = sel_n; - int j = 0; - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - if (s->selected) { - float sc = cosf(s->rotation), ss = sinf(s->rotation); - float hlx = (userdata->resize_start_wx - s->cx) * sc + (userdata->resize_start_wy - s->cy) * ss; - float hly = -(userdata->resize_start_wx - s->cx) * ss + (userdata->resize_start_wy - s->cy) * sc; - float plx = (userdata->resize_pivot_x - s->cx) * sc + (userdata->resize_pivot_y - s->cy) * ss; - float ply = -(userdata->resize_pivot_x - s->cx) * ss + (userdata->resize_pivot_y - s->cy) * sc; - - userdata->resize_init[j].idx = i; - userdata->resize_init[j].init_sx = s->sx; - userdata->resize_init[j].init_sy = s->sy; - userdata->resize_init[j].init_cx = s->cx; - userdata->resize_init[j].init_cy = s->cy; - userdata->resize_init[j].ext_x = hlx - plx; - userdata->resize_init[j].ext_y = hly - ply; - userdata->resize_init[j].lpi_x = plx; - userdata->resize_init[j].lpi_y = ply; - j++; - } - } - } - } - - if (resize_hit < 0) { - float grip = HANDLE_RADIUS_PX / userdata->zoom + tol; - float dcx = wx - userdata->rotate_center_x; - float dcy = wy - userdata->rotate_center_y; - float dist = sqrtf(dcx * dcx + dcy * dcy); - bool on_handle = (userdata->selected_count > 0) && - (fabsf(dist - userdata->handle_radius) <= grip); - - if (on_handle) { - userdata->rotate_dragging = true; - userdata->rotate_start_angle = atan2f( - wy - userdata->rotate_center_y, - wx - userdata->rotate_center_x); - userdata->rotate_total_delta = 0.0f; - } else { - int clicked_selected = -1; - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - if (s->selected && shape_hit_test(s, wx, wy, tol)) { - clicked_selected = i; - break; - } - } - - bool in_aabb = false; - if (clicked_selected < 0 && userdata->selected_count >= 2) { - float omin[2], omax[2]; - if (userdata->aabb_cached) { - omin[0] = userdata->cached_aabb[0]; omin[1] = userdata->cached_aabb[1]; - omax[0] = userdata->cached_aabb[2]; omax[1] = userdata->cached_aabb[3]; - } else { - selected_aabb(userdata, &omin[0], &omin[1], &omax[0], &omax[1]); - } - float pad = 8.0f / userdata->zoom; - omin[0] -= pad; omin[1] -= pad; - omax[0] += pad; omax[1] += pad; - in_aabb = (wx >= omin[0] && wx <= omax[0] && wy >= omin[1] && wy <= omax[1]); - } - - if (clicked_selected >= 0 || in_aabb) { - userdata->move_dragging = true; - userdata->move_start_wx = wx; - userdata->move_start_wy = wy; - userdata->move_total_dx = 0; - userdata->move_total_dy = 0; - } else { - userdata->selecting = true; - userdata->sel_dragging = false; - userdata->sel_sx = event->mouse_x; - userdata->sel_sy = event->mouse_y; - userdata->sel_cx = event->mouse_x; - userdata->sel_cy = event->mouse_y; - - userdata->sel_clicked_shape = -1; - for (int i = 0; i < userdata->shapes.count; i++) { - if (shape_hit_test((shape_t*) vec_get(&userdata->shapes, i), wx, wy, tol)) { - userdata->sel_clicked_shape = i; - break; - } - } - } - } - } - } - - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - shape_set_state(s, s->hovered, s->selected); - } - } - else if (event->modifiers & SAPP_MODIFIER_RMB) - { - userdata->dragger.dragging = true; - userdata->dragger.origin_x = event->mouse_x; - userdata->dragger.origin_y = event->mouse_y; - } - + handle_mouse_down(ud, event); break; case SAPP_EVENTTYPE_MOUSE_UP: - if (userdata->resize_dragging) { - int n = userdata->resize_init_count; - bool changed = false; - for (int j = 0; j < n; j++) { - resize_init_t *ini = &userdata->resize_init[j]; - shape_t *s = (shape_t*) vec_get(&userdata->shapes, ini->idx); - if (s->sx != ini->init_sx || s->sy != ini->init_sy || - s->cx != ini->init_cx || s->cy != ini->init_cy) - changed = true; - } - if (changed) { - hist_entry_t entry = { .changes = NULL, .count = n * 2 }; - entry.changes = (hist_change_t*) ALLOC((size_t)n * 2 * sizeof(hist_change_t)); - - for (int j = 0; j < n; j++) { - resize_init_t *ini = &userdata->resize_init[j]; - shape_t *s = (shape_t*) vec_get(&userdata->shapes, ini->idx); - entry.changes[j*2] = (hist_change_t){ - .shape_index = ini->idx, - .prop = HIST_POSITION, - .old_val = { ini->init_cx, ini->init_cy }, - .new_val = { s->cx, s->cy }, - }; - entry.changes[j*2+1] = (hist_change_t){ - .shape_index = ini->idx, - .prop = HIST_SCALE, - .old_val = { ini->init_sx, ini->init_sy }, - .new_val = { s->sx, s->sy }, - }; - } - - history_push_entry(&userdata->history, entry); - } - - FREE(userdata->resize_init); - userdata->resize_init = NULL; - userdata->resize_init_count = 0; - userdata->resize_dragging = false; - - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - shape_set_state(s, s->hovered, s->selected); - } - spatial_mark_dirty(&userdata->spatial_grid); - userdata->aabb_cached = false; - userdata->overlay_upload_needed = true; - } - else if (userdata->rotate_dragging) { - if (userdata->rotate_total_delta != 0.0f) { - int sel_count = 0; - for (int i = 0; i < userdata->shapes.count; i++) { - if (((shape_t*) vec_get(&userdata->shapes, i))->selected) sel_count++; - } - - float cos_b = cosf(-userdata->rotate_total_delta); - float sin_b = sinf(-userdata->rotate_total_delta); - float cx = userdata->rotate_center_x; - float cy = userdata->rotate_center_y; - - hist_entry_t entry = { .changes = NULL, .count = sel_count * 2 }; - entry.changes = (hist_change_t*) ALLOC(sel_count * 2 * sizeof(hist_change_t)); - - int idx = 0; - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - if (s->selected) { - float dx = s->cx - cx; - float dy = s->cy - cy; - float old_cx = cx + dx * cos_b - dy * sin_b; - float old_cy = cy + dx * sin_b + dy * cos_b; - - entry.changes[idx++] = (hist_change_t){ - .shape_index = i, - .prop = HIST_POSITION, - .old_val = { old_cx, old_cy }, - .new_val = { s->cx, s->cy }, - }; - entry.changes[idx++] = (hist_change_t){ - .shape_index = i, - .prop = HIST_ROTATION, - .old_val = { s->rotation - userdata->rotate_total_delta }, - .new_val = { s->rotation }, - }; - } - } - - history_push_entry(&userdata->history, entry); - } - - userdata->rotate_dragging = false; - - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - shape_set_state(s, s->hovered, s->selected); - } - spatial_mark_dirty(&userdata->spatial_grid); - userdata->aabb_cached = false; - userdata->overlay_upload_needed = true; - } - else if (userdata->move_dragging) { - if (userdata->move_total_dx != 0.0f || userdata->move_total_dy != 0.0f) { - int sel_count = 0; - for (int i = 0; i < userdata->shapes.count; i++) { - if (((shape_t*) vec_get(&userdata->shapes, i))->selected) sel_count++; - } - - hist_entry_t entry = { .changes = NULL, .count = sel_count }; - entry.changes = (hist_change_t*) ALLOC(sel_count * sizeof(hist_change_t)); - - int idx = 0; - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - if (s->selected) { - entry.changes[idx] = (hist_change_t){ - .shape_index = i, - .prop = HIST_POSITION, - .old_val = { s->cx - userdata->move_total_dx, s->cy - userdata->move_total_dy }, - .new_val = { s->cx, s->cy }, - }; - idx++; - } - } - - history_push_entry(&userdata->history, entry); - } - - userdata->move_dragging = false; - - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - shape_set_state(s, s->hovered, s->selected); - } - spatial_mark_dirty(&userdata->spatial_grid); - userdata->aabb_cached = false; - userdata->overlay_upload_needed = true; - } - else if (userdata->selecting) { - if (!userdata->sel_dragging) { - if (userdata->sel_clicked_shape >= 0) { - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - s->selected = false; - } - ((shape_t*) vec_get(&userdata->shapes, userdata->sel_clicked_shape))->selected = true; - userdata->selected_count = 1; - } else { - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - s->selected = false; - } - userdata->selected_count = 0; - } - } - - userdata->selecting = false; - userdata->sel_dragging = false; - - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - shape_set_state(s, s->hovered, s->selected); - } - userdata->overlay_upload_needed = true; - } - - userdata->dragger.dragging = false; - + handle_mouse_up(ud, event); break; case SAPP_EVENTTYPE_MOUSE_MOVE: - if (userdata->dragger.dragging) - { - userdata->pan[0] += event->mouse_dx; - userdata->pan[1] -= event->mouse_dy; - - compute_mvp(userdata); - } - else if (userdata->resize_dragging) - { - float wx, wy; - screen_to_world(userdata, event->mouse_x, event->mouse_y, &wx, &wy); - - float sx_total = 1.0f, sy_total = 1.0f; - - for (int j = 0; j < userdata->resize_init_count; j++) { - resize_init_t *ini = &userdata->resize_init[j]; - shape_t *s = (shape_t*) vec_get(&userdata->shapes, ini->idx); - - float sc = cosf(s->rotation), ss = sinf(s->rotation); - float mlx = (wx - ini->init_cx) * sc + (wy - ini->init_cy) * ss; - float mly = -(wx - ini->init_cx) * ss + (wy - ini->init_cy) * sc; - - float cex = mlx - ini->lpi_x; - float cey = mly - ini->lpi_y; - - float scale_x = 1.0f, scale_y = 1.0f; - if (userdata->resize_mask_x && fabsf(ini->ext_x) >= 0.0001f) - scale_x = fabsf(cex / ini->ext_x); - if (userdata->resize_mask_y && fabsf(ini->ext_y) >= 0.0001f) - scale_y = fabsf(cey / ini->ext_y); - - s->sx = ini->init_sx * scale_x; - s->sy = ini->init_sy * scale_y; - s->cx = ini->init_cx - ini->lpi_x * (scale_x - 1.0f) * sc + ini->lpi_y * (scale_y - 1.0f) * ss; - s->cy = ini->init_cy - ini->lpi_x * (scale_x - 1.0f) * ss - ini->lpi_y * (scale_y - 1.0f) * sc; - - shape_regenerate(s); - shape_set_state(s, false, true); - - sx_total = scale_x; - sy_total = scale_y; - } - - userdata->resize_total_scale_x = sx_total; - userdata->resize_total_scale_y = sy_total; - } - else if (userdata->rotate_dragging) - { - float wx, wy; - screen_to_world(userdata, event->mouse_x, event->mouse_y, &wx, &wy); - float angle = atan2f(wy - userdata->rotate_center_y, - wx - userdata->rotate_center_x); - float delta = angle - userdata->rotate_start_angle; - if (delta > GLM_PIf) delta -= 2.0f * GLM_PIf; - else if (delta < -GLM_PIf) delta += 2.0f * GLM_PIf; - float inc = delta - userdata->rotate_total_delta; - - float cos_a = cosf(inc); - float sin_a = sinf(inc); - float cx = userdata->rotate_center_x; - float cy = userdata->rotate_center_y; - - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - if (s->selected) { - float dx = s->cx - cx; - float dy = s->cy - cy; - s->cx = cx + dx * cos_a - dy * sin_a; - s->cy = cy + dx * sin_a + dy * cos_a; - s->rotation += inc; - shape_build_transform(s); - shape_set_state(s, false, true); - } - } - - userdata->rotate_total_delta = delta; - } - else if (userdata->move_dragging) - { - float wx, wy; - screen_to_world(userdata, event->mouse_x, event->mouse_y, &wx, &wy); - float dx = wx - userdata->move_start_wx; - float dy = wy - userdata->move_start_wy; - float delta_x = dx - userdata->move_total_dx; - float delta_y = dy - userdata->move_total_dy; - - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - if (s->selected) { - s->cx += delta_x; - s->cy += delta_y; - shape_build_transform(s); - shape_set_state(s, false, true); - } - } - - userdata->move_total_dx = dx; - userdata->move_total_dy = dy; - } - else if (userdata->selecting) - { - userdata->sel_cx = event->mouse_x; - userdata->sel_cy = event->mouse_y; - float dx = userdata->sel_cx - userdata->sel_sx; - float dy = userdata->sel_cy - userdata->sel_sy; - if (dx * dx + dy * dy > 9.0f) { - userdata->sel_dragging = true; - } - - if (userdata->sel_dragging) { - float wx1, wy1, wx2, wy2; - screen_to_world(userdata, userdata->sel_sx, userdata->sel_sy, &wx1, &wy1); - screen_to_world(userdata, userdata->sel_cx, userdata->sel_cy, &wx2, &wy2); - float min_x = fminf(wx1, wx2), min_y = fminf(wy1, wy2); - float max_x = fmaxf(wx1, wx2), max_y = fmaxf(wy1, wy2); - - userdata->selected_count = spatial_query_rect_select( - &userdata->spatial_grid, &userdata->shapes, - min_x, min_y, max_x, max_y); - - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - shape_set_state(s, false, s->selected); - } - } - } - else - { - float wx, wy; - screen_to_world(userdata, event->mouse_x, event->mouse_y, &wx, &wy); - const float tol = userdata->hover_tol; - - int hovered = spatial_query_point(&userdata->spatial_grid, - &userdata->shapes, wx, wy, tol); - if (hovered != userdata->hovered_shape) { - userdata->hovered_shape = hovered; - EM_ASM({ document.querySelector('canvas').style.cursor = $0 ? 'pointer' : 'default'; }, hovered >= 0); - } - for (int i = 0; i < userdata->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); - shape_set_state(s, i == hovered, s->selected); - } - } - + handle_mouse_move(ud, event); + break; + case SAPP_EVENTTYPE_MOUSE_SCROLL: + handle_scroll_zoom(ud, event); break; - case SAPP_EVENTTYPE_MOUSE_SCROLL: { - if((userdata->zoom >= 6.0f && event->scroll_y > 0.0f) || (userdata->zoom <= 0.1f && event->scroll_y < 0.0f)) - return; - - float wx, wy; - screen_to_world(userdata, event->mouse_x, event->mouse_y, &wx, &wy); - - const float diff = expf(event->scroll_y * 0.1f); - userdata->zoom = _sg_clamp(userdata->zoom * diff, 0.1f, 6.0f); - userdata->hover_tol = SHAPE_HOVER_PX / userdata->zoom; - - const float sx = event->mouse_x - userdata->half_width; - const float sy = userdata->half_height - event->mouse_y; - userdata->pan[0] = sx - wx * userdata->zoom; - userdata->pan[1] = sy - wy * userdata->zoom; - - userdata->overlay_upload_needed = true; - compute_mvp(userdata); - } break; default: break; } } -/** - * Application entry point. Allocates userdata and returns the sokol app - * descriptor wiring init, frame, cleanup, and event callbacks. - * - * @param argc argument count (unused) - * @param argv argument vector (unused) - * @return sokol application descriptor - */ +// -- entry point -- + sapp_desc sokol_main(int argc, char* argv[]) { userdata_t* userdata = (userdata_t*) ALLOC(sizeof(userdata_t)); @@ -1452,4 +1448,4 @@ sapp_desc sokol_main(int argc, char* argv[]) .free_fn = smemtrack_free, }, }; -} \ No newline at end of file +} diff --git a/src/rand.h b/src/rand.h index 7a4abe5..33aff26 100644 --- a/src/rand.h +++ b/src/rand.h @@ -57,7 +57,7 @@ static uint32_t next_int(void) */ static uint32_t next_int_max(uint32_t max) { - return (uint32_t) floorf(xorshift32() / (float) UINT32_MAX * max); + return (uint32_t)((double)xorshift32() / (double)UINT32_MAX * max); } /** * Return a random integer in [min, max]. @@ -68,9 +68,8 @@ static uint32_t next_int_max(uint32_t max) */ static uint32_t next_int_minmax(uint32_t min, uint32_t max) { - const float x = (float) xorshift32() / UINT32_MAX; - //(1.0f - Time) * A + Time * B - return (1.0f - x) * min + x * max; + const double x = (double)xorshift32() / (double)UINT32_MAX; + return (uint32_t)((1.0 - x) * min + x * max); } /** * Return a random float in [0, 1]. @@ -79,7 +78,7 @@ static uint32_t next_int_minmax(uint32_t min, uint32_t max) */ static float next_float(void) { - return (float) xorshift32() / UINT32_MAX; + return (float)((double)xorshift32() / (double)UINT32_MAX); } /** * Return a random float in [0, max]. @@ -89,7 +88,7 @@ static float next_float(void) */ static float next_float_max(float max) { - return (float) xorshift32() / UINT32_MAX * max; + return (float)((double)xorshift32() / (double)UINT32_MAX * max); } /** * Return a random float in [min, max]. @@ -100,8 +99,8 @@ static float next_float_max(float max) */ static float next_float_minmax(float min, float max) { - const float x = (float) xorshift32() / UINT32_MAX; - return (1.0f - x) * min + x * max; + const double x = (double)xorshift32() / (double)UINT32_MAX; + return (float)((1.0 - x) * min + x * max); } #endif \ No newline at end of file diff --git a/src/render.h b/src/render.h new file mode 100644 index 0000000..652b5ca --- /dev/null +++ b/src/render.h @@ -0,0 +1,72 @@ +#ifndef RENDER_H +#define RENDER_H + +#include "api.h" + +static sg_pipeline shape_pipeline; +static sg_shader shape_shader; +static int g_shape_frame_id; + +static void shape_begin_frame(void) +{ + g_shape_frame_id++; +} + +static void shape_init_pipeline(void) +{ + shape_shader = sg_make_shader(&(sg_shader_desc) { + .vertex_func = { + .source = (const char*) src_shaders_shape_wgsl, + .entry = "vs_main", + }, + .fragment_func = { + .source = (const char*) src_shaders_shape_wgsl, + .entry = "fs_main", + }, + .uniform_blocks = { + [0] = { + .size = sizeof(mat4), + .stage = SG_SHADERSTAGE_VERTEX, + .wgsl_group0_binding_n = 0, + }, + [1] = { + .size = sizeof(shape_uniform_t), + .stage = SG_SHADERSTAGE_VERTEX, + .wgsl_group0_binding_n = 1, + }, + }, + .attrs = { + [0] = { .base_type = SG_SHADERATTRBASETYPE_FLOAT }, + }, + .label = "Shape shader", + }); + + shape_pipeline = sg_make_pipeline(&(sg_pipeline_desc) { + .shader = shape_shader, + .index_type = SG_INDEXTYPE_UINT16, + .primitive_type = SG_PRIMITIVETYPE_LINE_STRIP, + .layout.attrs = { + [0].format = SG_VERTEXFORMAT_FLOAT2, + }, + .label = "Shape pipeline", + }); +} + +static void shape_shutdown_pipeline(void) +{ + sg_destroy_pipeline(shape_pipeline); + sg_destroy_shader(shape_shader); +} + +static void shape_draw(shape_t *s, const mat4 *mvp) +{ + sg_apply_uniforms(0, &SG_RANGE(*mvp)); + sg_apply_uniforms(1, &SG_RANGE(s->uniform)); + sg_apply_bindings(&(sg_bindings) { + .vertex_buffers[0] = s->vbuf, + .index_buffer = s->ibuf, + }); + sg_draw(0, s->num_indices, 1); +} + +#endif diff --git a/src/shaders/shape.wgsl b/src/shaders/shape.wgsl index 1c0776a..a3719f5 100644 --- a/src/shaders/shape.wgsl +++ b/src/shaders/shape.wgsl @@ -26,8 +26,8 @@ struct FsOut { @vertex fn vs_main(input: VsIn) -> Vs2Fs { var output: Vs2Fs; - let world_pos = vec4f(input.position.x, input.position.y, 0.0, 1.0) * shape_uniform.transform; - output.pos = world_pos * vs_uniforms.mvp; + let world_pos = shape_uniform.transform * vec4f(input.position.x, input.position.y, 0.0, 1.0); + output.pos = vs_uniforms.mvp * world_pos; if (shape_uniform.state == 2u) { output.color = vec4f(1.0, 0.84, 0.0, 1.0); } else if (shape_uniform.state == 1u) { diff --git a/src/shaders/sprite.wgsl b/src/shaders/sprite.wgsl index af29a8d..f6036c9 100644 --- a/src/shaders/sprite.wgsl +++ b/src/shaders/sprite.wgsl @@ -31,22 +31,6 @@ struct FsO { //Fragment shader output return output; } -// Convert a 32bit uint color (hex representation) into a normalized vec4f -/*fn convertColor(input: u32) -> vec4f { - let r: f32 = f32(((input >> 0) & 0xff)); - let g: f32 = f32(((input >> 8) & 0xff)); - let b: f32 = f32(((input >> 16) & 0xff)); - let a: f32 = f32(((input >> 24) & 0xff)); - - return vec4f(r / 255, g / 255, b / 255, a / 255); -}*/ -// Get the texture array index from the UV -/*fn indexFromCoord(uv: vec2f, width: u32, height: u32) -> u32 { - let x: u32 = clamp(floor(uv.x * f32(width)), 0, width); - let y: u32 = clamp(floor(uv.y * f32(height)), 0, height); - return y * width + x; -}*/ - @fragment fn fs_main(input: Vs2Fs) -> FsO { var output: FsO; diff --git a/src/shape.h b/src/shape.h index c17b914..46b36af 100644 --- a/src/shape.h +++ b/src/shape.h @@ -14,11 +14,6 @@ typedef struct shape_uniform_t { uint8_t _pad[12]; } shape_uniform_t; -typedef enum shape_kind_t { - SHAPE_CIRCLE, - SHAPE_STAR, -} shape_kind_t; - typedef struct shape_t { shape_vertex_t *verts; uint16_t *indices; @@ -30,98 +25,13 @@ typedef struct shape_t { bool hovered; bool selected; - shape_kind_t kind; float cx, cy; float sx, sy; float rotation; - int star_points; - float star_inner_ratio; - int last_update_frame; } shape_t; #define SHAPE_HOVER_PX 6.0f -static sg_pipeline shape_pipeline; -static sg_pipeline overlay_pipeline; -static sg_shader shape_shader; -static int g_shape_frame_id; - -static void shape_begin_frame(void) -{ - g_shape_frame_id++; -} - -/** - * Create the shape shader, shape pipeline (line strip), and overlay pipeline - * (triangles). Call once during app init before drawing any shapes. - */ -static void shape_init_pipeline(void) -{ - shape_shader = sg_make_shader(&(sg_shader_desc) { - .vertex_func = { - .source = (const char*) src_shaders_shape_wgsl, - .entry = "vs_main", - }, - .fragment_func = { - .source = (const char*) src_shaders_shape_wgsl, - .entry = "fs_main", - }, - .uniform_blocks = { - [0] = { - .size = sizeof(mat4), - .stage = SG_SHADERSTAGE_VERTEX, - .wgsl_group0_binding_n = 0, - }, - [1] = { - .size = sizeof(shape_uniform_t), - .stage = SG_SHADERSTAGE_VERTEX, - .wgsl_group0_binding_n = 1, - }, - }, - .attrs = { - [0] = { .base_type = SG_SHADERATTRBASETYPE_FLOAT }, - }, - .label = "Shape shader", - }); - - shape_pipeline = sg_make_pipeline(&(sg_pipeline_desc) { - .shader = shape_shader, - .index_type = SG_INDEXTYPE_UINT16, - .primitive_type = SG_PRIMITIVETYPE_LINE_STRIP, - .layout.attrs = { - [0].format = SG_VERTEXFORMAT_FLOAT2, - }, - .label = "Shape pipeline", - }); - - overlay_pipeline = sg_make_pipeline(&(sg_pipeline_desc) { - .shader = shape_shader, - .index_type = SG_INDEXTYPE_UINT16, - .primitive_type = SG_PRIMITIVETYPE_TRIANGLES, - .layout.attrs = { - [0].format = SG_VERTEXFORMAT_FLOAT2, - }, - .label = "Overlay pipeline", - }); -} - -/** - * Destroy the shape shader and both pipelines. Call during app shutdown. - */ -static void shape_shutdown_pipeline(void) -{ - sg_destroy_pipeline(shape_pipeline); - sg_destroy_pipeline(overlay_pipeline); - sg_destroy_shader(shape_shader); -} - -/** - * Return the number of line segments for a circle of the given radius. - * Clamped to [8, 128]; scales roughly with circumference. - * - * @param r circle radius in world units - * @return segment count - */ static int shape_calc_segments(float r) { int n = (int)(fabsf(r) * 0.5f) + 16; @@ -130,13 +40,6 @@ static int shape_calc_segments(float r) return n; } -/** - * Set default state for a newly created shape: identity transform, base color, - * not hovered, not selected. - * - * @param s shape to initialize - * @param color RGBA base color (copied) - */ static void shape_init_common(shape_t *s, const float color[4]) { s->hovered = false; @@ -146,34 +49,25 @@ static void shape_init_common(shape_t *s, const float color[4]) memset(s->uniform._pad, 0, sizeof(s->uniform._pad)); } -/** - * Build the per-shape transform matrix from cx, cy, rotation. - * Uses R(-angle) so the shader's row-vector convention matches the existing - * world-space vertex computation. - */ static void shape_build_transform(shape_t *s) { mat4 T, R, S, RS; glm_translate_make(T, (vec3){s->cx, s->cy, 0.0f}); - glm_rotate_make(R, -s->rotation, (vec3){0.0f, 0.0f, 1.0f}); + glm_rotate_make(R, s->rotation, (vec3){0.0f, 0.0f, 1.0f}); glm_scale_make(S, (vec3){s->sx, s->sy, 1.0f}); glm_mat4_mul(R, S, RS); glm_mat4_mul(T, RS, s->uniform.transform); } -/** - * Create GPU vertex and index buffers from the shape's current verts/indices. - * - * @param s shape (must have verts and indices allocated) - */ static void shape_make_buffers(shape_t *s) { + uint32_t vcount = s->num_verts + 1; s->vbuf = sg_make_buffer(&(sg_buffer_desc) { - .size = s->num_indices * sizeof(shape_vertex_t), + .size = (size_t)vcount * sizeof(shape_vertex_t), .usage = { .stream_update = true }, .label = "Shape vertices", }); - sg_update_buffer(s->vbuf, &(sg_range){s->verts, s->num_indices * sizeof(shape_vertex_t)}); + sg_update_buffer(s->vbuf, &(sg_range){s->verts, (size_t)vcount * sizeof(shape_vertex_t)}); s->ibuf = sg_make_buffer(&(sg_buffer_desc) { .usage = { .index_buffer = true }, .data = { s->indices, s->num_indices * sizeof(uint16_t) }, @@ -181,11 +75,6 @@ static void shape_make_buffers(shape_t *s) }); } -/** - * Destroy GPU buffers and free vertex/index arrays for a single shape. - * - * @param s shape to tear down - */ static void shape_shutdown(shape_t *s) { sg_destroy_buffer(s->vbuf); @@ -194,74 +83,11 @@ static void shape_shutdown(shape_t *s) FREE(s->indices); } -/** - * Rebuild vertex and index data from the shape's current parameters (position, - * scale, rotation, kind), then recreate GPU buffers. Call after any parameter - * change. - * - * @param s shape to regenerate - */ static void shape_regenerate(shape_t *s) { - int n, count; - if (s->kind == SHAPE_CIRCLE) { - int segs = shape_calc_segments(s->sx); - n = segs; - count = segs + 1; - } else { - n = s->star_points * 2; - count = n + 1; - } - - bool resized = ((uint32_t)count != s->num_indices); - if (resized) { - sg_destroy_buffer(s->vbuf); - sg_destroy_buffer(s->ibuf); - FREE(s->verts); - FREE(s->indices); - s->verts = (shape_vertex_t*) ALLOC(count * sizeof(shape_vertex_t)); - s->indices = (uint16_t*) ALLOC(count * sizeof(uint16_t)); - } - - if (s->kind == SHAPE_CIRCLE) { - int segs = n; - for (int i = 0; i < segs; i++) { - float a = (float)i / (float)segs * 2.0f * GLM_PIf - GLM_PI_2f; - s->verts[i] = (shape_vertex_t) { cosf(a), sinf(a) }; - } - s->verts[segs] = s->verts[0]; - } else { - for (int i = 0; i < n; i++) { - float a = (float)i / (float)n * 2.0f * GLM_PIf - GLM_PI_2f; - float r = (i & 1) ? s->star_inner_ratio : 1.0f; - s->verts[i] = (shape_vertex_t) { cosf(a) * r, sinf(a) * r }; - } - s->verts[n] = s->verts[0]; - } - - s->num_indices = (uint32_t)count; - s->num_verts = (uint32_t)n; - for (int i = 0; i <= n; i++) s->indices[i] = (uint16_t)i; - shape_build_transform(s); - - if (resized) { - shape_make_buffers(s); - s->last_update_frame = g_shape_frame_id; - } else if (s->last_update_frame != g_shape_frame_id) { - sg_update_buffer(s->vbuf, &(sg_range){s->verts, (size_t)count * sizeof(shape_vertex_t)}); - s->last_update_frame = g_shape_frame_id; - } } -/** - * Update hovered/selected flags and the shader uniform state. - * State is 0=normal, 1=hovered (brightened), 2=selected (green). - * - * @param s shape to update - * @param hovered true if cursor is over the shape - * @param selected true if shape is in the selection set - */ static void shape_set_state(shape_t *s, bool hovered, bool selected) { s->hovered = hovered; @@ -269,16 +95,6 @@ static void shape_set_state(shape_t *s, bool hovered, bool selected) s->uniform.state = selected ? 2u : (hovered ? 1u : 0u); } -/** - * Ray-casting point-in-polygon test. Handles arbitrary non-self-intersecting - * polygons. - * - * @param px point X in world space - * @param py point Y in world space - * @param verts polygon vertices - * @param n vertex count - * @return true if the point is inside the polygon - */ static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t n) { bool inside = false; @@ -291,17 +107,6 @@ static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t return inside; } -/** - * Test whether a world-space point hits this shape. Transforms the query - * to local space (verts are now stored relative to origin), then tests - * polygon containment and edge proximity. - * - * @param s shape to test - * @param wx point X in world space - * @param wy point Y in world space - * @param world_tol hit tolerance in world units - * @return true if the point hits the shape - */ static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol) { float sc = cosf(s->rotation), ss = sinf(s->rotation); @@ -330,37 +135,9 @@ static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol) return false; } -/** - * Issue a draw call for this shape using the shape line-strip pipeline. - * - * @param s shape to draw - * @param mvp model-view-projection matrix (from compute_mvp) - */ -static void shape_draw(shape_t *s, const mat4 *mvp) -{ - sg_apply_uniforms(0, &SG_RANGE(*mvp)); - sg_apply_uniforms(1, &SG_RANGE(s->uniform)); - sg_apply_bindings(&(sg_bindings) { - .vertex_buffers[0] = s->vbuf, - .index_buffer = s->ibuf, - }); - sg_draw(0, s->num_indices, 1); -} - -/** - * Create a circle shape (returned by value). Allocates verts/indices and GPU - * buffers. The number of line segments adapts to radius. - * - * @param x center X in world space - * @param y center Y in world space - * @param r radius in world units - * @param color RGBA base color - * @return fully initialized shape_t - */ static shape_t shape_circle(float x, float y, float r, const float color[4]) { shape_t s; - s.kind = SHAPE_CIRCLE; s.cx = x; s.cy = y; s.sx = r; s.sy = r; s.rotation = 0.0f; @@ -385,38 +162,23 @@ static shape_t shape_circle(float x, float y, float r, const float color[4]) return s; } -/** - * Create a star shape (returned by value). Alternates between outer_r and - * inner_r at each vertex, producing a star with the given number of points. - * Allocates verts/indices and GPU buffers. - * - * @param x center X in world space - * @param y center Y in world space - * @param outer_r outer radius in world units - * @param inner_r inner radius in world units - * @param points number of star points - * @param color RGBA base color - * @return fully initialized shape_t - */ static shape_t shape_star(float x, float y, float outer_r, float inner_r, int points, const float color[4]) { shape_t s; - s.kind = SHAPE_STAR; s.cx = x; s.cy = y; s.sx = outer_r; s.sy = outer_r; s.rotation = 0.0f; - s.star_points = points; - s.star_inner_ratio = inner_r / outer_r; int n = points * 2; int count = n + 1; s.verts = (shape_vertex_t*) ALLOC(count * sizeof(shape_vertex_t)); s.indices = (uint16_t*) ALLOC(count * sizeof(uint16_t)); + float inner_ratio = inner_r / outer_r; for (int i = 0; i < n; i++) { float a = (float)i / (float)n * 2.0f * GLM_PIf - GLM_PI_2f; - float r = (i & 1) ? s.star_inner_ratio : 1.0f; + float r = (i & 1) ? inner_ratio : 1.0f; s.verts[i] = (shape_vertex_t) { cosf(a) * r, sinf(a) * r }; } s.verts[n] = s.verts[0]; diff --git a/src/spatial.h b/src/spatial.h index bcc9f6b..d3dd24a 100644 --- a/src/spatial.h +++ b/src/spatial.h @@ -86,9 +86,11 @@ static void spatial_rebuild(spatial_grid_t *grid, vector_t *shapes) int ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE); int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1); - while (grid->slots[idx].occupied) { + int probe = 0; + while (grid->slots[idx].occupied && probe < SPATIAL_HASH_SIZE) { if (grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy) break; idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1); + probe++; } if (!grid->slots[idx].occupied) { @@ -118,9 +120,12 @@ static void spatial_rebuild(spatial_grid_t *grid, vector_t *shapes) int ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE); int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1); + int probe = 0; while (!(grid->slots[idx].occupied && - grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy)) { + grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy) && + probe < SPATIAL_HASH_SIZE) { idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1); + probe++; } spatial_entry_t *e = &grid->slots[idx].entries[grid->slots[idx].count++];