You've already forked flecs_tests
Compare commits
4 Commits
81616f8a5d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e3da1c424 | |||
| c4d657043c | |||
| e71641c094 | |||
| 9ce6e4accd |
11
CLAUDE.md
11
CLAUDE.md
@@ -9,6 +9,7 @@ A browser-based world map creation tool (like Wonderdraft/Inkarnate). C99 compil
|
||||
- **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`
|
||||
@@ -16,9 +17,13 @@ A browser-based world map creation tool (like Wonderdraft/Inkarnate). C99 compil
|
||||
- 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, input (zoom/pan/drag)
|
||||
- `src/api.h` — central include hub, backend defines
|
||||
- `src/sprite.h` — sprite batching, texture manager, file import stubs
|
||||
- `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
|
||||
|
||||
|
||||
84
README.md
84
README.md
@@ -1,18 +1,20 @@
|
||||
# Cartograph
|
||||
|
||||
A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. Uses shape-based terrain generation with procedural detail.
|
||||
A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. Uses line-strip vector shapes with procedural geometry.
|
||||
|
||||
## Features (planned)
|
||||
## Features
|
||||
|
||||
- **Shapes** — the fundamental building block. Each shape has:
|
||||
- **Height** — additive (raise terrain) or subtractive (lower terrain)
|
||||
- **Biome** — determines the sampled texture applied to the shape
|
||||
- **Intensity** — blending weight of the biome texture
|
||||
- **Roughness** — edge noise amount (voronoi-like jagged edges at maximum)
|
||||
- **Shape editing** — select, move, rotate, scale individual shapes
|
||||
- **Groups** — combine shapes into groups for batch operations
|
||||
- **Shake landmass** — procedurally decompose a shape into a more complex set of shapes
|
||||
- **Viewport** — zoom and pan across the map canvas
|
||||
- **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
|
||||
|
||||
@@ -23,7 +25,7 @@ A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. U
|
||||
| Graphics | [Sokol](https://github.com/floooh/sokol) with WebGPU backend |
|
||||
| UI | [Dear ImGui](https://github.com/ocornut/imgui) via [cimgui](https://github.com/cimgui/cimgui) |
|
||||
| Math | [cglm](https://github.com/recp/cglm) |
|
||||
| Shaders | WGSL (compiled to C headers via `xxd`) |
|
||||
| Shaders | WGSL (compiled to C headers via `xxd -i`) |
|
||||
|
||||
## Build
|
||||
|
||||
@@ -44,20 +46,62 @@ Output is `app.html`, served by Emscripten's built-in web server or any static s
|
||||
|
||||
```
|
||||
src/
|
||||
main.c Entry point, sokol init, render loop, input handling
|
||||
api.h Central include — all library headers and project-wide defines
|
||||
sprite.h Sprite batching, texture management, file import
|
||||
util.h Vector (dynamic array) and memory pool data structures
|
||||
rand.h Xorshift32 PRNG utilities
|
||||
shaders/ WGSL shader sources
|
||||
main.c Entry point, sokol init, render loop, input dispatch, UI panels, debug stats
|
||||
api.h Central include hub — backend defines, all library headers, ALLOC/FREE macros
|
||||
types.h Shared type definitions, constants, userdata_t
|
||||
camera.h Viewport state, zoom/pan, MVP matrix (via glm_ortho), screen↔world transforms
|
||||
render.h Shape & overlay pipeline init/shutdown, shader definitions
|
||||
shape.h Shape geometry types, procedural generation, Bezier editing, vertex hash grouping, instanced buffer pool
|
||||
spatial.h Spatial hash grid with linear probing, AABB queries, viewport culling
|
||||
history.h Undo/redo stack — property-level tracking, vertex snapshots, batch operations
|
||||
interact.h Selection AABB, group recursive helpers, resize handle hit-test, group rebuild
|
||||
overlay.h Selection overlay geometry, rotate/corner handles, edit-mode anchor & handle visualization
|
||||
draw.h Draw dispatch — frustum culling, instance map sorting, instanced draw calls
|
||||
input.h Mouse/keyboard event handlers — select, move, rotate, resize, pen, edit mode, clipboard
|
||||
ui_panels.h ImGui panels — toolbar, shape list tree, properties, debug log
|
||||
util.h Stripe-based vector_t (dynamic array)
|
||||
rand.h Xorshift32 PRNG
|
||||
shaders/ WGSL shader sources (sprite, shape, overlay)
|
||||
generated/ xxd-generated C headers from shaders
|
||||
lib/
|
||||
sokol/ Sokol single-file headers (gfx, app, glue, log)
|
||||
sokol/ Sokol single-file headers (gfx, app, glue, log, memtrack)
|
||||
imgui/ Dear ImGui + cimgui
|
||||
cglm/ C linear math library
|
||||
util/ Sokol utility headers (memtrack, imgui integration)
|
||||
util/ Sokol utility headers
|
||||
```
|
||||
|
||||
## Architecture notes
|
||||
|
||||
### Instanced rendering with vertex hash grouping
|
||||
|
||||
Shapes share vertex buffers when their local-space geometry is identical. Each shape stores a 64-bit FNV-1a hash of its vertex data. The geometry pool groups shapes by `(num_elements, vertex_hash)` — not just vertex count. This means:
|
||||
|
||||
- **Parametric shapes** (circles, stars, rectangles from fixed formulas): all instances of the same type naturally produce the same hash, so they share one vertex buffer regardless of count.
|
||||
- **Freeform paths** (pen tool): each path gets a unique hash, guaranteeing its own vertex buffer and preventing geometry corruption.
|
||||
- **Bezier edits**: `shape_regenerate_from_ctrl` updates the hash and modifies the group buffer in-place via `sg_update_buffer` rather than destroying/recreating it.
|
||||
|
||||
The previous implementation grouped only by vertex count, which caused pen-drawn paths with matching counts to silently share the wrong geometry.
|
||||
|
||||
### Lazy group index rebuild
|
||||
|
||||
The `g_group_by_id` lookup array is rebuilt lazily. Operations that modify groups set a `g_group_index_dirty` flag; the actual rebuild happens on the first `find_group()` call afterward. This avoids redundant rebuilds when multiple group operations occur within the same frame (e.g., undo then redo, or group then ungroup).
|
||||
|
||||
### Hover state optimization
|
||||
|
||||
`handle_hover` (called every frame) tracks the previous set of highlighted shapes (up to 64) and only toggles state on shapes entering or leaving the highlight set. The O(n) full-array sweep is only used as a fallback when the highlight set exceeds 64 shapes.
|
||||
|
||||
### Pool rebuild granularity
|
||||
|
||||
When the geometry pool rebuilds, existing group vertex buffers whose `(num_elements, vertex_hash)` key still exists are preserved rather than destroyed and recreated. Only new keys trigger buffer creation, and only orphaned keys trigger destruction. The shape data SSBO is also preserved when its size hasn't changed.
|
||||
|
||||
### Spatial grid memory reuse
|
||||
|
||||
The spatial hash grid retains per-slot entry arrays across rebuilds. Only slots whose shape count has grown beyond their current capacity trigger a reallocation.
|
||||
|
||||
### Log deduplication
|
||||
|
||||
The debug log ring buffer uses a 64-bit message hash for fast deduplication of warnings and errors (levels 0-2). Debug-level messages (level 3) skip dedup entirely to avoid the linear scan cost when verbose logging is active.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
3
makefile
3
makefile
@@ -65,7 +65,8 @@ debug: $(FETCH) $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES)
|
||||
$(CC) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) \
|
||||
-o $(TARGET) \
|
||||
$(EMCC_FLAGS) \
|
||||
-g -gsource-map=inline \
|
||||
-g --profiling-funcs -gsource-map=inline \
|
||||
-sASSERTIONS \
|
||||
-I$(LIB_DIR)/sokol \
|
||||
-I$(LIB_DIR)/imgui \
|
||||
-I$(LIB_DIR)/imgui/imgui \
|
||||
|
||||
36
src/api.h
36
src/api.h
@@ -23,14 +23,50 @@
|
||||
#include "cglm/cglm.h"
|
||||
|
||||
#include "rand.h"
|
||||
#include "camera.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>
|
||||
|
||||
40
src/camera.h
Normal file
40
src/camera.h
Normal 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
353
src/draw.h
Normal 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
91
src/generated/overlay.h
Normal 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;
|
||||
@@ -2,94 +2,109 @@ 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, 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, 0x62, 0x61, 0x73, 0x65, 0x5f,
|
||||
0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 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,
|
||||
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, 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, 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, 0x20,
|
||||
0x2a, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66,
|
||||
0x6f, 0x72, 0x6d, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72,
|
||||
0x6d, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75,
|
||||
0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x77, 0x6f, 0x72, 0x6c,
|
||||
0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x2a, 0x20, 0x76, 0x73, 0x5f, 0x75,
|
||||
0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, 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,
|
||||
0x63, 0x6c, 0x61, 0x6d, 0x70, 0x28, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f,
|
||||
0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x62, 0x61, 0x73, 0x65,
|
||||
0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x2a, 0x20, 0x31, 0x2e, 0x35,
|
||||
0x2c, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, 0x30, 0x29,
|
||||
0x2c, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x31, 0x2e, 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,
|
||||
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, 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, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69,
|
||||
0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x63, 0x6f,
|
||||
0x6c, 0x6f, 0x72, 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
|
||||
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 = 1103;
|
||||
unsigned int src_shaders_shape_wgsl_len = 1274;
|
||||
|
||||
@@ -74,60 +74,7 @@ unsigned char src_shaders_sprite_wgsl[] = {
|
||||
0x70, 0x75, 0x74, 0x2e, 0x75, 0x76, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x70,
|
||||
0x75, 0x74, 0x2e, 0x75, 0x76, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20,
|
||||
0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74,
|
||||
0x70, 0x75, 0x74, 0x3b, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x2f,
|
||||
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,
|
||||
0x70, 0x75, 0x74, 0x3b, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 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,
|
||||
@@ -142,4 +89,4 @@ unsigned char src_shaders_sprite_wgsl[] = {
|
||||
0x74, 0x75, 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b,
|
||||
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
14
src/globals.h
Normal file
@@ -0,0 +1,14 @@
|
||||
// Context struct declarations are now defined alongside the code they
|
||||
// belong to rather than in a single header:
|
||||
//
|
||||
// panel_log_ctx_t — api.h
|
||||
// shape_pool_ctx_t — shape.h (after shape_t / group_t)
|
||||
// group_index_ctx_t — shape.h
|
||||
// pipeline_ctx_t — render.h
|
||||
// shape_group_buf_t — shape.h
|
||||
//
|
||||
// This header exists only to avoid breaking includes elsewhere and can
|
||||
// be removed once all consumers are updated.
|
||||
#ifndef GLOBALS_H
|
||||
#define GLOBALS_H
|
||||
#endif
|
||||
517
src/history.h
517
src/history.h
@@ -3,144 +3,147 @@
|
||||
|
||||
#include "api.h"
|
||||
|
||||
// Each property kind we can undo/redo independently
|
||||
#define HISTORY_MAX_DEPTH 256
|
||||
|
||||
typedef enum {
|
||||
HIST_POSITION,
|
||||
HIST_SCALE,
|
||||
HIST_ROTATION,
|
||||
HIST_COLOR,
|
||||
HIST_CREATE,
|
||||
HIST_DELETE,
|
||||
HIST_GROUP,
|
||||
HIST_GROUP_CREATE,
|
||||
HIST_GROUP_DELETE,
|
||||
HIST_GROUP_REPARENT,
|
||||
HIST_EDIT,
|
||||
} hist_prop_t;
|
||||
|
||||
// One property change on one shape (old → new)
|
||||
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;
|
||||
|
||||
// A history entry is one or more changes batched together.
|
||||
// Single-property edits = 1 change. Whole-selection edits = N changes.
|
||||
typedef struct hist_entry_t {
|
||||
hist_change_t *changes;
|
||||
int count;
|
||||
} hist_entry_t;
|
||||
|
||||
#define HIST_MAX 64
|
||||
|
||||
typedef struct history_t {
|
||||
hist_entry_t entries[HIST_MAX];
|
||||
int count;
|
||||
int current; // index of last applied entry, -1 = initial state
|
||||
vector_t entries;
|
||||
int current;
|
||||
|
||||
// Pending edit session (one ImGui widget interaction)
|
||||
bool capturing;
|
||||
int pending_shape_idx;
|
||||
hist_prop_t pending_prop;
|
||||
float pending_old[4];
|
||||
} history_t;
|
||||
|
||||
// -- internal helpers --
|
||||
// -- helpers --
|
||||
|
||||
/**
|
||||
* Read the current value of a single property from a shape.
|
||||
*
|
||||
* @param s shape to read from
|
||||
* @param prop which property (HIST_POSITION, HIST_SCALE, etc.)
|
||||
* @param out receives the value, zero-padded to 4 floats
|
||||
*/
|
||||
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_COLOR: memcpy(out, s->uniform.base_color, sizeof(float[4])); break;
|
||||
case HIST_GROUP: out[0] = (float)s->group_id; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to a single property of a shape. Does NOT regenerate buffers.
|
||||
*
|
||||
* @param s shape to modify in-place
|
||||
* @param prop which property to set
|
||||
* @param val new value (4 floats, zero-padded for smaller properties)
|
||||
*/
|
||||
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_COLOR: memcpy(s->uniform.base_color, val, sizeof(float[4])); break;
|
||||
case HIST_GROUP: s->group_id = (int)val[0]; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// -- history API --
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero-initialize the history stack. Call once during app init.
|
||||
*
|
||||
* @param h history to initialize
|
||||
*/
|
||||
static void history_init(history_t *h) {
|
||||
memset(h, 0, sizeof(*h));
|
||||
vec_init(&h->entries, sizeof(hist_entry_t));
|
||||
h->current = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free all heap memory held by the history stack. Call during app shutdown.
|
||||
*
|
||||
* @param h history to destroy
|
||||
*/
|
||||
static void history_destroy(history_t *h) {
|
||||
for (int i = 0; i < h->count; i++) {
|
||||
if (h->entries[i].changes) FREE(h->entries[i].changes);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a completed entry onto the stack, discarding any redo branch.
|
||||
* Takes ownership of entry.changes (must be heap-allocated with ALLOC).
|
||||
* Used internally by begin_edit/end_edit, or directly for batch edits.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param entry entry to push (changes array is consumed, not copied)
|
||||
*/
|
||||
static void history_push_entry(history_t *h, hist_entry_t entry) {
|
||||
while (h->count > h->current + 1) {
|
||||
h->count--;
|
||||
if (h->entries[h->count].changes) {
|
||||
FREE(h->entries[h->count].changes);
|
||||
h->entries[h->count].changes = NULL;
|
||||
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);
|
||||
}
|
||||
|
||||
if (h->count >= HIST_MAX) {
|
||||
if (h->entries[0].changes) FREE(h->entries[0].changes);
|
||||
memmove(&h->entries[0], &h->entries[1],
|
||||
(h->count - 1) * sizeof(hist_entry_t));
|
||||
h->count--;
|
||||
*((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--;
|
||||
}
|
||||
|
||||
h->entries[h->count] = entry;
|
||||
h->count++;
|
||||
h->current = h->count - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin capturing an edit session. Snapshots the current value of one property.
|
||||
* If a prior session is still open (e.g. user switched widgets in the same frame),
|
||||
* it is finalized and pushed first.
|
||||
* Call when igIsItemActivated() is true after an ImGui widget.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param shapes the shapes vector (used to read current values)
|
||||
* @param shape_idx index of the shape being edited
|
||||
* @param prop which property is about to change
|
||||
*/
|
||||
static void history_begin_edit(history_t *h, vector_t *shapes,
|
||||
int shape_idx, hist_prop_t prop) {
|
||||
if (h->capturing) {
|
||||
@@ -148,15 +151,13 @@ static void history_begin_edit(history_t *h, vector_t *shapes,
|
||||
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_change_t change = {
|
||||
.shape_index = h->pending_shape_idx,
|
||||
.prop = h->pending_prop,
|
||||
};
|
||||
memcpy(change.old_val, h->pending_old, sizeof(float[4]));
|
||||
memcpy(change.new_val, new_val, sizeof(float[4]));
|
||||
hist_entry_t entry = { .changes = NULL, .count = 1 };
|
||||
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t));
|
||||
*entry.changes = change;
|
||||
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;
|
||||
@@ -169,14 +170,6 @@ static void history_begin_edit(history_t *h, vector_t *shapes,
|
||||
hist_read_prop(s, prop, h->pending_old);
|
||||
}
|
||||
|
||||
/**
|
||||
* End the current edit session and push an entry if the value changed.
|
||||
* Safe to call when no session is active (no-op).
|
||||
* Call when igIsAnyItemActive() transitions from true to false.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param shapes the shapes vector (used to read final values)
|
||||
*/
|
||||
static void history_end_edit(history_t *h, vector_t *shapes) {
|
||||
if (!h->capturing) return;
|
||||
|
||||
@@ -185,69 +178,321 @@ static void history_end_edit(history_t *h, vector_t *shapes) {
|
||||
hist_read_prop(s, h->pending_prop, new_val);
|
||||
|
||||
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) {
|
||||
hist_change_t change = {
|
||||
.shape_index = h->pending_shape_idx,
|
||||
.prop = h->pending_prop,
|
||||
};
|
||||
memcpy(change.old_val, h->pending_old, sizeof(float[4]));
|
||||
memcpy(change.new_val, new_val, sizeof(float[4]));
|
||||
hist_entry_t entry = { .changes = NULL, .count = 1 };
|
||||
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t));
|
||||
*entry.changes = change;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply every change in an entry to the shapes vector and regenerate buffers.
|
||||
*
|
||||
* @param entry the history entry to apply
|
||||
* @param shapes the shapes vector to modify
|
||||
* @param forward true to use new_val (redo), false to use old_val (undo)
|
||||
*/
|
||||
static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forward) {
|
||||
for (int i = 0; i < entry->count; i++) {
|
||||
hist_change_t *c = &entry->changes[i];
|
||||
if (c->shape_index >= shapes->count) continue;
|
||||
shape_t *s = (shape_t*) vec_get(shapes, c->shape_index);
|
||||
hist_apply_prop(s, c->prop, forward ? c->new_val : c->old_val);
|
||||
shape_regenerate(s);
|
||||
shape_set_state(s, s->hovered, s->selected);
|
||||
// -- 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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the most recent history entry.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param shapes the shapes vector to revert
|
||||
* @param selected_count out-parameter for updated selection count (currently passed through)
|
||||
* @return true if state was changed, false if nothing to undo
|
||||
*/
|
||||
static bool history_undo(history_t *h, vector_t *shapes, int *selected_count) {
|
||||
if (h->current < 0) return false;
|
||||
// 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);
|
||||
}
|
||||
|
||||
history_apply_entry(&h->entries[h->current], shapes, false);
|
||||
// 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--;
|
||||
(void)selected_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the next history entry.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param shapes the shapes vector to advance
|
||||
* @param selected_count out-parameter (currently passed through)
|
||||
* @return true if state was changed, false if nothing to redo
|
||||
*/
|
||||
static bool history_redo(history_t *h, vector_t *shapes, int *selected_count) {
|
||||
if (h->current + 1 >= h->count) return false;
|
||||
|
||||
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->entries[h->current], shapes, true);
|
||||
(void)selected_count;
|
||||
history_apply_entry(h, shapes, sp, groups, gi, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
1248
src/input.h
Normal file
1248
src/input.h
Normal file
File diff suppressed because it is too large
Load Diff
113
src/interact.h
Normal file
113
src/interact.h
Normal 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
|
||||
1489
src/main.c
1489
src/main.c
File diff suppressed because it is too large
Load Diff
247
src/overlay.h
Normal file
247
src/overlay.h
Normal 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
|
||||
87
src/rand.h
87
src/rand.h
@@ -3,105 +3,114 @@
|
||||
|
||||
#include "api.h"
|
||||
|
||||
static uint32_t seed;
|
||||
typedef struct {
|
||||
uint32_t seed;
|
||||
} rand_ctx_t;
|
||||
|
||||
static uint32_t xorshift32(void);
|
||||
static void rand_seed(uint32_t _seed);
|
||||
static uint32_t next_int(void);
|
||||
static uint32_t next_int_max(uint32_t max);
|
||||
static uint32_t next_int_minmax(uint32_t min, uint32_t max);
|
||||
static float next_float(void);
|
||||
static float next_float_max(float max);
|
||||
static float next_float_minmax(float min, float max);
|
||||
static uint32_t xorshift32(rand_ctx_t *r);
|
||||
static void rand_seed(rand_ctx_t *r, uint32_t _seed);
|
||||
static uint32_t next_int(rand_ctx_t *r);
|
||||
static uint32_t next_int_max(rand_ctx_t *r, uint32_t max);
|
||||
static uint32_t next_int_minmax(rand_ctx_t *r, uint32_t min, uint32_t max);
|
||||
static float next_float(rand_ctx_t *r);
|
||||
static float next_float_max(rand_ctx_t *r, float max);
|
||||
static float next_float_minmax(rand_ctx_t *r, float min, float max);
|
||||
|
||||
/**
|
||||
* Xorshift32 PRNG core. Advances the global seed and returns the new value.
|
||||
* Xorshift32 PRNG core. Advances the seed and returns the new value.
|
||||
*
|
||||
* @return pseudo-random 32-bit integer
|
||||
* @param r PRNG context
|
||||
* @return pseudo-random 32-bit integer
|
||||
*/
|
||||
static uint32_t xorshift32(void)
|
||||
static uint32_t xorshift32(rand_ctx_t *r)
|
||||
{
|
||||
seed ^= seed<<13;
|
||||
seed ^= seed>>17;
|
||||
seed ^= seed<<5;
|
||||
return seed;
|
||||
r->seed ^= r->seed<<13;
|
||||
r->seed ^= r->seed>>17;
|
||||
r->seed ^= r->seed<<5;
|
||||
return r->seed;
|
||||
}
|
||||
/**
|
||||
* Seed the global PRNG state. Zero is ignored (caller should pass a non-zero
|
||||
* Seed the PRNG state. Zero is ignored (caller should pass a non-zero
|
||||
* seed). Runs the generator once after seeding to mix the state.
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param _seed non-zero 32-bit seed value
|
||||
*/
|
||||
static void rand_seed(uint32_t _seed)
|
||||
static void rand_seed(rand_ctx_t *r, uint32_t _seed)
|
||||
{
|
||||
if(_seed == 0)
|
||||
return;
|
||||
|
||||
seed = _seed;
|
||||
xorshift32();
|
||||
|
||||
r->seed = _seed;
|
||||
xorshift32(r);
|
||||
}
|
||||
/**
|
||||
* Return a random integer in [0, UINT32_MAX].
|
||||
*
|
||||
* @return pseudo-random 32-bit integer
|
||||
* @param r PRNG context
|
||||
* @return pseudo-random 32-bit integer
|
||||
*/
|
||||
static uint32_t next_int(void)
|
||||
static uint32_t next_int(rand_ctx_t *r)
|
||||
{
|
||||
return xorshift32();
|
||||
return xorshift32(r);
|
||||
}
|
||||
/**
|
||||
* Return a random integer in [0, max].
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random integer
|
||||
*/
|
||||
static uint32_t next_int_max(uint32_t max)
|
||||
static uint32_t next_int_max(rand_ctx_t *r, uint32_t max)
|
||||
{
|
||||
return (uint32_t) floorf(xorshift32() / (float) UINT32_MAX * max);
|
||||
return (uint32_t)((double)xorshift32(r) / (double)UINT32_MAX * max);
|
||||
}
|
||||
/**
|
||||
* Return a random integer in [min, max].
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param min inclusive lower bound
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random integer
|
||||
*/
|
||||
static uint32_t next_int_minmax(uint32_t min, uint32_t max)
|
||||
static uint32_t next_int_minmax(rand_ctx_t *r, uint32_t min, uint32_t max)
|
||||
{
|
||||
const float x = (float) xorshift32() / UINT32_MAX;
|
||||
//(1.0f - Time) * A + Time * B
|
||||
return (1.0f - x) * min + x * max;
|
||||
const double x = (double)xorshift32(r) / (double)UINT32_MAX;
|
||||
return (uint32_t)((1.0 - x) * min + x * max);
|
||||
}
|
||||
/**
|
||||
* Return a random float in [0, 1].
|
||||
*
|
||||
* @return pseudo-random float
|
||||
* @param r PRNG context
|
||||
* @return pseudo-random float
|
||||
*/
|
||||
static float next_float(void)
|
||||
static float next_float(rand_ctx_t *r)
|
||||
{
|
||||
return (float) xorshift32() / UINT32_MAX;
|
||||
return (float)((double)xorshift32(r) / (double)UINT32_MAX);
|
||||
}
|
||||
/**
|
||||
* Return a random float in [0, max].
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random float
|
||||
*/
|
||||
static float next_float_max(float max)
|
||||
static float next_float_max(rand_ctx_t *r, float max)
|
||||
{
|
||||
return (float) xorshift32() / UINT32_MAX * max;
|
||||
return (float)((double)xorshift32(r) / (double)UINT32_MAX * max);
|
||||
}
|
||||
/**
|
||||
* Return a random float in [min, max].
|
||||
*
|
||||
* @param r PRNG context
|
||||
* @param min inclusive lower bound
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random float
|
||||
*/
|
||||
static float next_float_minmax(float min, float max)
|
||||
static float next_float_minmax(rand_ctx_t *r, float min, float max)
|
||||
{
|
||||
const float x = (float) xorshift32() / UINT32_MAX;
|
||||
return (1.0f - x) * min + x * max;
|
||||
const double x = (double)xorshift32(r) / (double)UINT32_MAX;
|
||||
return (float)((1.0 - x) * min + x * max);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
133
src/render.h
Normal file
133
src/render.h
Normal 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
44
src/shaders/overlay.wgsl
Normal 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;
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
struct VsUniform {
|
||||
mvp: mat4x4f,
|
||||
instance_base: u32,
|
||||
};
|
||||
|
||||
struct ShapeUniform {
|
||||
struct ShapeData {
|
||||
transform: mat4x4f,
|
||||
base_color: vec4f,
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -21,19 +27,18 @@ 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 = vec4f(input.position.x, input.position.y, 0.0, 1.0) * shape_uniform.transform;
|
||||
output.pos = world_pos * vs_uniforms.mvp;
|
||||
if (shape_uniform.state == 2u) {
|
||||
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_uniform.state == 1u) {
|
||||
output.color = clamp(shape_uniform.base_color * 1.5, vec4f(0.0), vec4f(1.0));
|
||||
} else if (shape.state == 1u) {
|
||||
output.color = vec4f(0.5, 0.6, 1.0, 1.0);
|
||||
} else {
|
||||
output.color = shape_uniform.base_color;
|
||||
output.color = vec4f(0.8, 0.8, 0.8, 1.0);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -31,22 +31,6 @@ struct FsO { //Fragment shader 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 {
|
||||
var output: FsO;
|
||||
|
||||
|
||||
1191
src/shape.h
1191
src/shape.h
File diff suppressed because it is too large
Load Diff
322
src/spatial.h
322
src/spatial.h
@@ -3,11 +3,10 @@
|
||||
|
||||
#include "api.h"
|
||||
|
||||
// Tunable constants
|
||||
#define SPATIAL_CELL_SIZE 250.0f
|
||||
#define SPATIAL_HASH_BITS 8
|
||||
#define SPATIAL_HASH_BITS 16
|
||||
#define SPATIAL_HASH_SIZE (1 << SPATIAL_HASH_BITS)
|
||||
#define SPATIAL_QUERY_RANGE 1
|
||||
#define SPATIAL_MAX_CELLS_PER_SHAPE 64
|
||||
|
||||
typedef struct {
|
||||
int shape_idx;
|
||||
@@ -24,6 +23,9 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
spatial_slot_t slots[SPATIAL_HASH_SIZE];
|
||||
int *visited; // per-shape query-dedup frame tags
|
||||
int visited_cap;
|
||||
int query_frame; // increments per query, never 0
|
||||
bool dirty;
|
||||
} spatial_grid_t;
|
||||
|
||||
@@ -32,23 +34,11 @@ static int spatial_hash(int cx, int cy)
|
||||
return (cx * 73856093) ^ (cy * 19349663);
|
||||
}
|
||||
|
||||
static void spatial_compute_aabb(shape_t *s, float *min_x, float *min_y,
|
||||
float *max_x, float *max_y)
|
||||
{
|
||||
float cos_r = cosf(s->rotation);
|
||||
float sin_r = sinf(s->rotation);
|
||||
float hx = fabsf(cos_r) * s->sx + fabsf(sin_r) * s->sy;
|
||||
float hy = fabsf(sin_r) * s->sx + fabsf(cos_r) * s->sy;
|
||||
*min_x = s->cx - hx;
|
||||
*min_y = s->cy - hy;
|
||||
*max_x = s->cx + hx;
|
||||
*max_y = s->cy + hy;
|
||||
}
|
||||
|
||||
static void spatial_init(spatial_grid_t *grid)
|
||||
{
|
||||
memset(grid, 0, sizeof(*grid));
|
||||
grid->dirty = true;
|
||||
grid->query_frame = 1;
|
||||
}
|
||||
|
||||
static void spatial_mark_dirty(spatial_grid_t *grid)
|
||||
@@ -61,9 +51,58 @@ static void spatial_destroy(spatial_grid_t *grid)
|
||||
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
|
||||
if (grid->slots[i].entries) FREE(grid->slots[i].entries);
|
||||
}
|
||||
if (grid->visited) FREE(grid->visited);
|
||||
memset(grid, 0, sizeof(*grid));
|
||||
}
|
||||
|
||||
// Find or create the slot for cell (cx, cy). Returns the slot index.
|
||||
static int spatial_find_slot(spatial_grid_t *grid, int cx, int cy)
|
||||
{
|
||||
int idx = spatial_hash(cx, cy) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe = 0;
|
||||
while (grid->slots[idx].occupied && probe < SPATIAL_HASH_SIZE) {
|
||||
if (grid->slots[idx].cx == cx && grid->slots[idx].cy == cy) break;
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
probe++;
|
||||
}
|
||||
if (!grid->slots[idx].occupied) {
|
||||
grid->slots[idx].occupied = true;
|
||||
grid->slots[idx].cx = cx;
|
||||
grid->slots[idx].cy = cy;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Enumerate the cells a shape's AABB overlaps. Returns the number of cells.
|
||||
// Capped at SPATIAL_MAX_CELLS_PER_SHAPE; falls back to the center cell for
|
||||
// degenerate huge shapes to avoid hash-table explosion.
|
||||
static int spatial_shape_cells(shape_t *s, int *cell_xs, int *cell_ys)
|
||||
{
|
||||
int cmin_x = (int)floorf((s->cx - s->aabb_hx) / SPATIAL_CELL_SIZE);
|
||||
int cmax_x = (int)floorf((s->cx + s->aabb_hx) / SPATIAL_CELL_SIZE);
|
||||
int cmin_y = (int)floorf((s->cy - s->aabb_hy) / SPATIAL_CELL_SIZE);
|
||||
int cmax_y = (int)floorf((s->cy + s->aabb_hy) / SPATIAL_CELL_SIZE);
|
||||
|
||||
int ncx = cmax_x - cmin_x + 1;
|
||||
int ncy = cmax_y - cmin_y + 1;
|
||||
|
||||
if (ncx <= 0 || ncy <= 0 || ncx * ncy > SPATIAL_MAX_CELLS_PER_SHAPE) {
|
||||
cell_xs[0] = (int)floorf(s->cx / SPATIAL_CELL_SIZE);
|
||||
cell_ys[0] = (int)floorf(s->cy / SPATIAL_CELL_SIZE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (int cy = cmin_y; cy <= cmax_y; cy++) {
|
||||
for (int cx = cmin_x; cx <= cmax_x; cx++) {
|
||||
cell_xs[count] = cx;
|
||||
cell_ys[count] = cy;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void spatial_rebuild(spatial_grid_t *grid, vector_t *shapes)
|
||||
{
|
||||
if (!grid->dirty) return;
|
||||
@@ -79,150 +118,180 @@ static void spatial_rebuild(spatial_grid_t *grid, vector_t *shapes)
|
||||
|
||||
if (n == 0) return;
|
||||
|
||||
// Grow visited array if needed (used for query-result dedup)
|
||||
if (n > grid->visited_cap) {
|
||||
if (grid->visited) FREE(grid->visited);
|
||||
grid->visited = (int*)ALLOC((size_t)n * sizeof(int));
|
||||
grid->visited_cap = n;
|
||||
}
|
||||
memset(grid->visited, 0, (size_t)n * sizeof(int));
|
||||
grid->query_frame = 1;
|
||||
|
||||
int cell_xs[128], cell_ys[128];
|
||||
|
||||
// Phase 1: count shapes per cell
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(shapes, i);
|
||||
int ccx = (int) floorf(s->cx / SPATIAL_CELL_SIZE);
|
||||
int ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE);
|
||||
|
||||
int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1);
|
||||
while (grid->slots[idx].occupied) {
|
||||
if (grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy) break;
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
shape_t *s = (shape_t*)vec_get(shapes, i);
|
||||
int nc = spatial_shape_cells(s, cell_xs, cell_ys);
|
||||
for (int c = 0; c < nc; c++) {
|
||||
int idx = spatial_find_slot(grid, cell_xs[c], cell_ys[c]);
|
||||
grid->slots[idx].count++;
|
||||
}
|
||||
|
||||
if (!grid->slots[idx].occupied) {
|
||||
grid->slots[idx].occupied = true;
|
||||
grid->slots[idx].cx = ccx;
|
||||
grid->slots[idx].cy = ccy;
|
||||
}
|
||||
grid->slots[idx].count++;
|
||||
}
|
||||
|
||||
// Phase 2: allocate entry arrays based on count
|
||||
// Phase 2: resize entry arrays when needed
|
||||
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
|
||||
if (!grid->slots[i].occupied) continue;
|
||||
if (grid->slots[i].count > grid->slots[i].capacity) {
|
||||
int need = grid->slots[i].count;
|
||||
if (need > grid->slots[i].capacity) {
|
||||
if (grid->slots[i].entries) FREE(grid->slots[i].entries);
|
||||
grid->slots[i].entries = (spatial_entry_t*) ALLOC(
|
||||
(size_t) grid->slots[i].count * sizeof(spatial_entry_t));
|
||||
grid->slots[i].capacity = grid->slots[i].count;
|
||||
grid->slots[i].entries = (spatial_entry_t*)ALLOC(
|
||||
(size_t)need * sizeof(spatial_entry_t));
|
||||
grid->slots[i].capacity = need;
|
||||
}
|
||||
grid->slots[i].count = 0; // reset for fill phase
|
||||
grid->slots[i].count = 0;
|
||||
}
|
||||
|
||||
// Phase 3: fill entries
|
||||
// Phase 3: fill entries — each shape is added to every cell its AABB overlaps
|
||||
for (int i = 0; i < n; i++) {
|
||||
shape_t *s = (shape_t*) vec_get(shapes, i);
|
||||
int ccx = (int) floorf(s->cx / SPATIAL_CELL_SIZE);
|
||||
int ccy = (int) floorf(s->cy / SPATIAL_CELL_SIZE);
|
||||
shape_t *s = (shape_t*)vec_get(shapes, i);
|
||||
float min_x = s->cx - s->aabb_hx;
|
||||
float min_y = s->cy - s->aabb_hy;
|
||||
float max_x = s->cx + s->aabb_hx;
|
||||
float max_y = s->cy + s->aabb_hy;
|
||||
|
||||
int idx = spatial_hash(ccx, ccy) & (SPATIAL_HASH_SIZE - 1);
|
||||
while (!(grid->slots[idx].occupied &&
|
||||
grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy)) {
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
int nc = spatial_shape_cells(s, cell_xs, cell_ys);
|
||||
for (int c = 0; c < nc; c++) {
|
||||
int idx = spatial_find_slot(grid, cell_xs[c], cell_ys[c]);
|
||||
spatial_entry_t *e = &grid->slots[idx].entries[grid->slots[idx].count++];
|
||||
e->shape_idx = i;
|
||||
e->min_x = min_x; e->min_y = min_y;
|
||||
e->max_x = max_x; e->max_y = max_y;
|
||||
}
|
||||
|
||||
spatial_entry_t *e = &grid->slots[idx].entries[grid->slots[idx].count++];
|
||||
e->shape_idx = i;
|
||||
spatial_compute_aabb(s, &e->min_x, &e->min_y, &e->max_x, &e->max_y);
|
||||
}
|
||||
}
|
||||
|
||||
// Point query — O(1) average. Only checks the cell containing the query point
|
||||
// because shapes are now inserted into every cell their AABB overlaps.
|
||||
static int spatial_query_point(spatial_grid_t *grid, vector_t *shapes,
|
||||
float wx, float wy, float world_tol)
|
||||
{
|
||||
int ccx = (int) floorf(wx / SPATIAL_CELL_SIZE);
|
||||
int ccy = (int) floorf(wy / SPATIAL_CELL_SIZE);
|
||||
int cx = (int)floorf(wx / SPATIAL_CELL_SIZE);
|
||||
int cy = (int)floorf(wy / SPATIAL_CELL_SIZE);
|
||||
|
||||
for (int dz = -SPATIAL_QUERY_RANGE; dz <= SPATIAL_QUERY_RANGE; dz++) {
|
||||
for (int dw = -SPATIAL_QUERY_RANGE; dw <= SPATIAL_QUERY_RANGE; dw++) {
|
||||
int cell_x = ccx + dz;
|
||||
int cell_y = ccy + dw;
|
||||
int idx = spatial_hash(cx, cy) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe_start = idx;
|
||||
|
||||
int idx = spatial_hash(cell_x, cell_y) & (SPATIAL_HASH_SIZE - 1);
|
||||
int probe_start = idx;
|
||||
do {
|
||||
if (!grid->slots[idx].occupied) break;
|
||||
|
||||
do {
|
||||
if (!grid->slots[idx].occupied) break;
|
||||
if (grid->slots[idx].cx == cx && grid->slots[idx].cy == cy) {
|
||||
for (int e = 0; e < grid->slots[idx].count; e++) {
|
||||
spatial_entry_t *entry = &grid->slots[idx].entries[e];
|
||||
|
||||
if (grid->slots[idx].cx == cell_x && grid->slots[idx].cy == cell_y) {
|
||||
for (int e = 0; e < grid->slots[idx].count; e++) {
|
||||
spatial_entry_t *entry = &grid->slots[idx].entries[e];
|
||||
if (wx < entry->min_x - world_tol ||
|
||||
wx > entry->max_x + world_tol ||
|
||||
wy < entry->min_y - world_tol ||
|
||||
wy > entry->max_y + world_tol)
|
||||
continue;
|
||||
|
||||
if (wx < entry->min_x - world_tol ||
|
||||
wx > entry->max_x + world_tol ||
|
||||
wy < entry->min_y - world_tol ||
|
||||
wy > entry->max_y + world_tol)
|
||||
continue;
|
||||
|
||||
shape_t *s = (shape_t*) vec_get(shapes, entry->shape_idx);
|
||||
if (shape_hit_test(s, wx, wy, world_tol))
|
||||
return entry->shape_idx;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
} while (idx != probe_start);
|
||||
shape_t *s = (shape_t*)vec_get(shapes, entry->shape_idx);
|
||||
if (shape_hit_test(s, wx, wy, world_tol))
|
||||
return entry->shape_idx;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
} while (idx != probe_start);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Rectangle selection for marquee. Uses a per-query frame counter for dedup
|
||||
// since shapes now appear in every cell they overlap.
|
||||
static int spatial_query_rect_select(spatial_grid_t *grid, vector_t *shapes,
|
||||
float min_x, float min_y,
|
||||
float max_x, float max_y)
|
||||
{
|
||||
for (int i = 0; i < shapes->count; i++) {
|
||||
((shape_t*) vec_get(shapes, i))->selected = false;
|
||||
int n = shapes->count;
|
||||
for (int i = 0; i < n; i++) {
|
||||
((shape_t*)vec_get(shapes, i))->selected = false;
|
||||
}
|
||||
int selected_count = 0;
|
||||
|
||||
int min_cx = (int) floorf(min_x / SPATIAL_CELL_SIZE);
|
||||
int min_cy = (int) floorf(min_y / SPATIAL_CELL_SIZE);
|
||||
int max_cx = (int) floorf(max_x / SPATIAL_CELL_SIZE);
|
||||
int max_cy = (int) floorf(max_y / SPATIAL_CELL_SIZE);
|
||||
grid->query_frame++;
|
||||
int frame = grid->query_frame;
|
||||
|
||||
for (int cell_x = min_cx; cell_x <= max_cx; cell_x++) {
|
||||
for (int cell_y = min_cy; cell_y <= max_cy; cell_y++) {
|
||||
int idx = spatial_hash(cell_x, cell_y) & (SPATIAL_HASH_SIZE - 1);
|
||||
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 == cell_x && grid->slots[idx].cy == cell_y) {
|
||||
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 *s = (shape_t*) vec_get(shapes, entry->shape_idx);
|
||||
if (s->selected) continue;
|
||||
|
||||
bool hit = (s->cx >= min_x && s->cx <= max_x &&
|
||||
s->cy >= min_y && s->cy <= max_y);
|
||||
float sc = cosf(s->rotation), ss = sinf(s->rotation);
|
||||
for (uint32_t v = 0; !hit && v < s->num_verts; v++) {
|
||||
float lx = s->verts[v].x * s->sx;
|
||||
float ly = s->verts[v].y * s->sy;
|
||||
float wx = s->cx + lx * sc - ly * ss;
|
||||
float wy = s->cy + lx * ss + ly * sc;
|
||||
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) {
|
||||
s->selected = true;
|
||||
selected_count++;
|
||||
}
|
||||
if (hit) { shape->selected = true; selected_count++; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
} while (idx != probe_start);
|
||||
}
|
||||
@@ -230,4 +299,43 @@ static int spatial_query_rect_select(spatial_grid_t *grid, vector_t *shapes,
|
||||
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
|
||||
|
||||
202
src/types.h
Normal file
202
src/types.h
Normal 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
503
src/ui_panels.h
Normal 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
|
||||
46
src/util.h
46
src/util.h
@@ -33,6 +33,11 @@ static void vec_grow(vector_t *v, int min_capacity) {
|
||||
int new_cap = v->capacity ? v->capacity * 2 : 8;
|
||||
if (new_cap < min_capacity) new_cap = min_capacity;
|
||||
uint8_t *new_data = (uint8_t*) ALLOC(new_cap * v->stride);
|
||||
if (!new_data) {
|
||||
EM_ASM({ console.error("vec_grow: ALLOC failed for %d elements of %d bytes", $0, $1); },
|
||||
new_cap, v->stride);
|
||||
return;
|
||||
}
|
||||
if (v->data) {
|
||||
memcpy(new_data, v->data, v->count * v->stride);
|
||||
FREE(v->data);
|
||||
@@ -61,6 +66,47 @@ 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;
|
||||
}
|
||||
}
|
||||
v->count -= count;
|
||||
}
|
||||
|
||||
static void *vec_insert(vector_t *v, int index) {
|
||||
if (index < 0 || index > v->count) return NULL;
|
||||
if (v->count >= v->capacity) vec_grow(v, v->count + 1);
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user