History, spatial grid and optimizations

This commit is contained in:
2026-04-28 19:02:00 +02:00
parent 5881a7dafc
commit 81616f8a5d
8 changed files with 1630 additions and 171 deletions

View File

@@ -30,7 +30,8 @@ EMCC_FLAGS = --use-port=emdawnwebgpu \
-sWASM_BIGINT \ -sWASM_BIGINT \
-sALLOW_MEMORY_GROWTH \ -sALLOW_MEMORY_GROWTH \
-msimd128 \ -msimd128 \
-sFILESYSTEM=0 -sFILESYSTEM=0 \
-flto
# Shell template # Shell template
SHELL_FILE = shell.html SHELL_FILE = shell.html

View File

@@ -29,6 +29,8 @@
#include "util.h" #include "util.h"
#include "shape.h" #include "shape.h"
#include "spatial.h"
#include "history.h"
#include <emscripten.h> #include <emscripten.h>
#include <stdio.h> #include <stdio.h>

254
src/history.h Normal file
View File

@@ -0,0 +1,254 @@
#ifndef HISTORY_H
#define HISTORY_H
#include "api.h"
// Each property kind we can undo/redo independently
typedef enum {
HIST_POSITION,
HIST_SCALE,
HIST_ROTATION,
HIST_COLOR,
} hist_prop_t;
// One property change on one shape (old → new)
typedef struct hist_change_t {
int shape_index;
hist_prop_t prop;
float old_val[4];
float new_val[4];
} hist_change_t;
// A history entry is one or more changes batched together.
// Single-property edits = 1 change. Whole-selection edits = N changes.
typedef struct hist_entry_t {
hist_change_t *changes;
int count;
} hist_entry_t;
#define HIST_MAX 64
typedef struct history_t {
hist_entry_t entries[HIST_MAX];
int count;
int current; // index of last applied entry, -1 = initial state
// Pending edit session (one ImGui widget interaction)
bool capturing;
int pending_shape_idx;
hist_prop_t pending_prop;
float pending_old[4];
} history_t;
// -- internal helpers --
/**
* Read the current value of a single property from a shape.
*
* @param s shape to read from
* @param prop which property (HIST_POSITION, HIST_SCALE, etc.)
* @param out receives the value, zero-padded to 4 floats
*/
static void hist_read_prop(shape_t *s, hist_prop_t prop, float out[4]) {
memset(out, 0, sizeof(float[4]));
switch (prop) {
case HIST_POSITION: out[0] = s->cx; out[1] = s->cy; break;
case HIST_SCALE: out[0] = s->sx; out[1] = s->sy; break;
case HIST_ROTATION: out[0] = s->rotation; break;
case HIST_COLOR: memcpy(out, s->uniform.base_color, sizeof(float[4])); break;
}
}
/**
* Write a value to a single property of a shape. Does NOT regenerate buffers.
*
* @param s shape to modify in-place
* @param prop which property to set
* @param val new value (4 floats, zero-padded for smaller properties)
*/
static void hist_apply_prop(shape_t *s, hist_prop_t prop, const float val[4]) {
switch (prop) {
case HIST_POSITION: s->cx = val[0]; s->cy = val[1]; break;
case HIST_SCALE: s->sx = val[0]; s->sy = val[1]; break;
case HIST_ROTATION: s->rotation = val[0]; break;
case HIST_COLOR: memcpy(s->uniform.base_color, val, sizeof(float[4])); break;
}
}
// -- history API --
/**
* Zero-initialize the history stack. Call once during app init.
*
* @param h history to initialize
*/
static void history_init(history_t *h) {
memset(h, 0, sizeof(*h));
h->current = -1;
}
/**
* Free all heap memory held by the history stack. Call during app shutdown.
*
* @param h history to destroy
*/
static void history_destroy(history_t *h) {
for (int i = 0; i < h->count; i++) {
if (h->entries[i].changes) FREE(h->entries[i].changes);
}
memset(h, 0, sizeof(*h));
h->current = -1;
}
/**
* Push a completed entry onto the stack, discarding any redo branch.
* Takes ownership of entry.changes (must be heap-allocated with ALLOC).
* Used internally by begin_edit/end_edit, or directly for batch edits.
*
* @param h history stack
* @param entry entry to push (changes array is consumed, not copied)
*/
static void history_push_entry(history_t *h, hist_entry_t entry) {
while (h->count > h->current + 1) {
h->count--;
if (h->entries[h->count].changes) {
FREE(h->entries[h->count].changes);
h->entries[h->count].changes = NULL;
}
}
if (h->count >= HIST_MAX) {
if (h->entries[0].changes) FREE(h->entries[0].changes);
memmove(&h->entries[0], &h->entries[1],
(h->count - 1) * sizeof(hist_entry_t));
h->count--;
h->current--;
}
h->entries[h->count] = entry;
h->count++;
h->current = h->count - 1;
}
/**
* Begin capturing an edit session. Snapshots the current value of one property.
* If a prior session is still open (e.g. user switched widgets in the same frame),
* it is finalized and pushed first.
* Call when igIsItemActivated() is true after an ImGui widget.
*
* @param h history stack
* @param shapes the shapes vector (used to read current values)
* @param shape_idx index of the shape being edited
* @param prop which property is about to change
*/
static void history_begin_edit(history_t *h, vector_t *shapes,
int shape_idx, hist_prop_t prop) {
if (h->capturing) {
shape_t *s = (shape_t*) vec_get(shapes, h->pending_shape_idx);
float new_val[4];
hist_read_prop(s, h->pending_prop, new_val);
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) {
hist_change_t change = {
.shape_index = h->pending_shape_idx,
.prop = h->pending_prop,
};
memcpy(change.old_val, h->pending_old, sizeof(float[4]));
memcpy(change.new_val, new_val, sizeof(float[4]));
hist_entry_t entry = { .changes = NULL, .count = 1 };
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t));
*entry.changes = change;
history_push_entry(h, entry);
}
h->capturing = false;
}
h->capturing = true;
h->pending_shape_idx = shape_idx;
h->pending_prop = prop;
shape_t *s = (shape_t*) vec_get(shapes, shape_idx);
hist_read_prop(s, prop, h->pending_old);
}
/**
* End the current edit session and push an entry if the value changed.
* Safe to call when no session is active (no-op).
* Call when igIsAnyItemActive() transitions from true to false.
*
* @param h history stack
* @param shapes the shapes vector (used to read final values)
*/
static void history_end_edit(history_t *h, vector_t *shapes) {
if (!h->capturing) return;
shape_t *s = (shape_t*) vec_get(shapes, h->pending_shape_idx);
float new_val[4];
hist_read_prop(s, h->pending_prop, new_val);
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) {
hist_change_t change = {
.shape_index = h->pending_shape_idx,
.prop = h->pending_prop,
};
memcpy(change.old_val, h->pending_old, sizeof(float[4]));
memcpy(change.new_val, new_val, sizeof(float[4]));
hist_entry_t entry = { .changes = NULL, .count = 1 };
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t));
*entry.changes = change;
history_push_entry(h, entry);
}
h->capturing = false;
}
/**
* Apply every change in an entry to the shapes vector and regenerate buffers.
*
* @param entry the history entry to apply
* @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) {
for (int i = 0; i < entry->count; i++) {
hist_change_t *c = &entry->changes[i];
if (c->shape_index >= shapes->count) continue;
shape_t *s = (shape_t*) vec_get(shapes, c->shape_index);
hist_apply_prop(s, c->prop, forward ? c->new_val : c->old_val);
shape_regenerate(s);
shape_set_state(s, s->hovered, s->selected);
}
}
/**
* 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;
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) {
if (h->current + 1 >= h->count) return false;
h->current++;
history_apply_entry(&h->entries[h->current], shapes, true);
(void)selected_count;
return true;
}
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,11 @@ static float next_float(void);
static float next_float_max(float max); 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);
/**
* Xorshift32 PRNG core. Advances the global seed and returns the new value.
*
* @return pseudo-random 32-bit integer
*/
static uint32_t xorshift32(void) static uint32_t xorshift32(void)
{ {
seed ^= seed<<13; seed ^= seed<<13;
@@ -21,6 +26,12 @@ static uint32_t xorshift32(void)
seed ^= seed<<5; seed ^= seed<<5;
return seed; return seed;
} }
/**
* Seed the global PRNG state. Zero is ignored (caller should pass a non-zero
* seed). Runs the generator once after seeding to mix the state.
*
* @param _seed non-zero 32-bit seed value
*/
static void rand_seed(uint32_t _seed) static void rand_seed(uint32_t _seed)
{ {
if(_seed == 0) if(_seed == 0)
@@ -29,34 +40,64 @@ static void rand_seed(uint32_t _seed)
seed = _seed; seed = _seed;
xorshift32(); xorshift32();
} }
// PRNG [0-UINT32_MAX] /**
* Return a random integer in [0, UINT32_MAX].
*
* @return pseudo-random 32-bit integer
*/
static uint32_t next_int(void) static uint32_t next_int(void)
{ {
return xorshift32(); return xorshift32();
} }
// PRNG [0-max] /**
* Return a random integer in [0, max].
*
* @param max inclusive upper bound
* @return pseudo-random integer
*/
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) floorf(xorshift32() / (float) UINT32_MAX * max);
} }
// PRNG [min-max] /**
* Return a random integer in [min, max].
*
* @param min inclusive lower bound
* @param max inclusive upper bound
* @return pseudo-random integer
*/
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 float x = (float) xorshift32() / UINT32_MAX;
//(1.0f - Time) * A + Time * B //(1.0f - Time) * A + Time * B
return (1.0f - x) * min + x * max; return (1.0f - x) * min + x * max;
} }
// PRNG [0-1] /**
* Return a random float in [0, 1].
*
* @return pseudo-random float
*/
static float next_float(void) static float next_float(void)
{ {
return (float) xorshift32() / UINT32_MAX; return (float) xorshift32() / UINT32_MAX;
} }
// PRNG [0-max] /**
* Return a random float in [0, max].
*
* @param max inclusive upper bound
* @return pseudo-random float
*/
static float next_float_max(float max) static float next_float_max(float max)
{ {
return (float) xorshift32() / UINT32_MAX * max; return (float) xorshift32() / UINT32_MAX * max;
} }
// PRNG [min-max] /**
* Return a random float in [min, max].
*
* @param min inclusive lower bound
* @param max inclusive upper bound
* @return pseudo-random float
*/
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 float x = (float) xorshift32() / UINT32_MAX;

