Compare commits

..

6 Commits

Author SHA1 Message Date
7e3da1c424 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.
2026-05-03 00:38:45 +02:00
c4d657043c Add copy/paste, rewrite rendering pipeline with instanced draw calls and add a lot of caching to minimize overhead on huge edits. 2026-05-01 00:10:19 +02:00
e71641c094 Add a geometry pool, shape hierarchy, addittion and deletion. 2026-04-30 17:55:46 +02:00
9ce6e4accd Reorganized the code structure, fix a lot of issues. 2026-04-28 21:05:43 +02:00
81616f8a5d History, spatial grid and optimizations 2026-04-28 19:02:00 +02:00
5881a7dafc Add shapes and basic selective actions 2026-04-27 18:26:02 +02:00
28 changed files with 5800 additions and 639 deletions

View File

@@ -1,5 +1,39 @@
# CLAUDE.md # CLAUDE.md
## Project: Cartograph
A browser-based world map creation tool (like Wonderdraft/Inkarnate). C99 compiled to WebAssembly via Emscripten.
### Stack
- **Graphics:** Sokol (WebGPU backend, `SOKOL_WGPU`) — `lib/sokol/`
- **UI:** Dear ImGui via cimgui — `lib/imgui/`
- **Math:** cglm (types are C arrays: `vec2` = `float[2]`, `mat4` = `float[4][4]` column-major) — `lib/cglm/`
- **Shaders:** WGSL in `src/shaders/`, compiled to C headers via `xxd -i` into `src/generated/`
- **Shapes:** Line-strip based vector shapes (circle, star) with procedural vertex generation
### Build
- `make` (release) / `make debug` — outputs `app.html`
- All includes go through `src/api.h` which defines `SOKOL_IMPL`, `SOKOL_WGPU`, and pulls in every library header
- Include paths: `lib/sokol`, `lib/imgui`, `lib/imgui/imgui`, `lib/util`, `lib/cglm/include`
### Key files
- `src/main.c` — entry point, sokol init, render loop, all input handling, overlay geometry, UI panels, and debug stats
- `src/api.h` — central include hub, backend defines, ALLOC/FREE macros (wired to smemtrack)
- `src/camera.h` — viewport state (zoom/pan), MVP matrix computation, screen↔world coordinate transforms
- `src/render.h` — shape pipeline init/shutdown, per-shape draw calls (shader uniform binding)
- `src/shape.h` — shape geometry types, procedural generation (circle/star), transform building, hit testing, buffer management
- `src/spatial.h` — spatial hash grid for accelerating hit tests and rect-selection queries
- `src/history.h` — undo/redo stack with property-level tracking (position/scale/rotation/color), edit session capture, batch operations
- `src/util.h``vector_t` (dynamic array) and `mem_pool_t` (free-list pool), both stripe-based
- `src/rand.h` — xorshift32 PRNG
### Conventions
- No malloc/free directly — use `ALLOC`/`FREE` macros (wired to smemtrack in main.c)
- Assert is encouraged for invariant checks
- Data structures use stripe-based allocation (byte stride per element, not sizeof)
---
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding ## 1. Think Before Coding

107
README.md Normal file
View File

@@ -0,0 +1,107 @@
# 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

View File

@@ -9,6 +9,7 @@ echo "=== Fetching library dependencies ==="
mkdir -p "$LIB_DIR/sokol" mkdir -p "$LIB_DIR/sokol"
mkdir -p "$LIB_DIR/imgui" mkdir -p "$LIB_DIR/imgui"
mkdir -p "$LIB_DIR/util" mkdir -p "$LIB_DIR/util"
mkdir -p "$LIB_DIR/cglm"
if [ ! -f "$LIB_DIR/sokol/sokol_gfx.h" ]; then if [ ! -f "$LIB_DIR/sokol/sokol_gfx.h" ]; then
echo " > Fetching sokol (pinned to emdawnwebgpu-compatible version)..." echo " > Fetching sokol (pinned to emdawnwebgpu-compatible version)..."
@@ -47,4 +48,15 @@ else
echo " > cimgui already present" echo " > cimgui already present"
fi fi
# 4. cglm
if [ ! -f "$LIB_DIR/cglm/include/cglm/cglm.h" ]; then
echo " > Fetching cglm..."
git clone --depth 1 --branch v0.9.6 https://github.com/recp/cglm.git "$LIB_DIR/cglm_tmp"
cp -r "$LIB_DIR/cglm_tmp/include" "$LIB_DIR/cglm/"
cp -r "$LIB_DIR/cglm_tmp/src" "$LIB_DIR/cglm/"
rm -rf "$LIB_DIR/cglm_tmp"
else
echo " > cglm already present"
fi
echo "=== Done ===" echo "=== Done ==="

View File

