@jamesyong42/infinite-canvas 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +170 -9
  2. package/dist/SelectionRenderer-CR2PBQwx.d.cts +105 -0
  3. package/dist/SelectionRenderer-CR2PBQwx.d.cts.map +1 -0
  4. package/dist/SelectionRenderer-DlsBstAq.d.mts +105 -0
  5. package/dist/SelectionRenderer-DlsBstAq.d.mts.map +1 -0
  6. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs +3560 -0
  7. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs.map +1 -0
  8. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs +3375 -0
  9. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs.map +1 -0
  10. package/dist/advanced.cjs +110 -165
  11. package/dist/advanced.cjs.map +1 -1
  12. package/dist/advanced.d.cts +58 -40
  13. package/dist/advanced.d.cts.map +1 -0
  14. package/dist/advanced.d.mts +99 -0
  15. package/dist/advanced.d.mts.map +1 -0
  16. package/dist/advanced.mjs +105 -0
  17. package/dist/advanced.mjs.map +1 -0
  18. package/dist/devtools.cjs +654 -0
  19. package/dist/devtools.cjs.map +1 -0
  20. package/dist/devtools.d.cts +23 -0
  21. package/dist/devtools.d.cts.map +1 -0
  22. package/dist/devtools.d.mts +23 -0
  23. package/dist/devtools.d.mts.map +1 -0
  24. package/dist/devtools.mjs +652 -0
  25. package/dist/devtools.mjs.map +1 -0
  26. package/dist/engine-BfbvWXSk.d.mts +982 -0
  27. package/dist/engine-BfbvWXSk.d.mts.map +1 -0
  28. package/dist/engine-CCjuFMC-.d.cts +982 -0
  29. package/dist/engine-CCjuFMC-.d.cts.map +1 -0
  30. package/dist/hooks-BwY7rRHg.mjs +425 -0
  31. package/dist/hooks-BwY7rRHg.mjs.map +1 -0
  32. package/dist/hooks-DHShH86C.cjs +707 -0
  33. package/dist/hooks-DHShH86C.cjs.map +1 -0
  34. package/dist/index.cjs +909 -803
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +199 -67
  37. package/dist/index.d.cts.map +1 -0
  38. package/dist/index.d.mts +258 -0
  39. package/dist/index.d.mts.map +1 -0
  40. package/dist/index.mjs +855 -0
  41. package/dist/index.mjs.map +1 -0
  42. package/package.json +47 -15
  43. package/dist/SelectionRenderer-CeWSNZT8.d.cts +0 -891
  44. package/dist/SelectionRenderer-CeWSNZT8.d.ts +0 -891
  45. package/dist/advanced.d.ts +0 -81
  46. package/dist/advanced.js +0 -124
  47. package/dist/advanced.js.map +0 -1
  48. package/dist/chunk-VSHXWTJH.cjs +0 -3228
  49. package/dist/chunk-VSHXWTJH.cjs.map +0 -1
  50. package/dist/chunk-Z6JQQOWL.js +0 -3142
  51. package/dist/chunk-Z6JQQOWL.js.map +0 -1
  52. package/dist/index.d.ts +0 -126
  53. package/dist/index.js +0 -602
  54. package/dist/index.js.map +0 -1