View File

@@ -36,6 +36,7 @@ typedef struct shape_t {
float rotation; float rotation;
int star_points; int star_points;
float star_inner_ratio; float star_inner_ratio;
int last_update_frame;
} shape_t; } shape_t;
#define SHAPE_HOVER_PX 6.0f #define SHAPE_HOVER_PX 6.0f
@@ -43,7 +44,17 @@ typedef struct shape_t {
static sg_pipeline shape_pipeline; static sg_pipeline shape_pipeline;
static sg_pipeline overlay_pipeline; static sg_pipeline overlay_pipeline;
static sg_shader shape_shader; 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) static void shape_init_pipeline(void)
{ {
shape_shader = sg_make_shader(&(sg_shader_desc) { shape_shader = sg_make_shader(&(sg_shader_desc) {
@@ -94,6 +105,9 @@ static void shape_init_pipeline(void)
}); });
} }
/**
* Destroy the shape shader and both pipelines. Call during app shutdown.
*/
static void shape_shutdown_pipeline(void) static void shape_shutdown_pipeline(void)
{ {
sg_destroy_pipeline(shape_pipeline); sg_destroy_pipeline(shape_pipeline);
@@ -101,6 +115,13 @@ static void shape_shutdown_pipeline(void)
sg_destroy_shader(shape_shader); 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;
@@ -109,22 +130,50 @@ 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;
s->selected = false; s->selected = false;
glm_mat4_identity(s->uniform.transform);
memcpy(s->uniform.base_color, color, sizeof(float[4])); memcpy(s->uniform.base_color, color, sizeof(float[4]));
s->uniform.state = 0; s->uniform.state = 0;
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)
{
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_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) static void shape_make_buffers(shape_t *s)
{ {
s->vbuf = sg_make_buffer(&(sg_buffer_desc) { s->vbuf = sg_make_buffer(&(sg_buffer_desc) {
.data = { s->verts, s->num_indices * sizeof(shape_vertex_t) }, .size = s->num_indices * sizeof(shape_vertex_t),
.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)});
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) },
@@ -132,6 +181,11 @@ 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);
@@ -140,50 +194,47 @@ 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)
{ {
sg_destroy_buffer(s->vbuf);
sg_destroy_buffer(s->ibuf);
int n, count; int n, count;
if (s->kind == SHAPE_CIRCLE) { if (s->kind == SHAPE_CIRCLE) {
int segs = shape_calc_segments(s->sx); int segs = shape_calc_segments(s->sx);
n = segs; n = segs;
count = segs + 1; count = segs + 1;
if (s->num_indices != (uint32_t)count) {
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));
}
for (int i = 0; i < segs; i++) {
float a = (float)i / (float)segs * 2.0f * GLM_PIf - GLM_PI_2f + s->rotation;
s->verts[i] = (shape_vertex_t) {
s->cx + cosf(a) * s->sx,
s->cy + sinf(a) * s->sy,
};
}
s->verts[segs] = s->verts[0];
} else { } else {
n = s->star_points * 2; n = s->star_points * 2;
count = n + 1; count = n + 1;
}
if (s->num_indices != (uint32_t)count) { 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->verts);
FREE(s->indices); FREE(s->indices);
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));
} }
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++) { for (int i = 0; i < n; i++) {
float a = (float)i / (float)n * 2.0f * GLM_PIf - GLM_PI_2f + s->rotation; float a = (float)i / (float)n * 2.0f * GLM_PIf - GLM_PI_2f;
float r = (i & 1) ? s->star_inner_ratio * s->sx : s->sx; float r = (i & 1) ? s->star_inner_ratio : 1.0f;
s->verts[i] = (shape_vertex_t) { s->verts[i] = (shape_vertex_t) { cosf(a) * r, sinf(a) * r };
s->cx + cosf(a) * r,
s->cy + sinf(a) * r,
};
} }
s->verts[n] = s->verts[0]; s->verts[n] = s->verts[0];
} }
@@ -192,9 +243,25 @@ static void shape_regenerate(shape_t *s)
s->num_verts = (uint32_t)n; s->num_verts = (uint32_t)n;
for (int i = 0; i <= n; i++) s->indices[i] = (uint16_t)i; for (int i = 0; i <= n; i++) s->indices[i] = (uint16_t)i;
shape_build_transform(s);
if (resized) {
shape_make_buffers(s); 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;
@@ -202,6 +269,16 @@ 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;
@@ -214,11 +291,28 @@ 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 tol_sq = world_tol * world_tol; float sc = cosf(s->rotation), ss = sinf(s->rotation);
float dx = wx - s->cx, dy = wy - s->cy;
float lx = (dx * sc + dy * ss) / s->sx;
float ly = (-dx * ss + dy * sc) / s->sy;
float min_scale = fminf(fabsf(s->sx), fabsf(s->sy));
float local_tol = world_tol / (min_scale > 0.0001f ? min_scale : 1.0f);
float tol_sq = local_tol * local_tol;
if (point_in_polygon(wx, wy, s->verts, s->num_verts)) if (point_in_polygon(lx, ly, s->verts, s->num_verts))
return true; return true;
for (uint32_t i = 0, j = s->num_verts - 1; i < s->num_verts; j = i++) { for (uint32_t i = 0, j = s->num_verts - 1; i < s->num_verts; j = i++) {
@@ -227,18 +321,23 @@ static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol)
float abx = bx - ax, aby = by - ay; float abx = bx - ax, aby = by - ay;
float len_sq = abx * abx + aby * aby; float len_sq = abx * abx + aby * aby;
if (len_sq < 0.0001f) continue; if (len_sq < 0.0001f) continue;
float t = ((wx - ax) * abx + (wy - ay) * aby) / len_sq; float t = ((lx - ax) * abx + (ly - ay) * aby) / len_sq;
t = fmaxf(0.0f, fminf(1.0f, t)); t = fmaxf(0.0f, fminf(1.0f, t));
float cx = ax + t * abx, cy = ay + t * aby; float cx = ax + t * abx, cy = ay + t * aby;
float dx = wx - cx, dy = wy - cy; float ddx = lx - cx, ddy = ly - cy;
if (dx * dx + dy * dy <= tol_sq) return true; if (ddx * ddx + ddy * ddy <= tol_sq) return true;
} }
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) static void shape_draw(shape_t *s, const mat4 *mvp)
{ {
sg_apply_pipeline(shape_pipeline);
sg_apply_uniforms(0, &SG_RANGE(*mvp)); sg_apply_uniforms(0, &SG_RANGE(*mvp));
sg_apply_uniforms(1, &SG_RANGE(s->uniform)); sg_apply_uniforms(1, &SG_RANGE(s->uniform));
sg_apply_bindings(&(sg_bindings) { sg_apply_bindings(&(sg_bindings) {
@@ -248,6 +347,16 @@ static void shape_draw(shape_t *s, const mat4 *mvp)
sg_draw(0, s->num_indices, 1); 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;
@@ -263,10 +372,7 @@ static shape_t shape_circle(float x, float y, float r, const float color[4])
for (int i = 0; i < segs; i++) { for (int i = 0; i < segs; i++) {
float a = (float)i / (float)segs * 2.0f * GLM_PIf - GLM_PI_2f; float a = (float)i / (float)segs * 2.0f * GLM_PIf - GLM_PI_2f;
s.verts[i] = (shape_vertex_t) { s.verts[i] = (shape_vertex_t) { cosf(a), sinf(a) };
x + cosf(a) * r,
y + sinf(a) * r,
};
} }
s.verts[segs] = s.verts[0]; s.verts[segs] = s.verts[0];
for (int i = 0; i <= segs; i++) s.indices[i] = (uint16_t)i; for (int i = 0; i <= segs; i++) s.indices[i] = (uint16_t)i;
@@ -274,10 +380,24 @@ static shape_t shape_circle(float x, float y, float r, const float color[4])
s.num_verts = (uint32_t)segs; s.num_verts = (uint32_t)segs;
shape_init_common(&s, color); shape_init_common(&s, color);
shape_build_transform(&s);
shape_make_buffers(&s); shape_make_buffers(&s);
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])
{ {
@@ -296,11 +416,8 @@ static shape_t shape_star(float x, float y, float outer_r, float inner_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) ? inner_r : outer_r; float r = (i & 1) ? s.star_inner_ratio : 1.0f;
s.verts[i] = (shape_vertex_t) { s.verts[i] = (shape_vertex_t) { cosf(a) * r, sinf(a) * r };
x + cosf(a) * r,
y + sinf(a) * r,
};
} }
s.verts[n] = s.verts[0]; s.verts[n] = s.verts[0];
for (int i = 0; i <= n; i++) s.indices[i] = (uint16_t)i; for (int i = 0; i <= n; i++) s.indices[i] = (uint16_t)i;
@@ -308,6 +425,7 @@ static shape_t shape_star(float x, float y, float outer_r, float inner_r,
s.num_verts = (uint32_t)n; s.num_verts = (uint32_t)n;
shape_init_common(&s, color); shape_init_common(&s, color);
shape_build_transform(&s);
shape_make_buffers(&s); shape_make_buffers(&s);
return s; return s;
} }

233
src/spatial.h Normal file
View File

@@ -0,0 +1,233 @@
#ifndef SPATIAL_H
#define SPATIAL_H
#include "api.h"
// Tunable constants
#define SPATIAL_CELL_SIZE 250.0f
#define SPATIAL_HASH_BITS 8
#define SPATIAL_HASH_SIZE (1 << SPATIAL_HASH_BITS)
#define SPATIAL_QUERY_RANGE 1
typedef struct {
int shape_idx;
float min_x, min_y, max_x, max_y;
} spatial_entry_t;
typedef struct {
bool occupied;
int cx, cy;
spatial_entry_t *entries;
int count;
int capacity;
} spatial_slot_t;
typedef struct {
spatial_slot_t slots[SPATIAL_HASH_SIZE];
bool dirty;
} spatial_grid_t;
static int spatial_hash(int cx, int cy)
{
return (cx * 73856093) ^ (cy * 19349663);
}
static void spatial_compute_aabb(shape_t *s, float *min_x, float *min_y,
float *max_x, float *max_y)
{
float cos_r = cosf(s->rotation);
float sin_r = sinf(s->rotation);
float hx = fabsf(cos_r) * s->sx + fabsf(sin_r) * s->sy;
float hy = fabsf(sin_r) * s->sx + fabsf(cos_r) * s->sy;
*min_x = s->cx - hx;
*min_y = s->cy - hy;
*max_x = s->cx + hx;
*max_y = s->cy + hy;
}
static void spatial_init(spatial_grid_t *grid)
{
memset(grid, 0, sizeof(*grid));
grid->dirty = true;
}
static void spatial_mark_dirty(spatial_grid_t *grid)
{
grid->dirty = true;
}
static void spatial_destroy(spatial_grid_t *grid)
{
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
if (grid->slots[i].entries) FREE(grid->slots[i].entries);
}
memset(grid, 0, sizeof(*grid));
}
static void spatial_rebuild(spatial_grid_t *grid, vector_t *shapes)
{
if (!grid->dirty) return;
grid->dirty = false;
int n = shapes->count;
// Phase 0: clear occupied flags
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
grid->slots[i].occupied = false;
grid->slots[i].count = 0;
}
if (n == 0) return;
// Phase 1: count shapes per cell
for (int i = 0; i < n; i++) {
shape_t *s = (shape_t*) vec_get(shapes, i);
int ccx = (int) floorf(s->cx / SPATIAL_CELL_SIZE);
int ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE);
int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1);
while (grid->slots[idx].occupied) {
if (grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy) break;
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
}
if (!grid->slots[idx].occupied) {
grid->slots[idx].occupied = true;
grid->slots[idx].cx = ccx;
grid->slots[idx].cy = ccy;
}
grid->slots[idx].count++;
}
// Phase 2: allocate entry arrays based on count
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
if (!grid->slots[i].occupied) continue;
if (grid->slots[i].count > grid->slots[i].capacity) {
if (grid->slots[i].entries) FREE(grid->slots[i].entries);
grid->slots[i].entries = (spatial_entry_t*) ALLOC(
(size_t) grid->slots[i].count * sizeof(spatial_entry_t));
grid->slots[i].capacity = grid->slots[i].count;
}
grid->slots[i].count = 0; // reset for fill phase
}
// Phase 3: fill entries
for (int i = 0; i < n; i++) {
shape_t *s = (shape_t*) vec_get(shapes, i);
int ccx = (int) floorf(s->cx / SPATIAL_CELL_SIZE);
int ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE);
int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1);
while (!(grid->slots[idx].occupied &&
grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy)) {
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
}
spatial_entry_t *e = &grid->slots[idx].entries[grid->slots[idx].count++];
e->shape_idx = i;
spatial_compute_aabb(s, &e->min_x, &e->min_y, &e->max_x, &e->max_y);
}
}
static int spatial_query_point(spatial_grid_t *grid, vector_t *shapes,
float wx, float wy, float world_tol)
{
int ccx = (int) floorf(wx / SPATIAL_CELL_SIZE);
int ccy = (int) floorf(wy / SPATIAL_CELL_SIZE);
for (int dz = -SPATIAL_QUERY_RANGE; dz <= SPATIAL_QUERY_RANGE; dz++) {
for (int dw = -SPATIAL_QUERY_RANGE; dw <= SPATIAL_QUERY_RANGE; dw++) {
int cell_x = ccx + dz;
int cell_y = ccy + dw;
int idx = spatial_hash(cell_x, cell_y) & (SPATIAL_HASH_SIZE - 1);
int probe_start = idx;
do {
if (!grid->slots[idx].occupied) break;
if (grid->slots[idx].cx == cell_x && grid->slots[idx].cy == cell_y) {
for (int e = 0; e < grid->slots[idx].count; e++) {
spatial_entry_t *entry = &grid->slots[idx].entries[e];
if (wx < entry->min_x - world_tol ||
wx > entry->max_x + world_tol ||
wy < entry->min_y - world_tol ||
wy > entry->max_y + world_tol)
continue;
shape_t *s = (shape_t*) vec_get(shapes, entry->shape_idx);
if (shape_hit_test(s, wx, wy, world_tol))
return entry->shape_idx;
}
break;
}
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
} while (idx != probe_start);
}
}
return -1;
}
static int spatial_query_rect_select(spatial_grid_t *grid, vector_t *shapes,
float min_x, float min_y,
float max_x, float max_y)
{
for (int i = 0; i < shapes->count; i++) {
((shape_t*) vec_get(shapes, i))->selected = false;
}
int selected_count = 0;
int min_cx = (int) floorf(min_x / SPATIAL_CELL_SIZE);
int min_cy = (int) floorf(min_y / SPATIAL_CELL_SIZE);
int max_cx = (int) floorf(max_x / SPATIAL_CELL_SIZE);
int max_cy = (int) floorf(max_y / SPATIAL_CELL_SIZE);
for (int cell_x = min_cx; cell_x <= max_cx; cell_x++) {
for (int cell_y = min_cy; cell_y <= max_cy; cell_y++) {
int idx = spatial_hash(cell_x, cell_y) & (SPATIAL_HASH_SIZE - 1);
int probe_start = idx;
do {
if (!grid->slots[idx].occupied) break;
if (grid->slots[idx].cx == cell_x && grid->slots[idx].cy == cell_y) {
for (int e = 0; e < grid->slots[idx].count; e++) {
spatial_entry_t *entry = &grid->slots[idx].entries[e];
if (entry->max_x < min_x || entry->min_x > max_x ||
entry->max_y < min_y || entry->min_y > max_y)
continue;
shape_t *s = (shape_t*) vec_get(shapes, entry->shape_idx);
if (s->selected) continue;
bool hit = (s->cx >= min_x && s->cx <= max_x &&
s->cy >= min_y && s->cy <= max_y);
float sc = cosf(s->rotation), ss = sinf(s->rotation);
for (uint32_t v = 0; !hit && v < s->num_verts; v++) {
float lx = s->verts[v].x * s->sx;
float ly = s->verts[v].y * s->sy;
float wx = s->cx + lx * sc - ly * ss;
float wy = s->cy + lx * ss + ly * sc;
if (wx >= min_x && wx <= max_x &&
wy >= min_y && wy <= max_y)
hit = true;
}
if (hit) {
s->selected = true;
selected_count++;
}
}
break;
}
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
} while (idx != probe_start);
}
}
return selected_count;
}
#endif

