Add a geometry pool, shape hierarchy, addittion and deletion.

This commit is contained in:
2026-04-30 17:55:46 +02:00
parent 9ce6e4accd
commit e71641c094
14 changed files with 2273 additions and 1341 deletions

View File

@@ -33,6 +33,12 @@
#include "render.h" #include "render.h"
#include "spatial.h" #include "spatial.h"
#include "history.h" #include "history.h"
#include "types.h"
#include "interact.h"
#include "overlay.h"
#include "draw.h"
#include "ui_panels.h"
#include "input.h"
#include <emscripten.h> #include <emscripten.h>
#include <stdio.h> #include <stdio.h>

72
src/draw.h Normal file
View File

@@ -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

View File

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

View File

@@ -3,143 +3,108 @@
#include "api.h" #include "api.h"
// Each property kind we can undo/redo independently
typedef enum { typedef enum {
HIST_POSITION, HIST_POSITION,
HIST_SCALE, HIST_SCALE,
HIST_ROTATION, HIST_ROTATION,
HIST_COLOR, HIST_CREATE,
HIST_DELETE,
HIST_GROUP,
} hist_prop_t; } hist_prop_t;
// One property change on one shape (old → new)
typedef struct hist_change_t { typedef struct hist_change_t {
int shape_index; int shape_index;
hist_prop_t prop; hist_prop_t prop;
float old_val[4]; float old_val[4];
float new_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; } 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 { typedef struct hist_entry_t {
hist_change_t *changes; hist_change_t *changes;
int count; int count;
} hist_entry_t; } hist_entry_t;
#define HIST_MAX 64
typedef struct history_t { typedef struct history_t {
hist_entry_t entries[HIST_MAX]; vector_t entries;
int count; int current;
int current; // index of last applied entry, -1 = initial state
// Pending edit session (one ImGui widget interaction)
bool capturing; bool capturing;
int pending_shape_idx; int pending_shape_idx;
hist_prop_t pending_prop; hist_prop_t pending_prop;
float pending_old[4]; float pending_old[4];
} history_t; } 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]) { static void hist_read_prop(shape_t *s, hist_prop_t prop, float out[4]) {
memset(out, 0, sizeof(float[4])); memset(out, 0, sizeof(float[4]));
switch (prop) { switch (prop) {
case HIST_POSITION: out[0] = s->cx; out[1] = s->cy; break; 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_SCALE: out[0] = s->sx; out[1] = s->sy; break;
case HIST_ROTATION: out[0] = s->rotation; 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]) { static void hist_apply_prop(shape_t *s, hist_prop_t prop, const float val[4]) {
switch (prop) { switch (prop) {
case HIST_POSITION: s->cx = val[0]; s->cy = val[1]; break; 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_SCALE: s->sx = val[0]; s->sy = val[1]; break;
case HIST_ROTATION: s->rotation = val[0]; 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) { static void history_init(history_t *h) {
memset(h, 0, sizeof(*h)); memset(h, 0, sizeof(*h));
vec_init(&h->entries, sizeof(hist_entry_t));
h->current = -1; 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) { static void history_destroy(history_t *h) {
for (int i = 0; i < h->count; i++) { for (int i = 0; i < h->entries.count; i++) {
if (h->entries[i].changes) FREE(h->entries[i].changes); 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)); memset(h, 0, sizeof(*h));
h->current = -1; 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) { static void history_push_entry(history_t *h, hist_entry_t entry) {
while (h->count > h->current + 1) { while (h->entries.count > h->current + 1) {
h->count--; hist_entry_t *e = (hist_entry_t*) vec_get(&h->entries, h->entries.count - 1);
if (h->entries[h->count].changes) { if (e->changes) {
FREE(h->entries[h->count].changes); for (int j = 0; j < e->count; j++)
h->entries[h->count].changes = NULL; hist_free_change(&e->changes[j]);
FREE(e->changes);
} }
vec_pop(&h->entries);
} }
if (h->count >= HIST_MAX) { *((hist_entry_t*) vec_push(&h->entries)) = entry;
if (h->entries[0].changes) FREE(h->entries[0].changes); h->current = h->entries.count - 1;
memmove(&h->entries[0], &h->entries[1],
(h->count - 1) * sizeof(hist_entry_t));
h->count--;
h->current--;
}
h->entries[h->count] = entry;
h->count++;
h->current = h->count - 1;
} }
/**
* Begin capturing an edit session. Snapshots the current value of one property.
* If a prior session is still open (e.g. user switched widgets in the same frame),
* it is finalized and pushed first.
* Call when igIsItemActivated() is true after an ImGui widget.
*
* @param h history stack
* @param shapes the shapes vector (used to read current values)
* @param shape_idx index of the shape being edited
* @param prop which property is about to change
*/
static void history_begin_edit(history_t *h, vector_t *shapes, static void history_begin_edit(history_t *h, vector_t *shapes,
int shape_idx, hist_prop_t prop) { int shape_idx, hist_prop_t prop) {
if (h->capturing) { if (h->capturing) {
@@ -147,15 +112,13 @@ static void history_begin_edit(history_t *h, vector_t *shapes,
float new_val[4]; float new_val[4];
hist_read_prop(s, h->pending_prop, new_val); hist_read_prop(s, h->pending_prop, new_val);
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) { 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 }; hist_entry_t entry = { .changes = NULL, .count = 1 };
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t)); 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); history_push_entry(h, entry);
} }
h->capturing = false; 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); 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) { static void history_end_edit(history_t *h, vector_t *shapes) {
if (!h->capturing) return; 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); hist_read_prop(s, h->pending_prop, new_val);
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) { 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 }; hist_entry_t entry = { .changes = NULL, .count = 1 };
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t)); 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); history_push_entry(h, entry);
} }
h->capturing = false; h->capturing = false;
} }
/** // -- batch API --
* 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) --
typedef struct { typedef struct {
hist_change_t *changes; hist_change_t *changes;
@@ -215,10 +161,12 @@ typedef struct {
static void history_batch_init(hist_batch_t *batch, int count) { static void history_batch_init(hist_batch_t *batch, int count) {
batch->changes = (hist_change_t*) ALLOC((size_t)count * sizeof(hist_change_t)); 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->count = 0;
batch->capacity = count; 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, 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]) { const float old_val[4], const float new_val[4]) {
hist_change_t *c = &batch->changes[batch->count++]; 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])); 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) { static void history_batch_commit(hist_batch_t *batch, history_t *h) {
hist_entry_t entry = { .changes = batch->changes, .count = batch->count }; hist_entry_t entry = { .changes = batch->changes, .count = batch->count };
history_push_entry(h, entry); history_push_entry(h, entry);
} }
/** // Reconstruct a shape_t from a HIST_CREATE / HIST_DELETE change snapshot.
* Apply every change in an entry to the shapes vector and regenerate buffers. static shape_t hist_rebuild_shape_from_snapshot(const hist_change_t *c) {
*/static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forward) { 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++) { 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]; 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; if (c->shape_index >= shapes->count) continue;
shape_t *s = (shape_t*) vec_get(shapes, c->shape_index); shape_t *s = (shape_t*) vec_get(shapes, c->shape_index);
hist_apply_prop(s, c->prop, forward ? c->new_val : c->old_val); 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) { static bool history_undo(history_t *h, vector_t *shapes) {
if (h->current < 0) return false; if (h->current < 0) return false;
history_apply_entry((hist_entry_t*) vec_get(&h->entries, h->current), shapes, false);
history_apply_entry(&h->entries[h->current], shapes, false);
h->current--; h->current--;
return true; return true;
} }
static bool history_redo(history_t *h, vector_t *shapes) { 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++; 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; return true;
} }

