Files
flecs_tests/diff_output.patch

1832 lines
74 KiB
Diff

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<uniform> vs_uniforms: VsUniform;
+
+@binding(0) @group(1) var<storage> shape_data: array<ShapeData>;
+@binding(1) @group(1) var<storage> instance_map: array<u32>;
+
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<uniform> vs_uniforms: VsUniform;
-@binding(1) @group(0) var<uniform> 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);