From e71641c09438fc83f727ba6206fa7faff067cec9 Mon Sep 17 00:00:00 2001 From: Peaceultime Date: Thu, 30 Apr 2026 17:55:46 +0200 Subject: [PATCH] Add a geometry pool, shape hierarchy, addittion and deletion. --- src/api.h | 6 + src/draw.h | 72 +++ src/generated/shape.h | 156 +++--- src/history.h | 256 +++++---- src/input.h | 894 ++++++++++++++++++++++++++++++ src/interact.h | 144 +++++ src/main.c | 1166 ++-------------------------------------- src/overlay.h | 144 +++++ src/render.h | 6 +- src/shaders/shape.wgsl | 5 +- src/shape.h | 184 ++++++- src/types.h | 133 +++++ src/ui_panels.h | 425 +++++++++++++++ src/util.h | 23 + 14 files changed, 2273 insertions(+), 1341 deletions(-) create mode 100644 src/draw.h create mode 100644 src/input.h create mode 100644 src/interact.h create mode 100644 src/overlay.h create mode 100644 src/types.h create mode 100644 src/ui_panels.h diff --git a/src/api.h b/src/api.h index 1520c40..dd80adf 100644 --- a/src/api.h +++ b/src/api.h @@ -33,6 +33,12 @@ #include "render.h" #include "spatial.h" #include "history.h" +#include "types.h" +#include "interact.h" +#include "overlay.h" +#include "draw.h" +#include "ui_panels.h" +#include "input.h" #include #include diff --git a/src/draw.h b/src/draw.h new file mode 100644 index 0000000..17ad292 --- /dev/null +++ b/src/draw.h @@ -0,0 +1,72 @@ +#ifndef DRAW_H +#define DRAW_H + +#include "api.h" +#include "types.h" + +static void draw_shapes(userdata_t *ud) +{ + if (g_shape_pool_dirty) + shape_pool_rebuild(&ud->shapes); + + if (ud->shapes.count == 0) return; + + sg_apply_pipeline(shape_pipeline); + sg_apply_bindings(&(sg_bindings){ + .vertex_buffers[0] = g_shape_vbuf, + .index_buffer = g_shape_ibuf, + }); + for (int i = 0; i < ud->shapes.count; i++) { + shape_draw((shape_t*) vec_get(&ud->shapes, i), &ud->renderer.uniform.mvp); + } +} + +static void draw_overlay_and_handles(userdata_t *ud, bool has_overlay, bool show_handle) +{ + if (has_overlay) { + shape_uniform_t u; + glm_mat4_identity(u.transform); + u.state = 0; + memset(u._pad, 0, sizeof(u._pad)); + + sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp)); + sg_apply_uniforms(1, &SG_RANGE(u)); + sg_apply_bindings(&(sg_bindings){ + .vertex_buffers[0] = ud->rect_vbuf, + .index_buffer = ud->rect_ibuf, + }); + sg_draw(0, 5, 1); + } + + if (show_handle) { + shape_uniform_t hu; + glm_mat4_identity(hu.transform); + hu.state = 0; + memset(hu._pad, 0, sizeof(hu._pad)); + + sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp)); + sg_apply_uniforms(1, &SG_RANGE(hu)); + sg_apply_bindings(&(sg_bindings){ + .vertex_buffers[0] = ud->handle_vbuf, + .index_buffer = ud->handle_ibuf, + }); + sg_draw(0, HANDLE_CIRCLE_SEGMENTS + 1, 1); + + { + shape_uniform_t cu; + glm_mat4_identity(cu.transform); + cu.state = 0; + memset(cu._pad, 0, sizeof(cu._pad)); + + sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp)); + sg_apply_uniforms(1, &SG_RANGE(cu)); + sg_apply_bindings(&(sg_bindings){ + .vertex_buffers[0] = ud->corner_vbuf, + .index_buffer = ud->corner_ibuf, + }); + for (int h = 0; h < 8; h++) sg_draw(h * 5, 5, 1); + } + } +} + +#endif diff --git a/src/generated/shape.h b/src/generated/shape.h index a0a0ac7..c5e95d0 100644 --- a/src/generated/shape.h +++ b/src/generated/shape.h @@ -6,90 +6,86 @@ unsigned char src_shaders_shape_wgsl[] = { 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, + 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, 0x76, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, - 0x3a, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3b, - 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31, 0x29, - 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29, 0x20, 0x76, - 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3e, 0x20, - 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, - 0x6d, 0x3a, 0x20, 0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69, 0x66, - 0x6f, 0x72, 0x6d, 0x3b, 0x0a, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74, 0x65, - 0x78, 0x20, 0x66, 0x6e, 0x20, 0x76, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, - 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x49, 0x6e, - 0x29, 0x20, 0x2d, 0x3e, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20, 0x7b, - 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, - 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b, 0x0a, - 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, 0x72, 0x6c, - 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, 0x70, - 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x74, 0x72, - 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x2a, 0x20, 0x76, 0x65, - 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, - 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69, 0x6e, - 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30, - 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, - 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x73, 0x5f, 0x75, - 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, 0x20, - 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x3b, - 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, + 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, + 0x72, 0x6d, 0x3a, 0x20, 0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69, + 0x66, 0x6f, 0x72, 0x6d, 0x3b, 0x0a, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74, + 0x65, 0x78, 0x20, 0x66, 0x6e, 0x20, 0x76, 0x73, 0x5f, 0x6d, 0x61, 0x69, + 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x49, + 0x6e, 0x29, 0x20, 0x2d, 0x3e, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20, + 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, 0x72, + 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, + 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x2a, 0x20, 0x76, + 0x65, 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, + 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x73, 0x5f, + 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, + 0x20, 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, + 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, + 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, + 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, + 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x31, 0x2e, 0x30, 0x2c, + 0x20, 0x30, 0x2e, 0x38, 0x34, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, + 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, + 0x65, 0x6c, 0x73, 0x65, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, 0x20, + 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x75, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x3d, - 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 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, + 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, 0x35, 0x2c, 0x20, + 0x30, 0x2e, 0x36, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, + 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, 0x6c, + 0x73, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, + 0x72, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, + 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, + 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x40, + 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20, + 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, 0x3e, + 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3a, + 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, - 0x20, 0x3d, 0x20, 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 + 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, + 0x6f, 0x72, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, + 0x72, 0x6e, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d, + 0x0a }; -unsigned int src_shaders_shape_wgsl_len = 1103; +unsigned int src_shaders_shape_wgsl_len = 1045; diff --git a/src/history.h b/src/history.h index 85a2823..cfcf0aa 100644 --- a/src/history.h +++ b/src/history.h @@ -3,143 +3,108 @@ #include "api.h" -// Each property kind we can undo/redo independently typedef enum { HIST_POSITION, HIST_SCALE, HIST_ROTATION, - HIST_COLOR, + HIST_CREATE, + HIST_DELETE, + HIST_GROUP, } hist_prop_t; -// One property change on one shape (old → new) typedef struct hist_change_t { int shape_index; hist_prop_t prop; float old_val[4]; float new_val[4]; + + // Owned vertex+index buffer snapshot — only used for HIST_CREATE / HIST_DELETE. + // Freed when the history entry is discarded or the stack is destroyed. + shape_vertex_t *vertex_data; + uint16_t *index_data; + int vertex_count; + int index_count; } hist_change_t; -// A history entry is one or more changes batched together. -// Single-property edits = 1 change. Whole-selection edits = N changes. typedef struct hist_entry_t { hist_change_t *changes; int count; } hist_entry_t; -#define HIST_MAX 64 - typedef struct history_t { - hist_entry_t entries[HIST_MAX]; - int count; - int current; // index of last applied entry, -1 = initial state + vector_t entries; + int current; - // Pending edit session (one ImGui widget interaction) bool capturing; int pending_shape_idx; hist_prop_t pending_prop; float pending_old[4]; } history_t; -// -- internal helpers -- +// -- helpers -- -/** - * Read the current value of a single property from a shape. - * - * @param s shape to read from - * @param prop which property (HIST_POSITION, HIST_SCALE, etc.) - * @param out receives the value, zero-padded to 4 floats - */ static void hist_read_prop(shape_t *s, hist_prop_t prop, float out[4]) { memset(out, 0, sizeof(float[4])); switch (prop) { case HIST_POSITION: out[0] = s->cx; out[1] = s->cy; break; case HIST_SCALE: out[0] = s->sx; out[1] = s->sy; break; case HIST_ROTATION: out[0] = s->rotation; break; - case HIST_COLOR: memcpy(out, s->uniform.base_color, sizeof(float[4])); break; + case HIST_GROUP: out[0] = (float)s->group_id; break; + default: break; } } -/** - * Write a value to a single property of a shape. Does NOT regenerate buffers. - * - * @param s shape to modify in-place - * @param prop which property to set - * @param val new value (4 floats, zero-padded for smaller properties) - */ static void hist_apply_prop(shape_t *s, hist_prop_t prop, const float val[4]) { switch (prop) { case HIST_POSITION: s->cx = val[0]; s->cy = val[1]; break; case HIST_SCALE: s->sx = val[0]; s->sy = val[1]; break; case HIST_ROTATION: s->rotation = val[0]; break; - case HIST_COLOR: memcpy(s->uniform.base_color, val, sizeof(float[4])); break; + case HIST_GROUP: s->group_id = (int)val[0]; break; + default: break; } } +static void hist_free_change(hist_change_t *c) { + if (c->vertex_data) FREE(c->vertex_data); + if (c->index_data) FREE(c->index_data); + memset(c, 0, sizeof(*c)); +} -/** - * Zero-initialize the history stack. Call once during app init. - * - * @param h history to initialize - */ static void history_init(history_t *h) { memset(h, 0, sizeof(*h)); + vec_init(&h->entries, sizeof(hist_entry_t)); h->current = -1; } -/** - * Free all heap memory held by the history stack. Call during app shutdown. - * - * @param h history to destroy - */ static void history_destroy(history_t *h) { - for (int i = 0; i < h->count; i++) { - if (h->entries[i].changes) FREE(h->entries[i].changes); + for (int i = 0; i < h->entries.count; i++) { + hist_entry_t *e = (hist_entry_t*) vec_get(&h->entries, i); + if (e->changes) { + for (int j = 0; j < e->count; j++) + hist_free_change(&e->changes[j]); + FREE(e->changes); + } } + vec_free(&h->entries); memset(h, 0, sizeof(*h)); h->current = -1; } -/** - * Push a completed entry onto the stack, discarding any redo branch. - * Takes ownership of entry.changes (must be heap-allocated with ALLOC). - * Used internally by begin_edit/end_edit, or directly for batch edits. - * - * @param h history stack - * @param entry entry to push (changes array is consumed, not copied) - */ static void history_push_entry(history_t *h, hist_entry_t entry) { - while (h->count > h->current + 1) { - h->count--; - if (h->entries[h->count].changes) { - FREE(h->entries[h->count].changes); - h->entries[h->count].changes = NULL; + while (h->entries.count > h->current + 1) { + hist_entry_t *e = (hist_entry_t*) vec_get(&h->entries, h->entries.count - 1); + if (e->changes) { + for (int j = 0; j < e->count; j++) + hist_free_change(&e->changes[j]); + FREE(e->changes); } + vec_pop(&h->entries); } - if (h->count >= HIST_MAX) { - if (h->entries[0].changes) FREE(h->entries[0].changes); - memmove(&h->entries[0], &h->entries[1], - (h->count - 1) * sizeof(hist_entry_t)); - h->count--; - h->current--; - } - - h->entries[h->count] = entry; - h->count++; - h->current = h->count - 1; + *((hist_entry_t*) vec_push(&h->entries)) = entry; + h->current = h->entries.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) { @@ -147,15 +112,13 @@ static void history_begin_edit(history_t *h, vector_t *shapes, float new_val[4]; hist_read_prop(s, h->pending_prop, new_val); if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) { - hist_change_t change = { - .shape_index = h->pending_shape_idx, - .prop = h->pending_prop, - }; - memcpy(change.old_val, h->pending_old, sizeof(float[4])); - memcpy(change.new_val, new_val, sizeof(float[4])); hist_entry_t entry = { .changes = NULL, .count = 1 }; entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t)); - *entry.changes = change; + memset(entry.changes, 0, sizeof(hist_change_t)); + entry.changes->shape_index = h->pending_shape_idx; + entry.changes->prop = h->pending_prop; + memcpy(entry.changes->old_val, h->pending_old, sizeof(float[4])); + memcpy(entry.changes->new_val, new_val, sizeof(float[4])); history_push_entry(h, entry); } h->capturing = false; @@ -168,14 +131,6 @@ static void history_begin_edit(history_t *h, vector_t *shapes, hist_read_prop(s, prop, h->pending_old); } -/** - * End the current edit session and push an entry if the value changed. - * Safe to call when no session is active (no-op). - * Call when igIsAnyItemActive() transitions from true to false. - * - * @param h history stack - * @param shapes the shapes vector (used to read final values) - */ static void history_end_edit(history_t *h, vector_t *shapes) { if (!h->capturing) return; @@ -184,28 +139,19 @@ static void history_end_edit(history_t *h, vector_t *shapes) { hist_read_prop(s, h->pending_prop, new_val); if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) { - hist_change_t change = { - .shape_index = h->pending_shape_idx, - .prop = h->pending_prop, - }; - memcpy(change.old_val, h->pending_old, sizeof(float[4])); - memcpy(change.new_val, new_val, sizeof(float[4])); hist_entry_t entry = { .changes = NULL, .count = 1 }; entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t)); - *entry.changes = change; + memset(entry.changes, 0, sizeof(hist_change_t)); + entry.changes->shape_index = h->pending_shape_idx; + entry.changes->prop = h->pending_prop; + memcpy(entry.changes->old_val, h->pending_old, sizeof(float[4])); + memcpy(entry.changes->new_val, new_val, sizeof(float[4])); history_push_entry(h, entry); } h->capturing = false; } -/** - * Apply every change in an entry to the shapes vector and regenerate buffers. - * - * @param entry the history entry to apply - * @param shapes the shapes vector to modify - * @param forward true to use new_val (redo), false to use old_val (undo) - */ -// -- batch API for multi-shape operations (move, rotate, resize) -- +// -- batch API -- typedef struct { hist_change_t *changes; @@ -215,10 +161,12 @@ typedef struct { static void history_batch_init(hist_batch_t *batch, int count) { batch->changes = (hist_change_t*) ALLOC((size_t)count * sizeof(hist_change_t)); + memset(batch->changes, 0, (size_t)count * sizeof(hist_change_t)); batch->count = 0; batch->capacity = count; } +// For property changes (POSITION, SCALE, ROTATION, GROUP) static void history_batch_add(hist_batch_t *batch, int shape_index, hist_prop_t prop, const float old_val[4], const float new_val[4]) { hist_change_t *c = &batch->changes[batch->count++]; @@ -228,16 +176,100 @@ static void history_batch_add(hist_batch_t *batch, int shape_index, hist_prop_t memcpy(c->new_val, new_val, sizeof(float[4])); } +// Snapshot a shape's full vertex data and metadata into a change entry. +// Used for HIST_CREATE and HIST_DELETE entries. +// old_val = { kind, cx, cy, num_verts } +// new_val = { sx, sy, rotation, group_id } +static void hist_snapshot_shape_verts(hist_change_t *c, shape_t *s) { + int n = (int)s->num_elements; + c->vertex_count = n; + c->index_count = n; + c->vertex_data = (shape_vertex_t*) ALLOC((size_t)n * sizeof(shape_vertex_t)); + c->index_data = (uint16_t*) ALLOC((size_t)n * sizeof(uint16_t)); + memcpy(c->vertex_data, s->verts, (size_t)n * sizeof(shape_vertex_t)); + memcpy(c->index_data, s->indices, (size_t)n * sizeof(uint16_t)); + c->old_val[0] = (float)s->kind; + c->old_val[1] = s->cx; + c->old_val[2] = s->cy; + c->old_val[3] = (float)s->num_verts; + c->new_val[0] = s->sx; + c->new_val[1] = s->sy; + c->new_val[2] = s->rotation; + c->new_val[3] = (float)s->group_id; +} + +// Append a CREATE or DELETE entry to a batch, snapshotting the shape's vertex data. +static void history_batch_add_shape(hist_batch_t *batch, int shape_index, + hist_prop_t prop, shape_t *s) { + hist_change_t *c = &batch->changes[batch->count++]; + c->shape_index = shape_index; + c->prop = prop; + hist_snapshot_shape_verts(c, s); +} + static void history_batch_commit(hist_batch_t *batch, history_t *h) { hist_entry_t entry = { .changes = batch->changes, .count = batch->count }; history_push_entry(h, entry); } -/** - * Apply every change in an entry to the shapes vector and regenerate buffers. - */static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forward) { +// Reconstruct a shape_t from a HIST_CREATE / HIST_DELETE change snapshot. +static shape_t hist_rebuild_shape_from_snapshot(const hist_change_t *c) { + shape_t s; + memset(&s, 0, sizeof(s)); + s.kind = (int)c->old_val[0]; + s.cx = c->old_val[1]; + s.cy = c->old_val[2]; + s.num_verts = (uint32_t)c->old_val[3]; + s.num_elements = (uint32_t)c->vertex_count; + s.sx = c->new_val[0]; + s.sy = c->new_val[1]; + s.rotation = c->new_val[2]; + s.group_id = (int)c->new_val[3]; + + int n = c->vertex_count; + s.verts = (shape_vertex_t*) ALLOC((size_t)n * sizeof(shape_vertex_t)); + s.indices = (uint16_t*) ALLOC((size_t)n * sizeof(uint16_t)); + memcpy(s.verts, c->vertex_data, (size_t)n * sizeof(shape_vertex_t)); + memcpy(s.indices, c->index_data, (size_t)n * sizeof(uint16_t)); + + shape_init_common(&s); + shape_build_transform(&s); + shape_make_buffers(&s); + return s; +} + +static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forward) { + bool has_shape_ops = false; for (int i = 0; i < entry->count; i++) { + hist_prop_t p = entry->changes[i].prop; + if (p == HIST_CREATE || p == HIST_DELETE) { has_shape_ops = true; break; } + } + + int start = 0, end = entry->count, step = 1; + if (has_shape_ops && !forward) { + start = entry->count - 1; + end = -1; + step = -1; + } + + for (int i = start; i != end; i += step) { hist_change_t *c = &entry->changes[i]; + + if (c->prop == HIST_CREATE || c->prop == HIST_DELETE) { + bool adding = (c->prop == HIST_CREATE) ? forward : !forward; + if (adding) { + shape_t s = hist_rebuild_shape_from_snapshot(c); + *((shape_t*) vec_insert(shapes, c->shape_index)) = s; + } else { + if (c->shape_index < shapes->count) { + shape_t *s = (shape_t*) vec_get(shapes, c->shape_index); + shape_shutdown(s); + vec_remove_ordered(shapes, c->shape_index); + } + } + continue; + } + if (c->shape_index >= shapes->count) continue; shape_t *s = (shape_t*) vec_get(shapes, c->shape_index); hist_apply_prop(s, c->prop, forward ? c->new_val : c->old_val); @@ -248,17 +280,15 @@ static void history_batch_commit(hist_batch_t *batch, history_t *h) { static bool history_undo(history_t *h, vector_t *shapes) { if (h->current < 0) return false; - - history_apply_entry(&h->entries[h->current], shapes, false); + history_apply_entry((hist_entry_t*) vec_get(&h->entries, h->current), shapes, false); h->current--; return true; } static bool history_redo(history_t *h, vector_t *shapes) { - if (h->current + 1 >= h->count) return false; - + if (h->current + 1 >= h->entries.count) return false; h->current++; - history_apply_entry(&h->entries[h->current], shapes, true); + history_apply_entry((hist_entry_t*) vec_get(&h->entries, h->current), shapes, true); return true; } diff --git a/src/input.h b/src/input.h new file mode 100644 index 0000000..ef7c48b --- /dev/null +++ b/src/input.h @@ -0,0 +1,894 @@ +#ifndef INPUT_H +#define INPUT_H + +#include "api.h" +#include "types.h" +#include "interact.h" +#include "overlay.h" + +static void handle_left_down_ctrl_click(userdata_t *ud, float wx, float wy, float tol) +{ + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (shape_hit_test(s, wx, wy, tol)) { + if (ud->interact.focused_group_id != 0) { + s->selected = !s->selected; + ud->interact.selected_count += s->selected ? 1 : -1; + } else if (s->group_id != 0) { + int topmost = get_topmost_group(&ud->groups, s->group_id); + toggle_group_recursive(ud, topmost); + } else { + s->selected = !s->selected; + ud->interact.selected_count += s->selected ? 1 : -1; + } + ud->overlay_upload_needed = true; + update_shape_states(ud); + break; + } + } +} + +static void handle_left_down_resize_begin(userdata_t *ud, float wx, float wy, int resize_hit) +{ + float omin[2], omax[2]; + if (ud->interact.aabb_cached) { + omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1]; + omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3]; + } else { + selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); + } + float mid_x = (omin[0] + omax[0]) * 0.5f; + float mid_y = (omin[1] + omax[1]) * 0.5f; + float px[8] = {omax[0], mid_x, omin[0], omin[0], omin[0], mid_x, omax[0], omax[0]}; + float py[8] = {omax[1], omax[1], omax[1], mid_y, omin[1], omin[1], omin[1], mid_y}; + ud->interact.resize.pivot_x = px[resize_hit]; + ud->interact.resize.pivot_y = py[resize_hit]; + ud->interact.resize.start_wx = wx; + ud->interact.resize.start_wy = wy; + ud->interact.resize.total_scale_x = 1.0f; + ud->interact.resize.total_scale_y = 1.0f; + ud->interact.resize.mask_x = (resize_hit == 3 || resize_hit == 7 || (resize_hit & 1) == 0) ? 1.0f : 0.0f; + ud->interact.resize.mask_y = (resize_hit == 1 || resize_hit == 5 || (resize_hit & 1) == 0) ? 1.0f : 0.0f; + ud->interact.resize.dragging = true; + + float sum_sin = 0, sum_cos = 0; + int sel_n = 0; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { sum_sin += sinf(s->rotation); sum_cos += cosf(s->rotation); sel_n++; } + } + ud->interact.resize.angle = atan2f(sum_sin, sum_cos); + + ud->interact.resize.init = (resize_init_t*) ALLOC((size_t)sel_n * sizeof(resize_init_t)); + ud->interact.resize.init_count = sel_n; + int j = 0; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + float sc = cosf(s->rotation), ss = sinf(s->rotation); + float hlx = (ud->interact.resize.start_wx - s->cx) * sc + (ud->interact.resize.start_wy - s->cy) * ss; + float hly = -(ud->interact.resize.start_wx - s->cx) * ss + (ud->interact.resize.start_wy - s->cy) * sc; + float plx = (ud->interact.resize.pivot_x - s->cx) * sc + (ud->interact.resize.pivot_y - s->cy) * ss; + float ply = -(ud->interact.resize.pivot_x - s->cx) * ss + (ud->interact.resize.pivot_y - s->cy) * sc; + + ud->interact.resize.init[j].idx = i; + ud->interact.resize.init[j].init_sx = s->sx; + ud->interact.resize.init[j].init_sy = s->sy; + ud->interact.resize.init[j].init_cx = s->cx; + ud->interact.resize.init[j].init_cy = s->cy; + ud->interact.resize.init[j].ext_x = hlx - plx; + ud->interact.resize.init[j].ext_y = hly - ply; + ud->interact.resize.init[j].lpi_x = plx; + ud->interact.resize.init[j].lpi_y = ply; + j++; + } + } +} + +static void handle_left_down_rotate_begin(userdata_t *ud, float wx, float wy) +{ + ud->interact.rotate.dragging = true; + ud->interact.rotate.start_angle = atan2f( + wy - ud->interact.rotate.center_y, + wx - ud->interact.rotate.center_x); + ud->interact.rotate.total_delta = 0.0f; +} + +static void handle_left_down_move_begin(userdata_t *ud, float wx, float wy) +{ + ud->interact.move.dragging = true; + ud->interact.move.start_wx = wx; + ud->interact.move.start_wy = wy; + ud->interact.move.total_dx = 0; + ud->interact.move.total_dy = 0; +} + +static void handle_left_down_select_or_marquee(userdata_t *ud, const sapp_event *event, float wx, float wy, float tol) +{ + ud->interact.select.active = true; + ud->interact.select.dragging = false; + ud->interact.select.start_x = event->mouse_x; + ud->interact.select.start_y = event->mouse_y; + ud->interact.select.current_x = event->mouse_x; + ud->interact.select.current_y = event->mouse_y; + + ud->interact.select.clicked_shape = -1; + for (int i = 0; i < ud->shapes.count; i++) { + if (shape_hit_test((shape_t*) vec_get(&ud->shapes, i), wx, wy, tol)) { + ud->interact.select.clicked_shape = i; + break; + } + } +} + +static void handle_right_down_pan_begin(userdata_t *ud, const sapp_event *event) +{ + ud->camera.pan_state.dragging = true; + ud->camera.pan_state.origin_x = event->mouse_x; + ud->camera.pan_state.origin_y = event->mouse_y; +} + +static void handle_resize_end(userdata_t *ud) +{ + int n = ud->interact.resize.init_count; + bool changed = false; + for (int j = 0; j < n; j++) { + resize_init_t *ini = &ud->interact.resize.init[j]; + shape_t *s = (shape_t*) vec_get(&ud->shapes, ini->idx); + if (s->sx != ini->init_sx || s->sy != ini->init_sy || + s->cx != ini->init_cx || s->cy != ini->init_cy) + changed = true; + } + if (changed) { + hist_batch_t batch; + history_batch_init(&batch, n * 2); + + for (int j = 0; j < n; j++) { + resize_init_t *ini = &ud->interact.resize.init[j]; + shape_t *s = (shape_t*) vec_get(&ud->shapes, ini->idx); + history_batch_add(&batch, ini->idx, HIST_POSITION, + (float[4]){ ini->init_cx, ini->init_cy }, + (float[4]){ s->cx, s->cy }); + history_batch_add(&batch, ini->idx, HIST_SCALE, + (float[4]){ ini->init_sx, ini->init_sy }, + (float[4]){ s->sx, s->sy }); + } + + history_batch_commit(&batch, &ud->history); + } + + FREE(ud->interact.resize.init); + ud->interact.resize.init = NULL; + ud->interact.resize.init_count = 0; + ud->interact.resize.dragging = false; + + update_shape_states(ud); + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; +} + +static void handle_rotate_end(userdata_t *ud) +{ + if (ud->interact.rotate.total_delta != 0.0f) { + int sel_count = 0; + for (int i = 0; i < ud->shapes.count; i++) { + if (((shape_t*) vec_get(&ud->shapes, i))->selected) sel_count++; + } + + float cos_b = cosf(-ud->interact.rotate.total_delta); + float sin_b = sinf(-ud->interact.rotate.total_delta); + float cx = ud->interact.rotate.center_x; + float cy = ud->interact.rotate.center_y; + + hist_batch_t batch; + history_batch_init(&batch, sel_count * 2); + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + float dx = s->cx - cx; + float dy = s->cy - cy; + float old_cx = cx + dx * cos_b - dy * sin_b; + float old_cy = cy + dx * sin_b + dy * cos_b; + + history_batch_add(&batch, i, HIST_POSITION, + (float[4]){ old_cx, old_cy }, + (float[4]){ s->cx, s->cy }); + history_batch_add(&batch, i, HIST_ROTATION, + (float[4]){ s->rotation - ud->interact.rotate.total_delta }, + (float[4]){ s->rotation }); + } + } + + history_batch_commit(&batch, &ud->history); + } + + ud->interact.rotate.dragging = false; + update_shape_states(ud); + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; +} + +static void handle_move_end(userdata_t *ud) +{ + if (ud->interact.move.total_dx != 0.0f || ud->interact.move.total_dy != 0.0f) { + int sel_count = 0; + for (int i = 0; i < ud->shapes.count; i++) { + if (((shape_t*) vec_get(&ud->shapes, i))->selected) sel_count++; + } + + hist_batch_t batch; + history_batch_init(&batch, sel_count); + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + history_batch_add(&batch, i, HIST_POSITION, + (float[4]){ s->cx - ud->interact.move.total_dx, s->cy - ud->interact.move.total_dy }, + (float[4]){ s->cx, s->cy }); + } + } + + history_batch_commit(&batch, &ud->history); + } + + ud->interact.move.dragging = false; + update_shape_states(ud); + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; +} + +static void handle_select_end(userdata_t *ud) +{ + if (!ud->interact.select.dragging) { + if (ud->interact.select.clicked_shape >= 0) { + shape_t *clicked = (shape_t*) vec_get(&ud->shapes, ud->interact.select.clicked_shape); + + for (int i = 0; i < ud->shapes.count; i++) + ((shape_t*) vec_get(&ud->shapes, i))->selected = false; + ud->interact.selected_count = 0; + + if (ud->interact.focused_group_id != 0) { + if (is_shape_in_group_hierarchy(clicked->group_id, ud->interact.focused_group_id, &ud->groups)) { + clicked->selected = true; + ud->interact.selected_count = 1; + } else { + ud->interact.focused_group_id = 0; + if (clicked->group_id != 0) { + int topmost = get_topmost_group(&ud->groups, clicked->group_id); + deselect_and_select_group_recursive(ud, topmost); + } else { + clicked->selected = true; + ud->interact.selected_count = 1; + } + } + } else if (clicked->group_id != 0) { + int topmost = get_topmost_group(&ud->groups, clicked->group_id); + deselect_and_select_group_recursive(ud, topmost); + } else { + clicked->selected = true; + ud->interact.selected_count = 1; + } + } else { + for (int i = 0; i < ud->shapes.count; i++) + ((shape_t*) vec_get(&ud->shapes, i))->selected = false; + ud->interact.selected_count = 0; + + if (ud->interact.focused_group_id != 0) + ud->interact.focused_group_id = 0; + } + } + + ud->interact.select.active = false; + ud->interact.select.dragging = false; + update_shape_states(ud); + ud->overlay_upload_needed = true; +} + +static void handle_pan_drag(userdata_t *ud, const sapp_event *event) +{ + ud->camera.pan[0] += event->mouse_dx; + ud->camera.pan[1] -= event->mouse_dy; + compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); +} + +static void handle_resize_drag(userdata_t *ud, const sapp_event *event) +{ + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + + float sx_total = 1.0f, sy_total = 1.0f; + + for (int j = 0; j < ud->interact.resize.init_count; j++) { + resize_init_t *ini = &ud->interact.resize.init[j]; + shape_t *s = (shape_t*) vec_get(&ud->shapes, ini->idx); + + float sc = cosf(s->rotation), ss = sinf(s->rotation); + float mlx = (wx - ini->init_cx) * sc + (wy - ini->init_cy) * ss; + float mly = -(wx - ini->init_cx) * ss + (wy - ini->init_cy) * sc; + + float cex = mlx - ini->lpi_x; + float cey = mly - ini->lpi_y; + + float scale_x = 1.0f, scale_y = 1.0f; + if (ud->interact.resize.mask_x && fabsf(ini->ext_x) >= 0.0001f) + scale_x = fabsf(cex / ini->ext_x); + if (ud->interact.resize.mask_y && fabsf(ini->ext_y) >= 0.0001f) + scale_y = fabsf(cey / ini->ext_y); + + s->sx = ini->init_sx * scale_x; + s->sy = ini->init_sy * scale_y; + s->cx = ini->init_cx - ini->lpi_x * (scale_x - 1.0f) * sc + ini->lpi_y * (scale_y - 1.0f) * ss; + s->cy = ini->init_cy - ini->lpi_x * (scale_x - 1.0f) * ss - ini->lpi_y * (scale_y - 1.0f) * sc; + + shape_regenerate(s); + shape_set_state(s, false, true); + + sx_total = scale_x; + sy_total = scale_y; + } + + ud->interact.resize.total_scale_x = sx_total; + ud->interact.resize.total_scale_y = sy_total; +} + +static void handle_rotate_drag(userdata_t *ud, const sapp_event *event) +{ + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + float angle = atan2f(wy - ud->interact.rotate.center_y, + wx - ud->interact.rotate.center_x); + float delta = angle - ud->interact.rotate.start_angle; + if (delta > GLM_PIf) delta -= 2.0f * GLM_PIf; + else if (delta < -GLM_PIf) delta += 2.0f * GLM_PIf; + float inc = delta - ud->interact.rotate.total_delta; + + float cos_a = cosf(inc); + float sin_a = sinf(inc); + float cx = ud->interact.rotate.center_x; + float cy = ud->interact.rotate.center_y; + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + float dx = s->cx - cx; + float dy = s->cy - cy; + s->cx = cx + dx * cos_a - dy * sin_a; + s->cy = cy + dx * sin_a + dy * cos_a; + s->rotation += inc; + shape_build_transform(s); + shape_set_state(s, false, true); + } + } + + ud->interact.rotate.total_delta = delta; +} + +static void handle_move_drag(userdata_t *ud, const sapp_event *event) +{ + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + float dx = wx - ud->interact.move.start_wx; + float dy = wy - ud->interact.move.start_wy; + float delta_x = dx - ud->interact.move.total_dx; + float delta_y = dy - ud->interact.move.total_dy; + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected) { + s->cx += delta_x; + s->cy += delta_y; + shape_build_transform(s); + shape_set_state(s, false, true); + } + } + + ud->interact.move.total_dx = dx; + ud->interact.move.total_dy = dy; +} + +static void handle_marquee_drag(userdata_t *ud, const sapp_event *event) +{ + ud->interact.select.current_x = event->mouse_x; + ud->interact.select.current_y = event->mouse_y; + float dx = ud->interact.select.current_x - ud->interact.select.start_x; + float dy = ud->interact.select.current_y - ud->interact.select.start_y; + if (dx * dx + dy * dy > 9.0f) { + ud->interact.select.dragging = true; + } + + if (ud->interact.select.dragging) { + float wx1, wy1, wx2, wy2; + screen_to_world(&ud->camera, ud->interact.select.start_x, ud->interact.select.start_y, &wx1, &wy1); + screen_to_world(&ud->camera, ud->interact.select.current_x, ud->interact.select.current_y, &wx2, &wy2); + float min_x = fminf(wx1, wx2), min_y = fminf(wy1, wy2); + float max_x = fmaxf(wx1, wx2), max_y = fmaxf(wy1, wy2); + + ud->interact.selected_count = spatial_query_rect_select( + &ud->spatial_grid, &ud->shapes, + min_x, min_y, max_x, max_y); + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + shape_set_state(s, false, s->selected); + } + } +} + +static void handle_hover(userdata_t *ud, const sapp_event *event) +{ + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + const float tol = ud->camera.hover_tol; + + int hovered = spatial_query_point(&ud->spatial_grid, + &ud->shapes, wx, wy, tol); + if (hovered != ud->interact.hovered_shape) { + ud->interact.hovered_shape = hovered; + EM_ASM({ document.querySelector('canvas').style.cursor = $0 ? 'pointer' : 'default'; }, hovered >= 0); + } + + int hovered_gid = 0; + if (hovered >= 0) { + shape_t *hs = (shape_t*) vec_get(&ud->shapes, hovered); + hovered_gid = hs->group_id; + } + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + bool in_group = (ud->interact.focused_group_id == 0 && + hovered_gid != 0 && s->group_id == hovered_gid); + shape_set_state(s, (i == hovered || in_group), s->selected); + } +} + +// -- public event handlers -- + +static bool handle_key_down(userdata_t *ud, const sapp_event *event) +{ + if (event->modifiers & SAPP_MODIFIER_CTRL) { + if (event->key_code == SAPP_KEYCODE_Z || event->key_code == SAPP_KEYCODE_W) { + if (history_undo(&ud->history, &ud->shapes)) { + rebuild_groups_from_shapes(&ud->groups, &ud->shapes); + ud->interact.hovered_shape = -1; + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; + } + return true; + } + if (event->key_code == SAPP_KEYCODE_Y) { + if (history_redo(&ud->history, &ud->shapes)) { + rebuild_groups_from_shapes(&ud->groups, &ud->shapes); + ud->interact.hovered_shape = -1; + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; + } + return true; + } + if (event->key_code == SAPP_KEYCODE_G) { + if (event->modifiers & SAPP_MODIFIER_SHIFT) { + // Ungroup: collect unique group IDs of selected shapes + if (ud->interact.selected_count == 0) return true; + int cap = ud->shapes.count; + int *gids = (int*) ALLOC((size_t)cap * sizeof(int)); + int n_gids = 0; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (!s->selected || s->group_id == 0) continue; + bool found = false; + for (int j = 0; j < n_gids; j++) { + if (gids[j] == s->group_id) { found = true; break; } + } + if (!found) gids[n_gids++] = s->group_id; + } + if (n_gids == 0) { FREE(gids); return true; } + + int *parents = (int*) ALLOC((size_t)n_gids * sizeof(int)); + for (int j = 0; j < n_gids; j++) { + group_t *grp = find_group(&ud->groups, gids[j]); + parents[j] = grp ? grp->parent_id : 0; + } + + int touched = 0; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->group_id == 0) continue; + for (int j = 0; j < n_gids; j++) { + if (s->group_id == gids[j]) { touched++; break; } + } + } + hist_batch_t batch; + history_batch_init(&batch, touched); + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->group_id == 0) continue; + int old_gid = s->group_id; + int parent = 0; + for (int j = 0; j < n_gids; j++) { + if (old_gid == gids[j]) { parent = parents[j]; break; } + } + if (old_gid != 0 && parent == 0) { + bool in_touched = false; + for (int j = 0; j < n_gids; j++) { + if (old_gid == gids[j]) { in_touched = true; break; } + } + if (!in_touched) continue; + } + bool in_touched = false; + for (int j = 0; j < n_gids; j++) { + if (old_gid == gids[j]) { in_touched = true; break; } + } + if (!in_touched) continue; + history_batch_add(&batch, i, HIST_GROUP, + (float[4]){ (float)old_gid }, + (float[4]){ (float)parent }); + s->group_id = parent; + } + history_batch_commit(&batch, &ud->history); + + for (int j = 0; j < n_gids; j++) { + for (int g = 0; g < ud->groups.count; g++) { + group_t *grp = (group_t*) vec_get(&ud->groups, g); + if (grp->parent_id == gids[j]) + grp->parent_id = parents[j]; + } + } + + for (int j = n_gids - 1; j >= 0; j--) { + for (int g = 0; g < ud->groups.count; g++) { + group_t *grp = (group_t*) vec_get(&ud->groups, g); + if (grp->id == gids[j]) { + vec_remove_ordered(&ud->groups, g); + break; + } + } + } + + FREE(parents); + FREE(gids); + ud->ui.list_last_shape = -1; + ud->overlay_upload_needed = true; + update_shape_states(ud); + } else { + // Group selected shapes + if (ud->interact.selected_count < 2) return true; + int gid = ud->next_group_id++; + int n = ud->shapes.count; + + int *full_gids = (int*) ALLOC((size_t)ud->groups.count * sizeof(int)); + int n_full = 0; + for (int g = 0; g < ud->groups.count; g++) { + group_t *grp = (group_t*) vec_get(&ud->groups, g); + bool all_sel = false; + int member_count = 0; + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->group_id == grp->id) { member_count++; if (!s->selected) break; } + } + if (member_count > 0) { + all_sel = true; + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->group_id == grp->id && !s->selected) { all_sel = false; break; } + } + } + if (!all_sel) continue; + bool parent_full = false; + if (grp->parent_id != 0) { + group_t *pg = find_group(&ud->groups, grp->parent_id); + if (pg) { + bool p_all_sel = true; + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->group_id == pg->id && !s->selected) { p_all_sel = false; break; } + } + if (p_all_sel) parent_full = true; + } + } + if (!parent_full) + full_gids[n_full++] = grp->id; + } + + int touched = 0; + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (!s->selected) continue; + bool in_full = false; + for (int j = 0; j < n_full; j++) { + if (s->group_id == full_gids[j]) { in_full = true; break; } + } + if (!in_full) touched++; + } + + hist_batch_t batch; + history_batch_init(&batch, touched); + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (!s->selected) continue; + bool in_full = false; + for (int j = 0; j < n_full; j++) { + if (s->group_id == full_gids[j]) { in_full = true; break; } + } + if (in_full) continue; + history_batch_add(&batch, i, HIST_GROUP, + (float[4]){ (float)s->group_id }, + (float[4]){ (float)gid }); + s->group_id = gid; + } + history_batch_commit(&batch, &ud->history); + + group_t new_grp = { .id = gid, .parent_id = 0 }; + *((group_t*) vec_push(&ud->groups)) = new_grp; + + for (int j = 0; j < n_full; j++) { + for (int g = 0; g < ud->groups.count; g++) { + group_t *grp = (group_t*) vec_get(&ud->groups, g); + if (grp->id == full_gids[j]) { + grp->parent_id = gid; + break; + } + } + } + + FREE(full_gids); + ud->ui.list_last_shape = -1; + ud->overlay_upload_needed = true; + update_shape_states(ud); + } + return true; + } + } + if (event->key_code == SAPP_KEYCODE_ESCAPE) { + if (ud->interact.focused_group_id != 0) { + ud->interact.focused_group_id = 0; + for (int i = 0; i < ud->shapes.count; i++) + ((shape_t*) vec_get(&ud->shapes, i))->selected = false; + ud->interact.selected_count = 0; + ud->overlay_upload_needed = true; + update_shape_states(ud); + } + return true; + } + if (event->key_code == SAPP_KEYCODE_GRAVE_ACCENT) { + ud->ui.log_show = !ud->ui.log_show; + return true; + } + if (event->key_code == SAPP_KEYCODE_DELETE || event->key_code == SAPP_KEYCODE_BACKSPACE) { + if (ud->interact.selected_count > 0) { + int cap = ud->shapes.count; + int *indices = (int*) ALLOC((size_t)cap * sizeof(int)); + int collected = 0; + + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (!s->selected) continue; + indices[collected++] = i; + } + + hist_batch_t batch; + history_batch_init(&batch, collected); + for (int j = collected - 1; j >= 0; j--) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, indices[j]); + history_batch_add_shape(&batch, indices[j], HIST_DELETE, s); + } + history_batch_commit(&batch, &ud->history); + + for (int j = collected - 1; j >= 0; j--) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, indices[j]); + shape_shutdown(s); + vec_remove_ordered(&ud->shapes, indices[j]); + } + + FREE(indices); + + ud->interact.selected_count = 0; + ud->interact.hovered_shape = -1; + ud->interact.aabb_cached = false; + spatial_mark_dirty(&ud->spatial_grid); + ud->overlay_upload_needed = true; + update_shape_states(ud); + } + return true; + } + return false; +} + +static void handle_resize(userdata_t *ud, const sapp_event *event) +{ + (void)event; + ud->camera.width = sapp_width(); + ud->camera.height = sapp_height(); + ud->camera.half_width = ud->camera.width * 0.5f; + ud->camera.half_height = ud->camera.height * 0.5f; + compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); +} + +static void handle_mouse_down(userdata_t *ud, const sapp_event *event) +{ + if (event->mouse_button == SAPP_MOUSEBUTTON_LEFT) { + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + const float tol = 4.0f / ud->camera.zoom; + + if (!(event->modifiers & SAPP_MODIFIER_CTRL) && ud->ui.active_tool >= TOOL_CIRCLE) { + shape_t s; + switch (ud->ui.active_tool) { + case TOOL_CIRCLE: + s = shape_circle(wx, wy, 100.0f); + break; + case TOOL_RECTANGLE: + s = shape_rectangle(wx, wy, 200.0f, 100.0f); + break; + default: + return; + } + *((shape_t*) vec_push(&ud->shapes)) = s; + spatial_mark_dirty(&ud->spatial_grid); + ud->overlay_upload_needed = true; + + { + int idx = ud->shapes.count - 1; + shape_t *sp = (shape_t*) vec_get(&ud->shapes, idx); + hist_batch_t batch; + history_batch_init(&batch, 1); + history_batch_add_shape(&batch, idx, HIST_CREATE, sp); + history_batch_commit(&batch, &ud->history); + } + return; + } + + // Double-click detection for focus mode + { + int hit_shape = -1; + for (int i = 0; i < ud->shapes.count; i++) { + if (shape_hit_test((shape_t*) vec_get(&ud->shapes, i), wx, wy, tol)) { + hit_shape = i; + break; + } + } + + bool dbl = (ud->time - ud->interact.last_click_time < 0.3 && + hit_shape >= 0 && + hit_shape == ud->interact.last_click_shape_idx); + + ud->interact.last_click_time = ud->time; + ud->interact.last_click_shape_idx = hit_shape; + + if (dbl && hit_shape >= 0) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, hit_shape); + if (s->group_id != 0 && ud->ui.active_tool == TOOL_SELECT) { + int gid = s->group_id; + if (ud->interact.focused_group_id != 0 && + is_shape_in_group_hierarchy(s->group_id, ud->interact.focused_group_id, &ud->groups)) { + gid = s->group_id; + } + for (int i = 0; i < ud->shapes.count; i++) + ((shape_t*) vec_get(&ud->shapes, i))->selected = false; + ud->interact.selected_count = 0; + ud->interact.focused_group_id = gid; + ud->overlay_upload_needed = true; + update_shape_states(ud); + return; + } + } + } + + if (event->modifiers & SAPP_MODIFIER_CTRL) { + handle_left_down_ctrl_click(ud, wx, wy, tol); + } else { + int resize_hit = hit_test_resize_handles(ud, wx, wy, tol); + if (resize_hit >= 0) { + handle_left_down_resize_begin(ud, wx, wy, resize_hit); + } else { + float grip = HANDLE_RADIUS_PX / ud->camera.zoom + tol; + float dcx = wx - ud->interact.rotate.center_x; + float dcy = wy - ud->interact.rotate.center_y; + float dist = sqrtf(dcx * dcx + dcy * dcy); + bool on_handle = (ud->interact.selected_count > 0) && + (fabsf(dist - ud->interact.rotate.handle_radius) <= grip); + + if (on_handle) { + handle_left_down_rotate_begin(ud, wx, wy); + } else { + int clicked_selected = -1; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->selected && shape_hit_test(s, wx, wy, tol)) { + clicked_selected = i; + break; + } + } + + bool in_aabb = false; + if (clicked_selected < 0 && ud->interact.selected_count >= 2) { + float omin[2], omax[2]; + if (ud->interact.aabb_cached) { + omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1]; + omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3]; + } else { + selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); + } + float pad = 8.0f / ud->camera.zoom; + omin[0] -= pad; omin[1] -= pad; + omax[0] += pad; omax[1] += pad; + in_aabb = (wx >= omin[0] && wx <= omax[0] && wy >= omin[1] && wy <= omax[1]); + } + + if (clicked_selected >= 0 || in_aabb) { + handle_left_down_move_begin(ud, wx, wy); + } else { + handle_left_down_select_or_marquee(ud, event, wx, wy, tol); + } + } + } + } + + update_shape_states(ud); + } else if (event->modifiers & SAPP_MODIFIER_RMB) { + handle_right_down_pan_begin(ud, event); + } +} + +static void handle_mouse_up(userdata_t *ud, const sapp_event *event) +{ + (void)event; + + if (ud->interact.resize.dragging) { + handle_resize_end(ud); + } else if (ud->interact.rotate.dragging) { + handle_rotate_end(ud); + } else if (ud->interact.move.dragging) { + handle_move_end(ud); + } else if (ud->interact.select.active) { + handle_select_end(ud); + } + + ud->camera.pan_state.dragging = false; +} + +static void handle_mouse_move(userdata_t *ud, const sapp_event *event) +{ + if (ud->camera.pan_state.dragging) { + handle_pan_drag(ud, event); + } else if (ud->interact.resize.dragging) { + handle_resize_drag(ud, event); + } else if (ud->interact.rotate.dragging) { + handle_rotate_drag(ud, event); + } else if (ud->interact.move.dragging) { + handle_move_drag(ud, event); + } else if (ud->interact.select.active) { + handle_marquee_drag(ud, event); + } else { + handle_hover(ud, event); + } +} + +static void handle_scroll_zoom(userdata_t *ud, const sapp_event *event) +{ + if ((ud->camera.zoom >= 6.0f && event->scroll_y > 0.0f) || + (ud->camera.zoom <= 0.1f && event->scroll_y < 0.0f)) + return; + + float wx, wy; + screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); + + const float diff = expf(event->scroll_y * 0.1f); + float new_zoom = ud->camera.zoom * diff; + ud->camera.zoom = new_zoom < 0.1f ? 0.1f : (new_zoom > 6.0f ? 6.0f : new_zoom); + ud->camera.hover_tol = SHAPE_HOVER_PX / ud->camera.zoom; + + const float sx = event->mouse_x - ud->camera.half_width; + const float sy = ud->camera.half_height - event->mouse_y; + ud->camera.pan[0] = sx - wx * ud->camera.zoom; + ud->camera.pan[1] = sy - wy * ud->camera.zoom; + + ud->overlay_upload_needed = true; + compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); +} + +#endif diff --git a/src/interact.h b/src/interact.h new file mode 100644 index 0000000..b1c97a7 --- /dev/null +++ b/src/interact.h @@ -0,0 +1,144 @@ +#ifndef INTERACT_H +#define INTERACT_H + +#include "api.h" +#include "types.h" + +static void selected_aabb(userdata_t *ud, float *min_x, float *min_y, + float *max_x, float *max_y) +{ + bool first = true; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (!s->selected) continue; + float sc = cosf(s->rotation), ss = sinf(s->rotation); + for (uint32_t v = 0; 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 (first) { *min_x = *max_x = wx; *min_y = *max_y = wy; first = false; } + else { + if (wx < *min_x) *min_x = wx; + if (wx > *max_x) *max_x = wx; + if (wy < *min_y) *min_y = wy; + if (wy > *max_y) *max_y = wy; + } + } + } +} + +static void update_shape_states(userdata_t *ud) +{ + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + shape_set_state(s, s->hovered, s->selected); + } +} + +static void select_group_recursive(userdata_t *ud, int gid) +{ + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (is_shape_in_group_hierarchy(s->group_id, gid, &ud->groups)) { + s->selected = true; + ud->interact.selected_count++; + } + } +} + +static void deselect_and_select_group_recursive(userdata_t *ud, int gid) +{ + for (int i = 0; i < ud->shapes.count; i++) + ((shape_t*) vec_get(&ud->shapes, i))->selected = false; + ud->interact.selected_count = 0; + select_group_recursive(ud, gid); +} + +static void toggle_group_recursive(userdata_t *ud, int gid) +{ + int total = 0, sel = 0; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (is_shape_in_group_hierarchy(s->group_id, gid, &ud->groups)) { + total++; + if (s->selected) sel++; + } + } + bool all_sel = (sel == total && total > 0); + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (is_shape_in_group_hierarchy(s->group_id, gid, &ud->groups)) { + s->selected = !all_sel; + ud->interact.selected_count += s->selected ? 1 : -1; + } + } +} + +static void rebuild_groups_from_shapes(vector_t *groups, vector_t *shapes) +{ + // Save existing parent relationships so nested groups survive rebuild + int old_count = groups->count; + int *saved = NULL; + if (old_count > 0) { + saved = (int*) ALLOC((size_t)old_count * 2 * sizeof(int)); + for (int i = 0; i < old_count; i++) { + group_t *g = (group_t*) vec_get(groups, i); + saved[i * 2] = g->id; + saved[i * 2 + 1] = g->parent_id; + } + } + + groups->count = 0; + for (int i = 0; i < shapes->count; i++) { + shape_t *s = (shape_t*) vec_get(shapes, i); + if (s->group_id == 0) continue; + bool found = false; + for (int g = 0; g < groups->count; g++) { + if (((group_t*) vec_get(groups, g))->id == s->group_id) { found = true; break; } + } + if (!found) { + group_t grp = { .id = s->group_id, .parent_id = 0 }; + *((group_t*) vec_push(groups)) = grp; + } + } + + // Restore parent relationships for groups that still exist + for (int i = 0; i < old_count; i++) { + int gid = saved[i * 2]; + int pid = saved[i * 2 + 1]; + if (pid == 0) continue; + group_t *g = find_group(groups, gid); + if (g) { + // Only restore if parent group still exists + if (find_group(groups, pid)) + g->parent_id = pid; + } + } + + if (saved) FREE(saved); +} + +static int hit_test_resize_handles(userdata_t *ud, float wx, float wy, float tol) +{ + if (ud->interact.selected_count <= 0) return -1; + float omin[2], omax[2]; + if (ud->interact.aabb_cached) { + omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1]; + omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3]; + } else { + selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); + } + float hs = CORNER_SIZE_PX / ud->camera.zoom * 0.5f + tol; + float mid_x = (omin[0] + omax[0]) * 0.5f; + float mid_y = (omin[1] + omax[1]) * 0.5f; + float hx[8] = {omin[0], mid_x, omax[0], omax[0], omax[0], mid_x, omin[0], omin[0]}; + float hy[8] = {omin[1], omin[1], omin[1], mid_y, omax[1], omax[1], omax[1], mid_y}; + for (int h = 0; h < 8; h++) { + if (fabsf(wx - hx[h]) <= hs && fabsf(wy - hy[h]) <= hs) + return h; + } + return -1; +} + +#endif diff --git a/src/main.c b/src/main.c index 57807c5..db1bf3c 100644 --- a/src/main.c +++ b/src/main.c @@ -1,151 +1,5 @@ #include "api.h" -#define LOG_RING_SIZE 64 -#define HANDLE_OFFSET_PX 0.0f -#define HANDLE_RADIUS_PX 12.0f -#define HANDLE_CIRCLE_SEGMENTS 256 -#define CORNER_SIZE_PX 8.0f - -typedef struct log_entry_t { - char text[256]; - uint32_t level; -} log_entry_t; - -typedef struct { - mat4 mvp; -} uniform_t; - -typedef struct renderer_t { - sg_pipeline pipeline; - sg_pass_action clear_pass; - uniform_t uniform; -} renderer_t; - -typedef struct { - int idx; - float init_sx, init_sy, init_cx, init_cy; - float ext_x, ext_y; - float lpi_x, lpi_y; -} resize_init_t; - -typedef struct { - 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; - - bool move_dragging; - float move_start_wx, move_start_wy; - float move_total_dx, move_total_dy; - - bool rotate_dragging; - float rotate_center_x, rotate_center_y; - float rotate_start_angle; - float rotate_total_delta; - float handle_radius; - - bool resize_dragging; - float resize_pivot_x, resize_pivot_y; - float resize_start_wx, resize_start_wy; - float resize_total_scale_x, resize_total_scale_y; - float resize_mask_x, resize_mask_y; - float resize_angle; - resize_init_t *resize_init; - int resize_init_count; - - float cached_aabb[4]; - bool aabb_cached; -} interact_state_t; - -typedef struct { - float fps_immediate; - float fps_average; - float frame_times[60]; - int frame_time_head; - int frame_time_count; - float frame_time_sum; -} debug_stats_t; - -typedef struct { - float panel_w; - log_entry_t log_ring[LOG_RING_SIZE]; - int log_head; - int log_count; - bool log_show; -} ui_state_t; - -typedef struct userdata_t { - camera_t camera; - renderer_t renderer; - vector_t shapes; - spatial_grid_t spatial_grid; - interact_state_t interact; - history_t history; - debug_stats_t debug; - ui_state_t ui; - bool overlay_upload_needed; - sg_buffer rect_vbuf, rect_ibuf; - sg_buffer handle_vbuf, handle_ibuf; - sg_buffer corner_vbuf, corner_ibuf; -} userdata_t; - -// -- forward declarations for the extraction helpers -- - -static void selected_aabb(userdata_t *ud, float *min_x, float *min_y, - float *max_x, float *max_y); -static void meter_fps(userdata_t *ud); -static void compute_overlay_geometry(userdata_t *ud, - shape_vertex_t overlay_verts[5], - float *sel_cx, float *sel_cy, float *sel_hw, float *sel_hh, float *sel_angle, - bool *has_overlay, bool *show_handle); -static void upload_overlay_buffers(userdata_t *ud, - const shape_vertex_t overlay_verts[5], - float sel_cx, float sel_cy, float sel_hw, float sel_hh, float sel_angle, - bool has_overlay, bool show_handle); -static void draw_shapes(userdata_t *ud); -static void draw_overlay_and_handles(userdata_t *ud, - bool has_overlay, bool show_handle); -static void draw_properties_panel(userdata_t *ud); -static void draw_log_panel(userdata_t *ud); -static bool handle_key_down(userdata_t *ud, const sapp_event *event); -static void handle_resize(userdata_t *ud, const sapp_event *event); -static void handle_mouse_down(userdata_t *ud, const sapp_event *event); -static void handle_mouse_up(userdata_t *ud, const sapp_event *event); -static void handle_mouse_move(userdata_t *ud, const sapp_event *event); -static void handle_scroll_zoom(userdata_t *ud, const sapp_event *event); - -// -- aabb helper -- - -static void selected_aabb(userdata_t *ud, float *min_x, float *min_y, - float *max_x, float *max_y) -{ - bool first = true; - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (!s->selected) continue; - float sc = cosf(s->rotation), ss = sinf(s->rotation); - for (uint32_t v = 0; 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 (first) { *min_x = *max_x = wx; *min_y = *max_y = wy; first = false; } - else { - if (wx < *min_x) *min_x = wx; - if (wx > *max_x) *max_x = wx; - if (wy < *min_y) *min_y = wy; - if (wy > *max_y) *max_y = wy; - } - } - } -} - -// -- log capture -- - 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) @@ -180,8 +34,6 @@ static void log_capture(const char* tag, uint32_t log_level, uint32_t log_item, if (log_level <= 1) ud->ui.log_show = true; } -// -- frame helpers -- - static void meter_fps(userdata_t *ud) { float dt = (float)sapp_frame_duration(); @@ -201,345 +53,13 @@ static void meter_fps(userdata_t *ud) ? (float)ud->debug.frame_time_count / ud->debug.frame_time_sum : 0.0f; } -static void compute_overlay_geometry(userdata_t *ud, - shape_vertex_t overlay_verts[5], - float *sel_cx, float *sel_cy, float *sel_hw, float *sel_hh, float *sel_angle, - bool *has_overlay, bool *show_handle) -{ - *has_overlay = false; - *sel_cx = *sel_cy = *sel_angle = 0; - *sel_hw = *sel_hh = 0; - - if (ud->interact.selecting && ud->interact.sel_dragging) { - float wx1, wy1, wx2, wy2; - screen_to_world(&ud->camera, ud->interact.sel_sx, ud->interact.sel_sy, &wx1, &wy1); - screen_to_world(&ud->camera, ud->interact.sel_cx, ud->interact.sel_cy, &wx2, &wy2); - float x1 = fminf(wx1, wx2), y1 = fminf(wy1, wy2); - float x2 = fmaxf(wx1, wx2), y2 = fmaxf(wy1, wy2); - overlay_verts[0] = (shape_vertex_t){x1, y1}; - overlay_verts[1] = (shape_vertex_t){x2, y1}; - overlay_verts[2] = (shape_vertex_t){x2, y2}; - overlay_verts[3] = (shape_vertex_t){x1, y2}; - overlay_verts[4] = (shape_vertex_t){x1, y1}; - *has_overlay = true; - } else if (ud->interact.selected_count >= 1) { - if (ud->interact.selected_count == 1) { - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (!s->selected) continue; - *sel_cx = s->cx; *sel_cy = s->cy; - *sel_hw = s->sx; *sel_hh = s->sy; - *sel_angle = s->rotation; - float x1, y1, x2, y2; - selected_aabb(ud, &x1, &y1, &x2, &y2); - ud->interact.cached_aabb[0] = x1; ud->interact.cached_aabb[1] = y1; - ud->interact.cached_aabb[2] = x2; ud->interact.cached_aabb[3] = y2; - ud->interact.aabb_cached = true; - overlay_verts[0] = (shape_vertex_t){x1, y1}; - overlay_verts[1] = (shape_vertex_t){x2, y1}; - overlay_verts[2] = (shape_vertex_t){x2, y2}; - overlay_verts[3] = (shape_vertex_t){x1, y2}; - overlay_verts[4] = overlay_verts[0]; - break; - } - } else { - float omin[2], omax[2]; - float sum_sin = 0, sum_cos = 0; - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (!s->selected) continue; - sum_sin += sinf(s->rotation); - sum_cos += cosf(s->rotation); - } - selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); - ud->interact.cached_aabb[0] = omin[0]; ud->interact.cached_aabb[1] = omin[1]; - ud->interact.cached_aabb[2] = omax[0]; ud->interact.cached_aabb[3] = omax[1]; - ud->interact.aabb_cached = true; - float pad = 8.0f / ud->camera.zoom; - omin[0] -= pad; omin[1] -= pad; - omax[0] += pad; omax[1] += pad; - *sel_cx = (omin[0] + omax[0]) * 0.5f; - *sel_cy = (omin[1] + omax[1]) * 0.5f; - *sel_hw = (omax[0] - omin[0]) * 0.5f; - *sel_hh = (omax[1] - omin[1]) * 0.5f; - *sel_angle = atan2f(sum_sin, sum_cos); - - overlay_verts[0] = (shape_vertex_t){omin[0], omin[1]}; - overlay_verts[1] = (shape_vertex_t){omax[0], omin[1]}; - overlay_verts[2] = (shape_vertex_t){omax[0], omax[1]}; - overlay_verts[3] = (shape_vertex_t){omin[0], omax[1]}; - overlay_verts[4] = overlay_verts[0]; - } - *has_overlay = true; - } - - *show_handle = ud->interact.selected_count > 0 && !ud->interact.selecting; -} - -static void upload_overlay_buffers(userdata_t *ud, - const shape_vertex_t overlay_verts[5], - float sel_cx, float sel_cy, float sel_hw, float sel_hh, float sel_angle, - bool has_overlay, bool show_handle) -{ - bool need_upload = ud->overlay_upload_needed || - ud->interact.move_dragging || ud->interact.rotate_dragging || - ud->interact.resize_dragging || ud->interact.selecting; - - if (has_overlay && need_upload) { - sg_update_buffer(ud->rect_vbuf, &(sg_range){overlay_verts, (size_t)5 * sizeof(shape_vertex_t)}); - } - - if (show_handle) { - float pad = HANDLE_OFFSET_PX / ud->camera.zoom; - float radius = sqrtf(sel_hw * sel_hw + sel_hh * sel_hh) + pad; - - ud->interact.rotate_center_x = sel_cx; - ud->interact.rotate_center_y = sel_cy; - ud->interact.handle_radius = radius; - - const int n = HANDLE_CIRCLE_SEGMENTS + 1; - shape_vertex_t hv[HANDLE_CIRCLE_SEGMENTS + 1]; - for (int i = 0; i < n; i++) { - float a = (float)i / (float)HANDLE_CIRCLE_SEGMENTS * 2.0f * GLM_PIf; - hv[i] = (shape_vertex_t){sel_cx + cosf(a) * radius, sel_cy + sinf(a) * radius}; - } - if (need_upload) - sg_update_buffer(ud->handle_vbuf, &(sg_range){hv, sizeof(hv)}); - - { - float hs = CORNER_SIZE_PX / ud->camera.zoom * 0.5f; - float mid_x = (overlay_verts[0].x + overlay_verts[1].x) * 0.5f; - float mid_y = (overlay_verts[0].y + overlay_verts[2].y) * 0.5f; - float handles[8][2] = { - {overlay_verts[0].x, overlay_verts[0].y}, - {mid_x, overlay_verts[0].y}, - {overlay_verts[1].x, overlay_verts[1].y}, - {overlay_verts[1].x, mid_y }, - {overlay_verts[2].x, overlay_verts[2].y}, - {mid_x, overlay_verts[2].y}, - {overlay_verts[3].x, overlay_verts[3].y}, - {overlay_verts[3].x, mid_y }, - }; - shape_vertex_t cv[40]; - for (int h = 0; h < 8; h++) { - float cx = handles[h][0], cy = handles[h][1]; - cv[h*5+0] = (shape_vertex_t){cx - hs, cy - hs}; - cv[h*5+1] = (shape_vertex_t){cx + hs, cy - hs}; - cv[h*5+2] = (shape_vertex_t){cx + hs, cy + hs}; - cv[h*5+3] = (shape_vertex_t){cx - hs, cy + hs}; - cv[h*5+4] = (shape_vertex_t){cx - hs, cy - hs}; - } - if (need_upload) - sg_update_buffer(ud->corner_vbuf, &(sg_range){cv, sizeof(cv)}); - } - } - - ud->overlay_upload_needed = false; -} - -static void draw_shapes(userdata_t *ud) -{ - sg_apply_pipeline(shape_pipeline); - for (int i = 0; i < ud->shapes.count; i++) { - shape_draw((shape_t*) vec_get(&ud->shapes, i), &ud->renderer.uniform.mvp); - } -} - -static void draw_overlay_and_handles(userdata_t *ud, bool has_overlay, bool show_handle) -{ - if (has_overlay) { - 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.8f; - u.state = 0; - memset(u._pad, 0, sizeof(u._pad)); - - sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp)); - sg_apply_uniforms(1, &SG_RANGE(u)); - sg_apply_bindings(&(sg_bindings){ - .vertex_buffers[0] = ud->rect_vbuf, - .index_buffer = ud->rect_ibuf, - }); - sg_draw(0, 5, 1); - } - - if (show_handle) { - shape_uniform_t hu; - glm_mat4_identity(hu.transform); - hu.base_color[0] = 0.3f; hu.base_color[1] = 0.5f; - hu.base_color[2] = 1.0f; hu.base_color[3] = 0.8f; - hu.state = 0; - memset(hu._pad, 0, sizeof(hu._pad)); - - sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp)); - sg_apply_uniforms(1, &SG_RANGE(hu)); - sg_apply_bindings(&(sg_bindings){ - .vertex_buffers[0] = ud->handle_vbuf, - .index_buffer = ud->handle_ibuf, - }); - sg_draw(0, HANDLE_CIRCLE_SEGMENTS + 1, 1); - - { - shape_uniform_t cu; - glm_mat4_identity(cu.transform); - cu.base_color[0] = 1.0f; cu.base_color[1] = 1.0f; - cu.base_color[2] = 1.0f; cu.base_color[3] = 0.9f; - cu.state = 0; - memset(cu._pad, 0, sizeof(cu._pad)); - - sg_apply_uniforms(0, &SG_RANGE(ud->renderer.uniform.mvp)); - sg_apply_uniforms(1, &SG_RANGE(cu)); - sg_apply_bindings(&(sg_bindings){ - .vertex_buffers[0] = ud->corner_vbuf, - .index_buffer = ud->corner_ibuf, - }); - for (int h = 0; h < 8; h++) sg_draw(h * 5, 5, 1); - } - } -} - -static void draw_properties_panel(userdata_t *ud) -{ - igSetNextWindowPos((ImVec2){ud->camera.width - ud->ui.panel_w, 20.0f}, ImGuiCond_Always, (ImVec2){0, 0}); - igSetNextWindowSize((ImVec2){ud->ui.panel_w, ud->camera.height - 20.0f}, ImGuiCond_Always); - igBegin("Properties", NULL, - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - - if (ud->interact.selected_count == 0) { - igText("No shape selected"); - } else if (ud->interact.selected_count > 1) { - igText("%d shapes selected", ud->interact.selected_count); - } else { - int idx = 0; - while (idx < ud->shapes.count) { - shape_t *tmp = (shape_t*) vec_get(&ud->shapes, idx); - if (tmp->selected) break; - idx++; - } - shape_t *s = (shape_t*) vec_get(&ud->shapes, idx); - bool changed = false; - - changed |= igDragFloat2("Position", &s->cx, 1.0f, 0, 0, "%.1f", 0); - if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_POSITION); - changed |= igDragFloat2("Scale", &s->sx, 1.0f, 0.1f, 0, "%.1f", 0); - if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_SCALE); - changed |= igDragFloat("Rotation", &s->rotation, 0.01f, 0, 0, "%.3f", 0); - if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_ROTATION); - if (igColorEdit4("Color", s->uniform.base_color, 0)) - changed = true; - if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_COLOR); - - if (changed) { shape_regenerate(s); spatial_mark_dirty(&ud->spatial_grid); ud->overlay_upload_needed = true; } - - igSeparator(); - { - mat4 *m = &s->uniform.transform; - float sc = cosf(s->rotation), ss = sinf(s->rotation); - float lx0 = s->verts[0].x * s->sx; - float ly0 = s->verts[0].y * s->sy; - float wx = s->cx + lx0 * sc - ly0 * ss; - float wy = s->cy + lx0 * ss + ly0 * sc; - igText("Debug - Transform Matrix:"); - igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[0][0], (*m)[0][1], (*m)[0][2], (*m)[0][3]); - igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[1][0], (*m)[1][1], (*m)[1][2], (*m)[1][3]); - igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[2][0], (*m)[2][1], (*m)[2][2], (*m)[2][3]); - igText("[%+.3f %+.3f %+.3f %+.3f]", (*m)[3][0], (*m)[3][1], (*m)[3][2], (*m)[3][3]); - igSpacing(); - igText("Local vert[0]: (%.1f, %.1f)", s->verts[0].x, s->verts[0].y); - igText("World vert[0]: (%.1f, %.1f)", wx, wy); - igText("cx=%.1f cy=%.1f sx=%.1f sy=%.1f rot=%.3f", s->cx, s->cy, s->sx, s->sy, s->rotation); - char dbg[512]; - snprintf(dbg, sizeof(dbg), - "Transform Matrix:\n" - "[%+.3f %+.3f %+.3f %+.3f]\n" - "[%+.3f %+.3f %+.3f %+.3f]\n" - "[%+.3f %+.3f %+.3f %+.3f]\n" - "[%+.3f %+.3f %+.3f %+.3f]\n" - "\nLocal vert[0]: (%.1f, %.1f)\n" - "World vert[0]: (%.1f, %.1f)\n" - "cx=%.1f cy=%.1f sx=%.1f sy=%.1f rot=%.3f", - (*m)[0][0], (*m)[0][1], (*m)[0][2], (*m)[0][3], - (*m)[1][0], (*m)[1][1], (*m)[1][2], (*m)[1][3], - (*m)[2][0], (*m)[2][1], (*m)[2][2], (*m)[2][3], - (*m)[3][0], (*m)[3][1], (*m)[3][2], (*m)[3][3], - s->verts[0].x, s->verts[0].y, wx, wy, - s->cx, s->cy, s->sx, s->sy, s->rotation); - if (igButton("Copy Debug", (ImVec2){0, 0})) - sapp_set_clipboard_string(dbg); - } - } - - igEnd(); - - if (ud->history.capturing && !igIsAnyItemActive()) { - history_end_edit(&ud->history, &ud->shapes); - } -} - -static void draw_log_panel(userdata_t *ud) -{ - if (!ud->ui.log_show) return; - - igSetNextWindowPos((ImVec2){10.0f, ud->camera.height - 200.0f}, ImGuiCond_FirstUseEver, (ImVec2){0, 0}); - igSetNextWindowSize((ImVec2){400.0f, 180.0f}, ImGuiCond_FirstUseEver); - igBegin("Log", &ud->ui.log_show, 0); - if (igButton("Clear", (ImVec2){0, 0})) { - ud->ui.log_head = 0; - ud->ui.log_count = 0; - } - igSameLine(0.0f, 10.0f); - if (igButton("Copy", (ImVec2){0, 0})) { - int total = ud->ui.log_count < LOG_RING_SIZE ? ud->ui.log_count : LOG_RING_SIZE; - int start = ud->ui.log_count < LOG_RING_SIZE ? 0 : ud->ui.log_head; - int cap = total * 260; - char *buf = (char*) ALLOC((size_t)cap); - int off = 0; - for (int i = 0; i < total; i++) { - int idx = (start + i) % LOG_RING_SIZE; - off += snprintf(buf + off, (size_t)(cap - off), "%s\n", ud->ui.log_ring[idx].text); - } - igSetClipboardText(buf); - FREE(buf); - } - igSameLine(0.0f, 10.0f); - igText("%d entries", ud->ui.log_count); - igSameLine(0.0f, 10.0f); - igText("FPS: %.0f (avg: %.0f)", ud->debug.fps_immediate, ud->debug.fps_average); - igSameLine(0.0f, 10.0f); - igText("%.3fms", sapp_frame_duration() * 1000); - igSeparator(); - - igBeginChild_Str("LogScroll", (ImVec2){0, 0}, false, 0); - int total = ud->ui.log_count < LOG_RING_SIZE ? ud->ui.log_count : LOG_RING_SIZE; - int start = ud->ui.log_count < LOG_RING_SIZE ? 0 : ud->ui.log_head; - for (int i = 0; i < total; i++) { - int idx = (start + i) % LOG_RING_SIZE; - log_entry_t *e = &ud->ui.log_ring[idx]; - ImVec4 color; - switch (e->level) { - case 0: color = (ImVec4){1.0f, 0.3f, 0.3f, 1.0f}; break; - case 1: color = (ImVec4){1.0f, 0.5f, 0.3f, 1.0f}; break; - case 2: color = (ImVec4){1.0f, 0.9f, 0.3f, 1.0f}; break; - default:color = (ImVec4){0.7f, 0.7f, 0.7f, 1.0f}; break; - } - igPushStyleColor_Vec4(ImGuiCol_Text, color); - igTextUnformatted(e->text, NULL); - igPopStyleColor(1); - } - if (total > 0) igSetScrollHereY(1.0f); - igEndChild(); - igEnd(); -} - -// -- frame -- - static void frame(void* _userdata) { userdata_t* ud = (userdata_t*) _userdata; shape_begin_frame(); meter_fps(ud); + ud->time += (double)sapp_frame_duration(); spatial_rebuild(&ud->spatial_grid, &ud->shapes); float sel_cx, sel_cy, sel_hw, sel_hh, sel_angle; @@ -566,6 +86,8 @@ static void frame(void* _userdata) .dpi_scale = sapp_dpi_scale(), }); + draw_top_panel(ud); + draw_shape_list_panel(ud); draw_properties_panel(ud); draw_log_panel(ud); @@ -574,8 +96,6 @@ static void frame(void* _userdata) sg_commit(); } -// -- init -- - static void init(void* _userdata) { rand_seed(1); @@ -618,7 +138,7 @@ static void init(void* _userdata) ud->camera.zoom = 0.5f; ud->camera.hover_tol = SHAPE_HOVER_PX / ud->camera.zoom; - sg_shader sprite_shader = sg_make_shader(&(sg_shader_desc) { + ud->renderer.shader = sg_make_shader(&(sg_shader_desc) { .vertex_func = { .source = (const char*) src_shaders_sprite_wgsl, .entry = "vs_main", @@ -671,48 +191,56 @@ static void init(void* _userdata) .label = "Sprite shader", }); - ud->renderer = (renderer_t) { - .clear_pass = (sg_pass_action) { - .colors[0] = { .clear_value = { 0.0f, 0.0f, 0.0f, 1.0f }, .load_action = SG_LOADACTION_CLEAR } + ud->renderer.clear_pass = (sg_pass_action) { + .colors[0] = { .clear_value = { 0.0f, 0.0f, 0.0f, 1.0f }, .load_action = SG_LOADACTION_CLEAR } + }; + ud->renderer.pipeline = sg_make_pipeline(&(sg_pipeline_desc) { + .shader = ud->renderer.shader, + .index_type = SG_INDEXTYPE_UINT16, + .layout.attrs = { + [0].format = SG_VERTEXFORMAT_FLOAT2, + [1].format = SG_VERTEXFORMAT_FLOAT2, + }, + .label = "Sprite pipeline", + }); + ud->renderer.uniform = (uniform_t) { + .mvp = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f }, - .pipeline = sg_make_pipeline(&(sg_pipeline_desc) { - .shader = sprite_shader, - .index_type = SG_INDEXTYPE_UINT16, - .layout.attrs = { - [0].format = SG_VERTEXFORMAT_FLOAT2, - [1].format = SG_VERTEXFORMAT_FLOAT2, - }, - .label = "Sprite pipeline", - }), - .uniform = (uniform_t) { - .mvp = { - 1.0f, 0.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, 0.0f, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f - }, - } }; shape_init_pipeline(); vec_init(&ud->shapes, sizeof(shape_t)); + vec_init(&ud->groups, sizeof(group_t)); spatial_init(&ud->spatial_grid); ud->interact.selected_count = 0; ud->interact.hovered_shape = -1; - ud->interact.selecting = false; - ud->interact.sel_dragging = false; - ud->ui.panel_w = 300; - ud->interact.move_dragging = false; - ud->interact.rotate_dragging = false; - ud->interact.resize_dragging = false; - ud->interact.resize_angle = 0.0f; - ud->interact.resize_init = NULL; + ud->interact.select.active = false; + ud->interact.select.dragging = false; + ud->ui.right_panel_w = 300; + ud->ui.left_panel_w = 220; + ud->ui.list_last_shape = -1; + ud->ui.list_prev_count = -1; + ud->interact.move.dragging = false; + ud->interact.rotate.dragging = false; + ud->interact.resize.dragging = false; + ud->interact.resize.angle = 0.0f; + ud->interact.resize.init = NULL; ud->overlay_upload_needed = true; - ud->interact.resize_init_count = 0; + ud->interact.resize.init_count = 0; + ud->next_group_id = 1; + ud->time = 0.0; + ud->interact.focused_group_id = 0; + ud->interact.last_click_time = 0.0; + ud->interact.last_click_shape_idx = -1; ud->ui.log_head = 0; ud->ui.log_count = 0; ud->ui.log_show = true; + ud->ui.active_tool = TOOL_SELECT; { ud->rect_vbuf = sg_make_buffer(&(sg_buffer_desc){ @@ -759,11 +287,9 @@ static void init(void* _userdata) }); } - *((shape_t*) vec_push(&ud->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(&ud->shapes)) = shape_star(0.0f, 0.0f, 200.0f, 80.0f, 7); - *((shape_t*) vec_push(&ud->shapes)) = shape_circle(300.0f, 0.0f, 120.0f, - (float[4]){ 1.0f, 0.47f, 0.0f, 1.0f }); + *((shape_t*) vec_push(&ud->shapes)) = shape_circle(300.0f, 0.0f, 120.0f); history_init(&ud->history); @@ -780,8 +306,6 @@ static void init(void* _userdata) compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); } -// -- cleanup -- - static void cleanup(void* _userdata) { userdata_t* ud = (userdata_t*) _userdata; @@ -791,14 +315,18 @@ static void cleanup(void* _userdata) } spatial_destroy(&ud->spatial_grid); vec_free(&ud->shapes); + vec_free(&ud->groups); history_destroy(&ud->history); - if (ud->interact.resize_init) FREE(ud->interact.resize_init); + if (ud->interact.resize.init) FREE(ud->interact.resize.init); sg_destroy_buffer(ud->rect_vbuf); sg_destroy_buffer(ud->rect_ibuf); sg_destroy_buffer(ud->handle_vbuf); sg_destroy_buffer(ud->handle_ibuf); sg_destroy_buffer(ud->corner_vbuf); sg_destroy_buffer(ud->corner_ibuf); + sg_destroy_pipeline(ud->renderer.pipeline); + sg_destroy_shader(ud->renderer.shader); + shape_pool_shutdown(); shape_shutdown_pipeline(); FREE(ud); @@ -807,599 +335,14 @@ static void cleanup(void* _userdata) sg_shutdown(); } -// -- event helpers -- - -static bool handle_key_down(userdata_t *ud, const sapp_event *event) -{ - if (event->modifiers & SAPP_MODIFIER_CTRL) { - if (event->key_code == SAPP_KEYCODE_Z || event->key_code == SAPP_KEYCODE_W) { - if (history_undo(&ud->history, &ud->shapes)) { - ud->interact.hovered_shape = -1; - spatial_mark_dirty(&ud->spatial_grid); - ud->interact.aabb_cached = false; - ud->overlay_upload_needed = true; - } - return true; - } - if (event->key_code == SAPP_KEYCODE_Y) { - if (history_redo(&ud->history, &ud->shapes)) { - ud->interact.hovered_shape = -1; - spatial_mark_dirty(&ud->spatial_grid); - ud->interact.aabb_cached = false; - ud->overlay_upload_needed = true; - } - return true; - } - } - if (event->key_code == SAPP_KEYCODE_GRAVE_ACCENT) { - ud->ui.log_show = !ud->ui.log_show; - return true; - } - return false; -} - -static void handle_resize(userdata_t *ud, const sapp_event *event) -{ - (void)event; - ud->camera.width = sapp_width(); - ud->camera.height = sapp_height(); - ud->camera.half_width = ud->camera.width * 0.5f; - ud->camera.half_height = ud->camera.height * 0.5f; - compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); -} - -static void update_shape_states(userdata_t *ud) -{ - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - shape_set_state(s, s->hovered, s->selected); - } -} - -static void handle_left_down_ctrl_click(userdata_t *ud, float wx, float wy, float tol) -{ - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (shape_hit_test(s, wx, wy, tol)) { - s->selected = !s->selected; - ud->interact.selected_count += s->selected ? 1 : -1; - ud->overlay_upload_needed = true; - break; - } - } -} - -static int hit_test_resize_handles(userdata_t *ud, float wx, float wy, float tol) -{ - if (ud->interact.selected_count <= 0) return -1; - float omin[2], omax[2]; - if (ud->interact.aabb_cached) { - omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1]; - omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3]; - } else { - selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); - } - float hs = CORNER_SIZE_PX / ud->camera.zoom * 0.5f + tol; - float mid_x = (omin[0] + omax[0]) * 0.5f; - float mid_y = (omin[1] + omax[1]) * 0.5f; - float hx[8] = {omin[0], mid_x, omax[0], omax[0], omax[0], mid_x, omin[0], omin[0]}; - float hy[8] = {omin[1], omin[1], omin[1], mid_y, omax[1], omax[1], omax[1], mid_y}; - for (int h = 0; h < 8; h++) { - if (fabsf(wx - hx[h]) <= hs && fabsf(wy - hy[h]) <= hs) - return h; - } - return -1; -} - -static void handle_left_down_resize_begin(userdata_t *ud, float wx, float wy, int resize_hit) -{ - float omin[2], omax[2]; - if (ud->interact.aabb_cached) { - omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1]; - omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3]; - } else { - selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); - } - float mid_x = (omin[0] + omax[0]) * 0.5f; - float mid_y = (omin[1] + omax[1]) * 0.5f; - float px[8] = {omax[0], mid_x, omin[0], omin[0], omin[0], mid_x, omax[0], omax[0]}; - float py[8] = {omax[1], omax[1], omax[1], mid_y, omin[1], omin[1], omin[1], mid_y}; - ud->interact.resize_pivot_x = px[resize_hit]; - ud->interact.resize_pivot_y = py[resize_hit]; - ud->interact.resize_start_wx = wx; - ud->interact.resize_start_wy = wy; - ud->interact.resize_total_scale_x = 1.0f; - ud->interact.resize_total_scale_y = 1.0f; - ud->interact.resize_mask_x = (resize_hit == 3 || resize_hit == 7 || (resize_hit & 1) == 0) ? 1.0f : 0.0f; - ud->interact.resize_mask_y = (resize_hit == 1 || resize_hit == 5 || (resize_hit & 1) == 0) ? 1.0f : 0.0f; - ud->interact.resize_dragging = true; - - float sum_sin = 0, sum_cos = 0; - int sel_n = 0; - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (s->selected) { sum_sin += sinf(s->rotation); sum_cos += cosf(s->rotation); sel_n++; } - } - ud->interact.resize_angle = atan2f(sum_sin, sum_cos); - - ud->interact.resize_init = (resize_init_t*) ALLOC((size_t)sel_n * sizeof(resize_init_t)); - ud->interact.resize_init_count = sel_n; - int j = 0; - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (s->selected) { - float sc = cosf(s->rotation), ss = sinf(s->rotation); - float hlx = (ud->interact.resize_start_wx - s->cx) * sc + (ud->interact.resize_start_wy - s->cy) * ss; - float hly = -(ud->interact.resize_start_wx - s->cx) * ss + (ud->interact.resize_start_wy - s->cy) * sc; - float plx = (ud->interact.resize_pivot_x - s->cx) * sc + (ud->interact.resize_pivot_y - s->cy) * ss; - float ply = -(ud->interact.resize_pivot_x - s->cx) * ss + (ud->interact.resize_pivot_y - s->cy) * sc; - - ud->interact.resize_init[j].idx = i; - ud->interact.resize_init[j].init_sx = s->sx; - ud->interact.resize_init[j].init_sy = s->sy; - ud->interact.resize_init[j].init_cx = s->cx; - ud->interact.resize_init[j].init_cy = s->cy; - ud->interact.resize_init[j].ext_x = hlx - plx; - ud->interact.resize_init[j].ext_y = hly - ply; - ud->interact.resize_init[j].lpi_x = plx; - ud->interact.resize_init[j].lpi_y = ply; - j++; - } - } -} - -static void handle_left_down_rotate_begin(userdata_t *ud, float wx, float wy) -{ - ud->interact.rotate_dragging = true; - ud->interact.rotate_start_angle = atan2f( - wy - ud->interact.rotate_center_y, - wx - ud->interact.rotate_center_x); - ud->interact.rotate_total_delta = 0.0f; -} - -static void handle_left_down_move_begin(userdata_t *ud, float wx, float wy) -{ - ud->interact.move_dragging = true; - ud->interact.move_start_wx = wx; - ud->interact.move_start_wy = wy; - ud->interact.move_total_dx = 0; - ud->interact.move_total_dy = 0; -} - -static void handle_left_down_select_or_marquee(userdata_t *ud, const sapp_event *event, float wx, float wy, float tol) -{ - ud->interact.selecting = true; - ud->interact.sel_dragging = false; - ud->interact.sel_sx = event->mouse_x; - ud->interact.sel_sy = event->mouse_y; - ud->interact.sel_cx = event->mouse_x; - ud->interact.sel_cy = event->mouse_y; - - ud->interact.sel_clicked_shape = -1; - for (int i = 0; i < ud->shapes.count; i++) { - if (shape_hit_test((shape_t*) vec_get(&ud->shapes, i), wx, wy, tol)) { - ud->interact.sel_clicked_shape = i; - break; - } - } -} - -static void handle_right_down_pan_begin(userdata_t *ud, const sapp_event *event) -{ - ud->camera.pan_state.dragging = true; - ud->camera.pan_state.origin_x = event->mouse_x; - ud->camera.pan_state.origin_y = event->mouse_y; -} - -static void handle_mouse_down(userdata_t *ud, const sapp_event *event) -{ - if (event->mouse_button == SAPP_MOUSEBUTTON_LEFT) { - float wx, wy; - screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); - const float tol = 4.0f / ud->camera.zoom; - - if (event->modifiers & SAPP_MODIFIER_CTRL) { - handle_left_down_ctrl_click(ud, wx, wy, tol); - } else { - int resize_hit = hit_test_resize_handles(ud, wx, wy, tol); - if (resize_hit >= 0) { - handle_left_down_resize_begin(ud, wx, wy, resize_hit); - } else { - float grip = HANDLE_RADIUS_PX / ud->camera.zoom + tol; - float dcx = wx - ud->interact.rotate_center_x; - float dcy = wy - ud->interact.rotate_center_y; - float dist = sqrtf(dcx * dcx + dcy * dcy); - bool on_handle = (ud->interact.selected_count > 0) && - (fabsf(dist - ud->interact.handle_radius) <= grip); - - if (on_handle) { - handle_left_down_rotate_begin(ud, wx, wy); - } else { - int clicked_selected = -1; - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (s->selected && shape_hit_test(s, wx, wy, tol)) { - clicked_selected = i; - break; - } - } - - bool in_aabb = false; - if (clicked_selected < 0 && ud->interact.selected_count >= 2) { - float omin[2], omax[2]; - if (ud->interact.aabb_cached) { - omin[0] = ud->interact.cached_aabb[0]; omin[1] = ud->interact.cached_aabb[1]; - omax[0] = ud->interact.cached_aabb[2]; omax[1] = ud->interact.cached_aabb[3]; - } else { - selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); - } - float pad = 8.0f / ud->camera.zoom; - omin[0] -= pad; omin[1] -= pad; - omax[0] += pad; omax[1] += pad; - in_aabb = (wx >= omin[0] && wx <= omax[0] && wy >= omin[1] && wy <= omax[1]); - } - - if (clicked_selected >= 0 || in_aabb) { - handle_left_down_move_begin(ud, wx, wy); - } else { - handle_left_down_select_or_marquee(ud, event, wx, wy, tol); - } - } - } - } - - update_shape_states(ud); - } else if (event->modifiers & SAPP_MODIFIER_RMB) { - handle_right_down_pan_begin(ud, event); - } -} - -static void handle_resize_end(userdata_t *ud) -{ - int n = ud->interact.resize_init_count; - bool changed = false; - for (int j = 0; j < n; j++) { - resize_init_t *ini = &ud->interact.resize_init[j]; - shape_t *s = (shape_t*) vec_get(&ud->shapes, ini->idx); - if (s->sx != ini->init_sx || s->sy != ini->init_sy || - s->cx != ini->init_cx || s->cy != ini->init_cy) - changed = true; - } - if (changed) { - hist_batch_t batch; - history_batch_init(&batch, n * 2); - - for (int j = 0; j < n; j++) { - resize_init_t *ini = &ud->interact.resize_init[j]; - shape_t *s = (shape_t*) vec_get(&ud->shapes, ini->idx); - history_batch_add(&batch, ini->idx, HIST_POSITION, - (float[4]){ ini->init_cx, ini->init_cy }, - (float[4]){ s->cx, s->cy }); - history_batch_add(&batch, ini->idx, HIST_SCALE, - (float[4]){ ini->init_sx, ini->init_sy }, - (float[4]){ s->sx, s->sy }); - } - - history_batch_commit(&batch, &ud->history); - } - - FREE(ud->interact.resize_init); - ud->interact.resize_init = NULL; - ud->interact.resize_init_count = 0; - ud->interact.resize_dragging = false; - - update_shape_states(ud); - spatial_mark_dirty(&ud->spatial_grid); - ud->interact.aabb_cached = false; - ud->overlay_upload_needed = true; -} - -static void handle_rotate_end(userdata_t *ud) -{ - if (ud->interact.rotate_total_delta != 0.0f) { - int sel_count = 0; - for (int i = 0; i < ud->shapes.count; i++) { - if (((shape_t*) vec_get(&ud->shapes, i))->selected) sel_count++; - } - - float cos_b = cosf(-ud->interact.rotate_total_delta); - float sin_b = sinf(-ud->interact.rotate_total_delta); - float cx = ud->interact.rotate_center_x; - float cy = ud->interact.rotate_center_y; - - hist_batch_t batch; - history_batch_init(&batch, sel_count * 2); - - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (s->selected) { - float dx = s->cx - cx; - float dy = s->cy - cy; - float old_cx = cx + dx * cos_b - dy * sin_b; - float old_cy = cy + dx * sin_b + dy * cos_b; - - history_batch_add(&batch, i, HIST_POSITION, - (float[4]){ old_cx, old_cy }, - (float[4]){ s->cx, s->cy }); - history_batch_add(&batch, i, HIST_ROTATION, - (float[4]){ s->rotation - ud->interact.rotate_total_delta }, - (float[4]){ s->rotation }); - } - } - - history_batch_commit(&batch, &ud->history); - } - - ud->interact.rotate_dragging = false; - update_shape_states(ud); - spatial_mark_dirty(&ud->spatial_grid); - ud->interact.aabb_cached = false; - ud->overlay_upload_needed = true; -} - -static void handle_move_end(userdata_t *ud) -{ - if (ud->interact.move_total_dx != 0.0f || ud->interact.move_total_dy != 0.0f) { - int sel_count = 0; - for (int i = 0; i < ud->shapes.count; i++) { - if (((shape_t*) vec_get(&ud->shapes, i))->selected) sel_count++; - } - - hist_batch_t batch; - history_batch_init(&batch, sel_count); - - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (s->selected) { - history_batch_add(&batch, i, HIST_POSITION, - (float[4]){ s->cx - ud->interact.move_total_dx, s->cy - ud->interact.move_total_dy }, - (float[4]){ s->cx, s->cy }); - } - } - - history_batch_commit(&batch, &ud->history); - } - - ud->interact.move_dragging = false; - update_shape_states(ud); - spatial_mark_dirty(&ud->spatial_grid); - ud->interact.aabb_cached = false; - ud->overlay_upload_needed = true; -} - -static void handle_select_end(userdata_t *ud) -{ - if (!ud->interact.sel_dragging) { - if (ud->interact.sel_clicked_shape >= 0) { - for (int i = 0; i < ud->shapes.count; i++) { - ((shape_t*) vec_get(&ud->shapes, i))->selected = false; - } - ((shape_t*) vec_get(&ud->shapes, ud->interact.sel_clicked_shape))->selected = true; - ud->interact.selected_count = 1; - } else { - for (int i = 0; i < ud->shapes.count; i++) { - ((shape_t*) vec_get(&ud->shapes, i))->selected = false; - } - ud->interact.selected_count = 0; - } - } - - ud->interact.selecting = false; - ud->interact.sel_dragging = false; - update_shape_states(ud); - ud->overlay_upload_needed = true; -} - -static void handle_mouse_up(userdata_t *ud, const sapp_event *event) -{ - (void)event; - - if (ud->interact.resize_dragging) { - handle_resize_end(ud); - } else if (ud->interact.rotate_dragging) { - handle_rotate_end(ud); - } else if (ud->interact.move_dragging) { - handle_move_end(ud); - } else if (ud->interact.selecting) { - handle_select_end(ud); - } - - ud->camera.pan_state.dragging = false; -} - -static void handle_pan_drag(userdata_t *ud, const sapp_event *event) -{ - ud->camera.pan[0] += event->mouse_dx; - ud->camera.pan[1] -= event->mouse_dy; - compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); -} - -static void handle_resize_drag(userdata_t *ud, const sapp_event *event) -{ - float wx, wy; - screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); - - float sx_total = 1.0f, sy_total = 1.0f; - - for (int j = 0; j < ud->interact.resize_init_count; j++) { - resize_init_t *ini = &ud->interact.resize_init[j]; - shape_t *s = (shape_t*) vec_get(&ud->shapes, ini->idx); - - float sc = cosf(s->rotation), ss = sinf(s->rotation); - float mlx = (wx - ini->init_cx) * sc + (wy - ini->init_cy) * ss; - float mly = -(wx - ini->init_cx) * ss + (wy - ini->init_cy) * sc; - - float cex = mlx - ini->lpi_x; - float cey = mly - ini->lpi_y; - - float scale_x = 1.0f, scale_y = 1.0f; - if (ud->interact.resize_mask_x && fabsf(ini->ext_x) >= 0.0001f) - scale_x = fabsf(cex / ini->ext_x); - if (ud->interact.resize_mask_y && fabsf(ini->ext_y) >= 0.0001f) - scale_y = fabsf(cey / ini->ext_y); - - s->sx = ini->init_sx * scale_x; - s->sy = ini->init_sy * scale_y; - s->cx = ini->init_cx - ini->lpi_x * (scale_x - 1.0f) * sc + ini->lpi_y * (scale_y - 1.0f) * ss; - s->cy = ini->init_cy - ini->lpi_x * (scale_x - 1.0f) * ss - ini->lpi_y * (scale_y - 1.0f) * sc; - - shape_regenerate(s); - shape_set_state(s, false, true); - - sx_total = scale_x; - sy_total = scale_y; - } - - ud->interact.resize_total_scale_x = sx_total; - ud->interact.resize_total_scale_y = sy_total; -} - -static void handle_rotate_drag(userdata_t *ud, const sapp_event *event) -{ - float wx, wy; - screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); - float angle = atan2f(wy - ud->interact.rotate_center_y, - wx - ud->interact.rotate_center_x); - float delta = angle - ud->interact.rotate_start_angle; - if (delta > GLM_PIf) delta -= 2.0f * GLM_PIf; - else if (delta < -GLM_PIf) delta += 2.0f * GLM_PIf; - float inc = delta - ud->interact.rotate_total_delta; - - float cos_a = cosf(inc); - float sin_a = sinf(inc); - float cx = ud->interact.rotate_center_x; - float cy = ud->interact.rotate_center_y; - - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (s->selected) { - float dx = s->cx - cx; - float dy = s->cy - cy; - s->cx = cx + dx * cos_a - dy * sin_a; - s->cy = cy + dx * sin_a + dy * cos_a; - s->rotation += inc; - shape_build_transform(s); - shape_set_state(s, false, true); - } - } - - ud->interact.rotate_total_delta = delta; -} - -static void handle_move_drag(userdata_t *ud, const sapp_event *event) -{ - float wx, wy; - screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); - float dx = wx - ud->interact.move_start_wx; - float dy = wy - ud->interact.move_start_wy; - float delta_x = dx - ud->interact.move_total_dx; - float delta_y = dy - ud->interact.move_total_dy; - - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - if (s->selected) { - s->cx += delta_x; - s->cy += delta_y; - shape_build_transform(s); - shape_set_state(s, false, true); - } - } - - ud->interact.move_total_dx = dx; - ud->interact.move_total_dy = dy; -} - -static void handle_marquee_drag(userdata_t *ud, const sapp_event *event) -{ - ud->interact.sel_cx = event->mouse_x; - ud->interact.sel_cy = event->mouse_y; - float dx = ud->interact.sel_cx - ud->interact.sel_sx; - float dy = ud->interact.sel_cy - ud->interact.sel_sy; - if (dx * dx + dy * dy > 9.0f) { - ud->interact.sel_dragging = true; - } - - if (ud->interact.sel_dragging) { - float wx1, wy1, wx2, wy2; - screen_to_world(&ud->camera, ud->interact.sel_sx, ud->interact.sel_sy, &wx1, &wy1); - screen_to_world(&ud->camera, ud->interact.sel_cx, ud->interact.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); - - ud->interact.selected_count = spatial_query_rect_select( - &ud->spatial_grid, &ud->shapes, - min_x, min_y, max_x, max_y); - - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - shape_set_state(s, false, s->selected); - } - } -} - -static void handle_hover(userdata_t *ud, const sapp_event *event) -{ - float wx, wy; - screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); - const float tol = ud->camera.hover_tol; - - int hovered = spatial_query_point(&ud->spatial_grid, - &ud->shapes, wx, wy, tol); - if (hovered != ud->interact.hovered_shape) { - ud->interact.hovered_shape = hovered; - EM_ASM({ document.querySelector('canvas').style.cursor = $0 ? 'pointer' : 'default'; }, hovered >= 0); - } - for (int i = 0; i < ud->shapes.count; i++) { - shape_t *s = (shape_t*) vec_get(&ud->shapes, i); - shape_set_state(s, i == hovered, s->selected); - } -} - -static void handle_mouse_move(userdata_t *ud, const sapp_event *event) -{ - if (ud->camera.pan_state.dragging) { - handle_pan_drag(ud, event); - } else if (ud->interact.resize_dragging) { - handle_resize_drag(ud, event); - } else if (ud->interact.rotate_dragging) { - handle_rotate_drag(ud, event); - } else if (ud->interact.move_dragging) { - handle_move_drag(ud, event); - } else if (ud->interact.selecting) { - handle_marquee_drag(ud, event); - } else { - handle_hover(ud, event); - } -} - -static void handle_scroll_zoom(userdata_t *ud, const sapp_event *event) -{ - if ((ud->camera.zoom >= 6.0f && event->scroll_y > 0.0f) || - (ud->camera.zoom <= 0.1f && event->scroll_y < 0.0f)) - return; - - float wx, wy; - screen_to_world(&ud->camera, event->mouse_x, event->mouse_y, &wx, &wy); - - const float diff = expf(event->scroll_y * 0.1f); - ud->camera.zoom = _sg_clamp(ud->camera.zoom * diff, 0.1f, 6.0f); - ud->camera.hover_tol = SHAPE_HOVER_PX / ud->camera.zoom; - - const float sx = event->mouse_x - ud->camera.half_width; - const float sy = ud->camera.half_height - event->mouse_y; - ud->camera.pan[0] = sx - wx * ud->camera.zoom; - ud->camera.pan[1] = sy - wy * ud->camera.zoom; - - ud->overlay_upload_needed = true; - compute_mvp(&ud->camera, &ud->renderer.uniform.mvp); -} - -// -- event -- - static void event(const sapp_event* event, void* _userdata) { userdata_t* ud = (userdata_t*) _userdata; + if (event->type == SAPP_EVENTTYPE_RESIZED) { + handle_resize(ud, event); + } + if (event->type == SAPP_EVENTTYPE_KEY_DOWN) { if (handle_key_down(ud, event)) return; } @@ -1407,9 +350,6 @@ static void event(const sapp_event* event, void* _userdata) if (simgui_handle_event(event)) return; switch (event->type) { - case SAPP_EVENTTYPE_RESIZED: - handle_resize(ud, event); - break; case SAPP_EVENTTYPE_MOUSE_DOWN: handle_mouse_down(ud, event); break; @@ -1427,8 +367,6 @@ static void event(const sapp_event* event, void* _userdata) } } -// -- entry point -- - sapp_desc sokol_main(int argc, char* argv[]) { userdata_t* userdata = (userdata_t*) ALLOC(sizeof(userdata_t)); diff --git a/src/overlay.h b/src/overlay.h new file mode 100644 index 0000000..e20795b --- /dev/null +++ b/src/overlay.h @@ -0,0 +1,144 @@ +#ifndef OVERLAY_H +#define OVERLAY_H + +#include "api.h" +#include "types.h" +#include "interact.h" + +static void compute_overlay_geometry(userdata_t *ud, + shape_vertex_t overlay_verts[5], + float *sel_cx, float *sel_cy, float *sel_hw, float *sel_hh, float *sel_angle, + bool *has_overlay, bool *show_handle) +{ + *has_overlay = false; + *sel_cx = *sel_cy = *sel_angle = 0; + *sel_hw = *sel_hh = 0; + + if (ud->interact.select.active && ud->interact.select.dragging) { + float wx1, wy1, wx2, wy2; + screen_to_world(&ud->camera, ud->interact.select.start_x, ud->interact.select.start_y, &wx1, &wy1); + screen_to_world(&ud->camera, ud->interact.select.current_x, ud->interact.select.current_y, &wx2, &wy2); + float x1 = fminf(wx1, wx2), y1 = fminf(wy1, wy2); + float x2 = fmaxf(wx1, wx2), y2 = fmaxf(wy1, wy2); + overlay_verts[0] = (shape_vertex_t){x1, y1}; + overlay_verts[1] = (shape_vertex_t){x2, y1}; + overlay_verts[2] = (shape_vertex_t){x2, y2}; + overlay_verts[3] = (shape_vertex_t){x1, y2}; + overlay_verts[4] = (shape_vertex_t){x1, y1}; + *has_overlay = true; + } else if (ud->interact.selected_count >= 1) { + if (ud->interact.selected_count == 1) { + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (!s->selected) continue; + *sel_cx = s->cx; *sel_cy = s->cy; + *sel_hw = s->sx; *sel_hh = s->sy; + *sel_angle = s->rotation; + float x1, y1, x2, y2; + selected_aabb(ud, &x1, &y1, &x2, &y2); + ud->interact.cached_aabb[0] = x1; ud->interact.cached_aabb[1] = y1; + ud->interact.cached_aabb[2] = x2; ud->interact.cached_aabb[3] = y2; + ud->interact.aabb_cached = true; + overlay_verts[0] = (shape_vertex_t){x1, y1}; + overlay_verts[1] = (shape_vertex_t){x2, y1}; + overlay_verts[2] = (shape_vertex_t){x2, y2}; + overlay_verts[3] = (shape_vertex_t){x1, y2}; + overlay_verts[4] = overlay_verts[0]; + break; + } + } else { + float omin[2], omax[2]; + float sum_sin = 0, sum_cos = 0; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (!s->selected) continue; + sum_sin += sinf(s->rotation); + sum_cos += cosf(s->rotation); + } + selected_aabb(ud, &omin[0], &omin[1], &omax[0], &omax[1]); + ud->interact.cached_aabb[0] = omin[0]; ud->interact.cached_aabb[1] = omin[1]; + ud->interact.cached_aabb[2] = omax[0]; ud->interact.cached_aabb[3] = omax[1]; + ud->interact.aabb_cached = true; + float pad = 8.0f / ud->camera.zoom; + omin[0] -= pad; omin[1] -= pad; + omax[0] += pad; omax[1] += pad; + *sel_cx = (omin[0] + omax[0]) * 0.5f; + *sel_cy = (omin[1] + omax[1]) * 0.5f; + *sel_hw = (omax[0] - omin[0]) * 0.5f; + *sel_hh = (omax[1] - omin[1]) * 0.5f; + *sel_angle = atan2f(sum_sin, sum_cos); + + overlay_verts[0] = (shape_vertex_t){omin[0], omin[1]}; + overlay_verts[1] = (shape_vertex_t){omax[0], omin[1]}; + overlay_verts[2] = (shape_vertex_t){omax[0], omax[1]}; + overlay_verts[3] = (shape_vertex_t){omin[0], omax[1]}; + overlay_verts[4] = overlay_verts[0]; + } + *has_overlay = true; + } + + *show_handle = ud->interact.selected_count > 0 && !ud->interact.select.active; +} + +static void upload_overlay_buffers(userdata_t *ud, + const shape_vertex_t overlay_verts[5], + float sel_cx, float sel_cy, float sel_hw, float sel_hh, float sel_angle, + bool has_overlay, bool show_handle) +{ + bool need_upload = ud->overlay_upload_needed || + ud->interact.move.dragging || ud->interact.rotate.dragging || + ud->interact.resize.dragging || ud->interact.select.active; + + if (has_overlay && need_upload) { + sg_update_buffer(ud->rect_vbuf, &(sg_range){overlay_verts, (size_t)5 * sizeof(shape_vertex_t)}); + } + + if (show_handle) { + float pad = HANDLE_OFFSET_PX / ud->camera.zoom; + float radius = sqrtf(sel_hw * sel_hw + sel_hh * sel_hh) + pad; + + ud->interact.rotate.center_x = sel_cx; + ud->interact.rotate.center_y = sel_cy; + ud->interact.rotate.handle_radius = radius; + + const int n = HANDLE_CIRCLE_SEGMENTS + 1; + shape_vertex_t hv[HANDLE_CIRCLE_SEGMENTS + 1]; + for (int i = 0; i < n; i++) { + float a = (float)i / (float)HANDLE_CIRCLE_SEGMENTS * 2.0f * GLM_PIf; + hv[i] = (shape_vertex_t){sel_cx + cosf(a) * radius, sel_cy + sinf(a) * radius}; + } + if (need_upload) + sg_update_buffer(ud->handle_vbuf, &(sg_range){hv, sizeof(hv)}); + + { + float hs = CORNER_SIZE_PX / ud->camera.zoom * 0.5f; + float mid_x = (overlay_verts[0].x + overlay_verts[1].x) * 0.5f; + float mid_y = (overlay_verts[0].y + overlay_verts[2].y) * 0.5f; + float handles[8][2] = { + {overlay_verts[0].x, overlay_verts[0].y}, + {mid_x, overlay_verts[0].y}, + {overlay_verts[1].x, overlay_verts[1].y}, + {overlay_verts[1].x, mid_y }, + {overlay_verts[2].x, overlay_verts[2].y}, + {mid_x, overlay_verts[2].y}, + {overlay_verts[3].x, overlay_verts[3].y}, + {overlay_verts[3].x, mid_y }, + }; + shape_vertex_t cv[40]; + for (int h = 0; h < 8; h++) { + float cx = handles[h][0], cy = handles[h][1]; + cv[h*5+0] = (shape_vertex_t){cx - hs, cy - hs}; + cv[h*5+1] = (shape_vertex_t){cx + hs, cy - hs}; + cv[h*5+2] = (shape_vertex_t){cx + hs, cy + hs}; + cv[h*5+3] = (shape_vertex_t){cx - hs, cy + hs}; + cv[h*5+4] = (shape_vertex_t){cx - hs, cy - hs}; + } + if (need_upload) + sg_update_buffer(ud->corner_vbuf, &(sg_range){cv, sizeof(cv)}); + } + } + + ud->overlay_upload_needed = false; +} + +#endif diff --git a/src/render.h b/src/render.h index 652b5ca..7f881b5 100644 --- a/src/render.h +++ b/src/render.h @@ -62,11 +62,7 @@ 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); + sg_draw((int)s->index_base, (int)s->num_elements, 1); } #endif diff --git a/src/shaders/shape.wgsl b/src/shaders/shape.wgsl index a3719f5..e76259a 100644 --- a/src/shaders/shape.wgsl +++ b/src/shaders/shape.wgsl @@ -4,7 +4,6 @@ struct VsUniform { struct ShapeUniform { transform: mat4x4f, - base_color: vec4f, state: u32, }; @@ -31,9 +30,9 @@ struct FsOut { 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)); + output.color = vec4f(0.5, 0.6, 1.0, 1.0); } else { - output.color = shape_uniform.base_color; + output.color = vec4f(0.8, 0.8, 0.8, 1.0); } return output; } diff --git a/src/shape.h b/src/shape.h index 46b36af..c1b65f3 100644 --- a/src/shape.h +++ b/src/shape.h @@ -7,9 +7,15 @@ typedef struct shape_vertex_t { float x, y; } shape_vertex_t; +typedef enum { + SHAPE_CIRCLE, + SHAPE_RECTANGLE, + SHAPE_STAR, + SHAPE_GENERIC, +} shape_kind_t; + typedef struct shape_uniform_t { mat4 transform; - float base_color[4]; uint32_t state; uint8_t _pad[12]; } shape_uniform_t; @@ -17,10 +23,8 @@ typedef struct shape_uniform_t { typedef struct shape_t { shape_vertex_t *verts; uint16_t *indices; - uint32_t num_indices; + uint32_t num_elements; uint32_t num_verts; - sg_buffer vbuf; - sg_buffer ibuf; shape_uniform_t uniform; bool hovered; bool selected; @@ -28,8 +32,119 @@ typedef struct shape_t { float cx, cy; float sx, sy; float rotation; + int kind; + + uint32_t vertex_base; + uint32_t index_base; + + int group_id; } shape_t; +// -- group entity (for nested groups) -- + +typedef struct { + int id; + int parent_id; // 0 = top-level group +} group_t; + +static group_t* find_group(vector_t *groups, int id) { + for (int i = 0; i < groups->count; i++) { + group_t *g = (group_t*) vec_get(groups, i); + if (g->id == id) return g; + } + return NULL; +} + +static int get_topmost_group(vector_t *groups, int gid) { + while (gid != 0) { + group_t *g = find_group(groups, gid); + if (!g || g->parent_id == 0) return gid; + gid = g->parent_id; + } + return 0; +} + +static bool is_shape_in_group_hierarchy(int shape_gid, int target_gid, vector_t *groups) { + int cur = shape_gid; + while (cur != 0) { + if (cur == target_gid) return true; + group_t *g = find_group(groups, cur); + if (!g) return false; + cur = g->parent_id; + } + return false; +} + +// -- shared geometry buffers (one vbuf + one ibuf for all shapes) -- + +static sg_buffer g_shape_vbuf = {0}; +static sg_buffer g_shape_ibuf = {0}; +static bool g_shape_pool_dirty; +static uint32_t g_shape_vert_count; +static uint32_t g_shape_idx_count; + +static void shape_pool_rebuild(vector_t *shapes) +{ + // count total vertices / indices (line strips: num_elements == num_verts + 1) + uint32_t total_verts = 0, total_indices = 0; + for (int i = 0; i < shapes->count; i++) { + shape_t *s = (shape_t*) vec_get(shapes, i); + total_verts += s->num_elements; + total_indices += s->num_elements; + } + + if (g_shape_vbuf.id) { sg_destroy_buffer(g_shape_vbuf); g_shape_vbuf.id = 0; } + if (g_shape_ibuf.id) { sg_destroy_buffer(g_shape_ibuf); g_shape_ibuf.id = 0; } + g_shape_vert_count = 0; + g_shape_idx_count = 0; + + if (total_verts == 0) { + g_shape_pool_dirty = false; + return; + } + + shape_vertex_t *all_v = (shape_vertex_t*) ALLOC((size_t)total_verts * sizeof(shape_vertex_t)); + uint16_t *all_i = (uint16_t*) ALLOC((size_t)total_indices * sizeof(uint16_t)); + + uint32_t voff = 0, ioff = 0; + for (int i = 0; i < shapes->count; i++) { + shape_t *s = (shape_t*) vec_get(shapes, i); + uint32_t n = s->num_elements; + memcpy(&all_v[voff], s->verts, (size_t)n * sizeof(shape_vertex_t)); + for (uint32_t j = 0; j < n; j++) + all_i[ioff + j] = (uint16_t)(voff + s->indices[j]); + s->vertex_base = voff; + s->index_base = ioff; + voff += n; + ioff += n; + } + + g_shape_vbuf = sg_make_buffer(&(sg_buffer_desc){ + .data = { all_v, (size_t)total_verts * sizeof(shape_vertex_t) }, + .label = "Shape verts (shared)", + }); + g_shape_ibuf = sg_make_buffer(&(sg_buffer_desc){ + .data = { all_i, (size_t)total_indices * sizeof(uint16_t) }, + .usage = { .index_buffer = true }, + .label = "Shape indices (shared)", + }); + + FREE(all_v); + FREE(all_i); + + g_shape_vert_count = total_verts; + g_shape_idx_count = total_indices; + g_shape_pool_dirty = false; +} + +static void shape_pool_shutdown(void) +{ + if (g_shape_vbuf.id) { sg_destroy_buffer(g_shape_vbuf); g_shape_vbuf.id = 0; } + if (g_shape_ibuf.id) { sg_destroy_buffer(g_shape_ibuf); g_shape_ibuf.id = 0; } + g_shape_vert_count = 0; + g_shape_idx_count = 0; +} + #define SHAPE_HOVER_PX 6.0f static int shape_calc_segments(float r) @@ -40,13 +155,13 @@ static int shape_calc_segments(float r) return n; } -static void shape_init_common(shape_t *s, const float color[4]) +static void shape_init_common(shape_t *s) { 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)); + s->group_id = 0; } static void shape_build_transform(shape_t *s) @@ -61,24 +176,13 @@ static void shape_build_transform(shape_t *s) static void shape_make_buffers(shape_t *s) { - uint32_t vcount = s->num_verts + 1; - s->vbuf = sg_make_buffer(&(sg_buffer_desc) { - .size = (size_t)vcount * sizeof(shape_vertex_t), - .usage = { .stream_update = true }, - .label = "Shape vertices", - }); - sg_update_buffer(s->vbuf, &(sg_range){s->verts, (size_t)vcount * 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", - }); + (void)s; + g_shape_pool_dirty = true; } static void shape_shutdown(shape_t *s) { - sg_destroy_buffer(s->vbuf); - sg_destroy_buffer(s->ibuf); + g_shape_pool_dirty = true; FREE(s->verts); FREE(s->indices); } @@ -135,12 +239,13 @@ static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol) return false; } -static shape_t shape_circle(float x, float y, float r, const float color[4]) +static shape_t shape_circle(float x, float y, float r) { shape_t s; s.cx = x; s.cy = y; s.sx = r; s.sy = r; s.rotation = 0.0f; + s.kind = SHAPE_CIRCLE; int segs = shape_calc_segments(r); int count = segs + 1; @@ -153,22 +258,23 @@ static shape_t shape_circle(float x, float y, float r, const float color[4]) } 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_elements = (uint32_t)count; s.num_verts = (uint32_t)segs; - shape_init_common(&s, color); + shape_init_common(&s); shape_build_transform(&s); 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]) + int points) { shape_t s; s.cx = x; s.cy = y; s.sx = outer_r; s.sy = outer_r; s.rotation = 0.0f; + s.kind = SHAPE_STAR; int n = points * 2; int count = n + 1; @@ -183,10 +289,36 @@ static shape_t shape_star(float x, float y, float outer_r, float inner_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_elements = (uint32_t)count; s.num_verts = (uint32_t)n; - shape_init_common(&s, color); + shape_init_common(&s); + shape_build_transform(&s); + shape_make_buffers(&s); + return s; +} + +static shape_t shape_rectangle(float x, float y, float w, float h) +{ + shape_t s; + s.cx = x; s.cy = y; + s.sx = w * 0.5f; s.sy = h * 0.5f; + s.rotation = 0.0f; + s.kind = SHAPE_RECTANGLE; + + s.num_verts = 4; + s.num_elements = 5; + s.verts = (shape_vertex_t*) ALLOC(5 * sizeof(shape_vertex_t)); + s.indices = (uint16_t*) ALLOC(5 * sizeof(uint16_t)); + + s.verts[0] = (shape_vertex_t){-1.0f, -1.0f}; + s.verts[1] = (shape_vertex_t){ 1.0f, -1.0f}; + s.verts[2] = (shape_vertex_t){ 1.0f, 1.0f}; + s.verts[3] = (shape_vertex_t){-1.0f, 1.0f}; + s.verts[4] = s.verts[0]; + for (int i = 0; i < 5; i++) s.indices[i] = (uint16_t)i; + + shape_init_common(&s); shape_build_transform(&s); shape_make_buffers(&s); return s; diff --git a/src/types.h b/src/types.h new file mode 100644 index 0000000..8a9272f --- /dev/null +++ b/src/types.h @@ -0,0 +1,133 @@ +#ifndef TYPES_H +#define TYPES_H + +#include "api.h" + +#define LOG_RING_SIZE 64 +#define HANDLE_OFFSET_PX 0.0f +#define HANDLE_RADIUS_PX 12.0f +#define HANDLE_CIRCLE_SEGMENTS 256 +#define CORNER_SIZE_PX 8.0f +#define TOP_PANEL_H 32.0f + +typedef enum { + TOOL_SELECT, + TOOL_PEN, + TOOL_CIRCLE, + TOOL_RECTANGLE, + TOOL_COUNT +} tool_t; + +typedef struct log_entry_t { + char text[256]; + uint32_t level; +} log_entry_t; + +typedef struct { + mat4 mvp; +} uniform_t; + +typedef struct renderer_t { + sg_pipeline pipeline; + sg_shader shader; + sg_pass_action clear_pass; + uniform_t uniform; +} renderer_t; + +typedef struct { + int idx; + float init_sx, init_sy, init_cx, init_cy; + float ext_x, ext_y; + float lpi_x, lpi_y; +} resize_init_t; + +typedef struct { + bool active; + float start_x, start_y; + float current_x, current_y; + bool dragging; + int clicked_shape; +} select_state_t; + +typedef struct { + bool dragging; + float start_wx, start_wy; + float total_dx, total_dy; +} move_state_t; + +typedef struct { + bool dragging; + float center_x, center_y; + float start_angle; + float total_delta; + float handle_radius; +} rotate_state_t; + +typedef struct { + bool dragging; + float pivot_x, pivot_y; + float start_wx, start_wy; + float total_scale_x, total_scale_y; + float mask_x, mask_y; + float angle; + resize_init_t *init; + int init_count; +} resize_state_t; + +typedef struct { + int selected_count; + int hovered_shape; + + select_state_t select; + move_state_t move; + rotate_state_t rotate; + resize_state_t resize; + + float cached_aabb[4]; + bool aabb_cached; + + int focused_group_id; + double last_click_time; + int last_click_shape_idx; +} interact_state_t; + +typedef struct { + float fps_immediate; + float fps_average; + float frame_times[60]; + int frame_time_head; + int frame_time_count; + float frame_time_sum; +} debug_stats_t; + +typedef struct { + float right_panel_w; + float left_panel_w; + log_entry_t log_ring[LOG_RING_SIZE]; + int log_head; + int log_count; + bool log_show; + tool_t active_tool; + int list_last_shape; + int list_prev_count; +} ui_state_t; + +typedef struct userdata_t { + camera_t camera; + renderer_t renderer; + vector_t shapes; + spatial_grid_t spatial_grid; + interact_state_t interact; + history_t history; + debug_stats_t debug; + ui_state_t ui; + bool overlay_upload_needed; + sg_buffer rect_vbuf, rect_ibuf; + sg_buffer handle_vbuf, handle_ibuf; + sg_buffer corner_vbuf, corner_ibuf; + int next_group_id; + vector_t groups; + double time; +} userdata_t; + +#endif diff --git a/src/ui_panels.h b/src/ui_panels.h new file mode 100644 index 0000000..6d7a243 --- /dev/null +++ b/src/ui_panels.h @@ -0,0 +1,425 @@ +#ifndef UI_PANELS_H +#define UI_PANELS_H + +#include "api.h" +#include "types.h" +#include "interact.h" + +static const char *shape_kind_label(int kind) { + switch (kind) { + case SHAPE_CIRCLE: return "Circle"; + case SHAPE_RECTANGLE: return "Rect"; + case SHAPE_STAR: return "Star"; + default: return "Shape"; + } +} + +static void build_display_recursive(vector_t *shapes, vector_t *groups, int parent_gid, int *display, int *dlen) +{ + for (int g = 0; g < groups->count; g++) { + group_t *grp = (group_t*) vec_get(groups, g); + if (grp->parent_id != parent_gid) continue; + for (int i = 0; i < shapes->count; i++) { + if (((shape_t*) vec_get(shapes, i))->group_id == grp->id) + display[(*dlen)++] = i; + } + build_display_recursive(shapes, groups, grp->id, display, dlen); + } + if (parent_gid == 0) { + for (int i = 0; i < shapes->count; i++) { + if (((shape_t*) vec_get(shapes, i))->group_id == 0) + display[(*dlen)++] = i; + } + } +} + +static int count_shapes_in_subtree(vector_t *shapes, vector_t *groups, int gid) +{ + int c = 0; + for (int i = 0; i < shapes->count; i++) + if (is_shape_in_group_hierarchy(((shape_t*) vec_get(shapes, i))->group_id, gid, groups)) + c++; + return c; +} + +static void list_shape_clicked(userdata_t *ud, shape_t *s, int *display, int display_len, int display_pos) +{ + int n = ud->shapes.count; + bool ctrl = igGetIO_Nil()->KeyCtrl; + bool shift = igGetIO_Nil()->KeyShift && ud->ui.list_last_shape >= 0; + + if (shift) { + int from = ud->ui.list_last_shape; + int to = display_pos; + if (from > to) { int tmp = from; from = to; to = tmp; } + for (int j = 0; j < n; j++) + ((shape_t*) vec_get(&ud->shapes, j))->selected = false; + ud->interact.selected_count = 0; + for (int d = from; d <= to; d++) { + shape_t *sv = (shape_t*) vec_get(&ud->shapes, display[d]); + sv->selected = true; + ud->interact.selected_count++; + } + } else if (ctrl) { + s->selected = !s->selected; + ud->interact.selected_count += s->selected ? 1 : -1; + } else { + for (int j = 0; j < n; j++) + ((shape_t*) vec_get(&ud->shapes, j))->selected = false; + ud->interact.selected_count = 0; + if (s->group_id != 0) { + int topmost = get_topmost_group(&ud->groups, s->group_id); + for (int j = 0; j < n; j++) { + shape_t *sj = (shape_t*) vec_get(&ud->shapes, j); + if (is_shape_in_group_hierarchy(sj->group_id, topmost, &ud->groups)) { + sj->selected = true; + ud->interact.selected_count++; + } + } + } else { + s->selected = true; + ud->interact.selected_count = 1; + } + } + ud->ui.list_last_shape = display_pos; + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; + update_shape_states(ud); +} + +static int render_tree_level(userdata_t *ud, int parent_gid, int *display, int display_len, int display_pos) +{ + int n = ud->shapes.count; + int pos = display_pos; + + for (int g = 0; g < ud->groups.count; g++) { + group_t *grp = (group_t*) vec_get(&ud->groups, g); + if (grp->parent_id != parent_gid) continue; + + int gid = grp->id; + + int member_count = 0, sel_count = 0; + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->group_id == gid) { member_count++; if (s->selected) sel_count++; } + } + + char hdr[128]; + snprintf(hdr, sizeof(hdr), "Group %d (%d)##g%d", gid, member_count, gid); + + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_DefaultOpen; + + bool open = igTreeNodeEx_Str(hdr, flags); + + int group_first = pos; + + if (igIsItemClicked(ImGuiMouseButton_Left)) { + bool ctrl = igGetIO_Nil()->KeyCtrl; + if (ctrl) + toggle_group_recursive(ud, gid); + else + deselect_and_select_group_recursive(ud, gid); + ud->ui.list_last_shape = group_first; + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; + update_shape_states(ud); + } + + if (open) { + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->group_id != gid) continue; + + char label[128]; + snprintf(label, sizeof(label), " %s##s%d", shape_kind_label(s->kind), i); + + if (igSelectable_Bool(label, s->selected, ImGuiSelectableFlags_None, (ImVec2){0,0})) + list_shape_clicked(ud, s, display, display_len, pos); + + pos++; + } + pos = render_tree_level(ud, gid, display, display_len, pos); + igTreePop(); + } else { + pos += count_shapes_in_subtree(&ud->shapes, &ud->groups, gid); + } + } + + if (parent_gid == 0) { + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (s->group_id != 0) continue; + + char label[128]; + snprintf(label, sizeof(label), "%s##s%d", shape_kind_label(s->kind), i); + + if (igSelectable_Bool(label, s->selected, ImGuiSelectableFlags_None, (ImVec2){0,0})) + list_shape_clicked(ud, s, display, display_len, pos); + + pos++; + } + } + + return pos; +} + +static void draw_top_panel(userdata_t *ud) +{ + igSetNextWindowPos((ImVec2){0, 0}, ImGuiCond_Always, (ImVec2){0, 0}); + igSetNextWindowSize((ImVec2){ud->camera.width, TOP_PANEL_H}, ImGuiCond_Always); + igBegin("Toolbar", NULL, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoCollapse); + + for (int t = 0; t < TOOL_COUNT; t++) { + const char *label = NULL; + switch (t) { + case TOOL_SELECT: label = "Sel"; break; + case TOOL_PEN: label = "Pen"; break; + case TOOL_CIRCLE: label = "Circle"; break; + case TOOL_RECTANGLE: label = "Rect"; break; + default: break; + } + if (t > 0) igSameLine(0.0f, 4.0f); + bool active = (ud->ui.active_tool == t); + if (active) { + igPushStyleColor_Vec4(ImGuiCol_Button, (ImVec4){0.3f, 0.5f, 0.8f, 1.0f}); + igPushStyleColor_Vec4(ImGuiCol_ButtonHovered, (ImVec4){0.4f, 0.6f, 0.9f, 1.0f}); + } + if (igButton(label, (ImVec2){0, 0})) { + tool_t new_tool = (tool_t)t; + if (new_tool != TOOL_SELECT && new_tool != ud->ui.active_tool) { + for (int i = 0; i < ud->shapes.count; i++) { + ((shape_t*)vec_get(&ud->shapes, i))->selected = false; + } + ud->interact.selected_count = 0; + ud->overlay_upload_needed = true; + update_shape_states(ud); + } + ud->ui.active_tool = new_tool; + } + if (active) + igPopStyleColor(2); + } + + igSameLine(0.0f, 16.0f); + + if (igButton("Undo", (ImVec2){0, 0})) { + if (history_undo(&ud->history, &ud->shapes)) { + rebuild_groups_from_shapes(&ud->groups, &ud->shapes); + ud->interact.hovered_shape = -1; + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; + } + } + + igSameLine(0.0f, 4.0f); + + if (igButton("Redo", (ImVec2){0, 0})) { + if (history_redo(&ud->history, &ud->shapes)) { + rebuild_groups_from_shapes(&ud->groups, &ud->shapes); + ud->interact.hovered_shape = -1; + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; + } + } + + if (ud->interact.focused_group_id != 0) { + igSameLine(0.0f, 16.0f); + char flbl[64]; + snprintf(flbl, sizeof(flbl), "Focus: Group %d (Esc)", ud->interact.focused_group_id); + igTextColored((ImVec4){0.3f, 1.0f, 0.3f, 1.0f}, "%s", flbl); + } + + igEnd(); +} + +static void draw_shape_list_panel(userdata_t *ud) +{ + igSetNextWindowPos((ImVec2){0, TOP_PANEL_H}, ImGuiCond_Always, (ImVec2){0, 0}); + igSetNextWindowSize((ImVec2){ud->ui.left_panel_w, ud->camera.height - TOP_PANEL_H}, ImGuiCond_Always); + igBegin("Shapes", NULL, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + int n = ud->shapes.count; + + if (n == 0) { + igText("No shapes"); + igEnd(); + return; + } + + igBeginChild_Str("ListScroll", (ImVec2){0, 0}, false, ImGuiWindowFlags_None); + + int *display = (int*) ALLOC((size_t)n * sizeof(int)); + int display_len = 0; + build_display_recursive(&ud->shapes, &ud->groups, 0, display, &display_len); + + if (n != ud->ui.list_prev_count) { ud->ui.list_last_shape = -1; ud->ui.list_prev_count = n; } + if (ud->ui.list_last_shape >= display_len) ud->ui.list_last_shape = -1; + + render_tree_level(ud, 0, display, display_len, 0); + + FREE(display); + igEndChild(); + igEnd(); +} + +static void draw_properties_panel(userdata_t *ud) +{ + igSetNextWindowPos((ImVec2){ud->camera.width - ud->ui.right_panel_w, TOP_PANEL_H}, ImGuiCond_Always, (ImVec2){0, 0}); + igSetNextWindowSize((ImVec2){ud->ui.right_panel_w, ud->camera.height - TOP_PANEL_H}, ImGuiCond_Always); + igBegin("Properties", NULL, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + if (ud->interact.selected_count == 0) { + igText("No shape selected"); + } else if (ud->interact.selected_count > 1) { + int common_gid = -1; + bool same_group = true; + for (int i = 0; i < ud->shapes.count; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (!s->selected) continue; + if (common_gid == -1) common_gid = s->group_id; + else if (s->group_id != common_gid) { same_group = false; break; } + } + if (same_group && common_gid != 0) { + igText("Group %d — %d shapes", common_gid, ud->interact.selected_count); + } else { + igText("%d shapes selected", ud->interact.selected_count); + } + } else { + int idx = 0; + while (idx < ud->shapes.count) { + shape_t *tmp = (shape_t*) vec_get(&ud->shapes, idx); + if (tmp->selected) break; + idx++; + } + shape_t *s = (shape_t*) vec_get(&ud->shapes, idx); + if (s->group_id != 0) { + char path[256] = ""; + int gid = s->group_id; + while (gid != 0) { + char seg[32]; + snprintf(seg, sizeof(seg), "%d", gid); + if (path[0]) { + int plen = (int)strlen(path); + int slen = (int)strlen(seg); + memmove(path + slen + 3, path, (size_t)(plen + 1)); + memcpy(path, seg, (size_t)slen); + path[slen] = ' '; path[slen + 1] = '>'; path[slen + 2] = ' '; + } else { + strcpy(path, seg); + } + group_t *g = find_group(&ud->groups, gid); + gid = g ? g->parent_id : 0; + } + int members = 0; + for (int i = 0; i < ud->shapes.count; i++) { + if (((shape_t*) vec_get(&ud->shapes, i))->group_id == s->group_id) members++; + } + igText("Group %s — %d member%s", path, members, members > 1 ? "s" : ""); + } + bool changed = false; + + changed |= igDragFloat2("Position", &s->cx, 1.0f, 0, 0, "%.1f", 0); + if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_POSITION); + changed |= igDragFloat2("Scale", &s->sx, 1.0f, 0.1f, 0, "%.1f", 0); + if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_SCALE); + changed |= igDragFloat("Rotation", &s->rotation, 0.01f, 0, 0, "%.3f", 0); + if (igIsItemActivated()) history_begin_edit(&ud->history, &ud->shapes, idx, HIST_ROTATION); + + if (changed) { shape_regenerate(s); spatial_mark_dirty(&ud->spatial_grid); ud->overlay_upload_needed = true; } + + igSeparator(); + { + mat4 *m = &s->uniform.transform; + char dbg[512]; + snprintf(dbg, sizeof(dbg), + "Transform Matrix:\n" + "[%+.3f %+.3f %+.3f %+.3f]\n" + "[%+.3f %+.3f %+.3f %+.3f]\n" + "[%+.3f %+.3f %+.3f %+.3f]\n" + "[%+.3f %+.3f %+.3f %+.3f]\n" + "\nLocal vert[0]: (%.1f, %.1f)\n" + "World vert[0]: (%.1f, %.1f)\n" + "cx=%.1f cy=%.1f sx=%.1f sy=%.1f rot=%.3f", + (*m)[0][0], (*m)[0][1], (*m)[0][2], (*m)[0][3], + (*m)[1][0], (*m)[1][1], (*m)[1][2], (*m)[1][3], + (*m)[2][0], (*m)[2][1], (*m)[2][2], (*m)[2][3], + (*m)[3][0], (*m)[3][1], (*m)[3][2], (*m)[3][3], + s->verts[0].x, s->verts[0].y, + s->cx + s->verts[0].x * s->sx * cosf(s->rotation) - s->verts[0].y * s->sy * sinf(s->rotation), + s->cy + s->verts[0].x * s->sx * sinf(s->rotation) + s->verts[0].y * s->sy * cosf(s->rotation), + s->cx, s->cy, s->sx, s->sy, s->rotation); + if (igButton("Copy Debug", (ImVec2){0, 0})) + sapp_set_clipboard_string(dbg); + } + } + + igEnd(); + + if (ud->history.capturing && !igIsAnyItemActive()) { + history_end_edit(&ud->history, &ud->shapes); + } +} + +static void draw_log_panel(userdata_t *ud) +{ + if (!ud->ui.log_show) return; + + igSetNextWindowPos((ImVec2){10.0f, ud->camera.height - 200.0f}, ImGuiCond_FirstUseEver, (ImVec2){0, 0}); + igSetNextWindowSize((ImVec2){400.0f, 180.0f}, ImGuiCond_FirstUseEver); + igBegin("Log", &ud->ui.log_show, 0); + if (igButton("Clear", (ImVec2){0, 0})) { + ud->ui.log_head = 0; + ud->ui.log_count = 0; + } + igSameLine(0.0f, 10.0f); + if (igButton("Copy", (ImVec2){0, 0})) { + int total = ud->ui.log_count < LOG_RING_SIZE ? ud->ui.log_count : LOG_RING_SIZE; + int start = ud->ui.log_count < LOG_RING_SIZE ? 0 : ud->ui.log_head; + int cap = total * 260; + char *buf = (char*) ALLOC((size_t)cap); + int off = 0; + for (int i = 0; i < total; i++) { + int idx = (start + i) % LOG_RING_SIZE; + off += snprintf(buf + off, (size_t)(cap - off), "%s\n", ud->ui.log_ring[idx].text); + } + igSetClipboardText(buf); + FREE(buf); + } + igSameLine(0.0f, 10.0f); + igText("%d entries", ud->ui.log_count); + igSameLine(0.0f, 10.0f); + igText("FPS: %.0f (avg: %.0f)", ud->debug.fps_immediate, ud->debug.fps_average); + igSameLine(0.0f, 10.0f); + igText("%.3fms", sapp_frame_duration() * 1000); + igSeparator(); + + igBeginChild_Str("LogScroll", (ImVec2){0, 0}, false, 0); + int total = ud->ui.log_count < LOG_RING_SIZE ? ud->ui.log_count : LOG_RING_SIZE; + int start = ud->ui.log_count < LOG_RING_SIZE ? 0 : ud->ui.log_head; + for (int i = 0; i < total; i++) { + int idx = (start + i) % LOG_RING_SIZE; + log_entry_t *e = &ud->ui.log_ring[idx]; + ImVec4 color; + switch (e->level) { + case 0: color = (ImVec4){1.0f, 0.3f, 0.3f, 1.0f}; break; + case 1: color = (ImVec4){1.0f, 0.5f, 0.3f, 1.0f}; break; + case 2: color = (ImVec4){1.0f, 0.9f, 0.3f, 1.0f}; break; + default:color = (ImVec4){0.7f, 0.7f, 0.7f, 1.0f}; break; + } + igPushStyleColor_Vec4(ImGuiCol_Text, color); + igTextUnformatted(e->text, NULL); + igPopStyleColor(1); + } + if (total > 0) igSetScrollHereY(1.0f); + igEndChild(); + igEnd(); +} + +#endif diff --git a/src/util.h b/src/util.h index 8ef4324..15d6261 100644 --- a/src/util.h +++ b/src/util.h @@ -33,6 +33,7 @@ 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); + assert(new_data != NULL); if (v->data) { memcpy(new_data, v->data, v->count * v->stride); FREE(v->data); @@ -61,6 +62,28 @@ static void vec_pop(vector_t *v) { if (v->count > 0) v->count--; } +static void vec_remove_ordered(vector_t *v, int index) { + if (index < 0 || index >= v->count) return; + if (index < v->count - 1) { + memmove(v->data + index * v->stride, + v->data + (index + 1) * v->stride, + (v->count - index - 1) * v->stride); + } + v->count--; +} + +static void *vec_insert(vector_t *v, int index) { + if (index < 0 || index > v->count) return NULL; + if (v->count >= v->capacity) vec_grow(v, v->count + 1); + if (index < v->count) { + memmove(v->data + (index + 1) * v->stride, + v->data + index * v->stride, + (v->count - index) * v->stride); + } + v->count++; + return v->data + index * v->stride; +} + /** * Remove the element at index by swapping in the last element (O(1)). * Order is not preserved.