Reorganized the code structure, fix a lot of issues.

This commit is contained in:
2026-04-28 21:05:43 +02:00
parent 81616f8a5d
commit 9ce6e4accd
14 changed files with 1213 additions and 1373 deletions

View File

@@ -9,6 +9,7 @@ A browser-based world map creation tool (like Wonderdraft/Inkarnate). C99 compil
- **UI:** Dear ImGui via cimgui — `lib/imgui/` - **UI:** Dear ImGui via cimgui — `lib/imgui/`
- **Math:** cglm (types are C arrays: `vec2` = `float[2]`, `mat4` = `float[4][4]` column-major) — `lib/cglm/` - **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/` - **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 ### Build
- `make` (release) / `make debug` — outputs `app.html` - `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` - Include paths: `lib/sokol`, `lib/imgui`, `lib/imgui/imgui`, `lib/util`, `lib/cglm/include`
### Key files ### Key files
- `src/main.c` — entry point, sokol init, render loop, input (zoom/pan/drag) - `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 - `src/api.h` — central include hub, backend defines, ALLOC/FREE macros (wired to smemtrack)
- `src/sprite.h`sprite batching, texture manager, file import stubs - `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/util.h``vector_t` (dynamic array) and `mem_pool_t` (free-list pool), both stripe-based
- `src/rand.h` — xorshift32 PRNG - `src/rand.h` — xorshift32 PRNG

View File

@@ -1,18 +1,14 @@
# Cartograph # 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: - **Shapes** — procedural circles and stars with per-instance transforms (position, scale, rotation)
- **Height** — additive (raise terrain) or subtractive (lower terrain) - **Shape editing** — select, move, rotate, scale individual shapes; rect-select multiple shapes
- **Biome** — determines the sampled texture applied to the shape - **Undo/redo** — property-level history stack (position, scale, rotation, color) with edit session capture and batch operations
- **Intensity** — blending weight of the biome texture - **Spatial index** — hash grid for fast hit testing and rect-selection queries on large shape counts
- **Roughness** — edge noise amount (voronoi-like jagged edges at maximum) - **Viewport** — zoom and pan with screen↔world coordinate transforms
- **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
## Tech stack ## Tech stack
@@ -44,12 +40,16 @@ Output is `app.html`, served by Emscripten's built-in web server or any static s
``` ```
src/ src/
main.c Entry point, sokol init, render loop, input handling main.c Entry point, sokol init, render loop, input handling, UI panels, debug stats
api.h Central include — all library headers and project-wide defines api.h Central include — all library headers, ALLOC/FREE macros
sprite.h Sprite batching, texture management, file import 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 util.h Vector (dynamic array) and memory pool data structures
rand.h Xorshift32 PRNG utilities rand.h Xorshift32 PRNG utilities
shaders/ WGSL shader sources shaders/ WGSL shader sources (shape, sprite)
generated/ xxd-generated C headers from shaders generated/ xxd-generated C headers from shaders
lib/ lib/
sokol/ Sokol single-file headers (gfx, app, glue, log) sokol/ Sokol single-file headers (gfx, app, glue, log)

View File

@@ -23,12 +23,14 @@
#include "cglm/cglm.h" #include "cglm/cglm.h"
#include "rand.h" #include "rand.h"
#include "camera.h"
#include "generated/sprite.h" #include "generated/sprite.h"
#include "generated/shape.h" #include "generated/shape.h"
#include "util.h" #include "util.h"
#include "shape.h" #include "shape.h"
#include "render.h"
#include "spatial.h" #include "spatial.h"
#include "history.h" #include "history.h"

57
src/camera.h Normal file
View File

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

View File

@@ -44,17 +44,17 @@ unsigned char src_shaders_shape_wgsl[] = {
0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74,
0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b, 0x0a, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, 0x72, 0x6c,
0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, 0x70,
0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x74, 0x72,
0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x2a, 0x20, 0x76, 0x65,
0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x79, 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f,
0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x29, 0x20, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69, 0x6e,
0x2a, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
0x6f, 0x72, 0x6d, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30,
0x6d, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75,
0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x73, 0x5f, 0x75,
0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x2a, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, 0x20,
0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, 0x3b, 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x3b,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61,
0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x73, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x73,
0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, 0x20, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, 0x20,

View File

@@ -74,60 +74,7 @@ unsigned char src_shaders_sprite_wgsl[] = {
0x70, 0x75, 0x74, 0x2e, 0x75, 0x76, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x70, 0x70, 0x75, 0x74, 0x2e, 0x75, 0x76, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x70,
0x75, 0x74, 0x2e, 0x75, 0x76, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x75, 0x74, 0x2e, 0x75, 0x76, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74,
0x70, 0x75, 0x74, 0x3b, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x2f, 0x70, 0x75, 0x74, 0x3b, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x40,
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,
0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20,
0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75,
0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, 0x3e, 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, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b,
0x0d, 0x0a, 0x7d 0x0d, 0x0a, 0x7d
}; };
unsigned int src_shaders_sprite_wgsl_len = 1695; unsigned int src_shaders_sprite_wgsl_len = 1059;