@@ -19,6 +19,7 @@ IMGUI_SOURCES = $(LIB_DIR)/imgui/imgui/imgui.cpp \
$(LIB_DIR)/imgui/imgui/imgui_tables.cpp \ $(LIB_DIR)/imgui/imgui/imgui_tables.cpp \
$(LIB_DIR)/imgui/imgui/imgui_widgets.cpp \ $(LIB_DIR)/imgui/imgui/imgui_widgets.cpp \
$(LIB_DIR)/imgui/cimgui.cpp $(LIB_DIR)/imgui/cimgui.cpp
CGLM_SOURCES = $(wildcard $(LIB_DIR)/cglm/src/*.c)
# Dynamic shader processing # Dynamic shader processing
SHADER_FILES = $(wildcard $(SHADER_DIR)/*.wgsl) SHADER_FILES = $(wildcard $(SHADER_DIR)/*.wgsl)
@@ -29,7 +30,8 @@ EMCC_FLAGS = --use-port=emdawnwebgpu \
-sWASM_BIGINT \ -sWASM_BIGINT \
-sALLOW_MEMORY_GROWTH \ -sALLOW_MEMORY_GROWTH \
-msimd128 \ -msimd128 \
-sFILESYSTEM=0 -sFILESYSTEM=0 \
-flto
# Shell template # Shell template
SHELL_FILE = shell.html SHELL_FILE = shell.html
@@ -38,8 +40,8 @@ SHELL_FILE = shell.html
all: $(FETCH) $(TARGET) all: $(FETCH) $(TARGET)
# Main build target # Main build target
$(TARGET): $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(SHELL_FILE) $(TARGET): $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) $(SHELL_FILE)
$(CC) $(C_SOURCES) $(IMGUI_SOURCES) \ $(CC) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) \
-o $(TARGET) \ -o $(TARGET) \
$(EMCC_FLAGS) \ $(EMCC_FLAGS) \
-O3 \ -O3 \
@@ -47,6 +49,7 @@ $(TARGET): $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(SHELL_FILE)
-I$(LIB_DIR)/imgui \ -I$(LIB_DIR)/imgui \
-I$(LIB_DIR)/imgui/imgui \ -I$(LIB_DIR)/imgui/imgui \
-I$(LIB_DIR)/util \ -I$(LIB_DIR)/util \
-I$(LIB_DIR)/cglm/include \
--shell-file=$(SHELL_FILE) --shell-file=$(SHELL_FILE)
# Shader header generation # Shader header generation
@@ -58,15 +61,17 @@ $(GENERATED_DIR)/%.h: $(SHADER_DIR)/%.wgsl | $(GENERATED_DIR)
$(GENERATED_DIR): $(GENERATED_DIR):
mkdir -p $(GENERATED_DIR) mkdir -p $(GENERATED_DIR)
debug: $(FETCH) $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(SHELL_FILE) debug: $(FETCH) $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) $(SHELL_FILE)
$(CC) $(C_SOURCES) $(IMGUI_SOURCES) \ $(CC) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) \
-o $(TARGET) \ -o $(TARGET) \
$(EMCC_FLAGS) \ $(EMCC_FLAGS) \
-g -gsource-map=inline \ -g --profiling-funcs -gsource-map=inline \
-sASSERTIONS \
-I$(LIB_DIR)/sokol \ -I$(LIB_DIR)/sokol \
-I$(LIB_DIR)/imgui \ -I$(LIB_DIR)/imgui \
-I$(LIB_DIR)/imgui/imgui \ -I$(LIB_DIR)/imgui/imgui \
-I$(LIB_DIR)/util \ -I$(LIB_DIR)/util \
-I$(LIB_DIR)/cglm/include \
--shell-file=$(SHELL_FILE) --shell-file=$(SHELL_FILE)
# Clean build artifacts # Clean build artifacts

View File

@@ -6,6 +6,7 @@
#define SOKOL_IMPL #define SOKOL_IMPL
#define SOKOL_WGPU #define SOKOL_WGPU
#define SOKOL_IMGUI_IMPL #define SOKOL_IMGUI_IMPL
#define SOKOL_VALIDATE_NON_FATAL
#include "sokol_gfx.h" #include "sokol_gfx.h"
#include "sokol_app.h" #include "sokol_app.h"
@@ -14,14 +15,60 @@
#include "cimgui.h" #include "cimgui.h"
#include "sokol_imgui.h" #include "sokol_imgui.h"
#include "sokol_memtrack.h" #include "sokol_memtrack.h"
#include "sokol_shape.h"
#define ALLOC(arg) smemtrack_alloc(arg, NULL)
#define FREE(arg) smemtrack_free(arg, NULL)
#include "cglm/cglm.h"
#include "math.h"
#include "rand.h" #include "rand.h"
#include "util.h" #include "camera.h"
#include "sprite.h"
#include "generated/sprite.h" #include "generated/sprite.h"
#include "generated/shape.h"
#include "generated/overlay.h"
// 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(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);
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"
#include "spatial.h"
#include "history.h"
#include "types.h"
#include "interact.h"
#include "overlay.h"
#include "draw.h"
#include "input.h"
#include "ui_panels.h"
#include <emscripten.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <math.h> #include <math.h>

40
src/camera.h Normal file
View File

@@ -0,0 +1,40 @@
#ifndef CAMERA_H
#define CAMERA_H
#include "api.h"
typedef struct {
bool dragging;
float origin_x, origin_y;
} pan_state_t;
typedef struct {
int width, height;
float half_width, half_height;
vec2 pan;
float zoom;
float hover_tol;
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)
{
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)
{
const float sx = mx - cam->half_width;
const float sy = cam->half_height - my;
*wx = (sx - cam->pan[0]) / cam->zoom;
*wy = (sy - cam->pan[1]) / cam->zoom;
}
#endif

353
src/draw.h Normal file
View File

@@ -0,0 +1,353 @@
#ifndef DRAW_H
#define DRAW_H
#include "api.h"
#include "types.h"
static void draw_shapes(userdata_t *ud)
{
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 (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;
}
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 *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;
int draw_count;
bool any_drag = ud->interact.move.dragging || ud->interact.rotate.dragging ||
ud->interact.resize.dragging;
bool use_culling = !any_drag;
if (use_culling) {
if (n > visible_cap) {
if (visible) FREE(visible);
visible = (int*) ALLOC((size_t)n * sizeof(int));
visible_cap = n;
}
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);
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));
vs_u.base = (uint32_t)base;
memset(vs_u._pad, 0, 12);
sg_apply_uniforms(0, &SG_RANGE(vs_u));
sg_apply_bindings(&(sg_bindings){
.vertex_buffers[0] = group_vbuf,
.views[0] = ud->shape_pool.data_view,
.views[1] = ud->shape_pool.instance_map_view,
});
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(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;
glm_mat4_identity(u.transform);
u.state = 0;
memset(u._pad, 0, sizeof(u._pad));
sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp));
sg_apply_uniforms(1, &SG_RANGE(u));
sg_apply_bindings(&(sg_bindings){
.vertex_buffers[0] = ud->rect_vbuf,
.index_buffer = ud->rect_ibuf,
});
sg_draw(0, 5, 1);
}
if (show_handle) {
shape_uniform_t hu;
glm_mat4_identity(hu.transform);
hu.state = 0;
memset(hu._pad, 0, sizeof(hu._pad));
sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp));
sg_apply_uniforms(1, &SG_RANGE(hu));
sg_apply_bindings(&(sg_bindings){
.vertex_buffers[0] = ud->handle_vbuf,
.index_buffer = ud->handle_ibuf,
});
sg_draw(0, HANDLE_CIRCLE_SEGMENTS + 1, 1);
{
shape_uniform_t cu;
glm_mat4_identity(cu.transform);
cu.state = 0;
memset(cu._pad, 0, sizeof(cu._pad));
sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp));
sg_apply_uniforms(1, &SG_RANGE(cu));
sg_apply_bindings(&(sg_bindings){
.vertex_buffers[0] = ud->corner_vbuf,
.index_buffer = ud->corner_ibuf,
});
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

91
src/generated/overlay.h Normal file
View File

@@ -0,0 +1,91 @@
unsigned char src_shaders_overlay_wgsl[] = {
0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69,
0x66, 0x6f, 0x72, 0x6d, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d,
0x76, 0x70, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, 0x66, 0x2c,
0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20,
0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d,
0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73,
0x66, 0x6f, 0x72, 0x6d, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34,
0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65,
0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73,
0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, 0x49, 0x6e, 0x20, 0x7b,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69,
0x6f, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66, 0x2c, 0x0a, 0x7d,
0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73,
0x32, 0x46, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x62,
0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70, 0x6f, 0x73, 0x69, 0x74,
0x69, 0x6f, 0x6e, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x3a, 0x20, 0x76, 0x65,
0x63, 0x34, 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f,
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x40, 0x69,
0x6e, 0x74, 0x65, 0x72, 0x70, 0x6f, 0x6c, 0x61, 0x74, 0x65, 0x28, 0x6c,
0x69, 0x6e, 0x65, 0x61, 0x72, 0x29, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72,
0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a,
0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x46, 0x73, 0x4f, 0x75,
0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, 0x63,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x63, 0x6f, 0x6c,
0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x7d,
0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28,
0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29,
0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d,
0x3e, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d,
0x73, 0x3a, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d,
0x3b, 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31,
0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29, 0x20,
0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3e,
0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f,
0x72, 0x6d, 0x3a, 0x20, 0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69,
0x66, 0x6f, 0x72, 0x6d, 0x3b, 0x0a, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74,
0x65, 0x78, 0x20, 0x66, 0x6e, 0x20, 0x76, 0x73, 0x5f, 0x6d, 0x61, 0x69,
0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x49,
0x6e, 0x29, 0x20, 0x2d, 0x3e, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20,
0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75,
0x74, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, 0x72,
0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61,
0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x74,
0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x2a, 0x20, 0x76,
0x65, 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70,
0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69,
0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f,
0x6e, 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e,
0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70,
0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x73, 0x5f,
0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70,
0x20, 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73,
0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68,
0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e,
0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29,
0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f,
0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20,
0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x31, 0x2e, 0x30, 0x2c,
0x20, 0x30, 0x2e, 0x38, 0x34, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20,
0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20,
0x65, 0x6c, 0x73, 0x65, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61,
0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x73,
0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x75, 0x29, 0x20,
0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75,
0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x3d,
0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, 0x35, 0x2c, 0x20,
0x30, 0x2e, 0x36, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e,
0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, 0x6c,
0x73, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f,
0x72, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e,
0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c,
0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20,
0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x40,
0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20,
0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75,
0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, 0x3e,
0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20,
0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3a,
0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72,
0x20, 0x3d, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c,
0x6f, 0x72, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75,
0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d,
0x0a
};
unsigned int src_shaders_overlay_wgsl_len = 1045;

110
src/generated/shape.h Normal file
View File

@@ -0,0 +1,110 @@
unsigned char src_shaders_shape_wgsl[] = {
0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69,
0x66, 0x6f, 0x72, 0x6d, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d,
0x76, 0x70, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, 0x66, 0x2c,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63,
0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c,
0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20,
0x53, 0x68, 0x61, 0x70, 0x65, 0x44, 0x61, 0x74, 0x61, 0x20, 0x7b, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72,
0x6d, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, 0x66, 0x2c, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x3a, 0x20, 0x75,
0x33, 0x32, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e,
0x64, 0x69, 0x6e, 0x67, 0x28, 0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f,
0x75, 0x70, 0x28, 0x30, 0x29, 0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e,
0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3e, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e,
0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x3a, 0x20, 0x56, 0x73, 0x55, 0x6e,
0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e,
0x64, 0x69, 0x6e, 0x67, 0x28, 0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f,
0x75, 0x70, 0x28, 0x31, 0x29, 0x20, 0x76, 0x61, 0x72, 0x3c, 0x73, 0x74,
0x6f, 0x72, 0x61, 0x67, 0x65, 0x3e, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65,
0x5f, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79,
0x3c, 0x53, 0x68, 0x61, 0x70, 0x65, 0x44, 0x61, 0x74, 0x61, 0x3e, 0x3b,
0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31, 0x29,
0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x31, 0x29, 0x20, 0x76,
0x61, 0x72, 0x3c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x3e, 0x20,
0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70,
0x3a, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x75, 0x33, 0x32, 0x3e,
0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73,
0x49, 0x6e, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x62, 0x75,
0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e,
0x63, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x29, 0x20, 0x69, 0x6e,
0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x3a, 0x20,
0x75, 0x33, 0x32, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f,
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x70, 0x6f,
0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32,
0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63,
0x74, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x40, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70,
0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29, 0x20, 0x70, 0x6f, 0x73,
0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20,
0x20, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30,
0x29, 0x20, 0x40, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x6f, 0x6c, 0x61,
0x74, 0x65, 0x28, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x29, 0x20, 0x63,
0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c,
0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20,
0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29,
0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34,
0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74,
0x65, 0x78, 0x20, 0x66, 0x6e, 0x20, 0x76, 0x73, 0x5f, 0x6d, 0x61, 0x69,
0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x49,
0x6e, 0x29, 0x20, 0x2d, 0x3e, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20,
0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75,
0x74, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x73, 0x68, 0x61,
0x70, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x73,
0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x5b, 0x76, 0x73,
0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x69, 0x6e,
0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x20,
0x2b, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x69, 0x6e, 0x73, 0x74,
0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x5d, 0x3b, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65,
0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x64, 0x61, 0x74,
0x61, 0x5b, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x5d,
0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f,
0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68,
0x61, 0x70, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72,
0x6d, 0x20, 0x2a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e,
0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
0x2e, 0x78, 0x2c, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f,
0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e,
0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20,
0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20,
0x3d, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d,
0x73, 0x2e, 0x6d, 0x76, 0x70, 0x20, 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c,
0x64, 0x5f, 0x70, 0x6f, 0x73, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69,
0x66, 0x20, 0x28, 0x73, 0x68, 0x61, 0x70, 0x65, 0x2e, 0x73, 0x74, 0x61,
0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, 0x20, 0x7b, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70,
0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x3d, 0x20, 0x76,
0x65, 0x63, 0x34, 0x66, 0x28, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x30, 0x2e,
0x38, 0x34, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30,
0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, 0x6c, 0x73,
0x65, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, 0x70, 0x65, 0x2e,
0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x75, 0x29,
0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f,
0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20,
0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, 0x35, 0x2c,
0x20, 0x30, 0x2e, 0x36, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x31,
0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65,
0x6c, 0x73, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c,
0x6f, 0x72, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30,
0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38,
0x2c, 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e,
0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a,
0x40, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e,
0x20, 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70,
0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d,
0x3e, 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74,
0x3a, 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20,
0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f,
0x72, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f,
0x6c, 0x6f, 0x72, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74,
0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a,
0x7d, 0x0a
};
unsigned int src_shaders_shape_wgsl_len = 1274;

View File

@@ -74,60 +74,7 @@ unsigned char src_shaders_sprite_wgsl[] = {
0x70, 0x75, 0x74, 0x2e, 0x75, 0x76, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x70, 0x70, 0x75, 0x74, 0x2e, 0x75, 0x76, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x70,
0x75, 0x74, 0x2e, 0x75, 0x76, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x75, 0x74, 0x2e, 0x75, 0x76, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74,
0x70, 0x75, 0x74, 0x3b, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x2f, 0x70, 0x75, 0x74, 0x3b, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x40,
0x2f, 0x20, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x20, 0x61, 0x20,
0x33, 0x32, 0x62, 0x69, 0x74, 0x20, 0x75, 0x69, 0x6e, 0x74, 0x20, 0x63,
0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x28, 0x68, 0x65, 0x78, 0x20, 0x72, 0x65,
0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x29, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x6e, 0x6f, 0x72,
0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x76, 0x65, 0x63, 0x34,
0x66, 0x0d, 0x0a, 0x2f, 0x2a, 0x66, 0x6e, 0x20, 0x63, 0x6f, 0x6e, 0x76,
0x65, 0x72, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x28, 0x69, 0x6e, 0x70,
0x75, 0x74, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x29, 0x20, 0x2d, 0x3e, 0x20,
0x76, 0x65, 0x63, 0x34, 0x66, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20,
0x20, 0x6c, 0x65, 0x74, 0x20, 0x72, 0x3a, 0x20, 0x66, 0x33, 0x32, 0x20,
0x3d, 0x20, 0x66, 0x33, 0x32, 0x28, 0x28, 0x28, 0x69, 0x6e, 0x70, 0x75,
0x74, 0x20, 0x3e, 0x3e, 0x20, 0x20, 0x30, 0x29, 0x20, 0x26, 0x20, 0x30,
0x78, 0x66, 0x66, 0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x6c, 0x65, 0x74, 0x20, 0x67, 0x3a, 0x20, 0x66, 0x33, 0x32, 0x20, 0x3d,
0x20, 0x66, 0x33, 0x32, 0x28, 0x28, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74,
0x20, 0x3e, 0x3e, 0x20, 0x20, 0x38, 0x29, 0x20, 0x26, 0x20, 0x30, 0x78,
0x66, 0x66, 0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c,
0x65, 0x74, 0x20, 0x62, 0x3a, 0x20, 0x66, 0x33, 0x32, 0x20, 0x3d, 0x20,
0x66, 0x33, 0x32, 0x28, 0x28, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20,
0x3e, 0x3e, 0x20, 0x31, 0x36, 0x29, 0x20, 0x26, 0x20, 0x30, 0x78, 0x66,
0x66, 0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65,
0x74, 0x20, 0x61, 0x3a, 0x20, 0x66, 0x33, 0x32, 0x20, 0x3d, 0x20, 0x66,
0x33, 0x32, 0x28, 0x28, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x3e,
0x3e, 0x20, 0x32, 0x34, 0x29, 0x20, 0x26, 0x20, 0x30, 0x78, 0x66, 0x66,
0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72,
0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28,
0x72, 0x20, 0x2f, 0x20, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x67, 0x20, 0x2f,
0x20, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x62, 0x20, 0x2f, 0x20, 0x32, 0x35,
0x35, 0x2c, 0x20, 0x61, 0x20, 0x2f, 0x20, 0x32, 0x35, 0x35, 0x29, 0x3b,
0x0d, 0x0a, 0x7d, 0x2a, 0x2f, 0x0d, 0x0a, 0x2f, 0x2f, 0x20, 0x47, 0x65,
0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x74, 0x65, 0x78, 0x74, 0x75, 0x72,
0x65, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79, 0x20, 0x69, 0x6e, 0x64, 0x65,
0x78, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x55,
0x56, 0x0d, 0x0a, 0x2f, 0x2a, 0x66, 0x6e, 0x20, 0x69, 0x6e, 0x64, 0x65,
0x78, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x28, 0x75,
0x76, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66, 0x2c, 0x20, 0x77, 0x69,
0x64, 0x74, 0x68, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c, 0x20, 0x68, 0x65,
0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x29, 0x20, 0x2d,
0x3e, 0x20, 0x75, 0x33, 0x32, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20,
0x20, 0x6c, 0x65, 0x74, 0x20, 0x78, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x20,
0x3d, 0x20, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x28, 0x66, 0x6c, 0x6f, 0x6f,
0x72, 0x28, 0x75, 0x76, 0x2e, 0x78, 0x20, 0x2a, 0x20, 0x66, 0x33, 0x32,
0x28, 0x77, 0x69, 0x64, 0x74, 0x68, 0x29, 0x29, 0x2c, 0x20, 0x30, 0x2c,
0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x79, 0x3a, 0x20, 0x75, 0x33, 0x32,
0x20, 0x3d, 0x20, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x28, 0x66, 0x6c, 0x6f,
0x6f, 0x72, 0x28, 0x75, 0x76, 0x2e, 0x79, 0x20, 0x2a, 0x20, 0x66, 0x33,
0x32, 0x28, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x29, 0x29, 0x2c, 0x20,
0x30, 0x2c, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x29, 0x3b, 0x0d,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20,
0x79, 0x20, 0x2a, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x20, 0x2b, 0x20,
0x78, 0x3b, 0x0d, 0x0a, 0x7d, 0x2a, 0x2f, 0x0d, 0x0a, 0x0d, 0x0a, 0x40,
0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20,
0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75,
0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, 0x3e, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, 0x3e,
@@ -142,4 +89,4 @@ unsigned char src_shaders_sprite_wgsl[] = {
0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b,
0x0d, 0x0a, 0x7d 0x0d, 0x0a, 0x7d
}; };
unsigned int src_shaders_sprite_wgsl_len = 1695; unsigned int src_shaders_sprite_wgsl_len = 1059;

14
src/globals.h Normal file
View File

@@ -0,0 +1,14 @@
// Context struct declarations are now defined alongside the code they
// belong to rather than in a single header:
//
// panel_log_ctx_t — api.h
// shape_pool_ctx_t — shape.h (after shape_t / group_t)
// group_index_ctx_t — shape.h
// pipeline_ctx_t — render.h
// shape_group_buf_t — shape.h
//
// This header exists only to avoid breaking includes elsewhere and can
// be removed once all consumers are updated.
#ifndef GLOBALS_H
#define GLOBALS_H
#endif

499
src/history.h Normal file
View File

@@ -0,0 +1,499 @@
#ifndef HISTORY_H
#define HISTORY_H
#include "api.h"
#define HISTORY_MAX_DEPTH 256
typedef enum {
HIST_POSITION,
HIST_SCALE,
HIST_ROTATION,
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 {
int shape_index;
hist_prop_t prop;
float old_val[4];
float new_val[4];
// 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 {
hist_change_t *changes;
int count;
} hist_entry_t;
typedef struct history_t {
vector_t entries;
int current;
bool capturing;
int pending_shape_idx;
hist_prop_t pending_prop;
float pending_old[4];
} history_t;
// -- helpers --
static void hist_read_prop(shape_t *s, hist_prop_t prop, float out[4]) {
memset(out, 0, sizeof(float[4]));
switch (prop) {
case HIST_POSITION: out[0] = s->cx; out[1] = s->cy; break;
case HIST_SCALE: out[0] = s->sx; out[1] = s->sy; break;
case HIST_ROTATION: out[0] = s->rotation; break;
case HIST_GROUP: out[0] = (float)s->group_id; break;
default: break;
}
}
static void hist_apply_prop(shape_t *s, hist_prop_t prop, const float val[4]) {
switch (prop) {
case HIST_POSITION: s->cx = val[0]; s->cy = val[1]; break;
case HIST_SCALE: s->sx = val[0]; s->sy = val[1]; break;
case HIST_ROTATION: s->rotation = val[0]; break;
case HIST_GROUP: s->group_id = (int)val[0]; break;
default: break;
}
}
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->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));
}
static void history_init(history_t *h) {
memset(h, 0, sizeof(*h));
vec_init(&h->entries, sizeof(hist_entry_t));
h->current = -1;
}
static void history_destroy(history_t *h) {
for (int i = 0; i < h->entries.count; i++) {
hist_entry_t *e = (hist_entry_t*) vec_get(&h->entries, i);
if (e->changes) {
for (int j = 0; j < e->count; j++)
hist_free_change(&e->changes[j]);
FREE(e->changes);
}
}
vec_free(&h->entries);
memset(h, 0, sizeof(*h));
h->current = -1;
}
static void history_push_entry(history_t *h, hist_entry_t entry) {
while (h->entries.count > h->current + 1) {
hist_entry_t *e = (hist_entry_t*) vec_get(&h->entries, h->entries.count - 1);
if (e->changes) {
for (int j = 0; j < e->count; j++)
hist_free_change(&e->changes[j]);
FREE(e->changes);
}
vec_pop(&h->entries);
}
*((hist_entry_t*) vec_push(&h->entries)) = entry;
h->current = h->entries.count - 1;
while (h->entries.count > HISTORY_MAX_DEPTH) {
hist_entry_t *e = (hist_entry_t*) vec_get(&h->entries, 0);
if (e->changes) {
for (int j = 0; j < e->count; j++)
hist_free_change(&e->changes[j]);
FREE(e->changes);
}
vec_remove_ordered(&h->entries, 0);
h->current--;
}
}
static void history_begin_edit(history_t *h, vector_t *shapes,
int shape_idx, hist_prop_t prop) {
if (h->capturing) {
shape_t *s = (shape_t*) vec_get(shapes, h->pending_shape_idx);
float new_val[4];
hist_read_prop(s, h->pending_prop, new_val);
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) {
hist_entry_t entry = { .changes = NULL, .count = 1 };
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t));
memset(entry.changes, 0, sizeof(hist_change_t));
entry.changes->shape_index = h->pending_shape_idx;
entry.changes->prop = h->pending_prop;
memcpy(entry.changes->old_val, h->pending_old, sizeof(float[4]));
memcpy(entry.changes->new_val, new_val, sizeof(float[4]));
history_push_entry(h, entry);
}
h->capturing = false;
}
h->capturing = true;
h->pending_shape_idx = shape_idx;
h->pending_prop = prop;
shape_t *s = (shape_t*) vec_get(shapes, shape_idx);
hist_read_prop(s, prop, h->pending_old);
}
static void history_end_edit(history_t *h, vector_t *shapes) {
if (!h->capturing) return;
shape_t *s = (shape_t*) vec_get(shapes, h->pending_shape_idx);
float new_val[4];
hist_read_prop(s, h->pending_prop, new_val);
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) {
hist_entry_t entry = { .changes = NULL, .count = 1 };
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t));
memset(entry.changes, 0, sizeof(hist_change_t));
entry.changes->shape_index = h->pending_shape_idx;
entry.changes->prop = h->pending_prop;
memcpy(entry.changes->old_val, h->pending_old, sizeof(float[4]));
memcpy(entry.changes->new_val, new_val, sizeof(float[4]));
history_push_entry(h, entry);
}
h->capturing = false;
}
// -- batch API --
typedef struct {
hist_change_t *changes;
int count;
int capacity;
} hist_batch_t;
static void history_batch_init(hist_batch_t *batch, int count) {
batch->changes = (hist_change_t*) ALLOC((size_t)count * sizeof(hist_change_t));
memset(batch->changes, 0, (size_t)count * sizeof(hist_change_t));
batch->count = 0;
batch->capacity = count;
}
// For property changes (POSITION, SCALE, ROTATION, GROUP)
static void history_batch_add(hist_batch_t *batch, int shape_index, hist_prop_t prop,
const float old_val[4], const float new_val[4]) {
hist_change_t *c = &batch->changes[batch->count++];
c->shape_index = shape_index;
c->prop = prop;
memcpy(c->old_val, old_val, sizeof(float[4]));
memcpy(c->new_val, new_val, sizeof(float[4]));
}
// 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] = 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;
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;
c->vertex_data = (shape_vertex_t*) ALLOC((size_t)n * sizeof(shape_vertex_t));
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));
}
}
// 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++];
c->shape_index = shape_index;
c->prop = prop;
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(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;
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;
return s;
}
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 ||
p == HIST_GROUP_CREATE || p == HIST_GROUP_DELETE) { has_shape_ops = true; break; }
}
int start = 0, end = entry->count, step = 1;
if (has_shape_ops && !forward) {
start = entry->count - 1;
end = -1;
step = -1;
}
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) {
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(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(sp, s);
shape_set_state(sp, s, s->hovered, s->selected);
}
}
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, 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(h, shapes, sp, groups, gi, true);
return true;
}
#endif

1248
src/input.h Normal file

File diff suppressed because it is too large Load Diff

113
src/interact.h Normal file
View File

@@ -0,0 +1,113 @@
#ifndef INTERACT_H
#define INTERACT_H
#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)
{
bool first = true;
for (int i = 0; i < ud->shapes.count; i++) {
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
if (!s->selected) continue;
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)
{
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(&ud->group_idx,s->group_id, gid, &ud->groups)) {
s->selected = true;
ud->interact.selected_count++;
}
}
}
static void deselect_and_select_group_recursive(userdata_t *ud, int gid)
{
for (int i = 0; i < ud->shapes.count; i++)
((shape_t*) vec_get(&ud->shapes, i))->selected = false;
ud->interact.selected_count = 0;
select_group_recursive(ud, gid);
}
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(&ud->group_idx,s->group_id, gid, &ud->groups)) {
total++;
if (s->selected) sel++;
}
}
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(&ud->group_idx,s->group_id, gid, &ud->groups)) {
s->selected = !all_sel;
ud->interact.selected_count += s->selected ? 1 : -1;
}
}
}
static int hit_test_resize_handles(userdata_t *ud, float wx, float wy, float tol)
{
if (ud->interact.selected_count <= 0) return -1;
float omin[2], omax[2];
if (ud->interact.aabb_cached) {
omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1];
omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3];
} else {
selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]);
}
float hs = CORNER_SIZE_PX / ud->camera.zoom * 0.5f + tol;
float mid_x = (omin[0] + omax[0]) * 0.5f;
float mid_y = (omin[1] + omax[1]) * 0.5f;
float hx[8] = {omin[0], mid_x, omax[0], omax[0], omax[0], mid_x, omin[0], omin[0]};
float hy[8] = {omin[1], omin[1], omin[1], mid_y, omax[1], omax[1], omax[1], mid_y};
for (int h = 0; h < 8; h++) {
if (fabsf(wx - hx[h]) <= hs && fabsf(wy - hy[h]) <= hs)
return h;
}
return -1;
}
#endif

View File

@@ -1,131 +1,184 @@
#include "api.h" #include "api.h"
#define ALLOC(arg) smemtrack_alloc(arg, NULL) static void log_capture(const char* tag, uint32_t log_level, uint32_t log_item,
#define FREE(arg) smemtrack_free(arg, NULL) const char* message, uint32_t line_nr, const char* filename,
void* user_data)
#define GRID_X 1000
#define GRID_Y 1000
typedef struct vs_uniform_t {
mat4x4f mvp;
} uniform_t;
typedef struct renderer_t {
sg_pipeline pipeline; //Configured sprite pipeline
sg_pass_action clear_pass; //Render pass - Clear screen
uniform_t uniform; //Uniform data
} renderer_t;
typedef struct dragger_t {
bool dragging;
float origin_x, origin_y;
} dragger_t;
typedef struct userdata_t {
int width, height;
vec2f pan;
float zoom;
dragger_t dragger;
renderer_t renderer;
manager_t manager;
} userdata_t;
const char* format(const char* format, ...)
{ {
char buffer[_SLOG_LINE_LENGTH]; userdata_t* ud = (userdata_t*)user_data;
va_list va; const char* level_str;
va_start(va, format); switch (log_level) {
case 0: level_str = "panic"; break;
case 1: level_str = "error"; break;
case 2: level_str = "warn"; break;
default:level_str = "info"; break;
}
int size = vsnprintf(buffer, _SLOG_LINE_LENGTH, format, va); char buf[256];
int n = snprintf(buf, sizeof(buf), "[%s][%s][id:%u]", tag, level_str, log_item);
if (filename) {
n += snprintf(buf + n, sizeof(buf) - n, " %s:%u:", filename, line_nr);
}
if (message && n < (int)sizeof(buf) - 2) {
snprintf(buf + n, sizeof(buf) - n, " %s", message);
}
va_end(va); int idx = ud->ui.log_head;
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++;
return buffer; fprintf(stderr, "%s\n", buf);
if (log_level <= 1) ud->ui.log_show = true;
} }
void compute_mvp(userdata_t *userdata)
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;
ud->ui.log_ring[idx].level = (uint32_t)level;
ud->ui.log_head = (idx + 1) % LOG_RING_SIZE;
if (ud->ui.log_count < LOG_RING_SIZE) ud->ui.log_count++;
}
static void meter_fps(userdata_t *ud)
{ {
userdata->renderer.uniform.mvp.e[0][0] = (2.0f / userdata->width) * userdata->zoom; float dt = (float)sapp_frame_duration();
userdata->renderer.uniform.mvp.e[1][1] = (2.0f / userdata->height) * userdata->zoom; float instant_fps = dt > 0.0001f ? 1.0f / dt : 0.0f;
userdata->renderer.uniform.mvp.e[0][3] = (2.0f / userdata->width) * userdata->pan.x; ud->debug.fps_immediate += (instant_fps - ud->debug.fps_immediate) * 0.1f;
userdata->renderer.uniform.mvp.e[1][3] = (2.0f / userdata->height) * userdata->pan.y;
int idx = ud->debug.frame_time_head;
if (ud->debug.frame_time_count == 60) {
ud->debug.frame_time_sum -= ud->debug.frame_times[idx];
} else {
ud->debug.frame_time_count++;
}
ud->debug.frame_times[idx] = dt;
ud->debug.frame_time_sum += dt;
ud->debug.frame_time_head = (idx + 1) % 60;
ud->debug.fps_average = ud->debug.frame_time_sum > 0.0001f
? (float)ud->debug.frame_time_count / ud->debug.frame_time_sum : 0.0f;
} }
static void frame(void* _userdata) static void frame(void* _userdata)
{ {
userdata_t* userdata = (userdata_t*) _userdata; userdata_t* ud = (userdata_t*) _userdata;
shape_begin_frame();
meter_fps(ud);
ud->time += (double)sapp_frame_duration();
spatial_rebuild(&ud->spatial_grid, &ud->shapes);
float sel_cx, sel_cy, sel_hw, sel_hh, sel_angle;
shape_vertex_t overlay_verts[5];
bool has_overlay, show_handle;
compute_overlay_geometry(ud, overlay_verts, &sel_cx, &sel_cy,
&sel_hw, &sel_hh, &sel_angle, &has_overlay, &show_handle);
upload_overlay_buffers(ud, overlay_verts, sel_cx, sel_cy,
sel_hw, sel_hh, sel_angle, has_overlay, show_handle);
sg_begin_pass(&(sg_pass){ sg_begin_pass(&(sg_pass){
.action = userdata->renderer.clear_pass, .action = ud->renderer.clear_pass,
.swapchain = sglue_swapchain(), .swapchain = sglue_swapchain(),
}); });
const uint32_t length = vector_length(&userdata->manager.textures); draw_shapes(ud);
if(length > 0) draw_overlay_and_handles(ud, has_overlay, show_handle);
{
sg_apply_pipeline(userdata->renderer.pipeline);
for(uint32_t i = 0; i < length; i++) simgui_new_frame(&(simgui_frame_desc_t){
{ .width = sapp_width(),
texture_t* texture = (texture_t*) vector_get(&userdata->manager.textures, i); .height = sapp_height(),
.delta_time = sapp_frame_duration(),
.dpi_scale = sapp_dpi_scale(),
});
if(texture->dirty) draw_top_panel(ud);
{ draw_shape_list_panel(ud);
const sg_range range = vector_range(&texture->sprites); draw_properties_panel(ud);
sg_buffer buf = sg_query_view_buffer(texture->binding.views[1]); draw_log_panel(ud);
sg_update_buffer(buf, &range);
texture->dirty = false;
}
sg_apply_bindings(&texture->binding);
sg_apply_uniforms(0, &SG_RANGE(userdata->renderer.uniform));
sg_draw(0, 6, vector_length(&texture->sprites));
}
}
simgui_render();
sg_end_pass(); sg_end_pass();
sg_commit(); sg_commit();
} }
static void init(void* _userdata) static void init(void* _userdata)
{ {
rand_seed(1); userdata_t* ud = (userdata_t*) _userdata;
userdata_t* userdata = (userdata_t*) _userdata; rand_seed(&ud->rand_ctx, 1);
ud->panel_log_ctx.fn = panel_log_impl;
ud->panel_log_ctx.ud = ud;
sg_desc sgdesc = { sg_desc sgdesc = {
.environment = sglue_environment(), .environment = sglue_environment(),
.logger.func = slog_func, .logger.func = log_capture,
.logger.user_data = ud,
.uniform_buffer_size = 16 * 1024 * 1024,
}; };
sg_setup(&sgdesc); sg_setup(&sgdesc);
if (!sg_isvalid()) { if (!sg_isvalid()) {
fprintf(stderr, "Failed to create Sokol GFX context!\n"); fprintf(stderr, "Failed to create Sokol GFX context!\n");
exit(-1); exit(-1);
} }
simgui_setup(&(simgui_desc_t){0});
const vec2f quad[4] = { const vec2 quad[4] = {
{-2.0f, 2.0f}, // bottom left {-2.0f, 2.0f},
{2.0f, 2.0f}, // bottom right {2.0f, 2.0f},
{2.0f, -2.0f}, // top right {2.0f, -2.0f},
{-2.0f, -2.0f}, // top left {-2.0f, -2.0f},
}; };
const vec2f uv[4] = { const vec2 uv[4] = {
{0.0f, 1.0f}, // bottom left {0.0f, 1.0f},
{1.0f, 1.0f}, // bottom right {1.0f, 1.0f},
{1.0f, 0.0f}, // top right {1.0f, 0.0f},
{0.0f, 0.0f}, // top left {0.0f, 0.0f},
}; };
const uint16_t indices[] = { const uint16_t indices[] = {
0, 1, 2, 0, 2, 3, 0, 1, 2, 0, 2, 3,
}; };
userdata->width = sapp_width();
userdata->height = sapp_height();
userdata->pan = (vec2f) { 0.0f, 0.0f };
userdata->zoom = 2;
sg_shader sprite_shader = sg_make_shader(&(sg_shader_desc) { ud->camera.width = sapp_width();
ud->camera.height = sapp_height();
ud->camera.half_width = ud->camera.width * 0.5f;
ud->camera.half_height = ud->camera.height * 0.5f;
glm_vec2_zero(ud->camera.pan);
ud->camera.zoom = 0.5f;
ud->camera.hover_tol = SHAPE_HOVER_PX / ud->camera.zoom;
ud->renderer.shader = sg_make_shader(&(sg_shader_desc) {
.vertex_func = { .vertex_func = {
.source = (const char*) src_shaders_sprite_wgsl, .source = (const char*) src_shaders_sprite_wgsl,
.entry = "vs_main", .entry = "vs_main",
@@ -178,114 +231,253 @@ static void init(void* _userdata)
.label = "Sprite shader", .label = "Sprite shader",
}); });
userdata->renderer = (renderer_t) { ud->renderer.clear_pass = (sg_pass_action) {
.clear_pass = (sg_pass_action) { .colors[0] = { .clear_value = { 0.0f, 0.0f, 0.0f, 1.0f }, .load_action = SG_LOADACTION_CLEAR }
.colors[0] = { .clear_value = { 0.0f, 0.0f, 0.0f, 1.0f }, .load_action = SG_LOADACTION_CLEAR } };
ud->renderer.pipeline = sg_make_pipeline(&(sg_pipeline_desc) {
.shader = ud->renderer.shader,
.index_type = SG_INDEXTYPE_UINT16,
.layout.attrs = {
[0].format = SG_VERTEXFORMAT_FLOAT2,
[1].format = SG_VERTEXFORMAT_FLOAT2,
},
.label = "Sprite pipeline",
});
ud->renderer.uniform = (uniform_t) {
.mvp = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
}, },
.pipeline = sg_make_pipeline(&(sg_pipeline_desc) {
.shader = sprite_shader,
.index_type = SG_INDEXTYPE_UINT16,
.layout.attrs = {
[0].format = SG_VERTEXFORMAT_FLOAT2,
[1].format = SG_VERTEXFORMAT_FLOAT2,
},
.label = "Sprite pipeline",
}),
.uniform = (uniform_t) {
.mvp = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
},
}
}; };
gs_init(&userdata->manager); shape_init_pipeline(&ud->pipelines, &ud->panel_log_ctx);
compute_mvp(userdata); vec_init(&ud->shapes, sizeof(shape_t));
vec_init(&ud->groups, sizeof(group_t));
vec_init(&ud->interact.drag_indices, sizeof(int));
spatial_init(&ud->spatial_grid);
ud->interact.selected_count = 0;
ud->interact.hovered_shape = -1;
ud->interact.select.active = false;
ud->interact.select.dragging = false;
ud->ui.right_panel_w = 300;
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;
overlay_invalidate(ud);
ud->interact.resize.init_count = 0;
ud->next_group_id = 1;
ud->time = 0.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;
ud->ui.active_tool = TOOL_SELECT;
{
ud->rect_vbuf = sg_make_buffer(&(sg_buffer_desc){
.size = 5 * sizeof(shape_vertex_t),
.usage = { .stream_update = true },
.label = "Sel rect verts",
});
uint16_t rect_idx[5] = {0, 1, 2, 3, 4};
ud->rect_ibuf = sg_make_buffer(&(sg_buffer_desc){
.usage = {.index_buffer = true},
.data = {rect_idx, sizeof(rect_idx)},
.label = "Sel rect indices",
});
}
{
const int n = HANDLE_CIRCLE_SEGMENTS + 1;
uint16_t handle_idx[HANDLE_CIRCLE_SEGMENTS + 1];
for (int i = 0; i < n; i++) handle_idx[i] = (uint16_t)i;
ud->handle_vbuf = sg_make_buffer(&(sg_buffer_desc){
.size = (size_t)n * sizeof(shape_vertex_t),
.usage = { .stream_update = true },
.label = "Handle verts",
});
ud->handle_ibuf = sg_make_buffer(&(sg_buffer_desc){
.usage = {.index_buffer = true},
.data = {handle_idx, sizeof(handle_idx)},
.label = "Handle indices",
});
}
{
ud->corner_vbuf = sg_make_buffer(&(sg_buffer_desc){
.size = 40 * sizeof(shape_vertex_t),
.usage = { .stream_update = true },
.label = "Corner verts",
});
uint16_t ci[40];
for (int i = 0; i < 40; i++) ci[i] = (uint16_t)i;
ud->corner_ibuf = sg_make_buffer(&(sg_buffer_desc){
.usage = {.index_buffer = true},
.data = {ci, sizeof(ci)},
.label = "Corner indices",
});
}
*((shape_t*) vec_push(&ud->shapes)) = shape_circle(&ud->shape_pool, 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' || e.key === 'g') {
e.preventDefault();
}
}
}, true);
Module._cartograph_canvas = document.querySelector('canvas');
});
compute_mvp(&ud->camera, &ud->renderer.uniform.mvp);
} }
static void cleanup(void* _userdata) static void cleanup(void* _userdata)
{ {
userdata_t* userdata = (userdata_t*) _userdata; userdata_t* ud = (userdata_t*) _userdata;
vector_free(&userdata->manager.textures); for (int i = 0; i < ud->shapes.count; 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(&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);
sg_destroy_buffer(ud->handle_vbuf);
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(&ud->shape_pool);
shape_shutdown_pipeline(&ud->pipelines);
FREE(userdata); 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->ui.display_cache);
FREE(ud);
simgui_shutdown();
sg_shutdown(); sg_shutdown();
} }
static void event(const sapp_event* event, void* _userdata) static void event(const sapp_event* event, void* _userdata)
{ {
userdata_t* userdata = (userdata_t*) _userdata; userdata_t* ud = (userdata_t*) _userdata;
switch(event->type) if (event->type == SAPP_EVENTTYPE_RESIZED) {
{ handle_resize(ud, event);
case SAPP_EVENTTYPE_FILES_DROPPED: }
uint32_t files = sapp_get_num_dropped_files();
for(uint32_t i = 0; i < files; i++) if (event->type == SAPP_EVENTTYPE_KEY_DOWN) {
{ if (handle_key_down(ud, event)) return;
const uint32_t size = sapp_html5_get_dropped_file_size(i); }
if(size > MAX_FILE_SIZE) if (simgui_handle_event(event)) return;
{
//Toast error
continue;
}
void* buffer = malloc(size); switch (event->type) {
sapp_html5_fetch_dropped_file(&(sapp_html5_fetch_request) {
.buffer = (sapp_range) { .ptr = buffer, .size = size },
.dropped_file_index = i,
.callback = gs_import_file,
.user_data = userdata
});
}
break;
case SAPP_EVENTTYPE_RESIZED:
userdata->width = sapp_width();
userdata->height = sapp_height();
compute_mvp(userdata);
break;
case SAPP_EVENTTYPE_MOUSE_DOWN: case SAPP_EVENTTYPE_MOUSE_DOWN:
if(event->modifiers & SAPP_MODIFIER_RMB) handle_mouse_down(ud, event);
{
userdata->dragger.dragging = true;
userdata->dragger.origin_x = event->mouse_x;
userdata->dragger.origin_y = event->mouse_y;
}
break; break;
case SAPP_EVENTTYPE_MOUSE_UP: case SAPP_EVENTTYPE_MOUSE_UP:
userdata->dragger.dragging = false; handle_mouse_up(ud, event);
break; break;
case SAPP_EVENTTYPE_MOUSE_MOVE: case SAPP_EVENTTYPE_MOUSE_MOVE:
if(userdata->dragger.dragging) handle_mouse_move(ud, event);
{
userdata->pan.x += event->mouse_dx;
userdata->pan.y -= event->mouse_dy;
compute_mvp(userdata);
}
break; break;
case SAPP_EVENTTYPE_MOUSE_SCROLL: case SAPP_EVENTTYPE_MOUSE_SCROLL:
if((userdata->zoom >= 6.0f && event->scroll_y > 0.0f) || (userdata->zoom <= 0.1f && event->scroll_y < 0.0f)) handle_scroll_zoom(ud, event);
return;
const float diff = expf(event->scroll_y * 0.01f);
userdata->zoom = _sg_clamp(userdata->zoom * diff, 0.1f, 6.0f);
compute_mvp(userdata);
break; break;
default: default:
break; break;
@@ -311,4 +503,4 @@ sapp_desc sokol_main(int argc, char* argv[])
.free_fn = smemtrack_free, .free_fn = smemtrack_free,
}, },
}; };
} }

View File

@@ -1,49 +0,0 @@
#ifndef MATH_IMPL_H
#define MATH_IMPL_H
#include "api.h"
#define PI 3.14159265358979323846
#define PI32 3.14159265359f
typedef struct vec2f {
float x, y;
} vec2f;
typedef struct vec3f {
float x, y, z;
} vec3f;
typedef struct vec4f {
float x, y, z, w;
} vec4f;
typedef struct vec2u {
uint32_t x, y;
} vec2u;
typedef struct vec3u {
uint32_t x, y, z;
} vec3u;
typedef struct vec4u {
uint32_t x, y, z, w;
} vec4u;
typedef struct vec2 {
int32_t x, y;
} vec2;
typedef struct vec3 {
int32_t x, y, z;
} vec3;
typedef struct vec4 {
int32_t x, y, z, w;
} vec4;
typedef struct mat4x4f {
float e[4][4];
} mat4x4f;
typedef struct mat4x4u {
uint32_t e[4][4];
} mat4x4u;
typedef union mat4x4 {
int32_t e[4][4];
} mat4x4;
#endif

247
src/overlay.h Normal file
View File

@@ -0,0 +1,247 @@
#ifndef OVERLAY_H
#define OVERLAY_H
#include "api.h"
#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,
bool *has_overlay, bool *show_handle)
{
*has_overlay = false;
*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);
screen_to_world(&ud->camera, ud->interact.select.current_x, ud->interact.select.current_y, &wx2, &wy2);
float x1 = fminf(wx1, wx2), y1 = fminf(wy1, wy2);
float x2 = fmaxf(wx1, wx2), y2 = fmaxf(wy1, wy2);
overlay_verts[0] = (shape_vertex_t){x1, y1};
overlay_verts[1] = (shape_vertex_t){x2, y1};
overlay_verts[2] = (shape_vertex_t){x2, y2};
overlay_verts[3] = (shape_vertex_t){x1, y2};
overlay_verts[4] = (shape_vertex_t){x1, y1};
*has_overlay = true;
} else if (ud->interact.selected_count >= 1) {
if (ud->interact.move.dragging && ud->interact.aabb_cached) {
float dx = ud->interact.move.total_dx;
float dy = ud->interact.move.total_dy;
float omin_x = ud->interact.cached_aabb[0] + dx;
float omin_y = ud->interact.cached_aabb[1] + dy;
float omax_x = ud->interact.cached_aabb[2] + dx;
float omax_y = ud->interact.cached_aabb[3] + dy;
float pad = 8.0f / ud->camera.zoom;
overlay_verts[0] = (shape_vertex_t){omin_x - pad, omin_y - pad};
overlay_verts[1] = (shape_vertex_t){omax_x + pad, omin_y - pad};
overlay_verts[2] = (shape_vertex_t){omax_x + pad, omax_y + pad};
overlay_verts[3] = (shape_vertex_t){omin_x - pad, omax_y + pad};
overlay_verts[4] = overlay_verts[0];
*sel_cx = (omin_x + omax_x) * 0.5f;
*sel_cy = (omin_y + omax_y) * 0.5f;
*sel_hw = (omax_x - omin_x) * 0.5f + pad;
*sel_hh = (omax_y - omin_y) * 0.5f + pad;
*sel_angle = 0;
*has_overlay = true;
} else if (ud->interact.selected_count == 1) {
for (int i = 0; i < ud->shapes.count; i++) {
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
if (!s->selected) continue;
*sel_cx = s->cx; *sel_cy = s->cy;
*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;
overlay_verts[0] = (shape_vertex_t){x1, y1};
overlay_verts[1] = (shape_vertex_t){x2, y1};
overlay_verts[2] = (shape_vertex_t){x2, y2};
overlay_verts[3] = (shape_vertex_t){x1, y2};
overlay_verts[4] = overlay_verts[0];
break;
}
} else {
float omin[2], omax[2];
float sum_sin = 0, sum_cos = 0;
for (int i = 0; i < ud->shapes.count; i++) {
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
if (!s->selected) continue;
sum_sin += s->sin_r;
sum_cos += s->cos_r;
}
selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]);
ud->interact.cached_aabb[0] = omin[0]; ud->interact.cached_aabb[1] = omin[1];
ud->interact.cached_aabb[2] = omax[0]; ud->interact.cached_aabb[3] = omax[1];
ud->interact.aabb_cached = true;
float pad = 8.0f / ud->camera.zoom;
omin[0] -= pad; omin[1] -= pad;
omax[0] += pad; omax[1] += pad;
*sel_cx = (omin[0] + omax[0]) * 0.5f;
*sel_cy = (omin[1] + omax[1]) * 0.5f;
*sel_hw = (omax[0] - omin[0]) * 0.5f;
*sel_hh = (omax[1] - omin[1]) * 0.5f;
*sel_angle = atan2f(sum_sin, sum_cos);
overlay_verts[0] = (shape_vertex_t){omin[0], omin[1]};
overlay_verts[1] = (shape_vertex_t){omax[0], omin[1]};
overlay_verts[2] = (shape_vertex_t){omax[0], omax[1]};
overlay_verts[3] = (shape_vertex_t){omin[0], omax[1]};
overlay_verts[4] = overlay_verts[0];
}
*has_overlay = true;
}
*show_handle = ud->interact.selected_count > 0 && !ud->interact.select.active;
}
static void upload_overlay_buffers(userdata_t *ud,
const shape_vertex_t overlay_verts[5],
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.rect ||
ud->interact.move.dragging || ud->interact.rotate.dragging ||
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)});
}
if (show_handle) {
float pad = HANDLE_OFFSET_PX / ud->camera.zoom;
float radius = sqrtf(sel_hw * sel_hw + sel_hh * sel_hh) + pad;
ud->interact.rotate.center_x = sel_cx;
ud->interact.rotate.center_y = sel_cy;
ud->interact.rotate.handle_radius = radius;
const int n = HANDLE_CIRCLE_SEGMENTS + 1;
static shape_vertex_t unit_circle[HANDLE_CIRCLE_SEGMENTS + 1];
static bool unit_circle_ready = false;
if (!unit_circle_ready) {
for (int i = 0; i < n; i++) {
float a = (float)i / (float)HANDLE_CIRCLE_SEGMENTS * 2.0f * GLM_PIf;
unit_circle[i] = (shape_vertex_t){cosf(a), sinf(a)};
}
unit_circle_ready = true;
}
shape_vertex_t hv[HANDLE_CIRCLE_SEGMENTS + 1];
for (int i = 0; i < n; i++) {
hv[i] = (shape_vertex_t){sel_cx + unit_circle[i].x * radius,
sel_cy + unit_circle[i].y * radius};
}
if (need_upload)
sg_update_buffer(ud->handle_vbuf, &(sg_range){hv, sizeof(hv)});
{
float hs = CORNER_SIZE_PX / ud->camera.zoom * 0.5f;
float mid_x = (overlay_verts[0].x + overlay_verts[1].x) * 0.5f;
float mid_y = (overlay_verts[0].y + overlay_verts[2].y) * 0.5f;
float handles[8][2] = {
{overlay_verts[0].x, overlay_verts[0].y},
{mid_x, overlay_verts[0].y},
{overlay_verts[1].x, overlay_verts[1].y},
{overlay_verts[1].x, mid_y },
{overlay_verts[2].x, overlay_verts[2].y},
{mid_x, overlay_verts[2].y},
{overlay_verts[3].x, overlay_verts[3].y},
{overlay_verts[3].x, mid_y },
};
shape_vertex_t cv[40];
for (int h = 0; h < 8; h++) {
float cx = handles[h][0], cy = handles[h][1];
cv[h*5+0] = (shape_vertex_t){cx - hs, cy - hs};
cv[h*5+1] = (shape_vertex_t){cx + hs, cy - hs};
cv[h*5+2] = (shape_vertex_t){cx + hs, cy + hs};
cv[h*5+3] = (shape_vertex_t){cx - hs, cy + hs};
cv[h*5+4] = (shape_vertex_t){cx - hs, cy - hs};
}
if (need_upload)
sg_update_buffer(ud->corner_vbuf, &(sg_range){cv, sizeof(cv)});
}
}
// Edit mode overlay
if (ud->interact.editing_shape_idx >= 0 && need_upload) {
shape_t *es = (shape_t*) vec_get(&ud->shapes, ud->interact.editing_shape_idx);
int n = es->ctrl_count;
if (n > 128) n = 128;
float as = EDIT_ANCHOR_SIZE_PX / ud->camera.zoom * 0.5f;
float hs = EDIT_HANDLE_SIZE_PX / ud->camera.zoom * 0.5f;
int ac = 0, hc = 0, lc = 0;
for (int i = 0; i < n; i++) {
shape_vertex_t wa = local_to_world(es, es->ctrl_points[i].x, es->ctrl_points[i].y);
// Anchor square
int ba = ac * 5;
ed_anchor_verts[ba+0] = (shape_vertex_t){wa.x - as, wa.y - as};
ed_anchor_verts[ba+1] = (shape_vertex_t){wa.x + as, wa.y - as};
ed_anchor_verts[ba+2] = (shape_vertex_t){wa.x + as, wa.y + as};
ed_anchor_verts[ba+3] = (shape_vertex_t){wa.x - as, wa.y + as};
ed_anchor_verts[ba+4] = (shape_vertex_t){wa.x - as, wa.y - as};
ac++;
shape_vertex_t wh_in = local_to_world(es, es->ctrl_handle_in[i].x, es->ctrl_handle_in[i].y);
shape_vertex_t wh_out = local_to_world(es, es->ctrl_handle_out[i].x, es->ctrl_handle_out[i].y);
// Handle line: anchor → in handle
ed_line_verts[lc++] = wa;
ed_line_verts[lc++] = wh_in;
// Handle line: anchor → out handle
ed_line_verts[lc++] = wa;
ed_line_verts[lc++] = wh_out;
// In handle square
int bh = hc * 5;
ed_handle_verts[bh+0] = (shape_vertex_t){wh_in.x - hs, wh_in.y - hs};
ed_handle_verts[bh+1] = (shape_vertex_t){wh_in.x + hs, wh_in.y - hs};
ed_handle_verts[bh+2] = (shape_vertex_t){wh_in.x + hs, wh_in.y + hs};
ed_handle_verts[bh+3] = (shape_vertex_t){wh_in.x - hs, wh_in.y + hs};
ed_handle_verts[bh+4] = (shape_vertex_t){wh_in.x - hs, wh_in.y - hs};
hc++;
// Out handle square
bh = hc * 5;
ed_handle_verts[bh+0] = (shape_vertex_t){wh_out.x - hs, wh_out.y - hs};
ed_handle_verts[bh+1] = (shape_vertex_t){wh_out.x + hs, wh_out.y - hs};
ed_handle_verts[bh+2] = (shape_vertex_t){wh_out.x + hs, wh_out.y + hs};
ed_handle_verts[bh+3] = (shape_vertex_t){wh_out.x - hs, wh_out.y + hs};
ed_handle_verts[bh+4] = (shape_vertex_t){wh_out.x - hs, wh_out.y - hs};
hc++;
}
ud->ed_anchor_count = ac;
ud->ed_handle_count = hc;
ud->ed_handle_line_count = lc;
if (ac > 0)
sg_update_buffer(ud->ed_anchor_vbuf, &(sg_range){ed_anchor_verts, (size_t)(ac * 5) * sizeof(shape_vertex_t)});
if (hc > 0)
sg_update_buffer(ud->ed_handle_vbuf, &(sg_range){ed_handle_verts, (size_t)(hc * 5) * sizeof(shape_vertex_t)});
if (lc > 0)
sg_update_buffer(ud->ed_handle_line_vbuf, &(sg_range){ed_line_verts, (size_t)lc * sizeof(shape_vertex_t)});
}
memset(&ud->overlay_upload, 0, sizeof(ud->overlay_upload));
}
#endif

View File

@@ -3,64 +3,114 @@
#include "api.h" #include "api.h"
static uint32_t seed; typedef struct {
uint32_t seed;
} rand_ctx_t;
static uint32_t xorshift32(void); static uint32_t xorshift32(rand_ctx_t *r);
static void rand_seed(uint32_t _seed); static void rand_seed(rand_ctx_t *r, uint32_t _seed);
static uint32_t next_int(void); static uint32_t next_int(rand_ctx_t *r);
static uint32_t next_int_max(uint32_t max); static uint32_t next_int_max(rand_ctx_t *r, uint32_t max);
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);
static float next_float(void); static float next_float(rand_ctx_t *r);
static float next_float_max(float max); static float next_float_max(rand_ctx_t *r, float max);
static float next_float_minmax(float min, float max); static float next_float_minmax(rand_ctx_t *r, float min, float max);
static uint32_t xorshift32(void) /**
* Xorshift32 PRNG core. Advances the seed and returns the new value.
*
* @param r PRNG context
* @return pseudo-random 32-bit integer
*/
static uint32_t xorshift32(rand_ctx_t *r)
{ {
seed ^= seed<<13; r->seed ^= r->seed<<13;
seed ^= seed>>17; r->seed ^= r->seed>>17;
seed ^= seed<<5; r->seed ^= r->seed<<5;
return seed; return r->seed;
} }
static void rand_seed(uint32_t _seed) /**
* 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(rand_ctx_t *r, uint32_t _seed)
{ {
if(_seed == 0) if(_seed == 0)
return; return;
seed = _seed; r->seed = _seed;
xorshift32(); xorshift32(r);
} }
// PRNG [0-UINT32_MAX] /**
static uint32_t next_int(void) * Return a random integer in [0, UINT32_MAX].
*
* @param r PRNG context
* @return pseudo-random 32-bit integer
*/
static uint32_t next_int(rand_ctx_t *r)
{ {
return xorshift32(); return xorshift32(r);
} }
// PRNG [0-max] /**
static uint32_t next_int_max(uint32_t max) * 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(rand_ctx_t *r, uint32_t max)
{ {
return (uint32_t) floorf(xorshift32() / (float) UINT32_MAX * max); return (uint32_t)((double)xorshift32(r) / (double)UINT32_MAX * max);
} }
// PRNG [min-max] /**
static uint32_t next_int_minmax(uint32_t min, uint32_t 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(rand_ctx_t *r, uint32_t min, uint32_t max)
{ {
const float x = (float) xorshift32() / UINT32_MAX; const double x = (double)xorshift32(r) / (double)UINT32_MAX;
//(1.0f - Time) * A + Time * B return (uint32_t)((1.0 - x) * min + x * max);
return (1.0f - x) * min + x * max;
} }
// PRNG [0-1] /**
static float next_float(void) * Return a random float in [0, 1].
*
* @param r PRNG context
* @return pseudo-random float
*/
static float next_float(rand_ctx_t *r)
{ {
return (float) xorshift32() / UINT32_MAX; return (float)((double)xorshift32(r) / (double)UINT32_MAX);
} }
// PRNG [0-max] /**
static float next_float_max(float 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(rand_ctx_t *r, float max)
{ {
return (float) xorshift32() / UINT32_MAX * max; return (float)((double)xorshift32(r) / (double)UINT32_MAX * max);
} }
// PRNG [min-max] /**
static float next_float_minmax(float min, float 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(rand_ctx_t *r, float min, float max)
{ {
const float x = (float) xorshift32() / UINT32_MAX; const double x = (double)xorshift32(r) / (double)UINT32_MAX;
return (1.0f - x) * min + x * max; return (float)((1.0 - x) * min + x * max);
} }
#endif #endif

133
src/render.h Normal file
View File

@@ -0,0 +1,133 @@
#ifndef RENDER_H
#define RENDER_H
#include "api.h"
// 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)
{
g_shape_frame_id++;
}
// 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)
p->overlay_shader = sg_make_shader(&(sg_shader_desc) {
.vertex_func = {
.source = (const char*) src_shaders_overlay_wgsl,
.entry = "vs_main",
},
.fragment_func = {
.source = (const char*) src_shaders_overlay_wgsl,
.entry = "fs_main",
},
.uniform_blocks = {
[0] = {
.size = sizeof(mat4),
.stage = SG_SHADERSTAGE_VERTEX,
.wgsl_group0_binding_n = 0,
},
[1] = {
.size = sizeof(shape_uniform_t),
.stage = SG_SHADERSTAGE_VERTEX,
.wgsl_group0_binding_n = 1,
},
},
.attrs = {
[0] = { .base_type = SG_SHADERATTRBASETYPE_FLOAT },
},
.label = "Overlay shader",
});
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");
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 = {
[0].format = SG_VERTEXFORMAT_FLOAT2,
},
.label = "Overlay pipeline",
});
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)
p->shape_shader = sg_make_shader(&(sg_shader_desc) {
.vertex_func = {
.source = (const char*) src_shaders_shape_wgsl,
.entry = "vs_main",
},
.fragment_func = {
.source = (const char*) src_shaders_shape_wgsl,
.entry = "fs_main",
},
.uniform_blocks = {
[0] = {
.size = 80,
.stage = SG_SHADERSTAGE_VERTEX,
.wgsl_group0_binding_n = 0,
},
},
.views = {
[0] = {
.storage_buffer = {
.stage = SG_SHADERSTAGE_VERTEX,
.readonly = true,
.wgsl_group1_binding_n = 0,
},
},
[1] = {
.storage_buffer = {
.stage = SG_SHADERSTAGE_VERTEX,
.readonly = true,
.wgsl_group1_binding_n = 1,
},
},
},
.attrs = {
[0] = { .base_type = SG_SHADERATTRBASETYPE_FLOAT },
},
.label = "Shape shader",
});
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");
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 = {
[0].format = SG_VERTEXFORMAT_FLOAT2,
},
.label = "Shape pipeline",
});
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(pipeline_ctx_t *p)
{
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

44
src/shaders/overlay.wgsl Normal file
View File

@@ -0,0 +1,44 @@
struct VsUniform {
mvp: mat4x4f,
};
struct ShapeUniform {
transform: mat4x4f,
state: u32,
};
struct VsIn {
@location(0) position: vec2f,
};
struct Vs2Fs {
@builtin(position) pos: vec4f,
@location(0) @interpolate(linear) color: vec4f,
};
struct FsOut {
@location(0) color: vec4f,
};
@binding(0) @group(0) var<uniform> vs_uniforms: VsUniform;
@binding(1) @group(0) var<uniform> shape_uniform: ShapeUniform;
@vertex fn vs_main(input: VsIn) -> Vs2Fs {
var output: Vs2Fs;
let world_pos = shape_uniform.transform * vec4f(input.position.x, input.position.y, 0.0, 1.0);
output.pos = vs_uniforms.mvp * world_pos;
if (shape_uniform.state == 2u) {
output.color = vec4f(1.0, 0.84, 0.0, 1.0);
} else if (shape_uniform.state == 1u) {
output.color = vec4f(0.5, 0.6, 1.0, 1.0);
} else {
output.color = vec4f(0.8, 0.8, 0.8, 1.0);
}
return output;
}
@fragment fn fs_main(input: Vs2Fs) -> FsOut {
var output: FsOut;
output.color = input.color;
return output;
}

50
src/shaders/shape.wgsl Normal file
View File

@@ -0,0 +1,50 @@
struct VsUniform {
mvp: mat4x4f,
instance_base: u32,
};
struct ShapeData {
transform: mat4x4f,
state: u32,
};
@binding(0) @group(0) var<uniform> vs_uniforms: VsUniform;
@binding(0) @group(1) var<storage> shape_data: array<ShapeData>;
@binding(1) @group(1) var<storage> instance_map: array<u32>;
struct VsIn {
@builtin(instance_index) instance_idx: u32,
@location(0) position: vec2f,
};
struct Vs2Fs {
@builtin(position) pos: vec4f,
@location(0) @interpolate(linear) color: vec4f,
};
struct FsOut {
@location(0) color: vec4f,
};
@vertex fn vs_main(input: VsIn) -> Vs2Fs {
var output: Vs2Fs;
let shape_idx = instance_map[vs_uniforms.instance_base + input.instance_idx];
let shape = shape_data[shape_idx];
let world_pos = shape.transform * vec4f(input.position.x, input.position.y, 0.0, 1.0);
output.pos = vs_uniforms.mvp * world_pos;
if (shape.state == 2u) {
output.color = vec4f(1.0, 0.84, 0.0, 1.0);
} else if (shape.state == 1u) {
output.color = vec4f(0.5, 0.6, 1.0, 1.0);
} else {
output.color = vec4f(0.8, 0.8, 0.8, 1.0);
}
return output;
}
@fragment fn fs_main(input: Vs2Fs) -> FsOut {
var output: FsOut;
output.color = input.color;
return output;
}

View File

@@ -31,22 +31,6 @@ struct FsO { //Fragment shader output
return output; return output;
} }
// Convert a 32bit uint color (hex representation) into a normalized vec4f
/*fn convertColor(input: u32) -> vec4f {
let r: f32 = f32(((input >> 0) & 0xff));
let g: f32 = f32(((input >> 8) & 0xff));
let b: f32 = f32(((input >> 16) & 0xff));
let a: f32 = f32(((input >> 24) & 0xff));
return vec4f(r / 255, g / 255, b / 255, a / 255);
}*/
// Get the texture array index from the UV
/*fn indexFromCoord(uv: vec2f, width: u32, height: u32) -> u32 {
let x: u32 = clamp(floor(uv.x * f32(width)), 0, width);
let y: u32 = clamp(floor(uv.y * f32(height)), 0, height);
return y * width + x;
}*/
@fragment fn fs_main(input: Vs2Fs) -> FsO { @fragment fn fs_main(input: Vs2Fs) -> FsO {
var output: FsO; var output: FsO;

1014
src/shape.h Normal file

File diff suppressed because it is too large Load Diff

341
src/spatial.h Normal file
View File

@@ -0,0 +1,341 @@
#ifndef SPATIAL_H
#define SPATIAL_H
#include "api.h"
#define SPATIAL_CELL_SIZE 250.0f
#define SPATIAL_HASH_BITS 16
#define SPATIAL_HASH_SIZE (1 << SPATIAL_HASH_BITS)
#define SPATIAL_MAX_CELLS_PER_SHAPE 64
typedef struct {
int shape_idx;
float min_x, min_y, max_x, max_y;
} spatial_entry_t;
typedef struct {
bool occupied;
int cx, cy;
spatial_entry_t *entries;
int count;
int capacity;
} spatial_slot_t;
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;
static int spatial_hash(int cx, int cy)
{
return (cx * 73856093) ^ (cy * 19349663);
}
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)
{
grid->dirty = true;
}
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;
grid->dirty = false;
int n = shapes->count;
// Phase 0: clear occupied flags
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
grid->slots[i].occupied = false;
grid->slots[i].count = 0;
}
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 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++;
}
}
// Phase 2: resize entry arrays when needed
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
if (!grid->slots[i].occupied) continue;
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)need * sizeof(spatial_entry_t));
grid->slots[i].capacity = need;
}
grid->slots[i].count = 0;
}
// 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);
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 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;
}
}
}
// 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 cx = (int)floorf(wx / SPATIAL_CELL_SIZE);
int cy = (int)floorf(wy / SPATIAL_CELL_SIZE);
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 (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);
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)
{
int n = shapes->count;
for (int i = 0; i < n; i++) {
((shape_t*)vec_get(shapes, i))->selected = false;
}
int selected_count = 0;
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 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;
if (entry->max_x < min_x || entry->min_x > max_x ||
entry->max_y < min_y || entry->min_y > max_y)
continue;
shape_t *shape = (shape_t*)vec_get(shapes, entry->shape_idx);
bool hit = (shape->cx >= min_x && shape->cx <= max_x &&
shape->cy >= min_y && shape->cy <= max_y);
float sx_cos = shape->sx * shape->cos_r;
float sy_sin = shape->sy * shape->sin_r;
float sx_sin = shape->sx * shape->sin_r;
float sy_cos = shape->sy * shape->cos_r;
for (uint32_t v = 0; !hit && v < shape->num_verts; v++) {
float wx = shape->cx + shape->verts[v].x * sx_cos - shape->verts[v].y * sy_sin;
float wy = shape->cy + shape->verts[v].x * sx_sin + shape->verts[v].y * sy_cos;
if (wx >= min_x && wx <= max_x &&
wy >= min_y && wy <= max_y)
hit = true;
}
if (hit) { shape->selected = true; selected_count++; }
}
}
return selected_count;
}
for (int cy = cell_min_y; cy <= cell_max_y; cy++) {
for (int cx = cell_min_x; cx <= cell_max_x; cx++) {
int idx = spatial_hash(cx, cy) & (SPATIAL_HASH_SIZE - 1);
int probe_start = idx;
do {
if (!grid->slots[idx].occupied) break;
if (grid->slots[idx].cx == cx && grid->slots[idx].cy == cy) {
for (int e = 0; e < grid->slots[idx].count; e++) {
spatial_entry_t *entry = &grid->slots[idx].entries[e];
if (grid->visited[entry->shape_idx] == frame) continue;
grid->visited[entry->shape_idx] = frame;
if (entry->max_x < min_x || entry->min_x > max_x ||
entry->max_y < min_y || entry->min_y > max_y)
continue;
shape_t *shape = (shape_t*)vec_get(shapes, entry->shape_idx);
bool hit = (shape->cx >= min_x && shape->cx <= max_x &&
shape->cy >= min_y && shape->cy <= max_y);
float sc = shape->cos_r, ss = shape->sin_r;
for (uint32_t v = 0; !hit && v < shape->num_verts; v++) {
float lx = shape->verts[v].x * shape->sx;
float ly = shape->verts[v].y * shape->sy;
float wx = shape->cx + lx * sc - ly * ss;
float wy = shape->cy + lx * ss + ly * sc;
if (wx >= min_x && wx <= max_x &&
wy >= min_y && wy <= max_y)
hit = true;
}
if (hit) { shape->selected = true; selected_count++; }
}
break;
}
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
} while (idx != probe_start);
}
}
return selected_count;
}
static int spatial_query_viewport(spatial_grid_t *grid,
float min_x, float min_y, float max_x, float max_y,
int *out_indices, int max_out)
{
grid->query_frame++;
int frame = grid->query_frame;
int cell_min_x = (int)floorf(min_x / SPATIAL_CELL_SIZE);
int cell_max_x = (int)floorf(max_x / SPATIAL_CELL_SIZE);
int cell_min_y = (int)floorf(min_y / SPATIAL_CELL_SIZE);
int cell_max_y = (int)floorf(max_y / SPATIAL_CELL_SIZE);
int count = 0;
for (int cy = cell_min_y; cy <= cell_max_y && count < max_out; cy++) {
for (int cx = cell_min_x; cx <= cell_max_x && count < max_out; cx++) {
int idx = spatial_hash(cx, cy) & (SPATIAL_HASH_SIZE - 1);
int probe_start = idx;
do {
if (!grid->slots[idx].occupied) break;
if (grid->slots[idx].cx == cx && grid->slots[idx].cy == cy) {
for (int e = 0; e < grid->slots[idx].count && count < max_out; e++) {
spatial_entry_t *entry = &grid->slots[idx].entries[e];
if (grid->visited[entry->shape_idx] == frame) continue;
grid->visited[entry->shape_idx] = frame;
if (entry->max_x < min_x || entry->min_x > max_x ||
entry->max_y < min_y || entry->min_y > max_y)
continue;
out_indices[count++] = entry->shape_idx;
}
break;
}
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
} while (idx != probe_start);
}
}
return count;
}
#endif