View File

@@ -11,11 +11,24 @@ typedef struct vector_t {
int stride; int stride;
} vector_t; } vector_t;
/**
* Zero-initialize a vector with the given element stride.
*
* @param v vector to initialize
* @param stride byte size of each element
*/
static void vec_init(vector_t *v, int stride) { static void vec_init(vector_t *v, int stride) {
memset(v, 0, sizeof(*v)); memset(v, 0, sizeof(*v));
v->stride = stride; v->stride = stride;
} }
/**
* Grow the vector's backing array to at least min_capacity elements.
* Doubles capacity (starting at 8) or uses min_capacity, whichever is larger.
*
* @param v vector to grow
* @param min_capacity minimum element count required
*/
static void vec_grow(vector_t *v, int min_capacity) { static void vec_grow(vector_t *v, int min_capacity) {
int new_cap = v->capacity ? v->capacity * 2 : 8; int new_cap = v->capacity ? v->capacity * 2 : 8;
if (new_cap < min_capacity) new_cap = min_capacity; if (new_cap < min_capacity) new_cap = min_capacity;
@@ -28,15 +41,33 @@ static void vec_grow(vector_t *v, int min_capacity) {
v->capacity = new_cap; v->capacity = new_cap;
} }
/**
* Append an uninitialized element to the end of the vector. Grows if needed.
*
* @param v vector to push into
* @return pointer to the new (uninitialized) element
*/
static void *vec_push(vector_t *v) { static void *vec_push(vector_t *v) {
if (v->count >= v->capacity) vec_grow(v, v->count + 1); if (v->count >= v->capacity) vec_grow(v, v->count + 1);
return v->data + (v->count++) * v->stride; return v->data + (v->count++) * v->stride;
} }
/**
* Remove the last element from the vector (decrements count, no free).
*
* @param v vector to pop from
*/
static void vec_pop(vector_t *v) { static void vec_pop(vector_t *v) {
if (v->count > 0) v->count--; if (v->count > 0) v->count--;
} }
/**
* Remove the element at index by swapping in the last element (O(1)).
* Order is not preserved.
*
* @param v vector to remove from
* @param index index of the element to remove
*/
static void vec_remove(vector_t *v, int index) { static void vec_remove(vector_t *v, int index) {
if (index < 0 || index >= v->count) return; if (index < 0 || index >= v->count) return;
if (index < v->count - 1) { if (index < v->count - 1) {
@@ -47,10 +78,22 @@ static void vec_remove(vector_t *v, int index) {
v->count--; v->count--;
} }
/**
* Return a pointer to the element at index (no bounds check).
*
* @param v vector to access
* @param index element index
* @return pointer to the element
*/
static void *vec_get(vector_t *v, int index) { static void *vec_get(vector_t *v, int index) {
return v->data + index * v->stride; return v->data + index * v->stride;
} }
/**
* Free the backing array and reset the vector to empty.
*
* @param v vector to free
*/
static void vec_free(vector_t *v) { static void vec_free(vector_t *v) {
if (v->data) FREE(v->data); if (v->data) FREE(v->data);
v->data = NULL; v->data = NULL;