You've already forked flecs_tests
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.
This commit is contained in:
27
src/api.h
27
src/api.h
@@ -29,20 +29,33 @@
|
||||
#include "generated/shape.h"
|
||||
#include "generated/overlay.h"
|
||||
|
||||
// Log-to-panel infrastructure
|
||||
static void (*g_panel_log_fn)(void*, int, const char*) = NULL;
|
||||
static void *g_panel_log_ud = NULL;
|
||||
// Log-to-panel infrastructure — ctx pointer passed explicitly.
|
||||
// The panel_log function writes into the panel's ring buffer through the
|
||||
// callback registered in panel_log_ctx_t. This avoids a global function
|
||||
// pointer that would restrict the codebase to a single compilation unit.
|
||||
typedef struct {
|
||||
void (*fn)(void*, int, const char*);
|
||||
void *ud;
|
||||
} panel_log_ctx_t;
|
||||
|
||||
static void panel_log(int level, const char *fmt, ...) {
|
||||
if (!g_panel_log_fn) return;
|
||||
static void panel_log(panel_log_ctx_t *pl, int level, const char *fmt, ...) {
|
||||
if (!pl || !pl->fn) return;
|
||||
char buf[256];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||
va_end(ap);
|
||||
g_panel_log_fn(g_panel_log_ud, level, buf);
|
||||
pl->fn(pl->ud, level, buf);
|
||||
}
|
||||
|
||||
// Debug-level log calls are stripped in release builds. All panel_log(3, ...)
|
||||
// calls should use this macro so they compile to nothing with -O3.
|
||||
#ifdef NDEBUG
|
||||
#define panel_log_debug(pl, fmt, ...) ((void)0)
|
||||
#else
|
||||
#define panel_log_debug(pl, fmt, ...) panel_log(pl, 3, fmt, ##__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
#include "util.h"
|
||||
#include "shape.h"
|
||||
#include "render.h"
|
||||
@@ -52,8 +65,8 @@ static void panel_log(int level, const char *fmt, ...) {
|
||||
#include "interact.h"
|
||||
#include "overlay.h"
|
||||
#include "draw.h"
|
||||
#include "ui_panels.h"
|
||||
#include "input.h"
|
||||
#include "ui_panels.h"
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <stdio.h>
|
||||
|
||||
33
src/camera.h
33
src/camera.h
@@ -17,33 +17,16 @@ typedef struct {
|
||||
pan_state_t pan_state;
|
||||
} camera_t;
|
||||
|
||||
// Build a view-projection matrix that maps world coordinates to clip space.
|
||||
// Uses glm_ortho rather than manual element assignment — the previous
|
||||
// hand-rolled version was equivalent but obscured the intent.
|
||||
static void compute_mvp(camera_t *cam, mat4 *mvp)
|
||||
{
|
||||
const float w = (float)cam->width;
|
||||
const float h = (float)cam->height;
|
||||
const float z = cam->zoom;
|
||||
const float px = cam->pan[0];
|
||||
const float py = cam->pan[1];
|
||||
|
||||
(*mvp)[0][0] = (2.0f / w) * z;
|
||||
(*mvp)[0][1] = 0.0f;
|
||||
(*mvp)[0][2] = 0.0f;
|
||||
(*mvp)[0][3] = 0.0f;
|
||||
|
||||
(*mvp)[1][0] = 0.0f;
|
||||
(*mvp)[1][1] = (2.0f / h) * z;
|
||||
(*mvp)[1][2] = 0.0f;
|
||||
(*mvp)[1][3] = 0.0f;
|
||||
|
||||
(*mvp)[2][0] = 0.0f;
|
||||
(*mvp)[2][1] = 0.0f;
|
||||
(*mvp)[2][2] = 0.0f;
|
||||
(*mvp)[2][3] = 0.0f;
|
||||
|
||||
(*mvp)[3][0] = (2.0f / w) * px;
|
||||
(*mvp)[3][1] = (2.0f / h) * py;
|
||||
(*mvp)[3][2] = 0.0f;
|
||||
(*mvp)[3][3] = 1.0f;
|
||||
float l = (-cam->pan[0] - cam->half_width) / cam->zoom;
|
||||
float r = (-cam->pan[0] + cam->half_width) / cam->zoom;
|
||||
float b = (-cam->pan[1] - cam->half_height) / cam->zoom;
|
||||
float t = (-cam->pan[1] + cam->half_height) / cam->zoom;
|
||||
glm_ortho(l, r, b, t, -1.0f, 1.0f, *mvp);
|
||||
}
|
||||
|
||||
static void screen_to_world(camera_t *cam, float mx, float my, float *wx, float *wy)
|
||||
|
||||
338
src/draw.h
338
src/draw.h
@@ -6,85 +6,213 @@
|
||||
|
||||
static void draw_shapes(userdata_t *ud)
|
||||
{
|
||||
if (g_shape_pool_dirty)
|
||||
shape_pool_rebuild(&ud->shapes);
|
||||
bool pool_was_dirty = ud->shape_pool.pool_dirty;
|
||||
if (ud->shape_pool.pool_dirty) {
|
||||
shape_pool_rebuild(&ud->shape_pool, &ud->panel_log_ctx, &ud->shapes);
|
||||
ud->shape_pool.data_dirty = true;
|
||||
}
|
||||
|
||||
int n = ud->shapes.count;
|
||||
if (n == 0) return;
|
||||
|
||||
if (g_shape_data_dirty) {
|
||||
shape_upload_data(&ud->shapes);
|
||||
g_shape_data_dirty = false;
|
||||
if (ud->shape_pool.states_dirty) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
shape_set_state(&ud->shape_pool, s, s->hovered, s->selected);
|
||||
}
|
||||
ud->shape_pool.states_dirty = false;
|
||||
}
|
||||
panel_log(3, "[shapes] draw_shapes: n=%d pipeline=%d", n, shape_pipeline.id);
|
||||
|
||||
if (ud->shape_pool.data_dirty) {
|
||||
shape_upload_data(&ud->shape_pool, &ud->shapes);
|
||||
ud->shape_pool.data_dirty = false;
|
||||
}
|
||||
|
||||
// -- frustum culling: viewport world bounds --
|
||||
float vp_min_x = -(ud->camera.pan[0] + ud->camera.half_width) / ud->camera.zoom;
|
||||
float vp_max_x = (ud->camera.half_width - ud->camera.pan[0]) / ud->camera.zoom;
|
||||
float vp_min_y = -(ud->camera.half_height + ud->camera.pan[1]) / ud->camera.zoom;
|
||||
float vp_max_y = (ud->camera.half_height - ud->camera.pan[1]) / ud->camera.zoom;
|
||||
float margin = FRUSTUM_CULL_MARGIN;
|
||||
vp_min_x -= margin; vp_max_x += margin;
|
||||
vp_min_y -= margin; vp_max_y += margin;
|
||||
|
||||
static uint32_t *imap = NULL;
|
||||
static int imap_cap = 0;
|
||||
static uint32_t *ne_counts = NULL;
|
||||
static uint32_t *ne_starts = NULL;
|
||||
static int ne_cap = 0;
|
||||
static uint32_t *gi_counts = NULL;
|
||||
static uint32_t *gi_starts = NULL;
|
||||
static int gi_cap = 0;
|
||||
static bool imap_valid = false;
|
||||
static int *visible = NULL;
|
||||
static int visible_cap = 0;
|
||||
|
||||
if (n > imap_cap) {
|
||||
if (imap) FREE(imap);
|
||||
imap = (uint32_t*) ALLOC((size_t)n * sizeof(uint32_t));
|
||||
imap_cap = n;
|
||||
}
|
||||
int draw_count;
|
||||
bool any_drag = ud->interact.move.dragging || ud->interact.rotate.dragging ||
|
||||
ud->interact.resize.dragging;
|
||||
bool use_culling = !any_drag;
|
||||
|
||||
// Group shapes by num_elements using counting sort
|
||||
uint32_t max_ne = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint32_t ne = ((shape_t*) vec_get(&ud->shapes, i))->num_elements;
|
||||
if (ne > max_ne) max_ne = ne;
|
||||
}
|
||||
if (use_culling) {
|
||||
if (n > visible_cap) {
|
||||
if (visible) FREE(visible);
|
||||
visible = (int*) ALLOC((size_t)n * sizeof(int));
|
||||
visible_cap = n;
|
||||
}
|
||||
|
||||
int ne_size = (int)(max_ne + 1);
|
||||
if (ne_size > ne_cap) {
|
||||
if (ne_counts) FREE(ne_counts);
|
||||
if (ne_starts) FREE(ne_starts);
|
||||
ne_counts = (uint32_t*) ALLOC((size_t)ne_size * sizeof(uint32_t));
|
||||
ne_starts = (uint32_t*) ALLOC((size_t)ne_size * sizeof(uint32_t));
|
||||
ne_cap = ne_size;
|
||||
}
|
||||
memset(ne_counts, 0, (size_t)ne_size * sizeof(uint32_t));
|
||||
int cell_min_x = (int) floorf(vp_min_x / SPATIAL_CELL_SIZE);
|
||||
int cell_max_x = (int) floorf(vp_max_x / SPATIAL_CELL_SIZE);
|
||||
int cell_min_y = (int) floorf(vp_min_y / SPATIAL_CELL_SIZE);
|
||||
int cell_max_y = (int) floorf(vp_max_y / SPATIAL_CELL_SIZE);
|
||||
int cell_count = (cell_max_x - cell_min_x + 1) * (cell_max_y - cell_min_y + 1);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint32_t ne = ((shape_t*) vec_get(&ud->shapes, i))->num_elements;
|
||||
ne_counts[ne]++;
|
||||
}
|
||||
|
||||
uint32_t pos = 0;
|
||||
for (uint32_t ne = 0; ne <= max_ne; ne++) {
|
||||
ne_starts[ne] = pos;
|
||||
pos += ne_counts[ne];
|
||||
ne_counts[ne] = 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint32_t ne = ((shape_t*) vec_get(&ud->shapes, i))->num_elements;
|
||||
imap[ne_starts[ne] + ne_counts[ne]++] = (uint32_t)i;
|
||||
}
|
||||
|
||||
shape_upload_instance_map(imap, n);
|
||||
|
||||
sg_apply_pipeline(shape_pipeline);
|
||||
|
||||
int base = 0;
|
||||
while (base < n) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, imap[base]);
|
||||
uint32_t ne = s->num_elements;
|
||||
int count = 1;
|
||||
while (base + count < n &&
|
||||
((shape_t*) vec_get(&ud->shapes, imap[base + count]))->num_elements == ne)
|
||||
count++;
|
||||
|
||||
// Find the vertex buffer for this num_elements
|
||||
sg_buffer group_vbuf = {0};
|
||||
for (int gi = 0; gi < g_shape_group_count; gi++) {
|
||||
if (g_shape_groups[gi].num_elements == ne) {
|
||||
group_vbuf = g_shape_groups[gi].vbuf;
|
||||
break;
|
||||
if (cell_count <= SPATIAL_HASH_SIZE) {
|
||||
draw_count = spatial_query_viewport(&ud->spatial_grid,
|
||||
vp_min_x, vp_min_y, vp_max_x, vp_max_y,
|
||||
visible, n);
|
||||
} else {
|
||||
// Viewport too large for cell iteration — linear scan
|
||||
draw_count = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
if (s->cx + s->aabb_hx < vp_min_x || s->cx - s->aabb_hx > vp_max_x ||
|
||||
s->cy + s->aabb_hy < vp_min_y || s->cy - s->aabb_hy > vp_max_y)
|
||||
continue;
|
||||
visible[draw_count++] = i;
|
||||
}
|
||||
}
|
||||
imap_valid = false;
|
||||
} else {
|
||||
draw_count = n;
|
||||
if (pool_was_dirty) imap_valid = false;
|
||||
}
|
||||
|
||||
if (draw_count == 0) return;
|
||||
|
||||
// Shrink draw buffers when usage drops far below peak for 60+ frames.
|
||||
// Prevents WASM memory from being held at a transient high-water mark
|
||||
// (e.g. after a large paste that was then undone).
|
||||
{
|
||||
static int peak_draw_count = 0;
|
||||
static int peak_gi_cap = 0;
|
||||
static int peak_visible_cap = 0;
|
||||
static int frames_at_peak = 0;
|
||||
|
||||
int cur_gi_size = ud->shape_pool.group_count;
|
||||
|
||||
if (draw_count > peak_draw_count || cur_gi_size > peak_gi_cap || n > peak_visible_cap) {
|
||||
peak_draw_count = draw_count;
|
||||
peak_gi_cap = cur_gi_size;
|
||||
peak_visible_cap = use_culling ? n : peak_visible_cap;
|
||||
frames_at_peak = 0;
|
||||
} else {
|
||||
frames_at_peak++;
|
||||
}
|
||||
|
||||
if (frames_at_peak > 60) {
|
||||
// Halve buffers when peak is more than 4× current usage
|
||||
if (imap_cap > 64 && imap_cap > draw_count * 4) {
|
||||
int new_cap = imap_cap / 2;
|
||||
if (new_cap < draw_count) new_cap = draw_count;
|
||||
uint32_t *new_imap = (uint32_t*) ALLOC((size_t)new_cap * sizeof(uint32_t));
|
||||
memcpy(new_imap, imap, (size_t)draw_count * sizeof(uint32_t));
|
||||
FREE(imap);
|
||||
imap = new_imap;
|
||||
imap_cap = new_cap;
|
||||
imap_valid = false;
|
||||
peak_draw_count = draw_count;
|
||||
}
|
||||
if (gi_cap > 64 && gi_cap > cur_gi_size * 4) {
|
||||
int new_cap = gi_cap / 2;
|
||||
if (new_cap < cur_gi_size) new_cap = cur_gi_size;
|
||||
uint32_t *new_gc = (uint32_t*) ALLOC((size_t)new_cap * sizeof(uint32_t));
|
||||
uint32_t *new_gs = (uint32_t*) ALLOC((size_t)new_cap * sizeof(uint32_t));
|
||||
if (cur_gi_size > 0) memcpy(new_gc, gi_counts, (size_t)cur_gi_size * sizeof(uint32_t));
|
||||
FREE(gi_counts);
|
||||
FREE(gi_starts);
|
||||
gi_counts = new_gc;
|
||||
gi_starts = new_gs;
|
||||
gi_cap = new_cap;
|
||||
peak_gi_cap = cur_gi_size;
|
||||
}
|
||||
if (visible_cap > 64 && visible_cap > n * 4) {
|
||||
int new_cap = visible_cap / 2;
|
||||
if (new_cap < n) new_cap = n;
|
||||
int *new_vis = (int*) ALLOC((size_t)new_cap * sizeof(int));
|
||||
FREE(visible);
|
||||
visible = new_vis;
|
||||
visible_cap = new_cap;
|
||||
peak_visible_cap = n;
|
||||
}
|
||||
frames_at_peak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (draw_count > imap_cap) {
|
||||
if (imap) FREE(imap);
|
||||
imap = (uint32_t*) ALLOC((size_t)draw_count * sizeof(uint32_t));
|
||||
imap_cap = draw_count;
|
||||
imap_valid = false;
|
||||
}
|
||||
|
||||
if (!imap_valid) {
|
||||
// Sort visible shape indices by group_index so that shapes sharing
|
||||
// the same vertex buffer are consecutive. This lets the draw loop
|
||||
// issue one instanced draw call per group run rather than per shape.
|
||||
int n_groups = ud->shape_pool.group_count;
|
||||
int max_gi = n_groups - 1;
|
||||
|
||||
int gi_size = max_gi >= 0 ? max_gi + 1 : 0;
|
||||
if (gi_size > gi_cap) {
|
||||
if (gi_counts) FREE(gi_counts);
|
||||
if (gi_starts) FREE(gi_starts);
|
||||
gi_counts = (uint32_t*) ALLOC((size_t)gi_size * sizeof(uint32_t));
|
||||
gi_starts = (uint32_t*) ALLOC((size_t)gi_size * sizeof(uint32_t));
|
||||
gi_cap = gi_size;
|
||||
}
|
||||
memset(gi_counts, 0, (size_t)gi_size * sizeof(uint32_t));
|
||||
|
||||
for (int i = 0; i < draw_count; i++) {
|
||||
int si = use_culling ? visible[i] : i;
|
||||
int gi = ((shape_t*) vec_get(&ud->shapes, si))->group_index;
|
||||
if (gi < 0 || gi >= gi_size) gi = 0;
|
||||
gi_counts[gi]++;
|
||||
}
|
||||
|
||||
uint32_t pos = 0;
|
||||
for (int gi = 0; gi < gi_size; gi++) {
|
||||
gi_starts[gi] = pos;
|
||||
pos += gi_counts[gi];
|
||||
gi_counts[gi] = 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < draw_count; i++) {
|
||||
int si = use_culling ? visible[i] : i;
|
||||
int gi = ((shape_t*) vec_get(&ud->shapes, si))->group_index;
|
||||
if (gi < 0 || gi >= gi_size) gi = 0;
|
||||
imap[gi_starts[gi] + gi_counts[gi]++] = (uint32_t)si;
|
||||
}
|
||||
|
||||
shape_upload_instance_map(&ud->shape_pool, imap, draw_count);
|
||||
imap_valid = true;
|
||||
}
|
||||
|
||||
sg_apply_pipeline(ud->pipelines.shape_pipeline);
|
||||
|
||||
int base = 0;
|
||||
while (base < draw_count) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, imap[base]);
|
||||
int gi = s->group_index;
|
||||
if (gi < 0 || gi >= ud->shape_pool.group_count) gi = 0;
|
||||
uint32_t ne = s->num_elements;
|
||||
int count = 1;
|
||||
while (base + count < draw_count) {
|
||||
shape_t *ns = (shape_t*) vec_get(&ud->shapes, imap[base + count]);
|
||||
int ngi = ns->group_index;
|
||||
if (ngi < 0 || ngi >= ud->shape_pool.group_count) ngi = 0;
|
||||
if (ngi != gi) break;
|
||||
count++;
|
||||
}
|
||||
|
||||
sg_buffer group_vbuf = ud->shape_pool.groups[gi].vbuf;
|
||||
|
||||
struct { mat4 mvp; uint32_t base; uint8_t _pad[12]; } vs_u;
|
||||
memcpy(vs_u.mvp, ud->renderer.uniform.mvp, sizeof(mat4));
|
||||
@@ -94,23 +222,20 @@ static void draw_shapes(userdata_t *ud)
|
||||
|
||||
sg_apply_bindings(&(sg_bindings){
|
||||
.vertex_buffers[0] = group_vbuf,
|
||||
.views[0] = g_shape_data_view,
|
||||
.views[1] = g_instance_map_view,
|
||||
.views[0] = ud->shape_pool.data_view,
|
||||
.views[1] = ud->shape_pool.instance_map_view,
|
||||
});
|
||||
panel_log(3, "[shapes] draw group: ne=%u count=%d base=%d vbuf=%d data_view=%d imap_view=%d",
|
||||
ne, count, base, group_vbuf.id, g_shape_data_view.id, g_instance_map_view.id);
|
||||
sg_draw(0, (int)ne, count);
|
||||
|
||||
base += count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void draw_overlay_and_handles(userdata_t *ud, bool has_overlay, bool show_handle)
|
||||
{
|
||||
sg_apply_pipeline(overlay_pipeline);
|
||||
panel_log(3, "[shapes] draw_overlay: pipeline=%d has_ov=%d show_h=%d",
|
||||
overlay_pipeline.id, has_overlay, show_handle);
|
||||
sg_apply_pipeline(ud->pipelines.overlay_pipeline);
|
||||
panel_log_debug(&ud->panel_log_ctx, "[shapes] draw_overlay: pipeline=%d has_ov=%d show_h=%d",
|
||||
ud->pipelines.overlay_pipeline.id, has_overlay, show_handle);
|
||||
|
||||
if (has_overlay) {
|
||||
shape_uniform_t u;
|
||||
@@ -156,6 +281,73 @@ static void draw_overlay_and_handles(userdata_t *ud, bool has_overlay, bool show
|
||||
for (int h = 0; h < 8; h++) sg_draw(h * 5, 5, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Pen preview
|
||||
if (ud->pen.drawing && ud->pen.preview_count >= 2) {
|
||||
sg_update_buffer(ud->pen_vbuf, &(sg_range){
|
||||
ud->pen.preview_verts,
|
||||
(size_t)ud->pen.preview_count * sizeof(shape_vertex_t)
|
||||
});
|
||||
|
||||
shape_uniform_t pu;
|
||||
glm_mat4_identity(pu.transform);
|
||||
pu.state = 0;
|
||||
memset(pu._pad, 0, sizeof(pu._pad));
|
||||
|
||||
sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp));
|
||||
sg_apply_uniforms(1, &SG_RANGE(pu));
|
||||
sg_apply_bindings(&(sg_bindings){
|
||||
.vertex_buffers[0] = ud->pen_vbuf,
|
||||
.index_buffer = ud->pen_ibuf,
|
||||
});
|
||||
sg_draw(0, ud->pen.preview_count, 1);
|
||||
}
|
||||
|
||||
// Edit mode overlays
|
||||
if (ud->interact.editing_shape_idx >= 0) {
|
||||
shape_uniform_t eu;
|
||||
glm_mat4_identity(eu.transform);
|
||||
|
||||
// Handle lines (anchor → handle) — drawn as separate 2-vert segments
|
||||
if (ud->ed_handle_line_count >= 2) {
|
||||
eu.state = 0;
|
||||
memset(eu._pad, 0, sizeof(eu._pad));
|
||||
sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp));
|
||||
sg_apply_uniforms(1, &SG_RANGE(eu));
|
||||
sg_apply_bindings(&(sg_bindings){
|
||||
.vertex_buffers[0] = ud->ed_handle_line_vbuf,
|
||||
.index_buffer = ud->ed_shared_ibuf,
|
||||
});
|
||||
int n_lines = ud->ed_handle_line_count / 2;
|
||||
for (int i = 0; i < n_lines; i++) sg_draw(i * 2, 2, 1);
|
||||
}
|
||||
|
||||
// Handle squares
|
||||
if (ud->ed_handle_count > 0) {
|
||||
eu.state = 0;
|
||||
memset(eu._pad, 0, sizeof(eu._pad));
|
||||
sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp));
|
||||
sg_apply_uniforms(1, &SG_RANGE(eu));
|
||||
sg_apply_bindings(&(sg_bindings){
|
||||
.vertex_buffers[0] = ud->ed_handle_vbuf,
|
||||
.index_buffer = ud->ed_shared_ibuf,
|
||||
});
|
||||
for (int h = 0; h < ud->ed_handle_count; h++) sg_draw(h * 5, 5, 1);
|
||||
}
|
||||
|
||||
// Anchor squares (drawn last so they're on top)
|
||||
if (ud->ed_anchor_count > 0) {
|
||||
eu.state = 0;
|
||||
memset(eu._pad, 0, sizeof(eu._pad));
|
||||
sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp));
|
||||
sg_apply_uniforms(1, &SG_RANGE(eu));
|
||||
sg_apply_bindings(&(sg_bindings){
|
||||
.vertex_buffers[0] = ud->ed_anchor_vbuf,
|
||||
.index_buffer = ud->ed_shared_ibuf,
|
||||
});
|
||||
for (int h = 0; h < ud->ed_anchor_count; h++) sg_draw(h * 5, 5, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
14
src/globals.h
Normal file
14
src/globals.h
Normal file
@@ -0,0 +1,14 @@
|
||||
// Context struct declarations are now defined alongside the code they
|
||||
// belong to rather than in a single header:
|
||||
//
|
||||
// panel_log_ctx_t — api.h
|
||||
// shape_pool_ctx_t — shape.h (after shape_t / group_t)
|
||||
// group_index_ctx_t — shape.h
|
||||
// pipeline_ctx_t — render.h
|
||||
// shape_group_buf_t — shape.h
|
||||
//
|
||||
// This header exists only to avoid breaking includes elsewhere and can
|
||||
// be removed once all consumers are updated.
|
||||
#ifndef GLOBALS_H
|
||||
#define GLOBALS_H
|
||||
#endif
|
||||
303
src/history.h
303
src/history.h
@@ -12,6 +12,10 @@ typedef enum {
|
||||
HIST_CREATE,
|
||||
HIST_DELETE,
|
||||
HIST_GROUP,
|
||||
HIST_GROUP_CREATE,
|
||||
HIST_GROUP_DELETE,
|
||||
HIST_GROUP_REPARENT,
|
||||
HIST_EDIT,
|
||||
} hist_prop_t;
|
||||
|
||||
typedef struct hist_change_t {
|
||||
@@ -20,12 +24,28 @@ typedef struct hist_change_t {
|
||||
float old_val[4];
|
||||
float new_val[4];
|
||||
|
||||
// Owned vertex+index buffer snapshot — only used for HIST_CREATE / HIST_DELETE.
|
||||
// Freed when the history entry is discarded or the stack is destroyed.
|
||||
// Owned vertex+index buffer snapshot — only used for HIST_CREATE / HIST_DELETE
|
||||
// when the shape has no control points (pen paths).
|
||||
shape_vertex_t *vertex_data;
|
||||
uint16_t *index_data;
|
||||
int vertex_count;
|
||||
int index_count;
|
||||
|
||||
// Control point snapshot — used for HIST_CREATE / HIST_DELETE / HIST_EDIT.
|
||||
// For HIST_CREATE / HIST_DELETE, ctrl_* holds the saved shape data.
|
||||
// For HIST_EDIT, ctrl_* holds the old (pre-edit) state and
|
||||
// new_ctrl_* holds the new (post-edit) state.
|
||||
char name[64];
|
||||
shape_vertex_t *ctrl_points;
|
||||
shape_vertex_t *ctrl_handle_in;
|
||||
shape_vertex_t *ctrl_handle_out;
|
||||
int ctrl_count;
|
||||
bool closed;
|
||||
// Post-edit control point state (only used for HIST_EDIT)
|
||||
shape_vertex_t *new_ctrl_points;
|
||||
shape_vertex_t *new_ctrl_handle_in;
|
||||
shape_vertex_t *new_ctrl_handle_out;
|
||||
int new_ctrl_count;
|
||||
} hist_change_t;
|
||||
|
||||
typedef struct hist_entry_t {
|
||||
@@ -67,8 +87,14 @@ static void hist_apply_prop(shape_t *s, hist_prop_t prop, const float val[4]) {
|
||||
}
|
||||
|
||||
static void hist_free_change(hist_change_t *c) {
|
||||
if (c->vertex_data) FREE(c->vertex_data);
|
||||
if (c->index_data) FREE(c->index_data);
|
||||
if (c->vertex_data) FREE(c->vertex_data);
|
||||
if (c->index_data) FREE(c->index_data);
|
||||
if (c->ctrl_points) FREE(c->ctrl_points);
|
||||
if (c->ctrl_handle_in) FREE(c->ctrl_handle_in);
|
||||
if (c->ctrl_handle_out) FREE(c->ctrl_handle_out);
|
||||
if (c->new_ctrl_points) FREE(c->new_ctrl_points);
|
||||
if (c->new_ctrl_handle_in) FREE(c->new_ctrl_handle_in);
|
||||
if (c->new_ctrl_handle_out) FREE(c->new_ctrl_handle_out);
|
||||
memset(c, 0, sizeof(*c));
|
||||
}
|
||||
|
||||
@@ -189,23 +215,34 @@ static void history_batch_add(hist_batch_t *batch, int shape_index, hist_prop_t
|
||||
memcpy(c->new_val, new_val, sizeof(float[4]));
|
||||
}
|
||||
|
||||
// Snapshot a shape's full vertex data and metadata into a change entry.
|
||||
// Used for HIST_CREATE and HIST_DELETE entries.
|
||||
// old_val = { kind, cx, cy, num_verts }
|
||||
// new_val = { sx, sy, rotation, group_id }
|
||||
// For procedural shapes (CIRCLE, STAR, RECTANGLE), vertices are reconstructed
|
||||
// from parameters instead of deep-copied. For STAR, new_val[1] stores inner_r.
|
||||
// Snapshot a shape's full data into a change entry.
|
||||
// old_val = { cx, cy, num_elements, 0 }
|
||||
// new_val = { sx, sy, rotation, group_id }
|
||||
// For procedural shapes (ctrl_count > 0), control points are deep-copied.
|
||||
// For pen paths (ctrl_count == 0), raw vertex/index data is deep-copied.
|
||||
static void hist_snapshot_shape_verts(hist_change_t *c, shape_t *s) {
|
||||
c->old_val[0] = (float)s->kind;
|
||||
c->old_val[1] = s->cx;
|
||||
c->old_val[2] = s->cy;
|
||||
c->old_val[3] = (float)s->num_verts;
|
||||
c->old_val[0] = s->cx;
|
||||
c->old_val[1] = s->cy;
|
||||
c->old_val[2] = (float)(int)s->num_elements;
|
||||
c->old_val[3] = 0;
|
||||
c->new_val[0] = s->sx;
|
||||
c->new_val[1] = s->sy;
|
||||
c->new_val[2] = s->rotation;
|
||||
c->new_val[3] = (float)s->group_id;
|
||||
|
||||
if (s->kind == SHAPE_GENERIC) {
|
||||
strncpy(c->name, s->name, sizeof(c->name) - 1);
|
||||
c->name[sizeof(c->name) - 1] = '\0';
|
||||
c->closed = s->closed;
|
||||
|
||||
if (s->ctrl_count > 0) {
|
||||
c->ctrl_count = s->ctrl_count;
|
||||
c->ctrl_points = (shape_vertex_t*) ALLOC((size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
c->ctrl_handle_in = (shape_vertex_t*) ALLOC((size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
c->ctrl_handle_out = (shape_vertex_t*) ALLOC((size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
memcpy(c->ctrl_points, s->ctrl_points, (size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
memcpy(c->ctrl_handle_in, s->ctrl_handle_in, (size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
memcpy(c->ctrl_handle_out, s->ctrl_handle_out, (size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
} else {
|
||||
int n = (int)s->num_elements;
|
||||
c->vertex_count = n;
|
||||
c->index_count = n;
|
||||
@@ -213,22 +250,10 @@ static void hist_snapshot_shape_verts(hist_change_t *c, shape_t *s) {
|
||||
c->index_data = (uint16_t*) ALLOC((size_t)n * sizeof(uint16_t));
|
||||
memcpy(c->vertex_data, s->verts, (size_t)n * sizeof(shape_vertex_t));
|
||||
memcpy(c->index_data, s->indices, (size_t)n * sizeof(uint16_t));
|
||||
} else if (s->kind == SHAPE_STAR) {
|
||||
float inner_ratio = sqrtf(s->verts[1].x * s->verts[1].x + s->verts[1].y * s->verts[1].y);
|
||||
c->new_val[1] = inner_ratio * s->sx;
|
||||
c->vertex_count = 0;
|
||||
c->index_count = 0;
|
||||
c->vertex_data = NULL;
|
||||
c->index_data = NULL;
|
||||
} else {
|
||||
c->vertex_count = 0;
|
||||
c->index_count = 0;
|
||||
c->vertex_data = NULL;
|
||||
c->index_data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Append a CREATE or DELETE entry to a batch, snapshotting the shape's vertex data.
|
||||
// Append a CREATE or DELETE entry to a batch, snapshotting the shape's data.
|
||||
static void history_batch_add_shape(hist_batch_t *batch, int shape_index,
|
||||
hist_prop_t prop, shape_t *s) {
|
||||
hist_change_t *c = &batch->changes[batch->count++];
|
||||
@@ -237,61 +262,112 @@ static void history_batch_add_shape(hist_batch_t *batch, int shape_index,
|
||||
hist_snapshot_shape_verts(c, s);
|
||||
}
|
||||
|
||||
// Snapshot both old and new control point states for a HIST_EDIT change.
|
||||
static void history_batch_add_edit(hist_batch_t *batch, int shape_index, shape_t *s,
|
||||
const shape_vertex_t *old_pts,
|
||||
const shape_vertex_t *old_hin,
|
||||
const shape_vertex_t *old_hout,
|
||||
int old_count) {
|
||||
hist_change_t *c = &batch->changes[batch->count++];
|
||||
c->shape_index = shape_index;
|
||||
c->prop = HIST_EDIT;
|
||||
// Store shape metadata
|
||||
c->old_val[0] = s->cx;
|
||||
c->old_val[1] = s->cy;
|
||||
c->old_val[2] = (float)(int)s->num_elements;
|
||||
c->old_val[3] = 0;
|
||||
c->new_val[0] = s->sx;
|
||||
c->new_val[1] = s->sy;
|
||||
c->new_val[2] = s->rotation;
|
||||
c->new_val[3] = (float)s->group_id;
|
||||
strncpy(c->name, s->name, sizeof(c->name) - 1);
|
||||
c->name[sizeof(c->name) - 1] = '\0';
|
||||
c->closed = s->closed;
|
||||
// Snapshot old ctrl state
|
||||
c->ctrl_count = old_count;
|
||||
c->ctrl_points = (shape_vertex_t*) ALLOC((size_t)old_count * sizeof(shape_vertex_t));
|
||||
c->ctrl_handle_in = (shape_vertex_t*) ALLOC((size_t)old_count * sizeof(shape_vertex_t));
|
||||
c->ctrl_handle_out = (shape_vertex_t*) ALLOC((size_t)old_count * sizeof(shape_vertex_t));
|
||||
memcpy(c->ctrl_points, old_pts, (size_t)old_count * sizeof(shape_vertex_t));
|
||||
memcpy(c->ctrl_handle_in, old_hin, (size_t)old_count * sizeof(shape_vertex_t));
|
||||
memcpy(c->ctrl_handle_out, old_hout, (size_t)old_count * sizeof(shape_vertex_t));
|
||||
// Snapshot new ctrl state
|
||||
c->new_ctrl_count = s->ctrl_count;
|
||||
c->new_ctrl_points = (shape_vertex_t*) ALLOC((size_t)s->ctrl_count * sizeof(shape_vertex_t));
|
||||
c->new_ctrl_handle_in = (shape_vertex_t*) ALLOC((size_t)s->ctrl_count * sizeof(shape_vertex_t));
|
||||
c->new_ctrl_handle_out = (shape_vertex_t*) ALLOC((size_t)s->ctrl_count * sizeof(shape_vertex_t));
|
||||
memcpy(c->new_ctrl_points, s->ctrl_points, (size_t)s->ctrl_count * sizeof(shape_vertex_t));
|
||||
memcpy(c->new_ctrl_handle_in, s->ctrl_handle_in, (size_t)s->ctrl_count * sizeof(shape_vertex_t));
|
||||
memcpy(c->new_ctrl_handle_out, s->ctrl_handle_out, (size_t)s->ctrl_count * sizeof(shape_vertex_t));
|
||||
}
|
||||
|
||||
static void history_batch_commit(hist_batch_t *batch, history_t *h) {
|
||||
hist_entry_t entry = { .changes = batch->changes, .count = batch->count };
|
||||
history_push_entry(h, entry);
|
||||
}
|
||||
|
||||
// Reconstruct a shape_t from a HIST_CREATE / HIST_DELETE change snapshot.
|
||||
static shape_t hist_rebuild_shape_from_snapshot(const hist_change_t *c) {
|
||||
float cx = c->old_val[1], cy = c->old_val[2];
|
||||
float sx = c->new_val[0], rot = c->new_val[2];
|
||||
static shape_t hist_rebuild_shape_from_snapshot(shape_pool_ctx_t *sp, const hist_change_t *c) {
|
||||
float cx = c->old_val[0], cy = c->old_val[1];
|
||||
float sx = c->new_val[0], sy = c->new_val[1];
|
||||
float rot = c->new_val[2];
|
||||
int gid = (int)c->new_val[3];
|
||||
shape_t s;
|
||||
|
||||
switch ((int)c->old_val[0]) {
|
||||
case SHAPE_CIRCLE:
|
||||
s = shape_circle(cx, cy, sx);
|
||||
break;
|
||||
case SHAPE_RECTANGLE:
|
||||
s = shape_rectangle(cx, cy, sx * 2.0f, c->new_val[1] * 2.0f);
|
||||
break;
|
||||
case SHAPE_STAR: {
|
||||
int points = (int)c->old_val[3] / 2;
|
||||
s = shape_star(cx, cy, sx, c->new_val[1], points);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
memset(&s, 0, sizeof(s));
|
||||
s.kind = (int)c->old_val[0];
|
||||
s.cx = cx;
|
||||
s.cy = cy;
|
||||
s.num_verts = (uint32_t)c->old_val[3];
|
||||
s.num_elements = (uint32_t)c->vertex_count;
|
||||
s.sx = sx;
|
||||
s.sy = c->new_val[1];
|
||||
int n = c->vertex_count;
|
||||
s.verts = (shape_vertex_t*) ALLOC((size_t)n * sizeof(shape_vertex_t));
|
||||
s.indices = (uint16_t*) ALLOC((size_t)n * sizeof(uint16_t));
|
||||
memcpy(s.verts, c->vertex_data, (size_t)n * sizeof(shape_vertex_t));
|
||||
memcpy(s.indices, c->index_data, (size_t)n * sizeof(uint16_t));
|
||||
shape_init_common(&s);
|
||||
shape_build_transform(&s);
|
||||
shape_make_buffers(&s);
|
||||
return s;
|
||||
}
|
||||
if (c->ctrl_count > 0) {
|
||||
memset(&s, 0, sizeof(s));
|
||||
s.cx = cx;
|
||||
s.cy = cy;
|
||||
s.sx = sx;
|
||||
s.sy = sy;
|
||||
s.rotation = rot;
|
||||
shape_init_common(&s);
|
||||
strncpy(s.name, c->name, sizeof(s.name) - 1);
|
||||
s.closed = c->closed;
|
||||
s.ctrl_count = c->ctrl_count;
|
||||
s.ctrl_points = (shape_vertex_t*) ALLOC((size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
s.ctrl_handle_in = (shape_vertex_t*) ALLOC((size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
s.ctrl_handle_out = (shape_vertex_t*) ALLOC((size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
memcpy(s.ctrl_points, c->ctrl_points, (size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
memcpy(s.ctrl_handle_in, c->ctrl_handle_in, (size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
memcpy(s.ctrl_handle_out, c->ctrl_handle_out, (size_t)c->ctrl_count * sizeof(shape_vertex_t));
|
||||
shape_regenerate_from_ctrl(sp, &s);
|
||||
} else {
|
||||
memset(&s, 0, sizeof(s));
|
||||
s.cx = cx;
|
||||
s.cy = cy;
|
||||
s.rotation = rot;
|
||||
s.num_verts = (uint32_t)c->vertex_count;
|
||||
s.num_elements = (uint32_t)c->vertex_count;
|
||||
s.sx = sx;
|
||||
s.sy = sy;
|
||||
int n = c->vertex_count;
|
||||
s.verts = (shape_vertex_t*) ALLOC((size_t)n * sizeof(shape_vertex_t));
|
||||
s.indices = (uint16_t*) ALLOC((size_t)n * sizeof(uint16_t));
|
||||
memcpy(s.verts, c->vertex_data, (size_t)n * sizeof(shape_vertex_t));
|
||||
memcpy(s.indices, c->index_data, (size_t)n * sizeof(uint16_t));
|
||||
shape_init_common(&s);
|
||||
strncpy(s.name, c->name, sizeof(s.name) - 1);
|
||||
s.vertex_hash = hash_vertex_data(s.verts, s.num_elements);
|
||||
shape_build_transform(sp, &s);
|
||||
shape_update_aabb(&s);
|
||||
shape_make_buffers(sp, &s);
|
||||
}
|
||||
s.rotation = rot;
|
||||
s.group_id = gid;
|
||||
shape_build_transform(&s);
|
||||
return s;
|
||||
}
|
||||
|
||||
static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forward) {
|
||||
static void history_apply_entry(history_t *h, vector_t *shapes,
|
||||
shape_pool_ctx_t *sp, vector_t *groups,
|
||||
group_index_ctx_t *gi, bool forward) {
|
||||
(void)h;
|
||||
hist_entry_t *entry = (hist_entry_t*) vec_get(&h->entries, h->current);
|
||||
bool has_shape_ops = false;
|
||||
for (int i = 0; i < entry->count; i++) {
|
||||
hist_prop_t p = entry->changes[i].prop;
|
||||
if (p == HIST_CREATE || p == HIST_DELETE) { has_shape_ops = true; break; }
|
||||
if (p == HIST_CREATE || p == HIST_DELETE ||
|
||||
p == HIST_GROUP_CREATE || p == HIST_GROUP_DELETE) { has_shape_ops = true; break; }
|
||||
}
|
||||
|
||||
int start = 0, end = entry->count, step = 1;
|
||||
@@ -304,40 +380,119 @@ static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forw
|
||||
for (int i = start; i != end; i += step) {
|
||||
hist_change_t *c = &entry->changes[i];
|
||||
|
||||
if (c->prop == HIST_GROUP_CREATE) {
|
||||
int gid = (int)c->new_val[0];
|
||||
int parent_id = (int)c->new_val[1];
|
||||
if (forward) {
|
||||
group_t g = { .id = gid, .parent_id = parent_id, .collapsed = false };
|
||||
*((group_t*) vec_push(groups)) = g;
|
||||
} else {
|
||||
for (int g = 0; g < groups->count; g++) {
|
||||
group_t *grp = (group_t*) vec_get(groups, g);
|
||||
if (grp->id == gid) {
|
||||
if (grp->member_indices) FREE(grp->member_indices);
|
||||
vec_remove_ordered(groups, g);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
gi->dirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c->prop == HIST_GROUP_DELETE) {
|
||||
int gid = (int)c->old_val[0];
|
||||
int parent_id = (int)c->old_val[1];
|
||||
if (forward) {
|
||||
for (int g = 0; g < groups->count; g++) {
|
||||
group_t *grp = (group_t*) vec_get(groups, g);
|
||||
if (grp->id == gid) {
|
||||
if (grp->member_indices) FREE(grp->member_indices);
|
||||
vec_remove_ordered(groups, g);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
group_t g = { .id = gid, .parent_id = parent_id, .collapsed = false };
|
||||
*((group_t*) vec_push(groups)) = g;
|
||||
}
|
||||
gi->dirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c->prop == HIST_GROUP_REPARENT) {
|
||||
int gid = (int)c->new_val[0];
|
||||
int new_pid = forward ? (int)c->new_val[1] : (int)c->old_val[1];
|
||||
for (int g = 0; g < groups->count; g++) {
|
||||
group_t *grp = (group_t*) vec_get(groups, g);
|
||||
if (grp->id == gid) {
|
||||
grp->parent_id = new_pid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c->prop == HIST_CREATE || c->prop == HIST_DELETE) {
|
||||
bool adding = (c->prop == HIST_CREATE) ? forward : !forward;
|
||||
if (adding) {
|
||||
shape_t s = hist_rebuild_shape_from_snapshot(c);
|
||||
if (c->shape_index < 0 || c->shape_index > shapes->count) continue;
|
||||
shape_t s = hist_rebuild_shape_from_snapshot(sp, c);
|
||||
*((shape_t*) vec_insert(shapes, c->shape_index)) = s;
|
||||
} else {
|
||||
if (c->shape_index < shapes->count) {
|
||||
shape_t *s = (shape_t*) vec_get(shapes, c->shape_index);
|
||||
shape_shutdown(s);
|
||||
shape_shutdown(sp, s);
|
||||
vec_remove_ordered(shapes, c->shape_index);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c->prop == HIST_EDIT) {
|
||||
if (c->shape_index >= shapes->count) continue;
|
||||
shape_t *s = (shape_t*) vec_get(shapes, c->shape_index);
|
||||
shape_vertex_t *pts = forward ? c->new_ctrl_points : c->ctrl_points;
|
||||
shape_vertex_t *hin = forward ? c->new_ctrl_handle_in : c->ctrl_handle_in;
|
||||
shape_vertex_t *hout = forward ? c->new_ctrl_handle_out : c->ctrl_handle_out;
|
||||
int cc = forward ? c->new_ctrl_count : c->ctrl_count;
|
||||
if (!pts || cc <= 0) continue;
|
||||
FREE(s->ctrl_points);
|
||||
FREE(s->ctrl_handle_in);
|
||||
FREE(s->ctrl_handle_out);
|
||||
s->ctrl_count = cc;
|
||||
s->ctrl_points = (shape_vertex_t*) ALLOC((size_t)cc * sizeof(shape_vertex_t));
|
||||
s->ctrl_handle_in = (shape_vertex_t*) ALLOC((size_t)cc * sizeof(shape_vertex_t));
|
||||
s->ctrl_handle_out = (shape_vertex_t*) ALLOC((size_t)cc * sizeof(shape_vertex_t));
|
||||
memcpy(s->ctrl_points, pts, (size_t)cc * sizeof(shape_vertex_t));
|
||||
memcpy(s->ctrl_handle_in, hin, (size_t)cc * sizeof(shape_vertex_t));
|
||||
memcpy(s->ctrl_handle_out, hout, (size_t)cc * sizeof(shape_vertex_t));
|
||||
shape_regenerate_from_ctrl(sp, s);
|
||||
shape_set_state(sp, s, s->hovered, s->selected);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c->shape_index >= shapes->count) continue;
|
||||
shape_t *s = (shape_t*) vec_get(shapes, c->shape_index);
|
||||
hist_apply_prop(s, c->prop, forward ? c->new_val : c->old_val);
|
||||
shape_regenerate(s);
|
||||
shape_set_state(s, s->hovered, s->selected);
|
||||
shape_regenerate(sp, s);
|
||||
shape_set_state(sp, s, s->hovered, s->selected);
|
||||
}
|
||||
}
|
||||
|
||||
static bool history_undo(history_t *h, vector_t *shapes) {
|
||||
if (h->current < 0) return false;
|
||||
history_apply_entry((hist_entry_t*) vec_get(&h->entries, h->current), shapes, false);
|
||||
static bool history_undo(history_t *h, vector_t *shapes, shape_pool_ctx_t *sp,
|
||||
vector_t *groups, group_index_ctx_t *gi) {
|
||||
if (h->current < 0 || h->current >= h->entries.count) return false;
|
||||
history_apply_entry(h, shapes, sp, groups, gi, false);
|
||||
h->current--;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool history_redo(history_t *h, vector_t *shapes) {
|
||||
static bool history_redo(history_t *h, vector_t *shapes, shape_pool_ctx_t *sp,
|
||||
vector_t *groups, group_index_ctx_t *gi) {
|
||||
if (h->current + 1 >= h->entries.count) return false;
|
||||
h->current++;
|
||||
history_apply_entry((hist_entry_t*) vec_get(&h->entries, h->current), shapes, true);
|
||||
history_apply_entry(h, shapes, sp, groups, gi, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
747
src/input.h
747
src/input.h
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,22 @@
|
||||
#include "api.h"
|
||||
#include "types.h"
|
||||
|
||||
// Forward-declared: defined in overlay.h (included after interact.h in api.h).
|
||||
static void overlay_invalidate(userdata_t *ud);
|
||||
|
||||
// Called after any operation that changes shape count or structure (undo,
|
||||
// redo, delete, paste, group, ungroup). Invalidates cached state so the next
|
||||
// frame rebuilds the spatial grid, overlay geometry, and GPU instance data.
|
||||
static void interact_structural_change(userdata_t *ud)
|
||||
{
|
||||
ud->interact.hovered_shape = -1;
|
||||
ud->shape_pool.pool_dirty = true;
|
||||
spatial_mark_dirty(&ud->spatial_grid);
|
||||
ud->interact.aabb_cached = false;
|
||||
ud->ui.display_cache_dirty = true;
|
||||
overlay_invalidate(ud);
|
||||
}
|
||||
|
||||
static void selected_aabb(userdata_t *ud, float *min_x, float *min_y,
|
||||
float *max_x, float *max_y)
|
||||
{
|
||||
@@ -11,36 +27,33 @@ static void selected_aabb(userdata_t *ud, float *min_x, float *min_y,
|
||||
for (int i = 0; i < ud->shapes.count; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
if (!s->selected) continue;
|
||||
float sc = s->cos_r, ss = s->sin_r;
|
||||
for (uint32_t v = 0; v < s->num_verts; v++) {
|
||||
float lx = s->verts[v].x * s->sx;
|
||||
float ly = s->verts[v].y * s->sy;
|
||||
float wx = s->cx + lx * sc - ly * ss;
|
||||
float wy = s->cy + lx * ss + ly * sc;
|
||||
if (first) { *min_x = *max_x = wx; *min_y = *max_y = wy; first = false; }
|
||||
else {
|
||||
if (wx < *min_x) *min_x = wx;
|
||||
if (wx > *max_x) *max_x = wx;
|
||||
if (wy < *min_y) *min_y = wy;
|
||||
if (wy > *max_y) *max_y = wy;
|
||||
}
|
||||
float smin_x = s->cx - s->aabb_hx;
|
||||
float smin_y = s->cy - s->aabb_hy;
|
||||
float smax_x = s->cx + s->aabb_hx;
|
||||
float smax_y = s->cy + s->aabb_hy;
|
||||
if (first) {
|
||||
*min_x = smin_x; *min_y = smin_y;
|
||||
*max_x = smax_x; *max_y = smax_y;
|
||||
first = false;
|
||||
} else {
|
||||
if (smin_x < *min_x) *min_x = smin_x;
|
||||
if (smin_y < *min_y) *min_y = smin_y;
|
||||
if (smax_x > *max_x) *max_x = smax_x;
|
||||
if (smax_y > *max_y) *max_y = smax_y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void update_shape_states(userdata_t *ud)
|
||||
{
|
||||
for (int i = 0; i < ud->shapes.count; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
shape_set_state(s, s->hovered, s->selected);
|
||||
}
|
||||
ud->shape_pool.states_dirty = true;
|
||||
}
|
||||
|
||||
static void select_group_recursive(userdata_t *ud, int gid)
|
||||
{
|
||||
for (int i = 0; i < ud->shapes.count; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
if (is_shape_in_group_hierarchy(s->group_id, gid, &ud->groups)) {
|
||||
if (is_shape_in_group_hierarchy(&ud->group_idx,s->group_id, gid, &ud->groups)) {
|
||||
s->selected = true;
|
||||
ud->interact.selected_count++;
|
||||
}
|
||||
@@ -60,7 +73,7 @@ static void toggle_group_recursive(userdata_t *ud, int gid)
|
||||
int total = 0, sel = 0;
|
||||
for (int i = 0; i < ud->shapes.count; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
if (is_shape_in_group_hierarchy(s->group_id, gid, &ud->groups)) {
|
||||
if (is_shape_in_group_hierarchy(&ud->group_idx,s->group_id, gid, &ud->groups)) {
|
||||
total++;
|
||||
if (s->selected) sel++;
|
||||
}
|
||||
@@ -68,58 +81,13 @@ static void toggle_group_recursive(userdata_t *ud, int gid)
|
||||
bool all_sel = (sel == total && total > 0);
|
||||
for (int i = 0; i < ud->shapes.count; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
if (is_shape_in_group_hierarchy(s->group_id, gid, &ud->groups)) {
|
||||
if (is_shape_in_group_hierarchy(&ud->group_idx,s->group_id, gid, &ud->groups)) {
|
||||
s->selected = !all_sel;
|
||||
ud->interact.selected_count += s->selected ? 1 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void rebuild_groups_from_shapes(vector_t *groups, vector_t *shapes)
|
||||
{
|
||||
// Save existing parent relationships so nested groups survive rebuild
|
||||
int old_count = groups->count;
|
||||
int *saved = NULL;
|
||||
if (old_count > 0) {
|
||||
saved = (int*) ALLOC((size_t)old_count * 2 * sizeof(int));
|
||||
for (int i = 0; i < old_count; i++) {
|
||||
group_t *g = (group_t*) vec_get(groups, i);
|
||||
saved[i * 2] = g->id;
|
||||
saved[i * 2 + 1] = g->parent_id;
|
||||
}
|
||||
}
|
||||
|
||||
groups->count = 0;
|
||||
for (int i = 0; i < shapes->count; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(shapes, i);
|
||||
if (s->group_id == 0) continue;
|
||||
bool found = false;
|
||||
for (int g = 0; g < groups->count; g++) {
|
||||
if (((group_t*) vec_get(groups, g))->id == s->group_id) { found = true; break; }
|
||||
}
|
||||
if (!found) {
|
||||
group_t grp = { .id = s->group_id, .parent_id = 0 };
|
||||
*((group_t*) vec_push(groups)) = grp;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore parent relationships for groups that still exist
|
||||
for (int i = 0; i < old_count; i++) {
|
||||
int gid = saved[i * 2];
|
||||
int pid = saved[i * 2 + 1];
|
||||
if (pid == 0) continue;
|
||||
group_t *g = find_group(groups, gid);
|
||||
if (g) {
|
||||
// Only restore if parent group still exists
|
||||
if (find_group(groups, pid))
|
||||
g->parent_id = pid;
|
||||
}
|
||||
}
|
||||
|
||||
if (saved) FREE(saved);
|
||||
group_index_rebuild(groups);
|
||||
}
|
||||
|
||||
static int hit_test_resize_handles(userdata_t *ud, float wx, float wy, float tol)
|
||||
{
|
||||
if (ud->interact.selected_count <= 0) return -1;
|
||||
|
||||
123
src/main.c
123
src/main.c
@@ -27,6 +27,7 @@ static void log_capture(const char* tag, uint32_t log_level, uint32_t log_item,
|
||||
strncpy(ud->ui.log_ring[idx].text, buf, 255);
|
||||
ud->ui.log_ring[idx].text[255] = 0;
|
||||
ud->ui.log_ring[idx].level = log_level;
|
||||
ud->ui.log_ring[idx].hash = 0;
|
||||
ud->ui.log_head = (idx + 1) % LOG_RING_SIZE;
|
||||
if (ud->ui.log_count < LOG_RING_SIZE) ud->ui.log_count++;
|
||||
|
||||
@@ -34,8 +35,33 @@ static void log_capture(const char* tag, uint32_t log_level, uint32_t log_item,
|
||||
if (log_level <= 1) ud->ui.log_show = true;
|
||||
}
|
||||
|
||||
static uint64_t fnv1a_64(const char *msg) {
|
||||
uint64_t h = 14695981039346656037ULL;
|
||||
while (*msg) {
|
||||
h ^= (uint64_t)(unsigned char)*msg++;
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static void panel_log_impl(void *ud_v, int level, const char *msg) {
|
||||
userdata_t *ud = (userdata_t*)ud_v;
|
||||
|
||||
// Use a 64-bit message hash to skip the O(n) strcmp scan for most
|
||||
// non-matches. Debug-level messages (3) skip dedup entirely — they are
|
||||
// expected to repeat and the linear scan cost isn't worth it.
|
||||
if (level < 3) {
|
||||
uint64_t h = fnv1a_64(msg);
|
||||
int total = ud->ui.log_count < LOG_RING_SIZE ? ud->ui.log_count : LOG_RING_SIZE;
|
||||
for (int i = 0; i < total; i++) {
|
||||
if (ud->ui.log_ring[i].hash == h &&
|
||||
strcmp(ud->ui.log_ring[i].text, msg) == 0) return;
|
||||
}
|
||||
ud->ui.log_ring[ud->ui.log_head].hash = h;
|
||||
} else {
|
||||
ud->ui.log_ring[ud->ui.log_head].hash = 0;
|
||||
}
|
||||
|
||||
int idx = ud->ui.log_head;
|
||||
strncpy(ud->ui.log_ring[idx].text, msg, 255);
|
||||
ud->ui.log_ring[idx].text[255] = 0;
|
||||
@@ -108,12 +134,12 @@ static void frame(void* _userdata)
|
||||
|
||||
static void init(void* _userdata)
|
||||
{
|
||||
rand_seed(1);
|
||||
|
||||
userdata_t* ud = (userdata_t*) _userdata;
|
||||
|
||||
g_panel_log_fn = panel_log_impl;
|
||||
g_panel_log_ud = ud;
|
||||
rand_seed(&ud->rand_ctx, 1);
|
||||
|
||||
ud->panel_log_ctx.fn = panel_log_impl;
|
||||
ud->panel_log_ctx.ud = ud;
|
||||
|
||||
sg_desc sgdesc = {
|
||||
.environment = sglue_environment(),
|
||||
@@ -226,7 +252,7 @@ static void init(void* _userdata)
|
||||
},
|
||||
};
|
||||
|
||||
shape_init_pipeline();
|
||||
shape_init_pipeline(&ud->pipelines, &ud->panel_log_ctx);
|
||||
|
||||
vec_init(&ud->shapes, sizeof(shape_t));
|
||||
vec_init(&ud->groups, sizeof(group_t));
|
||||
@@ -240,18 +266,22 @@ static void init(void* _userdata)
|
||||
ud->ui.left_panel_w = 220;
|
||||
ud->ui.list_last_shape = -1;
|
||||
ud->ui.list_prev_count = -1;
|
||||
ud->ui.display_cache = NULL;
|
||||
ud->ui.display_cache_len = 0;
|
||||
ud->ui.display_cache_dirty = true;
|
||||
ud->interact.move.dragging = false;
|
||||
ud->interact.rotate.dragging = false;
|
||||
ud->interact.resize.dragging = false;
|
||||
ud->interact.resize.angle = 0.0f;
|
||||
ud->interact.resize.init = NULL;
|
||||
ud->overlay_upload_needed = true;
|
||||
overlay_invalidate(ud);
|
||||
ud->interact.resize.init_count = 0;
|
||||
ud->next_group_id = 1;
|
||||
ud->time = 0.0;
|
||||
ud->interact.focused_group_id = 0;
|
||||
ud->interact.last_click_time = 0.0;
|
||||
ud->interact.last_click_shape_idx = -1;
|
||||
ud->map_w = 0;
|
||||
ud->map_h = 0;
|
||||
ud->ui.log_head = 0;
|
||||
ud->ui.log_count = 0;
|
||||
ud->ui.log_show = true;
|
||||
@@ -302,20 +332,75 @@ static void init(void* _userdata)
|
||||
});
|
||||
}
|
||||
|
||||
*((shape_t*) vec_push(&ud->shapes)) = shape_star(0.0f, 0.0f, 200.0f, 80.0f, 7);
|
||||
*((shape_t*) vec_push(&ud->shapes)) = shape_circle(&ud->shape_pool, 300.0f, 0.0f, 120.0f);
|
||||
|
||||
*((shape_t*) vec_push(&ud->shapes)) = shape_circle(300.0f, 0.0f, 120.0f);
|
||||
// Pen tool buffers
|
||||
{
|
||||
ud->pen_vbuf = sg_make_buffer(&(sg_buffer_desc){
|
||||
.size = PEN_PREVIEW_MAX_VERTS * sizeof(shape_vertex_t),
|
||||
.usage = { .stream_update = true },
|
||||
.label = "Pen preview verts",
|
||||
});
|
||||
uint16_t *pen_idx = (uint16_t*) ALLOC(PEN_PREVIEW_MAX_VERTS * sizeof(uint16_t));
|
||||
for (int i = 0; i < PEN_PREVIEW_MAX_VERTS; i++) pen_idx[i] = (uint16_t)i;
|
||||
ud->pen_ibuf = sg_make_buffer(&(sg_buffer_desc){
|
||||
.usage = {.index_buffer = true},
|
||||
.data = {pen_idx, (size_t)PEN_PREVIEW_MAX_VERTS * sizeof(uint16_t)},
|
||||
.label = "Pen preview indices",
|
||||
});
|
||||
FREE(pen_idx);
|
||||
memset(&ud->pen, 0, sizeof(ud->pen));
|
||||
}
|
||||
|
||||
// Edit mode buffers
|
||||
{
|
||||
int amax = PEN_MAX_CONTROL_POINTS * 5; // anchors
|
||||
int hmax = PEN_MAX_CONTROL_POINTS * 10; // handles (2 per anchor, 5 verts each)
|
||||
int lmax = PEN_MAX_CONTROL_POINTS * 4; // lines (2 per anchor, 2 verts each)
|
||||
|
||||
ud->ed_anchor_vbuf = sg_make_buffer(&(sg_buffer_desc){
|
||||
.size = (size_t)amax * sizeof(shape_vertex_t),
|
||||
.usage = { .stream_update = true },
|
||||
.label = "Edit anchor verts",
|
||||
});
|
||||
ud->ed_handle_vbuf = sg_make_buffer(&(sg_buffer_desc){
|
||||
.size = (size_t)hmax * sizeof(shape_vertex_t),
|
||||
.usage = { .stream_update = true },
|
||||
.label = "Edit handle verts",
|
||||
});
|
||||
ud->ed_handle_line_vbuf = sg_make_buffer(&(sg_buffer_desc){
|
||||
.size = (size_t)lmax * sizeof(shape_vertex_t),
|
||||
.usage = { .stream_update = true },
|
||||
.label = "Edit handle lines",
|
||||
});
|
||||
|
||||
int ibmax = hmax > lmax ? hmax : lmax;
|
||||
if (amax > ibmax) ibmax = amax;
|
||||
uint16_t *ed_idx = (uint16_t*) ALLOC((size_t)ibmax * sizeof(uint16_t));
|
||||
for (int i = 0; i < ibmax; i++) ed_idx[i] = (uint16_t)i;
|
||||
ud->ed_shared_ibuf = sg_make_buffer(&(sg_buffer_desc){
|
||||
.usage = {.index_buffer = true},
|
||||
.data = {ed_idx, (size_t)ibmax * sizeof(uint16_t)},
|
||||
.label = "Edit shared indices",
|
||||
});
|
||||
FREE(ed_idx);
|
||||
|
||||
ud->ed_anchor_count = 0;
|
||||
ud->ed_handle_count = 0;
|
||||
ud->ed_handle_line_count = 0;
|
||||
ud->interact.editing_shape_idx = -1;
|
||||
}
|
||||
history_init(&ud->history);
|
||||
|
||||
EM_ASM({
|
||||
window.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
if (e.key === 'z' || e.key === 'y' || e.key === 'c' || e.key === 'v') {
|
||||
if (e.key === 'z' || e.key === 'y' || e.key === 'c' || e.key === 'v' || e.key === 'g') {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
Module._cartograph_canvas = document.querySelector('canvas');
|
||||
});
|
||||
|
||||
compute_mvp(&ud->camera, &ud->renderer.uniform.mvp);
|
||||
@@ -326,14 +411,16 @@ static void cleanup(void* _userdata)
|
||||
userdata_t* ud = (userdata_t*) _userdata;
|
||||
|
||||
for (int i = 0; i < ud->shapes.count; i++) {
|
||||
shape_shutdown((shape_t*) vec_get(&ud->shapes, i));
|
||||
shape_shutdown(&ud->shape_pool, (shape_t*) vec_get(&ud->shapes, i));
|
||||
}
|
||||
spatial_destroy(&ud->spatial_grid);
|
||||
vec_free(&ud->shapes);
|
||||
group_shutdown_members(&ud->groups);
|
||||
vec_free(&ud->groups);
|
||||
vec_free(&ud->interact.drag_indices);
|
||||
group_index_shutdown();
|
||||
group_index_shutdown(&ud->group_idx);
|
||||
history_destroy(&ud->history);
|
||||
if (ud->interact.edit_saved_ctrl) { FREE(ud->interact.edit_saved_ctrl); FREE(ud->interact.edit_saved_hin); FREE(ud->interact.edit_saved_hout); }
|
||||
if (ud->interact.resize.init) FREE(ud->interact.resize.init);
|
||||
sg_destroy_buffer(ud->rect_vbuf);
|
||||
sg_destroy_buffer(ud->rect_ibuf);
|
||||
@@ -341,17 +428,23 @@ static void cleanup(void* _userdata)
|
||||
sg_destroy_buffer(ud->handle_ibuf);
|
||||
sg_destroy_buffer(ud->corner_vbuf);
|
||||
sg_destroy_buffer(ud->corner_ibuf);
|
||||
sg_destroy_buffer(ud->pen_vbuf);
|
||||
sg_destroy_buffer(ud->pen_ibuf);
|
||||
sg_destroy_buffer(ud->ed_anchor_vbuf);
|
||||
sg_destroy_buffer(ud->ed_handle_vbuf);
|
||||
sg_destroy_buffer(ud->ed_handle_line_vbuf);
|
||||
sg_destroy_buffer(ud->ed_shared_ibuf);
|
||||
sg_destroy_pipeline(ud->renderer.pipeline);
|
||||
sg_destroy_shader(ud->renderer.shader);
|
||||
shape_pool_shutdown();
|
||||
shape_shutdown_pipeline();
|
||||
shape_pool_shutdown(&ud->shape_pool);
|
||||
shape_shutdown_pipeline(&ud->pipelines);
|
||||
|
||||
for (int i = 0; i < ud->clipboard.shape_count; i++) {
|
||||
FREE(ud->clipboard.shapes[i].verts);
|
||||
FREE(ud->clipboard.shapes[i].indices);
|
||||
}
|
||||
FREE(ud->clipboard.shapes);
|
||||
FREE(ud->clipboard.groups);
|
||||
FREE(ud->ui.display_cache);
|
||||
|
||||
FREE(ud);
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
#include "types.h"
|
||||
#include "interact.h"
|
||||
|
||||
static void overlay_invalidate(userdata_t *ud)
|
||||
{
|
||||
memset(&ud->overlay_upload, 1, sizeof(ud->overlay_upload));
|
||||
}
|
||||
|
||||
static void compute_overlay_geometry(userdata_t *ud,
|
||||
shape_vertex_t overlay_verts[5],
|
||||
float *sel_cx, float *sel_cy, float *sel_hw, float *sel_hh, float *sel_angle,
|
||||
@@ -14,6 +19,9 @@ static void compute_overlay_geometry(userdata_t *ud,
|
||||
*sel_cx = *sel_cy = *sel_angle = 0;
|
||||
*sel_hw = *sel_hh = 0;
|
||||
|
||||
// Suppress selection/resize/rotate overlay during vertex edit mode
|
||||
if (ud->interact.editing_shape_idx >= 0) { *show_handle = false; return; }
|
||||
|
||||
if (ud->interact.select.active && ud->interact.select.dragging) {
|
||||
float wx1, wy1, wx2, wy2;
|
||||
screen_to_world(&ud->camera, ud->interact.select.start_x, ud->interact.select.start_y, &wx1, &wy1);
|
||||
@@ -51,10 +59,11 @@ static void compute_overlay_geometry(userdata_t *ud,
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
if (!s->selected) continue;
|
||||
*sel_cx = s->cx; *sel_cy = s->cy;
|
||||
*sel_hw = s->sx; *sel_hh = s->sy;
|
||||
*sel_angle = s->rotation;
|
||||
float x1, y1, x2, y2;
|
||||
selected_aabb(ud, &x1, &y1, &x2, &y2);
|
||||
*sel_hw = (x2 - x1) * 0.5f;
|
||||
*sel_hh = (y2 - y1) * 0.5f;
|
||||
ud->interact.cached_aabb[0] = x1; ud->interact.cached_aabb[1] = y1;
|
||||
ud->interact.cached_aabb[2] = x2; ud->interact.cached_aabb[3] = y2;
|
||||
ud->interact.aabb_cached = true;
|
||||
@@ -104,9 +113,14 @@ static void upload_overlay_buffers(userdata_t *ud,
|
||||
float sel_cx, float sel_cy, float sel_hw, float sel_hh, float sel_angle,
|
||||
bool has_overlay, bool show_handle)
|
||||
{
|
||||
bool need_upload = ud->overlay_upload_needed ||
|
||||
bool need_upload = ud->overlay_upload.rect ||
|
||||
ud->interact.move.dragging || ud->interact.rotate.dragging ||
|
||||
ud->interact.resize.dragging || ud->interact.select.active;
|
||||
ud->interact.resize.dragging || ud->interact.select.active ||
|
||||
(ud->interact.editing_shape_idx >= 0);
|
||||
|
||||
static shape_vertex_t ed_anchor_verts[128 * 5];
|
||||
static shape_vertex_t ed_handle_verts[256 * 5];
|
||||
static shape_vertex_t ed_line_verts[256 * 2];
|
||||
|
||||
if (has_overlay && need_upload) {
|
||||
sg_update_buffer(ud->rect_vbuf, &(sg_range){overlay_verts, (size_t)5 * sizeof(shape_vertex_t)});
|
||||
@@ -166,7 +180,68 @@ static void upload_overlay_buffers(userdata_t *ud,
|
||||
}
|
||||
}
|
||||
|
||||
ud->overlay_upload_needed = false;
|
||||
// Edit mode overlay
|
||||
if (ud->interact.editing_shape_idx >= 0 && need_upload) {
|
||||
shape_t *es = (shape_t*) vec_get(&ud->shapes, ud->interact.editing_shape_idx);
|
||||
int n = es->ctrl_count;
|
||||
if (n > 128) n = 128;
|
||||
|
||||
float as = EDIT_ANCHOR_SIZE_PX / ud->camera.zoom * 0.5f;
|
||||
float hs = EDIT_HANDLE_SIZE_PX / ud->camera.zoom * 0.5f;
|
||||
|
||||
int ac = 0, hc = 0, lc = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_vertex_t wa = local_to_world(es, es->ctrl_points[i].x, es->ctrl_points[i].y);
|
||||
// Anchor square
|
||||
int ba = ac * 5;
|
||||
ed_anchor_verts[ba+0] = (shape_vertex_t){wa.x - as, wa.y - as};
|
||||
ed_anchor_verts[ba+1] = (shape_vertex_t){wa.x + as, wa.y - as};
|
||||
ed_anchor_verts[ba+2] = (shape_vertex_t){wa.x + as, wa.y + as};
|
||||
ed_anchor_verts[ba+3] = (shape_vertex_t){wa.x - as, wa.y + as};
|
||||
ed_anchor_verts[ba+4] = (shape_vertex_t){wa.x - as, wa.y - as};
|
||||
ac++;
|
||||
|
||||
shape_vertex_t wh_in = local_to_world(es, es->ctrl_handle_in[i].x, es->ctrl_handle_in[i].y);
|
||||
shape_vertex_t wh_out = local_to_world(es, es->ctrl_handle_out[i].x, es->ctrl_handle_out[i].y);
|
||||
|
||||
// Handle line: anchor → in handle
|
||||
ed_line_verts[lc++] = wa;
|
||||
ed_line_verts[lc++] = wh_in;
|
||||
// Handle line: anchor → out handle
|
||||
ed_line_verts[lc++] = wa;
|
||||
ed_line_verts[lc++] = wh_out;
|
||||
|
||||
// In handle square
|
||||
int bh = hc * 5;
|
||||
ed_handle_verts[bh+0] = (shape_vertex_t){wh_in.x - hs, wh_in.y - hs};
|
||||
ed_handle_verts[bh+1] = (shape_vertex_t){wh_in.x + hs, wh_in.y - hs};
|
||||
ed_handle_verts[bh+2] = (shape_vertex_t){wh_in.x + hs, wh_in.y + hs};
|
||||
ed_handle_verts[bh+3] = (shape_vertex_t){wh_in.x - hs, wh_in.y + hs};
|
||||
ed_handle_verts[bh+4] = (shape_vertex_t){wh_in.x - hs, wh_in.y - hs};
|
||||
hc++;
|
||||
// Out handle square
|
||||
bh = hc * 5;
|
||||
ed_handle_verts[bh+0] = (shape_vertex_t){wh_out.x - hs, wh_out.y - hs};
|
||||
ed_handle_verts[bh+1] = (shape_vertex_t){wh_out.x + hs, wh_out.y - hs};
|
||||
ed_handle_verts[bh+2] = (shape_vertex_t){wh_out.x + hs, wh_out.y + hs};
|
||||
ed_handle_verts[bh+3] = (shape_vertex_t){wh_out.x - hs, wh_out.y + hs};
|
||||
ed_handle_verts[bh+4] = (shape_vertex_t){wh_out.x - hs, wh_out.y - hs};
|
||||
hc++;
|
||||
}
|
||||
|
||||
ud->ed_anchor_count = ac;
|
||||
ud->ed_handle_count = hc;
|
||||
ud->ed_handle_line_count = lc;
|
||||
|
||||
if (ac > 0)
|
||||
sg_update_buffer(ud->ed_anchor_vbuf, &(sg_range){ed_anchor_verts, (size_t)(ac * 5) * sizeof(shape_vertex_t)});
|
||||
if (hc > 0)
|
||||
sg_update_buffer(ud->ed_handle_vbuf, &(sg_range){ed_handle_verts, (size_t)(hc * 5) * sizeof(shape_vertex_t)});
|
||||
if (lc > 0)
|
||||
sg_update_buffer(ud->ed_handle_line_vbuf, &(sg_range){ed_line_verts, (size_t)lc * sizeof(shape_vertex_t)});
|
||||
}
|
||||
|
||||
memset(&ud->overlay_upload, 0, sizeof(ud->overlay_upload));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
82
src/rand.h
82
src/rand.h
@@ -3,104 +3,114 @@
|
||||
|
||||
#include "api.h"
|
||||
|
||||
static uint32_t seed;
|
||||
typedef struct {
|
||||
uint32_t seed;
|
||||
} rand_ctx_t;
|
||||
|
||||
static uint32_t xorshift32(void);
|
||||
static void rand_seed(uint32_t _seed);
|
||||
static uint32_t next_int(void);
|
||||
static uint32_t next_int_max(uint32_t max);
|
||||
static uint32_t next_int_minmax(uint32_t min, uint32_t max);
|
||||
static float next_float(void);
|
||||
static float next_float_max(float max);
|
||||
static float next_float_minmax(float min, float max);
|
||||
static uint32_t xorshift32(rand_ctx_t *r);
|
||||
static void rand_seed(rand_ctx_t *r, uint32_t _seed);
|
||||
static uint32_t next_int(rand_ctx_t *r);
|
||||
static uint32_t next_int_max(rand_ctx_t *r, uint32_t max);
|
||||
static uint32_t next_int_minmax(rand_ctx_t *r, uint32_t min, uint32_t max);
|
||||
static float next_float(rand_ctx_t *r);
|
||||
static float next_float_max(rand_ctx_t *r, float max);
|
||||
static float next_float_minmax(rand_ctx_t *r, float min, float max);
|
||||
|
||||
/**
|
||||
* Xorshift32 PRNG core. Advances the global seed and returns the new value.
|
||||
* Xorshift32 PRNG core. Advances the seed and returns the new value.
|
||||
*
|
||||
* @return pseudo-random 32-bit integer
|
||||
* @param r PRNG context
|
||||
* @return pseudo-random 32-bit integer
|
||||
*/
|
||||
static uint32_t xorshift32(void)
|
||||
static uint32_t xorshift32(rand_ctx_t *r)
|
||||
{
|
||||
seed ^= seed<<13;
|
||||
seed ^= seed>>17;
|
||||
seed ^= seed<<5;
|
||||
return seed;
|
||||
r->seed ^= r->seed<<13;
|
||||
r->seed ^= r->seed>>17;
|
||||
r->seed ^= r->seed<<5;
|
||||
return r->seed;
|
||||
}
|
||||
/**
|
||||
* Seed the global PRNG state. Zero is ignored (caller should pass a non-zero
|
||||
* Seed the PRNG state. Zero is ignored (caller should pass a non-zero
|
||||
* seed). Runs the generator once after seeding to mix the state.
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param _seed non-zero 32-bit seed value
|
||||
*/
|
||||
static void rand_seed(uint32_t _seed)
|
||||
static void rand_seed(rand_ctx_t *r, uint32_t _seed)
|
||||
{
|
||||
if(_seed == 0)
|
||||
return;
|
||||
|
||||
seed = _seed;
|
||||
xorshift32();
|
||||
|
||||
r->seed = _seed;
|
||||
xorshift32(r);
|
||||
}
|
||||
/**
|
||||
* Return a random integer in [0, UINT32_MAX].
|
||||
*
|
||||
* @return pseudo-random 32-bit integer
|
||||
* @param r PRNG context
|
||||
* @return pseudo-random 32-bit integer
|
||||
*/
|
||||
static uint32_t next_int(void)
|
||||
static uint32_t next_int(rand_ctx_t *r)
|
||||
{
|
||||
return xorshift32();
|
||||
return xorshift32(r);
|
||||
}
|
||||
/**
|
||||
* Return a random integer in [0, max].
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random integer
|
||||
*/
|
||||
static uint32_t next_int_max(uint32_t max)
|
||||
static uint32_t next_int_max(rand_ctx_t *r, uint32_t max)
|
||||
{
|
||||
return (uint32_t)((double)xorshift32() / (double)UINT32_MAX * max);
|
||||
return (uint32_t)((double)xorshift32(r) / (double)UINT32_MAX * max);
|
||||
}
|
||||
/**
|
||||
* Return a random integer in [min, max].
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param min inclusive lower bound
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random integer
|
||||
*/
|
||||
static uint32_t next_int_minmax(uint32_t min, uint32_t max)
|
||||
static uint32_t next_int_minmax(rand_ctx_t *r, uint32_t min, uint32_t max)
|
||||
{
|
||||
const double x = (double)xorshift32() / (double)UINT32_MAX;
|
||||
const double x = (double)xorshift32(r) / (double)UINT32_MAX;
|
||||
return (uint32_t)((1.0 - x) * min + x * max);
|
||||
}
|
||||
/**
|
||||
* Return a random float in [0, 1].
|
||||
*
|
||||
* @return pseudo-random float
|
||||
* @param r PRNG context
|
||||
* @return pseudo-random float
|
||||
*/
|
||||
static float next_float(void)
|
||||
static float next_float(rand_ctx_t *r)
|
||||
{
|
||||
return (float)((double)xorshift32() / (double)UINT32_MAX);
|
||||
return (float)((double)xorshift32(r) / (double)UINT32_MAX);
|
||||
}
|
||||
/**
|
||||
* Return a random float in [0, max].
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random float
|
||||
*/
|
||||
static float next_float_max(float max)
|
||||
static float next_float_max(rand_ctx_t *r, float max)
|
||||
{
|
||||
return (float)((double)xorshift32() / (double)UINT32_MAX * max);
|
||||
return (float)((double)xorshift32(r) / (double)UINT32_MAX * max);
|
||||
}
|
||||
/**
|
||||
* Return a random float in [min, max].
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param min inclusive lower bound
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random float
|
||||
*/
|
||||
static float next_float_minmax(float min, float max)
|
||||
static float next_float_minmax(rand_ctx_t *r, float min, float max)
|
||||
{
|
||||
const double x = (double)xorshift32() / (double)UINT32_MAX;
|
||||
const double x = (double)xorshift32(r) / (double)UINT32_MAX;
|
||||
return (float)((1.0 - x) * min + x * max);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
54
src/render.h
54
src/render.h
@@ -3,10 +3,14 @@
|
||||
|
||||
#include "api.h"
|
||||
|
||||
static sg_pipeline shape_pipeline;
|
||||
static sg_shader shape_shader;
|
||||
static sg_pipeline overlay_pipeline;
|
||||
static sg_shader overlay_shader;
|
||||
// Pipeline state — was static globals, now owned by userdata_t.
|
||||
typedef struct {
|
||||
sg_pipeline shape_pipeline;
|
||||
sg_shader shape_shader;
|
||||
sg_pipeline overlay_pipeline;
|
||||
sg_shader overlay_shader;
|
||||
} pipeline_ctx_t;
|
||||
|
||||
static int g_shape_frame_id;
|
||||
|
||||
static void shape_begin_frame(void)
|
||||
@@ -14,10 +18,12 @@ static void shape_begin_frame(void)
|
||||
g_shape_frame_id++;
|
||||
}
|
||||
|
||||
static void shape_init_pipeline(void)
|
||||
// Pipeline state is owned by pipeline_ctx_t (embedded in userdata_t).
|
||||
// Previously these were file-scope statics, which prevented multi-TU builds.
|
||||
static void shape_init_pipeline(pipeline_ctx_t *p, panel_log_ctx_t *pl)
|
||||
{
|
||||
// Overlay shader/pipeline (simple, no storage buffers)
|
||||
overlay_shader = sg_make_shader(&(sg_shader_desc) {
|
||||
p->overlay_shader = sg_make_shader(&(sg_shader_desc) {
|
||||
.vertex_func = {
|
||||
.source = (const char*) src_shaders_overlay_wgsl,
|
||||
.entry = "vs_main",
|
||||
@@ -43,10 +49,12 @@ static void shape_init_pipeline(void)
|
||||
},
|
||||
.label = "Overlay shader",
|
||||
});
|
||||
panel_log(3, "[shapes] overlay shader id=%d valid=%d", overlay_shader.id, sg_isvalid());
|
||||
panel_log_debug(pl, "[shapes] overlay shader id=%d valid=%d", p->overlay_shader.id, sg_isvalid());
|
||||
if (p->overlay_shader.id == SG_INVALID_ID)
|
||||
panel_log(pl, 1, "[shapes] FAILED to create overlay shader");
|
||||
|
||||
overlay_pipeline = sg_make_pipeline(&(sg_pipeline_desc) {
|
||||
.shader = overlay_shader,
|
||||
p->overlay_pipeline = sg_make_pipeline(&(sg_pipeline_desc) {
|
||||
.shader = p->overlay_shader,
|
||||
.index_type = SG_INDEXTYPE_UINT16,
|
||||
.primitive_type = SG_PRIMITIVETYPE_LINE_STRIP,
|
||||
.layout.attrs = {
|
||||
@@ -54,10 +62,12 @@ static void shape_init_pipeline(void)
|
||||
},
|
||||
.label = "Overlay pipeline",
|
||||
});
|
||||
panel_log(3, "[shapes] overlay pipeline id=%d valid=%d", overlay_pipeline.id, sg_isvalid());
|
||||
panel_log_debug(pl, "[shapes] overlay pipeline id=%d valid=%d", p->overlay_pipeline.id, sg_isvalid());
|
||||
if (p->overlay_pipeline.id == SG_INVALID_ID)
|
||||
panel_log(pl, 1, "[shapes] FAILED to create overlay pipeline");
|
||||
|
||||
// Shape shader/pipeline (storage buffers, instanced)
|
||||
shape_shader = sg_make_shader(&(sg_shader_desc) {
|
||||
p->shape_shader = sg_make_shader(&(sg_shader_desc) {
|
||||
.vertex_func = {
|
||||
.source = (const char*) src_shaders_shape_wgsl,
|
||||
.entry = "vs_main",
|
||||
@@ -94,10 +104,12 @@ static void shape_init_pipeline(void)
|
||||
},
|
||||
.label = "Shape shader",
|
||||
});
|
||||
panel_log(3, "[shapes] shader id=%d valid=%d", shape_shader.id, sg_isvalid());
|
||||
panel_log_debug(pl, "[shapes] shader id=%d valid=%d", p->shape_shader.id, sg_isvalid());
|
||||
if (p->shape_shader.id == SG_INVALID_ID)
|
||||
panel_log(pl, 1, "[shapes] FAILED to create shape shader");
|
||||
|
||||
shape_pipeline = sg_make_pipeline(&(sg_pipeline_desc) {
|
||||
.shader = shape_shader,
|
||||
p->shape_pipeline = sg_make_pipeline(&(sg_pipeline_desc) {
|
||||
.shader = p->shape_shader,
|
||||
.index_type = SG_INDEXTYPE_NONE,
|
||||
.primitive_type = SG_PRIMITIVETYPE_LINE_STRIP,
|
||||
.layout.attrs = {
|
||||
@@ -105,15 +117,17 @@ static void shape_init_pipeline(void)
|
||||
},
|
||||
.label = "Shape pipeline",
|
||||
});
|
||||
panel_log(3, "[shapes] pipeline id=%d valid=%d", shape_pipeline.id, sg_isvalid());
|
||||
panel_log_debug(pl, "[shapes] pipeline id=%d valid=%d", p->shape_pipeline.id, sg_isvalid());
|
||||
if (p->shape_pipeline.id == SG_INVALID_ID)
|
||||
panel_log(pl, 1, "[shapes] FAILED to create shape pipeline");
|
||||
}
|
||||
|
||||
static void shape_shutdown_pipeline(void)
|
||||
static void shape_shutdown_pipeline(pipeline_ctx_t *p)
|
||||
{
|
||||
sg_destroy_pipeline(shape_pipeline);
|
||||
sg_destroy_shader(shape_shader);
|
||||
sg_destroy_pipeline(overlay_pipeline);
|
||||
sg_destroy_shader(overlay_shader);
|
||||
sg_destroy_pipeline(p->shape_pipeline);
|
||||
sg_destroy_shader(p->shape_shader);
|
||||
sg_destroy_pipeline(p->overlay_pipeline);
|
||||
sg_destroy_shader(p->overlay_shader);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
1005
src/shape.h
1005
src/shape.h
File diff suppressed because it is too large
Load Diff
339
src/spatial.h
339
src/spatial.h
@@ -3,11 +3,10 @@
|
||||
|
||||
#include "api.h"
|
||||
|
||||
// Tunable constants
|
||||
#define SPATIAL_CELL_SIZE 250.0f
|
||||
#define SPATIAL_HASH_BITS 8
|
||||
#define SPATIAL_HASH_BITS 16
|
||||
#define SPATIAL_HASH_SIZE (1 << SPATIAL_HASH_BITS)
|
||||
#define SPATIAL_QUERY_RANGE 1
|
||||
#define SPATIAL_MAX_CELLS_PER_SHAPE 64
|
||||
|
||||
typedef struct {
|
||||
int shape_idx;
|
||||
@@ -24,6 +23,9 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
spatial_slot_t slots[SPATIAL_HASH_SIZE];
|
||||
int *visited; // per-shape query-dedup frame tags
|
||||
int visited_cap;
|
||||
int query_frame; // increments per query, never 0
|
||||
bool dirty;
|
||||
} spatial_grid_t;
|
||||
|
||||
@@ -32,23 +34,11 @@ static int spatial_hash(int cx, int cy)
|
||||
return (cx * 73856093) ^ (cy * 19349663);
|
||||
}
|
||||
|
||||
static void spatial_compute_aabb(shape_t *s, float *min_x, float *min_y,
|
||||
float *max_x, float *max_y)
|
||||
{
|
||||
float cos_r = s->cos_r;
|
||||
float sin_r = s->sin_r;
|
||||
float hx = fabsf(cos_r) * s->sx + fabsf(sin_r) * s->sy;
|
||||
float hy = fabsf(sin_r) * s->sx + fabsf(cos_r) * s->sy;
|
||||
*min_x = s->cx - hx;
|
||||
*min_y = s->cy - hy;
|
||||
*max_x = s->cx + hx;
|
||||
*max_y = s->cy + hy;
|
||||
}
|
||||
|
||||
static void spatial_init(spatial_grid_t *grid)
|
||||
{
|
||||
memset(grid, 0, sizeof(*grid));
|
||||
grid->dirty = true;
|
||||
grid->query_frame = 1;
|
||||
}
|
||||
|
||||
static void spatial_mark_dirty(spatial_grid_t *grid)
|
||||
@@ -61,9 +51,58 @@ static void spatial_destroy(spatial_grid_t *grid)
|
||||
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
|
||||
if (grid->slots[i].entries) FREE(grid->slots[i].entries);
|
||||
}
|
||||
if (grid->visited) FREE(grid->visited);
|
||||
memset(grid, 0, sizeof(*grid));
|
||||
}
|
||||
|
||||
// Find or create the slot for cell (cx, cy). Returns the slot index.
|
||||
static int spatial_find_slot(spatial_grid_t *grid, int cx, int cy)
|
||||
{
|
||||
int idx = spatial_hash(cx, cy) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe = 0;
|
||||
while (grid->slots[idx].occupied && probe < SPATIAL_HASH_SIZE) {
|
||||
if (grid->slots[idx].cx == cx && grid->slots[idx].cy == cy) break;
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
probe++;
|
||||
}
|
||||
if (!grid->slots[idx].occupied) {
|
||||
grid->slots[idx].occupied = true;
|
||||
grid->slots[idx].cx = cx;
|
||||
grid->slots[idx].cy = cy;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Enumerate the cells a shape's AABB overlaps. Returns the number of cells.
|
||||
// Capped at SPATIAL_MAX_CELLS_PER_SHAPE; falls back to the center cell for
|
||||
// degenerate huge shapes to avoid hash-table explosion.
|
||||
static int spatial_shape_cells(shape_t *s, int *cell_xs, int *cell_ys)
|
||||
{
|
||||
int cmin_x = (int)floorf((s->cx - s->aabb_hx) / SPATIAL_CELL_SIZE);
|
||||
int cmax_x = (int)floorf((s->cx + s->aabb_hx) / SPATIAL_CELL_SIZE);
|
||||
int cmin_y = (int)floorf((s->cy - s->aabb_hy) / SPATIAL_CELL_SIZE);
|
||||
int cmax_y = (int)floorf((s->cy + s->aabb_hy) / SPATIAL_CELL_SIZE);
|
||||
|
||||
int ncx = cmax_x - cmin_x + 1;
|
||||
int ncy = cmax_y - cmin_y + 1;
|
||||
|
||||
if (ncx <= 0 || ncy <= 0 || ncx * ncy > SPATIAL_MAX_CELLS_PER_SHAPE) {
|
||||
cell_xs[0] = (int)floorf(s->cx / SPATIAL_CELL_SIZE);
|
||||
cell_ys[0] = (int)floorf(s->cy / SPATIAL_CELL_SIZE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (int cy = cmin_y; cy <= cmax_y; cy++) {
|
||||
for (int cx = cmin_x; cx <= cmax_x; cx++) {
|
||||
cell_xs[count] = cx;
|
||||
cell_ys[count] = cy;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void spatial_rebuild(spatial_grid_t *grid, vector_t *shapes)
|
||||
{
|
||||
if (!grid->dirty) return;
|
||||
@@ -79,142 +118,224 @@ static void spatial_rebuild(spatial_grid_t *grid, vector_t *shapes)
|
||||
|
||||
if (n == 0) return;
|
||||
|
||||
// Grow visited array if needed (used for query-result dedup)
|
||||
if (n > grid->visited_cap) {
|
||||
if (grid->visited) FREE(grid->visited);
|
||||
grid->visited = (int*)ALLOC((size_t)n * sizeof(int));
|
||||
grid->visited_cap = n;
|
||||
}
|
||||
memset(grid->visited, 0, (size_t)n * sizeof(int));
|
||||
grid->query_frame = 1;
|
||||
|
||||
int cell_xs[128], cell_ys[128];
|
||||
|
||||
// Phase 1: count shapes per cell
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(shapes, i);
|
||||
int ccx = (int) floorf(s->cx / SPATIAL_CELL_SIZE);
|
||||
int ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE);
|
||||
|
||||
int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe = 0;
|
||||
while (grid->slots[idx].occupied && probe < SPATIAL_HASH_SIZE) {
|
||||
if (grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy) break;
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
probe++;
|
||||
shape_t *s = (shape_t*)vec_get(shapes, i);
|
||||
int nc = spatial_shape_cells(s, cell_xs, cell_ys);
|
||||
for (int c = 0; c < nc; c++) {
|
||||
int idx = spatial_find_slot(grid, cell_xs[c], cell_ys[c]);
|
||||
grid->slots[idx].count++;
|
||||
}
|
||||
|
||||
if (!grid->slots[idx].occupied) {
|
||||
grid->slots[idx].occupied = true;
|
||||
grid->slots[idx].cx = ccx;
|
||||
grid->slots[idx].cy = ccy;
|
||||
}
|
||||
grid->slots[idx].count++;
|
||||
}
|
||||
|
||||
// Phase 2: allocate entry arrays based on count
|
||||
// Phase 2: resize entry arrays when needed
|
||||
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
|
||||
if (!grid->slots[i].occupied) continue;
|
||||
if (grid->slots[i].count > grid->slots[i].capacity) {
|
||||
int need = grid->slots[i].count;
|
||||
if (need > grid->slots[i].capacity) {
|
||||
if (grid->slots[i].entries) FREE(grid->slots[i].entries);
|
||||
grid->slots[i].entries = (spatial_entry_t*) ALLOC(
|
||||
(size_t) grid->slots[i].count * sizeof(spatial_entry_t));
|
||||
grid->slots[i].capacity = grid->slots[i].count;
|
||||
grid->slots[i].entries = (spatial_entry_t*)ALLOC(
|
||||
(size_t)need * sizeof(spatial_entry_t));
|
||||
grid->slots[i].capacity = need;
|
||||
}
|
||||
grid->slots[i].count = 0; // reset for fill phase
|
||||
grid->slots[i].count = 0;
|
||||
}
|
||||
|
||||
// Phase 3: fill entries
|
||||
// Phase 3: fill entries — each shape is added to every cell its AABB overlaps
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(shapes, i);
|
||||
int ccx = (int) floorf(s->cx / SPATIAL_CELL_SIZE);
|
||||
int ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE);
|
||||
shape_t *s = (shape_t*)vec_get(shapes, i);
|
||||
float min_x = s->cx - s->aabb_hx;
|
||||
float min_y = s->cy - s->aabb_hy;
|
||||
float max_x = s->cx + s->aabb_hx;
|
||||
float max_y = s->cy + s->aabb_hy;
|
||||
|
||||
int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe = 0;
|
||||
while (!(grid->slots[idx].occupied &&
|
||||
grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy) &&
|
||||
probe < SPATIAL_HASH_SIZE) {
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
probe++;
|
||||
int nc = spatial_shape_cells(s, cell_xs, cell_ys);
|
||||
for (int c = 0; c < nc; c++) {
|
||||
int idx = spatial_find_slot(grid, cell_xs[c], cell_ys[c]);
|
||||
spatial_entry_t *e = &grid->slots[idx].entries[grid->slots[idx].count++];
|
||||
e->shape_idx = i;
|
||||
e->min_x = min_x; e->min_y = min_y;
|
||||
e->max_x = max_x; e->max_y = max_y;
|
||||
}
|
||||
|
||||
spatial_entry_t *e = &grid->slots[idx].entries[grid->slots[idx].count++];
|
||||
e->shape_idx = i;
|
||||
spatial_compute_aabb(s, &e->min_x, &e->min_y, &e->max_x, &e->max_y);
|
||||
}
|
||||
}
|
||||
|
||||
// Point query — O(1) average. Only checks the cell containing the query point
|
||||
// because shapes are now inserted into every cell their AABB overlaps.
|
||||
static int spatial_query_point(spatial_grid_t *grid, vector_t *shapes,
|
||||
float wx, float wy, float world_tol)
|
||||
{
|
||||
int ccx = (int) floorf(wx / SPATIAL_CELL_SIZE);
|
||||
int ccy = (int) floorf(wy / SPATIAL_CELL_SIZE);
|
||||
int cx = (int)floorf(wx / SPATIAL_CELL_SIZE);
|
||||
int cy = (int)floorf(wy / SPATIAL_CELL_SIZE);
|
||||
|
||||
for (int dz = -SPATIAL_QUERY_RANGE; dz <= SPATIAL_QUERY_RANGE; dz++) {
|
||||
for (int dw = -SPATIAL_QUERY_RANGE; dw <= SPATIAL_QUERY_RANGE; dw++) {
|
||||
int cell_x = ccx + dz;
|
||||
int cell_y = ccy + dw;
|
||||
int idx = spatial_hash(cx, cy) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe_start = idx;
|
||||
|
||||
int idx = spatial_hash(cell_x, cell_y) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe_start = idx;
|
||||
do {
|
||||
if (!grid->slots[idx].occupied) break;
|
||||
|
||||
do {
|
||||
if (!grid->slots[idx].occupied) break;
|
||||
if (grid->slots[idx].cx == cx && grid->slots[idx].cy == cy) {
|
||||
for (int e = 0; e < grid->slots[idx].count; e++) {
|
||||
spatial_entry_t *entry = &grid->slots[idx].entries[e];
|
||||
|
||||
if (grid->slots[idx].cx == cell_x && grid->slots[idx].cy == cell_y) {
|
||||
for (int e = 0; e < grid->slots[idx].count; e++) {
|
||||
spatial_entry_t *entry = &grid->slots[idx].entries[e];
|
||||
if (wx < entry->min_x - world_tol ||
|
||||
wx > entry->max_x + world_tol ||
|
||||
wy < entry->min_y - world_tol ||
|
||||
wy > entry->max_y + world_tol)
|
||||
continue;
|
||||
|
||||
if (wx < entry->min_x - world_tol ||
|
||||
wx > entry->max_x + world_tol ||
|
||||
wy < entry->min_y - world_tol ||
|
||||
wy > entry->max_y + world_tol)
|
||||
continue;
|
||||
|
||||
shape_t *s = (shape_t*) vec_get(shapes, entry->shape_idx);
|
||||
if (shape_hit_test(s, wx, wy, world_tol))
|
||||
return entry->shape_idx;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
} while (idx != probe_start);
|
||||
shape_t *s = (shape_t*)vec_get(shapes, entry->shape_idx);
|
||||
if (shape_hit_test(s, wx, wy, world_tol))
|
||||
return entry->shape_idx;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
} while (idx != probe_start);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Rectangle selection for marquee. Uses a per-query frame counter for dedup
|
||||
// since shapes now appear in every cell they overlap.
|
||||
static int spatial_query_rect_select(spatial_grid_t *grid, vector_t *shapes,
|
||||
float min_x, float min_y,
|
||||
float max_x, float max_y)
|
||||
{
|
||||
for (int i = 0; i < shapes->count; i++) {
|
||||
((shape_t*) vec_get(shapes, i))->selected = false;
|
||||
int n = shapes->count;
|
||||
for (int i = 0; i < n; i++) {
|
||||
((shape_t*)vec_get(shapes, i))->selected = false;
|
||||
}
|
||||
int selected_count = 0;
|
||||
|
||||
for (int s = 0; s < SPATIAL_HASH_SIZE; s++) {
|
||||
if (!grid->slots[s].occupied) continue;
|
||||
for (int e = 0; e < grid->slots[s].count; e++) {
|
||||
spatial_entry_t *entry = &grid->slots[s].entries[e];
|
||||
grid->query_frame++;
|
||||
int frame = grid->query_frame;
|
||||
|
||||
if (entry->max_x < min_x || entry->min_x > max_x ||
|
||||
entry->max_y < min_y || entry->min_y > max_y)
|
||||
continue;
|
||||
int cell_min_x = (int)floorf(min_x / SPATIAL_CELL_SIZE);
|
||||
int cell_max_x = (int)floorf(max_x / SPATIAL_CELL_SIZE);
|
||||
int cell_min_y = (int)floorf(min_y / SPATIAL_CELL_SIZE);
|
||||
int cell_max_y = (int)floorf(max_y / SPATIAL_CELL_SIZE);
|
||||
|
||||
shape_t *shape = (shape_t*) vec_get(shapes, entry->shape_idx);
|
||||
if (shape->selected) continue;
|
||||
int cell_count = (cell_max_x - cell_min_x + 1) * (cell_max_y - cell_min_y + 1);
|
||||
if (cell_count > SPATIAL_HASH_SIZE) {
|
||||
for (int s = 0; s < SPATIAL_HASH_SIZE; s++) {
|
||||
if (!grid->slots[s].occupied) continue;
|
||||
for (int e = 0; e < grid->slots[s].count; e++) {
|
||||
spatial_entry_t *entry = &grid->slots[s].entries[e];
|
||||
if (grid->visited[entry->shape_idx] == frame) continue;
|
||||
grid->visited[entry->shape_idx] = frame;
|
||||
|
||||
bool hit = (shape->cx >= min_x && shape->cx <= max_x &&
|
||||
shape->cy >= min_y && shape->cy <= max_y);
|
||||
float sc = shape->cos_r, ss = shape->sin_r;
|
||||
for (uint32_t v = 0; !hit && v < shape->num_verts; v++) {
|
||||
float lx = shape->verts[v].x * shape->sx;
|
||||
float ly = shape->verts[v].y * shape->sy;
|
||||
float wx = shape->cx + lx * sc - ly * ss;
|
||||
float wy = shape->cy + lx * ss + ly * sc;
|
||||
if (wx >= min_x && wx <= max_x &&
|
||||
wy >= min_y && wy <= max_y)
|
||||
hit = true;
|
||||
}
|
||||
if (hit) {
|
||||
shape->selected = true;
|
||||
selected_count++;
|
||||
if (entry->max_x < min_x || entry->min_x > max_x ||
|
||||
entry->max_y < min_y || entry->min_y > max_y)
|
||||
continue;
|
||||
shape_t *shape = (shape_t*)vec_get(shapes, entry->shape_idx);
|
||||
bool hit = (shape->cx >= min_x && shape->cx <= max_x &&
|
||||
shape->cy >= min_y && shape->cy <= max_y);
|
||||
float sx_cos = shape->sx * shape->cos_r;
|
||||
float sy_sin = shape->sy * shape->sin_r;
|
||||
float sx_sin = shape->sx * shape->sin_r;
|
||||
float sy_cos = shape->sy * shape->cos_r;
|
||||
for (uint32_t v = 0; !hit && v < shape->num_verts; v++) {
|
||||
float wx = shape->cx + shape->verts[v].x * sx_cos - shape->verts[v].y * sy_sin;
|
||||
float wy = shape->cy + shape->verts[v].x * sx_sin + shape->verts[v].y * sy_cos;
|
||||
if (wx >= min_x && wx <= max_x &&
|
||||
wy >= min_y && wy <= max_y)
|
||||
hit = true;
|
||||
}
|
||||
if (hit) { shape->selected = true; selected_count++; }
|
||||
}
|
||||
}
|
||||
return selected_count;
|
||||
}
|
||||
|
||||
for (int cy = cell_min_y; cy <= cell_max_y; cy++) {
|
||||
for (int cx = cell_min_x; cx <= cell_max_x; cx++) {
|
||||
int idx = spatial_hash(cx, cy) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe_start = idx;
|
||||
do {
|
||||
if (!grid->slots[idx].occupied) break;
|
||||
if (grid->slots[idx].cx == cx && grid->slots[idx].cy == cy) {
|
||||
for (int e = 0; e < grid->slots[idx].count; e++) {
|
||||
spatial_entry_t *entry = &grid->slots[idx].entries[e];
|
||||
if (grid->visited[entry->shape_idx] == frame) continue;
|
||||
grid->visited[entry->shape_idx] = frame;
|
||||
|
||||
if (entry->max_x < min_x || entry->min_x > max_x ||
|
||||
entry->max_y < min_y || entry->min_y > max_y)
|
||||
continue;
|
||||
shape_t *shape = (shape_t*)vec_get(shapes, entry->shape_idx);
|
||||
bool hit = (shape->cx >= min_x && shape->cx <= max_x &&
|
||||
shape->cy >= min_y && shape->cy <= max_y);
|
||||
float sc = shape->cos_r, ss = shape->sin_r;
|
||||
for (uint32_t v = 0; !hit && v < shape->num_verts; v++) {
|
||||
float lx = shape->verts[v].x * shape->sx;
|
||||
float ly = shape->verts[v].y * shape->sy;
|
||||
float wx = shape->cx + lx * sc - ly * ss;
|
||||
float wy = shape->cy + lx * ss + ly * sc;
|
||||
if (wx >= min_x && wx <= max_x &&
|
||||
wy >= min_y && wy <= max_y)
|
||||
hit = true;
|
||||
}
|
||||
if (hit) { shape->selected = true; selected_count++; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
} while (idx != probe_start);
|
||||
}
|
||||
}
|
||||
return selected_count;
|
||||
}
|
||||
|
||||
static int spatial_query_viewport(spatial_grid_t *grid,
|
||||
float min_x, float min_y, float max_x, float max_y,
|
||||
int *out_indices, int max_out)
|
||||
{
|
||||
grid->query_frame++;
|
||||
int frame = grid->query_frame;
|
||||
|
||||
int cell_min_x = (int)floorf(min_x / SPATIAL_CELL_SIZE);
|
||||
int cell_max_x = (int)floorf(max_x / SPATIAL_CELL_SIZE);
|
||||
int cell_min_y = (int)floorf(min_y / SPATIAL_CELL_SIZE);
|
||||
int cell_max_y = (int)floorf(max_y / SPATIAL_CELL_SIZE);
|
||||
|
||||
int count = 0;
|
||||
for (int cy = cell_min_y; cy <= cell_max_y && count < max_out; cy++) {
|
||||
for (int cx = cell_min_x; cx <= cell_max_x && count < max_out; cx++) {
|
||||
int idx = spatial_hash(cx, cy) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe_start = idx;
|
||||
do {
|
||||
if (!grid->slots[idx].occupied) break;
|
||||
if (grid->slots[idx].cx == cx && grid->slots[idx].cy == cy) {
|
||||
for (int e = 0; e < grid->slots[idx].count && count < max_out; e++) {
|
||||
spatial_entry_t *entry = &grid->slots[idx].entries[e];
|
||||
if (grid->visited[entry->shape_idx] == frame) continue;
|
||||
grid->visited[entry->shape_idx] = frame;
|
||||
|
||||
if (entry->max_x < min_x || entry->min_x > max_x ||
|
||||
entry->max_y < min_y || entry->min_y > max_y)
|
||||
continue;
|
||||
out_indices[count++] = entry->shape_idx;
|
||||
}
|
||||
break;
|
||||
}
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
} while (idx != probe_start);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
70
src/types.h
70
src/types.h
@@ -3,12 +3,23 @@
|
||||
|
||||
#include "api.h"
|
||||
|
||||
#define LOG_RING_SIZE 64
|
||||
#define LOG_RING_SIZE 256
|
||||
#define HANDLE_OFFSET_PX 0.0f
|
||||
#define HANDLE_RADIUS_PX 12.0f
|
||||
#define HANDLE_CIRCLE_SEGMENTS 256
|
||||
#define HANDLE_CIRCLE_SEGMENTS 64
|
||||
#define CORNER_SIZE_PX 8.0f
|
||||
#define TOP_PANEL_H 32.0f
|
||||
#define PEN_MAX_CONTROL_POINTS 256
|
||||
#define PEN_PREVIEW_MAX_VERTS 2048
|
||||
#define PEN_CLOSE_PX 10.0f
|
||||
#define EDIT_ANCHOR_SIZE_PX 8.0f
|
||||
#define EDIT_HANDLE_SIZE_PX 5.0f
|
||||
|
||||
#define DOUBLE_CLICK_TIME 0.3
|
||||
#define DRAG_THRESHOLD_SQ 9.0f
|
||||
#define FRUSTUM_CULL_MARGIN 300.0f
|
||||
#define CAMERA_ZOOM_MIN 0.1f
|
||||
#define CAMERA_ZOOM_MAX 6.0f
|
||||
|
||||
typedef enum {
|
||||
TOOL_SELECT,
|
||||
@@ -21,6 +32,7 @@ typedef enum {
|
||||
typedef struct log_entry_t {
|
||||
char text[256];
|
||||
uint32_t level;
|
||||
uint64_t hash;
|
||||
} log_entry_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -86,11 +98,23 @@ typedef struct {
|
||||
float cached_aabb[4];
|
||||
bool aabb_cached;
|
||||
|
||||
int focused_group_id;
|
||||
double last_click_time;
|
||||
int last_click_shape_idx;
|
||||
|
||||
vector_t drag_indices;
|
||||
|
||||
// Edit mode
|
||||
int editing_shape_idx;
|
||||
bool edit_dragging;
|
||||
int edit_drag_idx;
|
||||
bool edit_handle_dragging;
|
||||
int edit_handle_idx;
|
||||
bool edit_handle_is_in;
|
||||
// Pre-drag control point snapshot (for undo)
|
||||
shape_vertex_t *edit_saved_ctrl;
|
||||
shape_vertex_t *edit_saved_hin;
|
||||
shape_vertex_t *edit_saved_hout;
|
||||
int edit_saved_count;
|
||||
} interact_state_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -109,36 +133,70 @@ typedef struct {
|
||||
int log_head;
|
||||
int log_count;
|
||||
bool log_show;
|
||||
char log_filter[32];
|
||||
tool_t active_tool;
|
||||
int list_last_shape;
|
||||
int list_prev_count;
|
||||
int *display_cache;
|
||||
int display_cache_len;
|
||||
bool display_cache_dirty;
|
||||
} ui_state_t;
|
||||
|
||||
typedef struct {
|
||||
shape_t *shapes;
|
||||
int shape_count;
|
||||
group_t *groups;
|
||||
int group_count;
|
||||
} clipboard_t;
|
||||
|
||||
typedef struct {
|
||||
bool drawing;
|
||||
shape_vertex_t points[PEN_MAX_CONTROL_POINTS];
|
||||
int point_count;
|
||||
shape_vertex_t preview_verts[PEN_PREVIEW_MAX_VERTS];
|
||||
int preview_count;
|
||||
} pen_state_t;
|
||||
|
||||
// Per-overlay-buffer upload flags — replaces the single overlay_upload_needed
|
||||
// bool so that during drag we only upload the buffers that actually changed
|
||||
// (e.g. moving shapes only needs rect+corners, not edit-mode buffers).
|
||||
typedef struct {
|
||||
bool rect;
|
||||
bool handle_circle;
|
||||
bool corners;
|
||||
bool edit_anchors;
|
||||
bool edit_handles;
|
||||
bool edit_lines;
|
||||
bool pen;
|
||||
} overlay_upload_flags_t;
|
||||
|
||||
typedef struct userdata_t {
|
||||
camera_t camera;
|
||||
renderer_t renderer;
|
||||
shape_pool_ctx_t shape_pool;
|
||||
group_index_ctx_t group_idx;
|
||||
pipeline_ctx_t pipelines;
|
||||
panel_log_ctx_t panel_log_ctx;
|
||||
rand_ctx_t rand_ctx;
|
||||
vector_t shapes;
|
||||
spatial_grid_t spatial_grid;
|
||||
interact_state_t interact;
|
||||
history_t history;
|
||||
debug_stats_t debug;
|
||||
ui_state_t ui;
|
||||
bool overlay_upload_needed;
|
||||
overlay_upload_flags_t overlay_upload;
|
||||
sg_buffer rect_vbuf, rect_ibuf;
|
||||
sg_buffer handle_vbuf, handle_ibuf;
|
||||
sg_buffer corner_vbuf, corner_ibuf;
|
||||
int next_group_id;
|
||||
vector_t groups;
|
||||
clipboard_t clipboard;
|
||||
float map_w, map_h;
|
||||
float mouse_x, mouse_y;
|
||||
double time;
|
||||
pen_state_t pen;
|
||||
sg_buffer pen_vbuf, pen_ibuf;
|
||||
// Edit mode buffers
|
||||
sg_buffer ed_anchor_vbuf, ed_handle_vbuf, ed_handle_line_vbuf, ed_shared_ibuf;
|
||||
int ed_anchor_count, ed_handle_count, ed_handle_line_count;
|
||||
} userdata_t;
|
||||
|
||||
#endif
|
||||
|
||||
302
src/ui_panels.h
302
src/ui_panels.h
@@ -5,13 +5,9 @@
|
||||
#include "types.h"
|
||||
#include "interact.h"
|
||||
|
||||
static const char *shape_kind_label(int kind) {
|
||||
switch (kind) {
|
||||
case SHAPE_CIRCLE: return "Circle";
|
||||
case SHAPE_RECTANGLE: return "Rect";
|
||||
case SHAPE_STAR: return "Star";
|
||||
default: return "Shape";
|
||||
}
|
||||
static const char *shape_kind_label(const char *name) {
|
||||
if (name[0]) return name;
|
||||
return "Shape";
|
||||
}
|
||||
|
||||
static void build_display_recursive(vector_t *shapes, vector_t *groups, int parent_gid, int *display, int *dlen)
|
||||
@@ -19,10 +15,8 @@ static void build_display_recursive(vector_t *shapes, vector_t *groups, int pare
|
||||
for (int g = 0; g < groups->count; g++) {
|
||||
group_t *grp = (group_t*) vec_get(groups, g);
|
||||
if (grp->parent_id != parent_gid) continue;
|
||||
for (int i = 0; i < shapes->count; i++) {
|
||||
if (((shape_t*) vec_get(shapes, i))->group_id == grp->id)
|
||||
display[(*dlen)++] = i;
|
||||
}
|
||||
for (int m = 0; m < grp->member_count; m++)
|
||||
display[(*dlen)++] = grp->member_indices[m];
|
||||
build_display_recursive(shapes, groups, grp->id, display, dlen);
|
||||
}
|
||||
if (parent_gid == 0) {
|
||||
@@ -33,13 +27,28 @@ static void build_display_recursive(vector_t *shapes, vector_t *groups, int pare
|
||||
}
|
||||
}
|
||||
|
||||
static int count_shapes_in_subtree(vector_t *shapes, vector_t *groups, int gid)
|
||||
// Count shapes and group headers in a subtree (for collapsed path).
|
||||
static void count_subtree_items(vector_t *groups, int gid, int *shapes_out, int *headers_out)
|
||||
{
|
||||
int c = 0;
|
||||
for (int i = 0; i < shapes->count; i++)
|
||||
if (is_shape_in_group_hierarchy(((shape_t*) vec_get(shapes, i))->group_id, gid, groups))
|
||||
c++;
|
||||
return c;
|
||||
int s = 0, h = 0;
|
||||
for (int g = 0; g < groups->count; g++) {
|
||||
group_t *grp = (group_t*) vec_get(groups, g);
|
||||
if (grp->id == gid) {
|
||||
s = grp->member_count;
|
||||
for (int k = 0; k < groups->count; k++) {
|
||||
group_t *child = (group_t*) vec_get(groups, k);
|
||||
if (child->parent_id == gid) {
|
||||
h += 1;
|
||||
int cs, ch;
|
||||
count_subtree_items(groups, child->id, &cs, &ch);
|
||||
s += cs; h += ch;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
*shapes_out = s;
|
||||
*headers_out = h;
|
||||
}
|
||||
|
||||
static void list_shape_clicked(userdata_t *ud, shape_t *s, int *display, int display_len, int display_pos)
|
||||
@@ -67,95 +76,144 @@ static void list_shape_clicked(userdata_t *ud, shape_t *s, int *display, int dis
|
||||
for (int j = 0; j < n; j++)
|
||||
((shape_t*) vec_get(&ud->shapes, j))->selected = false;
|
||||
ud->interact.selected_count = 0;
|
||||
if (s->group_id != 0) {
|
||||
int topmost = get_topmost_group(&ud->groups, s->group_id);
|
||||
for (int j = 0; j < n; j++) {
|
||||
shape_t *sj = (shape_t*) vec_get(&ud->shapes, j);
|
||||
if (is_shape_in_group_hierarchy(sj->group_id, topmost, &ud->groups)) {
|
||||
sj->selected = true;
|
||||
ud->interact.selected_count++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s->selected = true;
|
||||
ud->interact.selected_count = 1;
|
||||
}
|
||||
s->selected = true;
|
||||
ud->interact.selected_count = 1;
|
||||
}
|
||||
ud->ui.list_last_shape = display_pos;
|
||||
ud->interact.aabb_cached = false;
|
||||
ud->overlay_upload_needed = true;
|
||||
overlay_invalidate(ud);
|
||||
update_shape_states(ud);
|
||||
}
|
||||
|
||||
static int render_tree_level(userdata_t *ud, int parent_gid, int *display, int display_len, int display_pos)
|
||||
// Count items that are currently visible (respecting collapse state).
|
||||
// Used to compute the exact scrollbar range.
|
||||
static int count_visible_items(vector_t *groups, int parent_gid)
|
||||
{
|
||||
int c = 0;
|
||||
for (int g = 0; g < groups->count; g++) {
|
||||
group_t *grp = (group_t*) vec_get(groups, g);
|
||||
if (grp->parent_id != parent_gid) continue;
|
||||
c += 1; // group header (always visible)
|
||||
if (!grp->collapsed) {
|
||||
c += grp->member_count;
|
||||
c += count_visible_items(groups, grp->id);
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// Render tree level with virtualized scrolling (ocornut's technique from imgui#3823).
|
||||
// `item_idx` tracks only VISIBLE items (collapsed subtrees don't contribute)
|
||||
// so that the scrollbar accurately reflects the viewable content. `pos` always
|
||||
// tracks the display-array position for shift-click range selection.
|
||||
static int render_tree_level(userdata_t *ud, int parent_gid, int *display, int display_len,
|
||||
int display_pos, int first_visible, int last_visible, int *item_idx)
|
||||
{
|
||||
int n = ud->shapes.count;
|
||||
int pos = display_pos;
|
||||
bool has_groups = (ud->groups.count > 0);
|
||||
|
||||
for (int g = 0; g < ud->groups.count; g++) {
|
||||
group_t *grp = (group_t*) vec_get(&ud->groups, g);
|
||||
if (grp->parent_id != parent_gid) continue;
|
||||
if (has_groups) {
|
||||
for (int g = 0; g < ud->groups.count; g++) {
|
||||
group_t *grp = (group_t*) vec_get(&ud->groups, g);
|
||||
if (grp->parent_id != parent_gid) continue;
|
||||
|
||||
int gid = grp->id;
|
||||
int gid = grp->id;
|
||||
bool visible = (*item_idx >= first_visible && *item_idx < last_visible);
|
||||
(*item_idx)++;
|
||||
|
||||
int member_count = 0, sel_count = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
if (s->group_id == gid) { member_count++; if (s->selected) sel_count++; }
|
||||
}
|
||||
if (visible) {
|
||||
// --- VISIBLE GROUP HEADER ---
|
||||
ImGuiID storage_id = (ImGuiID)gid;
|
||||
igSetNextItemStorageID(storage_id);
|
||||
|
||||
char hdr[128];
|
||||
snprintf(hdr, sizeof(hdr), "Group %d (%d)##g%d", gid, member_count, gid);
|
||||
char hdr[64];
|
||||
snprintf(hdr, sizeof(hdr), "Group##g%d", gid);
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow;
|
||||
if (!grp->collapsed) flags |= ImGuiTreeNodeFlags_DefaultOpen;
|
||||
bool open = igTreeNodeEx_Str(hdr, flags);
|
||||
grp->collapsed = !open;
|
||||
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_DefaultOpen;
|
||||
if (igIsItemClicked(ImGuiMouseButton_Left)) {
|
||||
bool ctrl = igGetIO_Nil()->KeyCtrl;
|
||||
if (ctrl)
|
||||
toggle_group_recursive(ud, gid);
|
||||
else
|
||||
deselect_and_select_group_recursive(ud, gid);
|
||||
ud->ui.list_last_shape = display_pos;
|
||||
ud->interact.aabb_cached = false;
|
||||
overlay_invalidate(ud);
|
||||
update_shape_states(ud);
|
||||
}
|
||||
|
||||
bool open = igTreeNodeEx_Str(hdr, flags);
|
||||
if (open) {
|
||||
for (int m = 0; m < grp->member_count; m++) {
|
||||
int si = grp->member_indices[m];
|
||||
bool child_visible = (*item_idx >= first_visible && *item_idx < last_visible);
|
||||
(*item_idx)++;
|
||||
|
||||
int group_first = pos;
|
||||
if (child_visible) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, si);
|
||||
char label[128];
|
||||
snprintf(label, sizeof(label), " %s##s%d", shape_kind_label(s->name), si);
|
||||
if (igSelectable_Bool(label, s->selected, ImGuiSelectableFlags_None, (ImVec2){0,0}))
|
||||
list_shape_clicked(ud, s, display, display_len, pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
pos = render_tree_level(ud, gid, display, display_len, pos,
|
||||
first_visible, last_visible, item_idx);
|
||||
igTreePop();
|
||||
}
|
||||
// Closed node: no TreePop — TreePushOverrideID was not called.
|
||||
// Also no item_idx advance for the hidden subtree — only the
|
||||
// header counts toward the visible-total. Still advance pos
|
||||
// for display-index accuracy.
|
||||
if (!open) {
|
||||
int cs, ch;
|
||||
count_subtree_items(&ud->groups, gid, &cs, &ch);
|
||||
pos += cs;
|
||||
// item_idx NOT advanced: collapsed items are not visible
|
||||
}
|
||||
} else {
|
||||
// --- CLIPPED (OFF-SCREEN) GROUP ---
|
||||
if (grp->collapsed) {
|
||||
int cs, ch;
|
||||
count_subtree_items(&ud->groups, gid, &cs, &ch);
|
||||
pos += cs;
|
||||
// item_idx already incremented for header; collapsed
|
||||
// subtree adds nothing (not visible).
|
||||
} else {
|
||||
// Open but clipped: TreePush walks subtree via recursion.
|
||||
*item_idx += grp->member_count;
|
||||
pos += grp->member_count;
|
||||
|
||||
if (igIsItemClicked(ImGuiMouseButton_Left)) {
|
||||
bool ctrl = igGetIO_Nil()->KeyCtrl;
|
||||
if (ctrl)
|
||||
toggle_group_recursive(ud, gid);
|
||||
else
|
||||
deselect_and_select_group_recursive(ud, gid);
|
||||
ud->ui.list_last_shape = group_first;
|
||||
ud->interact.aabb_cached = false;
|
||||
ud->overlay_upload_needed = true;
|
||||
update_shape_states(ud);
|
||||
}
|
||||
|
||||
if (open) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
if (s->group_id != gid) continue;
|
||||
|
||||
char label[128];
|
||||
snprintf(label, sizeof(label), " %s##s%d", shape_kind_label(s->kind), i);
|
||||
|
||||
if (igSelectable_Bool(label, s->selected, ImGuiSelectableFlags_None, (ImVec2){0,0}))
|
||||
list_shape_clicked(ud, s, display, display_len, pos);
|
||||
|
||||
pos++;
|
||||
char label[64];
|
||||
snprintf(label, sizeof(label), "Group##g%d", gid);
|
||||
igTreePush_Str(label);
|
||||
pos = render_tree_level(ud, gid, display, display_len, pos,
|
||||
first_visible, last_visible, item_idx);
|
||||
igTreePop();
|
||||
}
|
||||
}
|
||||
pos = render_tree_level(ud, gid, display, display_len, pos);
|
||||
igTreePop();
|
||||
} else {
|
||||
pos += count_shapes_in_subtree(&ud->shapes, &ud->groups, gid);
|
||||
}
|
||||
}
|
||||
|
||||
// Ungrouped shapes — only at the top level
|
||||
if (parent_gid == 0) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
|
||||
if (s->group_id != 0) continue;
|
||||
if (has_groups && s->group_id != 0) continue;
|
||||
|
||||
char label[128];
|
||||
snprintf(label, sizeof(label), "%s##s%d", shape_kind_label(s->kind), i);
|
||||
|
||||
if (igSelectable_Bool(label, s->selected, ImGuiSelectableFlags_None, (ImVec2){0,0}))
|
||||
list_shape_clicked(ud, s, display, display_len, pos);
|
||||
bool in_range = (*item_idx >= first_visible && *item_idx < last_visible);
|
||||
(*item_idx)++;
|
||||
|
||||
if (in_range) {
|
||||
char label[128];
|
||||
snprintf(label, sizeof(label), "%s##s%d", shape_kind_label(s->name), i);
|
||||
if (igSelectable_Bool(label, s->selected, ImGuiSelectableFlags_None, (ImVec2){0,0}))
|
||||
list_shape_clicked(ud, s, display, display_len, pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
@@ -194,7 +252,7 @@ static void draw_top_panel(userdata_t *ud)
|
||||
((shape_t*)vec_get(&ud->shapes, i))->selected = false;
|
||||
}
|
||||
ud->interact.selected_count = 0;
|
||||
ud->overlay_upload_needed = true;
|
||||
overlay_invalidate(ud);
|
||||
update_shape_states(ud);
|
||||
}
|
||||
ud->ui.active_tool = new_tool;
|
||||
@@ -206,34 +264,23 @@ static void draw_top_panel(userdata_t *ud)
|
||||
igSameLine(0.0f, 16.0f);
|
||||
|
||||
if (igButton("Undo", (ImVec2){0, 0})) {
|
||||
if (history_undo(&ud->history, &ud->shapes)) {
|
||||
rebuild_groups_from_shapes(&ud->groups, &ud->shapes);
|
||||
ud->interact.hovered_shape = -1;
|
||||
spatial_mark_dirty(&ud->spatial_grid);
|
||||
ud->interact.aabb_cached = false;
|
||||
ud->overlay_upload_needed = true;
|
||||
if (history_undo(&ud->history, &ud->shapes, &ud->shape_pool,
|
||||
&ud->groups, &ud->group_idx)) {
|
||||
group_rebuild_members(&ud->group_idx, &ud->groups, &ud->shapes);
|
||||
interact_structural_change(ud);
|
||||
}
|
||||
}
|
||||
|
||||
igSameLine(0.0f, 4.0f);
|
||||
|
||||
if (igButton("Redo", (ImVec2){0, 0})) {
|
||||
if (history_redo(&ud->history, &ud->shapes)) {
|
||||
rebuild_groups_from_shapes(&ud->groups, &ud->shapes);
|
||||
ud->interact.hovered_shape = -1;
|
||||
spatial_mark_dirty(&ud->spatial_grid);
|
||||
ud->interact.aabb_cached = false;
|
||||
ud->overlay_upload_needed = true;
|
||||
if (history_redo(&ud->history, &ud->shapes, &ud->shape_pool,
|
||||
&ud->groups, &ud->group_idx)) {
|
||||
group_rebuild_members(&ud->group_idx, &ud->groups, &ud->shapes);
|
||||
interact_structural_change(ud);
|
||||
}
|
||||
}
|
||||
|
||||
if (ud->interact.focused_group_id != 0) {
|
||||
igSameLine(0.0f, 16.0f);
|
||||
char flbl[64];
|
||||
snprintf(flbl, sizeof(flbl), "Focus: Group %d (Esc)", ud->interact.focused_group_id);
|
||||
igTextColored((ImVec4){0.3f, 1.0f, 0.3f, 1.0f}, "%s", flbl);
|
||||
}
|
||||
|
||||
igEnd();
|
||||
}
|
||||
|
||||
@@ -252,18 +299,49 @@ static void draw_shape_list_panel(userdata_t *ud)
|
||||
return;
|
||||
}
|
||||
|
||||
igBeginChild_Str("ListScroll", (ImVec2){0, 0}, false, ImGuiWindowFlags_None);
|
||||
|
||||
int *display = (int*) ALLOC((size_t)n * sizeof(int));
|
||||
int display_len = 0;
|
||||
build_display_recursive(&ud->shapes, &ud->groups, 0, display, &display_len);
|
||||
if (ud->ui.display_cache_dirty || ud->ui.display_cache_len != n) {
|
||||
FREE(ud->ui.display_cache);
|
||||
ud->ui.display_cache = (int*) ALLOC((size_t)n * sizeof(int));
|
||||
ud->ui.display_cache_len = 0;
|
||||
build_display_recursive(&ud->shapes, &ud->groups, 0, ud->ui.display_cache, &ud->ui.display_cache_len);
|
||||
ud->ui.display_cache_dirty = false;
|
||||
}
|
||||
int *display = ud->ui.display_cache;
|
||||
int display_len = ud->ui.display_cache_len;
|
||||
|
||||
if (n != ud->ui.list_prev_count) { ud->ui.list_last_shape = -1; ud->ui.list_prev_count = n; }
|
||||
if (ud->ui.list_last_shape >= display_len) ud->ui.list_last_shape = -1;
|
||||
|
||||
render_tree_level(ud, 0, display, display_len, 0);
|
||||
// Count only visible items (respects collapse state) for correct scrollbar.
|
||||
int total_items = count_visible_items(&ud->groups, 0);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (((shape_t*) vec_get(&ud->shapes, i))->group_id == 0)
|
||||
total_items++;
|
||||
}
|
||||
|
||||
igBeginChild_Str("ListScroll", (ImVec2){0, 0}, false, ImGuiWindowFlags_None);
|
||||
|
||||
float line_h = igGetTextLineHeightWithSpacing();
|
||||
float scroll_y = igGetScrollY();
|
||||
int first_visible = (int)(scroll_y / line_h);
|
||||
if (first_visible < 0) first_visible = 0;
|
||||
int visible_slack = (int)(igGetWindowHeight() / line_h) + 4;
|
||||
int last_visible = first_visible + visible_slack;
|
||||
if (last_visible > total_items) last_visible = total_items;
|
||||
if (first_visible > last_visible) first_visible = last_visible;
|
||||
|
||||
// Position cursor at first visible item, render, then set total content height.
|
||||
float cursor_base = igGetCursorPosY();
|
||||
igSetCursorPosY(cursor_base + first_visible * line_h);
|
||||
|
||||
int item_idx = 0;
|
||||
render_tree_level(ud, 0, display, display_len, 0,
|
||||
first_visible, last_visible, &item_idx);
|
||||
|
||||
// Stretch content height so the scrollbar reflects the total visible items.
|
||||
igSetCursorPosY(cursor_base + total_items * line_h);
|
||||
igDummy((ImVec2){0, 0});
|
||||
|
||||
FREE(display);
|
||||
igEndChild();
|
||||
igEnd();
|
||||
}
|
||||
@@ -314,7 +392,7 @@ static void draw_properties_panel(userdata_t *ud)
|
||||
} else {
|
||||
strcpy(path, seg);
|
||||
}
|
||||
group_t *g = find_group(&ud->groups, gid);
|
||||
group_t *g = find_group(&ud->group_idx, &ud->groups, gid);
|
||||
gid = g ? g->parent_id : 0;
|
||||
}
|
||||
int members = 0;
|
||||
@@ -327,12 +405,12 @@ static void draw_properties_panel(userdata_t *ud)
|
||||
|
||||
changed |= igDragFloat2("Position", &s->cx, 1.0f, 0, 0, "%.1f", 0);
|
||||
if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_POSITION);
|
||||
changed |= igDragFloat2("Scale", &s->sx, 1.0f, 0.1f, 0, "%.1f", 0);
|
||||
changed |= igDragFloat2("Scale", &s->sx, 1.0f, -10.0f, 10.0f, "%.1f", 0);
|
||||
if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_SCALE);
|
||||
changed |= igDragFloat("Rotation", &s->rotation, 0.01f, 0, 0, "%.3f", 0);
|
||||
if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_ROTATION);
|
||||
|
||||
if (changed) { shape_regenerate(s); spatial_mark_dirty(&ud->spatial_grid); ud->overlay_upload_needed = true; }
|
||||
if (changed) { shape_regenerate(&ud->shape_pool, s); spatial_mark_dirty(&ud->spatial_grid); overlay_invalidate(ud); }
|
||||
|
||||
igSeparator();
|
||||
{
|
||||
@@ -389,9 +467,7 @@ static void draw_log_panel(userdata_t *ud)
|
||||
int idx = (start + i) % LOG_RING_SIZE;
|
||||
off += snprintf(buf + off, (size_t)(cap - off), "%s\n", ud->ui.log_ring[idx].text);
|
||||
}
|
||||
EM_ASM({
|
||||
navigator.clipboard.writeText(UTF8ToString($0));
|
||||
}, buf);
|
||||
sapp_set_clipboard_string(buf);
|
||||
FREE(buf);
|
||||
}
|
||||
igSameLine(0.0f, 10.0f);
|
||||
|
||||
25
src/util.h
25
src/util.h
@@ -33,7 +33,11 @@ static void vec_grow(vector_t *v, int min_capacity) {
|
||||
int new_cap = v->capacity ? v->capacity * 2 : 8;
|
||||
if (new_cap < min_capacity) new_cap = min_capacity;
|
||||
uint8_t *new_data = (uint8_t*) ALLOC(new_cap * v->stride);
|
||||
assert(new_data != NULL);
|
||||
if (!new_data) {
|
||||
EM_ASM({ console.error("vec_grow: ALLOC failed for %d elements of %d bytes", $0, $1); },
|
||||
new_cap, v->stride);
|
||||
return;
|
||||
}
|
||||
if (v->data) {
|
||||
memcpy(new_data, v->data, v->count * v->stride);
|
||||
FREE(v->data);
|
||||
@@ -72,6 +76,25 @@ static void vec_remove_ordered(vector_t *v, int index) {
|
||||
v->count--;
|
||||
}
|
||||
|
||||
// Remove `count` elements at given indices in a single compaction pass.
|
||||
// Indices must be sorted in ascending order and must be valid.
|
||||
static void vec_remove_ordered_bulk(vector_t *v, const int *indices, int count) {
|
||||
if (count <= 0) return;
|
||||
int write = indices[0];
|
||||
for (int k = 0; k < count; k++) {
|
||||
int gap_start = indices[k];
|
||||
int gap_end = (k + 1 < count) ? indices[k + 1] : v->count;
|
||||
int keep = gap_end - gap_start - 1;
|
||||
if (keep > 0) {
|
||||
memmove(v->data + write * v->stride,
|
||||
v->data + (gap_start + 1) * v->stride,
|
||||
(size_t)keep * (size_t)v->stride);
|
||||
write += keep;
|
||||
}
|
||||
}
|
||||
v->count -= count;
|
||||
}
|
||||
|
||||
static void *vec_insert(vector_t *v, int index) {
|
||||
if (index < 0 || index > v->count) return NULL;
|
||||
if (v->count >= v->capacity) vec_grow(v, v->count + 1);
|
||||
|
||||
Reference in New Issue
Block a user