View File

@@ -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. * 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 shapes the shapes vector to modify
* @param forward true to use new_val (redo), false to use old_val (undo) * @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++) { for (int i = 0; i < entry->count; i++) {
hist_change_t *c = &entry->changes[i]; hist_change_t *c = &entry->changes[i];
if (c->shape_index >= shapes->count) continue; 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
} }
} }
/** static bool history_undo(history_t *h, vector_t *shapes) {
* 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) {
if (h->current < 0) return false; if (h->current < 0) return false;
history_apply_entry(&h->entries[h->current], shapes, false); history_apply_entry(&h->entries[h->current], shapes, false);
h->current--; h->current--;
(void)selected_count;
return true; return true;
} }
/** static bool history_redo(history_t *h, vector_t *shapes) {
* 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) {
if (h->current + 1 >= h->count) return false; if (h->current + 1 >= h->count) return false;
h->current++; h->current++;
history_apply_entry(&h->entries[h->current], shapes, true); history_apply_entry(&h->entries[h->current], shapes, true);
(void)selected_count;
return true; return true;
} }

1416
src/main.c

File diff suppressed because it is too large Load Diff

View File

@@ -57,7 +57,7 @@ static uint32_t next_int(void)
*/ */
static uint32_t next_int_max(uint32_t max) 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]. * 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) static uint32_t next_int_minmax(uint32_t min, uint32_t max)
{ {
const float x = (float) xorshift32() / UINT32_MAX; const double x = (double)xorshift32() / (double)UINT32_MAX;
//(1.0f - Time) * A + Time * B return (uint32_t)((1.0 - x) * min + x * max);
return (1.0f - x) * min + x * max;
} }
/** /**
* Return a random float in [0, 1]. * 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) static float next_float(void)
{ {
return (float) xorshift32() / UINT32_MAX; return (float)((double)xorshift32() / (double)UINT32_MAX);
} }
/** /**
* Return a random float in [0, max]. * Return a random float in [0, max].
@@ -89,7 +88,7 @@ static float next_float(void)
*/ */
static float next_float_max(float max) 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]. * 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) static float next_float_minmax(float min, float max)
{ {
const float x = (float) xorshift32() / UINT32_MAX; const double x = (double)xorshift32() / (double)UINT32_MAX;
return (1.0f - x) * min + x * max; return (float)((1.0 - x) * min + x * max);
} }
#endif #endif

72
src/render.h Normal file
View File

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

View File

