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:
78
README.md
78
README.md
@@ -4,11 +4,17 @@ A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. U
|
||||
|
||||
## Features
|
||||
|
||||
- **Shapes** — procedural circles and stars with per-instance transforms (position, scale, rotation)
|
||||
- **Shapes** — parametric circles, stars, rectangles; freeform pen tool with Catmull-Rom splines
|
||||
- **Instanced rendering** — shapes with identical local-space geometry share vertex buffers; per-instance transforms (position, scale, rotation) uploaded via SSBO
|
||||
- **Bezier editing** — double-click any shape to edit its Bezier control points and handles in local space
|
||||
- **Shape editing** — select, move, rotate, scale individual shapes; rect-select multiple shapes
|
||||
- **Undo/redo** — property-level history stack (position, scale, rotation, color) with edit session capture and batch operations
|
||||
- **Spatial index** — hash grid for fast hit testing and rect-selection queries on large shape counts
|
||||
- **Viewport** — zoom and pan with screen↔world coordinate transforms
|
||||
- **Groups** — group/ungroup shapes with nested hierarchy support, group-level selection and focus mode
|
||||
- **Clipboard** — copy/paste shapes with deep-copy of geometry, group remapping for pasted items
|
||||
- **Undo/redo** — property-level history stack (position, scale, rotation, vertex edits, create/delete, group) with batch operations
|
||||
- **Pen tool** — click to place control points; Enter, double-click, or close-to-start to commit; Escape to cancel
|
||||
- **Spatial index** — open-addressing hash grid for fast hit testing, rect-selection, and viewport culling on large shape counts
|
||||
- **Viewport** — zoom (scroll) and pan (right-click drag) with screen↔world coordinate transforms
|
||||
- **Debug panel** — toggleable log overlay (backtick), FPS meter with 60-frame rolling average, log filtering
|
||||
|
||||
## Tech stack
|
||||
|
||||
@@ -19,7 +25,7 @@ A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. U
|
||||
| Graphics | [Sokol](https://github.com/floooh/sokol) with WebGPU backend |
|
||||
| UI | [Dear ImGui](https://github.com/ocornut/imgui) via [cimgui](https://github.com/cimgui/cimgui) |
|
||||
| Math | [cglm](https://github.com/recp/cglm) |
|
||||
| Shaders | WGSL (compiled to C headers via `xxd`) |
|
||||
| Shaders | WGSL (compiled to C headers via `xxd -i`) |
|
||||
|
||||
## Build
|
||||
|
||||
@@ -40,24 +46,62 @@ Output is `app.html`, served by Emscripten's built-in web server or any static s
|
||||
|
||||
```
|
||||
src/
|
||||
main.c Entry point, sokol init, render loop, input handling, UI panels, debug stats
|
||||
api.h Central include — all library headers, ALLOC/FREE macros
|
||||
camera.h Viewport state, zoom/pan, MVP matrix, screen↔world transforms
|
||||
render.h Shape pipeline init/shutdown, per-shape draw calls
|
||||
shape.h Shape geometry types, procedural generation, hit testing, buffer management
|
||||
spatial.h Spatial hash grid for accelerated hit tests and rect-select queries
|
||||
history.h Undo/redo stack with property-level tracking and batch operations
|
||||
util.h Vector (dynamic array) and memory pool data structures
|
||||
rand.h Xorshift32 PRNG utilities
|
||||
shaders/ WGSL shader sources (shape, sprite)
|
||||
main.c Entry point, sokol init, render loop, input dispatch, UI panels, debug stats
|
||||
api.h Central include hub — backend defines, all library headers, ALLOC/FREE macros
|
||||
types.h Shared type definitions, constants, userdata_t
|
||||
camera.h Viewport state, zoom/pan, MVP matrix (via glm_ortho), screen↔world transforms
|
||||
render.h Shape & overlay pipeline init/shutdown, shader definitions
|
||||
shape.h Shape geometry types, procedural generation, Bezier editing, vertex hash grouping, instanced buffer pool
|
||||
spatial.h Spatial hash grid with linear probing, AABB queries, viewport culling
|
||||
history.h Undo/redo stack — property-level tracking, vertex snapshots, batch operations
|
||||
interact.h Selection AABB, group recursive helpers, resize handle hit-test, group rebuild
|
||||
overlay.h Selection overlay geometry, rotate/corner handles, edit-mode anchor & handle visualization
|
||||
draw.h Draw dispatch — frustum culling, instance map sorting, instanced draw calls
|
||||
input.h Mouse/keyboard event handlers — select, move, rotate, resize, pen, edit mode, clipboard
|
||||
ui_panels.h ImGui panels — toolbar, shape list tree, properties, debug log
|
||||
util.h Stripe-based vector_t (dynamic array)
|
||||
rand.h Xorshift32 PRNG
|
||||
shaders/ WGSL shader sources (sprite, shape, overlay)
|
||||
generated/ xxd-generated C headers from shaders
|
||||
lib/
|
||||
sokol/ Sokol single-file headers (gfx, app, glue, log)
|
||||
sokol/ Sokol single-file headers (gfx, app, glue, log, memtrack)
|
||||
imgui/ Dear ImGui + cimgui
|
||||
cglm/ C linear math library
|
||||
util/ Sokol utility headers (memtrack, imgui integration)
|
||||
util/ Sokol utility headers
|
||||
```
|
||||
|
||||
## Architecture notes
|
||||
|
||||
### Instanced rendering with vertex hash grouping
|
||||
|
||||
Shapes share vertex buffers when their local-space geometry is identical. Each shape stores a 64-bit FNV-1a hash of its vertex data. The geometry pool groups shapes by `(num_elements, vertex_hash)` — not just vertex count. This means:
|
||||
|
||||
- **Parametric shapes** (circles, stars, rectangles from fixed formulas): all instances of the same type naturally produce the same hash, so they share one vertex buffer regardless of count.
|
||||
- **Freeform paths** (pen tool): each path gets a unique hash, guaranteeing its own vertex buffer and preventing geometry corruption.
|
||||
- **Bezier edits**: `shape_regenerate_from_ctrl` updates the hash and modifies the group buffer in-place via `sg_update_buffer` rather than destroying/recreating it.
|
||||
|
||||
The previous implementation grouped only by vertex count, which caused pen-drawn paths with matching counts to silently share the wrong geometry.
|
||||
|
||||
### Lazy group index rebuild
|
||||
|
||||
The `g_group_by_id` lookup array is rebuilt lazily. Operations that modify groups set a `g_group_index_dirty` flag; the actual rebuild happens on the first `find_group()` call afterward. This avoids redundant rebuilds when multiple group operations occur within the same frame (e.g., undo then redo, or group then ungroup).
|
||||
|
||||
### Hover state optimization
|
||||
|
||||
`handle_hover` (called every frame) tracks the previous set of highlighted shapes (up to 64) and only toggles state on shapes entering or leaving the highlight set. The O(n) full-array sweep is only used as a fallback when the highlight set exceeds 64 shapes.
|
||||
|
||||
### Pool rebuild granularity
|
||||
|
||||
When the geometry pool rebuilds, existing group vertex buffers whose `(num_elements, vertex_hash)` key still exists are preserved rather than destroyed and recreated. Only new keys trigger buffer creation, and only orphaned keys trigger destruction. The shape data SSBO is also preserved when its size hasn't changed.
|
||||
|
||||
### Spatial grid memory reuse
|
||||
|
||||
The spatial hash grid retains per-slot entry arrays across rebuilds. Only slots whose shape count has grown beyond their current capacity trigger a reallocation.
|
||||
|
||||
### Log deduplication
|
||||
|
||||
The debug log ring buffer uses a 64-bit message hash for fast deduplication of warnings and errors (levels 0-2). Debug-level messages (level 3) skip dedup entirely to avoid the linear scan cost when verbose logging is active.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
1831
diff_output.patch
1831
diff_output.patch
File diff suppressed because it is too large
Load Diff
3
makefile
3
makefile
@@ -65,7 +65,8 @@ debug: $(FETCH) $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES)
|
||||
$(CC) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) \
|
||||
-o $(TARGET) \
|
||||
$(EMCC_FLAGS) \
|
||||
-g -gsource-map=inline \
|
||||
-g --profiling-funcs -gsource-map=inline \
|
||||
-sASSERTIONS \
|
||||
-I$(LIB_DIR)/sokol \
|
||||
-I$(LIB_DIR)/imgui \
|
||||
-I$(LIB_DIR)/imgui/imgui \
|
||||
|
||||
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