#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