You've already forked flecs_tests
Add a geometry pool, shape hierarchy, addittion and deletion.
This commit is contained in:
@@ -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
72
src/draw.h
Normal 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
|
||||||
@@ -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;
|
||||||
|
|||||||
256
src/history.h
256
src/history.h
@@ -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
894
src/input.h
Normal 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
144
src/interact.h
Normal 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
|
||||||
1166
src/main.c
1166
src/main.c
File diff suppressed because it is too large
Load Diff
144
src/overlay.h
Normal file
144
src/overlay.h
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
184
src/shape.h
184
src/shape.h
@@ -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
133
src/types.h
Normal 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
425
src/ui_panels.h
Normal 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
|
||||||
23
src/util.h
23
src/util.h
@@ -33,6 +33,7 @@ static void vec_grow(vector_t *v, int min_capacity) {
|
|||||||
int new_cap = v->capacity ? v->capacity * 2 : 8;
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user