View File

@@ -1,79 +0,0 @@
#ifndef SPRITE_H
#define SPRITE_H
#include "api.h"
#define KB (1024u)
#define MB (1024u * KB)
#define GB (1024u * MB)
#define MAX_FILE_SIZE (10u * MB)
typedef struct texture_t {
uint32_t id; //Texture ID
sg_bindings binding; //Texture bindings (texture, sampler and buffer)
vector_t sprites;
bool dirty;
} texture_t;
typedef struct sprite_t {
mat4x4f transform;
} sprite_t;
typedef struct manager_t {
vector_t textures;
} manager_t;
typedef struct loader_t {
uint8_t buffer[MAX_FILE_SIZE];
} loader_t;
typedef void (*ForEachTexture)(texture_t *texture, void *userdata);
typedef void (*ForEachSprite)(sprite_t *sprite, void *userdata);
void gs_init(manager_t *manager);
void gs_shutdown(manager_t *manager);
void gs_import_file(const sapp_html5_fetch_response *response);
void gs_each_textures(manager_t *manager, ForEachTexture callback, void *userdata);
void gs_each_sprites(manager_t *manager, ForEachSprite callback, void *userdata);
void gs_init(manager_t *manager)
{
manager->textures = vector_create(sizeof(texture_t));
}
void gs_shutdown(manager_t *manager)
{
vector_free(&manager->textures);
}
void gs_import_file(const sapp_html5_fetch_response *response)
{
const char* filename = sapp_get_dropped_file_path(response->file_index);
if(response->succeeded)
{
}
else
{
//Toast error
}
}
void gs_each_textures(manager_t *manager, ForEachTexture callback, void *userdata)
{
for(uint32_t i = 0; i < manager->textures.size; i++)
callback((texture_t*) manager->textures.data[i], userdata);
}
void gs_each_sprites(manager_t *manager, ForEachSprite callback, void *userdata)
{
for(uint32_t i = 0; i < manager->textures.size; i++)
{
const texture_t* texture = (texture_t*) manager->textures.data[i];
for(uint32_t j = 0; j < texture->sprites.size; j++)
{
callback((sprite_t*) texture->sprites.data[j], userdata);
}
}
}
#endif