@@ -0,0 +1,3560 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ const require_hooks = require("./hooks-DHShH86C.cjs");
24
+ let _jamesyong42_reactive_ecs = require("@jamesyong42/reactive-ecs");
25
+ let react = require("react");
26
+ let react_jsx_runtime = require("react/jsx-runtime");
27
+ let _react_three_fiber = require("@react-three/fiber");
28
+ let three = require("three");
29
+ let three$1 = __toESM(three, 1);
30
+ three = __toESM(three);
31
+ //#region src/archetype.ts
32
+ function createArchetypeRegistry(archetypes = []) {
33
+ const map = /* @__PURE__ */ new Map();
34
+ for (const a of archetypes) map.set(a.id, a);
35
+ return {
36
+ register(a) {
37
+ map.set(a.id, a);
38
+ },
39
+ get(id) {
40
+ return map.get(id) ?? null;
41
+ },
42
+ getAll() {
43
+ return [...map.values()];
44
+ }
45
+ };
46
+ }
47
+ //#endregion
48
+ //#region src/commands.ts
49
+ var CommandBuffer = class {
50
+ undoStack = [];
51
+ redoStack = [];
52
+ currentGroup = null;
53
+ /** Start grouping commands (e.g., on pointerdown). All commands until endGroup() are one undo step. */
54
+ beginGroup() {
55
+ if (this.currentGroup !== null) this.endGroup();
56
+ this.currentGroup = [];
57
+ }
58
+ /** Execute a command and record it for undo. */
59
+ execute(command, world) {
60
+ command.execute(world);
61
+ if (this.currentGroup) this.currentGroup.push(command);
62
+ else {
63
+ this.undoStack.push([command]);
64
+ this.redoStack.length = 0;
65
+ }
66
+ }
67
+ /** Close the current group — all commands since beginGroup() become one undo step. */
68
+ endGroup() {
69
+ if (this.currentGroup && this.currentGroup.length > 0) {
70
+ this.undoStack.push(this.currentGroup);
71
+ this.redoStack.length = 0;
72
+ }
73
+ this.currentGroup = null;
74
+ }
75
+ /** Undo the last command group. */
76
+ undo(world) {
77
+ if (this.currentGroup) this.endGroup();
78
+ const group = this.undoStack.pop();
79
+ if (!group) return false;
80
+ for (let i = group.length - 1; i >= 0; i--) group[i].undo(world);
81
+ this.redoStack.push(group);
82
+ return true;
83
+ }
84
+ /** Redo the last undone command group. */
85
+ redo(world) {
86
+ const group = this.redoStack.pop();
87
+ if (!group) return false;
88
+ for (const cmd of group) cmd.execute(world);
89
+ this.undoStack.push(group);
90
+ return true;
91
+ }
92
+ canUndo() {
93
+ return this.undoStack.length > 0 || this.currentGroup !== null && this.currentGroup.length > 0;
94
+ }
95
+ canRedo() {
96
+ return this.redoStack.length > 0;
97
+ }
98
+ clear() {
99
+ this.undoStack.length = 0;
100
+ this.redoStack.length = 0;
101
+ this.currentGroup = null;
102
+ }
103
+ get undoSize() {
104
+ return this.undoStack.length;
105
+ }
106
+ get redoSize() {
107
+ return this.redoStack.length;
108
+ }
109
+ };
110
+ var MoveCommand = class {
111
+ beforePositions = /* @__PURE__ */ new Map();
112
+ afterPositions = /* @__PURE__ */ new Map();
113
+ captured = false;
114
+ constructor(entityIds, dx, dy, transformType) {
115
+ this.entityIds = entityIds;
116
+ this.dx = dx;
117
+ this.dy = dy;
118
+ this.transformType = transformType;
119
+ }
120
+ execute(world) {
121
+ if (!this.captured) {
122
+ for (const id of this.entityIds) {
123
+ const t = world.getComponent(id, this.transformType);
124
+ if (t) {
125
+ this.beforePositions.set(id, {
126
+ x: t.x,
127
+ y: t.y
128
+ });
129
+ this.afterPositions.set(id, {
130
+ x: t.x + this.dx,
131
+ y: t.y + this.dy
132
+ });
133
+ }
134
+ }
135
+ this.captured = true;
136
+ }
137
+ for (const [id, pos] of this.afterPositions) world.setComponent(id, this.transformType, {
138
+ x: pos.x,
139
+ y: pos.y
140
+ });
141
+ }
142
+ undo(world) {
143
+ for (const [id, pos] of this.beforePositions) world.setComponent(id, this.transformType, {
144
+ x: pos.x,
145
+ y: pos.y
146
+ });
147
+ }
148
+ };
149
+ var ResizeCommand = class {
150
+ constructor(entityId, before, after, transformType) {
151
+ this.entityId = entityId;
152
+ this.before = before;
153
+ this.after = after;
154
+ this.transformType = transformType;
155
+ this.after = {
156
+ ...after,
157
+ width: Math.max(20, after.width),
158
+ height: Math.max(20, after.height)
159
+ };
160
+ }
161
+ execute(world) {
162
+ world.setComponent(this.entityId, this.transformType, this.after);
163
+ }
164
+ undo(world) {
165
+ world.setComponent(this.entityId, this.transformType, this.before);
166
+ }
167
+ };
168
+ var SetComponentCommand = class {
169
+ constructor(entityId, type, before, after) {
170
+ this.entityId = entityId;
171
+ this.type = type;
172
+ this.before = before;
173
+ this.after = after;
174
+ }
175
+ execute(world) {
176
+ world.setComponent(this.entityId, this.type, this.after);
177
+ }
178
+ undo(world) {
179
+ world.setComponent(this.entityId, this.type, this.before);
180
+ }
181
+ };
182
+ //#endregion
183
+ //#region src/math.ts
184
+ /** Convert WorldBounds-shaped data to AABB */
185
+ function worldBoundsToAABB(wb) {
186
+ return {
187
+ minX: wb.worldX,
188
+ minY: wb.worldY,
189
+ maxX: wb.worldX + wb.worldWidth,
190
+ maxY: wb.worldY + wb.worldHeight
191
+ };
192
+ }
193
+ /** Test if two AABBs overlap */
194
+ function intersectsAABB(a, b) {
195
+ return a.maxX >= b.minX && a.minX <= b.maxX && a.maxY >= b.minY && a.minY <= b.maxY;
196
+ }
197
+ /** Test if a point is inside an AABB */
198
+ function pointInAABB(px, py, a) {
199
+ return px >= a.minX && px <= a.maxX && py >= a.minY && py <= a.maxY;
200
+ }
201
+ /** Convert screen coordinates to world coordinates */
202
+ function screenToWorld(screenX, screenY, camera) {
203
+ return {
204
+ x: screenX / camera.zoom + camera.x,
205
+ y: screenY / camera.zoom + camera.y
206
+ };
207
+ }
208
+ /** Convert world coordinates to screen coordinates */
209
+ function worldToScreen(worldX, worldY, camera) {
210
+ return {
211
+ x: (worldX - camera.x) * camera.zoom,
212
+ y: (worldY - camera.y) * camera.zoom
213
+ };
214
+ }
215
+ /** Clamp a value between min and max */
216
+ function clamp(value, min, max) {
217
+ return Math.max(min, Math.min(max, value));
218
+ }
219
+ //#endregion
220
+ //#region src/profiler.ts
221
+ const TICK_RING_SIZE = 300;
222
+ const R3F_RING_SIZE = 300;
223
+ var Profiler = class {
224
+ enabled = false;
225
+ tickRing = [];
226
+ tickWrite = 0;
227
+ tickFilled = false;
228
+ r3fRing = [];
229
+ r3fWrite = 0;
230
+ r3fFilled = false;
231
+ frameStart = 0;
232
+ currentSystems = {};
233
+ visibilityMs = 0;
234
+ webglGridMs = 0;
235
+ webglSelectionMs = 0;
236
+ webglDrawCalls = 0;
237
+ webglTriangles = 0;
238
+ webglSelectionFrames = 0;
239
+ webglSnapGuides = 0;
240
+ webglSpacingIndicators = 0;
241
+ webglDomPositionsUpdated = 0;
242
+ currentTick = 0;
243
+ /** Enable/disable profiling. When disabled, all methods are no-ops. */
244
+ setEnabled(on) {
245
+ this.enabled = on;
246
+ if (!on) this.clear();
247
+ }
248
+ isEnabled() {
249
+ return this.enabled;
250
+ }
251
+ /** Call at the start of engine.tick(). */
252
+ beginFrame(tick) {
253
+ if (!this.enabled) return;
254
+ this.currentTick = tick;
255
+ this.currentSystems = {};
256
+ this.visibilityMs = 0;
257
+ this.webglGridMs = 0;
258
+ this.webglSelectionMs = 0;
259
+ this.webglDrawCalls = 0;
260
+ this.webglTriangles = 0;
261
+ this.webglSelectionFrames = 0;
262
+ this.webglSnapGuides = 0;
263
+ this.webglSpacingIndicators = 0;
264
+ this.webglDomPositionsUpdated = 0;
265
+ this.frameStart = performance.now();
266
+ performance.mark("ic-frame-start");
267
+ }
268
+ /** Call around each ECS system execution. */
269
+ beginSystem(name) {
270
+ if (!this.enabled) return;
271
+ performance.mark(`ic-sys-${name}-start`);
272
+ }
273
+ endSystem(name) {
274
+ if (!this.enabled) return;
275
+ performance.mark(`ic-sys-${name}-end`);
276
+ try {
277
+ const measure = performance.measure(`ic:sys:${name}`, `ic-sys-${name}-start`, `ic-sys-${name}-end`);
278
+ this.currentSystems[name] = measure.duration;
279
+ } catch {}
280
+ performance.clearMarks(`ic-sys-${name}-start`);
281
+ performance.clearMarks(`ic-sys-${name}-end`);
282
+ }
283
+ beginVisibility() {
284
+ if (!this.enabled) return;
285
+ performance.mark("ic-vis-start");
286
+ }
287
+ endVisibility() {
288
+ if (!this.enabled) return;
289
+ performance.mark("ic-vis-end");
290
+ try {
291
+ const measure = performance.measure("ic:visibility", "ic-vis-start", "ic-vis-end");
292
+ this.visibilityMs = measure.duration;
293
+ } catch {}
294
+ performance.clearMarks("ic-vis-start");
295
+ performance.clearMarks("ic-vis-end");
296
+ }
297
+ /** Call right before the named engine WebGL pass renders. */
298
+ beginWebGL(pass) {
299
+ if (!this.enabled) return;
300
+ performance.mark(`ic-gl-${pass}-start`);
301
+ }
302
+ /** Call right after the named engine WebGL pass renders. */
303
+ endWebGL(pass) {
304
+ if (!this.enabled) return;
305
+ performance.mark(`ic-gl-${pass}-end`);
306
+ try {
307
+ const measure = performance.measure(`ic:gl:${pass}`, `ic-gl-${pass}-start`, `ic-gl-${pass}-end`);
308
+ if (pass === "grid") this.webglGridMs = measure.duration;
309
+ else this.webglSelectionMs = measure.duration;
310
+ } catch {}
311
+ performance.clearMarks(`ic-gl-${pass}-start`);
312
+ performance.clearMarks(`ic-gl-${pass}-end`);
313
+ }
314
+ /**
315
+ * Record WebGL engine pass counters for the current tick. `drawCalls` and
316
+ * `triangles` should be the totals from `renderer.info.render` accumulated
317
+ * across all engine passes in this tick (grid + selection). Callers must
318
+ * reset `renderer.info` at the start of the tick (with `autoReset=false`)
319
+ * so these values cover both passes.
320
+ */
321
+ recordWebGLStats(stats) {
322
+ if (!this.enabled) return;
323
+ this.webglDrawCalls = stats.drawCalls;
324
+ this.webglTriangles = stats.triangles;
325
+ this.webglSelectionFrames = stats.selectionFrames;
326
+ this.webglSnapGuides = stats.snapGuides;
327
+ this.webglSpacingIndicators = stats.spacingIndicators;
328
+ this.webglDomPositionsUpdated = stats.domPositionsUpdated;
329
+ }
330
+ /** Call at the end of engine.tick() — flushes a TickSample to the ring. */
331
+ endFrame(entityCount, visibleCount) {
332
+ if (!this.enabled) return;
333
+ performance.mark("ic-frame-end");
334
+ let totalMs;
335
+ try {
336
+ totalMs = performance.measure("ic:frame", "ic-frame-start", "ic-frame-end").duration;
337
+ } catch {
338
+ totalMs = performance.now() - this.frameStart;
339
+ }
340
+ performance.clearMarks("ic-frame-start");
341
+ performance.clearMarks("ic-frame-end");
342
+ const sample = {
343
+ tick: this.currentTick,
344
+ timestamp: performance.now(),
345
+ totalMs,
346
+ ecs: {
347
+ systems: { ...this.currentSystems },
348
+ visibilityMs: this.visibilityMs,
349
+ entityCount,
350
+ visibleCount
351
+ },
352
+ webgl: {
353
+ gridMs: this.webglGridMs,
354
+ selectionMs: this.webglSelectionMs,
355
+ drawCalls: this.webglDrawCalls,
356
+ triangles: this.webglTriangles,
357
+ selectionFrames: this.webglSelectionFrames,
358
+ snapGuides: this.webglSnapGuides,
359
+ spacingIndicators: this.webglSpacingIndicators,
360
+ domPositionsUpdated: this.webglDomPositionsUpdated
361
+ }
362
+ };
363
+ if (this.tickRing.length < TICK_RING_SIZE) this.tickRing.push(sample);
364
+ else this.tickRing[this.tickWrite] = sample;
365
+ this.tickWrite = (this.tickWrite + 1) % TICK_RING_SIZE;
366
+ if (this.tickRing.length >= TICK_RING_SIZE) this.tickFilled = true;
367
+ }
368
+ /**
369
+ * Push one R3F frame sample. Called from the R3F canvas via a probe
370
+ * component that has access to `useThree`.
371
+ */
372
+ recordR3FFrame(sample) {
373
+ if (!this.enabled) return;
374
+ const full = {
375
+ ...sample,
376
+ timestamp: performance.now()
377
+ };
378
+ if (this.r3fRing.length < R3F_RING_SIZE) this.r3fRing.push(full);
379
+ else this.r3fRing[this.r3fWrite] = full;
380
+ this.r3fWrite = (this.r3fWrite + 1) % R3F_RING_SIZE;
381
+ if (this.r3fRing.length >= R3F_RING_SIZE) this.r3fFilled = true;
382
+ }
383
+ /** Get the last N tick samples (newest first). */
384
+ getSamples(count) {
385
+ return readRing(this.tickRing, this.tickWrite, this.tickFilled, count);
386
+ }
387
+ /** Get the last N R3F samples (newest first). */
388
+ getR3FSamples(count) {
389
+ return readRing(this.r3fRing, this.r3fWrite, this.r3fFilled, count);
390
+ }
391
+ /** Compute rolling statistics across all three layers. */
392
+ getStats() {
393
+ return {
394
+ ecs: this.getEcsStats(),
395
+ webgl: this.getWebGLStats(),
396
+ r3f: this.getR3FStats()
397
+ };
398
+ }
399
+ getEcsStats() {
400
+ const samples = this.tickRing;
401
+ const n = samples.length;
402
+ if (n === 0) return {
403
+ fps: 0,
404
+ frameTime: {
405
+ avg: 0,
406
+ p50: 0,
407
+ p95: 0,
408
+ p99: 0,
409
+ max: 0
410
+ },
411
+ systemAvg: {},
412
+ systemP95: {},
413
+ budgetUsed: 0,
414
+ sampleCount: 0
415
+ };
416
+ const frameTimes = samples.map((s) => s.totalMs).sort((a, b) => a - b);
417
+ const avg = mean(frameTimes);
418
+ const fps = ringFps(samples, this.tickWrite, this.tickFilled, TICK_RING_SIZE);
419
+ const systemNames = /* @__PURE__ */ new Set();
420
+ for (const s of samples) for (const k of Object.keys(s.ecs.systems)) systemNames.add(k);
421
+ const systemAvg = {};
422
+ const systemP95 = {};
423
+ for (const name of systemNames) {
424
+ const times = samples.map((s) => s.ecs.systems[name] ?? 0).sort((a, b) => a - b);
425
+ systemAvg[name] = mean(times);
426
+ systemP95[name] = percentile(times, 95);
427
+ }
428
+ return {
429
+ fps,
430
+ frameTime: {
431
+ avg,
432
+ p50: percentile(frameTimes, 50),
433
+ p95: percentile(frameTimes, 95),
434
+ p99: percentile(frameTimes, 99),
435
+ max: frameTimes[frameTimes.length - 1]
436
+ },
437
+ systemAvg,
438
+ systemP95,
439
+ budgetUsed: avg / 16.67 * 100,
440
+ sampleCount: n
441
+ };
442
+ }
443
+ getWebGLStats() {
444
+ const samples = this.tickRing;
445
+ const n = samples.length;
446
+ if (n === 0) return {
447
+ fps: 0,
448
+ frameTime: {
449
+ avg: 0,
450
+ p50: 0,
451
+ p95: 0,
452
+ p99: 0,
453
+ max: 0
454
+ },
455
+ budgetUsed: 0,
456
+ gridAvg: 0,
457
+ gridP95: 0,
458
+ selectionAvg: 0,
459
+ selectionP95: 0,
460
+ avgDrawCalls: 0,
461
+ avgTriangles: 0,
462
+ avgSelectionFrames: 0,
463
+ avgSnapGuides: 0,
464
+ avgDomUpdates: 0,
465
+ sampleCount: 0
466
+ };
467
+ const gridTimes = samples.map((s) => s.webgl.gridMs).sort((a, b) => a - b);
468
+ const selTimes = samples.map((s) => s.webgl.selectionMs).sort((a, b) => a - b);
469
+ const combinedTimes = samples.map((s) => s.webgl.gridMs + s.webgl.selectionMs).sort((a, b) => a - b);
470
+ const combinedAvg = mean(combinedTimes);
471
+ return {
472
+ fps: ringFps(samples, this.tickWrite, this.tickFilled, TICK_RING_SIZE),
473
+ frameTime: {
474
+ avg: combinedAvg,
475
+ p50: percentile(combinedTimes, 50),
476
+ p95: percentile(combinedTimes, 95),
477
+ p99: percentile(combinedTimes, 99),
478
+ max: combinedTimes[combinedTimes.length - 1]
479
+ },
480
+ budgetUsed: combinedAvg / 16.67 * 100,
481
+ gridAvg: mean(gridTimes),
482
+ gridP95: percentile(gridTimes, 95),
483
+ selectionAvg: mean(selTimes),
484
+ selectionP95: percentile(selTimes, 95),
485
+ avgDrawCalls: mean(samples.map((s) => s.webgl.drawCalls)),
486
+ avgTriangles: mean(samples.map((s) => s.webgl.triangles)),
487
+ avgSelectionFrames: mean(samples.map((s) => s.webgl.selectionFrames)),
488
+ avgSnapGuides: mean(samples.map((s) => s.webgl.snapGuides)),
489
+ avgDomUpdates: mean(samples.map((s) => s.webgl.domPositionsUpdated)),
490
+ sampleCount: n
491
+ };
492
+ }
493
+ getR3FStats() {
494
+ const samples = this.r3fRing;
495
+ const n = samples.length;
496
+ if (n === 0) return {
497
+ fps: 0,
498
+ frameTime: {
499
+ avg: 0,
500
+ p50: 0,
501
+ p95: 0,
502
+ p99: 0,
503
+ max: 0
504
+ },
505
+ avgDrawCalls: 0,
506
+ avgTriangles: 0,
507
+ programs: 0,
508
+ geometries: 0,
509
+ textures: 0,
510
+ activeWidgets: 0,
511
+ sampleCount: 0
512
+ };
513
+ const dts = samples.map((s) => s.dtMs).sort((a, b) => a - b);
514
+ const fps = ringFps(samples, this.r3fWrite, this.r3fFilled, R3F_RING_SIZE);
515
+ const latest = samples[this.r3fFilled ? (this.r3fWrite - 1 + R3F_RING_SIZE) % R3F_RING_SIZE : n - 1];
516
+ return {
517
+ fps,
518
+ frameTime: {
519
+ avg: mean(dts),
520
+ p50: percentile(dts, 50),
521
+ p95: percentile(dts, 95),
522
+ p99: percentile(dts, 99),
523
+ max: dts[dts.length - 1]
524
+ },
525
+ avgDrawCalls: mean(samples.map((s) => s.drawCalls)),
526
+ avgTriangles: mean(samples.map((s) => s.triangles)),
527
+ programs: latest.programs,
528
+ geometries: latest.geometries,
529
+ textures: latest.textures,
530
+ activeWidgets: latest.activeWidgets,
531
+ sampleCount: n
532
+ };
533
+ }
534
+ /** Clear all collected data. */
535
+ clear() {
536
+ this.tickRing = [];
537
+ this.tickWrite = 0;
538
+ this.tickFilled = false;
539
+ this.r3fRing = [];
540
+ this.r3fWrite = 0;
541
+ this.r3fFilled = false;
542
+ }
543
+ };
544
+ function mean(xs) {
545
+ if (xs.length === 0) return 0;
546
+ let sum = 0;
547
+ for (const x of xs) sum += x;
548
+ return sum / xs.length;
549
+ }
550
+ function percentile(sorted, p) {
551
+ if (sorted.length === 0) return 0;
552
+ return sorted[Math.floor(p / 100 * (sorted.length - 1))] ?? 0;
553
+ }
554
+ function readRing(ring, write, filled, count) {
555
+ const n = ring.length;
556
+ if (n === 0) return [];
557
+ const take = Math.min(count ?? n, n);
558
+ const out = [];
559
+ for (let i = 0; i < take; i++) {
560
+ const idx = (write - 1 - i + n) % n;
561
+ out.push(ring[idx]);
562
+ }
563
+ return out;
564
+ }
565
+ function ringFps(ring, write, filled, size) {
566
+ const n = ring.length;
567
+ if (n < 2) return 0;
568
+ const newest = ring[filled ? (write - 1 + size) % size : n - 1];
569
+ const oldest = ring[filled ? write : 0];
570
+ const spanMs = newest.timestamp - oldest.timestamp;
571
+ return spanMs > 0 ? Math.round((n - 1) / spanMs * 1e3) : 0;
572
+ }
573
+ //#endregion
574
+ //#region src/react/registry.ts
575
+ function createWidgetRegistry(defs = []) {
576
+ const map = /* @__PURE__ */ new Map();
577
+ for (const def of defs) map.set(def.type, def);
578
+ return {
579
+ register(def) {
580
+ map.set(def.type, def);
581
+ },
582
+ get(type) {
583
+ return map.get(type) ?? null;
584
+ },
585
+ getAll() {
586
+ return [...map.values()];
587
+ }
588
+ };
589
+ }
590
+ /** Narrows to the R3F variant. */
591
+ function isR3FWidget(widget) {
592
+ return widget.surface === "webgl";
593
+ }
594
+ //#endregion
595
+ //#region src/snap.ts
596
+ /**
597
+ * Compute snap guides for a dragged entity against reference entities.
598
+ */
599
+ function computeSnapGuides(dragged, references, threshold) {
600
+ const guides = [];
601
+ const spacings = [];
602
+ let snapDx = 0;
603
+ let snapDy = 0;
604
+ const dLeft = dragged.x;
605
+ const dRight = dragged.x + dragged.width;
606
+ const dCenterX = dragged.x + dragged.width / 2;
607
+ const dTop = dragged.y;
608
+ const dBottom = dragged.y + dragged.height;
609
+ const dCenterY = dragged.y + dragged.height / 2;
610
+ let bestSnapX = Number.POSITIVE_INFINITY;
611
+ let bestSnapY = Number.POSITIVE_INFINITY;
612
+ let bestDx = 0;
613
+ let bestDy = 0;
614
+ const xGuides = [];
615
+ const yGuides = [];
616
+ for (const ref of references) {
617
+ const rLeft = ref.x;
618
+ const rRight = ref.x + ref.width;
619
+ const rCenterX = ref.x + ref.width / 2;
620
+ const rTop = ref.y;
621
+ const rBottom = ref.y + ref.height;
622
+ const rCenterY = ref.y + ref.height / 2;
623
+ const xPairs = [
624
+ [
625
+ dLeft,
626
+ rLeft,
627
+ "edge"
628
+ ],
629
+ [
630
+ dLeft,
631
+ rRight,
632
+ "edge"
633
+ ],
634
+ [
635
+ dRight,
636
+ rLeft,
637
+ "edge"
638
+ ],
639
+ [
640
+ dRight,
641
+ rRight,
642
+ "edge"
643
+ ],
644
+ [
645
+ dCenterX,
646
+ rCenterX,
647
+ "center"
648
+ ],
649
+ [
650
+ dLeft,
651
+ rCenterX,
652
+ "edge"
653
+ ],
654
+ [
655
+ dRight,
656
+ rCenterX,
657
+ "edge"
658
+ ]
659
+ ];
660
+ for (const [dVal, rVal, type] of xPairs) {
661
+ const dist = Math.abs(dVal - rVal);
662
+ if (dist <= threshold) {
663
+ const dx = rVal - dVal;
664
+ if (dist < bestSnapX) {
665
+ bestSnapX = dist;
666
+ bestDx = dx;
667
+ xGuides.length = 0;
668
+ }
669
+ if (dist <= bestSnapX + .01) xGuides.push({
670
+ axis: "x",
671
+ position: rVal,
672
+ type
673
+ });
674
+ }
675
+ }
676
+ const yPairs = [
677
+ [
678
+ dTop,
679
+ rTop,
680
+ "edge"
681
+ ],
682
+ [
683
+ dTop,
684
+ rBottom,
685
+ "edge"
686
+ ],
687
+ [
688
+ dBottom,
689
+ rTop,
690
+ "edge"
691
+ ],
692
+ [
693
+ dBottom,
694
+ rBottom,
695
+ "edge"
696
+ ],
697
+ [
698
+ dCenterY,
699
+ rCenterY,
700
+ "center"
701
+ ],
702
+ [
703
+ dTop,
704
+ rCenterY,
705
+ "edge"
706
+ ],
707
+ [
708
+ dBottom,
709
+ rCenterY,
710
+ "edge"
711
+ ]
712
+ ];
713
+ for (const [dVal, rVal, type] of yPairs) {
714
+ const dist = Math.abs(dVal - rVal);
715
+ if (dist <= threshold) {
716
+ const dy = rVal - dVal;
717
+ if (dist < bestSnapY) {
718
+ bestSnapY = dist;
719
+ bestDy = dy;
720
+ yGuides.length = 0;
721
+ }
722
+ if (dist <= bestSnapY + .01) yGuides.push({
723
+ axis: "y",
724
+ position: rVal,
725
+ type
726
+ });
727
+ }
728
+ }
729
+ }
730
+ const eqResult = computeEqualSpacing(dragged, references, threshold);
731
+ if (bestSnapX <= threshold) snapDx = bestDx;
732
+ else if (eqResult.snapDx !== void 0) snapDx = eqResult.snapDx;
733
+ if (bestSnapY <= threshold) snapDy = bestDy;
734
+ else if (eqResult.snapDy !== void 0) snapDy = eqResult.snapDy;
735
+ if (bestSnapX <= threshold) {
736
+ const seen = /* @__PURE__ */ new Set();
737
+ for (const g of xGuides) if (!seen.has(g.position)) {
738
+ seen.add(g.position);
739
+ guides.push(g);
740
+ }
741
+ }
742
+ if (bestSnapY <= threshold) {
743
+ const seen = /* @__PURE__ */ new Set();
744
+ for (const g of yGuides) if (!seen.has(g.position)) {
745
+ seen.add(g.position);
746
+ guides.push(g);
747
+ }
748
+ }
749
+ const eqFinal = computeEqualSpacing({
750
+ x: dragged.x + snapDx,
751
+ y: dragged.y + snapDy,
752
+ width: dragged.width,
753
+ height: dragged.height
754
+ }, references, threshold * .5);
755
+ spacings.push(...eqFinal.indicators);
756
+ return {
757
+ snapDx,
758
+ snapDy,
759
+ guides,
760
+ spacings
761
+ };
762
+ }
763
+ function computeEqualSpacing(dragged, references, threshold) {
764
+ const indicators = [];
765
+ let snapDx;
766
+ let snapDy;
767
+ const xResult = checkAxisSpacing(dragged, references, threshold, "x");
768
+ if (xResult) {
769
+ snapDx = xResult.snap;
770
+ indicators.push(...xResult.indicators);
771
+ }
772
+ const yResult = checkAxisSpacing(dragged, references, threshold, "y");
773
+ if (yResult) {
774
+ snapDy = yResult.snap;
775
+ indicators.push(...yResult.indicators);
776
+ }
777
+ return {
778
+ snapDx,
779
+ snapDy,
780
+ indicators
781
+ };
782
+ }
783
+ function checkAxisSpacing(dragged, references, threshold, axis) {
784
+ const isX = axis === "x";
785
+ const pos = (b) => isX ? b.x : b.y;
786
+ const size = (b) => isX ? b.width : b.height;
787
+ const perpPos = (b) => isX ? b.y : b.x;
788
+ const perpSize = (b) => isX ? b.height : b.width;
789
+ const end = (b) => pos(b) + size(b);
790
+ const neighbors = references.filter((ref) => perpPos(ref) < perpPos(dragged) + perpSize(dragged) && perpPos(ref) + perpSize(ref) > perpPos(dragged));
791
+ if (neighbors.length < 1) return null;
792
+ const sorted = [...neighbors].sort((a, b) => pos(a) - pos(b));
793
+ const refGaps = [];
794
+ for (let i = 0; i < sorted.length - 1; i++) {
795
+ const gap = pos(sorted[i + 1]) - end(sorted[i]);
796
+ if (gap > .1) refGaps.push({
797
+ from: sorted[i],
798
+ to: sorted[i + 1],
799
+ gap
800
+ });
801
+ }
802
+ let bestSnap = null;
803
+ let bestIndicators = [];
804
+ let bestDiff = Number.POSITIVE_INFINITY;
805
+ let leftN = null;
806
+ let rightN = null;
807
+ for (const ref of sorted) {
808
+ if (end(ref) <= pos(dragged) + threshold) {
809
+ if (!leftN || end(ref) > end(leftN)) leftN = ref;
810
+ }
811
+ if (pos(ref) >= end(dragged) - threshold) {
812
+ if (!rightN || pos(ref) < pos(rightN)) rightN = ref;
813
+ }
814
+ }
815
+ if (leftN && rightN) {
816
+ const lGap = pos(dragged) - end(leftN);
817
+ const rGap = pos(rightN) - end(dragged);
818
+ const diff = Math.abs(lGap - rGap);
819
+ if (diff <= threshold && diff < bestDiff) {
820
+ const idealPos = (end(leftN) + pos(rightN) - size(dragged)) / 2;
821
+ const snap = idealPos - pos(dragged);
822
+ const equalGap = (pos(rightN) - end(leftN) - size(dragged)) / 2;
823
+ if (equalGap > .1) {
824
+ const perpY = computePerpCenter(dragged, [leftN, rightN], isX);
825
+ bestSnap = snap;
826
+ bestDiff = diff;
827
+ bestIndicators = [{
828
+ axis,
829
+ gap: equalGap,
830
+ segments: [{
831
+ from: end(leftN),
832
+ to: idealPos
833
+ }, {
834
+ from: idealPos + size(dragged),
835
+ to: pos(rightN)
836
+ }],
837
+ perpPosition: perpY
838
+ }];
839
+ }
840
+ }
841
+ }
842
+ for (const refGap of refGaps) {
843
+ const patternGap = refGap.gap;
844
+ if (rightN === null || pos(refGap.to) >= end(dragged) - threshold * 2) {
845
+ const chainEnd = refGap.to;
846
+ const dragGap = pos(dragged) - end(chainEnd);
847
+ const diff = Math.abs(dragGap - patternGap);
848
+ if (diff <= threshold && diff < bestDiff) {
849
+ const idealPos = end(chainEnd) + patternGap;
850
+ const snap = idealPos - pos(dragged);
851
+ const perpY = computePerpCenter(dragged, [refGap.from, refGap.to], isX);
852
+ bestSnap = snap;
853
+ bestDiff = diff;
854
+ bestIndicators = [{
855
+ axis,
856
+ gap: patternGap,
857
+ segments: [{
858
+ from: end(refGap.from),
859
+ to: pos(refGap.to)
860
+ }, {
861
+ from: end(chainEnd),
862
+ to: idealPos
863
+ }],
864
+ perpPosition: perpY
865
+ }];
866
+ }
867
+ }
868
+ if (leftN === null || end(refGap.from) <= pos(dragged) + threshold * 2) {
869
+ const chainStart = refGap.from;
870
+ const dragGap = pos(chainStart) - end(dragged);
871
+ const diff = Math.abs(dragGap - patternGap);
872
+ if (diff <= threshold && diff < bestDiff) {
873
+ const idealPos = pos(chainStart) - patternGap - size(dragged);
874
+ const snap = idealPos - pos(dragged);
875
+ const perpY = computePerpCenter(dragged, [refGap.from, refGap.to], isX);
876
+ bestSnap = snap;
877
+ bestDiff = diff;
878
+ bestIndicators = [{
879
+ axis,
880
+ gap: patternGap,
881
+ segments: [{
882
+ from: idealPos + size(dragged),
883
+ to: pos(chainStart)
884
+ }, {
885
+ from: end(refGap.from),
886
+ to: pos(refGap.to)
887
+ }],
888
+ perpPosition: perpY
889
+ }];
890
+ }
891
+ }
892
+ }
893
+ if (bestSnap !== null) return {
894
+ snap: bestSnap,
895
+ indicators: bestIndicators
896
+ };
897
+ return null;
898
+ }
899
+ function computePerpCenter(dragged, refs, isX) {
900
+ const perpPos = (b) => isX ? b.y : b.x;
901
+ const perpSize = (b) => isX ? b.height : b.width;
902
+ const allBounds = [dragged, ...refs];
903
+ const maxStart = Math.max(...allBounds.map(perpPos));
904
+ const minEnd = Math.min(...allBounds.map((b) => perpPos(b) + perpSize(b)));
905
+ if (minEnd < maxStart) return perpPos(allBounds[0]) + perpSize(allBounds[0]) / 2;
906
+ return maxStart + (minEnd - maxStart) / 2;
907
+ }
908
+ //#endregion
909
+ //#region ../../node_modules/.pnpm/quickselect@3.0.0/node_modules/quickselect/index.js
910
+ /**
911
+ * Rearranges items so that all items in the [left, k] are the smallest.
912
+ * The k-th element will have the (k - left + 1)-th smallest value in [left, right].
913
+ *
914
+ * @template T
915
+ * @param {T[]} arr the array to partially sort (in place)
916
+ * @param {number} k middle index for partial sorting (as defined above)
917
+ * @param {number} [left=0] left index of the range to sort
918
+ * @param {number} [right=arr.length-1] right index
919
+ * @param {(a: T, b: T) => number} [compare = (a, b) => a - b] compare function
920
+ */
921
+ function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) {
922
+ while (right > left) {
923
+ if (right - left > 600) {
924
+ const n = right - left + 1;
925
+ const m = k - left + 1;
926
+ const z = Math.log(n);
927
+ const s = .5 * Math.exp(2 * z / 3);
928
+ const sd = .5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
929
+ quickselect(arr, k, Math.max(left, Math.floor(k - m * s / n + sd)), Math.min(right, Math.floor(k + (n - m) * s / n + sd)), compare);
930
+ }
931
+ const t = arr[k];
932
+ let i = left;
933
+ /** @type {number} */
934
+ let j = right;
935
+ swap(arr, left, k);
936
+ if (compare(arr[right], t) > 0) swap(arr, left, right);
937
+ while (i < j) {
938
+ swap(arr, i, j);
939
+ i++;
940
+ j--;
941
+ while (compare(arr[i], t) < 0) i++;
942
+ while (compare(arr[j], t) > 0) j--;
943
+ }
944
+ if (compare(arr[left], t) === 0) swap(arr, left, j);
945
+ else {
946
+ j++;
947
+ swap(arr, j, right);
948
+ }
949
+ if (j <= k) left = j + 1;
950
+ if (k <= j) right = j - 1;
951
+ }
952
+ }
953
+ /**
954
+ * @template T
955
+ * @param {T[]} arr
956
+ * @param {number} i
957
+ * @param {number} j
958
+ */
959
+ function swap(arr, i, j) {
960
+ const tmp = arr[i];
961
+ arr[i] = arr[j];
962
+ arr[j] = tmp;
963
+ }
964
+ /**
965
+ * @template T
966
+ * @param {T} a
967
+ * @param {T} b
968
+ * @returns {number}
969
+ */
970
+ function defaultCompare(a, b) {
971
+ return a < b ? -1 : a > b ? 1 : 0;
972
+ }
973
+ //#endregion
974
+ //#region ../../node_modules/.pnpm/rbush@4.0.1/node_modules/rbush/index.js
975
+ var RBush$1 = class {
976
+ constructor(maxEntries = 9) {
977
+ this._maxEntries = Math.max(4, maxEntries);
978
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * .4));
979
+ this.clear();
980
+ }
981
+ all() {
982
+ return this._all(this.data, []);
983
+ }
984
+ search(bbox) {
985
+ let node = this.data;
986
+ const result = [];
987
+ if (!intersects(bbox, node)) return result;
988
+ const toBBox = this.toBBox;
989
+ const nodesToSearch = [];
990
+ while (node) {
991
+ for (let i = 0; i < node.children.length; i++) {
992
+ const child = node.children[i];
993
+ const childBBox = node.leaf ? toBBox(child) : child;
994
+ if (intersects(bbox, childBBox)) if (node.leaf) result.push(child);
995
+ else if (contains(bbox, childBBox)) this._all(child, result);
996
+ else nodesToSearch.push(child);
997
+ }
998
+ node = nodesToSearch.pop();
999
+ }
1000
+ return result;
1001
+ }
1002
+ collides(bbox) {
1003
+ let node = this.data;
1004
+ if (!intersects(bbox, node)) return false;
1005
+ const nodesToSearch = [];
1006
+ while (node) {
1007
+ for (let i = 0; i < node.children.length; i++) {
1008
+ const child = node.children[i];
1009
+ const childBBox = node.leaf ? this.toBBox(child) : child;
1010
+ if (intersects(bbox, childBBox)) {
1011
+ if (node.leaf || contains(bbox, childBBox)) return true;
1012
+ nodesToSearch.push(child);
1013
+ }
1014
+ }
1015
+ node = nodesToSearch.pop();
1016
+ }
1017
+ return false;
1018
+ }
1019
+ load(data) {
1020
+ if (!(data && data.length)) return this;
1021
+ if (data.length < this._minEntries) {
1022
+ for (let i = 0; i < data.length; i++) this.insert(data[i]);
1023
+ return this;
1024
+ }
1025
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
1026
+ if (!this.data.children.length) this.data = node;
1027
+ else if (this.data.height === node.height) this._splitRoot(this.data, node);
1028
+ else {
1029
+ if (this.data.height < node.height) {
1030
+ const tmpNode = this.data;
1031
+ this.data = node;
1032
+ node = tmpNode;
1033
+ }
1034
+ this._insert(node, this.data.height - node.height - 1, true);
1035
+ }
1036
+ return this;
1037
+ }
1038
+ insert(item) {
1039
+ if (item) this._insert(item, this.data.height - 1);
1040
+ return this;
1041
+ }
1042
+ clear() {
1043
+ this.data = createNode([]);
1044
+ return this;
1045
+ }
1046
+ remove(item, equalsFn) {
1047
+ if (!item) return this;
1048
+ let node = this.data;
1049
+ const bbox = this.toBBox(item);
1050
+ const path = [];
1051
+ const indexes = [];
1052
+ let i, parent, goingUp;
1053
+ while (node || path.length) {
1054
+ if (!node) {
1055
+ node = path.pop();
1056
+ parent = path[path.length - 1];
1057
+ i = indexes.pop();
1058
+ goingUp = true;
1059
+ }
1060
+ if (node.leaf) {
1061
+ const index = findItem(item, node.children, equalsFn);
1062
+ if (index !== -1) {
1063
+ node.children.splice(index, 1);
1064
+ path.push(node);
1065
+ this._condense(path);
1066
+ return this;
1067
+ }
1068
+ }
1069
+ if (!goingUp && !node.leaf && contains(node, bbox)) {
1070
+ path.push(node);
1071
+ indexes.push(i);
1072
+ i = 0;
1073
+ parent = node;
1074
+ node = node.children[0];
1075
+ } else if (parent) {
1076
+ i++;
1077
+ node = parent.children[i];
1078
+ goingUp = false;
1079
+ } else node = null;
1080
+ }
1081
+ return this;
1082
+ }
1083
+ toBBox(item) {
1084
+ return item;
1085
+ }
1086
+ compareMinX(a, b) {
1087
+ return a.minX - b.minX;
1088
+ }
1089
+ compareMinY(a, b) {
1090
+ return a.minY - b.minY;
1091
+ }
1092
+ toJSON() {
1093
+ return this.data;
1094
+ }
1095
+ fromJSON(data) {
1096
+ this.data = data;
1097
+ return this;
1098
+ }
1099
+ _all(node, result) {
1100
+ const nodesToSearch = [];
1101
+ while (node) {
1102
+ if (node.leaf) result.push(...node.children);
1103
+ else nodesToSearch.push(...node.children);
1104
+ node = nodesToSearch.pop();
1105
+ }
1106
+ return result;
1107
+ }
1108
+ _build(items, left, right, height) {
1109
+ const N = right - left + 1;
1110
+ let M = this._maxEntries;
1111
+ let node;
1112
+ if (N <= M) {
1113
+ node = createNode(items.slice(left, right + 1));
1114
+ calcBBox(node, this.toBBox);
1115
+ return node;
1116
+ }
1117
+ if (!height) {
1118
+ height = Math.ceil(Math.log(N) / Math.log(M));
1119
+ M = Math.ceil(N / Math.pow(M, height - 1));
1120
+ }
1121
+ node = createNode([]);
1122
+ node.leaf = false;
1123
+ node.height = height;
1124
+ const N2 = Math.ceil(N / M);
1125
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
1126
+ multiSelect(items, left, right, N1, this.compareMinX);
1127
+ for (let i = left; i <= right; i += N1) {
1128
+ const right2 = Math.min(i + N1 - 1, right);
1129
+ multiSelect(items, i, right2, N2, this.compareMinY);
1130
+ for (let j = i; j <= right2; j += N2) {
1131
+ const right3 = Math.min(j + N2 - 1, right2);
1132
+ node.children.push(this._build(items, j, right3, height - 1));
1133
+ }
1134
+ }
1135
+ calcBBox(node, this.toBBox);
1136
+ return node;
1137
+ }
1138
+ _chooseSubtree(bbox, node, level, path) {
1139
+ while (true) {
1140
+ path.push(node);
1141
+ if (node.leaf || path.length - 1 === level) break;
1142
+ let minArea = Infinity;
1143
+ let minEnlargement = Infinity;
1144
+ let targetNode;
1145
+ for (let i = 0; i < node.children.length; i++) {
1146
+ const child = node.children[i];
1147
+ const area = bboxArea(child);
1148
+ const enlargement = enlargedArea(bbox, child) - area;
1149
+ if (enlargement < minEnlargement) {
1150
+ minEnlargement = enlargement;
1151
+ minArea = area < minArea ? area : minArea;
1152
+ targetNode = child;
1153
+ } else if (enlargement === minEnlargement) {
1154
+ if (area < minArea) {
1155
+ minArea = area;
1156
+ targetNode = child;
1157
+ }
1158
+ }
1159
+ }
1160
+ node = targetNode || node.children[0];
1161
+ }
1162
+ return node;
1163
+ }
1164
+ _insert(item, level, isNode) {
1165
+ const bbox = isNode ? item : this.toBBox(item);
1166
+ const insertPath = [];
1167
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
1168
+ node.children.push(item);
1169
+ extend(node, bbox);
1170
+ while (level >= 0) if (insertPath[level].children.length > this._maxEntries) {
1171
+ this._split(insertPath, level);
1172
+ level--;
1173
+ } else break;
1174
+ this._adjustParentBBoxes(bbox, insertPath, level);
1175
+ }
1176
+ _split(insertPath, level) {
1177
+ const node = insertPath[level];
1178
+ const M = node.children.length;
1179
+ const m = this._minEntries;
1180
+ this._chooseSplitAxis(node, m, M);
1181
+ const splitIndex = this._chooseSplitIndex(node, m, M);
1182
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
1183
+ newNode.height = node.height;
1184
+ newNode.leaf = node.leaf;
1185
+ calcBBox(node, this.toBBox);
1186
+ calcBBox(newNode, this.toBBox);
1187
+ if (level) insertPath[level - 1].children.push(newNode);
1188
+ else this._splitRoot(node, newNode);
1189
+ }
1190
+ _splitRoot(node, newNode) {
1191
+ this.data = createNode([node, newNode]);
1192
+ this.data.height = node.height + 1;
1193
+ this.data.leaf = false;
1194
+ calcBBox(this.data, this.toBBox);
1195
+ }
1196
+ _chooseSplitIndex(node, m, M) {
1197
+ let index;
1198
+ let minOverlap = Infinity;
1199
+ let minArea = Infinity;
1200
+ for (let i = m; i <= M - m; i++) {
1201
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
1202
+ const bbox2 = distBBox(node, i, M, this.toBBox);
1203
+ const overlap = intersectionArea(bbox1, bbox2);
1204
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
1205
+ if (overlap < minOverlap) {
1206
+ minOverlap = overlap;
1207
+ index = i;
1208
+ minArea = area < minArea ? area : minArea;
1209
+ } else if (overlap === minOverlap) {
1210
+ if (area < minArea) {
1211
+ minArea = area;
1212
+ index = i;
1213
+ }
1214
+ }
1215
+ }
1216
+ return index || M - m;
1217
+ }
1218
+ _chooseSplitAxis(node, m, M) {
1219
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
1220
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
1221
+ if (this._allDistMargin(node, m, M, compareMinX) < this._allDistMargin(node, m, M, compareMinY)) node.children.sort(compareMinX);
1222
+ }
1223
+ _allDistMargin(node, m, M, compare) {
1224
+ node.children.sort(compare);
1225
+ const toBBox = this.toBBox;
1226
+ const leftBBox = distBBox(node, 0, m, toBBox);
1227
+ const rightBBox = distBBox(node, M - m, M, toBBox);
1228
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
1229
+ for (let i = m; i < M - m; i++) {
1230
+ const child = node.children[i];
1231
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
1232
+ margin += bboxMargin(leftBBox);
1233
+ }
1234
+ for (let i = M - m - 1; i >= m; i--) {
1235
+ const child = node.children[i];
1236
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
1237
+ margin += bboxMargin(rightBBox);
1238
+ }
1239
+ return margin;
1240
+ }
1241
+ _adjustParentBBoxes(bbox, path, level) {
1242
+ for (let i = level; i >= 0; i--) extend(path[i], bbox);
1243
+ }
1244
+ _condense(path) {
1245
+ for (let i = path.length - 1, siblings; i >= 0; i--) if (path[i].children.length === 0) if (i > 0) {
1246
+ siblings = path[i - 1].children;
1247
+ siblings.splice(siblings.indexOf(path[i]), 1);
1248
+ } else this.clear();
1249
+ else calcBBox(path[i], this.toBBox);
1250
+ }
1251
+ };
1252
+ function findItem(item, items, equalsFn) {
1253
+ if (!equalsFn) return items.indexOf(item);
1254
+ for (let i = 0; i < items.length; i++) if (equalsFn(item, items[i])) return i;
1255
+ return -1;
1256
+ }
1257
+ function calcBBox(node, toBBox) {
1258
+ distBBox(node, 0, node.children.length, toBBox, node);
1259
+ }
1260
+ function distBBox(node, k, p, toBBox, destNode) {
1261
+ if (!destNode) destNode = createNode(null);
1262
+ destNode.minX = Infinity;
1263
+ destNode.minY = Infinity;
1264
+ destNode.maxX = -Infinity;
1265
+ destNode.maxY = -Infinity;
1266
+ for (let i = k; i < p; i++) {
1267
+ const child = node.children[i];
1268
+ extend(destNode, node.leaf ? toBBox(child) : child);
1269
+ }
1270
+ return destNode;
1271
+ }
1272
+ function extend(a, b) {
1273
+ a.minX = Math.min(a.minX, b.minX);
1274
+ a.minY = Math.min(a.minY, b.minY);
1275
+ a.maxX = Math.max(a.maxX, b.maxX);
1276
+ a.maxY = Math.max(a.maxY, b.maxY);
1277
+ return a;
1278
+ }
1279
+ function compareNodeMinX(a, b) {
1280
+ return a.minX - b.minX;
1281
+ }
1282
+ function compareNodeMinY(a, b) {
1283
+ return a.minY - b.minY;
1284
+ }
1285
+ function bboxArea(a) {
1286
+ return (a.maxX - a.minX) * (a.maxY - a.minY);
1287
+ }
1288
+ function bboxMargin(a) {
1289
+ return a.maxX - a.minX + (a.maxY - a.minY);
1290
+ }
1291
+ function enlargedArea(a, b) {
1292
+ return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) * (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
1293
+ }
1294
+ function intersectionArea(a, b) {
1295
+ const minX = Math.max(a.minX, b.minX);
1296
+ const minY = Math.max(a.minY, b.minY);
1297
+ const maxX = Math.min(a.maxX, b.maxX);
1298
+ const maxY = Math.min(a.maxY, b.maxY);
1299
+ return Math.max(0, maxX - minX) * Math.max(0, maxY - minY);
1300
+ }
1301
+ function contains(a, b) {
1302
+ return a.minX <= b.minX && a.minY <= b.minY && b.maxX <= a.maxX && b.maxY <= a.maxY;
1303
+ }
1304
+ function intersects(a, b) {
1305
+ return b.minX <= a.maxX && b.minY <= a.maxY && b.maxX >= a.minX && b.maxY >= a.minY;
1306
+ }
1307
+ function createNode(children) {
1308
+ return {
1309
+ children,
1310
+ height: 1,
1311
+ leaf: true,
1312
+ minX: Infinity,
1313
+ minY: Infinity,
1314
+ maxX: -Infinity,
1315
+ maxY: -Infinity
1316
+ };
1317
+ }
1318
+ function multiSelect(arr, left, right, n, compare) {
1319
+ const stack = [left, right];
1320
+ while (stack.length) {
1321
+ right = stack.pop();
1322
+ left = stack.pop();
1323
+ if (right - left <= n) continue;
1324
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
1325
+ quickselect(arr, mid, left, right, compare);
1326
+ stack.push(left, mid, mid, right);
1327
+ }
1328
+ }
1329
+ //#endregion
1330
+ //#region src/spatial.ts
1331
+ const rbushModule = RBush$1;
1332
+ const RBush = typeof rbushModule.default === "function" ? rbushModule.default : RBush$1;
1333
+ /**
1334
+ * Spatial index backed by an R-tree (rbush).
1335
+ * Stores world-space AABBs for fast viewport culling and hit testing.
1336
+ */
1337
+ var SpatialIndex = class {
1338
+ tree = new RBush();
1339
+ entries = /* @__PURE__ */ new Map();
1340
+ upsert(entityId, bounds) {
1341
+ const existing = this.entries.get(entityId);
1342
+ if (existing) this.tree.remove(existing);
1343
+ const entry = {
1344
+ ...bounds,
1345
+ entityId
1346
+ };
1347
+ this.entries.set(entityId, entry);
1348
+ this.tree.insert(entry);
1349
+ }
1350
+ remove(entityId) {
1351
+ const existing = this.entries.get(entityId);
1352
+ if (existing) {
1353
+ this.tree.remove(existing);
1354
+ this.entries.delete(entityId);
1355
+ }
1356
+ }
1357
+ /** Query all entries intersecting the given AABB */
1358
+ search(bounds) {
1359
+ return this.tree.search(bounds);
1360
+ }
1361
+ /** Find the topmost entity at a point (by z-order — caller sorts) */
1362
+ searchPoint(x, y, tolerance = 0) {
1363
+ return this.tree.search({
1364
+ minX: x - tolerance,
1365
+ minY: y - tolerance,
1366
+ maxX: x + tolerance,
1367
+ maxY: y + tolerance
1368
+ });
1369
+ }
1370
+ clear() {
1371
+ this.tree.clear();
1372
+ this.entries.clear();
1373
+ }
1374
+ get size() {
1375
+ return this.entries.size;
1376
+ }
1377
+ };
1378
+ //#endregion
1379
+ //#region src/systems.ts
1380
+ /**
1381
+ * Stamp Transform2D width/height from Card.preset.
1382
+ * Runs before transformPropagateSystem so WorldBounds reflect the preset
1383
+ * size in the same tick. Manual writes to Transform2D.width/height on a
1384
+ * card entity get overwritten — to change card size, update `Card.preset`.
1385
+ */
1386
+ const cardSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
1387
+ name: "card",
1388
+ before: "transformPropagate",
1389
+ execute: (world) => {
1390
+ const resource = world.getResource(require_hooks.CardPresetsResource);
1391
+ if (!resource) return;
1392
+ const { presets } = resource;
1393
+ for (const entity of world.query(require_hooks.Card, require_hooks.Transform2D)) {
1394
+ const card = world.getComponent(entity, require_hooks.Card);
1395
+ const transform = world.getComponent(entity, require_hooks.Transform2D);
1396
+ if (!card || !transform) continue;
1397
+ const size = presets[card.preset];
1398
+ if (!size) continue;
1399
+ if (transform.width !== size.width || transform.height !== size.height) world.setComponent(entity, require_hooks.Transform2D, {
1400
+ width: size.width,
1401
+ height: size.height
1402
+ });
1403
+ }
1404
+ }
1405
+ });
1406
+ /**
1407
+ * Propagate transforms down the parent-child hierarchy.
1408
+ * Computes WorldBounds for every entity with Transform2D.
1409
+ * Uses change detection — only processes dirty entities and their descendants.
1410
+ */
1411
+ const transformPropagateSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
1412
+ name: "transformPropagate",
1413
+ execute: (world) => {
1414
+ const changed = world.queryChanged(require_hooks.Transform2D);
1415
+ const processed = /* @__PURE__ */ new Set();
1416
+ for (const entity of changed) propagateEntity(world, entity, processed);
1417
+ for (const entity of world.queryAdded(require_hooks.Transform2D)) if (!processed.has(entity)) propagateEntity(world, entity, processed);
1418
+ }
1419
+ });
1420
+ function propagateEntity(world, entity, processed) {
1421
+ if (processed.has(entity)) return;
1422
+ processed.add(entity);
1423
+ const transform = world.getComponent(entity, require_hooks.Transform2D);
1424
+ if (!transform) return;
1425
+ let worldX = transform.x;
1426
+ let worldY = transform.y;
1427
+ const parentComp = world.getComponent(entity, require_hooks.Parent);
1428
+ if (parentComp && world.entityExists(parentComp.id)) {
1429
+ if (!processed.has(parentComp.id)) propagateEntity(world, parentComp.id, processed);
1430
+ const parentBounds = world.getComponent(parentComp.id, require_hooks.WorldBounds);
1431
+ if (parentBounds) {
1432
+ worldX += parentBounds.worldX;
1433
+ worldY += parentBounds.worldY;
1434
+ }
1435
+ }
1436
+ if (!world.hasComponent(entity, require_hooks.WorldBounds)) world.addComponent(entity, require_hooks.WorldBounds, {
1437
+ worldX,
1438
+ worldY,
1439
+ worldWidth: transform.width,
1440
+ worldHeight: transform.height
1441
+ });
1442
+ else world.setComponent(entity, require_hooks.WorldBounds, {
1443
+ worldX,
1444
+ worldY,
1445
+ worldWidth: transform.width,
1446
+ worldHeight: transform.height
1447
+ });
1448
+ const children = world.getComponent(entity, require_hooks.Children);
1449
+ if (children) for (const childId of children.ids) propagateEntity(world, childId, processed);
1450
+ }
1451
+ /**
1452
+ * Specification for the 8 resize handles spawned around a selected resizable.
1453
+ * anchorX/anchorY are in 0..1 parent-local coordinates.
1454
+ * Corners (layer 15) sit above edges (layer 10) so corners win overlapping hit tests.
1455
+ */
1456
+ const HANDLE_SPECS = [
1457
+ {
1458
+ pos: "nw",
1459
+ ax: 0,
1460
+ ay: 0,
1461
+ layer: 15,
1462
+ cursor: "nw-resize"
1463
+ },
1464
+ {
1465
+ pos: "ne",
1466
+ ax: 1,
1467
+ ay: 0,
1468
+ layer: 15,
1469
+ cursor: "ne-resize"
1470
+ },
1471
+ {
1472
+ pos: "sw",
1473
+ ax: 0,
1474
+ ay: 1,
1475
+ layer: 15,
1476
+ cursor: "sw-resize"
1477
+ },
1478
+ {
1479
+ pos: "se",
1480
+ ax: 1,
1481
+ ay: 1,
1482
+ layer: 15,
1483
+ cursor: "se-resize"
1484
+ },
1485
+ {
1486
+ pos: "n",
1487
+ ax: .5,
1488
+ ay: 0,
1489
+ layer: 10,
1490
+ cursor: "n-resize"
1491
+ },
1492
+ {
1493
+ pos: "s",
1494
+ ax: .5,
1495
+ ay: 1,
1496
+ layer: 10,
1497
+ cursor: "s-resize"
1498
+ },
1499
+ {
1500
+ pos: "w",
1501
+ ax: 0,
1502
+ ay: .5,
1503
+ layer: 10,
1504
+ cursor: "w-resize"
1505
+ },
1506
+ {
1507
+ pos: "e",
1508
+ ax: 1,
1509
+ ay: .5,
1510
+ layer: 10,
1511
+ cursor: "e-resize"
1512
+ }
1513
+ ];
1514
+ function spawnResizeHandles(world, parentId) {
1515
+ const S = 16;
1516
+ const parentActive = world.hasTag(parentId, require_hooks.Active);
1517
+ const ids = [];
1518
+ for (const spec of HANDLE_SPECS) {
1519
+ const id = world.createEntity();
1520
+ world.addComponent(id, require_hooks.Parent, { id: parentId });
1521
+ world.addComponent(id, require_hooks.Hitbox, {
1522
+ anchorX: spec.ax,
1523
+ anchorY: spec.ay,
1524
+ width: S,
1525
+ height: S
1526
+ });
1527
+ world.addComponent(id, require_hooks.InteractionRole, {
1528
+ layer: spec.layer,
1529
+ role: {
1530
+ type: "resize",
1531
+ handle: spec.pos
1532
+ }
1533
+ });
1534
+ world.addComponent(id, require_hooks.CursorHint, {
1535
+ hover: spec.cursor,
1536
+ active: spec.cursor
1537
+ });
1538
+ if (parentActive) world.addTag(id, require_hooks.Active);
1539
+ ids.push(id);
1540
+ }
1541
+ world.addComponent(parentId, require_hooks.HandleSet, { ids });
1542
+ }
1543
+ function despawnHandles(world, parentId) {
1544
+ const set = world.getComponent(parentId, require_hooks.HandleSet);
1545
+ if (!set) return;
1546
+ for (const id of set.ids) if (world.entityExists(id)) world.destroyEntity(id);
1547
+ world.removeComponent(parentId, require_hooks.HandleSet);
1548
+ }
1549
+ /**
1550
+ * Spawn/despawn resize handle child entities based on selection state.
1551
+ * Handles appear only when exactly one Resizable entity is Selected.
1552
+ * Runs after transformPropagate (parent bounds fresh) and before hitboxWorldBounds
1553
+ * (so newly-spawned handles get their WorldBounds in the same tick).
1554
+ *
1555
+ * Phase 5: handles now drive interaction directly via the unified hit test —
1556
+ * InteractionRole + Hitbox on each handle entity replaces hitTestResizeHandle.
1557
+ */
1558
+ const handleSyncSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
1559
+ name: "handleSync",
1560
+ after: "transformPropagate",
1561
+ before: "hitboxWorldBounds",
1562
+ execute: (world) => {
1563
+ const selectedResizable = [];
1564
+ for (const entity of world.queryTagged(require_hooks.Resizable)) if (world.hasTag(entity, require_hooks.Selected)) selectedResizable.push(entity);
1565
+ const shouldSpawn = selectedResizable.length === 1 ? selectedResizable[0] : null;
1566
+ const owners = world.query(require_hooks.HandleSet).slice();
1567
+ for (const parentId of owners) if (parentId !== shouldSpawn) despawnHandles(world, parentId);
1568
+ if (shouldSpawn !== null && !world.hasComponent(shouldSpawn, require_hooks.HandleSet)) spawnResizeHandles(world, shouldSpawn);
1569
+ for (const entity of world.query(require_hooks.Hitbox, require_hooks.Parent).slice()) {
1570
+ const parent = world.getComponent(entity, require_hooks.Parent);
1571
+ if (!parent || !world.entityExists(parent.id)) {
1572
+ const role = world.getComponent(entity, require_hooks.InteractionRole);
1573
+ if (role && (role.role.type === "resize" || role.role.type === "rotate")) world.destroyEntity(entity);
1574
+ }
1575
+ }
1576
+ }
1577
+ });
1578
+ /**
1579
+ * Derive WorldBounds for every entity with Hitbox + Parent from the parent's
1580
+ * WorldBounds + anchor offset. Runs after transformPropagateSystem so parent
1581
+ * WorldBounds are up to date. No-op until Phase 4 spawns entities with Hitbox.
1582
+ */
1583
+ const hitboxWorldBoundsSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
1584
+ name: "hitboxWorldBounds",
1585
+ after: "transformPropagate",
1586
+ execute: (world) => {
1587
+ for (const entity of world.query(require_hooks.Hitbox, require_hooks.Parent)) {
1588
+ const parentRef = world.getComponent(entity, require_hooks.Parent);
1589
+ if (!parentRef) continue;
1590
+ if (!world.entityExists(parentRef.id)) continue;
1591
+ const parentWB = world.getComponent(parentRef.id, require_hooks.WorldBounds);
1592
+ if (!parentWB) continue;
1593
+ const hb = world.getComponent(entity, require_hooks.Hitbox);
1594
+ if (!hb) continue;
1595
+ const cx = parentWB.worldX + parentWB.worldWidth * hb.anchorX;
1596
+ const cy = parentWB.worldY + parentWB.worldHeight * hb.anchorY;
1597
+ const next = {
1598
+ worldX: cx - hb.width / 2,
1599
+ worldY: cy - hb.height / 2,
1600
+ worldWidth: hb.width,
1601
+ worldHeight: hb.height
1602
+ };
1603
+ if (world.hasComponent(entity, require_hooks.WorldBounds)) world.setComponent(entity, require_hooks.WorldBounds, next);
1604
+ else world.addComponent(entity, require_hooks.WorldBounds, next);
1605
+ }
1606
+ }
1607
+ });
1608
+ /**
1609
+ * Filter entities to the active navigation layer.
1610
+ * Runs on nav-stack changes (full refilter) and incrementally whenever new
1611
+ * Transform2D entities are added (so runtime spawns land in the active layer).
1612
+ */
1613
+ const navigationFilterSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
1614
+ name: "navigationFilter",
1615
+ after: "transformPropagate",
1616
+ execute: (world) => {
1617
+ const navStack = world.getResource(require_hooks.NavigationStackResource);
1618
+ const stackChanged = navStack.changed;
1619
+ const newEntities = world.queryAdded(require_hooks.Transform2D);
1620
+ if (!stackChanged && newEntities.length === 0) return;
1621
+ const activeContainer = navStack.frames[navStack.frames.length - 1].containerId;
1622
+ const belongsToCurrentFrame = (entity) => {
1623
+ if (activeContainer === null) return !world.hasComponent(entity, require_hooks.Parent);
1624
+ return world.getComponent(entity, require_hooks.Parent)?.id === activeContainer;
1625
+ };
1626
+ if (stackChanged) {
1627
+ for (const entity of world.queryTagged(require_hooks.Active)) world.removeTag(entity, require_hooks.Active);
1628
+ for (const entity of world.query(require_hooks.Transform2D)) if (belongsToCurrentFrame(entity)) world.addTag(entity, require_hooks.Active);
1629
+ navStack.changed = false;
1630
+ } else for (const entity of newEntities) if (belongsToCurrentFrame(entity) && !world.hasTag(entity, require_hooks.Active)) world.addTag(entity, require_hooks.Active);
1631
+ }
1632
+ });
1633
+ /**
1634
+ * Viewport culling — mark Active entities that intersect the viewport as Visible.
1635
+ */
1636
+ const cullSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
1637
+ name: "cull",
1638
+ after: "navigationFilter",
1639
+ execute: (world) => {
1640
+ const camera = world.getResource(require_hooks.CameraResource);
1641
+ const viewport = world.getResource(require_hooks.ViewportResource);
1642
+ if (viewport.width === 0 || viewport.height === 0) return;
1643
+ const spatialIndex = world.getResource(SpatialIndexResource).instance;
1644
+ const overscan = 200 / camera.zoom;
1645
+ const vpWorldAABB = {
1646
+ minX: camera.x - overscan,
1647
+ minY: camera.y - overscan,
1648
+ maxX: camera.x + viewport.width / camera.zoom + overscan,
1649
+ maxY: camera.y + viewport.height / camera.zoom + overscan
1650
+ };
1651
+ for (const entity of world.queryTagged(require_hooks.Visible)) world.removeTag(entity, require_hooks.Visible);
1652
+ if (spatialIndex && spatialIndex.size > 0) {
1653
+ const candidates = spatialIndex.search(vpWorldAABB);
1654
+ for (const entry of candidates) if (world.hasTag(entry.entityId, require_hooks.Active)) world.addTag(entry.entityId, require_hooks.Visible);
1655
+ } else for (const entity of world.queryTagged(require_hooks.Active)) {
1656
+ const wb = world.getComponent(entity, require_hooks.WorldBounds);
1657
+ if (wb && intersectsAABB(worldBoundsToAABB(wb), vpWorldAABB)) world.addTag(entity, require_hooks.Visible);
1658
+ }
1659
+ }
1660
+ });
1661
+ /**
1662
+ * Compute breakpoints for visible widgets based on screen size.
1663
+ * Fix #10: Always update screenWidth/screenHeight even if breakpoint tier doesn't change.
1664
+ */
1665
+ const breakpointSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
1666
+ name: "breakpoint",
1667
+ after: "cull",
1668
+ execute: (world) => {
1669
+ const camera = world.getResource(require_hooks.CameraResource);
1670
+ const config = world.getResource(require_hooks.BreakpointConfigResource);
1671
+ for (const entity of world.query(require_hooks.Widget, require_hooks.Visible)) {
1672
+ const transform = world.getComponent(entity, require_hooks.Transform2D);
1673
+ if (!transform) continue;
1674
+ const screenWidth = transform.width * camera.zoom;
1675
+ const screenHeight = transform.height * camera.zoom;
1676
+ let bp;
1677
+ if (screenWidth < config.micro) bp = "micro";
1678
+ else if (screenWidth < config.compact) bp = "compact";
1679
+ else if (screenWidth < config.normal) bp = "normal";
1680
+ else if (screenWidth < config.expanded) bp = "expanded";
1681
+ else bp = "detailed";
1682
+ const existing = world.getComponent(entity, require_hooks.WidgetBreakpoint);
1683
+ if (!existing) world.addComponent(entity, require_hooks.WidgetBreakpoint, {
1684
+ current: bp,
1685
+ screenWidth,
1686
+ screenHeight
1687
+ });
1688
+ else {
1689
+ const bpChanged = existing.current !== bp;
1690
+ const sizeChanged = Math.round(existing.screenWidth) !== Math.round(screenWidth) || Math.round(existing.screenHeight) !== Math.round(screenHeight);
1691
+ if (bpChanged || sizeChanged) world.setComponent(entity, require_hooks.WidgetBreakpoint, {
1692
+ current: bp,
1693
+ screenWidth,
1694
+ screenHeight
1695
+ });
1696
+ }
1697
+ }
1698
+ }
1699
+ });
1700
+ /**
1701
+ * Sort visible entities by z-index (handled in engine.tick()).
1702
+ */
1703
+ const sortSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
1704
+ name: "sort",
1705
+ after: "breakpoint",
1706
+ execute: (_world) => {}
1707
+ });
1708
+ //#endregion
1709
+ //#region src/engine.ts
1710
+ /** ECS resource holding the SpatialIndex instance for viewport culling and hit testing. */
1711
+ const SpatialIndexResource = (0, _jamesyong42_reactive_ecs.defineResource)("SpatialIndex", { instance: null });
1712
+ /**
1713
+ * Creates a new LayoutEngine instance with the given configuration.
1714
+ * This is the main entry point for the infinite canvas library.
1715
+ */
1716
+ function createLayoutEngine(config) {
1717
+ const world = (0, _jamesyong42_reactive_ecs.createWorld)();
1718
+ const scheduler = new _jamesyong42_reactive_ecs.SystemScheduler();
1719
+ const spatialIndex = new SpatialIndex();
1720
+ const profiler = new Profiler();
1721
+ scheduler.profiler = profiler;
1722
+ world.setResource(SpatialIndexResource, { instance: spatialIndex });
1723
+ const commandBuffer = new CommandBuffer();
1724
+ const widgetRegistry = createWidgetRegistry();
1725
+ const archetypeRegistry = createArchetypeRegistry();
1726
+ if (config?.zoom) world.setResource(require_hooks.ZoomConfigResource, config.zoom);
1727
+ if (config?.breakpoints) world.setResource(require_hooks.BreakpointConfigResource, config.breakpoints);
1728
+ if (config?.cardPresets) {
1729
+ const current = world.getResource(require_hooks.CardPresetsResource);
1730
+ world.setResource(require_hooks.CardPresetsResource, {
1731
+ presets: {
1732
+ ...current.presets,
1733
+ ...config.cardPresets.presets
1734
+ },
1735
+ gap: config.cardPresets.gap ?? current.gap
1736
+ });
1737
+ }
1738
+ let snapEnabledInit = true;
1739
+ let snapThresholdInit = 5;
1740
+ if (config?.snap?.enabled !== void 0) snapEnabledInit = config.snap.enabled;
1741
+ if (config?.snap?.threshold !== void 0) snapThresholdInit = config.snap.threshold;
1742
+ scheduler.register(cardSystem);
1743
+ scheduler.register(transformPropagateSystem);
1744
+ scheduler.register(handleSyncSystem);
1745
+ scheduler.register(hitboxWorldBoundsSystem);
1746
+ scheduler.register(navigationFilterSystem);
1747
+ scheduler.register(cullSystem);
1748
+ scheduler.register(breakpointSystem);
1749
+ scheduler.register(sortSystem);
1750
+ const unsubscribers = [];
1751
+ unsubscribers.push(world.onComponentChanged(require_hooks.WorldBounds, (entityId, _prev, wb) => {
1752
+ if (wb) spatialIndex.upsert(entityId, worldBoundsToAABB(wb));
1753
+ }));
1754
+ unsubscribers.push(world.onEntityDestroyed((entity) => {
1755
+ spatialIndex.remove(entity);
1756
+ }));
1757
+ function refreshInteractionRole(entity) {
1758
+ const current = world.getComponent(entity, require_hooks.InteractionRole);
1759
+ if (current && current.role.type !== "drag" && current.role.type !== "select" && current.role.type !== "canvas") return;
1760
+ const hasDraggable = world.hasTag(entity, require_hooks.Draggable);
1761
+ const hasSelectable = world.hasTag(entity, require_hooks.Selectable);
1762
+ const desiredRole = hasDraggable ? { type: "drag" } : hasSelectable ? { type: "select" } : null;
1763
+ if (desiredRole === null) {
1764
+ if (current) world.removeComponent(entity, require_hooks.InteractionRole);
1765
+ if (world.hasComponent(entity, require_hooks.CursorHint)) world.removeComponent(entity, require_hooks.CursorHint);
1766
+ return;
1767
+ }
1768
+ if (!current) world.addComponent(entity, require_hooks.InteractionRole, {
1769
+ layer: 5,
1770
+ role: desiredRole
1771
+ });
1772
+ else if (current.role.type !== desiredRole.type) world.setComponent(entity, require_hooks.InteractionRole, { role: desiredRole });
1773
+ if (desiredRole.type === "drag" && !world.hasComponent(entity, require_hooks.CursorHint)) world.addComponent(entity, require_hooks.CursorHint, {
1774
+ hover: "grab",
1775
+ active: "grabbing"
1776
+ });
1777
+ }
1778
+ unsubscribers.push(world.onTagAdded(require_hooks.Draggable, refreshInteractionRole));
1779
+ unsubscribers.push(world.onTagRemoved(require_hooks.Draggable, refreshInteractionRole));
1780
+ unsubscribers.push(world.onTagAdded(require_hooks.Selectable, refreshInteractionRole));
1781
+ unsubscribers.push(world.onTagRemoved(require_hooks.Selectable, refreshInteractionRole));
1782
+ if (config?.widgets) for (const w of config.widgets) widgetRegistry.register(w);
1783
+ if (config?.archetypes) for (const a of config.archetypes) archetypeRegistry.register(a);
1784
+ world.setResource(require_hooks.NavigationStackResource, { changed: true });
1785
+ let inputState = { mode: "idle" };
1786
+ let hoveredEntity = null;
1787
+ let snapEnabled = snapEnabledInit;
1788
+ let snapThreshold = snapThresholdInit;
1789
+ let currentSnap = {
1790
+ snapDx: 0,
1791
+ snapDy: 0,
1792
+ guides: [],
1793
+ spacings: []
1794
+ };
1795
+ let dirty = false;
1796
+ let cameraChangedThisTick = false;
1797
+ let selectionChangedThisTick = false;
1798
+ let prevVisible = /* @__PURE__ */ new Set();
1799
+ let currentVisible = [];
1800
+ let frameChanges = {
1801
+ positionsChanged: [],
1802
+ breakpointsChanged: [],
1803
+ entered: [],
1804
+ exited: [],
1805
+ cameraChanged: false,
1806
+ navigationChanged: false,
1807
+ selectionChanged: false
1808
+ };
1809
+ function markDirtyInternal() {
1810
+ dirty = true;
1811
+ }
1812
+ function hitTest(screenX, screenY) {
1813
+ const worldPos = screenToWorld(screenX, screenY, world.getResource(require_hooks.CameraResource));
1814
+ const candidates = spatialIndex.searchPoint(worldPos.x, worldPos.y, 0);
1815
+ const interactable = [];
1816
+ for (const c of candidates) {
1817
+ if (!world.hasTag(c.entityId, require_hooks.Active)) continue;
1818
+ const role = world.getComponent(c.entityId, require_hooks.InteractionRole);
1819
+ if (!role) continue;
1820
+ interactable.push({
1821
+ entityId: c.entityId,
1822
+ role
1823
+ });
1824
+ }
1825
+ if (interactable.length === 0) return null;
1826
+ interactable.sort((a, b) => {
1827
+ if (b.role.layer !== a.role.layer) return b.role.layer - a.role.layer;
1828
+ const zA = world.getComponent(a.entityId, require_hooks.ZIndex)?.value ?? 0;
1829
+ return (world.getComponent(b.entityId, require_hooks.ZIndex)?.value ?? 0) - zA;
1830
+ });
1831
+ return interactable[0];
1832
+ }
1833
+ /**
1834
+ * RFC-001 Phase 7: derive the root-container cursor from input state +
1835
+ * hover, write to CursorResource. Closes over `inputState`, `hoveredEntity`
1836
+ * and `world`, which is why it's a plain function instead of a SystemDef.
1837
+ * Called from engine.tick() after scheduler.execute(world).
1838
+ */
1839
+ function cursorSystem() {
1840
+ let cursor = "default";
1841
+ switch (inputState.mode) {
1842
+ case "idle":
1843
+ case "marquee":
1844
+ if (hoveredEntity !== null) cursor = world.getComponent(hoveredEntity, require_hooks.CursorHint)?.hover ?? "default";
1845
+ break;
1846
+ case "tracking":
1847
+ cursor = world.getComponent(inputState.entityId, require_hooks.CursorHint)?.hover ?? "default";
1848
+ break;
1849
+ case "dragging":
1850
+ cursor = world.getComponent(inputState.entityId, require_hooks.CursorHint)?.active ?? "grabbing";
1851
+ break;
1852
+ case "resizing":
1853
+ cursor = world.getComponent(inputState.handleEntityId, require_hooks.CursorHint)?.active ?? "default";
1854
+ break;
1855
+ }
1856
+ world.setResource(require_hooks.CursorResource, { cursor });
1857
+ }
1858
+ function selectEntity(entity, additive) {
1859
+ if (!world.hasTag(entity, require_hooks.Selectable)) return;
1860
+ if (additive) if (world.hasTag(entity, require_hooks.Selected)) world.removeTag(entity, require_hooks.Selected);
1861
+ else world.addTag(entity, require_hooks.Selected);
1862
+ else {
1863
+ for (const e of world.queryTagged(require_hooks.Selected)) if (e !== entity) world.removeTag(e, require_hooks.Selected);
1864
+ world.addTag(entity, require_hooks.Selected);
1865
+ }
1866
+ selectionChangedThisTick = true;
1867
+ }
1868
+ function clearSelection() {
1869
+ const selected = world.queryTagged(require_hooks.Selected);
1870
+ if (selected.length > 0) {
1871
+ for (const e of selected) world.removeTag(e, require_hooks.Selected);
1872
+ selectionChangedThisTick = true;
1873
+ }
1874
+ }
1875
+ const engine = {
1876
+ world,
1877
+ createEntity(inits) {
1878
+ const entity = world.createEntity();
1879
+ if (inits) for (const init of inits) {
1880
+ const type = init[0];
1881
+ if (type.__kind === "tag") world.addTag(entity, type);
1882
+ else world.addComponent(entity, type, init[1] ?? {});
1883
+ }
1884
+ markDirtyInternal();
1885
+ return entity;
1886
+ },
1887
+ spawn(id, opts = {}) {
1888
+ const archetype = archetypeRegistry.get(id);
1889
+ const widgetTypeId = archetype?.widget ?? id;
1890
+ const widget = widgetRegistry.get(widgetTypeId);
1891
+ const surface = widget?.surface ?? "dom";
1892
+ const defaultData = widget?.defaultData ?? {};
1893
+ const defaultSize = archetype?.defaultSize ?? widget?.defaultSize ?? {
1894
+ width: 100,
1895
+ height: 100
1896
+ };
1897
+ const position = opts.at ?? {
1898
+ x: 0,
1899
+ y: 0
1900
+ };
1901
+ const size = opts.size ?? defaultSize;
1902
+ const data = {
1903
+ ...defaultData,
1904
+ ...opts.data
1905
+ };
1906
+ const inits = [
1907
+ [require_hooks.Transform2D, {
1908
+ x: position.x,
1909
+ y: position.y,
1910
+ width: size.width,
1911
+ height: size.height,
1912
+ rotation: opts.rotation ?? 0
1913
+ }],
1914
+ [require_hooks.Widget, {
1915
+ surface,
1916
+ type: widgetTypeId
1917
+ }],
1918
+ [require_hooks.WidgetData, { data }],
1919
+ [require_hooks.ZIndex, { value: opts.zIndex ?? 0 }]
1920
+ ];
1921
+ if (archetype?.components) for (const init of archetype.components) inits.push(init);
1922
+ if (opts.parent !== void 0) inits.push([require_hooks.Parent, { id: opts.parent }]);
1923
+ const interactiveConfig = archetype?.interactive;
1924
+ const caps = interactiveConfig === false ? {
1925
+ selectable: false,
1926
+ draggable: false,
1927
+ resizable: false,
1928
+ selectionFrame: false
1929
+ } : interactiveConfig === void 0 || interactiveConfig === true ? {
1930
+ selectable: true,
1931
+ draggable: true,
1932
+ resizable: true,
1933
+ selectionFrame: true
1934
+ } : (() => {
1935
+ const selectable = interactiveConfig.selectable ?? false;
1936
+ return {
1937
+ selectable,
1938
+ draggable: interactiveConfig.draggable ?? false,
1939
+ resizable: interactiveConfig.resizable ?? false,
1940
+ selectionFrame: interactiveConfig.selectionFrame ?? selectable
1941
+ };
1942
+ })();
1943
+ if (caps.selectable) inits.push([require_hooks.Selectable]);
1944
+ if (caps.draggable) inits.push([require_hooks.Draggable]);
1945
+ if (caps.resizable) inits.push([require_hooks.Resizable]);
1946
+ if (caps.selectionFrame) inits.push([require_hooks.SelectionFrame]);
1947
+ if (archetype?.tags) for (const tag of archetype.tags) inits.push([tag]);
1948
+ return engine.createEntity(inits);
1949
+ },
1950
+ spawnAtCameraCenter(id, opts = {}) {
1951
+ const camera = world.getResource(require_hooks.CameraResource);
1952
+ const viewport = world.getResource(require_hooks.ViewportResource);
1953
+ const centerX = camera.x + viewport.width / (2 * camera.zoom);
1954
+ const centerY = camera.y + viewport.height / (2 * camera.zoom);
1955
+ const archetype = archetypeRegistry.get(id);
1956
+ const widget = widgetRegistry.get(archetype?.widget ?? id);
1957
+ const size = opts.size ?? archetype?.defaultSize ?? widget?.defaultSize ?? {
1958
+ width: 100,
1959
+ height: 100
1960
+ };
1961
+ return engine.spawn(id, {
1962
+ ...opts,
1963
+ at: {
1964
+ x: centerX - size.width / 2,
1965
+ y: centerY - size.height / 2
1966
+ }
1967
+ });
1968
+ },
1969
+ registerWidget(widget) {
1970
+ widgetRegistry.register(widget);
1971
+ },
1972
+ getWidget(type) {
1973
+ return widgetRegistry.get(type);
1974
+ },
1975
+ getWidgets() {
1976
+ return widgetRegistry.getAll();
1977
+ },
1978
+ registerArchetype(archetype) {
1979
+ archetypeRegistry.register(archetype);
1980
+ },
1981
+ getArchetype(id) {
1982
+ return archetypeRegistry.get(id);
1983
+ },
1984
+ destroyEntity(id) {
1985
+ const set = world.getComponent(id, require_hooks.HandleSet);
1986
+ if (set) {
1987
+ for (const handleId of set.ids) if (world.entityExists(handleId)) {
1988
+ spatialIndex.remove(handleId);
1989
+ world.destroyEntity(handleId);
1990
+ }
1991
+ }
1992
+ spatialIndex.remove(id);
1993
+ world.destroyEntity(id);
1994
+ markDirtyInternal();
1995
+ },
1996
+ get(entity, type) {
1997
+ return world.getComponent(entity, type);
1998
+ },
1999
+ set(entity, type, data) {
2000
+ world.setComponent(entity, type, data);
2001
+ markDirtyInternal();
2002
+ },
2003
+ has(entity, type) {
2004
+ if (type.__kind === "tag") return world.hasTag(entity, type);
2005
+ return world.hasComponent(entity, type);
2006
+ },
2007
+ addComponent(entity, type, data) {
2008
+ world.addComponent(entity, type, data ?? type.defaults);
2009
+ markDirtyInternal();
2010
+ },
2011
+ removeComponent(entity, type) {
2012
+ world.removeComponent(entity, type);
2013
+ markDirtyInternal();
2014
+ },
2015
+ addTag(entity, type) {
2016
+ world.addTag(entity, type);
2017
+ markDirtyInternal();
2018
+ },
2019
+ removeTag(entity, type) {
2020
+ world.removeTag(entity, type);
2021
+ markDirtyInternal();
2022
+ },
2023
+ getSchemaFor(entity) {
2024
+ const w = world.getComponent(entity, require_hooks.Widget);
2025
+ if (!w) return void 0;
2026
+ return widgetRegistry.get(w.type)?.schema;
2027
+ },
2028
+ registerSystem(system) {
2029
+ scheduler.register(system);
2030
+ },
2031
+ removeSystem(name) {
2032
+ scheduler.remove(name);
2033
+ },
2034
+ getCamera() {
2035
+ return world.getResource(require_hooks.CameraResource);
2036
+ },
2037
+ panBy(dx, dy) {
2038
+ const camera = world.getResource(require_hooks.CameraResource);
2039
+ camera.x -= dx / camera.zoom;
2040
+ camera.y -= dy / camera.zoom;
2041
+ cameraChangedThisTick = true;
2042
+ markDirtyInternal();
2043
+ },
2044
+ panTo(worldX, worldY) {
2045
+ const camera = world.getResource(require_hooks.CameraResource);
2046
+ const viewport = world.getResource(require_hooks.ViewportResource);
2047
+ camera.x = worldX - viewport.width / (2 * camera.zoom);
2048
+ camera.y = worldY - viewport.height / (2 * camera.zoom);
2049
+ cameraChangedThisTick = true;
2050
+ markDirtyInternal();
2051
+ },
2052
+ zoomAtPoint(screenX, screenY, delta) {
2053
+ const camera = world.getResource(require_hooks.CameraResource);
2054
+ const zoomConfig = world.getResource(require_hooks.ZoomConfigResource);
2055
+ const worldBefore = screenToWorld(screenX, screenY, camera);
2056
+ const newZoom = clamp(camera.zoom * (1 + delta), zoomConfig.min, zoomConfig.max);
2057
+ camera.zoom = newZoom;
2058
+ camera.x = worldBefore.x - screenX / newZoom;
2059
+ camera.y = worldBefore.y - screenY / newZoom;
2060
+ cameraChangedThisTick = true;
2061
+ markDirtyInternal();
2062
+ },
2063
+ zoomTo(zoom) {
2064
+ const camera = world.getResource(require_hooks.CameraResource);
2065
+ const zoomConfig = world.getResource(require_hooks.ZoomConfigResource);
2066
+ const viewport = world.getResource(require_hooks.ViewportResource);
2067
+ const centerWorldX = camera.x + viewport.width / (2 * camera.zoom);
2068
+ const centerWorldY = camera.y + viewport.height / (2 * camera.zoom);
2069
+ camera.zoom = clamp(zoom, zoomConfig.min, zoomConfig.max);
2070
+ camera.x = centerWorldX - viewport.width / (2 * camera.zoom);
2071
+ camera.y = centerWorldY - viewport.height / (2 * camera.zoom);
2072
+ cameraChangedThisTick = true;
2073
+ markDirtyInternal();
2074
+ },
2075
+ zoomToFit(entityIds, padding = 50) {
2076
+ const viewport = world.getResource(require_hooks.ViewportResource);
2077
+ if (viewport.width === 0) return;
2078
+ const entities = entityIds ?? world.queryTagged(require_hooks.Active);
2079
+ if (entities.length === 0) return;
2080
+ let minX = Number.POSITIVE_INFINITY;
2081
+ let minY = Number.POSITIVE_INFINITY;
2082
+ let maxX = Number.NEGATIVE_INFINITY;
2083
+ let maxY = Number.NEGATIVE_INFINITY;
2084
+ for (const e of entities) {
2085
+ const wb = world.getComponent(e, require_hooks.WorldBounds);
2086
+ if (!wb) continue;
2087
+ minX = Math.min(minX, wb.worldX);
2088
+ minY = Math.min(minY, wb.worldY);
2089
+ maxX = Math.max(maxX, wb.worldX + wb.worldWidth);
2090
+ maxY = Math.max(maxY, wb.worldY + wb.worldHeight);
2091
+ }
2092
+ if (!Number.isFinite(minX)) return;
2093
+ const contentWidth = maxX - minX + padding * 2;
2094
+ const contentHeight = maxY - minY + padding * 2;
2095
+ const zoomConfig = world.getResource(require_hooks.ZoomConfigResource);
2096
+ const zoom = clamp(Math.min(viewport.width / contentWidth, viewport.height / contentHeight), zoomConfig.min, zoomConfig.max);
2097
+ const camera = world.getResource(require_hooks.CameraResource);
2098
+ camera.zoom = zoom;
2099
+ camera.x = minX - padding - (viewport.width / zoom - contentWidth) / 2;
2100
+ camera.y = minY - padding - (viewport.height / zoom - contentHeight) / 2;
2101
+ cameraChangedThisTick = true;
2102
+ markDirtyInternal();
2103
+ },
2104
+ setViewport(width, height, dpr) {
2105
+ world.setResource(require_hooks.ViewportResource, {
2106
+ width,
2107
+ height,
2108
+ dpr: dpr ?? 1
2109
+ });
2110
+ markDirtyInternal();
2111
+ },
2112
+ execute(command) {
2113
+ commandBuffer.execute(command, world);
2114
+ markDirtyInternal();
2115
+ },
2116
+ beginCommandGroup() {
2117
+ commandBuffer.beginGroup();
2118
+ },
2119
+ endCommandGroup() {
2120
+ commandBuffer.endGroup();
2121
+ },
2122
+ undo() {
2123
+ const did = commandBuffer.undo(world);
2124
+ if (did) markDirtyInternal();
2125
+ return did;
2126
+ },
2127
+ redo() {
2128
+ const did = commandBuffer.redo(world);
2129
+ if (did) markDirtyInternal();
2130
+ return did;
2131
+ },
2132
+ canUndo() {
2133
+ return commandBuffer.canUndo();
2134
+ },
2135
+ canRedo() {
2136
+ return commandBuffer.canRedo();
2137
+ },
2138
+ handlePointerDown(screenX, screenY, _button, modifiers) {
2139
+ const hit = hitTest(screenX, screenY);
2140
+ if (!hit) {
2141
+ clearSelection();
2142
+ inputState = {
2143
+ mode: "marquee",
2144
+ startX: screenX,
2145
+ startY: screenY
2146
+ };
2147
+ markDirtyInternal();
2148
+ return { action: "capture-marquee" };
2149
+ }
2150
+ switch (hit.role.role.type) {
2151
+ case "resize": {
2152
+ const parentRef = world.getComponent(hit.entityId, require_hooks.Parent);
2153
+ if (!parentRef) return { action: "passthrough" };
2154
+ const parentId = parentRef.id;
2155
+ const t = world.getComponent(parentId, require_hooks.Transform2D);
2156
+ if (!t) return { action: "passthrough" };
2157
+ commandBuffer.beginGroup();
2158
+ inputState = {
2159
+ mode: "resizing",
2160
+ entityId: parentId,
2161
+ handleEntityId: hit.entityId,
2162
+ handle: hit.role.role.handle,
2163
+ startX: screenX,
2164
+ startY: screenY,
2165
+ startBounds: {
2166
+ x: t.x,
2167
+ y: t.y,
2168
+ width: t.width,
2169
+ height: t.height
2170
+ }
2171
+ };
2172
+ markDirtyInternal();
2173
+ return {
2174
+ action: "capture-resize",
2175
+ handle: hit.role.role.handle
2176
+ };
2177
+ }
2178
+ case "drag":
2179
+ selectEntity(hit.entityId, modifiers.shift);
2180
+ if (world.hasTag(hit.entityId, require_hooks.Draggable)) inputState = {
2181
+ mode: "tracking",
2182
+ entityId: hit.entityId,
2183
+ startX: screenX,
2184
+ startY: screenY
2185
+ };
2186
+ markDirtyInternal();
2187
+ return { action: "passthrough-track-drag" };
2188
+ case "select":
2189
+ selectEntity(hit.entityId, modifiers.shift);
2190
+ markDirtyInternal();
2191
+ return { action: "passthrough" };
2192
+ default: return { action: "passthrough" };
2193
+ }
2194
+ },
2195
+ handlePointerMove(screenX, screenY, _modifiers) {
2196
+ if (inputState.mode === "tracking") {
2197
+ const dx = screenX - inputState.startX;
2198
+ const dy = screenY - inputState.startY;
2199
+ if (Math.abs(dx) > 4 || Math.abs(dy) > 4) {
2200
+ const originalZIndices = /* @__PURE__ */ new Map();
2201
+ let maxZ = 0;
2202
+ for (const e of world.queryTagged(require_hooks.Active)) {
2203
+ const z = world.getComponent(e, require_hooks.ZIndex);
2204
+ if (z && z.value > maxZ) maxZ = z.value;
2205
+ }
2206
+ for (const e of world.queryTagged(require_hooks.Selected)) {
2207
+ const z = world.getComponent(e, require_hooks.ZIndex);
2208
+ originalZIndices.set(e, z?.value ?? 0);
2209
+ world.setComponent(e, require_hooks.ZIndex, { value: maxZ + 1 });
2210
+ }
2211
+ const startPositions = /* @__PURE__ */ new Map();
2212
+ for (const e of world.queryTagged(require_hooks.Selected)) {
2213
+ const t = world.getComponent(e, require_hooks.Transform2D);
2214
+ if (t) startPositions.set(e, {
2215
+ x: t.x,
2216
+ y: t.y
2217
+ });
2218
+ }
2219
+ for (const e of startPositions.keys()) world.addTag(e, require_hooks.Dragging);
2220
+ commandBuffer.beginGroup();
2221
+ inputState = {
2222
+ mode: "dragging",
2223
+ entityId: inputState.entityId,
2224
+ startScreenX: screenX,
2225
+ startScreenY: screenY,
2226
+ startPositions,
2227
+ originalZIndices
2228
+ };
2229
+ markDirtyInternal();
2230
+ return { action: "capture-drag" };
2231
+ }
2232
+ return { action: "passthrough" };
2233
+ }
2234
+ if (inputState.mode === "dragging") {
2235
+ const camera = world.getResource(require_hooks.CameraResource);
2236
+ const totalDx = (screenX - inputState.startScreenX) / camera.zoom;
2237
+ const totalDy = (screenY - inputState.startScreenY) / camera.zoom;
2238
+ if (snapEnabled && inputState.startPositions.size > 0) {
2239
+ const draggedIds = new Set(inputState.startPositions.keys());
2240
+ const firstId = inputState.startPositions.keys().next().value;
2241
+ const firstStart = inputState.startPositions.get(firstId);
2242
+ const firstT = world.getComponent(firstId, require_hooks.Transform2D);
2243
+ if (firstT && firstStart) {
2244
+ const draggedBounds = {
2245
+ x: firstStart.x + totalDx,
2246
+ y: firstStart.y + totalDy,
2247
+ width: firstT.width,
2248
+ height: firstT.height
2249
+ };
2250
+ const refs = [];
2251
+ for (const entity of world.queryTagged(require_hooks.Active)) {
2252
+ if (draggedIds.has(entity)) continue;
2253
+ if (world.hasComponent(entity, require_hooks.Hitbox)) continue;
2254
+ const wb = world.getComponent(entity, require_hooks.WorldBounds);
2255
+ if (wb) refs.push({
2256
+ x: wb.worldX,
2257
+ y: wb.worldY,
2258
+ width: wb.worldWidth,
2259
+ height: wb.worldHeight
2260
+ });
2261
+ }
2262
+ currentSnap = computeSnapGuides(draggedBounds, refs, snapThreshold / camera.zoom);
2263
+ }
2264
+ } else currentSnap = {
2265
+ snapDx: 0,
2266
+ snapDy: 0,
2267
+ guides: [],
2268
+ spacings: []
2269
+ };
2270
+ const finalDx = totalDx + currentSnap.snapDx;
2271
+ const finalDy = totalDy + currentSnap.snapDy;
2272
+ for (const [e, start] of inputState.startPositions) world.setComponent(e, require_hooks.Transform2D, {
2273
+ x: start.x + finalDx,
2274
+ y: start.y + finalDy
2275
+ });
2276
+ markDirtyInternal();
2277
+ return { action: "capture-drag" };
2278
+ }
2279
+ if (inputState.mode === "resizing") {
2280
+ const camera = world.getResource(require_hooks.CameraResource);
2281
+ const dx = (screenX - inputState.startX) / camera.zoom;
2282
+ const dy = (screenY - inputState.startY) / camera.zoom;
2283
+ const { x, y, width: w, height: h } = inputState.startBounds;
2284
+ const handle = inputState.handle;
2285
+ let newX = x;
2286
+ let newY = y;
2287
+ let newW = w;
2288
+ let newH = h;
2289
+ if (handle.includes("e")) newW = Math.max(20, w + dx);
2290
+ if (handle.includes("w")) {
2291
+ const clampedW = Math.max(20, w - dx);
2292
+ newX = x + w - clampedW;
2293
+ newW = clampedW;
2294
+ }
2295
+ if (handle.includes("s")) newH = Math.max(20, h + dy);
2296
+ if (handle.includes("n")) {
2297
+ const clampedH = Math.max(20, h - dy);
2298
+ newY = y + h - clampedH;
2299
+ newH = clampedH;
2300
+ }
2301
+ world.setComponent(inputState.entityId, require_hooks.Transform2D, {
2302
+ x: newX,
2303
+ y: newY,
2304
+ width: newW,
2305
+ height: newH
2306
+ });
2307
+ markDirtyInternal();
2308
+ return {
2309
+ action: "capture-resize",
2310
+ handle: inputState.handle
2311
+ };
2312
+ }
2313
+ if (inputState.mode === "marquee") return { action: "capture-marquee" };
2314
+ if (inputState.mode === "idle") {
2315
+ const hit = hitTest(screenX, screenY);
2316
+ const hoverTarget = hit ? hit.entityId : null;
2317
+ if (hoverTarget !== hoveredEntity) {
2318
+ hoveredEntity = hoverTarget;
2319
+ markDirtyInternal();
2320
+ }
2321
+ }
2322
+ return { action: "passthrough" };
2323
+ },
2324
+ handlePointerUp() {
2325
+ const prevState = inputState;
2326
+ if (prevState.mode === "dragging") {
2327
+ for (const e of prevState.startPositions.keys()) if (world.hasTag(e, require_hooks.Dragging)) world.removeTag(e, require_hooks.Dragging);
2328
+ for (const [entity, originalZ] of prevState.originalZIndices) world.setComponent(entity, require_hooks.ZIndex, { value: originalZ });
2329
+ const entityIds = [...prevState.startPositions.keys()];
2330
+ if (entityIds.length > 0) {
2331
+ const firstId = entityIds[0];
2332
+ const start = prevState.startPositions.get(firstId);
2333
+ const current = world.getComponent(firstId, require_hooks.Transform2D);
2334
+ if (current && start) {
2335
+ const totalDx = current.x - start.x;
2336
+ const totalDy = current.y - start.y;
2337
+ if (totalDx !== 0 || totalDy !== 0) {
2338
+ for (const [e, s] of prevState.startPositions) world.setComponent(e, require_hooks.Transform2D, {
2339
+ x: s.x,
2340
+ y: s.y
2341
+ });
2342
+ commandBuffer.execute(new MoveCommand(entityIds, totalDx, totalDy, require_hooks.Transform2D), world);
2343
+ }
2344
+ }
2345
+ }
2346
+ commandBuffer.endGroup();
2347
+ currentSnap = {
2348
+ snapDx: 0,
2349
+ snapDy: 0,
2350
+ guides: [],
2351
+ spacings: []
2352
+ };
2353
+ }
2354
+ if (prevState.mode === "resizing") {
2355
+ const t = world.getComponent(prevState.entityId, require_hooks.Transform2D);
2356
+ if (t) {
2357
+ const finalBounds = {
2358
+ x: t.x,
2359
+ y: t.y,
2360
+ width: t.width,
2361
+ height: t.height
2362
+ };
2363
+ const sb = prevState.startBounds;
2364
+ world.setComponent(prevState.entityId, require_hooks.Transform2D, sb);
2365
+ commandBuffer.execute(new ResizeCommand(prevState.entityId, sb, finalBounds, require_hooks.Transform2D), world);
2366
+ }
2367
+ commandBuffer.endGroup();
2368
+ }
2369
+ inputState = { mode: "idle" };
2370
+ if (prevState.mode === "dragging" || prevState.mode === "resizing") markDirtyInternal();
2371
+ return { action: "passthrough" };
2372
+ },
2373
+ handlePointerCancel() {
2374
+ if (inputState.mode === "dragging" || inputState.mode === "resizing") commandBuffer.endGroup();
2375
+ if (inputState.mode === "dragging") {
2376
+ for (const e of inputState.startPositions.keys()) if (world.hasTag(e, require_hooks.Dragging)) world.removeTag(e, require_hooks.Dragging);
2377
+ }
2378
+ currentSnap = {
2379
+ snapDx: 0,
2380
+ snapDy: 0,
2381
+ guides: [],
2382
+ spacings: []
2383
+ };
2384
+ inputState = { mode: "idle" };
2385
+ markDirtyInternal();
2386
+ },
2387
+ getSelectedEntities() {
2388
+ return world.queryTagged(require_hooks.Selected);
2389
+ },
2390
+ getHoveredEntity() {
2391
+ return hoveredEntity;
2392
+ },
2393
+ getSnapGuides() {
2394
+ return currentSnap.guides;
2395
+ },
2396
+ getEqualSpacing() {
2397
+ return currentSnap.spacings;
2398
+ },
2399
+ setSnapEnabled(on) {
2400
+ snapEnabled = on;
2401
+ },
2402
+ setSnapThreshold(worldPx) {
2403
+ snapThreshold = worldPx;
2404
+ },
2405
+ enterContainer(entity) {
2406
+ if (!world.hasComponent(entity, require_hooks.Container)) return;
2407
+ if (!world.hasComponent(entity, require_hooks.Children)) return;
2408
+ const navStack = world.getResource(require_hooks.NavigationStackResource);
2409
+ const camera = world.getResource(require_hooks.CameraResource);
2410
+ const currentFrame = navStack.frames[navStack.frames.length - 1];
2411
+ currentFrame.camera = {
2412
+ x: camera.x,
2413
+ y: camera.y,
2414
+ zoom: camera.zoom
2415
+ };
2416
+ navStack.frames.push({
2417
+ containerId: entity,
2418
+ camera: {
2419
+ x: camera.x,
2420
+ y: camera.y,
2421
+ zoom: camera.zoom
2422
+ }
2423
+ });
2424
+ navStack.changed = true;
2425
+ clearSelection();
2426
+ markDirtyInternal();
2427
+ },
2428
+ exitContainer() {
2429
+ const navStack = world.getResource(require_hooks.NavigationStackResource);
2430
+ if (navStack.frames.length <= 1) return;
2431
+ navStack.frames.pop();
2432
+ navStack.changed = true;
2433
+ const parentFrame = navStack.frames[navStack.frames.length - 1];
2434
+ const camera = world.getResource(require_hooks.CameraResource);
2435
+ camera.x = parentFrame.camera.x;
2436
+ camera.y = parentFrame.camera.y;
2437
+ camera.zoom = parentFrame.camera.zoom;
2438
+ clearSelection();
2439
+ cameraChangedThisTick = true;
2440
+ markDirtyInternal();
2441
+ },
2442
+ getActiveContainer() {
2443
+ const navStack = world.getResource(require_hooks.NavigationStackResource);
2444
+ return navStack.frames[navStack.frames.length - 1].containerId;
2445
+ },
2446
+ getNavigationDepth() {
2447
+ return world.getResource(require_hooks.NavigationStackResource).frames.length - 1;
2448
+ },
2449
+ markDirty() {
2450
+ markDirtyInternal();
2451
+ },
2452
+ profiler,
2453
+ tick() {
2454
+ profiler.beginFrame(world.currentTick);
2455
+ const navigationChangedThisTick = world.getResource(require_hooks.NavigationStackResource)?.changed ?? false;
2456
+ scheduler.execute(world);
2457
+ cursorSystem();
2458
+ profiler.beginVisibility();
2459
+ const newVisible = [];
2460
+ const newVisibleSet = /* @__PURE__ */ new Set();
2461
+ for (const entity of world.query(require_hooks.Widget, require_hooks.Visible)) {
2462
+ const wb = world.getComponent(entity, require_hooks.WorldBounds);
2463
+ const widget = world.getComponent(entity, require_hooks.Widget);
2464
+ const bp = world.getComponent(entity, require_hooks.WidgetBreakpoint);
2465
+ const zIdx = world.getComponent(entity, require_hooks.ZIndex);
2466
+ if (!wb || !widget) continue;
2467
+ newVisibleSet.add(entity);
2468
+ newVisible.push({
2469
+ entityId: entity,
2470
+ worldX: wb.worldX,
2471
+ worldY: wb.worldY,
2472
+ worldWidth: wb.worldWidth,
2473
+ worldHeight: wb.worldHeight,
2474
+ breakpoint: bp?.current ?? "normal",
2475
+ zIndex: zIdx?.value ?? 0,
2476
+ surface: widget.surface,
2477
+ widgetType: widget.type
2478
+ });
2479
+ }
2480
+ newVisible.sort((a, b) => a.zIndex - b.zIndex);
2481
+ profiler.endVisibility();
2482
+ const entered = [];
2483
+ const exited = [];
2484
+ for (const entity of newVisibleSet) if (!prevVisible.has(entity)) entered.push(entity);
2485
+ for (const entity of prevVisible) if (!newVisibleSet.has(entity)) exited.push(entity);
2486
+ frameChanges = {
2487
+ positionsChanged: world.queryChanged(require_hooks.WorldBounds),
2488
+ breakpointsChanged: world.queryChanged(require_hooks.WidgetBreakpoint),
2489
+ entered,
2490
+ exited,
2491
+ cameraChanged: cameraChangedThisTick,
2492
+ navigationChanged: navigationChangedThisTick,
2493
+ selectionChanged: selectionChangedThisTick
2494
+ };
2495
+ currentVisible = newVisible;
2496
+ prevVisible = newVisibleSet;
2497
+ cameraChangedThisTick = false;
2498
+ selectionChangedThisTick = false;
2499
+ profiler.endFrame(world.entityCount, newVisible.length);
2500
+ world.clearDirty();
2501
+ world.incrementTick();
2502
+ world.emitFrame();
2503
+ dirty = false;
2504
+ },
2505
+ flushIfDirty() {
2506
+ if (!dirty) return false;
2507
+ engine.tick();
2508
+ return true;
2509
+ },
2510
+ getVisibleEntities() {
2511
+ return currentVisible;
2512
+ },
2513
+ getFrameChanges() {
2514
+ return frameChanges;
2515
+ },
2516
+ getSpatialIndex() {
2517
+ return spatialIndex;
2518
+ },
2519
+ onFrame(handler) {
2520
+ return world.onFrame(handler);
2521
+ },
2522
+ destroy() {
2523
+ for (const unsub of unsubscribers) unsub();
2524
+ unsubscribers.length = 0;
2525
+ commandBuffer.clear();
2526
+ profiler.setEnabled(false);
2527
+ profiler.clear();
2528
+ spatialIndex.clear();
2529
+ }
2530
+ };
2531
+ return engine;
2532
+ }
2533
+ //#endregion
2534
+ //#region src/react/SelectionOverlaySlot.tsx
2535
+ function getMods$1(e) {
2536
+ return {
2537
+ shift: e.shiftKey,
2538
+ ctrl: e.ctrlKey,
2539
+ alt: e.altKey,
2540
+ meta: e.metaKey
2541
+ };
2542
+ }
2543
+ /**
2544
+ * DOM overlay for WebGL widgets — provides selection frame and pointer
2545
+ * interaction (select, drag, resize) without rendering widget content.
2546
+ */
2547
+ const SelectionOverlaySlot = (0, react.memo)(function SelectionOverlaySlot({ entityId, slotRef }) {
2548
+ const wrapperRef = (0, react.useRef)(null);
2549
+ const engine = require_hooks.useLayoutEngine();
2550
+ const containerRefObj = require_hooks.useContainerRef();
2551
+ (0, react.useEffect)(() => {
2552
+ slotRef(entityId, wrapperRef.current);
2553
+ return () => slotRef(entityId, null);
2554
+ }, [entityId, slotRef]);
2555
+ const toLocal = (0, react.useCallback)((e) => {
2556
+ const rect = containerRefObj?.current?.getBoundingClientRect();
2557
+ if (!rect) return {
2558
+ x: e.clientX,
2559
+ y: e.clientY
2560
+ };
2561
+ return {
2562
+ x: e.clientX - rect.left,
2563
+ y: e.clientY - rect.top
2564
+ };
2565
+ }, [containerRefObj]);
2566
+ const capturedRef = (0, react.useRef)(false);
2567
+ const onPointerDown = (0, react.useCallback)((e) => {
2568
+ e.stopPropagation();
2569
+ const { x, y } = toLocal(e);
2570
+ const directive = engine.handlePointerDown(x, y, e.button, getMods$1(e));
2571
+ if (directive.action === "capture-resize" || directive.action === "passthrough-track-drag") wrapperRef.current?.setPointerCapture(e.pointerId);
2572
+ if (directive.action === "capture-resize") e.preventDefault();
2573
+ }, [engine, toLocal]);
2574
+ const onPointerMove = (0, react.useCallback)((e) => {
2575
+ const { x, y } = toLocal(e);
2576
+ if (engine.handlePointerMove(x, y, getMods$1(e)).action === "capture-drag" && !capturedRef.current) {
2577
+ capturedRef.current = true;
2578
+ e.stopPropagation();
2579
+ }
2580
+ }, [engine, toLocal]);
2581
+ const onPointerUp = (0, react.useCallback)((e) => {
2582
+ e.stopPropagation();
2583
+ capturedRef.current = false;
2584
+ if (wrapperRef.current?.hasPointerCapture(e.pointerId)) wrapperRef.current.releasePointerCapture(e.pointerId);
2585
+ engine.handlePointerUp();
2586
+ }, [engine]);
2587
+ const onDoubleClick = (0, react.useCallback)((e) => {
2588
+ e.stopPropagation();
2589
+ engine.enterContainer(entityId);
2590
+ }, [engine, entityId]);
2591
+ const wb = engine.get(entityId, require_hooks.WorldBounds);
2592
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2593
+ ref: wrapperRef,
2594
+ className: "absolute left-0 top-0 origin-top-left will-change-transform",
2595
+ "data-widget-slot": "",
2596
+ style: wb ? {
2597
+ transform: `translate(${wb.worldX}px, ${wb.worldY}px)`,
2598
+ width: `${wb.worldWidth}px`,
2599
+ height: `${wb.worldHeight}px`
2600
+ } : {},
2601
+ onPointerDown,
2602
+ onPointerMove,
2603
+ onPointerUp,
2604
+ onDoubleClick
2605
+ });
2606
+ });
2607
+ //#endregion
2608
+ //#region src/react/WidgetSlot.tsx
2609
+ function getMods(e) {
2610
+ return {
2611
+ shift: e.shiftKey,
2612
+ ctrl: e.ctrlKey,
2613
+ alt: e.altKey,
2614
+ meta: e.metaKey
2615
+ };
2616
+ }
2617
+ const WidgetSlot = (0, react.memo)(function WidgetSlot({ entityId, slotRef }) {
2618
+ const wrapperRef = (0, react.useRef)(null);
2619
+ const engine = require_hooks.useLayoutEngine();
2620
+ const containerRefObj = require_hooks.useContainerRef();
2621
+ const resolve = require_hooks.useWidgetResolver();
2622
+ const widgetComp = require_hooks.useComponent(entityId, require_hooks.Widget);
2623
+ const resolved = resolve?.(entityId, widgetComp?.type ?? "");
2624
+ const WidgetComponent = resolved && resolved.surface === "dom" ? resolved.component : null;
2625
+ (0, react.useEffect)(() => {
2626
+ slotRef(entityId, wrapperRef.current);
2627
+ return () => slotRef(entityId, null);
2628
+ }, [entityId, slotRef]);
2629
+ const toLocal = (0, react.useCallback)((e) => {
2630
+ const rect = containerRefObj?.current?.getBoundingClientRect();
2631
+ if (!rect) return {
2632
+ x: e.clientX,
2633
+ y: e.clientY
2634
+ };
2635
+ return {
2636
+ x: e.clientX - rect.left,
2637
+ y: e.clientY - rect.top
2638
+ };
2639
+ }, [containerRefObj]);
2640
+ const onPointerDown = (0, react.useCallback)((e) => {
2641
+ if (e.target.closest("button, input, textarea, select, [contenteditable]")) {
2642
+ e.stopPropagation();
2643
+ return;
2644
+ }
2645
+ const { x, y } = toLocal(e);
2646
+ const directive = engine.handlePointerDown(x, y, e.button, getMods(e));
2647
+ e.stopPropagation();
2648
+ if (directive.action === "capture-resize" || directive.action === "passthrough-track-drag") wrapperRef.current?.setPointerCapture(e.pointerId);
2649
+ if (directive.action === "capture-resize") e.preventDefault();
2650
+ }, [engine, toLocal]);
2651
+ const capturedRef = (0, react.useRef)(false);
2652
+ const onPointerMove = (0, react.useCallback)((e) => {
2653
+ const { x, y } = toLocal(e);
2654
+ if (engine.handlePointerMove(x, y, getMods(e)).action === "capture-drag" && !capturedRef.current) {
2655
+ capturedRef.current = true;
2656
+ e.stopPropagation();
2657
+ }
2658
+ }, [engine, toLocal]);
2659
+ const onPointerUp = (0, react.useCallback)((e) => {
2660
+ e.stopPropagation();
2661
+ capturedRef.current = false;
2662
+ if (wrapperRef.current?.hasPointerCapture(e.pointerId)) wrapperRef.current.releasePointerCapture(e.pointerId);
2663
+ engine.handlePointerUp();
2664
+ }, [engine]);
2665
+ const onDoubleClick = (0, react.useCallback)((e) => {
2666
+ e.stopPropagation();
2667
+ engine.enterContainer(entityId);
2668
+ }, [engine, entityId]);
2669
+ const wb = engine.get(entityId, require_hooks.WorldBounds);
2670
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2671
+ ref: wrapperRef,
2672
+ "data-widget-slot": "",
2673
+ className: "absolute left-0 top-0 origin-top-left will-change-transform",
2674
+ style: wb ? {
2675
+ transform: `translate(${wb.worldX}px, ${wb.worldY}px)`,
2676
+ width: `${wb.worldWidth}px`,
2677
+ height: `${wb.worldHeight}px`
2678
+ } : {},
2679
+ onPointerDown,
2680
+ onPointerMove,
2681
+ onPointerUp,
2682
+ onDoubleClick,
2683
+ children: WidgetComponent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetComponent, { entityId }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-full w-full rounded border border-dashed border-gray-300 bg-gray-50" })
2684
+ });
2685
+ });
2686
+ //#endregion
2687
+ //#region src/react/webgl/GridRenderer.ts
2688
+ const DEFAULT_GRID_CONFIG = {
2689
+ spacings: [
2690
+ 8,
2691
+ 64,
2692
+ 512
2693
+ ],
2694
+ dotColor: [
2695
+ 0,
2696
+ 0,
2697
+ 0
2698
+ ],
2699
+ dotAlpha: .18,
2700
+ fadeIn: [4, 12],
2701
+ fadeOut: [250, 500],
2702
+ dotRadius: [.5, 1.4],
2703
+ levelWeight: [1, .4]
2704
+ };
2705
+ const vertexShader$1 = `
2706
+ void main() {
2707
+ gl_Position = vec4(position.xy, 0.0, 1.0);
2708
+ }
2709
+ `;
2710
+ const fragmentShader$1 = `
2711
+ precision highp float;
2712
+
2713
+ uniform vec2 u_resolution; // device pixels
2714
+ uniform vec2 u_camera; // world-space top-left
2715
+ uniform float u_zoom; // CSS zoom
2716
+ uniform float u_dpr; // device pixel ratio
2717
+ uniform vec3 u_spacings; // world-unit grid spacings
2718
+ uniform vec3 u_dotColor; // dot RGB
2719
+ uniform float u_dotAlpha; // dot base alpha
2720
+ uniform vec2 u_fadeIn; // CSS-px [start, end]
2721
+ uniform vec2 u_fadeOut; // CSS-px [start, end]
2722
+ uniform vec2 u_dotRadius; // CSS-px [min, max]
2723
+ uniform vec2 u_levelWeight; // [base, step]
2724
+
2725
+ void main() {
2726
+ vec2 devicePos = gl_FragCoord.xy;
2727
+ devicePos.y = u_resolution.y - devicePos.y;
2728
+
2729
+ float effectiveZoom = u_zoom * u_dpr;
2730
+ vec2 worldPos = devicePos / effectiveZoom + u_camera;
2731
+
2732
+ float totalAlpha = 0.0;
2733
+
2734
+ for (int i = 0; i < 3; i++) {
2735
+ float spacing;
2736
+ if (i == 0) spacing = u_spacings.x;
2737
+ else if (i == 1) spacing = u_spacings.y;
2738
+ else spacing = u_spacings.z;
2739
+
2740
+ // Screen spacing in CSS pixels (DPR-independent for consistent fading)
2741
+ float cssSpacing = spacing * u_zoom;
2742
+
2743
+ // Fade curve
2744
+ float opacity = 0.0;
2745
+ if (cssSpacing >= u_fadeIn.x && cssSpacing < u_fadeIn.y) {
2746
+ opacity = (cssSpacing - u_fadeIn.x) / (u_fadeIn.y - u_fadeIn.x);
2747
+ } else if (cssSpacing >= u_fadeIn.y && cssSpacing < u_fadeOut.x) {
2748
+ opacity = 1.0;
2749
+ } else if (cssSpacing >= u_fadeOut.x && cssSpacing < u_fadeOut.y) {
2750
+ opacity = 1.0 - (cssSpacing - u_fadeOut.x) / (u_fadeOut.y - u_fadeOut.x);
2751
+ }
2752
+ if (opacity <= 0.001) continue;
2753
+
2754
+ // Distance to nearest grid intersection in device pixels
2755
+ vec2 f = fract(worldPos / spacing + 0.5) - 0.5;
2756
+ float dist = length(f) * spacing * effectiveZoom;
2757
+
2758
+ // Dot radius in device pixels — grows as grid becomes sparser
2759
+ float t = clamp((cssSpacing - u_fadeIn.x) / 40.0, 0.0, 1.0);
2760
+ float radius = mix(u_dotRadius.x, u_dotRadius.y, t) * u_dpr;
2761
+
2762
+ // Anti-aliased dot (0.5 device pixel smoothstep)
2763
+ float dot = 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist);
2764
+
2765
+ // Larger grid levels get progressively stronger dots
2766
+ float weight = u_levelWeight.x + float(i) * u_levelWeight.y;
2767
+ totalAlpha += dot * opacity * weight;
2768
+ }
2769
+
2770
+ gl_FragColor = vec4(u_dotColor, clamp(totalAlpha * u_dotAlpha, 0.0, 1.0));
2771
+ }
2772
+ `;
2773
+ var GridRenderer = class {
2774
+ renderer;
2775
+ scene;
2776
+ camera;
2777
+ material;
2778
+ mesh;
2779
+ constructor(canvas) {
2780
+ this.renderer = new three$1.WebGLRenderer({
2781
+ canvas,
2782
+ alpha: true,
2783
+ antialias: false,
2784
+ premultipliedAlpha: false
2785
+ });
2786
+ this.renderer.setClearColor(0, 0);
2787
+ this.renderer.info.autoReset = false;
2788
+ this.scene = new three$1.Scene();
2789
+ this.camera = new three$1.OrthographicCamera(-1, 1, 1, -1, 0, 1);
2790
+ this.material = new three$1.ShaderMaterial({
2791
+ vertexShader: vertexShader$1,
2792
+ fragmentShader: fragmentShader$1,
2793
+ uniforms: {
2794
+ u_resolution: { value: new three$1.Vector2(1, 1) },
2795
+ u_camera: { value: new three$1.Vector2(0, 0) },
2796
+ u_zoom: { value: 1 },
2797
+ u_dpr: { value: 1 },
2798
+ u_spacings: { value: new three$1.Vector3(8, 64, 512) },
2799
+ u_dotColor: { value: new three$1.Vector3(0, 0, 0) },
2800
+ u_dotAlpha: { value: .18 },
2801
+ u_fadeIn: { value: new three$1.Vector2(4, 12) },
2802
+ u_fadeOut: { value: new three$1.Vector2(250, 500) },
2803
+ u_dotRadius: { value: new three$1.Vector2(.5, 1.4) },
2804
+ u_levelWeight: { value: new three$1.Vector2(1, .4) }
2805
+ },
2806
+ transparent: true,
2807
+ depthTest: false,
2808
+ depthWrite: false
2809
+ });
2810
+ const geometry = new three$1.BufferGeometry();
2811
+ const vertices = new Float32Array([
2812
+ -1,
2813
+ -1,
2814
+ 0,
2815
+ 3,
2816
+ -1,
2817
+ 0,
2818
+ -1,
2819
+ 3,
2820
+ 0
2821
+ ]);
2822
+ geometry.setAttribute("position", new three$1.BufferAttribute(vertices, 3));
2823
+ this.mesh = new three$1.Mesh(geometry, this.material);
2824
+ this.scene.add(this.mesh);
2825
+ }
2826
+ /** Apply a (partial) grid config. Only provided fields are updated. */
2827
+ setConfig(config) {
2828
+ const u = this.material.uniforms;
2829
+ if (config.spacings) u.u_spacings.value.set(...config.spacings);
2830
+ if (config.dotColor) u.u_dotColor.value.set(...config.dotColor);
2831
+ if (config.dotAlpha !== void 0) u.u_dotAlpha.value = config.dotAlpha;
2832
+ if (config.fadeIn) u.u_fadeIn.value.set(...config.fadeIn);
2833
+ if (config.fadeOut) u.u_fadeOut.value.set(...config.fadeOut);
2834
+ if (config.dotRadius) u.u_dotRadius.value.set(...config.dotRadius);
2835
+ if (config.levelWeight) u.u_levelWeight.value.set(...config.levelWeight);
2836
+ }
2837
+ setSize(width, height, dpr = 1) {
2838
+ this.renderer.setSize(width, height, false);
2839
+ this.renderer.setPixelRatio(dpr);
2840
+ const u = this.material.uniforms;
2841
+ u.u_resolution.value.set(width * dpr, height * dpr);
2842
+ u.u_dpr.value = dpr;
2843
+ }
2844
+ render(cameraX, cameraY, zoom) {
2845
+ const u = this.material.uniforms;
2846
+ u.u_camera.value.set(cameraX, cameraY);
2847
+ u.u_zoom.value = zoom;
2848
+ this.renderer.render(this.scene, this.camera);
2849
+ }
2850
+ dispose() {
2851
+ this.mesh.geometry.dispose();
2852
+ this.material.dispose();
2853
+ this.renderer.dispose();
2854
+ }
2855
+ /** Expose for future WebGL widget rendering */
2856
+ getWebGLRenderer() {
2857
+ return this.renderer;
2858
+ }
2859
+ };
2860
+ //#endregion
2861
+ //#region src/react/webgl/SelectionRenderer.ts
2862
+ const DEFAULT_SELECTION_CONFIG = {
2863
+ outlineColor: [
2864
+ .051,
2865
+ .6,
2866
+ 1
2867
+ ],
2868
+ outlineWidth: 1.5,
2869
+ hoverColor: [
2870
+ .051,
2871
+ .6,
2872
+ 1
2873
+ ],
2874
+ hoverWidth: 1,
2875
+ handleSize: 8,
2876
+ handleFill: [
2877
+ 1,
2878
+ 1,
2879
+ 1
2880
+ ],
2881
+ handleBorder: [
2882
+ .051,
2883
+ .6,
2884
+ 1
2885
+ ],
2886
+ handleBorderWidth: 1.5,
2887
+ groupDash: 4
2888
+ };
2889
+ const MAX_ENTITIES = 32;
2890
+ const vertexShader = `
2891
+ void main() {
2892
+ gl_Position = vec4(position.xy, 0.0, 1.0);
2893
+ }
2894
+ `;
2895
+ const fragmentShader = `
2896
+ precision highp float;
2897
+
2898
+ uniform vec2 u_resolution;
2899
+ uniform vec2 u_camera;
2900
+ uniform float u_zoom;
2901
+ uniform float u_dpr;
2902
+
2903
+ // Selection data
2904
+ uniform int u_count;
2905
+ uniform vec4 u_bounds[${MAX_ENTITIES}]; // (worldX, worldY, width, height)
2906
+ uniform int u_hoverIdx; // -1 = none
2907
+ uniform vec4 u_groupBounds; // group bbox (0 if count <= 1)
2908
+ uniform int u_hasGroup;
2909
+
2910
+ // Snap guides
2911
+ uniform int u_guideCount;
2912
+ uniform vec4 u_guides[16]; // (axis: 0=x/1=y, position, 0, 0)
2913
+ uniform int u_spacingCount;
2914
+ uniform vec4 u_spacings[8]; // equal-spacing segments: (axis, from, to, perpPos)
2915
+ uniform vec3 u_guideColor;
2916
+
2917
+ // Style
2918
+ uniform vec3 u_outlineColor;
2919
+ uniform float u_outlineWidth;
2920
+ uniform vec3 u_hoverColor;
2921
+ uniform float u_hoverWidth;
2922
+ uniform float u_handleSize;
2923
+ uniform vec3 u_handleFill;
2924
+ uniform vec3 u_handleBorder;
2925
+ uniform float u_handleBorderWidth;
2926
+ uniform float u_groupDash;
2927
+
2928
+ // SDF for axis-aligned rectangle outline (returns distance to edge)
2929
+ float sdRectOutline(vec2 p, vec2 center, vec2 halfSize) {
2930
+ vec2 d = abs(p - center) - halfSize;
2931
+ float outside = length(max(d, 0.0));
2932
+ float inside = min(max(d.x, d.y), 0.0);
2933
+ return abs(outside + inside);
2934
+ }
2935
+
2936
+ // SDF for filled square
2937
+ float sdSquare(vec2 p, vec2 center, float halfSize) {
2938
+ vec2 d = abs(p - center) - vec2(halfSize);
2939
+ return max(d.x, d.y);
2940
+ }
2941
+
2942
+ void main() {
2943
+ if (u_count == 0 && u_hoverIdx < 0) discard;
2944
+
2945
+ vec2 devicePos = gl_FragCoord.xy;
2946
+ devicePos.y = u_resolution.y - devicePos.y;
2947
+
2948
+ float effectiveZoom = u_zoom * u_dpr;
2949
+ vec2 worldPos = devicePos / effectiveZoom + u_camera;
2950
+
2951
+ // Screen-space conversion factor
2952
+ float pxToWorld = 1.0 / effectiveZoom;
2953
+
2954
+ vec4 color = vec4(0.0);
2955
+
2956
+ // --- Hover outline ---
2957
+ if (u_hoverIdx >= 0 && u_hoverIdx < ${MAX_ENTITIES}) {
2958
+ vec4 b = u_bounds[u_hoverIdx];
2959
+ vec2 center = vec2(b.x + b.z * 0.5, b.y + b.w * 0.5);
2960
+ vec2 halfSize = vec2(b.z, b.w) * 0.5;
2961
+ float dist = sdRectOutline(worldPos, center, halfSize);
2962
+ float width = u_hoverWidth * pxToWorld;
2963
+ float alpha = 1.0 - smoothstep(width - pxToWorld * 0.5, width + pxToWorld * 0.5, dist);
2964
+ color = max(color, vec4(u_hoverColor, alpha * 0.6));
2965
+ }
2966
+
2967
+ // --- Selection outlines ---
2968
+ for (int i = 0; i < ${MAX_ENTITIES}; i++) {
2969
+ if (i >= u_count) break;
2970
+ vec4 b = u_bounds[i];
2971
+ vec2 center = vec2(b.x + b.z * 0.5, b.y + b.w * 0.5);
2972
+ vec2 halfSize = vec2(b.z, b.w) * 0.5;
2973
+
2974
+ // Outline
2975
+ float dist = sdRectOutline(worldPos, center, halfSize);
2976
+ float width = u_outlineWidth * pxToWorld;
2977
+ float outlineAlpha = 1.0 - smoothstep(width - pxToWorld * 0.5, width + pxToWorld * 0.5, dist);
2978
+ color = max(color, vec4(u_outlineColor, outlineAlpha));
2979
+
2980
+ // 8 resize handles
2981
+ float hs = u_handleSize * 0.5 * pxToWorld;
2982
+ float bw = u_handleBorderWidth * pxToWorld;
2983
+ vec2 corners[8];
2984
+ corners[0] = vec2(b.x, b.y); // nw
2985
+ corners[1] = vec2(b.x + b.z * 0.5, b.y); // n
2986
+ corners[2] = vec2(b.x + b.z, b.y); // ne
2987
+ corners[3] = vec2(b.x + b.z, b.y + b.w * 0.5); // e
2988
+ corners[4] = vec2(b.x + b.z, b.y + b.w); // se
2989
+ corners[5] = vec2(b.x + b.z * 0.5, b.y + b.w); // s
2990
+ corners[6] = vec2(b.x, b.y + b.w); // sw
2991
+ corners[7] = vec2(b.x, b.y + b.w * 0.5); // w
2992
+
2993
+ for (int h = 0; h < 8; h++) {
2994
+ float d = sdSquare(worldPos, corners[h], hs);
2995
+ // Fill (white)
2996
+ float fillAlpha = 1.0 - smoothstep(-pxToWorld * 0.5, pxToWorld * 0.5, d);
2997
+ // Border
2998
+ float borderDist = abs(d + bw * 0.5) - bw * 0.5;
2999
+ float borderAlpha = 1.0 - smoothstep(-pxToWorld * 0.5, pxToWorld * 0.5, borderDist);
3000
+
3001
+ if (fillAlpha > 0.01) {
3002
+ // Composite: border color on top of fill
3003
+ vec3 handleColor = mix(u_handleFill, u_handleBorder, borderAlpha);
3004
+ color = vec4(handleColor, max(fillAlpha, color.a));
3005
+ }
3006
+ }
3007
+ }
3008
+
3009
+ // --- Group bounding box (dashed) ---
3010
+ if (u_hasGroup == 1 && u_count > 1) {
3011
+ vec4 gb = u_groupBounds;
3012
+ vec2 center = vec2(gb.x + gb.z * 0.5, gb.y + gb.w * 0.5);
3013
+ vec2 halfSize = vec2(gb.z, gb.w) * 0.5;
3014
+ float dist = sdRectOutline(worldPos, center, halfSize);
3015
+ float width = u_outlineWidth * 0.75 * pxToWorld;
3016
+ float lineAlpha = 1.0 - smoothstep(width - pxToWorld * 0.5, width + pxToWorld * 0.5, dist);
3017
+
3018
+ // Dash pattern along the rectangle perimeter
3019
+ if (u_groupDash > 0.0 && lineAlpha > 0.01) {
3020
+ // Proper perimeter arc-length from top-left corner going clockwise
3021
+ float perim;
3022
+ vec2 rel = worldPos - vec2(gb.x, gb.y);
3023
+ float w = gb.z;
3024
+ float h = gb.w;
3025
+
3026
+ // Determine which edge is nearest and compute cumulative arc length
3027
+ float dTop = abs(rel.y);
3028
+ float dRight = abs(rel.x - w);
3029
+ float dBottom = abs(rel.y - h);
3030
+ float dLeft = abs(rel.x);
3031
+
3032
+ if (dTop <= dBottom && dTop <= dLeft && dTop <= dRight) {
3033
+ perim = rel.x; // top edge: 0 to w
3034
+ } else if (dRight <= dLeft) {
3035
+ perim = w + rel.y; // right edge: w to w+h
3036
+ } else if (dBottom <= dTop) {
3037
+ perim = w + h + (w - rel.x); // bottom edge: w+h to 2w+h
3038
+ } else {
3039
+ perim = 2.0 * w + h + (h - rel.y); // left edge: 2w+h to 2w+2h
3040
+ }
3041
+
3042
+ float dashWorld = u_groupDash * pxToWorld;
3043
+ float dashPattern = step(0.5, fract(perim / (dashWorld * 2.0)));
3044
+ lineAlpha *= dashPattern;
3045
+ }
3046
+
3047
+ color = max(color, vec4(u_outlineColor, lineAlpha * 0.5));
3048
+ }
3049
+
3050
+ // --- Snap guide lines ---
3051
+ for (int i = 0; i < 16; i++) {
3052
+ if (i >= u_guideCount) break;
3053
+ vec4 g = u_guides[i];
3054
+ float guideWidth = 0.5 * pxToWorld;
3055
+ float dist;
3056
+ if (g.x < 0.5) {
3057
+ // Vertical line (x-axis alignment)
3058
+ dist = abs(worldPos.x - g.y);
3059
+ } else {
3060
+ // Horizontal line (y-axis alignment)
3061
+ dist = abs(worldPos.y - g.y);
3062
+ }
3063
+ float guideAlpha = 1.0 - smoothstep(guideWidth - pxToWorld * 0.3, guideWidth + pxToWorld * 0.3, dist);
3064
+ color = max(color, vec4(u_guideColor, guideAlpha * 0.8));
3065
+ }
3066
+
3067
+ // --- Equal spacing indicators ---
3068
+ for (int i = 0; i < 8; i++) {
3069
+ if (i >= u_spacingCount) break;
3070
+ vec4 s = u_spacings[i];
3071
+ float lineWidth = 0.5 * pxToWorld;
3072
+ float segAlpha = 0.0;
3073
+ if (s.x < 0.5) {
3074
+ // Horizontal segment (x-axis gap)
3075
+ float yDist = abs(worldPos.y - s.w);
3076
+ float xInRange = step(s.y, worldPos.x) * step(worldPos.x, s.z);
3077
+ // Center line
3078
+ segAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, yDist)) * xInRange;
3079
+ // End bars (perpendicular marks at from and to)
3080
+ float barHeight = 4.0 * pxToWorld;
3081
+ float barFromDist = abs(worldPos.x - s.y);
3082
+ float barFromAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, barFromDist))
3083
+ * (1.0 - smoothstep(barHeight, barHeight + pxToWorld, abs(worldPos.y - s.w)));
3084
+ float barToDist = abs(worldPos.x - s.z);
3085
+ float barToAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, barToDist))
3086
+ * (1.0 - smoothstep(barHeight, barHeight + pxToWorld, abs(worldPos.y - s.w)));
3087
+ segAlpha = max(segAlpha, max(barFromAlpha, barToAlpha));
3088
+ } else {
3089
+ // Vertical segment (y-axis gap)
3090
+ float xDist = abs(worldPos.x - s.w);
3091
+ float yInRange = step(s.y, worldPos.y) * step(worldPos.y, s.z);
3092
+ segAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, xDist)) * yInRange;
3093
+ float barWidth = 4.0 * pxToWorld;
3094
+ float barFromAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, abs(worldPos.y - s.y)))
3095
+ * (1.0 - smoothstep(barWidth, barWidth + pxToWorld, abs(worldPos.x - s.w)));
3096
+ float barToAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, abs(worldPos.y - s.z)))
3097
+ * (1.0 - smoothstep(barWidth, barWidth + pxToWorld, abs(worldPos.x - s.w)));
3098
+ segAlpha = max(segAlpha, max(barFromAlpha, barToAlpha));
3099
+ }
3100
+ color = max(color, vec4(u_guideColor, segAlpha * 0.7));
3101
+ }
3102
+
3103
+ if (color.a < 0.01) discard;
3104
+ gl_FragColor = color;
3105
+ }
3106
+ `;
3107
+ var SelectionRenderer = class {
3108
+ material;
3109
+ mesh;
3110
+ scene;
3111
+ camera;
3112
+ constructor() {
3113
+ this.scene = new three$1.Scene();
3114
+ this.camera = new three$1.OrthographicCamera(-1, 1, 1, -1, 0, 1);
3115
+ const boundsDefault = [];
3116
+ for (let i = 0; i < MAX_ENTITIES; i++) boundsDefault.push(new three$1.Vector4(0, 0, 0, 0));
3117
+ this.material = new three$1.ShaderMaterial({
3118
+ vertexShader,
3119
+ fragmentShader,
3120
+ uniforms: {
3121
+ u_resolution: { value: new three$1.Vector2(1, 1) },
3122
+ u_camera: { value: new three$1.Vector2(0, 0) },
3123
+ u_zoom: { value: 1 },
3124
+ u_dpr: { value: 1 },
3125
+ u_count: { value: 0 },
3126
+ u_bounds: { value: boundsDefault },
3127
+ u_hoverIdx: { value: -1 },
3128
+ u_groupBounds: { value: new three$1.Vector4(0, 0, 0, 0) },
3129
+ u_hasGroup: { value: 0 },
3130
+ u_outlineColor: { value: new three$1.Vector3(...DEFAULT_SELECTION_CONFIG.outlineColor) },
3131
+ u_outlineWidth: { value: DEFAULT_SELECTION_CONFIG.outlineWidth },
3132
+ u_hoverColor: { value: new three$1.Vector3(...DEFAULT_SELECTION_CONFIG.hoverColor) },
3133
+ u_hoverWidth: { value: DEFAULT_SELECTION_CONFIG.hoverWidth },
3134
+ u_handleSize: { value: DEFAULT_SELECTION_CONFIG.handleSize },
3135
+ u_handleFill: { value: new three$1.Vector3(...DEFAULT_SELECTION_CONFIG.handleFill) },
3136
+ u_handleBorder: { value: new three$1.Vector3(...DEFAULT_SELECTION_CONFIG.handleBorder) },
3137
+ u_handleBorderWidth: { value: DEFAULT_SELECTION_CONFIG.handleBorderWidth },
3138
+ u_groupDash: { value: DEFAULT_SELECTION_CONFIG.groupDash },
3139
+ u_guideCount: { value: 0 },
3140
+ u_guides: { value: Array.from({ length: 16 }, () => new three$1.Vector4(0, 0, 0, 0)) },
3141
+ u_spacingCount: { value: 0 },
3142
+ u_spacings: { value: Array.from({ length: 8 }, () => new three$1.Vector4(0, 0, 0, 0)) },
3143
+ u_guideColor: { value: new three$1.Vector3(1, 0, .55) }
3144
+ },
3145
+ transparent: true,
3146
+ depthTest: false,
3147
+ depthWrite: false
3148
+ });
3149
+ const geometry = new three$1.BufferGeometry();
3150
+ const vertices = new Float32Array([
3151
+ -1,
3152
+ -1,
3153
+ 0,
3154
+ 3,
3155
+ -1,
3156
+ 0,
3157
+ -1,
3158
+ 3,
3159
+ 0
3160
+ ]);
3161
+ geometry.setAttribute("position", new three$1.BufferAttribute(vertices, 3));
3162
+ this.mesh = new three$1.Mesh(geometry, this.material);
3163
+ this.scene.add(this.mesh);
3164
+ }
3165
+ setConfig(config) {
3166
+ const u = this.material.uniforms;
3167
+ if (config.outlineColor) u.u_outlineColor.value.set(...config.outlineColor);
3168
+ if (config.outlineWidth !== void 0) u.u_outlineWidth.value = config.outlineWidth;
3169
+ if (config.hoverColor) u.u_hoverColor.value.set(...config.hoverColor);
3170
+ if (config.hoverWidth !== void 0) u.u_hoverWidth.value = config.hoverWidth;
3171
+ if (config.handleSize !== void 0) u.u_handleSize.value = config.handleSize;
3172
+ if (config.handleFill) u.u_handleFill.value.set(...config.handleFill);
3173
+ if (config.handleBorder) u.u_handleBorder.value.set(...config.handleBorder);
3174
+ if (config.handleBorderWidth !== void 0) u.u_handleBorderWidth.value = config.handleBorderWidth;
3175
+ if (config.groupDash !== void 0) u.u_groupDash.value = config.groupDash;
3176
+ }
3177
+ setSize(resolution, dpr) {
3178
+ this.material.uniforms.u_resolution.value.copy(resolution);
3179
+ this.material.uniforms.u_dpr.value = dpr;
3180
+ }
3181
+ render(renderer, cameraX, cameraY, zoom, selected, hovered, guides = [], spacings = []) {
3182
+ const u = this.material.uniforms;
3183
+ u.u_camera.value.set(cameraX, cameraY);
3184
+ u.u_zoom.value = zoom;
3185
+ const count = Math.min(selected.length, MAX_ENTITIES);
3186
+ u.u_count.value = count;
3187
+ for (let i = 0; i < count; i++) {
3188
+ const b = selected[i];
3189
+ u.u_bounds.value[i].set(b.x, b.y, b.width, b.height);
3190
+ }
3191
+ if (hovered && count < MAX_ENTITIES) {
3192
+ let hoverIdx = -1;
3193
+ for (let i = 0; i < count; i++) {
3194
+ const b = selected[i];
3195
+ if (b.x === hovered.x && b.y === hovered.y) {
3196
+ hoverIdx = i;
3197
+ break;
3198
+ }
3199
+ }
3200
+ if (hoverIdx < 0) {
3201
+ u.u_bounds.value[count].set(hovered.x, hovered.y, hovered.width, hovered.height);
3202
+ u.u_hoverIdx.value = count;
3203
+ } else u.u_hoverIdx.value = -1;
3204
+ } else u.u_hoverIdx.value = -1;
3205
+ if (count > 1) {
3206
+ let minX = Number.POSITIVE_INFINITY;
3207
+ let minY = Number.POSITIVE_INFINITY;
3208
+ let maxX = Number.NEGATIVE_INFINITY;
3209
+ let maxY = Number.NEGATIVE_INFINITY;
3210
+ for (let i = 0; i < count; i++) {
3211
+ const b = selected[i];
3212
+ minX = Math.min(minX, b.x);
3213
+ minY = Math.min(minY, b.y);
3214
+ maxX = Math.max(maxX, b.x + b.width);
3215
+ maxY = Math.max(maxY, b.y + b.height);
3216
+ }
3217
+ u.u_groupBounds.value.set(minX, minY, maxX - minX, maxY - minY);
3218
+ u.u_hasGroup.value = 1;
3219
+ } else u.u_hasGroup.value = 0;
3220
+ const gCount = Math.min(guides.length, 16);
3221
+ u.u_guideCount.value = gCount;
3222
+ for (let i = 0; i < gCount; i++) {
3223
+ const g = guides[i];
3224
+ u.u_guides.value[i].set(g.axis === "x" ? 0 : 1, g.position, 0, 0);
3225
+ }
3226
+ let sIdx = 0;
3227
+ for (const sp of spacings) for (const seg of sp.segments) {
3228
+ if (sIdx >= 8) break;
3229
+ u.u_spacings.value[sIdx].set(sp.axis === "x" ? 0 : 1, seg.from, seg.to, sp.perpPosition);
3230
+ sIdx++;
3231
+ }
3232
+ u.u_spacingCount.value = sIdx;
3233
+ const prevAutoClear = renderer.autoClear;
3234
+ renderer.autoClear = false;
3235
+ renderer.render(this.scene, this.camera);
3236
+ renderer.autoClear = prevAutoClear;
3237
+ }
3238
+ dispose() {
3239
+ this.mesh.geometry.dispose();
3240
+ this.material.dispose();
3241
+ }
3242
+ };
3243
+ //#endregion
3244
+ //#region src/react/webgl/WebGLWidgetSlot.tsx
3245
+ /**
3246
+ * Positions a Three.js Group at the entity's world-space center.
3247
+ * The widget component renders in local space: origin at center,
3248
+ * X right, Y up, dimensions = (width, height) in world units.
3249
+ */
3250
+ function WebGLWidgetSlot({ entityId, component: WidgetComponent }) {
3251
+ const groupRef = (0, react.useRef)(null);
3252
+ const engine = require_hooks.useLayoutEngine();
3253
+ const wb = require_hooks.useComponent(entityId, require_hooks.WorldBounds);
3254
+ (0, _react_three_fiber.useFrame)(() => {
3255
+ if (!groupRef.current) return;
3256
+ const bounds = engine.get(entityId, require_hooks.WorldBounds);
3257
+ if (!bounds) return;
3258
+ groupRef.current.position.set(bounds.worldX + bounds.worldWidth / 2, -(bounds.worldY + bounds.worldHeight / 2), 0);
3259
+ });
3260
+ if (!wb) return null;
3261
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("group", {
3262
+ ref: groupRef,
3263
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetComponent, {
3264
+ entityId,
3265
+ width: wb.worldWidth,
3266
+ height: wb.worldHeight
3267
+ })
3268
+ });
3269
+ }
3270
+ //#endregion
3271
+ //#region src/react/webgl/WebGLWidgetLayer.tsx
3272
+ /**
3273
+ * Reports one R3F frame sample per animation frame to the engine profiler.
3274
+ * Reads `renderer.info` from three.js — draw calls / triangles / memory /
3275
+ * programs — which is maintained by R3F's default render loop regardless
3276
+ * of whether we opt in. Only samples when the profiler is enabled.
3277
+ */
3278
+ function ProfilerProbe({ engine, widgetCount }) {
3279
+ const { gl } = (0, _react_three_fiber.useThree)();
3280
+ const prevTimeRef = (0, react.useRef)(null);
3281
+ const prevCallsRef = (0, react.useRef)(0);
3282
+ const prevTrianglesRef = (0, react.useRef)(0);
3283
+ const prevPointsRef = (0, react.useRef)(0);
3284
+ const prevLinesRef = (0, react.useRef)(0);
3285
+ (0, _react_three_fiber.useFrame)(() => {
3286
+ const profiler = engine.profiler;
3287
+ if (!profiler.isEnabled()) {
3288
+ prevTimeRef.current = null;
3289
+ return;
3290
+ }
3291
+ const now = performance.now();
3292
+ const dtMs = prevTimeRef.current === null ? 0 : now - prevTimeRef.current;
3293
+ prevTimeRef.current = now;
3294
+ const info = gl.info;
3295
+ const calls = info.render.calls;
3296
+ const triangles = info.render.triangles;
3297
+ const points = info.render.points;
3298
+ const lines = info.render.lines;
3299
+ const frameCalls = info.autoReset ? calls : Math.max(0, calls - prevCallsRef.current);
3300
+ const frameTris = info.autoReset ? triangles : Math.max(0, triangles - prevTrianglesRef.current);
3301
+ const framePoints = info.autoReset ? points : Math.max(0, points - prevPointsRef.current);
3302
+ const frameLines = info.autoReset ? lines : Math.max(0, lines - prevLinesRef.current);
3303
+ prevCallsRef.current = calls;
3304
+ prevTrianglesRef.current = triangles;
3305
+ prevPointsRef.current = points;
3306
+ prevLinesRef.current = lines;
3307
+ profiler.recordR3FFrame({
3308
+ dtMs,
3309
+ drawCalls: frameCalls,
3310
+ triangles: frameTris,
3311
+ points: framePoints,
3312
+ lines: frameLines,
3313
+ programs: info.programs?.length ?? 0,
3314
+ geometries: info.memory.geometries,
3315
+ textures: info.memory.textures,
3316
+ activeWidgets: widgetCount
3317
+ });
3318
+ });
3319
+ return null;
3320
+ }
3321
+ function syncCamera(camera, size, engine) {
3322
+ const cam = engine.getCamera();
3323
+ const ortho = camera;
3324
+ ortho.left = 0;
3325
+ ortho.right = size.width / cam.zoom;
3326
+ ortho.top = 0;
3327
+ ortho.bottom = -(size.height / cam.zoom);
3328
+ ortho.near = .1;
3329
+ ortho.far = 1e4;
3330
+ ortho.position.set(cam.x, -cam.y, 1e3);
3331
+ ortho.updateProjectionMatrix();
3332
+ }
3333
+ function CameraSync({ engine }) {
3334
+ const { camera, size } = (0, _react_three_fiber.useThree)();
3335
+ (0, react.useLayoutEffect)(() => {
3336
+ syncCamera(camera, size, engine);
3337
+ }, [
3338
+ camera,
3339
+ size,
3340
+ engine
3341
+ ]);
3342
+ (0, _react_three_fiber.useFrame)(() => {
3343
+ syncCamera(camera, size, engine);
3344
+ });
3345
+ return null;
3346
+ }
3347
+ function WebGLWidgetLayer({ engine, entities, resolve }) {
3348
+ const canvasRef = (0, react.useRef)(null);
3349
+ const initialCamera = (0, react.useMemo)(() => {
3350
+ const cam = new three.OrthographicCamera(0, 1, 0, -1, .1, 1e4);
3351
+ cam.position.set(0, 0, 1e3);
3352
+ return cam;
3353
+ }, []);
3354
+ const widgetEntries = (0, react.useMemo)(() => {
3355
+ const result = [];
3356
+ for (const id of entities) {
3357
+ const resolved = resolve(id);
3358
+ if (resolved && resolved.surface === "webgl") result.push({
3359
+ entityId: id,
3360
+ component: resolved.component
3361
+ });
3362
+ }
3363
+ return result;
3364
+ }, [entities, resolve]);
3365
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_three_fiber.Canvas, {
3366
+ ref: canvasRef,
3367
+ camera: initialCamera,
3368
+ frameloop: "always",
3369
+ gl: {
3370
+ alpha: true,
3371
+ antialias: true
3372
+ },
3373
+ style: {
3374
+ position: "absolute",
3375
+ inset: 0,
3376
+ pointerEvents: "none",
3377
+ zIndex: 1,
3378
+ display: widgetEntries.length === 0 ? "none" : "block"
3379
+ },
3380
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_hooks.EngineProvider, {
3381
+ value: engine,
3382
+ children: [
3383
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CameraSync, { engine }),
3384
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProfilerProbe, {
3385
+ engine,
3386
+ widgetCount: widgetEntries.length
3387
+ }),
3388
+ widgetEntries.map(({ entityId, component }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WebGLWidgetSlot, {
3389
+ entityId,
3390
+ component
3391
+ }, entityId))
3392
+ ]
3393
+ })
3394
+ });
3395
+ }
3396
+ //#endregion
3397
+ Object.defineProperty(exports, "CommandBuffer", {
3398
+ enumerable: true,
3399
+ get: function() {
3400
+ return CommandBuffer;
3401
+ }
3402
+ });
3403
+ Object.defineProperty(exports, "DEFAULT_GRID_CONFIG", {
3404
+ enumerable: true,
3405
+ get: function() {
3406
+ return DEFAULT_GRID_CONFIG;
3407
+ }
3408
+ });
3409
+ Object.defineProperty(exports, "DEFAULT_SELECTION_CONFIG", {
3410
+ enumerable: true,
3411
+ get: function() {
3412
+ return DEFAULT_SELECTION_CONFIG;
3413
+ }
3414
+ });
3415
+ Object.defineProperty(exports, "GridRenderer", {
3416
+ enumerable: true,
3417
+ get: function() {
3418
+ return GridRenderer;
3419
+ }
3420
+ });
3421
+ Object.defineProperty(exports, "MoveCommand", {
3422
+ enumerable: true,
3423
+ get: function() {
3424
+ return MoveCommand;
3425
+ }
3426
+ });
3427
+ Object.defineProperty(exports, "Profiler", {
3428
+ enumerable: true,
3429
+ get: function() {
3430
+ return Profiler;
3431
+ }
3432
+ });
3433
+ Object.defineProperty(exports, "ResizeCommand", {
3434
+ enumerable: true,
3435
+ get: function() {
3436
+ return ResizeCommand;
3437
+ }
3438
+ });
3439
+ Object.defineProperty(exports, "SelectionOverlaySlot", {
3440
+ enumerable: true,
3441
+ get: function() {
3442
+ return SelectionOverlaySlot;
3443
+ }
3444
+ });
3445
+ Object.defineProperty(exports, "SelectionRenderer", {
3446
+ enumerable: true,
3447
+ get: function() {
3448
+ return SelectionRenderer;
3449
+ }
3450
+ });
3451
+ Object.defineProperty(exports, "SetComponentCommand", {
3452
+ enumerable: true,
3453
+ get: function() {
3454
+ return SetComponentCommand;
3455
+ }
3456
+ });
3457
+ Object.defineProperty(exports, "SpatialIndex", {
3458
+ enumerable: true,
3459
+ get: function() {
3460
+ return SpatialIndex;
3461
+ }
3462
+ });
3463
+ Object.defineProperty(exports, "SpatialIndexResource", {
3464
+ enumerable: true,
3465
+ get: function() {
3466
+ return SpatialIndexResource;
3467
+ }
3468
+ });
3469
+ Object.defineProperty(exports, "WebGLWidgetLayer", {
3470
+ enumerable: true,
3471
+ get: function() {
3472
+ return WebGLWidgetLayer;
3473
+ }
3474
+ });
3475
+ Object.defineProperty(exports, "WebGLWidgetSlot", {
3476
+ enumerable: true,
3477
+ get: function() {
3478
+ return WebGLWidgetSlot;
3479
+ }
3480
+ });
3481
+ Object.defineProperty(exports, "WidgetSlot", {
3482
+ enumerable: true,
3483
+ get: function() {
3484
+ return WidgetSlot;
3485
+ }
3486
+ });
3487
+ Object.defineProperty(exports, "__toESM", {
3488
+ enumerable: true,
3489
+ get: function() {
3490
+ return __toESM;
3491
+ }
3492
+ });
3493
+ Object.defineProperty(exports, "clamp", {
3494
+ enumerable: true,
3495
+ get: function() {
3496
+ return clamp;
3497
+ }
3498
+ });
3499
+ Object.defineProperty(exports, "computeSnapGuides", {
3500
+ enumerable: true,
3501
+ get: function() {
3502
+ return computeSnapGuides;
3503
+ }
3504
+ });
3505
+ Object.defineProperty(exports, "createArchetypeRegistry", {
3506
+ enumerable: true,
3507
+ get: function() {
3508
+ return createArchetypeRegistry;
3509
+ }
3510
+ });
3511
+ Object.defineProperty(exports, "createLayoutEngine", {
3512
+ enumerable: true,
3513
+ get: function() {
3514
+ return createLayoutEngine;
3515
+ }
3516
+ });
3517
+ Object.defineProperty(exports, "createWidgetRegistry", {
3518
+ enumerable: true,
3519
+ get: function() {
3520
+ return createWidgetRegistry;
3521
+ }
3522
+ });
3523
+ Object.defineProperty(exports, "intersectsAABB", {
3524
+ enumerable: true,
3525
+ get: function() {
3526
+ return intersectsAABB;
3527
+ }
3528
+ });
3529
+ Object.defineProperty(exports, "isR3FWidget", {
3530
+ enumerable: true,
3531
+ get: function() {
3532
+ return isR3FWidget;
3533
+ }
3534
+ });
3535
+ Object.defineProperty(exports, "pointInAABB", {
3536
+ enumerable: true,
3537
+ get: function() {
3538
+ return pointInAABB;
3539
+ }
3540
+ });
3541
+ Object.defineProperty(exports, "screenToWorld", {
3542
+ enumerable: true,
3543
+ get: function() {
3544
+ return screenToWorld;
3545
+ }
3546
+ });
3547
+ Object.defineProperty(exports, "worldBoundsToAABB", {
3548
+ enumerable: true,
3549
+ get: function() {
3550
+ return worldBoundsToAABB;
3551
+ }
3552
+ });
3553
+ Object.defineProperty(exports, "worldToScreen", {
3554
+ enumerable: true,
3555
+ get: function() {
3556
+ return worldToScreen;
3557
+ }
3558
+ });
3559
+
3560
+ //# sourceMappingURL=WebGLWidgetLayer-BBMuwzHq.cjs.map