894
src/input.h Normal file
View File

@@ -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

144
src/interact.h Normal file
View File

@@ -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

1142
src/main.c

File diff suppressed because it is too large Load Diff

144
src/overlay.h Normal file
View File

@@ -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

View File

@@ -62,11 +62,7 @@ static void shape_draw(shape_t *s, const mat4 *mvp)
{ {
sg_apply_uniforms(0, &SG_RANGE(*mvp)); sg_apply_uniforms(0, &SG_RANGE(*mvp));
sg_apply_uniforms(1, &SG_RANGE(s->uniform)); sg_apply_uniforms(1, &SG_RANGE(s->uniform));
sg_apply_bindings(&(sg_bindings) { sg_draw((int)s->index_base, (int)s->num_elements, 1);
.vertex_buffers[0] = s->vbuf,
.index_buffer = s->ibuf,
});
sg_draw(0, s->num_indices, 1);
} }
#endif #endif

View File

@@ -4,7 +4,6 @@ struct VsUniform {
struct ShapeUniform { struct ShapeUniform {
transform: mat4x4f, transform: mat4x4f,
base_color: vec4f,
state: u32, state: u32,
}; };
@@ -31,9 +30,9 @@ struct FsOut {
if (shape_uniform.state == 2u) { if (shape_uniform.state == 2u) {
output.color = vec4f(1.0, 0.84, 0.0, 1.0); output.color = vec4f(1.0, 0.84, 0.0, 1.0);
} else if (shape_uniform.state == 1u) { } 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 { } else {
output.color = shape_uniform.base_color; output.color = vec4f(0.8, 0.8, 0.8, 1.0);
} }
return output; return output;
} }

View File