202
src/types.h Normal file
View File

@@ -0,0 +1,202 @@
#ifndef TYPES_H
#define TYPES_H
#include "api.h"
#define LOG_RING_SIZE 256
#define HANDLE_OFFSET_PX 0.0f
#define HANDLE_RADIUS_PX 12.0f
#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,
TOOL_PEN,
TOOL_CIRCLE,
TOOL_RECTANGLE,
TOOL_COUNT
} tool_t;
typedef struct log_entry_t {
char text[256];
uint32_t level;
uint64_t hash;
} log_entry_t;
typedef struct {
mat4 mvp;
} uniform_t;
typedef struct renderer_t {
sg_pipeline pipeline;
sg_shader shader;
sg_pass_action clear_pass;
uniform_t uniform;
} renderer_t;
typedef struct {
int idx;
float init_sx, init_sy, init_cx, init_cy;
float ext_x, ext_y;
float lpi_x, lpi_y;
} resize_init_t;
typedef struct {
bool active;
float start_x, start_y;
float current_x, current_y;
bool dragging;
int clicked_shape;
} select_state_t;
typedef struct {
bool dragging;
float start_wx, start_wy;
float total_dx, total_dy;
} move_state_t;
typedef struct {
bool dragging;
float center_x, center_y;
float start_angle;
float total_delta;
float handle_radius;
} rotate_state_t;
typedef struct {
bool dragging;
float pivot_x, pivot_y;
float start_wx, start_wy;
float total_scale_x, total_scale_y;
float mask_x, mask_y;
float angle;
resize_init_t *init;
int init_count;
} resize_state_t;
typedef struct {
int selected_count;
int hovered_shape;
select_state_t select;
move_state_t move;
rotate_state_t rotate;
resize_state_t resize;
float cached_aabb[4];
bool aabb_cached;
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 {
float fps_immediate;
float fps_average;
float frame_times[60];
int frame_time_head;
int frame_time_count;
float frame_time_sum;
} debug_stats_t;
typedef struct {
float right_panel_w;
float left_panel_w;
log_entry_t log_ring[LOG_RING_SIZE];
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;
} 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;
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

503
src/ui_panels.h Normal file
View File

@@ -0,0 +1,503 @@
#ifndef UI_PANELS_H
#define UI_PANELS_H
#include "api.h"
#include "types.h"
#include "interact.h"
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)
{
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 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) {
for (int i = 0; i < shapes->count; i++) {
if (((shape_t*) vec_get(shapes, i))->group_id == 0)
display[(*dlen)++] = i;
}
}
}
// 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 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)
{
int n = ud->shapes.count;
bool ctrl = igGetIO_Nil()->KeyCtrl;
bool shift = igGetIO_Nil()->KeyShift && ud->ui.list_last_shape >= 0;
if (shift) {
int from = ud->ui.list_last_shape;
int to = display_pos;
if (from > to) { int tmp = from; from = to; to = tmp; }
for (int j = 0; j < n; j++)
((shape_t*) vec_get(&ud->shapes, j))->selected = false;
ud->interact.selected_count = 0;
for (int d = from; d <= to; d++) {
shape_t *sv = (shape_t*) vec_get(&ud->shapes, display[d]);
sv->selected = true;
ud->interact.selected_count++;
}
} else if (ctrl) {
s->selected = !s->selected;
ud->interact.selected_count += s->selected ? 1 : -1;
} else {
for (int j = 0; j < n; j++)
((shape_t*) vec_get(&ud->shapes, j))->selected = false;
ud->interact.selected_count = 0;
s->selected = true;
ud->interact.selected_count = 1;
}
ud->ui.list_last_shape = display_pos;
ud->interact.aabb_cached = false;
overlay_invalidate(ud);
update_shape_states(ud);
}
// 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);
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;
bool visible = (*item_idx >= first_visible && *item_idx < last_visible);
(*item_idx)++;
if (visible) {
// --- VISIBLE GROUP HEADER ---
ImGuiID storage_id = (ImGuiID)gid;
igSetNextItemStorageID(storage_id);
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;
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);
}
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)++;
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;
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();
}
}
}
}
// 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 (has_groups && s->group_id != 0) continue;
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++;
}
}
return pos;
}
static void draw_top_panel(userdata_t *ud)
{
igSetNextWindowPos((ImVec2){0, 0}, ImGuiCond_Always, (ImVec2){0, 0});
igSetNextWindowSize((ImVec2){ud->camera.width, TOP_PANEL_H}, ImGuiCond_Always);
igBegin("Toolbar", NULL,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoCollapse);
for (int t = 0; t < TOOL_COUNT; t++) {
const char *label = NULL;
switch (t) {
case TOOL_SELECT: label = "Sel"; break;
case TOOL_PEN: label = "Pen"; break;
case TOOL_CIRCLE: label = "Circle"; break;
case TOOL_RECTANGLE: label = "Rect"; break;
default: break;
}
if (t > 0) igSameLine(0.0f, 4.0f);
bool active = (ud->ui.active_tool == t);
if (active) {
igPushStyleColor_Vec4(ImGuiCol_Button, (ImVec4){0.3f, 0.5f, 0.8f, 1.0f});
igPushStyleColor_Vec4(ImGuiCol_ButtonHovered, (ImVec4){0.4f, 0.6f, 0.9f, 1.0f});
}
if (igButton(label, (ImVec2){0, 0})) {
tool_t new_tool = (tool_t)t;
if (new_tool != TOOL_SELECT && new_tool != ud->ui.active_tool) {
for (int i = 0; i < ud->shapes.count; i++) {
((shape_t*)vec_get(&ud->shapes, i))->selected = false;
}
ud->interact.selected_count = 0;
overlay_invalidate(ud);
update_shape_states(ud);
}
ud->ui.active_tool = new_tool;
}
if (active)
igPopStyleColor(2);
}
igSameLine(0.0f, 16.0f);
if (igButton("Undo", (ImVec2){0, 0})) {
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, &ud->shape_pool,
&ud->groups, &ud->group_idx)) {
group_rebuild_members(&ud->group_idx, &ud->groups, &ud->shapes);
interact_structural_change(ud);
}
}
igEnd();
}
static void draw_shape_list_panel(userdata_t *ud)
{
igSetNextWindowPos((ImVec2){0, TOP_PANEL_H}, ImGuiCond_Always, (ImVec2){0, 0});
igSetNextWindowSize((ImVec2){ud->ui.left_panel_w, ud->camera.height - TOP_PANEL_H}, ImGuiCond_Always);
igBegin("Shapes", NULL,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
int n = ud->shapes.count;
if (n == 0) {
igText("No shapes");
igEnd();
return;
}
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;
// 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});
igEndChild();
igEnd();
}
static void draw_properties_panel(userdata_t *ud)
{
igSetNextWindowPos((ImVec2){ud->camera.width - ud->ui.right_panel_w, TOP_PANEL_H}, ImGuiCond_Always, (ImVec2){0, 0});
igSetNextWindowSize((ImVec2){ud->ui.right_panel_w, ud->camera.height - TOP_PANEL_H}, ImGuiCond_Always);
igBegin("Properties", NULL,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
if (ud->interact.selected_count == 0) {
igText("No shape selected");
} else if (ud->interact.selected_count > 1) {
int common_gid = -1;
bool same_group = true;
for (int i = 0; i < ud->shapes.count; i++) {
shape_t *s = (shape_t*) vec_get(&ud->shapes, i);
if (!s->selected) continue;
if (common_gid == -1) common_gid = s->group_id;
else if (s->group_id != common_gid) { same_group = false; break; }
}
if (same_group && common_gid != 0) {
igText("Group %d — %d shapes", common_gid, ud->interact.selected_count);
} else {
igText("%d shapes selected", ud->interact.selected_count);
}
} else {
int idx = 0;
while (idx < ud->shapes.count) {
shape_t *tmp = (shape_t*) vec_get(&ud->shapes, idx);
if (tmp->selected) break;
idx++;
}
shape_t *s = (shape_t*) vec_get(&ud->shapes, idx);
if (s->group_id != 0) {
char path[256] = "";
int gid = s->group_id;
while (gid != 0) {
char seg[32];
snprintf(seg, sizeof(seg), "%d", gid);
if (path[0]) {
int plen = (int)strlen(path);
int slen = (int)strlen(seg);
memmove(path + slen + 3, path, (size_t)(plen + 1));
memcpy(path, seg, (size_t)slen);
path[slen] = ' '; path[slen + 1] = '>'; path[slen + 2] = ' ';
} else {
strcpy(path, seg);
}
group_t *g = find_group(&ud->group_idx, &ud->groups, gid);
gid = g ? g->parent_id : 0;
}
int members = 0;
for (int i = 0; i < ud->shapes.count; i++) {
if (((shape_t*) vec_get(&ud->shapes, i))->group_id == s->group_id) members++;
}
igText("Group %s — %d member%s", path, members, members > 1 ? "s" : "");
}
bool changed = false;
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, -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(&ud->shape_pool, s); spatial_mark_dirty(&ud->spatial_grid); overlay_invalidate(ud); }
igSeparator();
{
mat4 *m = &s->uniform.transform;
char dbg[512];
snprintf(dbg, sizeof(dbg),
"Transform Matrix:\n"
"[%+.3f %+.3f %+.3f %+.3f]\n"
"[%+.3f %+.3f %+.3f %+.3f]\n"
"[%+.3f %+.3f %+.3f %+.3f]\n"
"[%+.3f %+.3f %+.3f %+.3f]\n"
"\nLocal vert[0]: (%.1f, %.1f)\n"
"World vert[0]: (%.1f, %.1f)\n"
"cx=%.1f cy=%.1f sx=%.1f sy=%.1f rot=%.3f",
(*m)[0][0], (*m)[0][1], (*m)[0][2], (*m)[0][3],
(*m)[1][0], (*m)[1][1], (*m)[1][2], (*m)[1][3],
(*m)[2][0], (*m)[2][1], (*m)[2][2], (*m)[2][3],
(*m)[3][0], (*m)[3][1], (*m)[3][2], (*m)[3][3],
s->verts[0].x, s->verts[0].y,
s->cx + s->verts[0].x * s->sx * s->cos_r - s->verts[0].y * s->sy * s->sin_r,
s->cy + s->verts[0].x * s->sx * s->sin_r + s->verts[0].y * s->sy * s->cos_r,
s->cx, s->cy, s->sx, s->sy, s->rotation);
if (igButton("Copy Debug", (ImVec2){0, 0}))
sapp_set_clipboard_string(dbg);
}
}
igEnd();
if (ud->history.capturing && !igIsAnyItemActive()) {
history_end_edit(&ud->history, &ud->shapes);
}
}
static void draw_log_panel(userdata_t *ud)
{
if (!ud->ui.log_show) return;
igSetNextWindowPos((ImVec2){10.0f, ud->camera.height - 200.0f}, ImGuiCond_FirstUseEver, (ImVec2){0, 0});
igSetNextWindowSize((ImVec2){400.0f, 180.0f}, ImGuiCond_FirstUseEver);
igBegin("Log", &ud->ui.log_show, 0);
if (igButton("Clear", (ImVec2){0, 0})) {
ud->ui.log_head = 0;
ud->ui.log_count = 0;
}
igSameLine(0.0f, 10.0f);
if (igButton("Copy", (ImVec2){0, 0})) {
int total = ud->ui.log_count < LOG_RING_SIZE ? ud->ui.log_count : LOG_RING_SIZE;
int start = ud->ui.log_count < LOG_RING_SIZE ? 0 : ud->ui.log_head;
int cap = total * 260;
char *buf = (char*) ALLOC((size_t)cap);
int off = 0;
for (int i = 0; i < total; i++) {
int idx = (start + i) % LOG_RING_SIZE;
off += snprintf(buf + off, (size_t)(cap - off), "%s\n", ud->ui.log_ring[idx].text);
}
sapp_set_clipboard_string(buf);
FREE(buf);
}
igSameLine(0.0f, 10.0f);
igText("%d entries", ud->ui.log_count);
igSameLine(0.0f, 10.0f);
igText("FPS: %.0f (avg: %.0f)", ud->debug.fps_immediate, ud->debug.fps_average);
igSameLine(0.0f, 10.0f);
igText("%.3fms", sapp_frame_duration() * 1000);
igSeparator();
igBeginChild_Str("LogScroll", (ImVec2){0, 0}, false, 0);
int total = ud->ui.log_count < LOG_RING_SIZE ? ud->ui.log_count : LOG_RING_SIZE;
int start = ud->ui.log_count < LOG_RING_SIZE ? 0 : ud->ui.log_head;
for (int i = 0; i < total; i++) {
int idx = (start + i) % LOG_RING_SIZE;
log_entry_t *e = &ud->ui.log_ring[idx];
ImVec4 color;
switch (e->level) {
case 0: color = (ImVec4){1.0f, 0.3f, 0.3f, 1.0f}; break;
case 1: color = (ImVec4){1.0f, 0.5f, 0.3f, 1.0f}; break;
case 2: color = (ImVec4){1.0f, 0.9f, 0.3f, 1.0f}; break;
default:color = (ImVec4){0.7f, 0.7f, 0.7f, 1.0f}; break;
}
igPushStyleColor_Vec4(ImGuiCol_Text, color);
igTextUnformatted(e->text, NULL);
igPopStyleColor(1);
}
if (total > 0) igSetScrollHereY(1.0f);
igEndChild();
igEnd();
}
#endif

View File

@@ -1,241 +1,150 @@
#ifndef UTIL_H #ifndef UTIL_H
#define UTIL_H #define UTIL_H
#include "api.h" #include <stdint.h>
#include <string.h>
/*typedef struct linked_list_t {
linked_item_t *first, *last;
uint16_t size;
} linked_list_t;
typedef struct linked_item_t {
linked_item_t *next, *prev;
void *data;
} linked_item_t;
typedef void (*linked_list_callback)(void *item);
//Currently, these are the only method required.
//Many more could be implemented but this is unnecessary.
static void l_list_push(linked_list_t *l_list, void *item);
static void l_list_append(linked_list_t *l_list, void *item);
static void* l_list_pop(linked_list_t *l_list);
static void* l_list_unppend(linked_list_t *l_list);
static void l_list_each(linked_list_t *l_list, linked_list_callback callback);
static inline void l_list_push(linked_list_t *l_list, void *item)
{
linked_item_t l_item = (linked_item_t) { .data = item, .prev = l_list->last, .next = nullptr };
if(l_list->first == nullptr)
l_list->first = &l_item;
l_list->last->next = &l_item;
l_list->last = &l_item;
l_list->size++;
}
static inline void l_list_append(linked_list_t *l_list, void *item)
{
linked_item_t l_item = (linked_item_t) { .data = item, .prev = nullptr, .next = l_list->last };
if(l_list->last == nullptr)
l_list->last = &l_item;
l_list->first->prev = &l_item;
l_list->first = &l_item;
l_list->size++;
}
static inline void* l_list_pop(linked_list_t *l_list)
{
if(l_list->last == nullptr)
return;
if(l_list->first == l_list->last)
l_list->first = nullptr;
linked_item_t *item = l_list->last->prev;
l_list->last->prev = nullptr;
l_list->last = item;
l_list->size--;
}
static inline void* l_list_unppend(linked_list_t *l_list)
{
if(l_list->first == nullptr)
return;
if(l_list->last == l_list->first)
l_list->last = nullptr;
linked_item_t *item = l_list->first->next;
l_list->first->next = nullptr;
l_list->first = item;
l_list->size--;
}
static inline void l_list_each(linked_list_t *l_list, linked_list_callback callback)
{
}*/
typedef struct vector_t { typedef struct vector_t {
void **data; //Memory pool uint8_t *data;
uint16_t stripe; //Bit per item int count;
uint32_t size; //Current amount of items int capacity;
uint32_t capacity; //Max capacity int stride;
} vector_t; } vector_t;
#define MAX_BUCKET_SIZE (0xffffffffu) /**
#define MAX_STRIPE_SIZE (0xffffffu) * Zero-initialize a vector with the given element stride.
#define FIXED_START (0xfffu) *
* @param v vector to initialize
static vector_t vector_create(uint32_t stripe); //Create a new vector with a default size * @param stride byte size of each element
static void vector_clear(vector_t *vector); */
static void vector_free(vector_t *vector); static void vec_init(vector_t *v, int stride) {
memset(v, 0, sizeof(*v));
static uint32_t vector_length(vector_t *vector); v->stride = stride;
static void* vector_get(vector_t *vector, uint32_t index);
static void vector_set(vector_t *vector, uint32_t index, void *data);
static uint32_t vector_push(vector_t *vector, void *data);
static sg_range vector_range(vector_t *vector);
static vector_t vector_create(uint32_t stripe)
{
assert(stripe >= sizeof(uint32_t));
assert(stripe <= MAX_STRIPE_SIZE);
assert(FIXED_START * stripe <= MAX_BUCKET_SIZE);
return (vector_t) {
.capacity = FIXED_START,
.size = 0,
.stripe = stripe,
.data = (void**) malloc(FIXED_START * stripe),
};
}
static void vector_clear(vector_t *vector)
{
vector->size = 0;
}
static void vector_free(vector_t *vector)
{
free(vector->data);
} }
static uint32_t vector_length(vector_t *vector) /**
{ * Grow the vector's backing array to at least min_capacity elements.
return vector->size; * Doubles capacity (starting at 8) or uses min_capacity, whichever is larger.
} *
static void* vector_get(vector_t *vector, uint32_t index) * @param v vector to grow
{ * @param min_capacity minimum element count required
assert(index > 0); */
assert(index < vector->size); static void vec_grow(vector_t *v, int min_capacity) {
int new_cap = v->capacity ? v->capacity * 2 : 8;
return vector->data[index * vector->stripe]; if (new_cap < min_capacity) new_cap = min_capacity;
} uint8_t *new_data = (uint8_t*) ALLOC(new_cap * v->stride);
static void vector_set(vector_t *vector, uint32_t index, void *data) if (!new_data) {
{ EM_ASM({ console.error("vec_grow: ALLOC failed for %d elements of %d bytes", $0, $1); },
assert(index > 0); new_cap, v->stride);
assert(index < vector->size); return;
vector->data[index * vector->stripe] = data;
}
static uint32_t vector_push(vector_t *vector, void *data)
{
if(vector->size >= vector->capacity)
{
vector->capacity *= 2;
vector->data = (void**) realloc(vector->data, vector->capacity * vector->stripe);
} }
if (v->data) {
return vector->size++; memcpy(new_data, v->data, v->count * v->stride);
} FREE(v->data);
static sg_range vector_range(vector_t *vector)
{
return (sg_range) {
.ptr = vector->data,
.size = vector->size,
};
}
typedef struct mem_pool_t {
void **data; //Memory pool
uint32_t stripe; //Bit per item
uint32_t size; //Current amount of items
uint32_t capacity; //Max capacity
uint32_t free; //Linked list of available indices
} mem_pool_t;
static mem_pool_t pool_create_default(uint32_t stripe); //Create a new memory pool with a default size
static mem_pool_t pool_create(uint32_t stripe, uint32_t size); //Create a new memory pool with a default size
static void pool_clear(mem_pool_t *pool);
static void pool_free(mem_pool_t *pool);
static const uint32_t pool_add(mem_pool_t *pool); //Request a new free index
static const void* pool_get(mem_pool_t *pool, uint32_t index); //Get the pointer
static void pool_remove(mem_pool_t *pool, uint32_t index); //Flag the given index as free
static mem_pool_t pool_create_default(uint32_t stripe)
{
return pool_create(stripe, FIXED_START);
}
static mem_pool_t pool_create(uint32_t stripe, uint32_t size)
{
assert(stripe >= sizeof(uint32_t));
assert(stripe <= MAX_STRIPE_SIZE);
assert(size * stripe <= MAX_BUCKET_SIZE);
return (mem_pool_t) {
.capacity = size,
.size = 0,
.stripe = stripe,
.free = UINT32_MAX,
.data = (void**) malloc(size * stripe),
};
}
static void pool_clear(mem_pool_t *pool)
{
pool->size = 0;
pool->free = UINT32_MAX;
}
static void pool_free(mem_pool_t *pool)
{
free(pool->data);
}
static const uint32_t pool_add(mem_pool_t *pool)
{
if(pool->free != UINT32_MAX)
{
const uint32_t index = pool->free;
pool->free = (uint32_t) pool->data[index * pool->stripe];
return index;
} }
else v->data = new_data;
{ v->capacity = new_cap;
if(pool->size >= pool->capacity) }
{
pool->capacity *= 2;
pool->data = (void**) realloc(pool->data, pool->capacity * pool->stripe); /**
* Append an uninitialized element to the end of the vector. Grows if needed.
*
* @param v vector to push into
* @return pointer to the new (uninitialized) element
*/
static void *vec_push(vector_t *v) {
if (v->count >= v->capacity) vec_grow(v, v->count + 1);
return v->data + (v->count++) * v->stride;
}
/**
* Remove the last element from the vector (decrements count, no free).
*
* @param v vector to pop from
*/
static void vec_pop(vector_t *v) {
if (v->count > 0) v->count--;
}
static void vec_remove_ordered(vector_t *v, int index) {
if (index < 0 || index >= v->count) return;
if (index < v->count - 1) {
memmove(v->data + index * v->stride,
v->data + (index + 1) * v->stride,
(v->count - index - 1) * v->stride);
}
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;
} }
return pool->size++;
} }
} v->count -= count;
static const void* pool_get(mem_pool_t *pool, uint32_t index)
{
assert(index > 0);
assert(index < pool->capacity);
return pool->data[index * pool->stripe];
}
static void pool_remove(mem_pool_t *pool, uint32_t index)
{
const uint32_t pos = index * pool->stripe;
pool->data[pos] = (void*) pool->free;
pool->free = index;
} }
#endif 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);
if (index < v->count) {
memmove(v->data + (index + 1) * v->stride,
v->data + index * v->stride,
(v->count - index) * v->stride);
}
v->count++;
return v->data + index * v->stride;
}
/**
* Remove the element at index by swapping in the last element (O(1)).
* Order is not preserved.
*
* @param v vector to remove from
* @param index index of the element to remove
*/
static void vec_remove(vector_t *v, int index) {
if (index < 0 || index >= v->count) return;
if (index < v->count - 1) {
memcpy(v->data + index * v->stride,
v->data + (v->count - 1) * v->stride,
v->stride);
}
v->count--;
}
/**
* Return a pointer to the element at index (no bounds check).
*
* @param v vector to access
* @param index element index
* @return pointer to the element
*/
static void *vec_get(vector_t *v, int index) {
return v->data + index * v->stride;
}
/**
* Free the backing array and reset the vector to empty.
*
* @param v vector to free
*/
static void vec_free(vector_t *v) {
if (v->data) FREE(v->data);
v->data = NULL;
v->count = 0;
v->capacity = 0;
}
#endif