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);