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:
2026-05-03 00:38:45 +02:00
parent c4d657043c
commit 7e3da1c424
19 changed files with 2610 additions and 2945 deletions

View File

@@ -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>

View File

@@ -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)

View File

@@ -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
View 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

View File

@@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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);