Files
flecs_tests/src/shape.h
Peaceultime 7e3da1c424 refactor: eliminate globals, add pen tool + edit mode, frustum culling
Remove all file-scope mutable state so the codebase compiles cleanly
as a single translation unit. State moved into structs owned by
userdata_t:

- group_index_ctx_t replaces g_group_by_id/g_group_by_id_cap
- shape_pool_ctx_t replaces g_shape_pool_dirty/g_shape_data_dirty
  and g_shape_groups/g_shape_groups_count
- panel_log_ctx_t wired through all subsystems explicitly
- pipeline_ctx_t owns render pipeline handles
- overlay_upload_state_t (per-buffer flags) replaces single bool

New features piggybacking on the refactor:

- Pen tool: click-to-place anchors, Catmull-Rom preview, finalize
  into Bezier shapes with control points
- Edit mode: anchor/handle hit testing, dragging, pre-drag undo
  snapshots, dedicated GPU buffers for edit overlays
- Frustum culling: spatial-grid-based viewport cull in draw_shapes
  with linear-scan fallback for oversized viewports
- Log dedup: FNV-1a 64-bit hash to skip duplicate messages
- Buffer shrink: halve draw buffers after 60 frames of low usage
- Shape geometry hashing for instanced-draw vertex-buffer grouping
- Group member_indices arrays with O(n) rebuild
- Log ring expanded 64→256 entries, added log_filter

Debug build: added --profiling-funcs and -sASSERTIONS flags.
2026-05-03 00:38:45 +02:00

1015 lines
36 KiB
C

