diff --git a/CLAUDE.md b/CLAUDE.md index f837ff8..11b0737 100644 --- a/CLAUDE.md +++ b/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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d2ef06 --- /dev/null +++ b/README.md @@ -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 diff --git a/fetch_libs.sh b/fetch_libs.sh index bf50ac9..20ddc2c 100644 --- a/fetch_libs.sh +++ b/fetch_libs.sh @@ -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 ===" diff --git a/makefile b/makefile index d5ba8d1..9d47cec 100644 --- a/makefile +++ b/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) @@ -38,8 +39,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 +48,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 +60,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 +69,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 diff --git a/src/api.h b/src/api.h index e91c7b0..058b8cc 100644 --- a/src/api.h +++ b/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,22 @@ #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 #include #include #include diff --git a/src/generated/shape.h b/src/generated/shape.h new file mode 100644 index 0000000..11b3f6a --- /dev/null +++ b/src/generated/shape.h @@ -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; diff --git a/src/main.c b/src/main.c index 86c326c..9e76905 100644 --- a/src/main.c +++ b/src/main.c @@ -1,13 +1,16 @@ #include "api.h" -#define ALLOC(arg) smemtrack_alloc(arg, NULL) -#define FREE(arg) smemtrack_free(arg, NULL) - #define GRID_X 1000 #define GRID_Y 1000 +#define LOG_RING_SIZE 64 + +typedef struct log_entry_t { + char text[256]; + uint32_t level; +} log_entry_t; typedef struct vs_uniform_t { - mat4x4f mvp; + mat4 mvp; } uniform_t; typedef struct renderer_t { @@ -23,11 +26,26 @@ typedef struct dragger_t { typedef struct userdata_t { int width, height; - vec2f pan; + vec2 pan; float zoom; dragger_t dragger; renderer_t renderer; - manager_t manager; + vector_t shapes; + int selected_count; + int hovered_shape; + + bool selecting; + float sel_sx, sel_sy; + float sel_cx, sel_cy; + bool sel_dragging; + int sel_clicked_shape; + + sg_buffer rect_vbuf, rect_ibuf; + + log_entry_t log_ring[LOG_RING_SIZE]; + int log_head; + int log_count; + bool log_show; } userdata_t; const char* format(const char* format, ...) @@ -45,10 +63,73 @@ const char* format(const char* format, ...) } void compute_mvp(userdata_t *userdata) { - userdata->renderer.uniform.mvp.e[0][0] = (2.0f / userdata->width) * userdata->zoom; - userdata->renderer.uniform.mvp.e[1][1] = (2.0f / userdata->height) * userdata->zoom; - userdata->renderer.uniform.mvp.e[0][3] = (2.0f / userdata->width) * userdata->pan.x; - userdata->renderer.uniform.mvp.e[1][3] = (2.0f / userdata->height) * userdata->pan.y; + const float w = (float)userdata->width; + const float h = (float)userdata->height; + const float z = userdata->zoom; + const float px = userdata->pan[0]; + const float py = userdata->pan[1]; + mat4 *m = &userdata->renderer.uniform.mvp; + + (*m)[0][0] = (2.0f / w) * z; + (*m)[0][1] = 0.0f; + (*m)[0][2] = 0.0f; + (*m)[0][3] = (2.0f / w) * px; + + (*m)[1][0] = 0.0f; + (*m)[1][1] = (2.0f / h) * z; + (*m)[1][2] = 0.0f; + (*m)[1][3] = (2.0f / h) * py; + + (*m)[2][0] = 0.0f; + (*m)[2][1] = 0.0f; + (*m)[2][2] = 0.0f; + (*m)[2][3] = 0.0f; + + (*m)[3][0] = 0.0f; + (*m)[3][1] = 0.0f; + (*m)[3][2] = 0.0f; + (*m)[3][3] = 1.0f; +} +static void screen_to_world(userdata_t *ud, float mx, float my, float *wx, float *wy) +{ + const float sx = mx - ud->width * 0.5f; + const float sy = ud->height * 0.5f - my; + *wx = (sx - ud->pan[0]) / ud->zoom; + *wy = (sy - ud->pan[1]) / ud->zoom; +} + +static void log_capture(const char* tag, uint32_t log_level, uint32_t log_item, + const char* message, uint32_t line_nr, const char* filename, + void* user_data) +{ + userdata_t* ud = (userdata_t*)user_data; + + const char* level_str; + switch (log_level) { + case 0: level_str = "panic"; break; + case 1: level_str = "error"; break; + case 2: level_str = "warn"; break; + default:level_str = "info"; break; + } + + char buf[256]; + int n = snprintf(buf, sizeof(buf), "[%s][%s][id:%u]", tag, level_str, log_item); + if (filename) { + n += snprintf(buf + n, sizeof(buf) - n, " %s:%u:", filename, line_nr); + } + if (message && n < (int)sizeof(buf) - 2) { + snprintf(buf + n, sizeof(buf) - n, " %s", message); + } + + int idx = ud->log_head; + strncpy(ud->log_ring[idx].text, buf, 255); + ud->log_ring[idx].text[255] = 0; + ud->log_ring[idx].level = log_level; + ud->log_head = (idx + 1) % LOG_RING_SIZE; + if (ud->log_count < LOG_RING_SIZE) ud->log_count++; + + fprintf(stderr, "%s\n", buf); + if (log_level <= 1) ud->log_show = true; } static void frame(void* _userdata) @@ -60,30 +141,166 @@ static void frame(void* _userdata) .swapchain = sglue_swapchain(), }); - const uint32_t length = vector_length(&userdata->manager.textures); - if(length > 0) + for (int i = 0; i < userdata->shapes.count; i++) { + shape_draw((shape_t*) vec_get(&userdata->shapes, i), &userdata->renderer.uniform.mvp); + } + + // Draw selection overlay (marquee during drag, AABB for multi-select) { - sg_apply_pipeline(userdata->renderer.pipeline); + float omin[2], omax[2]; + bool has_overlay = false; - for(uint32_t i = 0; i < length; i++) - { - texture_t* texture = (texture_t*) vector_get(&userdata->manager.textures, i); - - if(texture->dirty) - { - const sg_range range = vector_range(&texture->sprites); - sg_buffer buf = sg_query_view_buffer(texture->binding.views[1]); - sg_update_buffer(buf, &range); - texture->dirty = false; + if (userdata->selecting && userdata->sel_dragging) { + float wx1, wy1, wx2, wy2; + screen_to_world(userdata, userdata->sel_sx, userdata->sel_sy, &wx1, &wy1); + screen_to_world(userdata, userdata->sel_cx, userdata->sel_cy, &wx2, &wy2); + omin[0] = fminf(wx1, wx2); omin[1] = fminf(wy1, wy2); + omax[0] = fmaxf(wx1, wx2); omax[1] = fmaxf(wy1, wy2); + has_overlay = true; + } else if (userdata->selected_count >= 2) { + bool first = true; + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + if (!s->selected) continue; + for (uint32_t v = 0; v < s->num_verts; v++) { + float vx = s->verts[v].x, vy = s->verts[v].y; + if (first) { + omin[0] = omax[0] = vx; + omin[1] = omax[1] = vy; + first = false; + } else { + if (vx < omin[0]) omin[0] = vx; + if (vx > omax[0]) omax[0] = vx; + if (vy < omin[1]) omin[1] = vy; + if (vy > omax[1]) omax[1] = vy; + } + } } + float pad = 8.0f / userdata->zoom; + omin[0] -= pad; omin[1] -= pad; + omax[0] += pad; omax[1] += pad; + has_overlay = true; + } - sg_apply_bindings(&texture->binding); - sg_apply_uniforms(0, &SG_RANGE(userdata->renderer.uniform)); + if (has_overlay) { + shape_vertex_t rect_verts[4] = { + {omin[0], omin[1]}, {omax[0], omin[1]}, {omax[0], omax[1]}, {omin[0], omax[1]}, + }; + sg_update_buffer(userdata->rect_vbuf, &(sg_range){rect_verts, sizeof(rect_verts)}); - sg_draw(0, 6, vector_length(&texture->sprites)); + shape_uniform_t u; + glm_mat4_identity(u.transform); + u.base_color[0] = 0.3f; u.base_color[1] = 0.5f; + u.base_color[2] = 1.0f; u.base_color[3] = 0.35f; + u.state = 0; + memset(u._pad, 0, sizeof(u._pad)); + + sg_apply_pipeline(overlay_pipeline); + sg_apply_uniforms(0, &SG_RANGE(userdata->renderer.uniform.mvp)); + sg_apply_uniforms(1, &SG_RANGE(u)); + sg_apply_bindings(&(sg_bindings){ + .vertex_buffers[0] = userdata->rect_vbuf, + .index_buffer = userdata->rect_ibuf, + }); + sg_draw(0, 6, 1); } } + simgui_new_frame(&(simgui_frame_desc_t){ + .width = sapp_width(), + .height = sapp_height(), + .delta_time = sapp_frame_duration(), + .dpi_scale = sapp_dpi_scale(), + }); + + // Properties panel + { + const float panel_w = 250.0f; + igSetNextWindowPos((ImVec2){userdata->width - panel_w, 20.0f}, ImGuiCond_Always, (ImVec2){0, 0}); + igSetNextWindowSize((ImVec2){panel_w, userdata->height - 20.0f}, ImGuiCond_Always); + igBegin("Properties", NULL, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + if (userdata->selected_count == 0) { + igText("No shape selected"); + } else if (userdata->selected_count > 1) { + igText("%d shapes selected", userdata->selected_count); + } else { + int idx = 0; + while (idx < userdata->shapes.count) { + shape_t *tmp = (shape_t*) vec_get(&userdata->shapes, idx); + if (tmp->selected) break; + idx++; + } + shape_t *s = (shape_t*) vec_get(&userdata->shapes, idx); + bool changed = false; + + changed |= igDragFloat2("Position", &s->cx, 1.0f, 0, 0, "%.1f", 0); + changed |= igDragFloat2("Scale", &s->sx, 1.0f, 0.1f, 0, "%.1f", 0); + changed |= igDragFloat("Rotation", &s->rotation, 0.01f, 0, 0, "%.3f", 0); + igColorEdit4("Color", s->uniform.base_color, 0); + + if (changed) shape_regenerate(s); + + igSeparator(); + + igBeginDisabled(true); + { + float dummy = 0; + igDragFloat("Height", &dummy, 1.0f, 0, 0, "%.1f", 0); + igDragFloat("Biome", &dummy, 1.0f, 0, 0, "%.1f", 0); + igDragFloat("Roughness", &dummy, 1.0f, 0, 0, "%.1f", 0); + igDragFloat("Intensity", &dummy, 1.0f, 0, 0, "%.0f", 0); + } + igEndDisabled(); + + igSeparator(); + + if (igButton("Shake Landmass", (ImVec2){0, 0})) { + // dummy for now + } + } + + igEnd(); + } + + // Log panel + if (userdata->log_show) { + igSetNextWindowPos((ImVec2){10.0f, userdata->height - 200.0f}, ImGuiCond_FirstUseEver, (ImVec2){0, 0}); + igSetNextWindowSize((ImVec2){400.0f, 180.0f}, ImGuiCond_FirstUseEver); + igBegin("Log", &userdata->log_show, 0); + if (igButton("Clear", (ImVec2){0, 0})) { + userdata->log_head = 0; + userdata->log_count = 0; + } + igSameLine(0.0f, 10.0f); + igText("%d entries", userdata->log_count); + igSeparator(); + + igBeginChild_Str("LogScroll", (ImVec2){0, 0}, false, 0); + int total = userdata->log_count < LOG_RING_SIZE ? userdata->log_count : LOG_RING_SIZE; + int start = userdata->log_count < LOG_RING_SIZE ? 0 : userdata->log_head; + for (int i = 0; i < total; i++) { + int idx = (start + i) % LOG_RING_SIZE; + log_entry_t *e = &userdata->log_ring[idx]; + ImVec4 color; + switch (e->level) { + case 0: color = (ImVec4){1.0f, 0.3f, 0.3f, 1.0f}; break; // panic: red + case 1: color = (ImVec4){1.0f, 0.5f, 0.3f, 1.0f}; break; // error: orange + case 2: color = (ImVec4){1.0f, 0.9f, 0.3f, 1.0f}; break; // warn: yellow + default:color = (ImVec4){0.7f, 0.7f, 0.7f, 1.0f}; break; // info: gray + } + igPushStyleColor_Vec4(ImGuiCol_Text, color); + igTextUnformatted(e->text, NULL); + igPopStyleColor(1); + } + if (total > 0) igSetScrollHereY(1.0f); + igEndChild(); + igEnd(); + } + + simgui_render(); + sg_end_pass(); sg_commit(); } @@ -96,21 +313,23 @@ static void init(void* _userdata) sg_desc sgdesc = { .environment = sglue_environment(), - .logger.func = slog_func, + .logger.func = log_capture, + .logger.user_data = userdata, }; sg_setup(&sgdesc); if (!sg_isvalid()) { fprintf(stderr, "Failed to create Sokol GFX context!\n"); exit(-1); } + simgui_setup(&(simgui_desc_t){0}); - const vec2f quad[4] = { + const vec2 quad[4] = { {-2.0f, 2.0f}, // bottom left {2.0f, 2.0f}, // bottom right {2.0f, -2.0f}, // top right {-2.0f, -2.0f}, // top left }; - const vec2f uv[4] = { + const vec2 uv[4] = { {0.0f, 1.0f}, // bottom left {1.0f, 1.0f}, // bottom right {1.0f, 0.0f}, // top right @@ -122,7 +341,7 @@ static void init(void* _userdata) userdata->width = sapp_width(); userdata->height = sapp_height(); - userdata->pan = (vec2f) { 0.0f, 0.0f }; + glm_vec2_zero(userdata->pan); userdata->zoom = 2; sg_shader sprite_shader = sg_make_shader(&(sg_shader_desc) { @@ -201,7 +420,37 @@ static void init(void* _userdata) } }; - gs_init(&userdata->manager); + shape_init_pipeline(); + + vec_init(&userdata->shapes, sizeof(shape_t)); + userdata->selected_count = 0; + userdata->hovered_shape = -1; + userdata->selecting = false; + userdata->sel_dragging = false; + userdata->log_head = 0; + userdata->log_count = 0; + userdata->log_show = false; + + { + shape_vertex_t dummy[4] = {0}; + uint16_t indices[6] = {0, 1, 2, 0, 2, 3}; + userdata->rect_vbuf = sg_make_buffer(&(sg_buffer_desc){ + .usage = { .stream_update = true }, + .data = {dummy, sizeof(dummy)}, + .label = "Sel rect verts", + }); + userdata->rect_ibuf = sg_make_buffer(&(sg_buffer_desc){ + .usage = {.index_buffer = true}, + .data = {indices, sizeof(indices)}, + .label = "Sel rect indices", + }); + } + + *((shape_t*) vec_push(&userdata->shapes)) = shape_star(0.0f, 0.0f, 200.0f, 80.0f, 7, + (float[4]){ 0.0f, 0.94f, 1.0f, 1.0f }); + + *((shape_t*) vec_push(&userdata->shapes)) = shape_circle(300.0f, 0.0f, 120.0f, + (float[4]){ 1.0f, 0.47f, 0.0f, 1.0f }); compute_mvp(userdata); } @@ -210,52 +459,73 @@ static void cleanup(void* _userdata) { userdata_t* userdata = (userdata_t*) _userdata; - vector_free(&userdata->manager.textures); + for (int i = 0; i < userdata->shapes.count; i++) { + shape_shutdown((shape_t*) vec_get(&userdata->shapes, i)); + } + vec_free(&userdata->shapes); + sg_destroy_buffer(userdata->rect_vbuf); + sg_destroy_buffer(userdata->rect_ibuf); + shape_shutdown_pipeline(); FREE(userdata); + simgui_shutdown(); sg_shutdown(); } static void event(const sapp_event* event, void* _userdata) { + if (simgui_handle_event(event)) return; userdata_t* userdata = (userdata_t*) _userdata; switch(event->type) { - case SAPP_EVENTTYPE_FILES_DROPPED: - uint32_t files = sapp_get_num_dropped_files(); - - for(uint32_t i = 0; i < files; i++) - { - const uint32_t size = sapp_html5_get_dropped_file_size(i); - - if(size > MAX_FILE_SIZE) - { - //Toast error - continue; - } - - void* buffer = malloc(size); - - sapp_html5_fetch_dropped_file(&(sapp_html5_fetch_request) { - .buffer = (sapp_range) { .ptr = buffer, .size = size }, - .dropped_file_index = i, - .callback = gs_import_file, - .user_data = userdata - }); - } - - break; case SAPP_EVENTTYPE_RESIZED: userdata->width = sapp_width(); userdata->height = sapp_height(); - + compute_mvp(userdata); break; case SAPP_EVENTTYPE_MOUSE_DOWN: - if(event->modifiers & SAPP_MODIFIER_RMB) + if (event->mouse_button == SAPP_MOUSEBUTTON_LEFT) + { + float wx, wy; + screen_to_world(userdata, event->mouse_x, event->mouse_y, &wx, &wy); + const float tol = 4.0f / userdata->zoom; + + if (event->modifiers & SAPP_MODIFIER_CTRL) { + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + if (shape_hit_test(s, wx, wy, tol)) { + s->selected = !s->selected; + userdata->selected_count += s->selected ? 1 : -1; + break; + } + } + } else { + userdata->selecting = true; + userdata->sel_dragging = false; + userdata->sel_sx = event->mouse_x; + userdata->sel_sy = event->mouse_y; + userdata->sel_cx = event->mouse_x; + userdata->sel_cy = event->mouse_y; + + userdata->sel_clicked_shape = -1; + for (int i = 0; i < userdata->shapes.count; i++) { + if (shape_hit_test((shape_t*) vec_get(&userdata->shapes, i), wx, wy, tol)) { + userdata->sel_clicked_shape = i; + break; + } + } + } + + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + shape_set_state(s, s->hovered, s->selected); + } + } + else if (event->modifiers & SAPP_MODIFIER_RMB) { userdata->dragger.dragging = true; userdata->dragger.origin_x = event->mouse_x; @@ -264,29 +534,133 @@ static void event(const sapp_event* event, void* _userdata) break; case SAPP_EVENTTYPE_MOUSE_UP: + if (userdata->selecting) { + if (!userdata->sel_dragging) { + if (userdata->sel_clicked_shape >= 0) { + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + s->selected = false; + } + ((shape_t*) vec_get(&userdata->shapes, userdata->sel_clicked_shape))->selected = true; + userdata->selected_count = 1; + } else { + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + s->selected = false; + } + userdata->selected_count = 0; + } + } + + userdata->selecting = false; + userdata->sel_dragging = false; + + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + shape_set_state(s, s->hovered, s->selected); + } + } + userdata->dragger.dragging = false; break; case SAPP_EVENTTYPE_MOUSE_MOVE: - if(userdata->dragger.dragging) + if (userdata->dragger.dragging) { - userdata->pan.x += event->mouse_dx; - userdata->pan.y -= event->mouse_dy; + userdata->pan[0] += event->mouse_dx; + userdata->pan[1] -= event->mouse_dy; compute_mvp(userdata); } + else if (userdata->selecting) + { + userdata->sel_cx = event->mouse_x; + userdata->sel_cy = event->mouse_y; + float dx = userdata->sel_cx - userdata->sel_sx; + float dy = userdata->sel_cy - userdata->sel_sy; + if (dx * dx + dy * dy > 9.0f) { + userdata->sel_dragging = true; + } + + if (userdata->sel_dragging) { + float wx1, wy1, wx2, wy2; + screen_to_world(userdata, userdata->sel_sx, userdata->sel_sy, &wx1, &wy1); + screen_to_world(userdata, userdata->sel_cx, userdata->sel_cy, &wx2, &wy2); + float min_x = fminf(wx1, wx2), min_y = fminf(wy1, wy2); + float max_x = fmaxf(wx1, wx2), max_y = fmaxf(wy1, wy2); + + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + s->selected = false; + } + userdata->selected_count = 0; + + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + bool hit = false; + if (s->cx >= min_x && s->cx <= max_x && s->cy >= min_y && s->cy <= max_y) + hit = true; + for (uint32_t v = 0; !hit && v < s->num_verts; v++) { + if (s->verts[v].x >= min_x && s->verts[v].x <= max_x && + s->verts[v].y >= min_y && s->verts[v].y <= max_y) + hit = true; + } + if (hit) { + s->selected = true; + userdata->selected_count++; + } + } + + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + shape_set_state(s, false, s->selected); + } + } + } + else + { + float wx, wy; + screen_to_world(userdata, event->mouse_x, event->mouse_y, &wx, &wy); + const float tol = SHAPE_HOVER_PX / userdata->zoom; + + int hovered = -1; + for (int i = 0; i < userdata->shapes.count; i++) { + if (shape_hit_test((shape_t*) vec_get(&userdata->shapes, i), wx, wy, tol)) { + hovered = i; + break; + } + } + if (hovered != userdata->hovered_shape) { + userdata->hovered_shape = hovered; + emscripten_run_script( + hovered >= 0 + ? "document.querySelector('canvas').style.cursor = 'pointer'" + : "document.querySelector('canvas').style.cursor = 'default'"); + } + for (int i = 0; i < userdata->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&userdata->shapes, i); + shape_set_state(s, i == hovered, s->selected); + } + } break; - case SAPP_EVENTTYPE_MOUSE_SCROLL: + case SAPP_EVENTTYPE_MOUSE_SCROLL: { if((userdata->zoom >= 6.0f && event->scroll_y > 0.0f) || (userdata->zoom <= 0.1f && event->scroll_y < 0.0f)) return; - const float diff = expf(event->scroll_y * 0.01f); + float wx, wy; + screen_to_world(userdata, event->mouse_x, event->mouse_y, &wx, &wy); + + const float diff = expf(event->scroll_y * 0.1f); userdata->zoom = _sg_clamp(userdata->zoom * diff, 0.1f, 6.0f); - compute_mvp(userdata); + const float sx = event->mouse_x - userdata->width * 0.5f; + const float sy = userdata->height * 0.5f - event->mouse_y; + userdata->pan[0] = sx - wx * userdata->zoom; + userdata->pan[1] = sy - wy * userdata->zoom; - break; + compute_mvp(userdata); + } break; default: break; } diff --git a/src/math.h b/src/math.h deleted file mode 100644 index d3a03d5..0000000 --- a/src/math.h +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/shaders/shape.wgsl b/src/shaders/shape.wgsl new file mode 100644 index 0000000..1c0776a --- /dev/null +++ b/src/shaders/shape.wgsl @@ -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 vs_uniforms: VsUniform; +@binding(1) @group(0) var 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; +} diff --git a/src/shape.h b/src/shape.h new file mode 100644 index 0000000..c15c730 --- /dev/null +++ b/src/shape.h @@ -0,0 +1,315 @@ +#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; +} shape_t; + +#define SHAPE_HOVER_PX 6.0f + +static sg_pipeline shape_pipeline; +static sg_pipeline overlay_pipeline; +static sg_shader shape_shader; + +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", + }); +} + +static void shape_shutdown_pipeline(void) +{ + sg_destroy_pipeline(shape_pipeline); + sg_destroy_pipeline(overlay_pipeline); + sg_destroy_shader(shape_shader); +} + +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; +} + +static void shape_init_common(shape_t *s, const float color[4]) +{ + s->hovered = false; + s->selected = false; + glm_mat4_identity(s->uniform.transform); + memcpy(s->uniform.base_color, color, sizeof(float[4])); + s->uniform.state = 0; + memset(s->uniform._pad, 0, sizeof(s->uniform._pad)); +} + +static void shape_make_buffers(shape_t *s) +{ + s->vbuf = sg_make_buffer(&(sg_buffer_desc) { + .data = { s->verts, s->num_indices * sizeof(shape_vertex_t) }, + .label = "Shape vertices", + }); + s->ibuf = sg_make_buffer(&(sg_buffer_desc) { + .usage = { .index_buffer = true }, + .data = { s->indices, s->num_indices * sizeof(uint16_t) }, + .label = "Shape indices", + }); +} + +static void shape_shutdown(shape_t *s) +{ + sg_destroy_buffer(s->vbuf); + sg_destroy_buffer(s->ibuf); + FREE(s->verts); + FREE(s->indices); +} + +static void shape_regenerate(shape_t *s) +{ + sg_destroy_buffer(s->vbuf); + sg_destroy_buffer(s->ibuf); + + int n, count; + if (s->kind == SHAPE_CIRCLE) { + int segs = shape_calc_segments(s->sx); + n = segs; + count = segs + 1; + + if (s->num_indices != (uint32_t)count) { + 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)); + } + + for (int i = 0; i < segs; i++) { + float a = (float)i / (float)segs * 2.0f * GLM_PIf - GLM_PI_2f + s->rotation; + s->verts[i] = (shape_vertex_t) { + s->cx + cosf(a) * s->sx, + s->cy + sinf(a) * s->sy, + }; + } + s->verts[segs] = s->verts[0]; + } else { + n = s->star_points * 2; + count = n + 1; + + if (s->num_indices != (uint32_t)count) { + 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)); + } + + for (int i = 0; i < n; i++) { + float a = (float)i / (float)n * 2.0f * GLM_PIf - GLM_PI_2f + s->rotation; + float r = (i & 1) ? s->star_inner_ratio * s->sx : s->sx; + s->verts[i] = (shape_vertex_t) { + s->cx + cosf(a) * r, + s->cy + 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_make_buffers(s); +} + +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); +} + +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; +} + +static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol) +{ + float tol_sq = world_tol * world_tol; + + if (point_in_polygon(wx, wy, 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 = ((wx - ax) * abx + (wy - ay) * aby) / len_sq; + t = fmaxf(0.0f, fminf(1.0f, t)); + float cx = ax + t * abx, cy = ay + t * aby; + float dx = wx - cx, dy = wy - cy; + if (dx * dx + dy * dy <= tol_sq) return true; + } + return false; +} + +static void shape_draw(shape_t *s, const mat4 *mvp) +{ + sg_apply_pipeline(shape_pipeline); + 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); +} + +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) { + x + cosf(a) * r, + y + sinf(a) * r, + }; + } + 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_make_buffers(&s); + return s; +} + +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) ? inner_r : outer_r; + s.verts[i] = (shape_vertex_t) { + x + cosf(a) * r, + y + 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_make_buffers(&s); + return s; +} + +#endif diff --git a/src/sprite.h b/src/sprite.h deleted file mode 100644 index 7f4bf9e..0000000 --- a/src/sprite.h +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/util.h b/src/util.h index f746ef9..5342dd7 100644 --- a/src/util.h +++ b/src/util.h @@ -1,241 +1,61 @@ #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 +#include 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); +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); +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); +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; +static void vec_pop(vector_t *v) { + if (v->count > 0) v->count--; +} - pool->free = (uint32_t) pool->data[index * pool->stripe]; - - return index; +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); } - else - { - if(pool->size >= pool->capacity) - { - pool->capacity *= 2; - - pool->data = (void**) realloc(pool->data, pool->capacity * pool->stripe); - } - - return pool->size++; - } -} -static const void* pool_get(mem_pool_t *pool, uint32_t index) -{ - assert(index > 0); - assert(index < pool->capacity); - - return pool->data[index * pool->stripe]; -} -static void pool_remove(mem_pool_t *pool, uint32_t index) -{ - const uint32_t pos = index * pool->stripe; - - pool->data[pos] = (void*) pool->free; - pool->free = index; + v->count--; } -#endif \ No newline at end of file +static void *vec_get(vector_t *v, int index) { + return v->data + index * v->stride; +} + +static void vec_free(vector_t *v) { + if (v->data) FREE(v->data); + v->data = NULL; + v->count = 0; + v->capacity = 0; +} + +#endif