You've already forked flecs_tests
Remove all file-scope mutable state so the codebase compiles cleanly as a single translation unit. State moved into structs owned by userdata_t: - group_index_ctx_t replaces g_group_by_id/g_group_by_id_cap - shape_pool_ctx_t replaces g_shape_pool_dirty/g_shape_data_dirty and g_shape_groups/g_shape_groups_count - panel_log_ctx_t wired through all subsystems explicitly - pipeline_ctx_t owns render pipeline handles - overlay_upload_state_t (per-buffer flags) replaces single bool New features piggybacking on the refactor: - Pen tool: click-to-place anchors, Catmull-Rom preview, finalize into Bezier shapes with control points - Edit mode: anchor/handle hit testing, dragging, pre-drag undo snapshots, dedicated GPU buffers for edit overlays - Frustum culling: spatial-grid-based viewport cull in draw_shapes with linear-scan fallback for oversized viewports - Log dedup: FNV-1a 64-bit hash to skip duplicate messages - Buffer shrink: halve draw buffers after 60 frames of low usage - Shape geometry hashing for instanced-draw vertex-buffer grouping - Group member_indices arrays with O(n) rebuild - Log ring expanded 64→256 entries, added log_filter Debug build: added --profiling-funcs and -sASSERTIONS flags.
108 lines
6.0 KiB
Markdown
108 lines
6.0 KiB
Markdown
# Cartograph
|
|
|
|
A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. Uses line-strip vector shapes with procedural geometry.
|
|
|
|
## Features
|
|
|
|
- **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
|
|
- **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
|
|
|
|
| Layer | Library |
|
|
|---|---|
|
|
| Language | C (C99) |
|
|
| Compiler | [Emscripten](https://emscripten.org/) (emcc) → WebAssembly |
|
|
| 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 -i`) |
|
|
|
|
## Build
|
|
|
|
```sh
|
|
# Install dependencies
|
|
./fetch_libs.sh
|
|
|
|
# Release build
|
|
make
|
|
|
|
# Debug build
|
|
make debug
|
|
```
|
|
|
|
Output is `app.html`, served by Emscripten's built-in web server or any static server.
|
|
|
|
## Project structure
|
|
|
|
```
|
|
src/
|
|
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, memtrack)
|
|
imgui/ Dear ImGui + cimgui
|
|
cglm/ C linear math library
|
|
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
|