#ifndef SHAPE_H
#define SHAPE_H
#include "api.h"
#define PEN_SUBDIVISIONS 20
// Bezier tessellation: target chord length in local-space units.
// Smaller = denser curves. Circle (r=1) with 0.10 gives ~17 subd/segment.
#define BEZIER_TARGET_CHORD 0.05f
#define BEZIER_MIN_SUBD 4
#define BEZIER_MAX_SUBD 64
typedef struct shape_vertex_t {
float x, y;
} shape_vertex_t;
// FNV-1a 64-bit hash over raw vertex bytes. Used to group shapes that share
// identical local-space geometry (e.g. all circles) while keeping freeform
// pen paths isolated in their own vertex buffers even when they happen to
// have the same vertex count.
static uint64_t hash_vertex_data(const shape_vertex_t *verts, uint32_t n) {
uint64_t h = 14695981039346656037ULL;
const uint8_t *data = (const uint8_t*)verts;
size_t len = (size_t)n * sizeof(shape_vertex_t);
for (size_t i = 0; i < len; i++) {
h ^= data[i];
h *= 1099511628211ULL;
}
return h;
}
typedef struct shape_uniform_t {
mat4 transform;
uint32_t state;
uint8_t _pad[12];
} shape_uniform_t;
typedef struct {
mat4 transform;
uint32_t state;
uint8_t _pad[12];
} shape_gpu_data_t;
typedef struct shape_t {
shape_vertex_t *verts;
uint16_t *indices;
uint32_t num_elements;
uint32_t num_verts;
shape_uniform_t uniform;
bool hovered;
bool selected;
bool dirty; // true when GPU instance data needs re-upload
float cx, cy;
float sx, sy;
float rotation;
float cos_r, sin_r;
float aabb_hx, aabb_hy;
int group_id;
// Bezier edit data (local space, NULL for shapes never edited)
shape_vertex_t *ctrl_points;
shape_vertex_t *ctrl_handle_in;
shape_vertex_t *ctrl_handle_out;
int ctrl_count;
bool closed;
char name[64];
// Grouping key for instanced rendering: shapes with the same
// (num_elements, vertex_hash) share a vertex buffer. Parametric shapes
// (circle, star, rect) naturally hash identically; freeform pen paths
// get unique hashes to prevent geometry cross-contamination.
uint64_t vertex_hash;
int group_index; // index into g_shape_groups[], -1 until pool rebuild
} shape_t;
// -- group entity (for nested groups) --
typedef struct {
int id;
int parent_id; // 0 = top-level group
int member_count;
int *member_indices;
bool collapsed;
} group_t;
// Group lookup index — maps group_id → group_t* for O(1) find_group().
// Must be defined before the lookup functions that dereference its fields.
struct group_index_ctx_t {
group_t **by_id;
int cap;
bool dirty;
};
// Full typedef for client use (userdata_t, etc.)
typedef struct group_index_ctx_t group_index_ctx_t;
// Group lookup functions now take group_index_ctx_t* so the index array is
// owned by the caller (embedded in userdata_t), not by a file-scope static.
static void group_index_rebuild(group_index_ctx_t *gi, vector_t *groups)
{
int max_id = 0;
for (int i = 0; i < groups->count; i++) {
int gid = ((group_t*) vec_get(groups, i))->id;
if (gid > max_id) max_id = gid;
}
if (max_id >= gi->cap) {
if (gi->by_id) FREE(gi->by_id);
int new_cap = max_id + 64;
gi->by_id = (group_t**) ALLOC((size_t)new_cap * sizeof(group_t*));
memset(gi->by_id, 0, (size_t)new_cap * sizeof(group_t*));
gi->cap = new_cap;
} else {
for (int i = 0; i <= max_id; i++) gi->by_id[i] = NULL;
}
for (int i = 0; i < groups->count; i++) {
group_t *g = (group_t*) vec_get(groups, i);
gi->by_id[g->id] = g;
}
gi->dirty = false;
}
static void group_index_ensure_cap(group_index_ctx_t *gi, int max_id)
{
if (max_id >= gi->cap) {
int new_cap = max_id + 64;
group_t **old = gi->by_id;
gi->by_id = (group_t**) ALLOC((size_t)new_cap * sizeof(group_t*));
if (old) {
memcpy(gi->by_id, old, (size_t)gi->cap * sizeof(group_t*));
FREE(old);
}
memset(gi->by_id + gi->cap, 0,
(size_t)(new_cap - gi->cap) * sizeof(group_t*));
gi->cap = new_cap;
}
}
static group_t* find_group(group_index_ctx_t *gi, vector_t *groups, int id) {
if (gi->dirty) {
group_index_rebuild(gi, groups);
gi->dirty = false;
}
if (id <= 0 || id >= gi->cap) return NULL;
return gi->by_id[id];
}
static bool is_shape_in_group_hierarchy(group_index_ctx_t *gi, int shape_gid, int target_gid, vector_t *groups) {
(void)groups;
int cur = shape_gid;
while (cur != 0) {
if (cur == target_gid) return true;
group_t *g = find_group(gi, groups, cur);
if (!g) return false;
cur = g->parent_id;
}
return false;
}
static void group_index_add(group_index_ctx_t *gi, vector_t *groups, int id, int parent_id)
{
bool found = false;
for (int i = 0; i < groups->count; i++) {
if (((group_t*) vec_get(groups, i))->id == id) { found = true; break; }
}
if (!found) {
group_t g = { .id = id, .parent_id = parent_id, .collapsed = true };
*((group_t*) vec_push(groups)) = g;
group_index_ensure_cap(gi, id);
if (id < gi->cap) gi->by_id[id] = (group_t*) vec_get(groups, groups->count - 1);
}
}
static void group_index_remove(group_index_ctx_t *gi, vector_t *groups, int id)
{
for (int i = 0; i < groups->count; i++) {
if (((group_t*) vec_get(groups, i))->id == id) {
group_t *grp = (group_t*) vec_get(groups, i);
if (grp->member_indices) FREE(grp->member_indices);
vec_remove_ordered(groups, i);
gi->dirty = true;
return;
}
}
}
static void group_index_shutdown(group_index_ctx_t *gi)
{
if (gi->by_id) FREE(gi->by_id);
gi->by_id = NULL;
gi->cap = 0;
gi->dirty = false;
}
// Rebuild member_indices for every group by scanning all shapes once.
// Call after any structural change (group, ungroup, delete, paste, undo, redo).
static void group_rebuild_members(group_index_ctx_t *gi, vector_t *groups, vector_t *shapes)
{
if (gi->dirty) group_index_rebuild(gi, groups);
// Free old member arrays and count members
for (int g = 0; g < groups->count; g++) {
group_t *grp = (group_t*) vec_get(groups, g);
if (grp->member_indices) { FREE(grp->member_indices); grp->member_indices = NULL; }
grp->member_count = 0;
}
// Count shapes per group
for (int i = 0; i < shapes->count; i++) {
int gid = ((shape_t*) vec_get(shapes, i))->group_id;
if (gid == 0) continue;
if (gid < gi->cap) {
group_t *grp = gi->by_id[gid];
if (grp) grp->member_count++;
}
}
// Allocate and fill
for (int g = 0; g < groups->count; g++) {
group_t *grp = (group_t*) vec_get(groups, g);
if (grp->member_count > 0) {
grp->member_indices = (int*) ALLOC((size_t)grp->member_count * sizeof(int));
grp->member_count = 0; // reset for fill pass
}
}
for (int i = 0; i < shapes->count; i++) {
shape_t *s = (shape_t*) vec_get(shapes, i);
if (s->group_id == 0) continue;
if (s->group_id < gi->cap) {
group_t *grp = gi->by_id[s->group_id];
if (grp && grp->member_indices)
grp->member_indices[grp->member_count++] = i;
}
}
}
// Free all member_indices. Call before freeing groups vector or resetting count.
static void group_shutdown_members(vector_t *groups)
{
for (int g = 0; g < groups->count; g++) {
group_t *grp = (group_t*) vec_get(groups, g);
if (grp->member_indices) { FREE(grp->member_indices); grp->member_indices = NULL; }
}
}
// -- Context structs (owned by userdata_t) --
// Each unique (num_elements, vertex_hash) pair gets one vertex buffer shared
// by all shapes with identical local-space geometry (e.g. all circles).
typedef struct {
uint32_t num_elements;
uint64_t vertex_hash;
sg_buffer vbuf;
} shape_group_buf_t;
// GPU pool state for instanced shape rendering.
typedef struct {
sg_buffer data_sbuf;
sg_buffer instance_map_sbuf;
sg_view data_view;
sg_view instance_map_view;
shape_group_buf_t *groups;
int group_count;
shape_gpu_data_t *upload_buf; // CPU-side mirror of data_sbuf contents
int upload_buf_cap;
size_t data_buf_size;
int instance_map_capacity;
bool pool_dirty;
bool data_dirty;
bool states_dirty;
int frame_id;
} shape_pool_ctx_t;
// -- shared geometry buffers (one vbuf per group + one instance-data sbuf) --
// All state is in shape_pool_ctx_t, owned by the caller (userdata_t).
static void shape_make_view_for_buffer(sg_view *view, sg_buffer buf)
{
if (view->id) sg_destroy_view(*view);
*view = sg_make_view(&(sg_view_desc){
.storage_buffer = { .buffer = buf },
});
}
// Upload shape instance data to GPU. Maintains a CPU-side mirror (upload_buf)
// so that only shapes marked dirty are rewritten; unchanged entries stay in
// the mirror from prior frames. Issues a single sg_update_buffer per frame
// to respect Sokol's per-buffer update limit.
static void shape_upload_data(shape_pool_ctx_t *sp, vector_t *shapes)
{
int n = shapes->count;
if (n == 0 || !sp->data_sbuf.id) return;
size_t need = (size_t)n * sizeof(shape_gpu_data_t);
if (need > sp->data_buf_size) {
sp->pool_dirty = true;
return;
}
// Resize CPU mirror when shape count grows (add/delete/paste)
if (n > sp->upload_buf_cap) {
if (sp->upload_buf) FREE(sp->upload_buf);
sp->upload_buf = (shape_gpu_data_t*) ALLOC(need);
sp->upload_buf_cap = n;
// Full mirror rebuild — all shapes are dirty on capacity change
for (int i = 0; i < n; i++) {
shape_t *s = (shape_t*) vec_get(shapes, i);
memcpy(sp->upload_buf[i].transform, s->uniform.transform, sizeof(mat4));
sp->upload_buf[i].state = s->uniform.state;
memset(sp->upload_buf[i]._pad, 0, sizeof(sp->upload_buf[i]._pad));
s->dirty = false;
}
sg_update_buffer(sp->data_sbuf, &(sg_range){sp->upload_buf, need});
sp->data_dirty = false;
return;
}
// Incremental: only write entries for shapes whose transform changed
bool any_dirty = false;
for (int i = 0; i < n; i++) {
shape_t *s = (shape_t*) vec_get(shapes, i);
if (!s->dirty) continue;
memcpy(sp->upload_buf[i].transform, s->uniform.transform, sizeof(mat4));
sp->upload_buf[i].state = s->uniform.state;
memset(sp->upload_buf[i]._pad, 0, sizeof(sp->upload_buf[i]._pad));
s->dirty = false;
any_dirty = true;
}
if (any_dirty) {
// Single sg_update_buffer — Sokol allows at most one per buffer per frame
sg_update_buffer(sp->data_sbuf, &(sg_range){sp->upload_buf, need});
}
sp->data_dirty = false;
}
static void shape_upload_instance_map(shape_pool_ctx_t *sp, const uint32_t *map, int count)
{
if (count > sp->instance_map_capacity) {
if (sp->instance_map_sbuf.id) sg_destroy_buffer(sp->instance_map_sbuf);
sp->instance_map_sbuf = sg_make_buffer(&(sg_buffer_desc){
.size = (size_t)count * sizeof(uint32_t),
.usage = { .storage_buffer = true, .stream_update = true },
.label = "Instance map",
});
sp->instance_map_capacity = count;
shape_make_view_for_buffer(&sp->instance_map_view, sp->instance_map_sbuf);
}
sg_update_buffer(sp->instance_map_sbuf, &(sg_range){map, (size_t)count * sizeof(uint32_t)});
}
static void shape_pool_rebuild(shape_pool_ctx_t *sp, panel_log_ctx_t *pl, vector_t *shapes)
{
int n = shapes->count;
if (n == 0) {
for (int i = 0; i < sp->group_count; i++) {
if (sp->groups[i].vbuf.id) sg_destroy_buffer(sp->groups[i].vbuf);
}
FREE(sp->groups);
sp->groups = NULL;
sp->group_count = 0;
if (sp->data_sbuf.id) { sg_destroy_buffer(sp->data_sbuf); sp->data_sbuf.id = 0; }
if (sp->data_view.id) { sg_destroy_view(sp->data_view); sp->data_view.id = 0; }
sp->data_buf_size = 0;
sp->pool_dirty = false;
return;
}
// Resize shape data buffer if needed (keep existing if size unchanged)
size_t need_data_size = (size_t)n * sizeof(shape_gpu_data_t);
if (need_data_size > sp->data_buf_size) {
if (sp->data_sbuf.id) { sg_destroy_buffer(sp->data_sbuf); sp->data_sbuf.id = 0; }
if (sp->data_view.id) { sg_destroy_view(sp->data_view); sp->data_view.id = 0; }
sp->data_buf_size = need_data_size;
sp->data_sbuf = sg_make_buffer(&(sg_buffer_desc){
.size = sp->data_buf_size,
.usage = { .storage_buffer = true, .stream_update = true },
.label = "Shape data",
});
if (sp->data_sbuf.id == SG_INVALID_ID)
panel_log(pl, 1, "[shapes] FAILED to create shape data buffer (%zu bytes)", sp->data_buf_size);
shape_make_view_for_buffer(&sp->data_view, sp->data_sbuf);
}
// Collect unique (num_elements, vertex_hash) pairs using a small
// open-addressing hash table to convert the O(n*n_keys) dedup scan
// into amortized O(n). n_keys is usually < 10 for procedural shapes.
typedef struct { uint32_t ne; uint64_t hash; } group_key_t;
group_key_t *keys = (group_key_t*) ALLOC((size_t)n * sizeof(group_key_t));
int n_keys = 0;
enum { KEY_TABLE_SIZE = 64 };
typedef struct { uint32_t ne; uint64_t hash; int idx; } key_entry_t;
key_entry_t key_table[KEY_TABLE_SIZE];
memset(key_table, 0, sizeof(key_table));
for (int i = 0; i < n; i++) {
shape_t *s = (shape_t*) vec_get(shapes, i);
uint32_t ne = s->num_elements;
uint64_t hash = s->vertex_hash;
uint32_t slot = (uint32_t)((ne * 0x9E3779B9) ^ (hash >> 32) ^ (hash & 0xFFFFFFFF))
& (KEY_TABLE_SIZE - 1);
while (key_table[slot].ne != 0 || key_table[slot].hash != 0) {
if (key_table[slot].ne == ne && key_table[slot].hash == hash) break;
slot = (slot + 1) & (KEY_TABLE_SIZE - 1);
}
if (key_table[slot].ne == 0 && key_table[slot].hash == 0) {
key_table[slot].ne = ne;
key_table[slot].hash = hash;
key_table[slot].idx = n_keys;
keys[n_keys].ne = ne;
keys[n_keys].hash = hash;
n_keys++;
}
}
// Preserve existing group buffers whose (ne, hash) key still exists.
// Only new keys and orphaned keys cause allocation/deallocation.
shape_group_buf_t *new_groups = (shape_group_buf_t*) ALLOC((size_t)n_keys * sizeof(shape_group_buf_t));
memset(new_groups, 0, (size_t)n_keys * sizeof(shape_group_buf_t));
bool *old_kept = (bool*) ALLOC((size_t)sp->group_count * sizeof(bool));
memset(old_kept, 0, (size_t)sp->group_count * sizeof(bool));
// Build a quick lookup from (ne, hash) -> old group index using the same
// fixed-size open-addressing scheme used for key dedup above.
int old_lookup_size = sp->group_count > 0 ? sp->group_count * 2 + 16 : 16;
if (old_lookup_size > 512) old_lookup_size = 512;
int *old_lookup_keys = NULL;
int *old_lookup_vals = NULL;
if (sp->group_count > 0) {
old_lookup_keys = (int*) ALLOC((size_t)old_lookup_size * 2 * sizeof(int));
old_lookup_vals = old_lookup_keys + old_lookup_size;
for (int i = 0; i < old_lookup_size; i++) {
old_lookup_keys[i] = -1;
old_lookup_vals[i] = -1;
}
for (int old_i = 0; old_i < sp->group_count; old_i++) {
uint32_t ne = sp->groups[old_i].num_elements;
uint64_t hash = sp->groups[old_i].vertex_hash;
uint32_t slot = (uint32_t)((ne * 0x9E3779B9) ^ (hash >> 32) ^ (hash & 0xFFFFFFFF))
& (uint32_t)(old_lookup_size - 1);
while (old_lookup_keys[slot] != -1)
slot = (slot + 1) & (uint32_t)(old_lookup_size - 1);
int packed = (int)(ne ^ (uint32_t)(hash ^ (hash >> 32)));
old_lookup_keys[slot] = packed;
old_lookup_vals[slot] = old_i;
}
}
for (int k = 0; k < n_keys; k++) {
bool reused = false;
if (sp->group_count > 0) {
uint32_t ne = keys[k].ne;
uint64_t hash = keys[k].hash;
int search_packed = (int)(ne ^ (uint32_t)(hash ^ (hash >> 32)));
uint32_t slot = (uint32_t)((ne * 0x9E3779B9) ^ (hash >> 32) ^ (hash & 0xFFFFFFFF))
& (uint32_t)(old_lookup_size - 1);
while (old_lookup_keys[slot] != -1) {
if (old_lookup_keys[slot] == search_packed) {
int old_i = old_lookup_vals[slot];
if (sp->groups[old_i].num_elements == ne &&
sp->groups[old_i].vertex_hash == hash &&
sp->groups[old_i].vbuf.id) {
new_groups[k] = sp->groups[old_i];
old_kept[old_i] = true;
reused = true;
break;
}
}
slot = (slot + 1) & (uint32_t)(old_lookup_size - 1);
}
}
if (!reused) {
shape_t *ref = NULL;
for (int i = 0; i < n; i++) {
shape_t *s = (shape_t*) vec_get(shapes, i);
if (s->num_elements == keys[k].ne && s->vertex_hash == keys[k].hash) {
ref = s;
break;
}
}
new_groups[k].num_elements = keys[k].ne;
new_groups[k].vertex_hash = keys[k].hash;
new_groups[k].vbuf = sg_make_buffer(&(sg_buffer_desc){
.size = (size_t)keys[k].ne * sizeof(shape_vertex_t),
.usage = { .stream_update = true },
.label = "Shape group verts",
});
sg_update_buffer(new_groups[k].vbuf, &(sg_range){
ref->verts, (size_t)keys[k].ne * sizeof(shape_vertex_t)
});
}
}
if (old_lookup_keys) FREE(old_lookup_keys);
for (int old_i = 0; old_i < sp->group_count; old_i++) {
if (!old_kept[old_i] && sp->groups[old_i].vbuf.id)
sg_destroy_buffer(sp->groups[old_i].vbuf);
}
FREE(old_kept);
FREE(sp->groups);
sp->groups = new_groups;
sp->group_count = n_keys;
// Assign group_index to shapes that were created since the last rebuild.
// Pre-existing shapes keep their cached group_index (validated above).
for (int i = 0; i < n; i++) {
shape_t *s = (shape_t*) vec_get(shapes, i);
if (s->group_index >= 0 && s->group_index < n_keys) continue;
s->group_index = -1;
for (int k = 0; k < n_keys; k++) {
if (sp->groups[k].num_elements == s->num_elements &&
sp->groups[k].vertex_hash == s->vertex_hash) {
s->group_index = k;
break;
}
}
}
FREE(keys);
if (pl) {
panel_log_debug(pl, "[shapes] pool_rebuild: %d shapes, %d groups, data_buf=%d data_view=%d",
n, n_keys, sp->data_sbuf.id, sp->data_view.id);
for (int gi = 0; gi < n_keys; gi++) {
panel_log_debug(pl, "[shapes] group[%d]: ne=%u hash=%llx vbuf=%d",
gi, sp->groups[gi].num_elements,
(unsigned long long)sp->groups[gi].vertex_hash,
sp->groups[gi].vbuf.id);
}
}
sp->pool_dirty = false;
}
static void shape_pool_shutdown(shape_pool_ctx_t *sp)
{
for (int i = 0; i < sp->group_count; i++) {
if (sp->groups[i].vbuf.id) sg_destroy_buffer(sp->groups[i].vbuf);
}
FREE(sp->groups);
sp->groups = NULL;
sp->group_count = 0;
if (sp->data_view.id) { sg_destroy_view(sp->data_view); sp->data_view.id = 0; }
if (sp->instance_map_view.id) { sg_destroy_view(sp->instance_map_view); sp->instance_map_view.id = 0; }
if (sp->data_sbuf.id) { sg_destroy_buffer(sp->data_sbuf); sp->data_sbuf.id = 0; }
if (sp->instance_map_sbuf.id) { sg_destroy_buffer(sp->instance_map_sbuf); sp->instance_map_sbuf.id = 0; }
sp->instance_map_capacity = 0;
if (sp->upload_buf) { FREE(sp->upload_buf); sp->upload_buf = NULL; }
sp->upload_buf_cap = 0;
}
#define SHAPE_HOVER_PX 6.0f
static void shape_regenerate_from_ctrl(shape_pool_ctx_t *sp, shape_t *s);
static int shape_calc_segments(float r)
{
int n = (int)(fabsf(r) * 0.5f) + 16;
if (n < 8) n = 8;
if (n > 128) n = 128;
return n;
}
static void shape_init_common(shape_t *s)
{
s->hovered = false;
s->selected = false;
s->dirty = true; // first upload always needed
s->uniform.state = 0;
memset(s->uniform._pad, 0, sizeof(s->uniform._pad));
s->group_id = 0;
s->aabb_hx = 0;
s->aabb_hy = 0;
s->ctrl_points = NULL;
s->ctrl_handle_in = NULL;
s->ctrl_handle_out = NULL;
s->ctrl_count = 0;
s->closed = false;
s->name[0] = '\0';
s->vertex_hash = 0;
s->group_index = -1;
}
// Recompute AABB from actual local-space vertex positions rather than
// assuming [-1,1] bounds. Needed because Bezier edits can push vertices
// outside the unit square. Also re-centers local-space vertices around
// origin so cx/cy tracks the true geometry center.
static void shape_update_aabb(shape_t *s) {
if (s->num_verts == 0) return;
float lmin_x = s->verts[0].x, lmax_x = s->verts[0].x;
float lmin_y = s->verts[0].y, lmax_y = s->verts[0].y;
for (uint32_t i = 1; i < s->num_verts; i++) {
float vx = s->verts[i].x, vy = s->verts[i].y;
if (vx < lmin_x) lmin_x = vx;
if (vx > lmax_x) lmax_x = vx;
if (vy < lmin_y) lmin_y = vy;
if (vy > lmax_y) lmax_y = vy;
}
float lcx = (lmin_x + lmax_x) * 0.5f;
float lcy = (lmin_y + lmax_y) * 0.5f;
float lhx = (lmax_x - lmin_x) * 0.5f;
float lhy = (lmax_y - lmin_y) * 0.5f;
float sc = s->cos_r, ss = s->sin_r;
s->cx += lcx * s->sx * sc - lcy * s->sy * ss;
s->cy += lcx * s->sx * ss + lcy * s->sy * sc;
for (uint32_t i = 0; i < s->num_verts; i++) {
s->verts[i].x -= lcx;
s->verts[i].y -= lcy;
}
for (int i = 0; i < s->ctrl_count; i++) {
s->ctrl_points[i].x -= lcx;
s->ctrl_points[i].y -= lcy;
s->ctrl_handle_in[i].x -= lcx;
s->ctrl_handle_in[i].y -= lcy;
s->ctrl_handle_out[i].x -= lcx;
s->ctrl_handle_out[i].y -= lcy;
}
s->vertex_hash = hash_vertex_data(s->verts, s->num_elements);
s->aabb_hx = fabsf(sc) * fabsf(s->sx) * lhx + fabsf(ss) * fabsf(s->sy) * lhy;
s->aabb_hy = fabsf(ss) * fabsf(s->sx) * lhx + fabsf(sc) * fabsf(s->sy) * lhy;
}
static void shape_build_transform(shape_pool_ctx_t *sp, 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);
s->cos_r = R[0][0];
s->sin_r = R[0][1];
s->aabb_hx = fabsf(s->cos_r) * fabsf(s->sx) + fabsf(s->sin_r) * fabsf(s->sy);
s->aabb_hy = fabsf(s->sin_r) * fabsf(s->sx) + fabsf(s->cos_r) * fabsf(s->sy);
s->dirty = true;
sp->data_dirty = true;
}
static void shape_retranslate(shape_pool_ctx_t *sp, shape_t *s)
{
s->uniform.transform[3][0] = s->cx;
s->uniform.transform[3][1] = s->cy;
s->dirty = true;
sp->data_dirty = true;
}
// Cache group_index at shape creation time. Only triggers a pool rebuild when
// the (num_elements, vertex_hash) key is genuinely new — the common case
// (e.g. creating another circle) reuses an existing vertex buffer.
static void shape_make_buffers(shape_pool_ctx_t *sp, shape_t *s)
{
for (int i = 0; i < sp->group_count; i++) {
if (sp->groups[i].num_elements == s->num_elements &&
sp->groups[i].vertex_hash == s->vertex_hash) {
s->group_index = i;
return;
}
}
s->group_index = -1;
sp->pool_dirty = true;
}
static void shape_shutdown(shape_pool_ctx_t *sp, shape_t *s)
{
sp->pool_dirty = true;
FREE(s->verts);
FREE(s->indices);
FREE(s->ctrl_points);
FREE(s->ctrl_handle_in);
FREE(s->ctrl_handle_out);
s->ctrl_count = 0;
}
static void shape_regenerate(shape_pool_ctx_t *sp, shape_t *s)
{
shape_build_transform(sp, s);
shape_update_aabb(s);
}
static void shape_set_state(shape_pool_ctx_t *sp, shape_t *s, bool hovered, bool selected)
{
uint32_t new_state = selected ? 2u : (hovered ? 1u : 0u);
if (s->uniform.state != new_state) { s->dirty = true; sp->data_dirty = true; }
s->hovered = hovered;
s->selected = selected;
s->uniform.state = new_state;
}
static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t n)
{
bool inside = false;
for (uint32_t i = 0, j = n - 1; i < n; j = i++) {
float xi = verts[i].x, yi = verts[i].y;
float xj = verts[j].x, yj = verts[j].y;
if ((yi > py) != (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
inside = !inside;
}
return inside;
}
static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol)
{
float sc = s->cos_r, ss = s->sin_r;
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;
// Per-axis inverse tolerance: a world-space circle of radius world_tol
// maps to a local-space axis-aligned ellipse with semi-axes
// world_tol/|sx| and world_tol/|sy|. The rotation terms cancel out
// because the coordinate transform already handles them.
float ix = fabsf(s->sx) / world_tol;
float iy = fabsf(s->sy) / world_tol;
if (point_in_polygon(lx, ly, s->verts, s->num_verts))
return true;
for (uint32_t i = 0, j = s->num_verts - 1; i < s->num_verts; j = i++) {
float ax = s->verts[i].x, ay = s->verts[i].y;
float bx = s->verts[j].x, by = s->verts[j].y;
float abx = bx - ax, aby = by - ay;
float len_sq = abx * abx + aby * aby;
if (len_sq < 0.0001f) continue;
float t = ((lx - ax) * abx + (ly - ay) * aby) / len_sq;
t = fmaxf(0.0f, fminf(1.0f, t));
float cx = ax + t * abx, cy = ay + t * aby;
float ddx = (lx - cx) * ix, ddy = (ly - cy) * iy;
if (ddx * ddx + ddy * ddy <= 1.0f) return true;
}
return false;
}
static shape_t shape_circle(shape_pool_ctx_t *sp, float x, float y, float r)
{
// 4-point Bezier circle approximation: magic constant k = (4/3)*tan(pi/8)
const float k = 0.5522847498f;
shape_t s;
memset(&s, 0, sizeof(s));
s.cx = x; s.cy = y;
s.sx = r; s.sy = r;
s.rotation = 0.0f;
shape_init_common(&s);
s.ctrl_count = 4;
s.closed = true;
s.ctrl_points = (shape_vertex_t*) ALLOC(4 * sizeof(shape_vertex_t));
s.ctrl_handle_in = (shape_vertex_t*) ALLOC(4 * sizeof(shape_vertex_t));
s.ctrl_handle_out = (shape_vertex_t*) ALLOC(4 * sizeof(shape_vertex_t));
// Anchors: top, right, bottom, left (clockwise from top)
s.ctrl_points[0] = (shape_vertex_t){ 0.0f, 1.0f};
s.ctrl_points[1] = (shape_vertex_t){ 1.0f, 0.0f};
s.ctrl_points[2] = (shape_vertex_t){ 0.0f, -1.0f};
s.ctrl_points[3] = (shape_vertex_t){-1.0f, 0.0f};
// Handles at k distance along tangent (perpendicular to radius)
s.ctrl_handle_in[0] = (shape_vertex_t){ -k, 1.0f};
s.ctrl_handle_out[0] = (shape_vertex_t){ k, 1.0f};
s.ctrl_handle_in[1] = (shape_vertex_t){ 1.0f, k};
s.ctrl_handle_out[1] = (shape_vertex_t){ 1.0f, -k};
s.ctrl_handle_in[2] = (shape_vertex_t){ k, -1.0f};
s.ctrl_handle_out[2] = (shape_vertex_t){ -k, -1.0f};
s.ctrl_handle_in[3] = (shape_vertex_t){-1.0f, -k};
s.ctrl_handle_out[3] = (shape_vertex_t){-1.0f, k};
strncpy(s.name, "Circle", sizeof(s.name) - 1);
shape_regenerate_from_ctrl(sp, &s);
return s;
}
static shape_t shape_rectangle(shape_pool_ctx_t *sp, float x, float y, float w, float h)
{
shape_t s;
memset(&s, 0, sizeof(s));
s.cx = x; s.cy = y;
s.sx = w * 0.5f; s.sy = h * 0.5f;
s.rotation = 0.0f;
shape_init_common(&s);
s.ctrl_count = 4;
s.closed = true;
s.ctrl_points = (shape_vertex_t*) ALLOC(4 * sizeof(shape_vertex_t));
s.ctrl_handle_in = (shape_vertex_t*) ALLOC(4 * sizeof(shape_vertex_t));
s.ctrl_handle_out = (shape_vertex_t*) ALLOC(4 * sizeof(shape_vertex_t));
s.ctrl_points[0] = (shape_vertex_t){-1.0f, -1.0f};
s.ctrl_points[1] = (shape_vertex_t){ 1.0f, -1.0f};
s.ctrl_points[2] = (shape_vertex_t){ 1.0f, 1.0f};
s.ctrl_points[3] = (shape_vertex_t){-1.0f, 1.0f};
s.ctrl_handle_in[0] = (shape_vertex_t){-1.0f, -1.0f/3.0f};
s.ctrl_handle_out[0] = (shape_vertex_t){-1.0f/3.0f, -1.0f};
s.ctrl_handle_in[1] = (shape_vertex_t){ 1.0f/3.0f, -1.0f};
s.ctrl_handle_out[1] = (shape_vertex_t){ 1.0f, -1.0f/3.0f};
s.ctrl_handle_in[2] = (shape_vertex_t){ 1.0f, 1.0f/3.0f};
s.ctrl_handle_out[2] = (shape_vertex_t){ 1.0f/3.0f, 1.0f};
s.ctrl_handle_in[3] = (shape_vertex_t){-1.0f/3.0f, 1.0f};
s.ctrl_handle_out[3] = (shape_vertex_t){-1.0f, 1.0f/3.0f};
strncpy(s.name, "Rectangle", sizeof(s.name) - 1);
shape_regenerate_from_ctrl(sp, &s);
return s;
}
// -- Catmull-Rom spline for pen tool --
static shape_vertex_t catmull_rom_eval(float t, shape_vertex_t p0, shape_vertex_t p1,
shape_vertex_t p2, shape_vertex_t p3)
{
float t2 = t * t;
float t3 = t2 * t;
return (shape_vertex_t){
0.5f * ((2.0f * p1.x) +
(-p0.x + p2.x) * t +
(2.0f * p0.x - 5.0f * p1.x + 4.0f * p2.x - p3.x) * t2 +
(-p0.x + 3.0f * p1.x - 3.0f * p2.x + p3.x) * t3),
0.5f * ((2.0f * p1.y) +
(-p0.y + p2.y) * t +
(2.0f * p0.y - 5.0f * p1.y + 4.0f * p2.y - p3.y) * t2 +
(-p0.y + 3.0f * p1.y - 3.0f * p2.y + p3.y) * t3)
};
}
// Build a smooth line strip from control points using Catmull-Rom interpolation.
// Returns the number of output vertices written to `out`.
static int pen_generate_curve(const shape_vertex_t *ctrl, int ctrl_count,
shape_vertex_t *out, int out_cap, int subdivisions)
{
if (ctrl_count < 2 || out_cap < 2) return 0;
int max_out = (ctrl_count - 1) * subdivisions + 1;
if (max_out > out_cap) max_out = out_cap;
// Phantom endpoints: duplicate first and last control points
for (int seg = 0; seg < ctrl_count - 1 && (seg * subdivisions) < max_out; seg++) {
shape_vertex_t p0 = ctrl[seg > 0 ? seg - 1 : 0];
shape_vertex_t p1 = ctrl[seg];
shape_vertex_t p2 = ctrl[seg + 1];
shape_vertex_t p3 = ctrl[seg + 2 < ctrl_count ? seg + 2 : ctrl_count - 1];
int steps = subdivisions;
if (seg == ctrl_count - 2) steps = max_out - seg * subdivisions;
for (int s = 0; s < steps; s++) {
float t = (float)s / (float)steps;
out[seg * subdivisions + s] = catmull_rom_eval(t, p0, p1, p2, p3);
}
}
// Ensure last point is exactly the final control point
out[max_out - 1] = ctrl[ctrl_count - 1];
return max_out;
}
// Create a shape from world-space line-strip vertices.
// Vertices are converted to local space (centered, normalized to AABB).
static shape_t shape_from_world_verts(shape_pool_ctx_t *sp, const shape_vertex_t *wverts, int wcount)
{
shape_t s;
memset(&s, 0, sizeof(s));
s.rotation = 0.0f;
// Compute world-space AABB
float min_x = wverts[0].x, min_y = wverts[0].y;
float max_x = min_x, max_y = min_y;
for (int i = 1; i < wcount; i++) {
if (wverts[i].x < min_x) min_x = wverts[i].x;
if (wverts[i].y < min_y) min_y = wverts[i].y;
if (wverts[i].x > max_x) max_x = wverts[i].x;
if (wverts[i].y > max_y) max_y = wverts[i].y;
}
float hx = (max_x - min_x) * 0.5f;
float hy = (max_y - min_y) * 0.5f;
s.cx = (min_x + max_x) * 0.5f;
s.cy = (min_y + max_y) * 0.5f;
s.sx = hx > 0.0001f ? hx : 1.0f;
s.sy = hy > 0.0001f ? hy : 1.0f;
s.num_verts = (uint32_t)wcount;
s.num_elements = (uint32_t)wcount;
s.verts = (shape_vertex_t*) ALLOC((size_t)wcount * sizeof(shape_vertex_t));
s.indices = (uint16_t*) ALLOC((size_t)wcount * sizeof(uint16_t));
for (int i = 0; i < wcount; i++) {
s.verts[i].x = (wverts[i].x - s.cx) / s.sx;
s.verts[i].y = (wverts[i].y - s.cy) / s.sy;
s.indices[i] = (uint16_t)i;
}
shape_init_common(&s);
s.vertex_hash = hash_vertex_data(s.verts, s.num_elements);
strncpy(s.name, "Path", sizeof(s.name) - 1);
shape_build_transform(sp, &s);
shape_update_aabb(&s);
shape_make_buffers(sp, &s);
return s;
}
// -- Coordinate helpers for edit mode --
static shape_vertex_t local_to_world(const shape_t *s, float lx, float ly)
{
return (shape_vertex_t){
s->cx + lx * s->sx * s->cos_r - ly * s->sy * s->sin_r,
s->cy + lx * s->sx * s->sin_r + ly * s->sy * s->cos_r
};
}
static shape_vertex_t world_to_local(const shape_t *s, float wx, float wy)
{
float dx = wx - s->cx, dy = wy - s->cy;
return (shape_vertex_t){
(dx * s->cos_r + dy * s->sin_r) / s->sx,
(-dx * s->sin_r + dy * s->cos_r) / s->sy
};
}
// -- Cubic Bezier evaluation for edit mode --
static shape_vertex_t bezier_eval_segment(float t,
shape_vertex_t p0, shape_vertex_t p1,
shape_vertex_t p2, shape_vertex_t p3)
{
float u = 1.0f - t;
float u2 = u * u, u3 = u2 * u;
float t2 = t * t, t3 = t2 * t;
return (shape_vertex_t){
u3 * p0.x + 3.0f * u2 * t * p1.x + 3.0f * u * t2 * p2.x + t3 * p3.x,
u3 * p0.y + 3.0f * u2 * t * p1.y + 3.0f * u * t2 * p2.y + t3 * p3.y
};
}
// Approximate arc length of a bezier segment via its control polygon.
static float bezier_control_polygon_len(shape_vertex_t p0, shape_vertex_t p1,
shape_vertex_t p2, shape_vertex_t p3) {
float dx = p1.x - p0.x, dy = p1.y - p0.y;
float len = sqrtf(dx*dx + dy*dy);
dx = p2.x - p1.x; dy = p2.y - p1.y;
len += sqrtf(dx*dx + dy*dy);
dx = p3.x - p2.x; dy = p3.y - p2.y;
len += sqrtf(dx*dx + dy*dy);
return len;
}
// Regenerate verts/indices from ctrl_points + Bezier handles.
// Subdivisions per segment are derived from the control-polygon arc length,
// giving consistent smoothness regardless of shape type or size.
static void shape_regenerate_from_ctrl(shape_pool_ctx_t *sp, shape_t *s)
{
int n = s->ctrl_count;
if (n < 2) return;
FREE(s->verts);
FREE(s->indices);
int segs = s->closed ? n : n - 1;
// First pass: compute subdivisions per segment, sum total vertex count
int *seg_subd = (int*) ALLOC((size_t)segs * sizeof(int));
int total_verts = 1; // +1 for the closing anchor duplicate
for (int seg = 0; seg < segs; seg++) {
int i0 = seg;
int i1 = s->closed ? ((seg + 1) % n) : seg + 1;
float len = bezier_control_polygon_len(
s->ctrl_points[i0], s->ctrl_handle_out[i0],
s->ctrl_handle_in[i1], s->ctrl_points[i1]);
int subd = (int)(len / BEZIER_TARGET_CHORD);
if (subd < BEZIER_MIN_SUBD) subd = BEZIER_MIN_SUBD;
if (subd > BEZIER_MAX_SUBD) subd = BEZIER_MAX_SUBD;
seg_subd[seg] = subd;
total_verts += subd;
}
s->num_verts = s->closed ? (uint32_t)(total_verts - 1) : (uint32_t)total_verts;
s->num_elements = (uint32_t)total_verts;
s->verts = (shape_vertex_t*) ALLOC((size_t)total_verts * sizeof(shape_vertex_t));
s->indices = (uint16_t*) ALLOC((size_t)total_verts * sizeof(uint16_t));
// Second pass: generate vertices
int vi = 0;
for (int seg = 0; seg < segs; seg++) {
int i0 = seg;
int i1 = s->closed ? ((seg + 1) % n) : seg + 1;
shape_vertex_t p0 = s->ctrl_points[i0];
shape_vertex_t p1 = s->ctrl_handle_out[i0];
shape_vertex_t p2 = s->ctrl_handle_in[i1];
shape_vertex_t p3 = s->ctrl_points[i1];
int steps = seg_subd[seg];
for (int k = 0; k < steps; k++) {
float t = (float)k / (float)steps;
s->verts[vi++] = bezier_eval_segment(t, p0, p1, p2, p3);
}
}
s->verts[vi++] = s->ctrl_points[0];
for (int i = 0; i < total_verts; i++) s->indices[i] = (uint16_t)i;
FREE(seg_subd);
s->vertex_hash = hash_vertex_data(s->verts, s->num_elements);
shape_build_transform(sp, s);
shape_update_aabb(s);
sp->pool_dirty = true;
}
#endif