You've already forked flecs_tests
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.
1015 lines
36 KiB
C
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
|