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/`
- **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

View File

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

View File

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

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

View File

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

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

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

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

View File

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

View File

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

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 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++];