@@ -7,9 +7,15 @@ typedef struct shape_vertex_t {
float x, y; float x, y;
} shape_vertex_t; } shape_vertex_t;
typedef enum {
SHAPE_CIRCLE,
SHAPE_RECTANGLE,
SHAPE_STAR,
SHAPE_GENERIC,
} shape_kind_t;
typedef struct shape_uniform_t { typedef struct shape_uniform_t {
mat4 transform; mat4 transform;
float base_color[4];
uint32_t state; uint32_t state;
uint8_t _pad[12]; uint8_t _pad[12];
} shape_uniform_t; } shape_uniform_t;
@@ -17,10 +23,8 @@ typedef struct shape_uniform_t {
typedef struct shape_t { typedef struct shape_t {
shape_vertex_t *verts; shape_vertex_t *verts;
uint16_t *indices; uint16_t *indices;
uint32_t num_indices; uint32_t num_elements;
uint32_t num_verts; uint32_t num_verts;
sg_buffer vbuf;
sg_buffer ibuf;
shape_uniform_t uniform; shape_uniform_t uniform;
bool hovered; bool hovered;
bool selected; bool selected;
@@ -28,8 +32,119 @@ typedef struct shape_t {
float cx, cy; float cx, cy;
float sx, sy; float sx, sy;
float rotation; float rotation;
int kind;
uint32_t vertex_base;
uint32_t index_base;
int group_id;
} shape_t; } 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 #define SHAPE_HOVER_PX 6.0f
static int shape_calc_segments(float r) static int shape_calc_segments(float r)
@@ -40,13 +155,13 @@ static int shape_calc_segments(float r)
return n; 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->hovered = false;
s->selected = false; s->selected = false;
memcpy(s->uniform.base_color, color, sizeof(float[4]));
s->uniform.state = 0; s->uniform.state = 0;
memset(s->uniform._pad, 0, sizeof(s->uniform._pad)); memset(s->uniform._pad, 0, sizeof(s->uniform._pad));
s->group_id = 0;
} }
static void shape_build_transform(shape_t *s) 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) static void shape_make_buffers(shape_t *s)
{ {
uint32_t vcount = s->num_verts + 1; (void)s;
s->vbuf = sg_make_buffer(&(sg_buffer_desc) { g_shape_pool_dirty = true;
.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",
});
} }
static void shape_shutdown(shape_t *s) static void shape_shutdown(shape_t *s)
{ {
sg_destroy_buffer(s->vbuf); g_shape_pool_dirty = true;
sg_destroy_buffer(s->ibuf);
FREE(s->verts); FREE(s->verts);
FREE(s->indices); FREE(s->indices);
} }
@@ -135,12 +239,13 @@ static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol)
return false; 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; shape_t s;
s.cx = x; s.cy = y; s.cx = x; s.cy = y;
s.sx = r; s.sy = r; s.sx = r; s.sy = r;
s.rotation = 0.0f; s.rotation = 0.0f;
s.kind = SHAPE_CIRCLE;
int segs = shape_calc_segments(r); int segs = shape_calc_segments(r);
int count = segs + 1; 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]; s.verts[segs] = s.verts[0];
for (int i = 0; i <= segs; i++) s.indices[i] = (uint16_t)i; 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; s.num_verts = (uint32_t)segs;
shape_init_common(&s, color); shape_init_common(&s);
shape_build_transform(&s); shape_build_transform(&s);
shape_make_buffers(&s); shape_make_buffers(&s);
return s; return s;
} }
static shape_t shape_star(float x, float y, float outer_r, float inner_r, 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; shape_t s;
s.cx = x; s.cy = y; s.cx = x; s.cy = y;
s.sx = outer_r; s.sy = outer_r; s.sx = outer_r; s.sy = outer_r;
s.rotation = 0.0f; s.rotation = 0.0f;
s.kind = SHAPE_STAR;
int n = points * 2; int n = points * 2;
int count = n + 1; 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]; s.verts[n] = s.verts[0];
for (int i = 0; i <= n; i++) s.indices[i] = (uint16_t)i; 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; 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_build_transform(&s);
shape_make_buffers(&s); shape_make_buffers(&s);
return s; return s;

133
src/types.h Normal file
View File

@@ -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

425
src/ui_panels.h Normal file
View File

@@ -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

View File

@@ -33,6 +33,7 @@ static void vec_grow(vector_t *v, int min_capacity) {
int new_cap = v->capacity ? v->capacity * 2 : 8; int new_cap = v->capacity ? v->capacity * 2 : 8;
if (new_cap < min_capacity) new_cap = min_capacity; if (new_cap < min_capacity) new_cap = min_capacity;
uint8_t *new_data = (uint8_t*) ALLOC(new_cap * v->stride); uint8_t *new_data = (uint8_t*) ALLOC(new_cap * v->stride);
assert(new_data != NULL);
if (v->data) { if (v->data) {
memcpy(new_data, v->data, v->count * v->stride); memcpy(new_data, v->data, v->count * v->stride);
FREE(v->data); FREE(v->data);
@@ -61,6 +62,28 @@ static void vec_pop(vector_t *v) {
if (v->count > 0) v->count--; 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)). * Remove the element at index by swapping in the last element (O(1)).
* Order is not preserved. * Order is not preserved.