You've already forked flecs_tests
Compare commits
2 Commits
21476a3b95
...
81616f8a5d
| Author | SHA1 | Date | |
|---|---|---|---|
| 81616f8a5d | |||
| 5881a7dafc |
29
CLAUDE.md
29
CLAUDE.md
@@ -1,5 +1,34 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## Project: Cartograph
|
||||
|
||||
A browser-based world map creation tool (like Wonderdraft/Inkarnate). C99 compiled to WebAssembly via Emscripten.
|
||||
|
||||
### Stack
|
||||
- **Graphics:** Sokol (WebGPU backend, `SOKOL_WGPU`) — `lib/sokol/`
|
||||
- **UI:** Dear ImGui via cimgui — `lib/imgui/`
|
||||
- **Math:** cglm (types are C arrays: `vec2` = `float[2]`, `mat4` = `float[4][4]` column-major) — `lib/cglm/`
|
||||
- **Shaders:** WGSL in `src/shaders/`, compiled to C headers via `xxd -i` into `src/generated/`
|
||||
|
||||
### Build
|
||||
- `make` (release) / `make debug` — outputs `app.html`
|
||||
- All includes go through `src/api.h` which defines `SOKOL_IMPL`, `SOKOL_WGPU`, and pulls in every library header
|
||||
- Include paths: `lib/sokol`, `lib/imgui`, `lib/imgui/imgui`, `lib/util`, `lib/cglm/include`
|
||||
|
||||
### Key files
|
||||
- `src/main.c` — entry point, sokol init, render loop, input (zoom/pan/drag)
|
||||
- `src/api.h` — central include hub, backend defines
|
||||
- `src/sprite.h` — sprite batching, texture manager, file import stubs
|
||||
- `src/util.h` — `vector_t` (dynamic array) and `mem_pool_t` (free-list pool), both stripe-based
|
||||
- `src/rand.h` — xorshift32 PRNG
|
||||
|
||||
### Conventions
|
||||
- No malloc/free directly — use `ALLOC`/`FREE` macros (wired to smemtrack in main.c)
|
||||
- Assert is encouraged for invariant checks
|
||||
- Data structures use stripe-based allocation (byte stride per element, not sizeof)
|
||||
|
||||
---
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
63
README.md
Normal file
63
README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Cartograph
|
||||
|
||||
A browser-based world map creation tool inspired by Wonderdraft and Inkarnate. Uses shape-based terrain generation with procedural detail.
|
||||
|
||||
## Features (planned)
|
||||
|
||||
- **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
|
||||
|
||||
## Tech stack
|
||||
|
||||
| Layer | Library |
|
||||
|---|---|
|
||||
| Language | C (C99) |
|
||||
| Compiler | [Emscripten](https://emscripten.org/) (emcc) → WebAssembly |
|
||||
| Graphics | [Sokol](https://github.com/floooh/sokol) with WebGPU backend |
|
||||
| UI | [Dear ImGui](https://github.com/ocornut/imgui) via [cimgui](https://github.com/cimgui/cimgui) |
|
||||
| Math | [cglm](https://github.com/recp/cglm) |
|
||||
| Shaders | WGSL (compiled to C headers via `xxd`) |
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
# Install dependencies
|
||||
./fetch_libs.sh
|
||||
|
||||
# Release build
|
||||
make
|
||||
|
||||
# Debug build
|
||||
make debug
|
||||
```
|
||||
|
||||
Output is `app.html`, served by Emscripten's built-in web server or any static server.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
src/
|
||||
main.c Entry point, sokol init, render loop, input 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
|
||||
generated/ xxd-generated C headers from shaders
|
||||
lib/
|
||||
sokol/ Sokol single-file headers (gfx, app, glue, log)
|
||||
imgui/ Dear ImGui + cimgui
|
||||
cglm/ C linear math library
|
||||
util/ Sokol utility headers (memtrack, imgui integration)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -9,6 +9,7 @@ echo "=== Fetching library dependencies ==="
|
||||
mkdir -p "$LIB_DIR/sokol"
|
||||
mkdir -p "$LIB_DIR/imgui"
|
||||
mkdir -p "$LIB_DIR/util"
|
||||
mkdir -p "$LIB_DIR/cglm"
|
||||
|
||||
if [ ! -f "$LIB_DIR/sokol/sokol_gfx.h" ]; then
|
||||
echo " > Fetching sokol (pinned to emdawnwebgpu-compatible version)..."
|
||||
@@ -47,4 +48,15 @@ else
|
||||
echo " > cimgui already present"
|
||||
fi
|
||||
|
||||
# 4. cglm
|
||||
if [ ! -f "$LIB_DIR/cglm/include/cglm/cglm.h" ]; then
|
||||
echo " > Fetching cglm..."
|
||||
git clone --depth 1 --branch v0.9.6 https://github.com/recp/cglm.git "$LIB_DIR/cglm_tmp"
|
||||
cp -r "$LIB_DIR/cglm_tmp/include" "$LIB_DIR/cglm/"
|
||||
cp -r "$LIB_DIR/cglm_tmp/src" "$LIB_DIR/cglm/"
|
||||
rm -rf "$LIB_DIR/cglm_tmp"
|
||||
else
|
||||
echo " > cglm already present"
|
||||
fi
|
||||
|
||||
echo "=== Done ==="
|
||||
|
||||
14
makefile
14
makefile
@@ -19,6 +19,7 @@ IMGUI_SOURCES = $(LIB_DIR)/imgui/imgui/imgui.cpp \
|
||||
$(LIB_DIR)/imgui/imgui/imgui_tables.cpp \
|
||||
$(LIB_DIR)/imgui/imgui/imgui_widgets.cpp \
|
||||
$(LIB_DIR)/imgui/cimgui.cpp
|
||||
CGLM_SOURCES = $(wildcard $(LIB_DIR)/cglm/src/*.c)
|
||||
|
||||
# Dynamic shader processing
|
||||
SHADER_FILES = $(wildcard $(SHADER_DIR)/*.wgsl)
|
||||
@@ -29,7 +30,8 @@ EMCC_FLAGS = --use-port=emdawnwebgpu \
|
||||
-sWASM_BIGINT \
|
||||
-sALLOW_MEMORY_GROWTH \
|
||||
-msimd128 \
|
||||
-sFILESYSTEM=0
|
||||
-sFILESYSTEM=0 \
|
||||
-flto
|
||||
|
||||
# Shell template
|
||||
SHELL_FILE = shell.html
|
||||
@@ -38,8 +40,8 @@ SHELL_FILE = shell.html
|
||||
all: $(FETCH) $(TARGET)
|
||||
|
||||
# Main build target
|
||||
$(TARGET): $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(SHELL_FILE)
|
||||
$(CC) $(C_SOURCES) $(IMGUI_SOURCES) \
|
||||
$(TARGET): $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) $(SHELL_FILE)
|
||||
$(CC) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) \
|
||||
-o $(TARGET) \
|
||||
$(EMCC_FLAGS) \
|
||||
-O3 \
|
||||
@@ -47,6 +49,7 @@ $(TARGET): $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(SHELL_FILE)
|
||||
-I$(LIB_DIR)/imgui \
|
||||
-I$(LIB_DIR)/imgui/imgui \
|
||||
-I$(LIB_DIR)/util \
|
||||
-I$(LIB_DIR)/cglm/include \
|
||||
--shell-file=$(SHELL_FILE)
|
||||
|
||||
# Shader header generation
|
||||
@@ -58,8 +61,8 @@ $(GENERATED_DIR)/%.h: $(SHADER_DIR)/%.wgsl | $(GENERATED_DIR)
|
||||
$(GENERATED_DIR):
|
||||
mkdir -p $(GENERATED_DIR)
|
||||
|
||||
debug: $(FETCH) $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(SHELL_FILE)
|
||||
$(CC) $(C_SOURCES) $(IMGUI_SOURCES) \
|
||||
debug: $(FETCH) $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) $(SHELL_FILE)
|
||||
$(CC) $(C_SOURCES) $(IMGUI_SOURCES) $(CGLM_SOURCES) \
|
||||
-o $(TARGET) \
|
||||
$(EMCC_FLAGS) \
|
||||
-g -gsource-map=inline \
|
||||
@@ -67,6 +70,7 @@ debug: $(FETCH) $(SHADER_HEADERS) $(C_SOURCES) $(IMGUI_SOURCES) $(SHELL_FILE)
|
||||
-I$(LIB_DIR)/imgui \
|
||||
-I$(LIB_DIR)/imgui/imgui \
|
||||
-I$(LIB_DIR)/util \
|
||||
-I$(LIB_DIR)/cglm/include \
|
||||
--shell-file=$(SHELL_FILE)
|
||||
|
||||
# Clean build artifacts
|
||||
|
||||
17
src/api.h
17
src/api.h
@@ -6,6 +6,7 @@
|
||||
#define SOKOL_IMPL
|
||||
#define SOKOL_WGPU
|
||||
#define SOKOL_IMGUI_IMPL
|
||||
#define SOKOL_VALIDATE_NON_FATAL
|
||||
|
||||
#include "sokol_gfx.h"
|
||||
#include "sokol_app.h"
|
||||
@@ -14,14 +15,24 @@
|
||||
#include "cimgui.h"
|
||||
#include "sokol_imgui.h"
|
||||
#include "sokol_memtrack.h"
|
||||
#include "sokol_shape.h"
|
||||
|
||||
#define ALLOC(arg) smemtrack_alloc(arg, NULL)
|
||||
#define FREE(arg) smemtrack_free(arg, NULL)
|
||||
|
||||
#include "cglm/cglm.h"
|
||||
|
||||
#include "math.h"
|
||||
#include "rand.h"
|
||||
#include "util.h"
|
||||
#include "sprite.h"
|
||||
|
||||
#include "generated/sprite.h"
|
||||
#include "generated/shape.h"
|
||||
|
||||
#include "util.h"
|
||||
#include "shape.h"
|
||||
#include "spatial.h"
|
||||
#include "history.h"
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
|
||||
95
src/generated/shape.h
Normal file
95
src/generated/shape.h
Normal file
@@ -0,0 +1,95 @@
|
||||
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, 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,
|
||||
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,
|
||||
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
|
||||
};
|
||||
unsigned int src_shaders_shape_wgsl_len = 1103;
|
||||
254
src/history.h
Normal file
254
src/history.h
Normal file
@@ -0,0 +1,254 @@
|
||||
#ifndef HISTORY_H
|
||||
#define HISTORY_H
|
||||
|
||||
#include "api.h"
|
||||
|
||||
// Each property kind we can undo/redo independently
|
||||
typedef enum {
|
||||
HIST_POSITION,
|
||||
HIST_SCALE,
|
||||
HIST_ROTATION,
|
||||
HIST_COLOR,
|
||||
} 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];
|
||||
} 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
|
||||
|
||||
// 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 --
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
// -- history API --
|
||||
|
||||
/**
|
||||
* 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));
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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--;
|
||||
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) {
|
||||
shape_t *s = (shape_t*) vec_get(shapes, h->pending_shape_idx);
|
||||
float new_val[4];
|
||||
hist_read_prop(s, h->pending_prop, new_val);
|
||||
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) {
|
||||
hist_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;
|
||||
history_push_entry(h, entry);
|
||||
}
|
||||
h->capturing = false;
|
||||
}
|
||||
|
||||
h->capturing = true;
|
||||
h->pending_shape_idx = shape_idx;
|
||||
h->pending_prop = prop;
|
||||
shape_t *s = (shape_t*) vec_get(shapes, shape_idx);
|
||||
hist_read_prop(s, prop, h->pending_old);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
shape_t *s = (shape_t*) vec_get(shapes, h->pending_shape_idx);
|
||||
float new_val[4];
|
||||
hist_read_prop(s, h->pending_prop, new_val);
|
||||
|
||||
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) {
|
||||
hist_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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
history_apply_entry(&h->entries[h->current], shapes, 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;
|
||||
|
||||
h->current++;
|
||||
history_apply_entry(&h->entries[h->current], shapes, true);
|
||||
(void)selected_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
1279
src/main.c
1279
src/main.c
File diff suppressed because it is too large
Load Diff
49
src/math.h
49
src/math.h
@@ -1,49 +0,0 @@
|
||||
#ifndef MATH_IMPL_H
|
||||
#define MATH_IMPL_H
|
||||
|
||||
#include "api.h"
|
||||
|
||||
#define PI 3.14159265358979323846
|
||||
#define PI32 3.14159265359f
|
||||
|
||||
typedef struct vec2f {
|
||||
float x, y;
|
||||
} vec2f;
|
||||
typedef struct vec3f {
|
||||
float x, y, z;
|
||||
} vec3f;
|
||||
typedef struct vec4f {
|
||||
float x, y, z, w;
|
||||
} vec4f;
|
||||
|
||||
typedef struct vec2u {
|
||||
uint32_t x, y;
|
||||
} vec2u;
|
||||
typedef struct vec3u {
|
||||
uint32_t x, y, z;
|
||||
} vec3u;
|
||||
typedef struct vec4u {
|
||||
uint32_t x, y, z, w;
|
||||
} vec4u;
|
||||
|
||||
typedef struct vec2 {
|
||||
int32_t x, y;
|
||||
} vec2;
|
||||
typedef struct vec3 {
|
||||
int32_t x, y, z;
|
||||
} vec3;
|
||||
typedef struct vec4 {
|
||||
int32_t x, y, z, w;
|
||||
} vec4;
|
||||
|
||||
typedef struct mat4x4f {
|
||||
float e[4][4];
|
||||
} mat4x4f;
|
||||
typedef struct mat4x4u {
|
||||
uint32_t e[4][4];
|
||||
} mat4x4u;
|
||||
typedef union mat4x4 {
|
||||
int32_t e[4][4];
|
||||
} mat4x4;
|
||||
|
||||
#endif
|
||||
53
src/rand.h
53
src/rand.h
@@ -14,6 +14,11 @@ static float next_float(void);
|
||||
static float next_float_max(float max);
|
||||
static float next_float_minmax(float min, float max);
|
||||
|
||||
/**
|
||||
* Xorshift32 PRNG core. Advances the global seed and returns the new value.
|
||||
*
|
||||
* @return pseudo-random 32-bit integer
|
||||
*/
|
||||
static uint32_t xorshift32(void)
|
||||
{
|
||||
seed ^= seed<<13;
|
||||
@@ -21,6 +26,12 @@ static uint32_t xorshift32(void)
|
||||
seed ^= seed<<5;
|
||||
return seed;
|
||||
}
|
||||
/**
|
||||
* Seed the global PRNG state. Zero is ignored (caller should pass a non-zero
|
||||
* seed). Runs the generator once after seeding to mix the state.
|
||||
*
|
||||
* @param _seed non-zero 32-bit seed value
|
||||
*/
|
||||
static void rand_seed(uint32_t _seed)
|
||||
{
|
||||
if(_seed == 0)
|
||||
@@ -29,34 +40,64 @@ static void rand_seed(uint32_t _seed)
|
||||
seed = _seed;
|
||||
xorshift32();
|
||||
}
|
||||
// PRNG [0-UINT32_MAX]
|
||||
/**
|
||||
* Return a random integer in [0, UINT32_MAX].
|
||||
*
|
||||
* @return pseudo-random 32-bit integer
|
||||
*/
|
||||
static uint32_t next_int(void)
|
||||
{
|
||||
return xorshift32();
|
||||
}
|
||||
// PRNG [0-max]
|
||||
/**
|
||||
* Return a random integer in [0, max].
|
||||
*
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random integer
|
||||
*/
|
||||
static uint32_t next_int_max(uint32_t max)
|
||||
{
|
||||
return (uint32_t) floorf(xorshift32() / (float) UINT32_MAX * max);
|
||||
}
|
||||
// PRNG [min-max]
|
||||
/**
|
||||
* Return a random integer in [min, max].
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
const float x = (float) xorshift32() / UINT32_MAX;
|
||||
//(1.0f - Time) * A + Time * B
|
||||
return (1.0f - x) * min + x * max;
|
||||
}
|
||||
// PRNG [0-1]
|
||||
/**
|
||||
* Return a random float in [0, 1].
|
||||
*
|
||||
* @return pseudo-random float
|
||||
*/
|
||||
static float next_float(void)
|
||||
{
|
||||
return (float) xorshift32() / UINT32_MAX;
|
||||
}
|
||||
// PRNG [0-max]
|
||||
/**
|
||||
* Return a random float in [0, max].
|
||||
*
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random float
|
||||
*/
|
||||
static float next_float_max(float max)
|
||||
{
|
||||
return (float) xorshift32() / UINT32_MAX * max;
|
||||
}
|
||||
// PRNG [min-max]
|
||||
/**
|
||||
* Return a random float in [min, max].
|
||||
*
|
||||
* @param min inclusive lower bound
|
||||
* @param max inclusive upper bound
|
||||
* @return pseudo-random float
|
||||
*/
|
||||
static float next_float_minmax(float min, float max)
|
||||
{
|
||||
const float x = (float) xorshift32() / UINT32_MAX;
|
||||
|
||||
45
src/shaders/shape.wgsl
Normal file
45
src/shaders/shape.wgsl
Normal file
@@ -0,0 +1,45 @@
|
||||
struct VsUniform {
|
||||
mvp: mat4x4f,
|
||||
};
|
||||
|
||||
struct ShapeUniform {
|
||||
transform: mat4x4f,
|
||||
base_color: vec4f,
|
||||
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 = 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) {
|
||||
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 {
|
||||
output.color = shape_uniform.base_color;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@fragment fn fs_main(input: Vs2Fs) -> FsOut {
|
||||
var output: FsOut;
|
||||
output.color = input.color;
|
||||
return output;
|
||||
}
|
||||
433
src/shape.h
Normal file
433
src/shape.h
Normal file
@@ -0,0 +1,433 @@
|
||||
#ifndef SHAPE_H
|
||||
#define SHAPE_H
|
||||
|
||||
#include "api.h"
|
||||
|
||||
typedef struct shape_vertex_t {
|
||||
float x, y;
|
||||
} shape_vertex_t;
|
||||
|
||||
typedef struct shape_uniform_t {
|
||||
mat4 transform;
|
||||
float base_color[4];
|
||||
uint32_t state;
|
||||
uint8_t _pad[12];
|
||||
} shape_uniform_t;
|
||||
|
||||
typedef enum shape_kind_t {
|
||||
SHAPE_CIRCLE,
|
||||
SHAPE_STAR,
|
||||
} shape_kind_t;
|
||||
|
||||
typedef struct shape_t {
|
||||
shape_vertex_t *verts;
|
||||
uint16_t *indices;
|
||||
uint32_t num_indices;
|
||||
uint32_t num_verts;
|
||||
sg_buffer vbuf;
|
||||
sg_buffer ibuf;
|
||||
shape_uniform_t uniform;
|
||||
bool hovered;
|
||||
bool selected;
|
||||
|
||||
shape_kind_t kind;
|
||||
float cx, cy;
|
||||
float sx, sy;
|
||||
float rotation;
|
||||
int star_points;
|
||||
float star_inner_ratio;
|
||||
int last_update_frame;
|
||||
} shape_t;
|
||||
|
||||
#define SHAPE_HOVER_PX 6.0f
|
||||
|
||||
static sg_pipeline shape_pipeline;
|
||||
static sg_pipeline overlay_pipeline;
|
||||
static sg_shader shape_shader;
|
||||
static int g_shape_frame_id;
|
||||
|
||||
static void shape_begin_frame(void)
|
||||
{
|
||||
g_shape_frame_id++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the shape shader, shape pipeline (line strip), and overlay pipeline
|
||||
* (triangles). Call once during app init before drawing any shapes.
|
||||
*/
|
||||
static void shape_init_pipeline(void)
|
||||
{
|
||||
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 = 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 = "Shape shader",
|
||||
});
|
||||
|
||||
shape_pipeline = sg_make_pipeline(&(sg_pipeline_desc) {
|
||||
.shader = shape_shader,
|
||||
.index_type = SG_INDEXTYPE_UINT16,
|
||||
.primitive_type = SG_PRIMITIVETYPE_LINE_STRIP,
|
||||
.layout.attrs = {
|
||||
[0].format = SG_VERTEXFORMAT_FLOAT2,
|
||||
},
|
||||
.label = "Shape pipeline",
|
||||
});
|
||||
|
||||
overlay_pipeline = sg_make_pipeline(&(sg_pipeline_desc) {
|
||||
.shader = shape_shader,
|
||||
.index_type = SG_INDEXTYPE_UINT16,
|
||||
.primitive_type = SG_PRIMITIVETYPE_TRIANGLES,
|
||||
.layout.attrs = {
|
||||
[0].format = SG_VERTEXFORMAT_FLOAT2,
|
||||
},
|
||||
.label = "Overlay pipeline",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the shape shader and both pipelines. Call during app shutdown.
|
||||
*/
|
||||
static void shape_shutdown_pipeline(void)
|
||||
{
|
||||
sg_destroy_pipeline(shape_pipeline);
|
||||
sg_destroy_pipeline(overlay_pipeline);
|
||||
sg_destroy_shader(shape_shader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of line segments for a circle of the given radius.
|
||||
* Clamped to [8, 128]; scales roughly with circumference.
|
||||
*
|
||||
* @param r circle radius in world units
|
||||
* @return segment count
|
||||
*/
|
||||
static int shape_calc_segments(float r)
|
||||
{
|
||||
int n = (int)(fabsf(r) * 0.5f) + 16;
|
||||
if (n < 8) n = 8;
|
||||
if (n > 128) n = 128;
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default state for a newly created shape: identity transform, base color,
|
||||
* not hovered, not selected.
|
||||
*
|
||||
* @param s shape to initialize
|
||||
* @param color RGBA base color (copied)
|
||||
*/
|
||||
static void shape_init_common(shape_t *s, const float color[4])
|
||||
{
|
||||
s->hovered = false;
|
||||
s->selected = false;
|
||||
memcpy(s->uniform.base_color, color, sizeof(float[4]));
|
||||
s->uniform.state = 0;
|
||||
memset(s->uniform._pad, 0, sizeof(s->uniform._pad));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the per-shape transform matrix from cx, cy, rotation.
|
||||
* Uses R(-angle) so the shader's row-vector convention matches the existing
|
||||
* world-space vertex computation.
|
||||
*/
|
||||
static void shape_build_transform(shape_t *s)
|
||||
{
|
||||
mat4 T, R, S, RS;
|
||||
glm_translate_make(T, (vec3){s->cx, s->cy, 0.0f});
|
||||
glm_rotate_make(R, -s->rotation, (vec3){0.0f, 0.0f, 1.0f});
|
||||
glm_scale_make(S, (vec3){s->sx, s->sy, 1.0f});
|
||||
glm_mat4_mul(R, S, RS);
|
||||
glm_mat4_mul(T, RS, s->uniform.transform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create GPU vertex and index buffers from the shape's current verts/indices.
|
||||
*
|
||||
* @param s shape (must have verts and indices allocated)
|
||||
*/
|
||||
static void shape_make_buffers(shape_t *s)
|
||||
{
|
||||
s->vbuf = sg_make_buffer(&(sg_buffer_desc) {
|
||||
.size = s->num_indices * sizeof(shape_vertex_t),
|
||||
.usage = { .stream_update = true },
|
||||
.label = "Shape vertices",
|
||||
});
|
||||
sg_update_buffer(s->vbuf, &(sg_range){s->verts, s->num_indices * sizeof(shape_vertex_t)});
|
||||
s->ibuf = sg_make_buffer(&(sg_buffer_desc) {
|
||||
.usage = { .index_buffer = true },
|
||||
.data = { s->indices, s->num_indices * sizeof(uint16_t) },
|
||||
.label = "Shape indices",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy GPU buffers and free vertex/index arrays for a single shape.
|
||||
*
|
||||
* @param s shape to tear down
|
||||
*/
|
||||
static void shape_shutdown(shape_t *s)
|
||||
{
|
||||
sg_destroy_buffer(s->vbuf);
|
||||
sg_destroy_buffer(s->ibuf);
|
||||
FREE(s->verts);
|
||||
FREE(s->indices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild vertex and index data from the shape's current parameters (position,
|
||||
* scale, rotation, kind), then recreate GPU buffers. Call after any parameter
|
||||
* change.
|
||||
*
|
||||
* @param s shape to regenerate
|
||||
*/
|
||||
static void shape_regenerate(shape_t *s)
|
||||
{
|
||||
int n, count;
|
||||
if (s->kind == SHAPE_CIRCLE) {
|
||||
int segs = shape_calc_segments(s->sx);
|
||||
n = segs;
|
||||
count = segs + 1;
|
||||
} else {
|
||||
n = s->star_points * 2;
|
||||
count = n + 1;
|
||||
}
|
||||
|
||||
bool resized = ((uint32_t)count != s->num_indices);
|
||||
if (resized) {
|
||||
sg_destroy_buffer(s->vbuf);
|
||||
sg_destroy_buffer(s->ibuf);
|
||||
FREE(s->verts);
|
||||
FREE(s->indices);
|
||||
s->verts = (shape_vertex_t*) ALLOC(count * sizeof(shape_vertex_t));
|
||||
s->indices = (uint16_t*) ALLOC(count * sizeof(uint16_t));
|
||||
}
|
||||
|
||||
if (s->kind == SHAPE_CIRCLE) {
|
||||
int segs = n;
|
||||
for (int i = 0; i < segs; i++) {
|
||||
float a = (float)i / (float)segs * 2.0f * GLM_PIf - GLM_PI_2f;
|
||||
s->verts[i] = (shape_vertex_t) { cosf(a), sinf(a) };
|
||||
}
|
||||
s->verts[segs] = s->verts[0];
|
||||
} else {
|
||||
for (int i = 0; i < n; i++) {
|
||||
float a = (float)i / (float)n * 2.0f * GLM_PIf - GLM_PI_2f;
|
||||
float r = (i & 1) ? s->star_inner_ratio : 1.0f;
|
||||
s->verts[i] = (shape_vertex_t) { cosf(a) * r, sinf(a) * r };
|
||||
}
|
||||
s->verts[n] = s->verts[0];
|
||||
}
|
||||
|
||||
s->num_indices = (uint32_t)count;
|
||||
s->num_verts = (uint32_t)n;
|
||||
for (int i = 0; i <= n; i++) s->indices[i] = (uint16_t)i;
|
||||
|
||||
shape_build_transform(s);
|
||||
|
||||
if (resized) {
|
||||
shape_make_buffers(s);
|
||||
s->last_update_frame = g_shape_frame_id;
|
||||
} else if (s->last_update_frame != g_shape_frame_id) {
|
||||
sg_update_buffer(s->vbuf, &(sg_range){s->verts, (size_t)count * sizeof(shape_vertex_t)});
|
||||
s->last_update_frame = g_shape_frame_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update hovered/selected flags and the shader uniform state.
|
||||
* State is 0=normal, 1=hovered (brightened), 2=selected (green).
|
||||
*
|
||||
* @param s shape to update
|
||||
* @param hovered true if cursor is over the shape
|
||||
* @param selected true if shape is in the selection set
|
||||
*/
|
||||
static void shape_set_state(shape_t *s, bool hovered, bool selected)
|
||||
{
|
||||
s->hovered = hovered;
|
||||
s->selected = selected;
|
||||
s->uniform.state = selected ? 2u : (hovered ? 1u : 0u);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ray-casting point-in-polygon test. Handles arbitrary non-self-intersecting
|
||||
* polygons.
|
||||
*
|
||||
* @param px point X in world space
|
||||
* @param py point Y in world space
|
||||
* @param verts polygon vertices
|
||||
* @param n vertex count
|
||||
* @return true if the point is inside the polygon
|
||||
*/
|
||||
static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t n)
|
||||
{
|
||||
bool inside = false;
|
||||
for (uint32_t i = 0, j = n - 1; i < n; j = i++) {
|
||||
float xi = verts[i].x, yi = verts[i].y;
|
||||
float xj = verts[j].x, yj = verts[j].y;
|
||||
if ((yi > py) != (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a world-space point hits this shape. Transforms the query
|
||||
* to local space (verts are now stored relative to origin), then tests
|
||||
* polygon containment and edge proximity.
|
||||
*
|
||||
* @param s shape to test
|
||||
* @param wx point X in world space
|
||||
* @param wy point Y in world space
|
||||
* @param world_tol hit tolerance in world units
|
||||
* @return true if the point hits the shape
|
||||
*/
|
||||
static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol)
|
||||
{
|
||||
float sc = cosf(s->rotation), ss = sinf(s->rotation);
|
||||
float dx = wx - s->cx, dy = wy - s->cy;
|
||||
float lx = (dx * sc + dy * ss) / s->sx;
|
||||
float ly = (-dx * ss + dy * sc) / s->sy;
|
||||
float min_scale = fminf(fabsf(s->sx), fabsf(s->sy));
|
||||
float local_tol = world_tol / (min_scale > 0.0001f ? min_scale : 1.0f);
|
||||
float tol_sq = local_tol * local_tol;
|
||||
|
||||
if (point_in_polygon(lx, ly, s->verts, s->num_verts))
|
||||
return true;
|
||||
|
||||
for (uint32_t i = 0, j = s->num_verts - 1; i < s->num_verts; j = i++) {
|
||||
float ax = s->verts[i].x, ay = s->verts[i].y;
|
||||
float bx = s->verts[j].x, by = s->verts[j].y;
|
||||
float abx = bx - ax, aby = by - ay;
|
||||
float len_sq = abx * abx + aby * aby;
|
||||
if (len_sq < 0.0001f) continue;
|
||||
float t = ((lx - ax) * abx + (ly - ay) * aby) / len_sq;
|
||||
t = fmaxf(0.0f, fminf(1.0f, t));
|
||||
float cx = ax + t * abx, cy = ay + t * aby;
|
||||
float ddx = lx - cx, ddy = ly - cy;
|
||||
if (ddx * ddx + ddy * ddy <= tol_sq) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a draw call for this shape using the shape line-strip pipeline.
|
||||
*
|
||||
* @param s shape to draw
|
||||
* @param mvp model-view-projection matrix (from compute_mvp)
|
||||
*/
|
||||
static void shape_draw(shape_t *s, const mat4 *mvp)
|
||||
{
|
||||
sg_apply_uniforms(0, &SG_RANGE(*mvp));
|
||||
sg_apply_uniforms(1, &SG_RANGE(s->uniform));
|
||||
sg_apply_bindings(&(sg_bindings) {
|
||||
.vertex_buffers[0] = s->vbuf,
|
||||
.index_buffer = s->ibuf,
|
||||
});
|
||||
sg_draw(0, s->num_indices, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a circle shape (returned by value). Allocates verts/indices and GPU
|
||||
* buffers. The number of line segments adapts to radius.
|
||||
*
|
||||
* @param x center X in world space
|
||||
* @param y center Y in world space
|
||||
* @param r radius in world units
|
||||
* @param color RGBA base color
|
||||
* @return fully initialized shape_t
|
||||
*/
|
||||
static shape_t shape_circle(float x, float y, float r, const float color[4])
|
||||
{
|
||||
shape_t s;
|
||||
s.kind = SHAPE_CIRCLE;
|
||||
s.cx = x; s.cy = y;
|
||||
s.sx = r; s.sy = r;
|
||||
s.rotation = 0.0f;
|
||||
|
||||
int segs = shape_calc_segments(r);
|
||||
int count = segs + 1;
|
||||
s.verts = (shape_vertex_t*) ALLOC(count * sizeof(shape_vertex_t));
|
||||
s.indices = (uint16_t*) ALLOC(count * sizeof(uint16_t));
|
||||
|
||||
for (int i = 0; i < segs; i++) {
|
||||
float a = (float)i / (float)segs * 2.0f * GLM_PIf - GLM_PI_2f;
|
||||
s.verts[i] = (shape_vertex_t) { cosf(a), sinf(a) };
|
||||
}
|
||||
s.verts[segs] = s.verts[0];
|
||||
for (int i = 0; i <= segs; i++) s.indices[i] = (uint16_t)i;
|
||||
s.num_indices = (uint32_t)count;
|
||||
s.num_verts = (uint32_t)segs;
|
||||
|
||||
shape_init_common(&s, color);
|
||||
shape_build_transform(&s);
|
||||
shape_make_buffers(&s);
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a star shape (returned by value). Alternates between outer_r and
|
||||
* inner_r at each vertex, producing a star with the given number of points.
|
||||
* Allocates verts/indices and GPU buffers.
|
||||
*
|
||||
* @param x center X in world space
|
||||
* @param y center Y in world space
|
||||
* @param outer_r outer radius in world units
|
||||
* @param inner_r inner radius in world units
|
||||
* @param points number of star points
|
||||
* @param color RGBA base color
|
||||
* @return fully initialized shape_t
|
||||
*/
|
||||
static shape_t shape_star(float x, float y, float outer_r, float inner_r,
|
||||
int points, const float color[4])
|
||||
{
|
||||
shape_t s;
|
||||
s.kind = SHAPE_STAR;
|
||||
s.cx = x; s.cy = y;
|
||||
s.sx = outer_r; s.sy = outer_r;
|
||||
s.rotation = 0.0f;
|
||||
s.star_points = points;
|
||||
s.star_inner_ratio = inner_r / outer_r;
|
||||
|
||||
int n = points * 2;
|
||||
int count = n + 1;
|
||||
s.verts = (shape_vertex_t*) ALLOC(count * sizeof(shape_vertex_t));
|
||||
s.indices = (uint16_t*) ALLOC(count * sizeof(uint16_t));
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
float a = (float)i / (float)n * 2.0f * GLM_PIf - GLM_PI_2f;
|
||||
float r = (i & 1) ? s.star_inner_ratio : 1.0f;
|
||||
s.verts[i] = (shape_vertex_t) { cosf(a) * r, sinf(a) * r };
|
||||
}
|
||||
s.verts[n] = s.verts[0];
|
||||
for (int i = 0; i <= n; i++) s.indices[i] = (uint16_t)i;
|
||||
s.num_indices = (uint32_t)count;
|
||||
s.num_verts = (uint32_t)n;
|
||||
|
||||
shape_init_common(&s, color);
|
||||
shape_build_transform(&s);
|
||||
shape_make_buffers(&s);
|
||||
return s;
|
||||
}
|
||||
|
||||
#endif
|
||||
233
src/spatial.h
Normal file
233
src/spatial.h
Normal file
@@ -0,0 +1,233 @@
|
||||
#ifndef SPATIAL_H
|
||||
#define SPATIAL_H
|
||||
|
||||
#include "api.h"
|
||||
|
||||
// Tunable constants
|
||||
#define SPATIAL_CELL_SIZE 250.0f
|
||||
#define SPATIAL_HASH_BITS 8
|
||||
#define SPATIAL_HASH_SIZE (1 << SPATIAL_HASH_BITS)
|
||||
#define SPATIAL_QUERY_RANGE 1
|
||||
|
||||
typedef struct {
|
||||
int shape_idx;
|
||||
float min_x, min_y, max_x, max_y;
|
||||
} spatial_entry_t;
|
||||
|
||||
typedef struct {
|
||||
bool occupied;
|
||||
int cx, cy;
|
||||
spatial_entry_t *entries;
|
||||
int count;
|
||||
int capacity;
|
||||
} spatial_slot_t;
|
||||
|
||||
typedef struct {
|
||||
spatial_slot_t slots[SPATIAL_HASH_SIZE];
|
||||
bool dirty;
|
||||
} spatial_grid_t;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void spatial_mark_dirty(spatial_grid_t *grid)
|
||||
{
|
||||
grid->dirty = true;
|
||||
}
|
||||
|
||||
static void spatial_destroy(spatial_grid_t *grid)
|
||||
{
|
||||
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
|
||||
if (grid->slots[i].entries) FREE(grid->slots[i].entries);
|
||||
}
|
||||
memset(grid, 0, sizeof(*grid));
|
||||
}
|
||||
|
||||
static void spatial_rebuild(spatial_grid_t *grid, vector_t *shapes)
|
||||
{
|
||||
if (!grid->dirty) return;
|
||||
grid->dirty = false;
|
||||
|
||||
int n = shapes->count;
|
||||
|
||||
// Phase 0: clear occupied flags
|
||||
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
|
||||
grid->slots[i].occupied = false;
|
||||
grid->slots[i].count = 0;
|
||||
}
|
||||
|
||||
if (n == 0) return;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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
|
||||
for (int i = 0; i < SPATIAL_HASH_SIZE; i++) {
|
||||
if (!grid->slots[i].occupied) continue;
|
||||
if (grid->slots[i].count > 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].count = 0; // reset for fill phase
|
||||
}
|
||||
|
||||
// Phase 3: fill entries
|
||||
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 &&
|
||||
grid->slots[idx].cx == ccx && grid->slots[idx].cy == ccy)) {
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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(cell_x, cell_y) & (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) {
|
||||
for (int e = 0; e < grid->slots[idx].count; e++) {
|
||||
spatial_entry_t *entry = &grid->slots[idx].entries[e];
|
||||
|
||||
if (wx < entry->min_x - world_tol ||
|
||||
wx > entry->max_x + world_tol ||
|
||||
wy < entry->min_y - world_tol ||
|
||||
wy > entry->max_y + world_tol)
|
||||
continue;
|
||||
|
||||
shape_t *s = (shape_t*) vec_get(shapes, entry->shape_idx);
|
||||
if (shape_hit_test(s, wx, wy, world_tol))
|
||||
return entry->shape_idx;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
} while (idx != probe_start);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
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 probe_start = idx;
|
||||
|
||||
do {
|
||||
if (!grid->slots[idx].occupied) break;
|
||||
|
||||
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 (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;
|
||||
if (wx >= min_x && wx <= max_x &&
|
||||
wy >= min_y && wy <= max_y)
|
||||
hit = true;
|
||||
}
|
||||
if (hit) {
|
||||
s->selected = true;
|
||||
selected_count++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1);
|
||||
} while (idx != probe_start);
|
||||
}
|
||||
}
|
||||
return selected_count;
|
||||
}
|
||||
|
||||
#endif
|
||||
79
src/sprite.h
79
src/sprite.h
@@ -1,79 +0,0 @@
|
||||
#ifndef SPRITE_H
|
||||
#define SPRITE_H
|
||||
|
||||
#include "api.h"
|
||||
|
||||
#define KB (1024u)
|
||||
#define MB (1024u * KB)
|
||||
#define GB (1024u * MB)
|
||||
#define MAX_FILE_SIZE (10u * MB)
|
||||
|
||||
typedef struct texture_t {
|
||||
uint32_t id; //Texture ID
|
||||
sg_bindings binding; //Texture bindings (texture, sampler and buffer)
|
||||
vector_t sprites;
|
||||
bool dirty;
|
||||
} texture_t;
|
||||
|
||||
typedef struct sprite_t {
|
||||
mat4x4f transform;
|
||||
} sprite_t;
|
||||
|
||||
typedef struct manager_t {
|
||||
vector_t textures;
|
||||
} manager_t;
|
||||
|
||||
typedef struct loader_t {
|
||||
uint8_t buffer[MAX_FILE_SIZE];
|
||||
} loader_t;
|
||||
|
||||
typedef void (*ForEachTexture)(texture_t *texture, void *userdata);
|
||||
typedef void (*ForEachSprite)(sprite_t *sprite, void *userdata);
|
||||
|
||||
void gs_init(manager_t *manager);
|
||||
void gs_shutdown(manager_t *manager);
|
||||
void gs_import_file(const sapp_html5_fetch_response *response);
|
||||
|
||||
void gs_each_textures(manager_t *manager, ForEachTexture callback, void *userdata);
|
||||
void gs_each_sprites(manager_t *manager, ForEachSprite callback, void *userdata);
|
||||
|
||||
void gs_init(manager_t *manager)
|
||||
{
|
||||
manager->textures = vector_create(sizeof(texture_t));
|
||||
}
|
||||
void gs_shutdown(manager_t *manager)
|
||||
{
|
||||
vector_free(&manager->textures);
|
||||
}
|
||||
|
||||
void gs_import_file(const sapp_html5_fetch_response *response)
|
||||
{
|
||||
const char* filename = sapp_get_dropped_file_path(response->file_index);
|
||||
|
||||
if(response->succeeded)
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//Toast error
|
||||
}
|
||||
}
|
||||
void gs_each_textures(manager_t *manager, ForEachTexture callback, void *userdata)
|
||||
{
|
||||
for(uint32_t i = 0; i < manager->textures.size; i++)
|
||||
callback((texture_t*) manager->textures.data[i], userdata);
|
||||
}
|
||||
void gs_each_sprites(manager_t *manager, ForEachSprite callback, void *userdata)
|
||||
{
|
||||
for(uint32_t i = 0; i < manager->textures.size; i++)
|
||||
{
|
||||
const texture_t* texture = (texture_t*) manager->textures.data[i];
|
||||
for(uint32_t j = 0; j < texture->sprites.size; j++)
|
||||
{
|
||||
callback((sprite_t*) texture->sprites.data[j], userdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
299
src/util.h
299
src/util.h
@@ -1,241 +1,104 @@
|
||||
#ifndef UTIL_H
|
||||
#define UTIL_H
|
||||
|
||||
#include "api.h"
|
||||
|
||||
/*typedef struct linked_list_t {
|
||||
linked_item_t *first, *last;
|
||||
uint16_t size;
|
||||
} linked_list_t;
|
||||
|
||||
typedef struct linked_item_t {
|
||||
linked_item_t *next, *prev;
|
||||
void *data;
|
||||
} linked_item_t;
|
||||
|
||||
typedef void (*linked_list_callback)(void *item);
|
||||
|
||||
//Currently, these are the only method required.
|
||||
//Many more could be implemented but this is unnecessary.
|
||||
static void l_list_push(linked_list_t *l_list, void *item);
|
||||
static void l_list_append(linked_list_t *l_list, void *item);
|
||||
static void* l_list_pop(linked_list_t *l_list);
|
||||
static void* l_list_unppend(linked_list_t *l_list);
|
||||
static void l_list_each(linked_list_t *l_list, linked_list_callback callback);
|
||||
|
||||
static inline void l_list_push(linked_list_t *l_list, void *item)
|
||||
{
|
||||
linked_item_t l_item = (linked_item_t) { .data = item, .prev = l_list->last, .next = nullptr };
|
||||
|
||||
if(l_list->first == nullptr)
|
||||
l_list->first = &l_item;
|
||||
|
||||
l_list->last->next = &l_item;
|
||||
l_list->last = &l_item;
|
||||
l_list->size++;
|
||||
}
|
||||
static inline void l_list_append(linked_list_t *l_list, void *item)
|
||||
{
|
||||
linked_item_t l_item = (linked_item_t) { .data = item, .prev = nullptr, .next = l_list->last };
|
||||
|
||||
if(l_list->last == nullptr)
|
||||
l_list->last = &l_item;
|
||||
|
||||
l_list->first->prev = &l_item;
|
||||
l_list->first = &l_item;
|
||||
l_list->size++;
|
||||
}
|
||||
static inline void* l_list_pop(linked_list_t *l_list)
|
||||
{
|
||||
if(l_list->last == nullptr)
|
||||
return;
|
||||
|
||||
if(l_list->first == l_list->last)
|
||||
l_list->first = nullptr;
|
||||
|
||||
linked_item_t *item = l_list->last->prev;
|
||||
l_list->last->prev = nullptr;
|
||||
l_list->last = item;
|
||||
l_list->size--;
|
||||
}
|
||||
static inline void* l_list_unppend(linked_list_t *l_list)
|
||||
{
|
||||
if(l_list->first == nullptr)
|
||||
return;
|
||||
|
||||
if(l_list->last == l_list->first)
|
||||
l_list->last = nullptr;
|
||||
|
||||
linked_item_t *item = l_list->first->next;
|
||||
l_list->first->next = nullptr;
|
||||
l_list->first = item;
|
||||
l_list->size--;
|
||||
}
|
||||
static inline void l_list_each(linked_list_t *l_list, linked_list_callback callback)
|
||||
{
|
||||
|
||||
}*/
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct vector_t {
|
||||
void **data; //Memory pool
|
||||
uint16_t stripe; //Bit per item
|
||||
uint32_t size; //Current amount of items
|
||||
uint32_t capacity; //Max capacity
|
||||
uint8_t *data;
|
||||
int count;
|
||||
int capacity;
|
||||
int stride;
|
||||
} vector_t;
|
||||
|
||||
#define MAX_BUCKET_SIZE (0xffffffffu)
|
||||
#define MAX_STRIPE_SIZE (0xffffffu)
|
||||
#define FIXED_START (0xfffu)
|
||||
|
||||
static vector_t vector_create(uint32_t stripe); //Create a new vector with a default size
|
||||
static void vector_clear(vector_t *vector);
|
||||
static void vector_free(vector_t *vector);
|
||||
|
||||
static uint32_t vector_length(vector_t *vector);
|
||||
static void* vector_get(vector_t *vector, uint32_t index);
|
||||
static void vector_set(vector_t *vector, uint32_t index, void *data);
|
||||
static uint32_t vector_push(vector_t *vector, void *data);
|
||||
static sg_range vector_range(vector_t *vector);
|
||||
|
||||
static vector_t vector_create(uint32_t stripe)
|
||||
{
|
||||
assert(stripe >= sizeof(uint32_t));
|
||||
assert(stripe <= MAX_STRIPE_SIZE);
|
||||
assert(FIXED_START * stripe <= MAX_BUCKET_SIZE);
|
||||
|
||||
return (vector_t) {
|
||||
.capacity = FIXED_START,
|
||||
.size = 0,
|
||||
.stripe = stripe,
|
||||
.data = (void**) malloc(FIXED_START * stripe),
|
||||
};
|
||||
}
|
||||
static void vector_clear(vector_t *vector)
|
||||
{
|
||||
vector->size = 0;
|
||||
}
|
||||
static void vector_free(vector_t *vector)
|
||||
{
|
||||
free(vector->data);
|
||||
/**
|
||||
* Zero-initialize a vector with the given element stride.
|
||||
*
|
||||
* @param v vector to initialize
|
||||
* @param stride byte size of each element
|
||||
*/
|
||||
static void vec_init(vector_t *v, int stride) {
|
||||
memset(v, 0, sizeof(*v));
|
||||
v->stride = stride;
|
||||
}
|
||||
|
||||
static uint32_t vector_length(vector_t *vector)
|
||||
{
|
||||
return vector->size;
|
||||
}
|
||||
static void* vector_get(vector_t *vector, uint32_t index)
|
||||
{
|
||||
assert(index > 0);
|
||||
assert(index < vector->size);
|
||||
|
||||
return vector->data[index * vector->stripe];
|
||||
}
|
||||
static void vector_set(vector_t *vector, uint32_t index, void *data)
|
||||
{
|
||||
assert(index > 0);
|
||||
assert(index < vector->size);
|
||||
|
||||
vector->data[index * vector->stripe] = data;
|
||||
}
|
||||
static uint32_t vector_push(vector_t *vector, void *data)
|
||||
{
|
||||
if(vector->size >= vector->capacity)
|
||||
{
|
||||
vector->capacity *= 2;
|
||||
|
||||
vector->data = (void**) realloc(vector->data, vector->capacity * vector->stripe);
|
||||
/**
|
||||
* Grow the vector's backing array to at least min_capacity elements.
|
||||
* Doubles capacity (starting at 8) or uses min_capacity, whichever is larger.
|
||||
*
|
||||
* @param v vector to grow
|
||||
* @param min_capacity minimum element count required
|
||||
*/
|
||||
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 (v->data) {
|
||||
memcpy(new_data, v->data, v->count * v->stride);
|
||||
FREE(v->data);
|
||||
}
|
||||
|
||||
return vector->size++;
|
||||
}
|
||||
static sg_range vector_range(vector_t *vector)
|
||||
{
|
||||
return (sg_range) {
|
||||
.ptr = vector->data,
|
||||
.size = vector->size,
|
||||
};
|
||||
v->data = new_data;
|
||||
v->capacity = new_cap;
|
||||
}
|
||||
|
||||
typedef struct mem_pool_t {
|
||||
void **data; //Memory pool
|
||||
uint32_t stripe; //Bit per item
|
||||
uint32_t size; //Current amount of items
|
||||
uint32_t capacity; //Max capacity
|
||||
uint32_t free; //Linked list of available indices
|
||||
} mem_pool_t;
|
||||
|
||||
static mem_pool_t pool_create_default(uint32_t stripe); //Create a new memory pool with a default size
|
||||
static mem_pool_t pool_create(uint32_t stripe, uint32_t size); //Create a new memory pool with a default size
|
||||
static void pool_clear(mem_pool_t *pool);
|
||||
static void pool_free(mem_pool_t *pool);
|
||||
|
||||
static const uint32_t pool_add(mem_pool_t *pool); //Request a new free index
|
||||
static const void* pool_get(mem_pool_t *pool, uint32_t index); //Get the pointer
|
||||
static void pool_remove(mem_pool_t *pool, uint32_t index); //Flag the given index as free
|
||||
|
||||
static mem_pool_t pool_create_default(uint32_t stripe)
|
||||
{
|
||||
return pool_create(stripe, FIXED_START);
|
||||
}
|
||||
static mem_pool_t pool_create(uint32_t stripe, uint32_t size)
|
||||
{
|
||||
assert(stripe >= sizeof(uint32_t));
|
||||
assert(stripe <= MAX_STRIPE_SIZE);
|
||||
assert(size * stripe <= MAX_BUCKET_SIZE);
|
||||
|
||||
return (mem_pool_t) {
|
||||
.capacity = size,
|
||||
.size = 0,
|
||||
.stripe = stripe,
|
||||
.free = UINT32_MAX,
|
||||
.data = (void**) malloc(size * stripe),
|
||||
};
|
||||
}
|
||||
static void pool_clear(mem_pool_t *pool)
|
||||
{
|
||||
pool->size = 0;
|
||||
pool->free = UINT32_MAX;
|
||||
}
|
||||
static void pool_free(mem_pool_t *pool)
|
||||
{
|
||||
free(pool->data);
|
||||
/**
|
||||
* Append an uninitialized element to the end of the vector. Grows if needed.
|
||||
*
|
||||
* @param v vector to push into
|
||||
* @return pointer to the new (uninitialized) element
|
||||
*/
|
||||
static void *vec_push(vector_t *v) {
|
||||
if (v->count >= v->capacity) vec_grow(v, v->count + 1);
|
||||
return v->data + (v->count++) * v->stride;
|
||||
}
|
||||
|
||||
static const uint32_t pool_add(mem_pool_t *pool)
|
||||
{
|
||||
if(pool->free != UINT32_MAX)
|
||||
{
|
||||
const uint32_t index = pool->free;
|
||||
/**
|
||||
* Remove the last element from the vector (decrements count, no free).
|
||||
*
|
||||
* @param v vector to pop from
|
||||
*/
|
||||
static void vec_pop(vector_t *v) {
|
||||
if (v->count > 0) v->count--;
|
||||
}
|
||||
|
||||
pool->free = (uint32_t) pool->data[index * pool->stripe];
|
||||
|
||||
return index;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(pool->size >= pool->capacity)
|
||||
{
|
||||
pool->capacity *= 2;
|
||||
|
||||
pool->data = (void**) realloc(pool->data, pool->capacity * pool->stripe);
|
||||
}
|
||||
|
||||
return pool->size++;
|
||||
/**
|
||||
* Remove the element at index by swapping in the last element (O(1)).
|
||||
* Order is not preserved.
|
||||
*
|
||||
* @param v vector to remove from
|
||||
* @param index index of the element to remove
|
||||
*/
|
||||
static void vec_remove(vector_t *v, int index) {
|
||||
if (index < 0 || index >= v->count) return;
|
||||
if (index < v->count - 1) {
|
||||
memcpy(v->data + index * v->stride,
|
||||
v->data + (v->count - 1) * v->stride,
|
||||
v->stride);
|
||||
}
|
||||
v->count--;
|
||||
}
|
||||
static const void* pool_get(mem_pool_t *pool, uint32_t index)
|
||||
{
|
||||
assert(index > 0);
|
||||
assert(index < pool->capacity);
|
||||
|
||||
return pool->data[index * pool->stripe];
|
||||
/**
|
||||
* Return a pointer to the element at index (no bounds check).
|
||||
*
|
||||
* @param v vector to access
|
||||
* @param index element index
|
||||
* @return pointer to the element
|
||||
*/
|
||||
static void *vec_get(vector_t *v, int index) {
|
||||
return v->data + index * v->stride;
|
||||
}
|
||||
static void pool_remove(mem_pool_t *pool, uint32_t index)
|
||||
{
|
||||
const uint32_t pos = index * pool->stripe;
|
||||
|
||||
pool->data[pos] = (void*) pool->free;
|
||||
pool->free = index;
|
||||
/**
|
||||
* Free the backing array and reset the vector to empty.
|
||||
*
|
||||
* @param v vector to free
|
||||
*/
|
||||
static void vec_free(vector_t *v) {
|
||||
if (v->data) FREE(v->data);
|
||||
v->data = NULL;
|
||||
v->count = 0;
|
||||
v->capacity = 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user