diff --git a/diff_output.patch b/diff_output.patch new file mode 100644 index 0000000..285fe07 --- /dev/null +++ b/diff_output.patch @@ -0,0 +1,1831 @@ +diff --git a/src/api.h b/src/api.h +index dd80adf..a4707c7 100644 +--- a/src/api.h ++++ b/src/api.h +@@ -27,6 +27,21 @@ + + #include "generated/sprite.h" + #include "generated/shape.h" ++#include "generated/overlay.h" ++ ++// Log-to-panel infrastructure ++static void (*g_panel_log_fn)(void*, int, const char*) = NULL; ++static void *g_panel_log_ud = NULL; ++ ++static void panel_log(int level, const char *fmt, ...) { ++ if (!g_panel_log_fn) return; ++ char buf[256]; ++ va_list ap; ++ va_start(ap, fmt); ++ vsnprintf(buf, sizeof(buf), fmt, ap); ++ va_end(ap); ++ g_panel_log_fn(g_panel_log_ud, level, buf); ++} + + #include "util.h" + #include "shape.h" +diff --git a/src/draw.h b/src/draw.h +index 17ad292..e22234b 100644 +--- a/src/draw.h ++++ b/src/draw.h +@@ -9,20 +9,109 @@ static void draw_shapes(userdata_t *ud) + if (g_shape_pool_dirty) + shape_pool_rebuild(&ud->shapes); + +- if (ud->shapes.count == 0) return; ++ int n = ud->shapes.count; ++ if (n == 0) return; ++ ++ if (g_shape_data_dirty) { ++ shape_upload_data(&ud->shapes); ++ g_shape_data_dirty = false; ++ } ++ panel_log(3, "[shapes] draw_shapes: n=%d pipeline=%d", n, shape_pipeline.id); ++ ++ static uint32_t *imap = NULL; ++ static int imap_cap = 0; ++ static uint32_t *ne_counts = NULL; ++ static uint32_t *ne_starts = NULL; ++ static int ne_cap = 0; ++ ++ if (n > imap_cap) { ++ if (imap) FREE(imap); ++ imap = (uint32_t*) ALLOC((size_t)n * sizeof(uint32_t)); ++ imap_cap = n; ++ } ++ ++ // Group shapes by num_elements using counting sort ++ uint32_t max_ne = 0; ++ for (int i = 0; i < n; i++) { ++ uint32_t ne = ((shape_t*) vec_get(&ud->shapes, i))->num_elements; ++ if (ne > max_ne) max_ne = ne; ++ } ++ ++ int ne_size = (int)(max_ne + 1); ++ if (ne_size > ne_cap) { ++ if (ne_counts) FREE(ne_counts); ++ if (ne_starts) FREE(ne_starts); ++ ne_counts = (uint32_t*) ALLOC((size_t)ne_size * sizeof(uint32_t)); ++ ne_starts = (uint32_t*) ALLOC((size_t)ne_size * sizeof(uint32_t)); ++ ne_cap = ne_size; ++ } ++ memset(ne_counts, 0, (size_t)ne_size * sizeof(uint32_t)); ++ ++ for (int i = 0; i < n; i++) { ++ uint32_t ne = ((shape_t*) vec_get(&ud->shapes, i))->num_elements; ++ ne_counts[ne]++; ++ } ++ ++ uint32_t pos = 0; ++ for (uint32_t ne = 0; ne <= max_ne; ne++) { ++ ne_starts[ne] = pos; ++ pos += ne_counts[ne]; ++ ne_counts[ne] = 0; ++ } ++ ++ for (int i = 0; i < n; i++) { ++ uint32_t ne = ((shape_t*) vec_get(&ud->shapes, i))->num_elements; ++ imap[ne_starts[ne] + ne_counts[ne]++] = (uint32_t)i; ++ } ++ ++ shape_upload_instance_map(imap, n); + + 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); ++ ++ int base = 0; ++ while (base < n) { ++ shape_t *s = (shape_t*) vec_get(&ud->shapes, imap[base]); ++ uint32_t ne = s->num_elements; ++ int count = 1; ++ while (base + count < n && ++ ((shape_t*) vec_get(&ud->shapes, imap[base + count]))->num_elements == ne) ++ count++; ++ ++ // Find the vertex buffer for this num_elements ++ sg_buffer group_vbuf = {0}; ++ for (int gi = 0; gi < g_shape_group_count; gi++) { ++ if (g_shape_groups[gi].num_elements == ne) { ++ group_vbuf = g_shape_groups[gi].vbuf; ++ break; ++ } ++ } ++ ++ struct { mat4 mvp; uint32_t base; uint8_t _pad[12]; } vs_u; ++ memcpy(vs_u.mvp, ud->renderer.uniform.mvp, sizeof(mat4)); ++ vs_u.base = (uint32_t)base; ++ memset(vs_u._pad, 0, 12); ++ sg_apply_uniforms(0, &SG_RANGE(vs_u)); ++ ++ sg_apply_bindings(&(sg_bindings){ ++ .vertex_buffers[0] = group_vbuf, ++ .views[0] = g_shape_data_view, ++ .views[1] = g_instance_map_view, ++ }); ++ panel_log(3, "[shapes] draw group: ne=%u count=%d base=%d vbuf=%d data_view=%d imap_view=%d", ++ ne, count, base, group_vbuf.id, g_shape_data_view.id, g_instance_map_view.id); ++ sg_draw(0, (int)ne, count); ++ ++ base += count; + } ++ + } + + static void draw_overlay_and_handles(userdata_t *ud, bool has_overlay, bool show_handle) + { ++ sg_apply_pipeline(overlay_pipeline); ++ panel_log(3, "[shapes] draw_overlay: pipeline=%d has_ov=%d show_h=%d", ++ overlay_pipeline.id, has_overlay, show_handle); ++ + if (has_overlay) { + shape_uniform_t u; + glm_mat4_identity(u.transform); +diff --git a/src/generated/shape.h b/src/generated/shape.h +index c5e95d0..a35c0b1 100644 +--- a/src/generated/shape.h ++++ b/src/generated/shape.h +@@ -2,90 +2,109 @@ unsigned char src_shaders_shape_wgsl[] = { + 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69, + 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, + 0x76, 0x70, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, 0x66, 0x2c, ++ 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, ++ 0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c, + 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, +- 0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, +- 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, +- 0x66, 0x6f, 0x72, 0x6d, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, +- 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, +- 0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, +- 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, 0x49, 0x6e, 0x20, 0x7b, +- 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, +- 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, +- 0x6f, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66, 0x2c, 0x0a, 0x7d, ++ 0x53, 0x68, 0x61, 0x70, 0x65, 0x44, 0x61, 0x74, 0x61, 0x20, 0x7b, 0x0a, ++ 0x20, 0x20, 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, ++ 0x6d, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, 0x66, 0x2c, 0x0a, ++ 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x3a, 0x20, 0x75, ++ 0x33, 0x32, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e, ++ 0x64, 0x69, 0x6e, 0x67, 0x28, 0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, ++ 0x75, 0x70, 0x28, 0x30, 0x29, 0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, ++ 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3e, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, ++ 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x3a, 0x20, 0x56, 0x73, 0x55, 0x6e, ++ 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e, ++ 0x64, 0x69, 0x6e, 0x67, 0x28, 0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, ++ 0x75, 0x70, 0x28, 0x31, 0x29, 0x20, 0x76, 0x61, 0x72, 0x3c, 0x73, 0x74, ++ 0x6f, 0x72, 0x61, 0x67, 0x65, 0x3e, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, ++ 0x5f, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79, ++ 0x3c, 0x53, 0x68, 0x61, 0x70, 0x65, 0x44, 0x61, 0x74, 0x61, 0x3e, 0x3b, ++ 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31, 0x29, ++ 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x31, 0x29, 0x20, 0x76, ++ 0x61, 0x72, 0x3c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x3e, 0x20, ++ 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, ++ 0x3a, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x75, 0x33, 0x32, 0x3e, + 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, +- 0x32, 0x46, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x62, +- 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70, 0x6f, 0x73, 0x69, 0x74, +- 0x69, 0x6f, 0x6e, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x3a, 0x20, 0x76, 0x65, +- 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, +- 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x40, 0x69, +- 0x6e, 0x74, 0x65, 0x72, 0x70, 0x6f, 0x6c, 0x61, 0x74, 0x65, 0x28, 0x6c, +- 0x69, 0x6e, 0x65, 0x61, 0x72, 0x29, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, +- 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, +- 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x46, 0x73, 0x4f, 0x75, +- 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, 0x63, +- 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x63, 0x6f, 0x6c, +- 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x7d, +- 0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, +- 0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29, +- 0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, +- 0x3e, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, +- 0x73, 0x3a, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, +- 0x3b, 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31, +- 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29, 0x20, +- 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3e, +- 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, +- 0x72, 0x6d, 0x3a, 0x20, 0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69, +- 0x66, 0x6f, 0x72, 0x6d, 0x3b, 0x0a, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74, ++ 0x49, 0x6e, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x62, 0x75, ++ 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, ++ 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x29, 0x20, 0x69, 0x6e, ++ 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x3a, 0x20, ++ 0x75, 0x33, 0x32, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, ++ 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x70, 0x6f, ++ 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, ++ 0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, ++ 0x74, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, ++ 0x20, 0x20, 0x40, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70, ++ 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29, 0x20, 0x70, 0x6f, 0x73, ++ 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, ++ 0x20, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, ++ 0x29, 0x20, 0x40, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x6f, 0x6c, 0x61, ++ 0x74, 0x65, 0x28, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x29, 0x20, 0x63, ++ 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, ++ 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, ++ 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, ++ 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, ++ 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, ++ 0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74, + 0x65, 0x78, 0x20, 0x66, 0x6e, 0x20, 0x76, 0x73, 0x5f, 0x6d, 0x61, 0x69, + 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x49, + 0x6e, 0x29, 0x20, 0x2d, 0x3e, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20, + 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b, +- 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, 0x72, +- 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, +- 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x74, +- 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x2a, 0x20, 0x76, +- 0x65, 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, +- 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69, +- 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, +- 0x6e, 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, +- 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, +- 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x73, 0x5f, +- 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, +- 0x20, 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, +- 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, +- 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, +- 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, ++ 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x73, 0x68, 0x61, ++ 0x70, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x73, ++ 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x5b, 0x76, 0x73, ++ 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x69, 0x6e, ++ 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x20, ++ 0x2b, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x69, 0x6e, 0x73, 0x74, ++ 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x5d, 0x3b, 0x0a, 0x20, ++ 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, ++ 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x64, 0x61, 0x74, ++ 0x61, 0x5b, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x5d, ++ 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, ++ 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, ++ 0x61, 0x70, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, ++ 0x6d, 0x20, 0x2a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, ++ 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, ++ 0x2e, 0x78, 0x2c, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, ++ 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, ++ 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, ++ 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, ++ 0x3d, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, ++ 0x73, 0x2e, 0x6d, 0x76, 0x70, 0x20, 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, ++ 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, ++ 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, 0x70, 0x65, 0x2e, 0x73, 0x74, 0x61, ++ 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, 0x20, 0x7b, 0x0a, ++ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, ++ 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x3d, 0x20, 0x76, ++ 0x65, 0x63, 0x34, 0x66, 0x28, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x30, 0x2e, ++ 0x38, 0x34, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30, ++ 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, 0x6c, 0x73, ++ 0x65, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, 0x70, 0x65, 0x2e, ++ 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x75, 0x29, + 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, +- 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x31, 0x2e, 0x30, 0x2c, +- 0x20, 0x30, 0x2e, 0x38, 0x34, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, +- 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, +- 0x65, 0x6c, 0x73, 0x65, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, +- 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x73, +- 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x75, 0x29, 0x20, +- 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, +- 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x3d, +- 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, 0x35, 0x2c, 0x20, +- 0x30, 0x2e, 0x36, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, +- 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, 0x6c, +- 0x73, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, ++ 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, 0x35, 0x2c, ++ 0x20, 0x30, 0x2e, 0x36, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x31, ++ 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, ++ 0x6c, 0x73, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, ++ 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, ++ 0x6f, 0x72, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, ++ 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, ++ 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, ++ 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, ++ 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, ++ 0x40, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, ++ 0x20, 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, ++ 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, ++ 0x3e, 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, ++ 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, ++ 0x3a, 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, +- 0x72, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, +- 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, +- 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, +- 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, +- 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x40, +- 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20, +- 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, +- 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, 0x3e, +- 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, +- 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3a, +- 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, +- 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, +- 0x20, 0x3d, 0x20, 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 ++ 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 = 1045; ++unsigned int src_shaders_shape_wgsl_len = 1274; +diff --git a/src/history.h b/src/history.h +index cfcf0aa..75471d2 100644 +--- a/src/history.h ++++ b/src/history.h +@@ -3,6 +3,8 @@ + + #include "api.h" + ++#define HISTORY_MAX_DEPTH 256 ++ + typedef enum { + HIST_POSITION, + HIST_SCALE, +@@ -103,6 +105,17 @@ static void history_push_entry(history_t *h, hist_entry_t entry) { + + *((hist_entry_t*) vec_push(&h->entries)) = entry; + h->current = h->entries.count - 1; ++ ++ while (h->entries.count > HISTORY_MAX_DEPTH) { ++ hist_entry_t *e = (hist_entry_t*) vec_get(&h->entries, 0); ++ if (e->changes) { ++ for (int j = 0; j < e->count; j++) ++ hist_free_change(&e->changes[j]); ++ FREE(e->changes); ++ } ++ vec_remove_ordered(&h->entries, 0); ++ h->current--; ++ } + } + + static void history_begin_edit(history_t *h, vector_t *shapes, +@@ -180,14 +193,9 @@ static void history_batch_add(hist_batch_t *batch, int shape_index, hist_prop_t + // Used for HIST_CREATE and HIST_DELETE entries. + // old_val = { kind, cx, cy, num_verts } + // new_val = { sx, sy, rotation, group_id } ++// For procedural shapes (CIRCLE, STAR, RECTANGLE), vertices are reconstructed ++// from parameters instead of deep-copied. For STAR, new_val[1] stores inner_r. + 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; +@@ -196,6 +204,28 @@ static void hist_snapshot_shape_verts(hist_change_t *c, shape_t *s) { + c->new_val[1] = s->sy; + c->new_val[2] = s->rotation; + c->new_val[3] = (float)s->group_id; ++ ++ if (s->kind == SHAPE_GENERIC) { ++ 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)); ++ } else if (s->kind == SHAPE_STAR) { ++ float inner_ratio = sqrtf(s->verts[1].x * s->verts[1].x + s->verts[1].y * s->verts[1].y); ++ c->new_val[1] = inner_ratio * s->sx; ++ c->vertex_count = 0; ++ c->index_count = 0; ++ c->vertex_data = NULL; ++ c->index_data = NULL; ++ } else { ++ c->vertex_count = 0; ++ c->index_count = 0; ++ c->vertex_data = NULL; ++ c->index_data = NULL; ++ } + } + + // Append a CREATE or DELETE entry to a batch, snapshotting the shape's vertex data. +@@ -214,27 +244,46 @@ static void history_batch_commit(hist_batch_t *batch, history_t *h) { + + // Reconstruct a shape_t from a HIST_CREATE / HIST_DELETE change snapshot. + static shape_t hist_rebuild_shape_from_snapshot(const hist_change_t *c) { ++ float cx = c->old_val[1], cy = c->old_val[2]; ++ float sx = c->new_val[0], rot = c->new_val[2]; ++ int gid = (int)c->new_val[3]; + 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); ++ ++ switch ((int)c->old_val[0]) { ++ case SHAPE_CIRCLE: ++ s = shape_circle(cx, cy, sx); ++ break; ++ case SHAPE_RECTANGLE: ++ s = shape_rectangle(cx, cy, sx * 2.0f, c->new_val[1] * 2.0f); ++ break; ++ case SHAPE_STAR: { ++ int points = (int)c->old_val[3] / 2; ++ s = shape_star(cx, cy, sx, c->new_val[1], points); ++ break; ++ } ++ default: { ++ memset(&s, 0, sizeof(s)); ++ s.kind = (int)c->old_val[0]; ++ s.cx = cx; ++ s.cy = cy; ++ s.num_verts = (uint32_t)c->old_val[3]; ++ s.num_elements = (uint32_t)c->vertex_count; ++ s.sx = sx; ++ s.sy = c->new_val[1]; ++ 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; ++ } ++ } ++ s.rotation = rot; ++ s.group_id = gid; + shape_build_transform(&s); +- shape_make_buffers(&s); + return s; + } + +diff --git a/src/input.h b/src/input.h +index ef7c48b..47714bc 100644 +--- a/src/input.h ++++ b/src/input.h +@@ -55,7 +55,7 @@ static void handle_left_down_resize_begin(userdata_t *ud, float wx, float wy, in + 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++; } ++ if (s->selected) { sum_sin += s->sin_r; sum_cos += s->cos_r; sel_n++; } + } + ud->interact.resize.angle = atan2f(sum_sin, sum_cos); + +@@ -65,7 +65,7 @@ static void handle_left_down_resize_begin(userdata_t *ud, float wx, float wy, in + 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 sc = s->cos_r, ss = s->sin_r; + 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; +@@ -92,6 +92,12 @@ static void handle_left_down_rotate_begin(userdata_t *ud, float wx, float wy) + wy - ud->interact.rotate.center_y, + wx - ud->interact.rotate.center_x); + ud->interact.rotate.total_delta = 0.0f; ++ ++ ud->interact.drag_indices.count = 0; ++ for (int i = 0; i < ud->shapes.count; i++) { ++ if (((shape_t*) vec_get(&ud->shapes, i))->selected) ++ *(int*) vec_push(&ud->interact.drag_indices) = i; ++ } + } + + static void handle_left_down_move_begin(userdata_t *ud, float wx, float wy) +@@ -101,6 +107,12 @@ static void handle_left_down_move_begin(userdata_t *ud, float wx, float wy) + ud->interact.move.start_wy = wy; + ud->interact.move.total_dx = 0; + ud->interact.move.total_dy = 0; ++ ++ ud->interact.drag_indices.count = 0; ++ for (int i = 0; i < ud->shapes.count; i++) { ++ if (((shape_t*) vec_get(&ud->shapes, i))->selected) ++ *(int*) vec_push(&ud->interact.drag_indices) = i; ++ } + } + + static void handle_left_down_select_or_marquee(userdata_t *ud, const sapp_event *event, float wx, float wy, float tol) +@@ -170,41 +182,37 @@ static void handle_resize_end(userdata_t *ud) + + 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++; +- } +- ++ int n = ud->interact.drag_indices.count; ++ if (n > 0 && ud->interact.rotate.total_delta != 0.0f) { + 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); ++ history_batch_init(&batch, n * 2); + +- for (int i = 0; i < ud->shapes.count; i++) { ++ for (int j = 0; j < n; j++) { ++ int i = *(int*) vec_get(&ud->interact.drag_indices, j); + 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 }); +- } ++ 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; ++ ud->interact.drag_indices.count = 0; + update_shape_states(ud); + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; +@@ -213,28 +221,24 @@ static void handle_rotate_end(userdata_t *ud) + + 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++; +- } +- ++ int n = ud->interact.drag_indices.count; ++ if (n > 0 && (ud->interact.move.total_dx != 0.0f || ud->interact.move.total_dy != 0.0f)) { + hist_batch_t batch; +- history_batch_init(&batch, sel_count); ++ history_batch_init(&batch, n); + +- for (int i = 0; i < ud->shapes.count; i++) { ++ for (int j = 0; j < n; j++) { ++ int i = *(int*) vec_get(&ud->interact.drag_indices, j); + 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_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; ++ ud->interact.drag_indices.count = 0; + update_shape_states(ud); + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; +@@ -306,7 +310,7 @@ static void handle_resize_drag(userdata_t *ud, const sapp_event *event) + 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 sc = s->cos_r, ss = s->sin_r; + float mlx = (wx - ini->init_cx) * sc + (wy - ini->init_cy) * ss; + float mly = -(wx - ini->init_cx) * ss + (wy - ini->init_cy) * sc; + +@@ -351,17 +355,16 @@ static void handle_rotate_drag(userdata_t *ud, const sapp_event *event) + float cx = ud->interact.rotate.center_x; + float cy = ud->interact.rotate.center_y; + +- for (int i = 0; i < ud->shapes.count; i++) { ++ for (int j = 0; j < ud->interact.drag_indices.count; j++) { ++ int i = *(int*) vec_get(&ud->interact.drag_indices, j); + 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); +- } ++ 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; +@@ -376,14 +379,13 @@ static void handle_move_drag(userdata_t *ud, const sapp_event *event) + 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++) { ++ for (int j = 0; j < ud->interact.drag_indices.count; j++) { ++ int i = *(int*) vec_get(&ud->interact.drag_indices, j); + 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); +- } ++ s->cx += delta_x; ++ s->cy += delta_y; ++ shape_retranslate(s); ++ shape_set_state(s, false, true); + } + + ud->interact.move.total_dx = dx; +@@ -411,6 +413,35 @@ static void handle_marquee_drag(userdata_t *ud, const sapp_event *event) + &ud->spatial_grid, &ud->shapes, + min_x, min_y, max_x, max_y); + ++ if (ud->interact.focused_group_id == 0) { ++ 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; ++ int topmost = get_topmost_group(&ud->groups, s->group_id); ++ bool found = false; ++ for (int j = 0; j < n_gids; j++) { ++ if (gids[j] == topmost) { found = true; break; } ++ } ++ if (!found) gids[n_gids++] = topmost; ++ } ++ for (int j = 0; j < n_gids; j++) { ++ 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, gids[j], &ud->groups)) ++ s->selected = true; ++ } ++ } ++ FREE(gids); ++ ud->interact.selected_count = 0; ++ for (int i = 0; i < ud->shapes.count; i++) { ++ if (((shape_t*) vec_get(&ud->shapes, i))->selected) ++ ud->interact.selected_count++; ++ } ++ } ++ + 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); +@@ -435,16 +466,179 @@ static void handle_hover(userdata_t *ud, const sapp_event *event) + if (hovered >= 0) { + shape_t *hs = (shape_t*) vec_get(&ud->shapes, hovered); + hovered_gid = hs->group_id; ++ if (hovered_gid != 0 && ud->interact.focused_group_id == 0) ++ hovered_gid = get_topmost_group(&ud->groups, hovered_gid); + } + + 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); ++ hovered_gid != 0 && ++ is_shape_in_group_hierarchy(s->group_id, hovered_gid, &ud->groups)); + shape_set_state(s, (i == hovered || in_group), s->selected); + } + } + ++// -- clipboard -- ++ ++static shape_t clipboard_deep_copy_shape(const shape_t *src) ++{ ++ shape_t dst = *src; ++ dst.verts = (shape_vertex_t*) ALLOC((size_t)src->num_elements * sizeof(shape_vertex_t)); ++ dst.indices = (uint16_t*) ALLOC((size_t)src->num_elements * sizeof(uint16_t)); ++ memcpy(dst.verts, src->verts, (size_t)src->num_elements * sizeof(shape_vertex_t)); ++ memcpy(dst.indices, src->indices, (size_t)src->num_elements * sizeof(uint16_t)); ++ return dst; ++} ++ ++static void clipboard_clear(clipboard_t *cb) ++{ ++ for (int i = 0; i < cb->shape_count; i++) { ++ FREE(cb->shapes[i].verts); ++ FREE(cb->shapes[i].indices); ++ } ++ FREE(cb->shapes); ++ FREE(cb->groups); ++ memset(cb, 0, sizeof(*cb)); ++} ++ ++static int clipboard_lookup_gid(int old_id, const int *map, int map_count) ++{ ++ for (int j = 0; j < map_count; j++) { ++ if (map[j * 2] == old_id) return map[j * 2 + 1]; ++ } ++ return 0; ++} ++ ++static void handle_copy(userdata_t *ud) ++{ ++ if (ud->interact.selected_count == 0) return; ++ ++ clipboard_clear(&ud->clipboard); ++ ++ int n = ud->shapes.count; ++ int sel = 0; ++ for (int i = 0; i < n; i++) { ++ if (((shape_t*) vec_get(&ud->shapes, i))->selected) sel++; ++ } ++ ++ ud->clipboard.shapes = (shape_t*) ALLOC((size_t)sel * sizeof(shape_t)); ++ ud->clipboard.shape_count = 0; ++ ++ int *gids = (int*) ALLOC((size_t)n * sizeof(int)); ++ int n_gids = 0; ++ ++ for (int i = 0; i < n; i++) { ++ shape_t *s = (shape_t*) vec_get(&ud->shapes, i); ++ if (!s->selected) continue; ++ ++ ud->clipboard.shapes[ud->clipboard.shape_count++] = clipboard_deep_copy_shape(s); ++ ++ int gid = s->group_id; ++ while (gid != 0) { ++ bool found = false; ++ for (int j = 0; j < n_gids; j++) { ++ if (gids[j] == gid) { found = true; break; } ++ } ++ if (!found) gids[n_gids++] = gid; ++ group_t *g = find_group(&ud->groups, gid); ++ gid = g ? g->parent_id : 0; ++ } ++ } ++ ++ ud->clipboard.groups = (group_t*) ALLOC((size_t)n_gids * sizeof(group_t)); ++ ud->clipboard.group_count = n_gids; ++ for (int j = 0; j < n_gids; j++) { ++ group_t *src = find_group(&ud->groups, gids[j]); ++ ud->clipboard.groups[j] = *src; ++ } ++ ++ FREE(gids); ++} ++ ++static void handle_paste(userdata_t *ud) ++{ ++ clipboard_t *cb = &ud->clipboard; ++ if (cb->shape_count == 0) return; ++ ++ int gc = cb->group_count; ++ int *gid_map = NULL; ++ if (gc > 0) { ++ gid_map = (int*) ALLOC((size_t)gc * 2 * sizeof(int)); ++ for (int j = 0; j < gc; j++) { ++ gid_map[j * 2] = cb->groups[j].id; ++ gid_map[j * 2 + 1] = ud->next_group_id++; ++ } ++ for (int j = 0; j < gc; j++) { ++ group_t g = cb->groups[j]; ++ g.id = clipboard_lookup_gid(g.id, gid_map, gc); ++ g.parent_id = clipboard_lookup_gid(g.parent_id, gid_map, gc); ++ *((group_t*) vec_push(&ud->groups)) = g; ++ } ++ group_index_rebuild(&ud->groups); ++ } ++ ++ for (int i = 0; i < ud->shapes.count; i++) ++ ((shape_t*) vec_get(&ud->shapes, i))->selected = false; ++ ud->interact.selected_count = 0; ++ ++ float cx, cy; ++ screen_to_world(&ud->camera, ud->mouse_x, ud->mouse_y, &cx, &cy); ++ ++ float cb_min_x = 0, cb_min_y = 0, cb_max_x = 0, cb_max_y = 0; ++ for (int i = 0; i < cb->shape_count; i++) { ++ shape_t *s = &cb->shapes[i]; ++ float sc = s->cos_r, ss = s->sin_r; ++ 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 (i == 0 && v == 0) { ++ cb_min_x = cb_max_x = wx; ++ cb_min_y = cb_max_y = wy; ++ } else { ++ if (wx < cb_min_x) cb_min_x = wx; ++ if (wx > cb_max_x) cb_max_x = wx; ++ if (wy < cb_min_y) cb_min_y = wy; ++ if (wy > cb_max_y) cb_max_y = wy; ++ } ++ } ++ } ++ float cb_cx = (cb_min_x + cb_max_x) * 0.5f; ++ float cb_cy = (cb_min_y + cb_max_y) * 0.5f; ++ ++ int sc = cb->shape_count; ++ hist_batch_t batch; ++ history_batch_init(&batch, sc); ++ ++ for (int i = 0; i < sc; i++) { ++ shape_t s = clipboard_deep_copy_shape(&cb->shapes[i]); ++ s.cx += cx - cb_cx; ++ s.cy += cy - cb_cy; ++ if (gc > 0) ++ s.group_id = clipboard_lookup_gid(s.group_id, gid_map, gc); ++ else ++ s.group_id = 0; ++ s.selected = true; ++ ud->interact.selected_count++; ++ ++ *((shape_t*) vec_push(&ud->shapes)) = s; ++ g_shape_pool_dirty = true; ++ history_batch_add_shape(&batch, ud->shapes.count - 1, HIST_CREATE, ++ (shape_t*) vec_get(&ud->shapes, ud->shapes.count - 1)); ++ } ++ ++ history_batch_commit(&batch, &ud->history); ++ FREE(gid_map); ++ ++ spatial_mark_dirty(&ud->spatial_grid); ++ ud->interact.aabb_cached = false; ++ ud->interact.focused_group_id = 0; ++ ud->overlay_upload_needed = true; ++ update_shape_states(ud); ++} ++ + // -- public event handlers -- + + static bool handle_key_down(userdata_t *ud, const sapp_event *event) +@@ -454,6 +648,7 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) + if (history_undo(&ud->history, &ud->shapes)) { + rebuild_groups_from_shapes(&ud->groups, &ud->shapes); + ud->interact.hovered_shape = -1; ++ g_shape_pool_dirty = true; + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; +@@ -464,12 +659,21 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) + if (history_redo(&ud->history, &ud->shapes)) { + rebuild_groups_from_shapes(&ud->groups, &ud->shapes); + ud->interact.hovered_shape = -1; ++ g_shape_pool_dirty = true; + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->overlay_upload_needed = true; + } + return true; + } ++ if (event->key_code == SAPP_KEYCODE_C) { ++ handle_copy(ud); ++ return true; ++ } ++ if (event->key_code == SAPP_KEYCODE_V) { ++ handle_paste(ud); ++ return true; ++ } + if (event->key_code == SAPP_KEYCODE_G) { + if (event->modifiers & SAPP_MODIFIER_SHIFT) { + // Ungroup: collect unique group IDs of selected shapes +@@ -549,6 +753,8 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) + } + } + ++ group_index_rebuild(&ud->groups); ++ + FREE(parents); + FREE(gids); + ud->ui.list_last_shape = -1; +@@ -624,6 +830,7 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) + + group_t new_grp = { .id = gid, .parent_id = 0 }; + *((group_t*) vec_push(&ud->groups)) = new_grp; ++ group_index_rebuild(&ud->groups); + + for (int j = 0; j < n_full; j++) { + for (int g = 0; g < ud->groups.count; g++) { +@@ -683,6 +890,7 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) + shape_shutdown(s); + vec_remove_ordered(&ud->shapes, indices[j]); + } ++ g_shape_pool_dirty = true; + + FREE(indices); + +@@ -853,6 +1061,9 @@ static void handle_mouse_up(userdata_t *ud, const sapp_event *event) + + static void handle_mouse_move(userdata_t *ud, const sapp_event *event) + { ++ ud->mouse_x = event->mouse_x; ++ ud->mouse_y = event->mouse_y; ++ + if (ud->camera.pan_state.dragging) { + handle_pan_drag(ud, event); + } else if (ud->interact.resize.dragging) { +diff --git a/src/interact.h b/src/interact.h +index b1c97a7..0360207 100644 +--- a/src/interact.h ++++ b/src/interact.h +@@ -11,7 +11,7 @@ static void selected_aabb(userdata_t *ud, float *min_x, float *min_y, + 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); ++ float sc = s->cos_r, ss = s->sin_r; + 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; +@@ -117,6 +117,7 @@ static void rebuild_groups_from_shapes(vector_t *groups, vector_t *shapes) + } + + if (saved) FREE(saved); ++ group_index_rebuild(groups); + } + + static int hit_test_resize_handles(userdata_t *ud, float wx, float wy, float tol) +diff --git a/src/main.c b/src/main.c +index db1bf3c..33aa624 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -34,6 +34,16 @@ static void log_capture(const char* tag, uint32_t log_level, uint32_t log_item, + if (log_level <= 1) ud->ui.log_show = true; + } + ++static void panel_log_impl(void *ud_v, int level, const char *msg) { ++ userdata_t *ud = (userdata_t*)ud_v; ++ int idx = ud->ui.log_head; ++ strncpy(ud->ui.log_ring[idx].text, msg, 255); ++ ud->ui.log_ring[idx].text[255] = 0; ++ ud->ui.log_ring[idx].level = (uint32_t)level; ++ ud->ui.log_head = (idx + 1) % LOG_RING_SIZE; ++ if (ud->ui.log_count < LOG_RING_SIZE) ud->ui.log_count++; ++} ++ + static void meter_fps(userdata_t *ud) + { + float dt = (float)sapp_frame_duration(); +@@ -102,10 +112,14 @@ static void init(void* _userdata) + + userdata_t* ud = (userdata_t*) _userdata; + ++ g_panel_log_fn = panel_log_impl; ++ g_panel_log_ud = ud; ++ + sg_desc sgdesc = { + .environment = sglue_environment(), + .logger.func = log_capture, + .logger.user_data = ud, ++ .uniform_buffer_size = 16 * 1024 * 1024, + }; + sg_setup(&sgdesc); + if (!sg_isvalid()) { +@@ -216,6 +230,7 @@ static void init(void* _userdata) + + vec_init(&ud->shapes, sizeof(shape_t)); + vec_init(&ud->groups, sizeof(group_t)); ++ vec_init(&ud->interact.drag_indices, sizeof(int)); + spatial_init(&ud->spatial_grid); + ud->interact.selected_count = 0; + ud->interact.hovered_shape = -1; +@@ -296,7 +311,7 @@ static void init(void* _userdata) + EM_ASM({ + window.addEventListener('keydown', function(e) { + if (e.ctrlKey && !e.altKey && !e.metaKey) { +- if (e.key === 'z' || e.key === 'y') { ++ if (e.key === 'z' || e.key === 'y' || e.key === 'c' || e.key === 'v') { + e.preventDefault(); + } + } +@@ -316,6 +331,8 @@ static void cleanup(void* _userdata) + spatial_destroy(&ud->spatial_grid); + vec_free(&ud->shapes); + vec_free(&ud->groups); ++ vec_free(&ud->interact.drag_indices); ++ group_index_shutdown(); + history_destroy(&ud->history); + if (ud->interact.resize.init) FREE(ud->interact.resize.init); + sg_destroy_buffer(ud->rect_vbuf); +@@ -329,6 +346,13 @@ static void cleanup(void* _userdata) + shape_pool_shutdown(); + shape_shutdown_pipeline(); + ++ for (int i = 0; i < ud->clipboard.shape_count; i++) { ++ FREE(ud->clipboard.shapes[i].verts); ++ FREE(ud->clipboard.shapes[i].indices); ++ } ++ FREE(ud->clipboard.shapes); ++ FREE(ud->clipboard.groups); ++ + FREE(ud); + + simgui_shutdown(); +diff --git a/src/overlay.h b/src/overlay.h +index e20795b..c22a676 100644 +--- a/src/overlay.h ++++ b/src/overlay.h +@@ -27,7 +27,26 @@ static void compute_overlay_geometry(userdata_t *ud, + overlay_verts[4] = (shape_vertex_t){x1, y1}; + *has_overlay = true; + } else if (ud->interact.selected_count >= 1) { +- if (ud->interact.selected_count == 1) { ++ if (ud->interact.move.dragging && ud->interact.aabb_cached) { ++ float dx = ud->interact.move.total_dx; ++ float dy = ud->interact.move.total_dy; ++ float omin_x = ud->interact.cached_aabb[0] + dx; ++ float omin_y = ud->interact.cached_aabb[1] + dy; ++ float omax_x = ud->interact.cached_aabb[2] + dx; ++ float omax_y = ud->interact.cached_aabb[3] + dy; ++ float pad = 8.0f / ud->camera.zoom; ++ overlay_verts[0] = (shape_vertex_t){omin_x - pad, omin_y - pad}; ++ overlay_verts[1] = (shape_vertex_t){omax_x + pad, omin_y - pad}; ++ overlay_verts[2] = (shape_vertex_t){omax_x + pad, omax_y + pad}; ++ overlay_verts[3] = (shape_vertex_t){omin_x - pad, omax_y + pad}; ++ overlay_verts[4] = overlay_verts[0]; ++ *sel_cx = (omin_x + omax_x) * 0.5f; ++ *sel_cy = (omin_y + omax_y) * 0.5f; ++ *sel_hw = (omax_x - omin_x) * 0.5f + pad; ++ *sel_hh = (omax_y - omin_y) * 0.5f + pad; ++ *sel_angle = 0; ++ *has_overlay = true; ++ } else 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; +@@ -52,8 +71,8 @@ static void compute_overlay_geometry(userdata_t *ud, + 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); ++ sum_sin += s->sin_r; ++ sum_cos += s->cos_r; + } + 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]; +@@ -102,10 +121,19 @@ static void upload_overlay_buffers(userdata_t *ud, + ud->interact.rotate.handle_radius = radius; + + const int n = HANDLE_CIRCLE_SEGMENTS + 1; ++ static shape_vertex_t unit_circle[HANDLE_CIRCLE_SEGMENTS + 1]; ++ static bool unit_circle_ready = false; ++ if (!unit_circle_ready) { ++ for (int i = 0; i < n; i++) { ++ float a = (float)i / (float)HANDLE_CIRCLE_SEGMENTS * 2.0f * GLM_PIf; ++ unit_circle[i] = (shape_vertex_t){cosf(a), sinf(a)}; ++ } ++ unit_circle_ready = true; ++ } + 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}; ++ hv[i] = (shape_vertex_t){sel_cx + unit_circle[i].x * radius, ++ sel_cy + unit_circle[i].y * radius}; + } + if (need_upload) + sg_update_buffer(ud->handle_vbuf, &(sg_range){hv, sizeof(hv)}); +diff --git a/src/render.h b/src/render.h +index 7f881b5..b0a4ae0 100644 +--- a/src/render.h ++++ b/src/render.h +@@ -5,6 +5,8 @@ + + static sg_pipeline shape_pipeline; + static sg_shader shape_shader; ++static sg_pipeline overlay_pipeline; ++static sg_shader overlay_shader; + static int g_shape_frame_id; + + static void shape_begin_frame(void) +@@ -14,13 +16,14 @@ static void shape_begin_frame(void) + + static void shape_init_pipeline(void) + { +- shape_shader = sg_make_shader(&(sg_shader_desc) { ++ // Overlay shader/pipeline (simple, no storage buffers) ++ overlay_shader = sg_make_shader(&(sg_shader_desc) { + .vertex_func = { +- .source = (const char*) src_shaders_shape_wgsl, ++ .source = (const char*) src_shaders_overlay_wgsl, + .entry = "vs_main", + }, + .fragment_func = { +- .source = (const char*) src_shaders_shape_wgsl, ++ .source = (const char*) src_shaders_overlay_wgsl, + .entry = "fs_main", + }, + .uniform_blocks = { +@@ -38,31 +41,79 @@ static void shape_init_pipeline(void) + .attrs = { + [0] = { .base_type = SG_SHADERATTRBASETYPE_FLOAT }, + }, ++ .label = "Overlay shader", ++ }); ++ panel_log(3, "[shapes] overlay shader id=%d valid=%d", overlay_shader.id, sg_isvalid()); ++ ++ overlay_pipeline = sg_make_pipeline(&(sg_pipeline_desc) { ++ .shader = overlay_shader, ++ .index_type = SG_INDEXTYPE_UINT16, ++ .primitive_type = SG_PRIMITIVETYPE_LINE_STRIP, ++ .layout.attrs = { ++ [0].format = SG_VERTEXFORMAT_FLOAT2, ++ }, ++ .label = "Overlay pipeline", ++ }); ++ panel_log(3, "[shapes] overlay pipeline id=%d valid=%d", overlay_pipeline.id, sg_isvalid()); ++ ++ // Shape shader/pipeline (storage buffers, instanced) ++ shape_shader = sg_make_shader(&(sg_shader_desc) { ++ .vertex_func = { ++ .source = (const char*) src_shaders_shape_wgsl, ++ .entry = "vs_main", ++ }, ++ .fragment_func = { ++ .source = (const char*) src_shaders_shape_wgsl, ++ .entry = "fs_main", ++ }, ++ .uniform_blocks = { ++ [0] = { ++ .size = 80, ++ .stage = SG_SHADERSTAGE_VERTEX, ++ .wgsl_group0_binding_n = 0, ++ }, ++ }, ++ .views = { ++ [0] = { ++ .storage_buffer = { ++ .stage = SG_SHADERSTAGE_VERTEX, ++ .readonly = true, ++ .wgsl_group1_binding_n = 0, ++ }, ++ }, ++ [1] = { ++ .storage_buffer = { ++ .stage = SG_SHADERSTAGE_VERTEX, ++ .readonly = true, ++ .wgsl_group1_binding_n = 1, ++ }, ++ }, ++ }, ++ .attrs = { ++ [0] = { .base_type = SG_SHADERATTRBASETYPE_FLOAT }, ++ }, + .label = "Shape shader", + }); ++ panel_log(3, "[shapes] shader id=%d valid=%d", shape_shader.id, sg_isvalid()); + + shape_pipeline = sg_make_pipeline(&(sg_pipeline_desc) { + .shader = shape_shader, +- .index_type = SG_INDEXTYPE_UINT16, ++ .index_type = SG_INDEXTYPE_NONE, + .primitive_type = SG_PRIMITIVETYPE_LINE_STRIP, + .layout.attrs = { + [0].format = SG_VERTEXFORMAT_FLOAT2, + }, + .label = "Shape pipeline", + }); ++ panel_log(3, "[shapes] pipeline id=%d valid=%d", shape_pipeline.id, sg_isvalid()); + } + + static void shape_shutdown_pipeline(void) + { + sg_destroy_pipeline(shape_pipeline); + sg_destroy_shader(shape_shader); +-} +- +-static void shape_draw(shape_t *s, const mat4 *mvp) +-{ +- sg_apply_uniforms(0, &SG_RANGE(*mvp)); +- sg_apply_uniforms(1, &SG_RANGE(s->uniform)); +- sg_draw((int)s->index_base, (int)s->num_elements, 1); ++ sg_destroy_pipeline(overlay_pipeline); ++ sg_destroy_shader(overlay_shader); + } + + #endif +diff --git a/src/shaders/shape.wgsl b/src/shaders/shape.wgsl +index e76259a..4d4a3d0 100644 +--- a/src/shaders/shape.wgsl ++++ b/src/shaders/shape.wgsl +@@ -1,13 +1,20 @@ + struct VsUniform { + mvp: mat4x4f, ++ instance_base: u32, + }; + +-struct ShapeUniform { ++struct ShapeData { + transform: mat4x4f, + state: u32, + }; + ++@binding(0) @group(0) var vs_uniforms: VsUniform; ++ ++@binding(0) @group(1) var shape_data: array; ++@binding(1) @group(1) var instance_map: array; ++ + struct VsIn { ++ @builtin(instance_index) instance_idx: u32, + @location(0) position: vec2f, + }; + +@@ -20,16 +27,15 @@ struct FsOut { + @location(0) color: vec4f, + }; + +-@binding(0) @group(0) var vs_uniforms: VsUniform; +-@binding(1) @group(0) var shape_uniform: ShapeUniform; +- + @vertex fn vs_main(input: VsIn) -> Vs2Fs { + var output: Vs2Fs; +- let world_pos = shape_uniform.transform * vec4f(input.position.x, input.position.y, 0.0, 1.0); ++ let shape_idx = instance_map[vs_uniforms.instance_base + input.instance_idx]; ++ let shape = shape_data[shape_idx]; ++ let world_pos = shape.transform * vec4f(input.position.x, input.position.y, 0.0, 1.0); + output.pos = vs_uniforms.mvp * world_pos; +- if (shape_uniform.state == 2u) { ++ if (shape.state == 2u) { + output.color = vec4f(1.0, 0.84, 0.0, 1.0); +- } else if (shape_uniform.state == 1u) { ++ } else if (shape.state == 1u) { + output.color = vec4f(0.5, 0.6, 1.0, 1.0); + } else { + output.color = vec4f(0.8, 0.8, 0.8, 1.0); +diff --git a/src/shape.h b/src/shape.h +index c1b65f3..fa59d5b 100644 +--- a/src/shape.h ++++ b/src/shape.h +@@ -20,6 +20,12 @@ typedef struct shape_uniform_t { + uint8_t _pad[12]; + } shape_uniform_t; + ++typedef struct { ++ mat4 transform; ++ uint32_t state; ++ uint8_t _pad[12]; ++} shape_gpu_data_t; ++ + typedef struct shape_t { + shape_vertex_t *verts; + uint16_t *indices; +@@ -32,11 +38,9 @@ typedef struct shape_t { + float cx, cy; + float sx, sy; + float rotation; ++ float cos_r, sin_r; + int kind; + +- uint32_t vertex_base; +- uint32_t index_base; +- + int group_id; + } shape_t; + +@@ -47,15 +51,55 @@ typedef struct { + int parent_id; // 0 = top-level group + } group_t; + +-static group_t* find_group(vector_t *groups, int id) { ++static group_t **g_group_by_id = NULL; ++static int g_group_by_id_cap = 0; ++ ++static void group_index_rebuild(vector_t *groups) ++{ ++ int max_id = 0; ++ for (int i = 0; i < groups->count; i++) { ++ int gid = ((group_t*) vec_get(groups, i))->id; ++ if (gid > max_id) max_id = gid; ++ } ++ if (max_id >= g_group_by_id_cap) { ++ if (g_group_by_id) FREE(g_group_by_id); ++ int new_cap = max_id + 64; ++ g_group_by_id = (group_t**) ALLOC((size_t)new_cap * sizeof(group_t*)); ++ memset(g_group_by_id, 0, (size_t)new_cap * sizeof(group_t*)); ++ g_group_by_id_cap = new_cap; ++ } else { ++ for (int i = 0; i <= max_id; i++) g_group_by_id[i] = NULL; ++ } + for (int i = 0; i < groups->count; i++) { + group_t *g = (group_t*) vec_get(groups, i); +- if (g->id == id) return g; ++ g_group_by_id[g->id] = g; ++ } ++} ++ ++static void group_index_ensure_cap(int max_id) ++{ ++ if (max_id >= g_group_by_id_cap) { ++ int new_cap = max_id + 64; ++ group_t **old = g_group_by_id; ++ g_group_by_id = (group_t**) ALLOC((size_t)new_cap * sizeof(group_t*)); ++ if (old) { ++ memcpy(g_group_by_id, old, (size_t)g_group_by_id_cap * sizeof(group_t*)); ++ FREE(old); ++ } ++ memset(g_group_by_id + g_group_by_id_cap, 0, ++ (size_t)(new_cap - g_group_by_id_cap) * sizeof(group_t*)); ++ g_group_by_id_cap = new_cap; + } +- return NULL; ++} ++ ++static group_t* find_group(vector_t *groups, int id) { ++ (void)groups; ++ if (id <= 0 || id >= g_group_by_id_cap) return NULL; ++ return g_group_by_id[id]; + } + + static int get_topmost_group(vector_t *groups, int gid) { ++ (void)groups; + while (gid != 0) { + group_t *g = find_group(groups, gid); + if (!g || g->parent_id == 0) return gid; +@@ -65,6 +109,7 @@ static int get_topmost_group(vector_t *groups, int gid) { + } + + static bool is_shape_in_group_hierarchy(int shape_gid, int target_gid, vector_t *groups) { ++ (void)groups; + int cur = shape_gid; + while (cur != 0) { + if (cur == target_gid) return true; +@@ -75,74 +120,189 @@ static bool is_shape_in_group_hierarchy(int shape_gid, int target_gid, vector_t + return false; + } + ++static void group_index_shutdown(void) ++{ ++ if (g_group_by_id) FREE(g_group_by_id); ++ g_group_by_id = NULL; ++ g_group_by_id_cap = 0; ++} ++ + // -- 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 sg_buffer g_shape_data_sbuf = {0}; ++static sg_buffer g_instance_map_sbuf = {0}; ++static sg_view g_shape_data_view = {0}; ++static sg_view g_instance_map_view = {0}; ++ ++// Per-group vertex buffers: one per unique num_elements ++typedef struct { ++ uint32_t num_elements; ++ sg_buffer vbuf; ++} shape_group_buf_t; ++static shape_group_buf_t *g_shape_groups = NULL; ++static int g_shape_group_count = 0; ++ + static bool g_shape_pool_dirty; +-static uint32_t g_shape_vert_count; +-static uint32_t g_shape_idx_count; ++static bool g_shape_data_dirty; ++static size_t g_shape_data_buf_size = 0; ++static int g_instance_map_capacity = 0; + +-static void shape_pool_rebuild(vector_t *shapes) ++static void shape_make_view_for_buffer(sg_view *view, sg_buffer buf) ++{ ++ if (view->id) sg_destroy_view(*view); ++ *view = sg_make_view(&(sg_view_desc){ ++ .storage_buffer = { .buffer = buf }, ++ }); ++} ++ ++static shape_gpu_data_t *g_upload_buf = NULL; ++static int g_upload_buf_cap = 0; ++ ++static void shape_upload_data(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++) { ++ int n = shapes->count; ++ if (n == 0 || !g_shape_data_sbuf.id) return; ++ ++ size_t need = (size_t)n * sizeof(shape_gpu_data_t); ++ if (need > g_shape_data_buf_size) { ++ panel_log(2, "[shapes] upload_data: buffer too small (%zu < %zu), forcing rebuild", ++ g_shape_data_buf_size, need); ++ g_shape_pool_dirty = true; ++ return; ++ } ++ ++ if (n > g_upload_buf_cap) { ++ if (g_upload_buf) FREE(g_upload_buf); ++ g_upload_buf = (shape_gpu_data_t*) ALLOC(need); ++ g_upload_buf_cap = n; ++ } ++ ++ for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(shapes, i); +- total_verts += s->num_elements; +- total_indices += s->num_elements; ++ memcpy(g_upload_buf[i].transform, s->uniform.transform, sizeof(mat4)); ++ g_upload_buf[i].state = s->uniform.state; ++ memset(g_upload_buf[i]._pad, 0, sizeof(g_upload_buf[i]._pad)); ++ } ++ sg_update_buffer(g_shape_data_sbuf, &(sg_range){g_upload_buf, need}); ++} ++ ++static void shape_upload_instance_map(const uint32_t *map, int count) ++{ ++ if (count > g_instance_map_capacity) { ++ if (g_instance_map_sbuf.id) sg_destroy_buffer(g_instance_map_sbuf); ++ g_instance_map_sbuf = sg_make_buffer(&(sg_buffer_desc){ ++ .size = (size_t)count * sizeof(uint32_t), ++ .usage = { .storage_buffer = true, .stream_update = true }, ++ .label = "Instance map", ++ }); ++ g_instance_map_capacity = count; ++ shape_make_view_for_buffer(&g_instance_map_view, g_instance_map_sbuf); ++ } ++ sg_update_buffer(g_instance_map_sbuf, &(sg_range){map, (size_t)count * sizeof(uint32_t)}); ++ panel_log(3, "[shapes] upload_instance_map: count=%d buf=%d view=%d", ++ count, g_instance_map_sbuf.id, g_instance_map_view.id); ++} ++ ++static void shape_pool_rebuild(vector_t *shapes) ++{ ++ int n = shapes->count; ++ ++ // Destroy old groups ++ for (int i = 0; i < g_shape_group_count; i++) { ++ if (g_shape_groups[i].vbuf.id) sg_destroy_buffer(g_shape_groups[i].vbuf); + } ++ FREE(g_shape_groups); ++ g_shape_groups = NULL; ++ g_shape_group_count = 0; + +- 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 (g_shape_data_sbuf.id) { sg_destroy_buffer(g_shape_data_sbuf); g_shape_data_sbuf.id = 0; } ++ if (g_shape_data_view.id) { sg_destroy_view(g_shape_data_view); g_shape_data_view.id = 0; } + +- if (total_verts == 0) { ++ if (n == 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_data_buf_size = (size_t)n * sizeof(shape_gpu_data_t); ++ g_shape_data_sbuf = sg_make_buffer(&(sg_buffer_desc){ ++ .size = g_shape_data_buf_size, ++ .usage = { .storage_buffer = true, .stream_update = true }, ++ .label = "Shape data", ++ }); ++ shape_make_view_for_buffer(&g_shape_data_view, g_shape_data_sbuf); ++ // Data filled by shape_upload_data() in draw_shapes ++ ++ // Count unique num_elements ++ uint32_t max_ne = 0; ++ for (int i = 0; i < n; i++) { ++ uint32_t ne = ((shape_t*) vec_get(shapes, i))->num_elements; ++ if (ne > max_ne) max_ne = ne; + } + +- 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)", +- }); ++ int *ne_seen = (int*) ALLOC((size_t)(max_ne + 1) * sizeof(int)); ++ memset(ne_seen, 0, (size_t)(max_ne + 1) * sizeof(int)); ++ for (int i = 0; i < n; i++) { ++ uint32_t ne = ((shape_t*) vec_get(shapes, i))->num_elements; ++ ne_seen[ne] = 1; ++ } ++ int group_count = 0; ++ for (uint32_t ne = 0; ne <= max_ne; ne++) ++ if (ne_seen[ne]) group_count++; ++ ++ // Create per-group vertex buffers (one copy of vertex data per unique num_elements) ++ g_shape_groups = (shape_group_buf_t*) ALLOC((size_t)group_count * sizeof(shape_group_buf_t)); ++ memset(g_shape_groups, 0, (size_t)group_count * sizeof(shape_group_buf_t)); ++ ++ int gi = 0; ++ for (uint32_t ne = 0; ne <= max_ne; ne++) { ++ if (!ne_seen[ne]) continue; ++ ++ // Find first shape with this num_elements to use as vertex template ++ shape_t *ref = NULL; ++ for (int i = 0; i < n; i++) { ++ if (((shape_t*) vec_get(shapes, i))->num_elements == ne) { ++ ref = (shape_t*) vec_get(shapes, i); ++ break; ++ } ++ } ++ ++ g_shape_groups[gi].num_elements = ne; ++ g_shape_groups[gi].vbuf = sg_make_buffer(&(sg_buffer_desc){ ++ .data = { ref->verts, (size_t)ne * sizeof(shape_vertex_t) }, ++ .label = "Shape group verts", ++ }); ++ gi++; ++ } ++ g_shape_group_count = group_count; + +- FREE(all_v); +- FREE(all_i); ++ FREE(ne_seen); ++ ++ panel_log(3, "[shapes] pool_rebuild: %d shapes, %d groups, data_buf=%d data_view=%d", ++ n, group_count, g_shape_data_sbuf.id, g_shape_data_view.id); ++ for (int gi = 0; gi < group_count; gi++) { ++ panel_log(3, "[shapes] group[%d]: ne=%u vbuf=%d", ++ gi, g_shape_groups[gi].num_elements, g_shape_groups[gi].vbuf.id); ++ } + +- 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; ++ for (int i = 0; i < g_shape_group_count; i++) { ++ if (g_shape_groups[i].vbuf.id) sg_destroy_buffer(g_shape_groups[i].vbuf); ++ } ++ FREE(g_shape_groups); ++ g_shape_groups = NULL; ++ g_shape_group_count = 0; ++ ++ if (g_shape_data_view.id) { sg_destroy_view(g_shape_data_view); g_shape_data_view.id = 0; } ++ if (g_instance_map_view.id) { sg_destroy_view(g_instance_map_view); g_instance_map_view.id = 0; } ++ if (g_shape_data_sbuf.id) { sg_destroy_buffer(g_shape_data_sbuf); g_shape_data_sbuf.id = 0; } ++ if (g_instance_map_sbuf.id) { sg_destroy_buffer(g_instance_map_sbuf); g_instance_map_sbuf.id = 0; } ++ g_instance_map_capacity = 0; ++ if (g_upload_buf) { FREE(g_upload_buf); g_upload_buf = NULL; } ++ g_upload_buf_cap = 0; + } + + #define SHAPE_HOVER_PX 6.0f +@@ -172,11 +332,23 @@ static void shape_build_transform(shape_t *s) + glm_scale_make(S, (vec3){s->sx, s->sy, 1.0f}); + glm_mat4_mul(R, S, RS); + glm_mat4_mul(T, RS, s->uniform.transform); ++ s->cos_r = R[0][0]; ++ s->sin_r = R[0][1]; ++ g_shape_data_dirty = true; ++} ++ ++static void shape_retranslate(shape_t *s) ++{ ++ s->uniform.transform[3][0] = s->cx; ++ s->uniform.transform[3][1] = s->cy; ++ g_shape_data_dirty = true; + } + + static void shape_make_buffers(shape_t *s) + { +- (void)s; ++ for (int i = 0; i < g_shape_group_count; i++) { ++ if (g_shape_groups[i].num_elements == s->num_elements) return; ++ } + g_shape_pool_dirty = true; + } + +@@ -194,9 +366,11 @@ static void shape_regenerate(shape_t *s) + + static void shape_set_state(shape_t *s, bool hovered, bool selected) + { ++ uint32_t new_state = selected ? 2u : (hovered ? 1u : 0u); ++ if (s->uniform.state != new_state) g_shape_data_dirty = true; + s->hovered = hovered; + s->selected = selected; +- s->uniform.state = selected ? 2u : (hovered ? 1u : 0u); ++ s->uniform.state = new_state; + } + + static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t n) +@@ -213,7 +387,7 @@ static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t + + static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol) + { +- float sc = cosf(s->rotation), ss = sinf(s->rotation); ++ float sc = s->cos_r, ss = s->sin_r; + float dx = wx - s->cx, dy = wy - s->cy; + float lx = (dx * sc + dy * ss) / s->sx; + float ly = (-dx * ss + dy * sc) / s->sy; +diff --git a/src/spatial.h b/src/spatial.h +index d3dd24a..51bc4e9 100644 +--- a/src/spatial.h ++++ b/src/spatial.h +@@ -35,8 +35,8 @@ static int spatial_hash(int cx, int cy) + static void spatial_compute_aabb(shape_t *s, float *min_x, float *min_y, + float *max_x, float *max_y) + { +- float cos_r = cosf(s->rotation); +- float sin_r = sinf(s->rotation); ++ float cos_r = s->cos_r; ++ float sin_r = s->sin_r; + float hx = fabsf(cos_r) * s->sx + fabsf(sin_r) * s->sy; + float hy = fabsf(sin_r) * s->sx + fabsf(cos_r) * s->sy; + *min_x = s->cx - hx; +@@ -184,52 +184,34 @@ static int spatial_query_rect_select(spatial_grid_t *grid, vector_t *shapes, + } + int selected_count = 0; + +- int min_cx = (int) floorf(min_x / SPATIAL_CELL_SIZE); +- int min_cy = (int) floorf(min_y / SPATIAL_CELL_SIZE); +- int max_cx = (int) floorf(max_x / SPATIAL_CELL_SIZE); +- int max_cy = (int) floorf(max_y / SPATIAL_CELL_SIZE); +- +- for (int cell_x = min_cx; cell_x <= max_cx; cell_x++) { +- for (int cell_y = min_cy; cell_y <= max_cy; cell_y++) { +- int idx = spatial_hash(cell_x, cell_y) & (SPATIAL_HASH_SIZE - 1); +- int probe_start = idx; +- +- do { +- if (!grid->slots[idx].occupied) break; +- +- if (grid->slots[idx].cx == cell_x && grid->slots[idx].cy == cell_y) { +- for (int e = 0; e < grid->slots[idx].count; e++) { +- spatial_entry_t *entry = &grid->slots[idx].entries[e]; +- +- if (entry->max_x < min_x || entry->min_x > max_x || +- entry->max_y < min_y || entry->min_y > max_y) +- continue; +- +- shape_t *s = (shape_t*) vec_get(shapes, entry->shape_idx); +- if (s->selected) continue; +- +- bool hit = (s->cx >= min_x && s->cx <= max_x && +- s->cy >= min_y && s->cy <= max_y); +- float sc = cosf(s->rotation), ss = sinf(s->rotation); +- for (uint32_t v = 0; !hit && v < s->num_verts; v++) { +- float lx = s->verts[v].x * s->sx; +- float ly = s->verts[v].y * s->sy; +- float wx = s->cx + lx * sc - ly * ss; +- float wy = s->cy + lx * ss + ly * sc; +- if (wx >= min_x && wx <= max_x && +- wy >= min_y && wy <= max_y) +- hit = true; +- } +- if (hit) { +- s->selected = true; +- selected_count++; +- } +- } +- break; +- } +- +- idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1); +- } while (idx != probe_start); ++ for (int s = 0; s < SPATIAL_HASH_SIZE; s++) { ++ if (!grid->slots[s].occupied) continue; ++ for (int e = 0; e < grid->slots[s].count; e++) { ++ spatial_entry_t *entry = &grid->slots[s].entries[e]; ++ ++ if (entry->max_x < min_x || entry->min_x > max_x || ++ entry->max_y < min_y || entry->min_y > max_y) ++ continue; ++ ++ shape_t *shape = (shape_t*) vec_get(shapes, entry->shape_idx); ++ if (shape->selected) continue; ++ ++ bool hit = (shape->cx >= min_x && shape->cx <= max_x && ++ shape->cy >= min_y && shape->cy <= max_y); ++ float sc = shape->cos_r, ss = shape->sin_r; ++ for (uint32_t v = 0; !hit && v < shape->num_verts; v++) { ++ float lx = shape->verts[v].x * shape->sx; ++ float ly = shape->verts[v].y * shape->sy; ++ float wx = shape->cx + lx * sc - ly * ss; ++ float wy = shape->cy + lx * ss + ly * sc; ++ if (wx >= min_x && wx <= max_x && ++ wy >= min_y && wy <= max_y) ++ hit = true; ++ } ++ if (hit) { ++ shape->selected = true; ++ selected_count++; ++ } + } + } + return selected_count; +diff --git a/src/types.h b/src/types.h +index 8a9272f..98fcef8 100644 +--- a/src/types.h ++++ b/src/types.h +@@ -89,6 +89,8 @@ typedef struct { + int focused_group_id; + double last_click_time; + int last_click_shape_idx; ++ ++ vector_t drag_indices; + } interact_state_t; + + typedef struct { +@@ -112,6 +114,13 @@ typedef struct { + int list_prev_count; + } ui_state_t; + ++typedef struct { ++ shape_t *shapes; ++ int shape_count; ++ group_t *groups; ++ int group_count; ++} clipboard_t; ++ + typedef struct userdata_t { + camera_t camera; + renderer_t renderer; +@@ -127,6 +136,8 @@ typedef struct userdata_t { + sg_buffer corner_vbuf, corner_ibuf; + int next_group_id; + vector_t groups; ++ clipboard_t clipboard; ++ float mouse_x, mouse_y; + double time; + } userdata_t; + +diff --git a/src/ui_panels.h b/src/ui_panels.h +index 6d7a243..7ba4920 100644 +--- a/src/ui_panels.h ++++ b/src/ui_panels.h +@@ -352,8 +352,8 @@ static void draw_properties_panel(userdata_t *ud) + (*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->verts[0].x * s->sx * s->cos_r - s->verts[0].y * s->sy * s->sin_r, ++ s->cy + s->verts[0].x * s->sx * s->sin_r + s->verts[0].y * s->sy * s->cos_r, + s->cx, s->cy, s->sx, s->sy, s->rotation); + if (igButton("Copy Debug", (ImVec2){0, 0})) + sapp_set_clipboard_string(dbg); +@@ -389,7 +389,9 @@ static void draw_log_panel(userdata_t *ud) + 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); ++ EM_ASM({ ++ navigator.clipboard.writeText(UTF8ToString($0)); ++ }, buf); + FREE(buf); + } + igSameLine(0.0f, 10.0f); diff --git a/src/api.h b/src/api.h index dd80adf..a4707c7 100644 --- a/src/api.h +++ b/src/api.h @@ -27,6 +27,21 @@ #include "generated/sprite.h" #include "generated/shape.h" +#include "generated/overlay.h" + +// Log-to-panel infrastructure +static void (*g_panel_log_fn)(void*, int, const char*) = NULL; +static void *g_panel_log_ud = NULL; + +static void panel_log(int level, const char *fmt, ...) { + if (!g_panel_log_fn) return; + char buf[256]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + g_panel_log_fn(g_panel_log_ud, level, buf); +} #include "util.h" #include "shape.h" diff --git a/src/draw.h b/src/draw.h index 17ad292..e22234b 100644 --- a/src/draw.h +++ b/src/draw.h @@ -9,20 +9,109 @@ static void draw_shapes(userdata_t *ud) if (g_shape_pool_dirty) shape_pool_rebuild(&ud->shapes); - if (ud->shapes.count == 0) return; + int n = ud->shapes.count; + if (n == 0) return; + + if (g_shape_data_dirty) { + shape_upload_data(&ud->shapes); + g_shape_data_dirty = false; + } + panel_log(3, "[shapes] draw_shapes: n=%d pipeline=%d", n, shape_pipeline.id); + + static uint32_t *imap = NULL; + static int imap_cap = 0; + static uint32_t *ne_counts = NULL; + static uint32_t *ne_starts = NULL; + static int ne_cap = 0; + + if (n > imap_cap) { + if (imap) FREE(imap); + imap = (uint32_t*) ALLOC((size_t)n * sizeof(uint32_t)); + imap_cap = n; + } + + // Group shapes by num_elements using counting sort + uint32_t max_ne = 0; + for (int i = 0; i < n; i++) { + uint32_t ne = ((shape_t*) vec_get(&ud->shapes, i))->num_elements; + if (ne > max_ne) max_ne = ne; + } + + int ne_size = (int)(max_ne + 1); + if (ne_size > ne_cap) { + if (ne_counts) FREE(ne_counts); + if (ne_starts) FREE(ne_starts); + ne_counts = (uint32_t*) ALLOC((size_t)ne_size * sizeof(uint32_t)); + ne_starts = (uint32_t*) ALLOC((size_t)ne_size * sizeof(uint32_t)); + ne_cap = ne_size; + } + memset(ne_counts, 0, (size_t)ne_size * sizeof(uint32_t)); + + for (int i = 0; i < n; i++) { + uint32_t ne = ((shape_t*) vec_get(&ud->shapes, i))->num_elements; + ne_counts[ne]++; + } + + uint32_t pos = 0; + for (uint32_t ne = 0; ne <= max_ne; ne++) { + ne_starts[ne] = pos; + pos += ne_counts[ne]; + ne_counts[ne] = 0; + } + + for (int i = 0; i < n; i++) { + uint32_t ne = ((shape_t*) vec_get(&ud->shapes, i))->num_elements; + imap[ne_starts[ne] + ne_counts[ne]++] = (uint32_t)i; + } + + shape_upload_instance_map(imap, n); 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); + + int base = 0; + while (base < n) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, imap[base]); + uint32_t ne = s->num_elements; + int count = 1; + while (base + count < n && + ((shape_t*) vec_get(&ud->shapes, imap[base + count]))->num_elements == ne) + count++; + + // Find the vertex buffer for this num_elements + sg_buffer group_vbuf = {0}; + for (int gi = 0; gi < g_shape_group_count; gi++) { + if (g_shape_groups[gi].num_elements == ne) { + group_vbuf = g_shape_groups[gi].vbuf; + break; + } + } + + struct { mat4 mvp; uint32_t base; uint8_t _pad[12]; } vs_u; + memcpy(vs_u.mvp, ud->renderer.uniform.mvp, sizeof(mat4)); + vs_u.base = (uint32_t)base; + memset(vs_u._pad, 0, 12); + sg_apply_uniforms(0, &SG_RANGE(vs_u)); + + sg_apply_bindings(&(sg_bindings){ + .vertex_buffers[0] = group_vbuf, + .views[0] = g_shape_data_view, + .views[1] = g_instance_map_view, + }); + panel_log(3, "[shapes] draw group: ne=%u count=%d base=%d vbuf=%d data_view=%d imap_view=%d", + ne, count, base, group_vbuf.id, g_shape_data_view.id, g_instance_map_view.id); + sg_draw(0, (int)ne, count); + + base += count; } + } static void draw_overlay_and_handles(userdata_t *ud, bool has_overlay, bool show_handle) { + sg_apply_pipeline(overlay_pipeline); + panel_log(3, "[shapes] draw_overlay: pipeline=%d has_ov=%d show_h=%d", + overlay_pipeline.id, has_overlay, show_handle); + if (has_overlay) { shape_uniform_t u; glm_mat4_identity(u.transform); diff --git a/src/generated/overlay.h b/src/generated/overlay.h new file mode 100644 index 0000000..15114c2 --- /dev/null +++ b/src/generated/overlay.h @@ -0,0 +1,91 @@ +unsigned char src_shaders_overlay_wgsl[] = { + 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69, + 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, + 0x76, 0x70, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, 0x66, 0x2c, + 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, + 0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, + 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x66, 0x6f, 0x72, 0x6d, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, + 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, 0x49, 0x6e, 0x20, 0x7b, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66, 0x2c, 0x0a, 0x7d, + 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, + 0x32, 0x46, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x62, + 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x3a, 0x20, 0x76, 0x65, + 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x40, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x70, 0x6f, 0x6c, 0x61, 0x74, 0x65, 0x28, 0x6c, + 0x69, 0x6e, 0x65, 0x61, 0x72, 0x29, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, + 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, + 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x46, 0x73, 0x4f, 0x75, + 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x63, 0x6f, 0x6c, + 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x7d, + 0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, + 0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29, + 0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, + 0x3e, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, + 0x73, 0x3a, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, + 0x3b, 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31, + 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29, 0x20, + 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3e, + 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, + 0x72, 0x6d, 0x3a, 0x20, 0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69, + 0x66, 0x6f, 0x72, 0x6d, 0x3b, 0x0a, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74, + 0x65, 0x78, 0x20, 0x66, 0x6e, 0x20, 0x76, 0x73, 0x5f, 0x6d, 0x61, 0x69, + 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x49, + 0x6e, 0x29, 0x20, 0x2d, 0x3e, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20, + 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, 0x72, + 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, + 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x2a, 0x20, 0x76, + 0x65, 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, + 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x73, 0x5f, + 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, + 0x20, 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, + 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, + 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, + 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, + 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x31, 0x2e, 0x30, 0x2c, + 0x20, 0x30, 0x2e, 0x38, 0x34, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, + 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, + 0x65, 0x6c, 0x73, 0x65, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, + 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x75, 0x29, 0x20, + 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x3d, + 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, 0x35, 0x2c, 0x20, + 0x30, 0x2e, 0x36, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, + 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, 0x6c, + 0x73, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, + 0x72, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, + 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, + 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x40, + 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20, + 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, 0x3e, + 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3a, + 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, + 0x20, 0x3d, 0x20, 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_overlay_wgsl_len = 1045; diff --git a/src/generated/shape.h b/src/generated/shape.h index c5e95d0..a35c0b1 100644 --- a/src/generated/shape.h +++ b/src/generated/shape.h @@ -2,90 +2,109 @@ unsigned char src_shaders_shape_wgsl[] = { 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x76, 0x70, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, 0x66, 0x2c, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, - 0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, - 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x66, 0x6f, 0x72, 0x6d, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, - 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x3a, 0x20, 0x75, 0x33, 0x32, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, - 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, 0x49, 0x6e, 0x20, 0x7b, - 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, 0x66, 0x2c, 0x0a, 0x7d, + 0x53, 0x68, 0x61, 0x70, 0x65, 0x44, 0x61, 0x74, 0x61, 0x20, 0x7b, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, + 0x6d, 0x3a, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x78, 0x34, 0x66, 0x2c, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x3a, 0x20, 0x75, + 0x33, 0x32, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x28, 0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x28, 0x30, 0x29, 0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, + 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3e, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, + 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x3a, 0x20, 0x56, 0x73, 0x55, 0x6e, + 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x28, 0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x28, 0x31, 0x29, 0x20, 0x76, 0x61, 0x72, 0x3c, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x3e, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, + 0x5f, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79, + 0x3c, 0x53, 0x68, 0x61, 0x70, 0x65, 0x44, 0x61, 0x74, 0x61, 0x3e, 0x3b, + 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31, 0x29, + 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x31, 0x29, 0x20, 0x76, + 0x61, 0x72, 0x3c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x3e, 0x20, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, + 0x3a, 0x20, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x75, 0x33, 0x32, 0x3e, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x56, 0x73, - 0x32, 0x46, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x62, - 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x29, 0x20, 0x70, 0x6f, 0x73, 0x3a, 0x20, 0x76, 0x65, - 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x40, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x70, 0x6f, 0x6c, 0x61, 0x74, 0x65, 0x28, 0x6c, - 0x69, 0x6e, 0x65, 0x61, 0x72, 0x29, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, - 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, - 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, 0x46, 0x73, 0x4f, 0x75, - 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x63, 0x6f, 0x6c, - 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x7d, - 0x3b, 0x0a, 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, - 0x30, 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29, - 0x20, 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, - 0x3e, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, - 0x73, 0x3a, 0x20, 0x56, 0x73, 0x55, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, - 0x3b, 0x0a, 0x40, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x28, 0x31, - 0x29, 0x20, 0x40, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x28, 0x30, 0x29, 0x20, - 0x76, 0x61, 0x72, 0x3c, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x3e, - 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, - 0x72, 0x6d, 0x3a, 0x20, 0x53, 0x68, 0x61, 0x70, 0x65, 0x55, 0x6e, 0x69, - 0x66, 0x6f, 0x72, 0x6d, 0x3b, 0x0a, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74, + 0x49, 0x6e, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x62, 0x75, + 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x29, 0x20, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x3a, 0x20, + 0x75, 0x33, 0x32, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x40, 0x6c, 0x6f, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, 0x20, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x32, + 0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, + 0x74, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x40, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x28, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29, 0x20, 0x70, 0x6f, 0x73, + 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, + 0x29, 0x20, 0x40, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x6f, 0x6c, 0x61, + 0x74, 0x65, 0x28, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x29, 0x20, 0x63, + 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x2c, + 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x20, + 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x40, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x30, 0x29, + 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x76, 0x65, 0x63, 0x34, + 0x66, 0x2c, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x40, 0x76, 0x65, 0x72, 0x74, 0x65, 0x78, 0x20, 0x66, 0x6e, 0x20, 0x76, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x49, 0x6e, 0x29, 0x20, 0x2d, 0x3e, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x3b, - 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, 0x72, - 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, - 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x74, - 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x2a, 0x20, 0x76, - 0x65, 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x78, 0x2c, 0x20, 0x69, - 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, - 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x76, 0x73, 0x5f, - 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x6d, 0x76, 0x70, - 0x20, 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, - 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, - 0x61, 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x73, 0x68, 0x61, + 0x70, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x5b, 0x76, 0x73, + 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x2e, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x20, + 0x2b, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x5d, 0x3b, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, + 0x20, 0x3d, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x64, 0x61, 0x74, + 0x61, 0x5b, 0x73, 0x68, 0x61, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x78, 0x5d, + 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x77, 0x6f, + 0x72, 0x6c, 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x73, 0x68, + 0x61, 0x70, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, + 0x6d, 0x20, 0x2a, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x69, 0x6e, + 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x78, 0x2c, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x79, 0x2c, 0x20, 0x30, 0x2e, + 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x6f, 0x73, 0x20, + 0x3d, 0x20, 0x76, 0x73, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, + 0x73, 0x2e, 0x6d, 0x76, 0x70, 0x20, 0x2a, 0x20, 0x77, 0x6f, 0x72, 0x6c, + 0x64, 0x5f, 0x70, 0x6f, 0x73, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, + 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, 0x70, 0x65, 0x2e, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x75, 0x29, 0x20, 0x7b, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x3d, 0x20, 0x76, + 0x65, 0x63, 0x34, 0x66, 0x28, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x30, 0x2e, + 0x38, 0x34, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, 0x30, + 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, 0x6c, 0x73, + 0x65, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, 0x70, 0x65, 0x2e, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x75, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, - 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x31, 0x2e, 0x30, 0x2c, - 0x20, 0x30, 0x2e, 0x38, 0x34, 0x2c, 0x20, 0x30, 0x2e, 0x30, 0x2c, 0x20, - 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, - 0x65, 0x6c, 0x73, 0x65, 0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x68, 0x61, - 0x70, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x75, 0x29, 0x20, - 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, - 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x3d, - 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, 0x35, 0x2c, 0x20, - 0x30, 0x2e, 0x36, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x31, 0x2e, - 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, 0x6c, - 0x73, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, 0x35, 0x2c, + 0x20, 0x30, 0x2e, 0x36, 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x31, + 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x20, 0x65, + 0x6c, 0x73, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, + 0x6f, 0x72, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, + 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, + 0x2c, 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, + 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, + 0x40, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, + 0x20, 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, + 0x3e, 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x3a, 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, - 0x72, 0x20, 0x3d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x66, 0x28, 0x30, 0x2e, - 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, 0x20, 0x30, 0x2e, 0x38, 0x2c, - 0x20, 0x31, 0x2e, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, - 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, - 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x40, - 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x6e, 0x20, - 0x66, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x28, 0x69, 0x6e, 0x70, 0x75, - 0x74, 0x3a, 0x20, 0x56, 0x73, 0x32, 0x46, 0x73, 0x29, 0x20, 0x2d, 0x3e, - 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, - 0x20, 0x76, 0x61, 0x72, 0x20, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x3a, - 0x20, 0x46, 0x73, 0x4f, 0x75, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, - 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x2e, 0x63, 0x6f, 0x6c, 0x6f, 0x72, - 0x20, 0x3d, 0x20, 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 + 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 = 1045; +unsigned int src_shaders_shape_wgsl_len = 1274; diff --git a/src/history.h b/src/history.h index cfcf0aa..75471d2 100644 --- a/src/history.h +++ b/src/history.h @@ -3,6 +3,8 @@ #include "api.h" +#define HISTORY_MAX_DEPTH 256 + typedef enum { HIST_POSITION, HIST_SCALE, @@ -103,6 +105,17 @@ static void history_push_entry(history_t *h, hist_entry_t entry) { *((hist_entry_t*) vec_push(&h->entries)) = entry; h->current = h->entries.count - 1; + + while (h->entries.count > HISTORY_MAX_DEPTH) { + hist_entry_t *e = (hist_entry_t*) vec_get(&h->entries, 0); + if (e->changes) { + for (int j = 0; j < e->count; j++) + hist_free_change(&e->changes[j]); + FREE(e->changes); + } + vec_remove_ordered(&h->entries, 0); + h->current--; + } } static void history_begin_edit(history_t *h, vector_t *shapes, @@ -180,14 +193,9 @@ static void history_batch_add(hist_batch_t *batch, int shape_index, hist_prop_t // Used for HIST_CREATE and HIST_DELETE entries. // old_val = { kind, cx, cy, num_verts } // new_val = { sx, sy, rotation, group_id } +// For procedural shapes (CIRCLE, STAR, RECTANGLE), vertices are reconstructed +// from parameters instead of deep-copied. For STAR, new_val[1] stores inner_r. 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; @@ -196,6 +204,28 @@ static void hist_snapshot_shape_verts(hist_change_t *c, shape_t *s) { c->new_val[1] = s->sy; c->new_val[2] = s->rotation; c->new_val[3] = (float)s->group_id; + + if (s->kind == SHAPE_GENERIC) { + 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)); + } else if (s->kind == SHAPE_STAR) { + float inner_ratio = sqrtf(s->verts[1].x * s->verts[1].x + s->verts[1].y * s->verts[1].y); + c->new_val[1] = inner_ratio * s->sx; + c->vertex_count = 0; + c->index_count = 0; + c->vertex_data = NULL; + c->index_data = NULL; + } else { + c->vertex_count = 0; + c->index_count = 0; + c->vertex_data = NULL; + c->index_data = NULL; + } } // Append a CREATE or DELETE entry to a batch, snapshotting the shape's vertex data. @@ -214,27 +244,46 @@ static void history_batch_commit(hist_batch_t *batch, history_t *h) { // Reconstruct a shape_t from a HIST_CREATE / HIST_DELETE change snapshot. static shape_t hist_rebuild_shape_from_snapshot(const hist_change_t *c) { + float cx = c->old_val[1], cy = c->old_val[2]; + float sx = c->new_val[0], rot = c->new_val[2]; + int gid = (int)c->new_val[3]; 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); + switch ((int)c->old_val[0]) { + case SHAPE_CIRCLE: + s = shape_circle(cx, cy, sx); + break; + case SHAPE_RECTANGLE: + s = shape_rectangle(cx, cy, sx * 2.0f, c->new_val[1] * 2.0f); + break; + case SHAPE_STAR: { + int points = (int)c->old_val[3] / 2; + s = shape_star(cx, cy, sx, c->new_val[1], points); + break; + } + default: { + memset(&s, 0, sizeof(s)); + s.kind = (int)c->old_val[0]; + s.cx = cx; + s.cy = cy; + s.num_verts = (uint32_t)c->old_val[3]; + s.num_elements = (uint32_t)c->vertex_count; + s.sx = sx; + s.sy = c->new_val[1]; + 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; + } + } + s.rotation = rot; + s.group_id = gid; shape_build_transform(&s); - shape_make_buffers(&s); return s; } diff --git a/src/input.h b/src/input.h index ef7c48b..47714bc 100644 --- a/src/input.h +++ b/src/input.h @@ -55,7 +55,7 @@ static void handle_left_down_resize_begin(userdata_t *ud, float wx, float wy, in 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++; } + if (s->selected) { sum_sin += s->sin_r; sum_cos += s->cos_r; sel_n++; } } ud->interact.resize.angle = atan2f(sum_sin, sum_cos); @@ -65,7 +65,7 @@ static void handle_left_down_resize_begin(userdata_t *ud, float wx, float wy, in 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 sc = s->cos_r, ss = s->sin_r; 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; @@ -92,6 +92,12 @@ static void handle_left_down_rotate_begin(userdata_t *ud, float wx, float wy) wy - ud->interact.rotate.center_y, wx - ud->interact.rotate.center_x); ud->interact.rotate.total_delta = 0.0f; + + ud->interact.drag_indices.count = 0; + for (int i = 0; i < ud->shapes.count; i++) { + if (((shape_t*) vec_get(&ud->shapes, i))->selected) + *(int*) vec_push(&ud->interact.drag_indices) = i; + } } static void handle_left_down_move_begin(userdata_t *ud, float wx, float wy) @@ -101,6 +107,12 @@ static void handle_left_down_move_begin(userdata_t *ud, float wx, float wy) ud->interact.move.start_wy = wy; ud->interact.move.total_dx = 0; ud->interact.move.total_dy = 0; + + ud->interact.drag_indices.count = 0; + for (int i = 0; i < ud->shapes.count; i++) { + if (((shape_t*) vec_get(&ud->shapes, i))->selected) + *(int*) vec_push(&ud->interact.drag_indices) = i; + } } static void handle_left_down_select_or_marquee(userdata_t *ud, const sapp_event *event, float wx, float wy, float tol) @@ -170,41 +182,37 @@ static void handle_resize_end(userdata_t *ud) 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++; - } - + int n = ud->interact.drag_indices.count; + if (n > 0 && ud->interact.rotate.total_delta != 0.0f) { 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); + history_batch_init(&batch, n * 2); - for (int i = 0; i < ud->shapes.count; i++) { + for (int j = 0; j < n; j++) { + int i = *(int*) vec_get(&ud->interact.drag_indices, j); 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; + 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_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; + ud->interact.drag_indices.count = 0; update_shape_states(ud); spatial_mark_dirty(&ud->spatial_grid); ud->interact.aabb_cached = false; @@ -213,28 +221,24 @@ static void handle_rotate_end(userdata_t *ud) 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++; - } - + int n = ud->interact.drag_indices.count; + if (n > 0 && (ud->interact.move.total_dx != 0.0f || ud->interact.move.total_dy != 0.0f)) { hist_batch_t batch; - history_batch_init(&batch, sel_count); + history_batch_init(&batch, n); - for (int i = 0; i < ud->shapes.count; i++) { + for (int j = 0; j < n; j++) { + int i = *(int*) vec_get(&ud->interact.drag_indices, j); 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_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; + ud->interact.drag_indices.count = 0; update_shape_states(ud); spatial_mark_dirty(&ud->spatial_grid); ud->interact.aabb_cached = false; @@ -306,7 +310,7 @@ static void handle_resize_drag(userdata_t *ud, const sapp_event *event) 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 sc = s->cos_r, ss = s->sin_r; float mlx = (wx - ini->init_cx) * sc + (wy - ini->init_cy) * ss; float mly = -(wx - ini->init_cx) * ss + (wy - ini->init_cy) * sc; @@ -351,17 +355,16 @@ static void handle_rotate_drag(userdata_t *ud, const sapp_event *event) float cx = ud->interact.rotate.center_x; float cy = ud->interact.rotate.center_y; - for (int i = 0; i < ud->shapes.count; i++) { + for (int j = 0; j < ud->interact.drag_indices.count; j++) { + int i = *(int*) vec_get(&ud->interact.drag_indices, j); 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); - } + 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; @@ -376,14 +379,13 @@ static void handle_move_drag(userdata_t *ud, const sapp_event *event) 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++) { + for (int j = 0; j < ud->interact.drag_indices.count; j++) { + int i = *(int*) vec_get(&ud->interact.drag_indices, j); 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); - } + s->cx += delta_x; + s->cy += delta_y; + shape_retranslate(s); + shape_set_state(s, false, true); } ud->interact.move.total_dx = dx; @@ -411,6 +413,35 @@ static void handle_marquee_drag(userdata_t *ud, const sapp_event *event) &ud->spatial_grid, &ud->shapes, min_x, min_y, max_x, max_y); + if (ud->interact.focused_group_id == 0) { + 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; + int topmost = get_topmost_group(&ud->groups, s->group_id); + bool found = false; + for (int j = 0; j < n_gids; j++) { + if (gids[j] == topmost) { found = true; break; } + } + if (!found) gids[n_gids++] = topmost; + } + for (int j = 0; j < n_gids; j++) { + 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, gids[j], &ud->groups)) + s->selected = true; + } + } + FREE(gids); + ud->interact.selected_count = 0; + for (int i = 0; i < ud->shapes.count; i++) { + if (((shape_t*) vec_get(&ud->shapes, i))->selected) + ud->interact.selected_count++; + } + } + 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); @@ -435,16 +466,179 @@ static void handle_hover(userdata_t *ud, const sapp_event *event) if (hovered >= 0) { shape_t *hs = (shape_t*) vec_get(&ud->shapes, hovered); hovered_gid = hs->group_id; + if (hovered_gid != 0 && ud->interact.focused_group_id == 0) + hovered_gid = get_topmost_group(&ud->groups, hovered_gid); } 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); + hovered_gid != 0 && + is_shape_in_group_hierarchy(s->group_id, hovered_gid, &ud->groups)); shape_set_state(s, (i == hovered || in_group), s->selected); } } +// -- clipboard -- + +static shape_t clipboard_deep_copy_shape(const shape_t *src) +{ + shape_t dst = *src; + dst.verts = (shape_vertex_t*) ALLOC((size_t)src->num_elements * sizeof(shape_vertex_t)); + dst.indices = (uint16_t*) ALLOC((size_t)src->num_elements * sizeof(uint16_t)); + memcpy(dst.verts, src->verts, (size_t)src->num_elements * sizeof(shape_vertex_t)); + memcpy(dst.indices, src->indices, (size_t)src->num_elements * sizeof(uint16_t)); + return dst; +} + +static void clipboard_clear(clipboard_t *cb) +{ + for (int i = 0; i < cb->shape_count; i++) { + FREE(cb->shapes[i].verts); + FREE(cb->shapes[i].indices); + } + FREE(cb->shapes); + FREE(cb->groups); + memset(cb, 0, sizeof(*cb)); +} + +static int clipboard_lookup_gid(int old_id, const int *map, int map_count) +{ + for (int j = 0; j < map_count; j++) { + if (map[j * 2] == old_id) return map[j * 2 + 1]; + } + return 0; +} + +static void handle_copy(userdata_t *ud) +{ + if (ud->interact.selected_count == 0) return; + + clipboard_clear(&ud->clipboard); + + int n = ud->shapes.count; + int sel = 0; + for (int i = 0; i < n; i++) { + if (((shape_t*) vec_get(&ud->shapes, i))->selected) sel++; + } + + ud->clipboard.shapes = (shape_t*) ALLOC((size_t)sel * sizeof(shape_t)); + ud->clipboard.shape_count = 0; + + int *gids = (int*) ALLOC((size_t)n * sizeof(int)); + int n_gids = 0; + + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(&ud->shapes, i); + if (!s->selected) continue; + + ud->clipboard.shapes[ud->clipboard.shape_count++] = clipboard_deep_copy_shape(s); + + int gid = s->group_id; + while (gid != 0) { + bool found = false; + for (int j = 0; j < n_gids; j++) { + if (gids[j] == gid) { found = true; break; } + } + if (!found) gids[n_gids++] = gid; + group_t *g = find_group(&ud->groups, gid); + gid = g ? g->parent_id : 0; + } + } + + ud->clipboard.groups = (group_t*) ALLOC((size_t)n_gids * sizeof(group_t)); + ud->clipboard.group_count = n_gids; + for (int j = 0; j < n_gids; j++) { + group_t *src = find_group(&ud->groups, gids[j]); + ud->clipboard.groups[j] = *src; + } + + FREE(gids); +} + +static void handle_paste(userdata_t *ud) +{ + clipboard_t *cb = &ud->clipboard; + if (cb->shape_count == 0) return; + + int gc = cb->group_count; + int *gid_map = NULL; + if (gc > 0) { + gid_map = (int*) ALLOC((size_t)gc * 2 * sizeof(int)); + for (int j = 0; j < gc; j++) { + gid_map[j * 2] = cb->groups[j].id; + gid_map[j * 2 + 1] = ud->next_group_id++; + } + for (int j = 0; j < gc; j++) { + group_t g = cb->groups[j]; + g.id = clipboard_lookup_gid(g.id, gid_map, gc); + g.parent_id = clipboard_lookup_gid(g.parent_id, gid_map, gc); + *((group_t*) vec_push(&ud->groups)) = g; + } + group_index_rebuild(&ud->groups); + } + + for (int i = 0; i < ud->shapes.count; i++) + ((shape_t*) vec_get(&ud->shapes, i))->selected = false; + ud->interact.selected_count = 0; + + float cx, cy; + screen_to_world(&ud->camera, ud->mouse_x, ud->mouse_y, &cx, &cy); + + float cb_min_x = 0, cb_min_y = 0, cb_max_x = 0, cb_max_y = 0; + for (int i = 0; i < cb->shape_count; i++) { + shape_t *s = &cb->shapes[i]; + float sc = s->cos_r, ss = s->sin_r; + 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 (i == 0 && v == 0) { + cb_min_x = cb_max_x = wx; + cb_min_y = cb_max_y = wy; + } else { + if (wx < cb_min_x) cb_min_x = wx; + if (wx > cb_max_x) cb_max_x = wx; + if (wy < cb_min_y) cb_min_y = wy; + if (wy > cb_max_y) cb_max_y = wy; + } + } + } + float cb_cx = (cb_min_x + cb_max_x) * 0.5f; + float cb_cy = (cb_min_y + cb_max_y) * 0.5f; + + int sc = cb->shape_count; + hist_batch_t batch; + history_batch_init(&batch, sc); + + for (int i = 0; i < sc; i++) { + shape_t s = clipboard_deep_copy_shape(&cb->shapes[i]); + s.cx += cx - cb_cx; + s.cy += cy - cb_cy; + if (gc > 0) + s.group_id = clipboard_lookup_gid(s.group_id, gid_map, gc); + else + s.group_id = 0; + s.selected = true; + ud->interact.selected_count++; + + *((shape_t*) vec_push(&ud->shapes)) = s; + g_shape_pool_dirty = true; + history_batch_add_shape(&batch, ud->shapes.count - 1, HIST_CREATE, + (shape_t*) vec_get(&ud->shapes, ud->shapes.count - 1)); + } + + history_batch_commit(&batch, &ud->history); + FREE(gid_map); + + spatial_mark_dirty(&ud->spatial_grid); + ud->interact.aabb_cached = false; + ud->interact.focused_group_id = 0; + ud->overlay_upload_needed = true; + update_shape_states(ud); +} + // -- public event handlers -- static bool handle_key_down(userdata_t *ud, const sapp_event *event) @@ -454,6 +648,7 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) if (history_undo(&ud->history, &ud->shapes)) { rebuild_groups_from_shapes(&ud->groups, &ud->shapes); ud->interact.hovered_shape = -1; + g_shape_pool_dirty = true; spatial_mark_dirty(&ud->spatial_grid); ud->interact.aabb_cached = false; ud->overlay_upload_needed = true; @@ -464,12 +659,21 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) if (history_redo(&ud->history, &ud->shapes)) { rebuild_groups_from_shapes(&ud->groups, &ud->shapes); ud->interact.hovered_shape = -1; + g_shape_pool_dirty = true; spatial_mark_dirty(&ud->spatial_grid); ud->interact.aabb_cached = false; ud->overlay_upload_needed = true; } return true; } + if (event->key_code == SAPP_KEYCODE_C) { + handle_copy(ud); + return true; + } + if (event->key_code == SAPP_KEYCODE_V) { + handle_paste(ud); + return true; + } if (event->key_code == SAPP_KEYCODE_G) { if (event->modifiers & SAPP_MODIFIER_SHIFT) { // Ungroup: collect unique group IDs of selected shapes @@ -549,6 +753,8 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) } } + group_index_rebuild(&ud->groups); + FREE(parents); FREE(gids); ud->ui.list_last_shape = -1; @@ -624,6 +830,7 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) group_t new_grp = { .id = gid, .parent_id = 0 }; *((group_t*) vec_push(&ud->groups)) = new_grp; + group_index_rebuild(&ud->groups); for (int j = 0; j < n_full; j++) { for (int g = 0; g < ud->groups.count; g++) { @@ -683,6 +890,7 @@ static bool handle_key_down(userdata_t *ud, const sapp_event *event) shape_shutdown(s); vec_remove_ordered(&ud->shapes, indices[j]); } + g_shape_pool_dirty = true; FREE(indices); @@ -853,6 +1061,9 @@ static void handle_mouse_up(userdata_t *ud, const sapp_event *event) static void handle_mouse_move(userdata_t *ud, const sapp_event *event) { + ud->mouse_x = event->mouse_x; + ud->mouse_y = event->mouse_y; + if (ud->camera.pan_state.dragging) { handle_pan_drag(ud, event); } else if (ud->interact.resize.dragging) { diff --git a/src/interact.h b/src/interact.h index b1c97a7..0360207 100644 --- a/src/interact.h +++ b/src/interact.h @@ -11,7 +11,7 @@ static void selected_aabb(userdata_t *ud, float *min_x, float *min_y, 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); + float sc = s->cos_r, ss = s->sin_r; 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; @@ -117,6 +117,7 @@ static void rebuild_groups_from_shapes(vector_t *groups, vector_t *shapes) } if (saved) FREE(saved); + group_index_rebuild(groups); } static int hit_test_resize_handles(userdata_t *ud, float wx, float wy, float tol) diff --git a/src/main.c b/src/main.c index db1bf3c..33aa624 100644 --- a/src/main.c +++ b/src/main.c @@ -34,6 +34,16 @@ static void log_capture(const char* tag, uint32_t log_level, uint32_t log_item, if (log_level <= 1) ud->ui.log_show = true; } +static void panel_log_impl(void *ud_v, int level, const char *msg) { + userdata_t *ud = (userdata_t*)ud_v; + int idx = ud->ui.log_head; + strncpy(ud->ui.log_ring[idx].text, msg, 255); + ud->ui.log_ring[idx].text[255] = 0; + ud->ui.log_ring[idx].level = (uint32_t)level; + ud->ui.log_head = (idx + 1) % LOG_RING_SIZE; + if (ud->ui.log_count < LOG_RING_SIZE) ud->ui.log_count++; +} + static void meter_fps(userdata_t *ud) { float dt = (float)sapp_frame_duration(); @@ -102,10 +112,14 @@ static void init(void* _userdata) userdata_t* ud = (userdata_t*) _userdata; + g_panel_log_fn = panel_log_impl; + g_panel_log_ud = ud; + sg_desc sgdesc = { .environment = sglue_environment(), .logger.func = log_capture, .logger.user_data = ud, + .uniform_buffer_size = 16 * 1024 * 1024, }; sg_setup(&sgdesc); if (!sg_isvalid()) { @@ -216,6 +230,7 @@ static void init(void* _userdata) vec_init(&ud->shapes, sizeof(shape_t)); vec_init(&ud->groups, sizeof(group_t)); + vec_init(&ud->interact.drag_indices, sizeof(int)); spatial_init(&ud->spatial_grid); ud->interact.selected_count = 0; ud->interact.hovered_shape = -1; @@ -296,7 +311,7 @@ static void init(void* _userdata) EM_ASM({ window.addEventListener('keydown', function(e) { if (e.ctrlKey && !e.altKey && !e.metaKey) { - if (e.key === 'z' || e.key === 'y') { + if (e.key === 'z' || e.key === 'y' || e.key === 'c' || e.key === 'v') { e.preventDefault(); } } @@ -316,6 +331,8 @@ static void cleanup(void* _userdata) spatial_destroy(&ud->spatial_grid); vec_free(&ud->shapes); vec_free(&ud->groups); + vec_free(&ud->interact.drag_indices); + group_index_shutdown(); history_destroy(&ud->history); if (ud->interact.resize.init) FREE(ud->interact.resize.init); sg_destroy_buffer(ud->rect_vbuf); @@ -329,6 +346,13 @@ static void cleanup(void* _userdata) shape_pool_shutdown(); shape_shutdown_pipeline(); + for (int i = 0; i < ud->clipboard.shape_count; i++) { + FREE(ud->clipboard.shapes[i].verts); + FREE(ud->clipboard.shapes[i].indices); + } + FREE(ud->clipboard.shapes); + FREE(ud->clipboard.groups); + FREE(ud); simgui_shutdown(); diff --git a/src/overlay.h b/src/overlay.h index e20795b..c22a676 100644 --- a/src/overlay.h +++ b/src/overlay.h @@ -27,7 +27,26 @@ static void compute_overlay_geometry(userdata_t *ud, overlay_verts[4] = (shape_vertex_t){x1, y1}; *has_overlay = true; } else if (ud->interact.selected_count >= 1) { - if (ud->interact.selected_count == 1) { + if (ud->interact.move.dragging && ud->interact.aabb_cached) { + float dx = ud->interact.move.total_dx; + float dy = ud->interact.move.total_dy; + float omin_x = ud->interact.cached_aabb[0] + dx; + float omin_y = ud->interact.cached_aabb[1] + dy; + float omax_x = ud->interact.cached_aabb[2] + dx; + float omax_y = ud->interact.cached_aabb[3] + dy; + float pad = 8.0f / ud->camera.zoom; + overlay_verts[0] = (shape_vertex_t){omin_x - pad, omin_y - pad}; + overlay_verts[1] = (shape_vertex_t){omax_x + pad, omin_y - pad}; + overlay_verts[2] = (shape_vertex_t){omax_x + pad, omax_y + pad}; + overlay_verts[3] = (shape_vertex_t){omin_x - pad, omax_y + pad}; + overlay_verts[4] = overlay_verts[0]; + *sel_cx = (omin_x + omax_x) * 0.5f; + *sel_cy = (omin_y + omax_y) * 0.5f; + *sel_hw = (omax_x - omin_x) * 0.5f + pad; + *sel_hh = (omax_y - omin_y) * 0.5f + pad; + *sel_angle = 0; + *has_overlay = true; + } else 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; @@ -52,8 +71,8 @@ static void compute_overlay_geometry(userdata_t *ud, 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); + sum_sin += s->sin_r; + sum_cos += s->cos_r; } 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]; @@ -102,10 +121,19 @@ static void upload_overlay_buffers(userdata_t *ud, ud->interact.rotate.handle_radius = radius; const int n = HANDLE_CIRCLE_SEGMENTS + 1; + static shape_vertex_t unit_circle[HANDLE_CIRCLE_SEGMENTS + 1]; + static bool unit_circle_ready = false; + if (!unit_circle_ready) { + for (int i = 0; i < n; i++) { + float a = (float)i / (float)HANDLE_CIRCLE_SEGMENTS * 2.0f * GLM_PIf; + unit_circle[i] = (shape_vertex_t){cosf(a), sinf(a)}; + } + unit_circle_ready = true; + } 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}; + hv[i] = (shape_vertex_t){sel_cx + unit_circle[i].x * radius, + sel_cy + unit_circle[i].y * radius}; } if (need_upload) sg_update_buffer(ud->handle_vbuf, &(sg_range){hv, sizeof(hv)}); diff --git a/src/render.h b/src/render.h index 7f881b5..b0a4ae0 100644 --- a/src/render.h +++ b/src/render.h @@ -5,6 +5,8 @@ static sg_pipeline shape_pipeline; static sg_shader shape_shader; +static sg_pipeline overlay_pipeline; +static sg_shader overlay_shader; static int g_shape_frame_id; static void shape_begin_frame(void) @@ -14,13 +16,14 @@ static void shape_begin_frame(void) static void shape_init_pipeline(void) { - shape_shader = sg_make_shader(&(sg_shader_desc) { + // Overlay shader/pipeline (simple, no storage buffers) + overlay_shader = sg_make_shader(&(sg_shader_desc) { .vertex_func = { - .source = (const char*) src_shaders_shape_wgsl, + .source = (const char*) src_shaders_overlay_wgsl, .entry = "vs_main", }, .fragment_func = { - .source = (const char*) src_shaders_shape_wgsl, + .source = (const char*) src_shaders_overlay_wgsl, .entry = "fs_main", }, .uniform_blocks = { @@ -38,31 +41,79 @@ static void shape_init_pipeline(void) .attrs = { [0] = { .base_type = SG_SHADERATTRBASETYPE_FLOAT }, }, + .label = "Overlay shader", + }); + panel_log(3, "[shapes] overlay shader id=%d valid=%d", overlay_shader.id, sg_isvalid()); + + overlay_pipeline = sg_make_pipeline(&(sg_pipeline_desc) { + .shader = overlay_shader, + .index_type = SG_INDEXTYPE_UINT16, + .primitive_type = SG_PRIMITIVETYPE_LINE_STRIP, + .layout.attrs = { + [0].format = SG_VERTEXFORMAT_FLOAT2, + }, + .label = "Overlay pipeline", + }); + panel_log(3, "[shapes] overlay pipeline id=%d valid=%d", overlay_pipeline.id, sg_isvalid()); + + // Shape shader/pipeline (storage buffers, instanced) + shape_shader = sg_make_shader(&(sg_shader_desc) { + .vertex_func = { + .source = (const char*) src_shaders_shape_wgsl, + .entry = "vs_main", + }, + .fragment_func = { + .source = (const char*) src_shaders_shape_wgsl, + .entry = "fs_main", + }, + .uniform_blocks = { + [0] = { + .size = 80, + .stage = SG_SHADERSTAGE_VERTEX, + .wgsl_group0_binding_n = 0, + }, + }, + .views = { + [0] = { + .storage_buffer = { + .stage = SG_SHADERSTAGE_VERTEX, + .readonly = true, + .wgsl_group1_binding_n = 0, + }, + }, + [1] = { + .storage_buffer = { + .stage = SG_SHADERSTAGE_VERTEX, + .readonly = true, + .wgsl_group1_binding_n = 1, + }, + }, + }, + .attrs = { + [0] = { .base_type = SG_SHADERATTRBASETYPE_FLOAT }, + }, .label = "Shape shader", }); + panel_log(3, "[shapes] shader id=%d valid=%d", shape_shader.id, sg_isvalid()); shape_pipeline = sg_make_pipeline(&(sg_pipeline_desc) { .shader = shape_shader, - .index_type = SG_INDEXTYPE_UINT16, + .index_type = SG_INDEXTYPE_NONE, .primitive_type = SG_PRIMITIVETYPE_LINE_STRIP, .layout.attrs = { [0].format = SG_VERTEXFORMAT_FLOAT2, }, .label = "Shape pipeline", }); + panel_log(3, "[shapes] pipeline id=%d valid=%d", shape_pipeline.id, sg_isvalid()); } static void shape_shutdown_pipeline(void) { sg_destroy_pipeline(shape_pipeline); sg_destroy_shader(shape_shader); -} - -static void shape_draw(shape_t *s, const mat4 *mvp) -{ - sg_apply_uniforms(0, &SG_RANGE(*mvp)); - sg_apply_uniforms(1, &SG_RANGE(s->uniform)); - sg_draw((int)s->index_base, (int)s->num_elements, 1); + sg_destroy_pipeline(overlay_pipeline); + sg_destroy_shader(overlay_shader); } #endif diff --git a/src/shaders/overlay.wgsl b/src/shaders/overlay.wgsl new file mode 100644 index 0000000..e76259a --- /dev/null +++ b/src/shaders/overlay.wgsl @@ -0,0 +1,44 @@ +struct VsUniform { + mvp: mat4x4f, +}; + +struct ShapeUniform { + transform: mat4x4f, + state: u32, +}; + +struct VsIn { + @location(0) position: vec2f, +}; + +struct Vs2Fs { + @builtin(position) pos: vec4f, + @location(0) @interpolate(linear) color: vec4f, +}; + +struct FsOut { + @location(0) color: vec4f, +}; + +@binding(0) @group(0) var vs_uniforms: VsUniform; +@binding(1) @group(0) var shape_uniform: ShapeUniform; + +@vertex fn vs_main(input: VsIn) -> Vs2Fs { + var output: Vs2Fs; + let world_pos = shape_uniform.transform * vec4f(input.position.x, input.position.y, 0.0, 1.0); + output.pos = vs_uniforms.mvp * world_pos; + if (shape_uniform.state == 2u) { + output.color = vec4f(1.0, 0.84, 0.0, 1.0); + } else if (shape_uniform.state == 1u) { + output.color = vec4f(0.5, 0.6, 1.0, 1.0); + } else { + output.color = vec4f(0.8, 0.8, 0.8, 1.0); + } + return output; +} + +@fragment fn fs_main(input: Vs2Fs) -> FsOut { + var output: FsOut; + output.color = input.color; + return output; +} diff --git a/src/shaders/shape.wgsl b/src/shaders/shape.wgsl index e76259a..4d4a3d0 100644 --- a/src/shaders/shape.wgsl +++ b/src/shaders/shape.wgsl @@ -1,13 +1,20 @@ struct VsUniform { mvp: mat4x4f, + instance_base: u32, }; -struct ShapeUniform { +struct ShapeData { transform: mat4x4f, state: u32, }; +@binding(0) @group(0) var vs_uniforms: VsUniform; + +@binding(0) @group(1) var shape_data: array; +@binding(1) @group(1) var instance_map: array; + struct VsIn { + @builtin(instance_index) instance_idx: u32, @location(0) position: vec2f, }; @@ -20,16 +27,15 @@ struct FsOut { @location(0) color: vec4f, }; -@binding(0) @group(0) var vs_uniforms: VsUniform; -@binding(1) @group(0) var shape_uniform: ShapeUniform; - @vertex fn vs_main(input: VsIn) -> Vs2Fs { var output: Vs2Fs; - let world_pos = shape_uniform.transform * vec4f(input.position.x, input.position.y, 0.0, 1.0); + let shape_idx = instance_map[vs_uniforms.instance_base + input.instance_idx]; + let shape = shape_data[shape_idx]; + let world_pos = shape.transform * vec4f(input.position.x, input.position.y, 0.0, 1.0); output.pos = vs_uniforms.mvp * world_pos; - if (shape_uniform.state == 2u) { + if (shape.state == 2u) { output.color = vec4f(1.0, 0.84, 0.0, 1.0); - } else if (shape_uniform.state == 1u) { + } else if (shape.state == 1u) { output.color = vec4f(0.5, 0.6, 1.0, 1.0); } else { output.color = vec4f(0.8, 0.8, 0.8, 1.0); diff --git a/src/shape.h b/src/shape.h index c1b65f3..fa59d5b 100644 --- a/src/shape.h +++ b/src/shape.h @@ -20,6 +20,12 @@ typedef struct shape_uniform_t { uint8_t _pad[12]; } shape_uniform_t; +typedef struct { + mat4 transform; + uint32_t state; + uint8_t _pad[12]; +} shape_gpu_data_t; + typedef struct shape_t { shape_vertex_t *verts; uint16_t *indices; @@ -32,11 +38,9 @@ typedef struct shape_t { float cx, cy; float sx, sy; float rotation; + float cos_r, sin_r; int kind; - uint32_t vertex_base; - uint32_t index_base; - int group_id; } shape_t; @@ -47,15 +51,55 @@ typedef struct { int parent_id; // 0 = top-level group } group_t; -static group_t* find_group(vector_t *groups, int id) { +static group_t **g_group_by_id = NULL; +static int g_group_by_id_cap = 0; + +static void group_index_rebuild(vector_t *groups) +{ + int max_id = 0; + for (int i = 0; i < groups->count; i++) { + int gid = ((group_t*) vec_get(groups, i))->id; + if (gid > max_id) max_id = gid; + } + if (max_id >= g_group_by_id_cap) { + if (g_group_by_id) FREE(g_group_by_id); + int new_cap = max_id + 64; + g_group_by_id = (group_t**) ALLOC((size_t)new_cap * sizeof(group_t*)); + memset(g_group_by_id, 0, (size_t)new_cap * sizeof(group_t*)); + g_group_by_id_cap = new_cap; + } else { + for (int i = 0; i <= max_id; i++) g_group_by_id[i] = NULL; + } for (int i = 0; i < groups->count; i++) { group_t *g = (group_t*) vec_get(groups, i); - if (g->id == id) return g; + g_group_by_id[g->id] = g; } - return NULL; +} + +static void group_index_ensure_cap(int max_id) +{ + if (max_id >= g_group_by_id_cap) { + int new_cap = max_id + 64; + group_t **old = g_group_by_id; + g_group_by_id = (group_t**) ALLOC((size_t)new_cap * sizeof(group_t*)); + if (old) { + memcpy(g_group_by_id, old, (size_t)g_group_by_id_cap * sizeof(group_t*)); + FREE(old); + } + memset(g_group_by_id + g_group_by_id_cap, 0, + (size_t)(new_cap - g_group_by_id_cap) * sizeof(group_t*)); + g_group_by_id_cap = new_cap; + } +} + +static group_t* find_group(vector_t *groups, int id) { + (void)groups; + if (id <= 0 || id >= g_group_by_id_cap) return NULL; + return g_group_by_id[id]; } static int get_topmost_group(vector_t *groups, int gid) { + (void)groups; while (gid != 0) { group_t *g = find_group(groups, gid); if (!g || g->parent_id == 0) return gid; @@ -65,6 +109,7 @@ static int get_topmost_group(vector_t *groups, int gid) { } static bool is_shape_in_group_hierarchy(int shape_gid, int target_gid, vector_t *groups) { + (void)groups; int cur = shape_gid; while (cur != 0) { if (cur == target_gid) return true; @@ -75,74 +120,189 @@ static bool is_shape_in_group_hierarchy(int shape_gid, int target_gid, vector_t return false; } +static void group_index_shutdown(void) +{ + if (g_group_by_id) FREE(g_group_by_id); + g_group_by_id = NULL; + g_group_by_id_cap = 0; +} + // -- 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 sg_buffer g_shape_data_sbuf = {0}; +static sg_buffer g_instance_map_sbuf = {0}; +static sg_view g_shape_data_view = {0}; +static sg_view g_instance_map_view = {0}; + +// Per-group vertex buffers: one per unique num_elements +typedef struct { + uint32_t num_elements; + sg_buffer vbuf; +} shape_group_buf_t; +static shape_group_buf_t *g_shape_groups = NULL; +static int g_shape_group_count = 0; + static bool g_shape_pool_dirty; -static uint32_t g_shape_vert_count; -static uint32_t g_shape_idx_count; +static bool g_shape_data_dirty; +static size_t g_shape_data_buf_size = 0; +static int g_instance_map_capacity = 0; + +static void shape_make_view_for_buffer(sg_view *view, sg_buffer buf) +{ + if (view->id) sg_destroy_view(*view); + *view = sg_make_view(&(sg_view_desc){ + .storage_buffer = { .buffer = buf }, + }); +} + +static shape_gpu_data_t *g_upload_buf = NULL; +static int g_upload_buf_cap = 0; + +static void shape_upload_data(vector_t *shapes) +{ + int n = shapes->count; + if (n == 0 || !g_shape_data_sbuf.id) return; + + size_t need = (size_t)n * sizeof(shape_gpu_data_t); + if (need > g_shape_data_buf_size) { + panel_log(2, "[shapes] upload_data: buffer too small (%zu < %zu), forcing rebuild", + g_shape_data_buf_size, need); + g_shape_pool_dirty = true; + return; + } + + if (n > g_upload_buf_cap) { + if (g_upload_buf) FREE(g_upload_buf); + g_upload_buf = (shape_gpu_data_t*) ALLOC(need); + g_upload_buf_cap = n; + } + + for (int i = 0; i < n; i++) { + shape_t *s = (shape_t*) vec_get(shapes, i); + memcpy(g_upload_buf[i].transform, s->uniform.transform, sizeof(mat4)); + g_upload_buf[i].state = s->uniform.state; + memset(g_upload_buf[i]._pad, 0, sizeof(g_upload_buf[i]._pad)); + } + sg_update_buffer(g_shape_data_sbuf, &(sg_range){g_upload_buf, need}); +} + +static void shape_upload_instance_map(const uint32_t *map, int count) +{ + if (count > g_instance_map_capacity) { + if (g_instance_map_sbuf.id) sg_destroy_buffer(g_instance_map_sbuf); + g_instance_map_sbuf = sg_make_buffer(&(sg_buffer_desc){ + .size = (size_t)count * sizeof(uint32_t), + .usage = { .storage_buffer = true, .stream_update = true }, + .label = "Instance map", + }); + g_instance_map_capacity = count; + shape_make_view_for_buffer(&g_instance_map_view, g_instance_map_sbuf); + } + sg_update_buffer(g_instance_map_sbuf, &(sg_range){map, (size_t)count * sizeof(uint32_t)}); + panel_log(3, "[shapes] upload_instance_map: count=%d buf=%d view=%d", + count, g_instance_map_sbuf.id, g_instance_map_view.id); +} 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; + int n = shapes->count; + + // Destroy old groups + for (int i = 0; i < g_shape_group_count; i++) { + if (g_shape_groups[i].vbuf.id) sg_destroy_buffer(g_shape_groups[i].vbuf); } + FREE(g_shape_groups); + g_shape_groups = NULL; + g_shape_group_count = 0; - 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 (g_shape_data_sbuf.id) { sg_destroy_buffer(g_shape_data_sbuf); g_shape_data_sbuf.id = 0; } + if (g_shape_data_view.id) { sg_destroy_view(g_shape_data_view); g_shape_data_view.id = 0; } - if (total_verts == 0) { + if (n == 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)); + g_shape_data_buf_size = (size_t)n * sizeof(shape_gpu_data_t); + g_shape_data_sbuf = sg_make_buffer(&(sg_buffer_desc){ + .size = g_shape_data_buf_size, + .usage = { .storage_buffer = true, .stream_update = true }, + .label = "Shape data", + }); + shape_make_view_for_buffer(&g_shape_data_view, g_shape_data_sbuf); + // Data filled by shape_upload_data() in draw_shapes - 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; + // Count unique num_elements + uint32_t max_ne = 0; + for (int i = 0; i < n; i++) { + uint32_t ne = ((shape_t*) vec_get(shapes, i))->num_elements; + if (ne > max_ne) max_ne = ne; } - 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)", - }); + int *ne_seen = (int*) ALLOC((size_t)(max_ne + 1) * sizeof(int)); + memset(ne_seen, 0, (size_t)(max_ne + 1) * sizeof(int)); + for (int i = 0; i < n; i++) { + uint32_t ne = ((shape_t*) vec_get(shapes, i))->num_elements; + ne_seen[ne] = 1; + } + int group_count = 0; + for (uint32_t ne = 0; ne <= max_ne; ne++) + if (ne_seen[ne]) group_count++; - FREE(all_v); - FREE(all_i); + // Create per-group vertex buffers (one copy of vertex data per unique num_elements) + g_shape_groups = (shape_group_buf_t*) ALLOC((size_t)group_count * sizeof(shape_group_buf_t)); + memset(g_shape_groups, 0, (size_t)group_count * sizeof(shape_group_buf_t)); + + int gi = 0; + for (uint32_t ne = 0; ne <= max_ne; ne++) { + if (!ne_seen[ne]) continue; + + // Find first shape with this num_elements to use as vertex template + shape_t *ref = NULL; + for (int i = 0; i < n; i++) { + if (((shape_t*) vec_get(shapes, i))->num_elements == ne) { + ref = (shape_t*) vec_get(shapes, i); + break; + } + } + + g_shape_groups[gi].num_elements = ne; + g_shape_groups[gi].vbuf = sg_make_buffer(&(sg_buffer_desc){ + .data = { ref->verts, (size_t)ne * sizeof(shape_vertex_t) }, + .label = "Shape group verts", + }); + gi++; + } + g_shape_group_count = group_count; + + FREE(ne_seen); + + panel_log(3, "[shapes] pool_rebuild: %d shapes, %d groups, data_buf=%d data_view=%d", + n, group_count, g_shape_data_sbuf.id, g_shape_data_view.id); + for (int gi = 0; gi < group_count; gi++) { + panel_log(3, "[shapes] group[%d]: ne=%u vbuf=%d", + gi, g_shape_groups[gi].num_elements, g_shape_groups[gi].vbuf.id); + } - 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; + for (int i = 0; i < g_shape_group_count; i++) { + if (g_shape_groups[i].vbuf.id) sg_destroy_buffer(g_shape_groups[i].vbuf); + } + FREE(g_shape_groups); + g_shape_groups = NULL; + g_shape_group_count = 0; + + if (g_shape_data_view.id) { sg_destroy_view(g_shape_data_view); g_shape_data_view.id = 0; } + if (g_instance_map_view.id) { sg_destroy_view(g_instance_map_view); g_instance_map_view.id = 0; } + if (g_shape_data_sbuf.id) { sg_destroy_buffer(g_shape_data_sbuf); g_shape_data_sbuf.id = 0; } + if (g_instance_map_sbuf.id) { sg_destroy_buffer(g_instance_map_sbuf); g_instance_map_sbuf.id = 0; } + g_instance_map_capacity = 0; + if (g_upload_buf) { FREE(g_upload_buf); g_upload_buf = NULL; } + g_upload_buf_cap = 0; } #define SHAPE_HOVER_PX 6.0f @@ -172,11 +332,23 @@ static void shape_build_transform(shape_t *s) glm_scale_make(S, (vec3){s->sx, s->sy, 1.0f}); glm_mat4_mul(R, S, RS); glm_mat4_mul(T, RS, s->uniform.transform); + s->cos_r = R[0][0]; + s->sin_r = R[0][1]; + g_shape_data_dirty = true; +} + +static void shape_retranslate(shape_t *s) +{ + s->uniform.transform[3][0] = s->cx; + s->uniform.transform[3][1] = s->cy; + g_shape_data_dirty = true; } static void shape_make_buffers(shape_t *s) { - (void)s; + for (int i = 0; i < g_shape_group_count; i++) { + if (g_shape_groups[i].num_elements == s->num_elements) return; + } g_shape_pool_dirty = true; } @@ -194,9 +366,11 @@ static void shape_regenerate(shape_t *s) static void shape_set_state(shape_t *s, bool hovered, bool selected) { + uint32_t new_state = selected ? 2u : (hovered ? 1u : 0u); + if (s->uniform.state != new_state) g_shape_data_dirty = true; s->hovered = hovered; s->selected = selected; - s->uniform.state = selected ? 2u : (hovered ? 1u : 0u); + s->uniform.state = new_state; } static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t n) @@ -213,7 +387,7 @@ static bool point_in_polygon(float px, float py, shape_vertex_t *verts, uint32_t static bool shape_hit_test(shape_t *s, float wx, float wy, float world_tol) { - float sc = cosf(s->rotation), ss = sinf(s->rotation); + float sc = s->cos_r, ss = s->sin_r; float dx = wx - s->cx, dy = wy - s->cy; float lx = (dx * sc + dy * ss) / s->sx; float ly = (-dx * ss + dy * sc) / s->sy; diff --git a/src/spatial.h b/src/spatial.h index d3dd24a..51bc4e9 100644 --- a/src/spatial.h +++ b/src/spatial.h @@ -35,8 +35,8 @@ static int spatial_hash(int cx, int cy) static void spatial_compute_aabb(shape_t *s, float *min_x, float *min_y, float *max_x, float *max_y) { - float cos_r = cosf(s->rotation); - float sin_r = sinf(s->rotation); + float cos_r = s->cos_r; + float sin_r = s->sin_r; float hx = fabsf(cos_r) * s->sx + fabsf(sin_r) * s->sy; float hy = fabsf(sin_r) * s->sx + fabsf(cos_r) * s->sy; *min_x = s->cx - hx; @@ -184,52 +184,34 @@ static int spatial_query_rect_select(spatial_grid_t *grid, vector_t *shapes, } int selected_count = 0; - int min_cx = (int) floorf(min_x / SPATIAL_CELL_SIZE); - int min_cy = (int) floorf(min_y / SPATIAL_CELL_SIZE); - int max_cx = (int) floorf(max_x / SPATIAL_CELL_SIZE); - int max_cy = (int) floorf(max_y / SPATIAL_CELL_SIZE); + for (int s = 0; s < SPATIAL_HASH_SIZE; s++) { + if (!grid->slots[s].occupied) continue; + for (int e = 0; e < grid->slots[s].count; e++) { + spatial_entry_t *entry = &grid->slots[s].entries[e]; - for (int cell_x = min_cx; cell_x <= max_cx; cell_x++) { - for (int cell_y = min_cy; cell_y <= max_cy; cell_y++) { - int idx = spatial_hash(cell_x, cell_y) & (SPATIAL_HASH_SIZE - 1); - int probe_start = idx; + if (entry->max_x < min_x || entry->min_x > max_x || + entry->max_y < min_y || entry->min_y > max_y) + continue; - do { - if (!grid->slots[idx].occupied) break; + shape_t *shape = (shape_t*) vec_get(shapes, entry->shape_idx); + if (shape->selected) continue; - if (grid->slots[idx].cx == cell_x && grid->slots[idx].cy == cell_y) { - for (int e = 0; e < grid->slots[idx].count; e++) { - spatial_entry_t *entry = &grid->slots[idx].entries[e]; - - if (entry->max_x < min_x || entry->min_x > max_x || - entry->max_y < min_y || entry->min_y > max_y) - continue; - - shape_t *s = (shape_t*) vec_get(shapes, entry->shape_idx); - if (s->selected) continue; - - bool hit = (s->cx >= min_x && s->cx <= max_x && - s->cy >= min_y && s->cy <= max_y); - float sc = cosf(s->rotation), ss = sinf(s->rotation); - for (uint32_t v = 0; !hit && v < s->num_verts; v++) { - float lx = s->verts[v].x * s->sx; - float ly = s->verts[v].y * s->sy; - float wx = s->cx + lx * sc - ly * ss; - float wy = s->cy + lx * ss + ly * sc; - if (wx >= min_x && wx <= max_x && - wy >= min_y && wy <= max_y) - hit = true; - } - if (hit) { - s->selected = true; - selected_count++; - } - } - break; - } - - idx = (idx + 1) & (SPATIAL_HASH_SIZE - 1); - } while (idx != probe_start); + bool hit = (shape->cx >= min_x && shape->cx <= max_x && + shape->cy >= min_y && shape->cy <= max_y); + float sc = shape->cos_r, ss = shape->sin_r; + for (uint32_t v = 0; !hit && v < shape->num_verts; v++) { + float lx = shape->verts[v].x * shape->sx; + float ly = shape->verts[v].y * shape->sy; + float wx = shape->cx + lx * sc - ly * ss; + float wy = shape->cy + lx * ss + ly * sc; + if (wx >= min_x && wx <= max_x && + wy >= min_y && wy <= max_y) + hit = true; + } + if (hit) { + shape->selected = true; + selected_count++; + } } } return selected_count; diff --git a/src/types.h b/src/types.h index 8a9272f..98fcef8 100644 --- a/src/types.h +++ b/src/types.h @@ -89,6 +89,8 @@ typedef struct { int focused_group_id; double last_click_time; int last_click_shape_idx; + + vector_t drag_indices; } interact_state_t; typedef struct { @@ -112,6 +114,13 @@ typedef struct { int list_prev_count; } ui_state_t; +typedef struct { + shape_t *shapes; + int shape_count; + group_t *groups; + int group_count; +} clipboard_t; + typedef struct userdata_t { camera_t camera; renderer_t renderer; @@ -127,6 +136,8 @@ typedef struct userdata_t { sg_buffer corner_vbuf, corner_ibuf; int next_group_id; vector_t groups; + clipboard_t clipboard; + float mouse_x, mouse_y; double time; } userdata_t; diff --git a/src/ui_panels.h b/src/ui_panels.h index 6d7a243..7ba4920 100644 --- a/src/ui_panels.h +++ b/src/ui_panels.h @@ -352,8 +352,8 @@ static void draw_properties_panel(userdata_t *ud) (*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->verts[0].x * s->sx * s->cos_r - s->verts[0].y * s->sy * s->sin_r, + s->cy + s->verts[0].x * s->sx * s->sin_r + s->verts[0].y * s->sy * s->cos_r, s->cx, s->cy, s->sx, s->sy, s->rotation); if (igButton("Copy Debug", (ImVec2){0, 0})) sapp_set_clipboard_string(dbg); @@ -389,7 +389,9 @@ static void draw_log_panel(userdata_t *ud) 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); + EM_ASM({ + navigator.clipboard.writeText(UTF8ToString($0)); + }, buf); FREE(buf); } igSameLine(0.0f, 10.0f);