@@ -26,8 +26,8 @@ struct FsOut {
@vertex fn vs_main(input: VsIn) -> Vs2Fs { @vertex fn vs_main(input: VsIn) -> Vs2Fs {
var output: Vs2Fs; var output: Vs2Fs;
let world_pos = vec4f(input.position.x, input.position.y, 0.0, 1.0) * shape_uniform.transform; let world_pos = shape_uniform.transform * vec4f(input.position.x, input.position.y, 0.0, 1.0);
output.pos = world_pos * vs_uniforms.mvp; output.pos = vs_uniforms.mvp * world_pos;
if (shape_uniform.state == 2u) { if (shape_uniform.state == 2u) {
output.color = vec4f(1.0, 0.84, 0.0, 1.0); output.color = vec4f(1.0, 0.84, 0.0, 1.0);
} else if (shape_uniform.state == 1u) { } else if (shape_uniform.state == 1u) {

View File

@@ -31,22 +31,6 @@ struct FsO { //Fragment shader output
return 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 { @fragment fn fs_main(input: Vs2Fs) -> FsO {
var output: FsO; var output: FsO;

View File

@@ -14,11 +14,6 @@ typedef struct shape_uniform_t {
uint8_t _pad[12]; uint8_t _pad[12];
} shape_uniform_t; } shape_uniform_t;
typedef enum shape_kind_t {
SHAPE_CIRCLE,
SHAPE_STAR,
} shape_kind_t;
typedef struct shape_t { typedef struct shape_t {
shape_vertex_t *verts; shape_vertex_t *verts;
uint16_t *indices; uint16_t *indices;
@@ -30,98 +25,13 @@ typedef struct shape_t {
bool hovered; bool hovered;
bool selected; bool selected;
shape_kind_t kind;
float cx, cy; float cx, cy;
float sx, sy; float sx, sy;
float rotation; float rotation;
int star_points;
float star_inner_ratio;
int last_update_frame;
} shape_t; } shape_t;
#define SHAPE_HOVER_PX 6.0f #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) static int shape_calc_segments(float r)
{ {
int n = (int)(fabsf(r) * 0.5f) + 16; int n = (int)(fabsf(r) * 0.5f) + 16;
@@ -130,13 +40,6 @@ static int shape_calc_segments(float r)
return n; 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]) static void shape_init_common(shape_t *s, const float color[4])
{ {
s->hovered = false; 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)); 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) static void shape_build_transform(shape_t *s)
{ {
mat4 T, R, S, RS; mat4 T, R, S, RS;
glm_translate_make(T, (vec3){s->cx, s->cy, 0.0f}); 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_scale_make(S, (vec3){s->sx, s->sy, 1.0f});
glm_mat4_mul(R, S, RS); glm_mat4_mul(R, S, RS);
glm_mat4_mul(T, RS, s->uniform.transform); 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) static void shape_make_buffers(shape_t *s)
{ {
uint32_t vcount = s->num_verts + 1;
s->vbuf = sg_make_buffer(&(sg_buffer_desc) { 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 }, .usage = { .stream_update = true },
.label = "Shape vertices", .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) { s->ibuf = sg_make_buffer(&(sg_buffer_desc) {
.usage = { .index_buffer = true }, .usage = { .index_buffer = true },
.data = { s->indices, s->num_indices * sizeof(uint16_t) }, .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) static void shape_shutdown(shape_t *s)
{ {
sg_destroy_buffer(s->vbuf); sg_destroy_buffer(s->vbuf);
@@ -194,74 +83,11 @@ static void shape_shutdown(shape_t *s)
FREE(s->indices); 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) 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); 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) static void shape_set_state(shape_t *s, bool hovered, bool selected)
{ {
s->hovered = hovered; 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); 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) static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t n)
{ {
bool inside = false; bool inside = false;
@@ -291,17 +107,6 @@ static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t
return inside; 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) static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol)
{ {
float sc = cosf(s->rotation), ss = sinf(s->rotation); 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; 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]) static shape_t shape_circle(float x, float y, float r, const float color[4])
{ {
shape_t s; shape_t s;
s.kind = SHAPE_CIRCLE;
s.cx = x; s.cy = y; s.cx = x; s.cy = y;
s.sx = r; s.sy = r; s.sx = r; s.sy = r;
s.rotation = 0.0f; 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; 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, static shape_t shape_star(float x, float y, float outer_r, float inner_r,
int points, const float color[4]) int points, const float color[4])
{ {
shape_t s; shape_t s;
s.kind = SHAPE_STAR;
s.cx = x; s.cy = y; s.cx = x; s.cy = y;
s.sx = outer_r; s.sy = outer_r; s.sx = outer_r; s.sy = outer_r;
s.rotation = 0.0f; s.rotation = 0.0f;
s.star_points = points;
s.star_inner_ratio = inner_r / outer_r;
int n = points * 2; int n = points * 2;
int count = n + 1; int count = n + 1;
s.verts = (shape_vertex_t*) ALLOC(count * sizeof(shape_vertex_t)); s.verts = (shape_vertex_t*) ALLOC(count * sizeof(shape_vertex_t));
s.indices = (uint16_t*) ALLOC(count * sizeof(uint16_t)); s.indices = (uint16_t*) ALLOC(count * sizeof(uint16_t));
float inner_ratio = inner_r / outer_r;
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
float a = (float)i / (float)n * 2.0f * GLM_PIf - GLM_PI_2f; 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[i] = (shape_vertex_t) { cosf(a) * r, sinf(a) * r };
} }
s.verts[n] = s.verts[0]; s.verts[n] = s.verts[0];

View File

@@ -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 ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE);
int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1); 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; if (grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy) break;
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1); idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
probe++;
} }
if (!grid->slots[idx].occupied) { 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 ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE);
int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1); int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1);
int probe = 0;
while (!(grid->slots[idx].occupied && 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); idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
probe++;
} }
spatial_entry_t *e = &grid->slots[idx].entries[grid->slots[idx].count++]; spatial_entry_t *e = &grid->slots[idx].entries[grid->slots[idx].count++];