refactor: eliminate globals, add pen tool + edit mode, frustum culling

Remove all file-scope mutable state so the codebase compiles cleanly
as a single translation unit. State moved into structs owned by
userdata_t:

- group_index_ctx_t replaces g_group_by_id/g_group_by_id_cap
- shape_pool_ctx_t replaces g_shape_pool_dirty/g_shape_data_dirty
  and g_shape_groups/g_shape_groups_count
- panel_log_ctx_t wired through all subsystems explicitly
- pipeline_ctx_t owns render pipeline handles
- overlay_upload_state_t (per-buffer flags) replaces single bool

New features piggybacking on the refactor:

- Pen tool: click-to-place anchors, Catmull-Rom preview, finalize
  into Bezier shapes with control points
- Edit mode: anchor/handle hit testing, dragging, pre-drag undo
  snapshots, dedicated GPU buffers for edit overlays
- Frustum culling: spatial-grid-based viewport cull in draw_shapes
  with linear-scan fallback for oversized viewports
- Log dedup: FNV-1a 64-bit hash to skip duplicate messages
- Buffer shrink: halve draw buffers after 60 frames of low usage
- Shape geometry hashing for instanced-draw vertex-buffer grouping
- Group member_indices arrays with O(n) rebuild
- Log ring expanded 64→256 entries, added log_filter

Debug build: added --profiling-funcs and -sASSERTIONS flags.
This commit is contained in:
2026-05-03 00:38:45 +02:00
parent c4d657043c
commit 7e3da1c424
19 changed files with 2610 additions and 2945 deletions

View File

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