@jamesyong42/infinite-canvas 1.2.0 → 1.3.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 (56) hide show
  1. package/README.md +65 -0
  2. package/dist/advanced.cjs +61 -24
  3. package/dist/advanced.cjs.map +1 -1
  4. package/dist/advanced.d.cts +180 -64
  5. package/dist/advanced.d.cts.map +1 -1
  6. package/dist/advanced.d.mts +180 -64
  7. package/dist/advanced.d.mts.map +1 -1
  8. package/dist/advanced.mjs +29 -12
  9. package/dist/advanced.mjs.map +1 -1
  10. package/dist/devtools.cjs +22 -22
  11. package/dist/devtools.cjs.map +1 -1
  12. package/dist/devtools.d.cts +2 -2
  13. package/dist/devtools.d.cts.map +1 -1
  14. package/dist/devtools.d.mts +2 -2
  15. package/dist/devtools.d.mts.map +1 -1
  16. package/dist/devtools.mjs +2 -2
  17. package/dist/devtools.mjs.map +1 -1
  18. package/dist/{hooks-BwY7rRHg.mjs → ecs-3kimUV5Z.mjs} +238 -74
  19. package/dist/ecs-3kimUV5Z.mjs.map +1 -0
  20. package/dist/{hooks-DHShH86C.cjs → ecs-B4QrqfvQ.cjs} +320 -108
  21. package/dist/ecs-B4QrqfvQ.cjs.map +1 -0
  22. package/dist/hooks-CtP02JNt.cjs +3762 -0
  23. package/dist/hooks-CtP02JNt.cjs.map +1 -0
  24. package/dist/hooks-gsQDDE56.mjs +3494 -0
  25. package/dist/hooks-gsQDDE56.mjs.map +1 -0
  26. package/dist/index-3GY7T8JM.d.mts +480 -0
  27. package/dist/index-3GY7T8JM.d.mts.map +1 -0
  28. package/dist/index-B7B1tRPl.d.cts +480 -0
  29. package/dist/index-B7B1tRPl.d.cts.map +1 -0
  30. package/dist/index-DSdbSQ_t.d.cts +1451 -0
  31. package/dist/index-DSdbSQ_t.d.cts.map +1 -0
  32. package/dist/index-Dj9odADH.d.mts +1451 -0
  33. package/dist/index-Dj9odADH.d.mts.map +1 -0
  34. package/dist/index.cjs +3865 -643
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +315 -138
  37. package/dist/index.d.cts.map +1 -1
  38. package/dist/index.d.mts +315 -138
  39. package/dist/index.d.mts.map +1 -1
  40. package/dist/index.mjs +3767 -571
  41. package/dist/index.mjs.map +1 -1
  42. package/package.json +1 -1
  43. package/dist/SelectionRenderer-CR2PBQwx.d.cts +0 -105
  44. package/dist/SelectionRenderer-CR2PBQwx.d.cts.map +0 -1
  45. package/dist/SelectionRenderer-DlsBstAq.d.mts +0 -105
  46. package/dist/SelectionRenderer-DlsBstAq.d.mts.map +0 -1
  47. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs +0 -3560
  48. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs.map +0 -1
  49. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs +0 -3375
  50. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs.map +0 -1
  51. package/dist/engine-BfbvWXSk.d.mts +0 -982
  52. package/dist/engine-BfbvWXSk.d.mts.map +0 -1
  53. package/dist/engine-CCjuFMC-.d.cts +0 -982
  54. package/dist/engine-CCjuFMC-.d.cts.map +0 -1
  55. package/dist/hooks-BwY7rRHg.mjs.map +0 -1
  56. package/dist/hooks-DHShH86C.cjs.map +0 -1
@@ -0,0 +1,3762 @@
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_ecs = require("./ecs-B4QrqfvQ.cjs");
24
+ let _jamesyong42_reactive_ecs = require("@jamesyong42/reactive-ecs");
25
+ let react = require("react");
26
+ let _react_three_fiber = require("@react-three/fiber");
27
+ let three = require("three");
28
+ let three$1 = __toESM(three, 1);
29
+ three = __toESM(three);
30
+ let react_jsx_runtime = require("react/jsx-runtime");
31
+ //#region src/profiler/Profiler.ts
32
+ const TICK_RING_SIZE = 300;
33
+ const R3F_RING_SIZE = 300;
34
+ var Profiler = class {
35
+ enabled = false;
36
+ tickRing = [];
37
+ tickWrite = 0;
38
+ tickFilled = false;
39
+ r3fRing = [];
40
+ r3fWrite = 0;
41
+ r3fFilled = false;
42
+ frameStart = 0;
43
+ currentSystems = {};
44
+ visibilityMs = 0;
45
+ currentTick = 0;
46
+ /** Enable/disable profiling. When disabled, all methods are no-ops. */
47
+ setEnabled(on) {
48
+ this.enabled = on;
49
+ if (!on) this.clear();
50
+ }
51
+ isEnabled() {
52
+ return this.enabled;
53
+ }
54
+ /** Call at the start of engine.tick(). */
55
+ beginFrame(tick) {
56
+ if (!this.enabled) return;
57
+ this.currentTick = tick;
58
+ this.currentSystems = {};
59
+ this.visibilityMs = 0;
60
+ this.frameStart = performance.now();
61
+ performance.mark("ic-frame-start");
62
+ }
63
+ /** Call around each ECS system execution. */
64
+ beginSystem(name) {
65
+ if (!this.enabled) return;
66
+ performance.mark(`ic-sys-${name}-start`);
67
+ }
68
+ endSystem(name) {
69
+ if (!this.enabled) return;
70
+ performance.mark(`ic-sys-${name}-end`);
71
+ try {
72
+ const measure = performance.measure(`ic:sys:${name}`, `ic-sys-${name}-start`, `ic-sys-${name}-end`);
73
+ this.currentSystems[name] = measure.duration;
74
+ } catch {}
75
+ performance.clearMarks(`ic-sys-${name}-start`);
76
+ performance.clearMarks(`ic-sys-${name}-end`);
77
+ }
78
+ beginVisibility() {
79
+ if (!this.enabled) return;
80
+ performance.mark("ic-vis-start");
81
+ }
82
+ endVisibility() {
83
+ if (!this.enabled) return;
84
+ performance.mark("ic-vis-end");
85
+ try {
86
+ const measure = performance.measure("ic:visibility", "ic-vis-start", "ic-vis-end");
87
+ this.visibilityMs = measure.duration;
88
+ } catch {}
89
+ performance.clearMarks("ic-vis-start");
90
+ performance.clearMarks("ic-vis-end");
91
+ }
92
+ /** Call right before the named engine WebGL pass renders. */
93
+ beginWebGL(pass) {
94
+ if (!this.enabled) return;
95
+ performance.mark(`ic-gl-${pass}-start`);
96
+ }
97
+ /**
98
+ * Call right after the named engine WebGL pass renders.
99
+ *
100
+ * The WebGL pass runs AFTER endFrame has flushed the tick sample to the
101
+ * ring, so we mutate the most-recent ring entry in place instead of
102
+ * writing to scratch state (which would be wiped by the next beginFrame
103
+ * before ever being read).
104
+ */
105
+ endWebGL(pass) {
106
+ if (!this.enabled) return;
107
+ performance.mark(`ic-gl-${pass}-end`);
108
+ try {
109
+ const measure = performance.measure(`ic:gl:${pass}`, `ic-gl-${pass}-start`, `ic-gl-${pass}-end`);
110
+ const sample = this.getMostRecentTickSample();
111
+ if (sample) if (pass === "grid") sample.webgl.gridMs = measure.duration;
112
+ else sample.webgl.selectionMs = measure.duration;
113
+ } catch {}
114
+ performance.clearMarks(`ic-gl-${pass}-start`);
115
+ performance.clearMarks(`ic-gl-${pass}-end`);
116
+ }
117
+ /**
118
+ * Record WebGL engine pass counters for the current tick. `drawCalls` and
119
+ * `triangles` should be the totals from `renderer.info.render` accumulated
120
+ * across all engine passes in this tick (grid + selection). Callers must
121
+ * reset `renderer.info` at the start of the tick (with `autoReset=false`)
122
+ * so these values cover both passes.
123
+ *
124
+ * Called from the rAF loop AFTER engine.tick() / endFrame, so it targets
125
+ * the most-recent ring sample directly (see {@link endWebGL}).
126
+ */
127
+ recordWebGLStats(stats) {
128
+ if (!this.enabled) return;
129
+ const sample = this.getMostRecentTickSample();
130
+ if (!sample) return;
131
+ sample.webgl.drawCalls = stats.drawCalls;
132
+ sample.webgl.triangles = stats.triangles;
133
+ sample.webgl.selectionFrames = stats.selectionFrames;
134
+ sample.webgl.snapGuides = stats.snapGuides;
135
+ sample.webgl.spacingIndicators = stats.spacingIndicators;
136
+ sample.webgl.domPositionsUpdated = stats.domPositionsUpdated;
137
+ }
138
+ /** Returns the most recently pushed tick sample, or null if the ring is empty. */
139
+ getMostRecentTickSample() {
140
+ const n = this.tickRing.length;
141
+ if (n === 0) return null;
142
+ const idx = (this.tickWrite - 1 + TICK_RING_SIZE) % TICK_RING_SIZE;
143
+ return this.tickRing[idx % n] ?? null;
144
+ }
145
+ /** Call at the end of engine.tick() — flushes a TickSample to the ring. */
146
+ endFrame(entityCount, visibleCount) {
147
+ if (!this.enabled) return;
148
+ performance.mark("ic-frame-end");
149
+ let totalMs;
150
+ try {
151
+ totalMs = performance.measure("ic:frame", "ic-frame-start", "ic-frame-end").duration;
152
+ } catch {
153
+ totalMs = performance.now() - this.frameStart;
154
+ }
155
+ performance.clearMarks("ic-frame-start");
156
+ performance.clearMarks("ic-frame-end");
157
+ const sample = {
158
+ tick: this.currentTick,
159
+ timestamp: performance.now(),
160
+ totalMs,
161
+ ecs: {
162
+ systems: { ...this.currentSystems },
163
+ visibilityMs: this.visibilityMs,
164
+ entityCount,
165
+ visibleCount
166
+ },
167
+ webgl: {
168
+ gridMs: 0,
169
+ selectionMs: 0,
170
+ drawCalls: 0,
171
+ triangles: 0,
172
+ selectionFrames: 0,
173
+ snapGuides: 0,
174
+ spacingIndicators: 0,
175
+ domPositionsUpdated: 0
176
+ }
177
+ };
178
+ if (this.tickRing.length < TICK_RING_SIZE) this.tickRing.push(sample);
179
+ else this.tickRing[this.tickWrite] = sample;
180
+ this.tickWrite = (this.tickWrite + 1) % TICK_RING_SIZE;
181
+ if (this.tickRing.length >= TICK_RING_SIZE) this.tickFilled = true;
182
+ }
183
+ /**
184
+ * Push one R3F frame sample. Called from the R3F canvas via a probe
185
+ * component that has access to `useThree`.
186
+ */
187
+ recordR3FFrame(sample) {
188
+ if (!this.enabled) return;
189
+ const full = {
190
+ ...sample,
191
+ timestamp: performance.now()
192
+ };
193
+ if (this.r3fRing.length < R3F_RING_SIZE) this.r3fRing.push(full);
194
+ else this.r3fRing[this.r3fWrite] = full;
195
+ this.r3fWrite = (this.r3fWrite + 1) % R3F_RING_SIZE;
196
+ if (this.r3fRing.length >= R3F_RING_SIZE) this.r3fFilled = true;
197
+ }
198
+ /** Get the last N tick samples (newest first). */
199
+ getSamples(count) {
200
+ return readRing(this.tickRing, this.tickWrite, this.tickFilled, count);
201
+ }
202
+ /** Get the last N R3F samples (newest first). */
203
+ getR3FSamples(count) {
204
+ return readRing(this.r3fRing, this.r3fWrite, this.r3fFilled, count);
205
+ }
206
+ /** Compute rolling statistics across all three layers. */
207
+ getStats() {
208
+ return {
209
+ ecs: this.getEcsStats(),
210
+ webgl: this.getWebGLStats(),
211
+ r3f: this.getR3FStats()
212
+ };
213
+ }
214
+ getEcsStats() {
215
+ const samples = this.tickRing;
216
+ const n = samples.length;
217
+ if (n === 0) return {
218
+ fps: 0,
219
+ frameTime: {
220
+ avg: 0,
221
+ p50: 0,
222
+ p95: 0,
223
+ p99: 0,
224
+ max: 0
225
+ },
226
+ systemAvg: {},
227
+ systemP95: {},
228
+ budgetUsed: 0,
229
+ sampleCount: 0
230
+ };
231
+ const frameTimes = samples.map((s) => s.totalMs).toSorted((a, b) => a - b);
232
+ const avg = mean(frameTimes);
233
+ const fps = ringFps(samples, this.tickWrite, this.tickFilled, TICK_RING_SIZE);
234
+ const systemNames = /* @__PURE__ */ new Set();
235
+ for (const s of samples) for (const k of Object.keys(s.ecs.systems)) systemNames.add(k);
236
+ const systemAvg = {};
237
+ const systemP95 = {};
238
+ for (const name of systemNames) {
239
+ const times = samples.map((s) => s.ecs.systems[name] ?? 0).toSorted((a, b) => a - b);
240
+ systemAvg[name] = mean(times);
241
+ systemP95[name] = percentile(times, 95);
242
+ }
243
+ return {
244
+ fps,
245
+ frameTime: {
246
+ avg,
247
+ p50: percentile(frameTimes, 50),
248
+ p95: percentile(frameTimes, 95),
249
+ p99: percentile(frameTimes, 99),
250
+ max: frameTimes[frameTimes.length - 1]
251
+ },
252
+ systemAvg,
253
+ systemP95,
254
+ budgetUsed: avg / 16.67 * 100,
255
+ sampleCount: n
256
+ };
257
+ }
258
+ getWebGLStats() {
259
+ const samples = this.tickRing;
260
+ const n = samples.length;
261
+ if (n === 0) return {
262
+ fps: 0,
263
+ frameTime: {
264
+ avg: 0,
265
+ p50: 0,
266
+ p95: 0,
267
+ p99: 0,
268
+ max: 0
269
+ },
270
+ budgetUsed: 0,
271
+ gridAvg: 0,
272
+ gridP95: 0,
273
+ selectionAvg: 0,
274
+ selectionP95: 0,
275
+ avgDrawCalls: 0,
276
+ avgTriangles: 0,
277
+ avgSelectionFrames: 0,
278
+ avgSnapGuides: 0,
279
+ avgDomUpdates: 0,
280
+ sampleCount: 0
281
+ };
282
+ const gridTimes = samples.map((s) => s.webgl.gridMs).toSorted((a, b) => a - b);
283
+ const selTimes = samples.map((s) => s.webgl.selectionMs).toSorted((a, b) => a - b);
284
+ const combinedTimes = samples.map((s) => s.webgl.gridMs + s.webgl.selectionMs).toSorted((a, b) => a - b);
285
+ const combinedAvg = mean(combinedTimes);
286
+ return {
287
+ fps: ringFps(samples, this.tickWrite, this.tickFilled, TICK_RING_SIZE),
288
+ frameTime: {
289
+ avg: combinedAvg,
290
+ p50: percentile(combinedTimes, 50),
291
+ p95: percentile(combinedTimes, 95),
292
+ p99: percentile(combinedTimes, 99),
293
+ max: combinedTimes[combinedTimes.length - 1]
294
+ },
295
+ budgetUsed: combinedAvg / 16.67 * 100,
296
+ gridAvg: mean(gridTimes),
297
+ gridP95: percentile(gridTimes, 95),
298
+ selectionAvg: mean(selTimes),
299
+ selectionP95: percentile(selTimes, 95),
300
+ avgDrawCalls: mean(samples.map((s) => s.webgl.drawCalls)),
301
+ avgTriangles: mean(samples.map((s) => s.webgl.triangles)),
302
+ avgSelectionFrames: mean(samples.map((s) => s.webgl.selectionFrames)),
303
+ avgSnapGuides: mean(samples.map((s) => s.webgl.snapGuides)),
304
+ avgDomUpdates: mean(samples.map((s) => s.webgl.domPositionsUpdated)),
305
+ sampleCount: n
306
+ };
307
+ }
308
+ getR3FStats() {
309
+ const samples = this.r3fRing;
310
+ const n = samples.length;
311
+ if (n === 0) return {
312
+ fps: 0,
313
+ frameTime: {
314
+ avg: 0,
315
+ p50: 0,
316
+ p95: 0,
317
+ p99: 0,
318
+ max: 0
319
+ },
320
+ avgDrawCalls: 0,
321
+ avgTriangles: 0,
322
+ programs: 0,
323
+ geometries: 0,
324
+ textures: 0,
325
+ activeWidgets: 0,
326
+ avgWidgetsRepainted: 0,
327
+ fboBytes: 0,
328
+ phases: {
329
+ hot: 0,
330
+ warm: 0,
331
+ cold: 0,
332
+ waking: 0,
333
+ dormant: 0
334
+ },
335
+ sampleCount: 0
336
+ };
337
+ const dts = samples.map((s) => s.dtMs).toSorted((a, b) => a - b);
338
+ const fps = ringFps(samples, this.r3fWrite, this.r3fFilled, R3F_RING_SIZE);
339
+ const latest = samples[this.r3fFilled ? (this.r3fWrite - 1 + R3F_RING_SIZE) % R3F_RING_SIZE : n - 1];
340
+ const gpuPaintSamples = samples.filter((s) => s.gpuPaintMs !== void 0);
341
+ const gpuCompositeSamples = samples.filter((s) => s.gpuCompositeMs !== void 0);
342
+ return {
343
+ fps,
344
+ frameTime: {
345
+ avg: mean(dts),
346
+ p50: percentile(dts, 50),
347
+ p95: percentile(dts, 95),
348
+ p99: percentile(dts, 99),
349
+ max: dts[dts.length - 1]
350
+ },
351
+ avgDrawCalls: mean(samples.map((s) => s.drawCalls)),
352
+ avgTriangles: mean(samples.map((s) => s.triangles)),
353
+ programs: latest.programs,
354
+ geometries: latest.geometries,
355
+ textures: latest.textures,
356
+ activeWidgets: latest.activeWidgets,
357
+ avgWidgetsRepainted: mean(samples.map((s) => s.widgetsRepainted)),
358
+ fboBytes: latest.fboBytes,
359
+ phases: latest.phases,
360
+ avgGpuPaintMs: gpuPaintSamples.length > 0 ? mean(gpuPaintSamples.map((s) => s.gpuPaintMs)) : void 0,
361
+ avgGpuCompositeMs: gpuCompositeSamples.length > 0 ? mean(gpuCompositeSamples.map((s) => s.gpuCompositeMs)) : void 0,
362
+ sampleCount: n
363
+ };
364
+ }
365
+ /** Clear all collected data. */
366
+ clear() {
367
+ this.tickRing = [];
368
+ this.tickWrite = 0;
369
+ this.tickFilled = false;
370
+ this.r3fRing = [];
371
+ this.r3fWrite = 0;
372
+ this.r3fFilled = false;
373
+ }
374
+ };
375
+ function mean(xs) {
376
+ if (xs.length === 0) return 0;
377
+ let sum = 0;
378
+ for (const x of xs) sum += x;
379
+ return sum / xs.length;
380
+ }
381
+ function percentile(sorted, p) {
382
+ if (sorted.length === 0) return 0;
383
+ return sorted[Math.floor(p / 100 * (sorted.length - 1))] ?? 0;
384
+ }
385
+ function readRing(ring, write, filled, count) {
386
+ const n = ring.length;
387
+ if (n === 0) return [];
388
+ const take = Math.min(count ?? n, n);
389
+ const out = [];
390
+ for (let i = 0; i < take; i++) {
391
+ const idx = (write - 1 - i + n) % n;
392
+ out.push(ring[idx]);
393
+ }
394
+ return out;
395
+ }
396
+ function ringFps(ring, write, filled, size) {
397
+ const n = ring.length;
398
+ if (n < 2) return 0;
399
+ const newest = ring[filled ? (write - 1 + size) % size : n - 1];
400
+ const oldest = ring[filled ? write : 0];
401
+ const spanMs = newest.timestamp - oldest.timestamp;
402
+ return spanMs > 0 ? Math.round((n - 1) / spanMs * 1e3) : 0;
403
+ }
404
+ //#endregion
405
+ //#region ../../node_modules/.pnpm/quickselect@3.0.0/node_modules/quickselect/index.js
406
+ /**
407
+ * Rearranges items so that all items in the [left, k] are the smallest.
408
+ * The k-th element will have the (k - left + 1)-th smallest value in [left, right].
409
+ *
410
+ * @template T
411
+ * @param {T[]} arr the array to partially sort (in place)
412
+ * @param {number} k middle index for partial sorting (as defined above)
413
+ * @param {number} [left=0] left index of the range to sort
414
+ * @param {number} [right=arr.length-1] right index
415
+ * @param {(a: T, b: T) => number} [compare = (a, b) => a - b] compare function
416
+ */
417
+ function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) {
418
+ while (right > left) {
419
+ if (right - left > 600) {
420
+ const n = right - left + 1;
421
+ const m = k - left + 1;
422
+ const z = Math.log(n);
423
+ const s = .5 * Math.exp(2 * z / 3);
424
+ const sd = .5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
425
+ 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);
426
+ }
427
+ const t = arr[k];
428
+ let i = left;
429
+ /** @type {number} */
430
+ let j = right;
431
+ swap(arr, left, k);
432
+ if (compare(arr[right], t) > 0) swap(arr, left, right);
433
+ while (i < j) {
434
+ swap(arr, i, j);
435
+ i++;
436
+ j--;
437
+ while (compare(arr[i], t) < 0) i++;
438
+ while (compare(arr[j], t) > 0) j--;
439
+ }
440
+ if (compare(arr[left], t) === 0) swap(arr, left, j);
441
+ else {
442
+ j++;
443
+ swap(arr, j, right);
444
+ }
445
+ if (j <= k) left = j + 1;
446
+ if (k <= j) right = j - 1;
447
+ }
448
+ }
449
+ /**
450
+ * @template T
451
+ * @param {T[]} arr
452
+ * @param {number} i
453
+ * @param {number} j
454
+ */
455
+ function swap(arr, i, j) {
456
+ const tmp = arr[i];
457
+ arr[i] = arr[j];
458
+ arr[j] = tmp;
459
+ }
460
+ /**
461
+ * @template T
462
+ * @param {T} a
463
+ * @param {T} b
464
+ * @returns {number}
465
+ */
466
+ function defaultCompare(a, b) {
467
+ return a < b ? -1 : a > b ? 1 : 0;
468
+ }
469
+ //#endregion
470
+ //#region ../../node_modules/.pnpm/rbush@4.0.1/node_modules/rbush/index.js
471
+ var RBush$1 = class {
472
+ constructor(maxEntries = 9) {
473
+ this._maxEntries = Math.max(4, maxEntries);
474
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * .4));
475
+ this.clear();
476
+ }
477
+ all() {
478
+ return this._all(this.data, []);
479
+ }
480
+ search(bbox) {
481
+ let node = this.data;
482
+ const result = [];
483
+ if (!intersects(bbox, node)) return result;
484
+ const toBBox = this.toBBox;
485
+ const nodesToSearch = [];
486
+ while (node) {
487
+ for (let i = 0; i < node.children.length; i++) {
488
+ const child = node.children[i];
489
+ const childBBox = node.leaf ? toBBox(child) : child;
490
+ if (intersects(bbox, childBBox)) if (node.leaf) result.push(child);
491
+ else if (contains(bbox, childBBox)) this._all(child, result);
492
+ else nodesToSearch.push(child);
493
+ }
494
+ node = nodesToSearch.pop();
495
+ }
496
+ return result;
497
+ }
498
+ collides(bbox) {
499
+ let node = this.data;
500
+ if (!intersects(bbox, node)) return false;
501
+ const nodesToSearch = [];
502
+ while (node) {
503
+ for (let i = 0; i < node.children.length; i++) {
504
+ const child = node.children[i];
505
+ const childBBox = node.leaf ? this.toBBox(child) : child;
506
+ if (intersects(bbox, childBBox)) {
507
+ if (node.leaf || contains(bbox, childBBox)) return true;
508
+ nodesToSearch.push(child);
509
+ }
510
+ }
511
+ node = nodesToSearch.pop();
512
+ }
513
+ return false;
514
+ }
515
+ load(data) {
516
+ if (!(data && data.length)) return this;
517
+ if (data.length < this._minEntries) {
518
+ for (let i = 0; i < data.length; i++) this.insert(data[i]);
519
+ return this;
520
+ }
521
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
522
+ if (!this.data.children.length) this.data = node;
523
+ else if (this.data.height === node.height) this._splitRoot(this.data, node);
524
+ else {
525
+ if (this.data.height < node.height) {
526
+ const tmpNode = this.data;
527
+ this.data = node;
528
+ node = tmpNode;
529
+ }
530
+ this._insert(node, this.data.height - node.height - 1, true);
531
+ }
532
+ return this;
533
+ }
534
+ insert(item) {
535
+ if (item) this._insert(item, this.data.height - 1);
536
+ return this;
537
+ }
538
+ clear() {
539
+ this.data = createNode([]);
540
+ return this;
541
+ }
542
+ remove(item, equalsFn) {
543
+ if (!item) return this;
544
+ let node = this.data;
545
+ const bbox = this.toBBox(item);
546
+ const path = [];
547
+ const indexes = [];
548
+ let i, parent, goingUp;
549
+ while (node || path.length) {
550
+ if (!node) {
551
+ node = path.pop();
552
+ parent = path[path.length - 1];
553
+ i = indexes.pop();
554
+ goingUp = true;
555
+ }
556
+ if (node.leaf) {
557
+ const index = findItem(item, node.children, equalsFn);
558
+ if (index !== -1) {
559
+ node.children.splice(index, 1);
560
+ path.push(node);
561
+ this._condense(path);
562
+ return this;
563
+ }
564
+ }
565
+ if (!goingUp && !node.leaf && contains(node, bbox)) {
566
+ path.push(node);
567
+ indexes.push(i);
568
+ i = 0;
569
+ parent = node;
570
+ node = node.children[0];
571
+ } else if (parent) {
572
+ i++;
573
+ node = parent.children[i];
574
+ goingUp = false;
575
+ } else node = null;
576
+ }
577
+ return this;
578
+ }
579
+ toBBox(item) {
580
+ return item;
581
+ }
582
+ compareMinX(a, b) {
583
+ return a.minX - b.minX;
584
+ }
585
+ compareMinY(a, b) {
586
+ return a.minY - b.minY;
587
+ }
588
+ toJSON() {
589
+ return this.data;
590
+ }
591
+ fromJSON(data) {
592
+ this.data = data;
593
+ return this;
594
+ }
595
+ _all(node, result) {
596
+ const nodesToSearch = [];
597
+ while (node) {
598
+ if (node.leaf) result.push(...node.children);
599
+ else nodesToSearch.push(...node.children);
600
+ node = nodesToSearch.pop();
601
+ }
602
+ return result;
603
+ }
604
+ _build(items, left, right, height) {
605
+ const N = right - left + 1;
606
+ let M = this._maxEntries;
607
+ let node;
608
+ if (N <= M) {
609
+ node = createNode(items.slice(left, right + 1));
610
+ calcBBox(node, this.toBBox);
611
+ return node;
612
+ }
613
+ if (!height) {
614
+ height = Math.ceil(Math.log(N) / Math.log(M));
615
+ M = Math.ceil(N / Math.pow(M, height - 1));
616
+ }
617
+ node = createNode([]);
618
+ node.leaf = false;
619
+ node.height = height;
620
+ const N2 = Math.ceil(N / M);
621
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
622
+ multiSelect(items, left, right, N1, this.compareMinX);
623
+ for (let i = left; i <= right; i += N1) {
624
+ const right2 = Math.min(i + N1 - 1, right);
625
+ multiSelect(items, i, right2, N2, this.compareMinY);
626
+ for (let j = i; j <= right2; j += N2) {
627
+ const right3 = Math.min(j + N2 - 1, right2);
628
+ node.children.push(this._build(items, j, right3, height - 1));
629
+ }
630
+ }
631
+ calcBBox(node, this.toBBox);
632
+ return node;
633
+ }
634
+ _chooseSubtree(bbox, node, level, path) {
635
+ while (true) {
636
+ path.push(node);
637
+ if (node.leaf || path.length - 1 === level) break;
638
+ let minArea = Infinity;
639
+ let minEnlargement = Infinity;
640
+ let targetNode;
641
+ for (let i = 0; i < node.children.length; i++) {
642
+ const child = node.children[i];
643
+ const area = bboxArea(child);
644
+ const enlargement = enlargedArea(bbox, child) - area;
645
+ if (enlargement < minEnlargement) {
646
+ minEnlargement = enlargement;
647
+ minArea = area < minArea ? area : minArea;
648
+ targetNode = child;
649
+ } else if (enlargement === minEnlargement) {
650
+ if (area < minArea) {
651
+ minArea = area;
652
+ targetNode = child;
653
+ }
654
+ }
655
+ }
656
+ node = targetNode || node.children[0];
657
+ }
658
+ return node;
659
+ }
660
+ _insert(item, level, isNode) {
661
+ const bbox = isNode ? item : this.toBBox(item);
662
+ const insertPath = [];
663
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
664
+ node.children.push(item);
665
+ extend(node, bbox);
666
+ while (level >= 0) if (insertPath[level].children.length > this._maxEntries) {
667
+ this._split(insertPath, level);
668
+ level--;
669
+ } else break;
670
+ this._adjustParentBBoxes(bbox, insertPath, level);
671
+ }
672
+ _split(insertPath, level) {
673
+ const node = insertPath[level];
674
+ const M = node.children.length;
675
+ const m = this._minEntries;
676
+ this._chooseSplitAxis(node, m, M);
677
+ const splitIndex = this._chooseSplitIndex(node, m, M);
678
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
679
+ newNode.height = node.height;
680
+ newNode.leaf = node.leaf;
681
+ calcBBox(node, this.toBBox);
682
+ calcBBox(newNode, this.toBBox);
683
+ if (level) insertPath[level - 1].children.push(newNode);
684
+ else this._splitRoot(node, newNode);
685
+ }
686
+ _splitRoot(node, newNode) {
687
+ this.data = createNode([node, newNode]);
688
+ this.data.height = node.height + 1;
689
+ this.data.leaf = false;
690
+ calcBBox(this.data, this.toBBox);
691
+ }
692
+ _chooseSplitIndex(node, m, M) {
693
+ let index;
694
+ let minOverlap = Infinity;
695
+ let minArea = Infinity;
696
+ for (let i = m; i <= M - m; i++) {
697
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
698
+ const bbox2 = distBBox(node, i, M, this.toBBox);
699
+ const overlap = intersectionArea(bbox1, bbox2);
700
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
701
+ if (overlap < minOverlap) {
702
+ minOverlap = overlap;
703
+ index = i;
704
+ minArea = area < minArea ? area : minArea;
705
+ } else if (overlap === minOverlap) {
706
+ if (area < minArea) {
707
+ minArea = area;
708
+ index = i;
709
+ }
710
+ }
711
+ }
712
+ return index || M - m;
713
+ }
714
+ _chooseSplitAxis(node, m, M) {
715
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
716
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
717
+ if (this._allDistMargin(node, m, M, compareMinX) < this._allDistMargin(node, m, M, compareMinY)) node.children.sort(compareMinX);
718
+ }
719
+ _allDistMargin(node, m, M, compare) {
720
+ node.children.sort(compare);
721
+ const toBBox = this.toBBox;
722
+ const leftBBox = distBBox(node, 0, m, toBBox);
723
+ const rightBBox = distBBox(node, M - m, M, toBBox);
724
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
725
+ for (let i = m; i < M - m; i++) {
726
+ const child = node.children[i];
727
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
728
+ margin += bboxMargin(leftBBox);
729
+ }
730
+ for (let i = M - m - 1; i >= m; i--) {
731
+ const child = node.children[i];
732
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
733
+ margin += bboxMargin(rightBBox);
734
+ }
735
+ return margin;
736
+ }
737
+ _adjustParentBBoxes(bbox, path, level) {
738
+ for (let i = level; i >= 0; i--) extend(path[i], bbox);
739
+ }
740
+ _condense(path) {
741
+ for (let i = path.length - 1, siblings; i >= 0; i--) if (path[i].children.length === 0) if (i > 0) {
742
+ siblings = path[i - 1].children;
743
+ siblings.splice(siblings.indexOf(path[i]), 1);
744
+ } else this.clear();
745
+ else calcBBox(path[i], this.toBBox);
746
+ }
747
+ };
748
+ function findItem(item, items, equalsFn) {
749
+ if (!equalsFn) return items.indexOf(item);
750
+ for (let i = 0; i < items.length; i++) if (equalsFn(item, items[i])) return i;
751
+ return -1;
752
+ }
753
+ function calcBBox(node, toBBox) {
754
+ distBBox(node, 0, node.children.length, toBBox, node);
755
+ }
756
+ function distBBox(node, k, p, toBBox, destNode) {
757
+ if (!destNode) destNode = createNode(null);
758
+ destNode.minX = Infinity;
759
+ destNode.minY = Infinity;
760
+ destNode.maxX = -Infinity;
761
+ destNode.maxY = -Infinity;
762
+ for (let i = k; i < p; i++) {
763
+ const child = node.children[i];
764
+ extend(destNode, node.leaf ? toBBox(child) : child);
765
+ }
766
+ return destNode;
767
+ }
768
+ function extend(a, b) {
769
+ a.minX = Math.min(a.minX, b.minX);
770
+ a.minY = Math.min(a.minY, b.minY);
771
+ a.maxX = Math.max(a.maxX, b.maxX);
772
+ a.maxY = Math.max(a.maxY, b.maxY);
773
+ return a;
774
+ }
775
+ function compareNodeMinX(a, b) {
776
+ return a.minX - b.minX;
777
+ }
778
+ function compareNodeMinY(a, b) {
779
+ return a.minY - b.minY;
780
+ }
781
+ function bboxArea(a) {
782
+ return (a.maxX - a.minX) * (a.maxY - a.minY);
783
+ }
784
+ function bboxMargin(a) {
785
+ return a.maxX - a.minX + (a.maxY - a.minY);
786
+ }
787
+ function enlargedArea(a, b) {
788
+ 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));
789
+ }
790
+ function intersectionArea(a, b) {
791
+ const minX = Math.max(a.minX, b.minX);
792
+ const minY = Math.max(a.minY, b.minY);
793
+ const maxX = Math.min(a.maxX, b.maxX);
794
+ const maxY = Math.min(a.maxY, b.maxY);
795
+ return Math.max(0, maxX - minX) * Math.max(0, maxY - minY);
796
+ }
797
+ function contains(a, b) {
798
+ return a.minX <= b.minX && a.minY <= b.minY && b.maxX <= a.maxX && b.maxY <= a.maxY;
799
+ }
800
+ function intersects(a, b) {
801
+ return b.minX <= a.maxX && b.minY <= a.maxY && b.maxX >= a.minX && b.maxY >= a.minY;
802
+ }
803
+ function createNode(children) {
804
+ return {
805
+ children,
806
+ height: 1,
807
+ leaf: true,
808
+ minX: Infinity,
809
+ minY: Infinity,
810
+ maxX: -Infinity,
811
+ maxY: -Infinity
812
+ };
813
+ }
814
+ function multiSelect(arr, left, right, n, compare) {
815
+ const stack = [left, right];
816
+ while (stack.length) {
817
+ right = stack.pop();
818
+ left = stack.pop();
819
+ if (right - left <= n) continue;
820
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
821
+ quickselect(arr, mid, left, right, compare);
822
+ stack.push(left, mid, mid, right);
823
+ }
824
+ }
825
+ //#endregion
826
+ //#region src/ecs/spatial/SpatialIndex.ts
827
+ const rbushModule = RBush$1;
828
+ const RBush = typeof rbushModule.default === "function" ? rbushModule.default : RBush$1;
829
+ /**
830
+ * Spatial index backed by an R-tree (rbush).
831
+ * Stores world-space AABBs for fast viewport culling and hit testing.
832
+ */
833
+ var SpatialIndex = class {
834
+ tree = new RBush();
835
+ entries = /* @__PURE__ */ new Map();
836
+ upsert(entityId, bounds) {
837
+ const existing = this.entries.get(entityId);
838
+ if (existing) this.tree.remove(existing);
839
+ const entry = {
840
+ ...bounds,
841
+ entityId
842
+ };
843
+ this.entries.set(entityId, entry);
844
+ this.tree.insert(entry);
845
+ }
846
+ remove(entityId) {
847
+ const existing = this.entries.get(entityId);
848
+ if (existing) {
849
+ this.tree.remove(existing);
850
+ this.entries.delete(entityId);
851
+ }
852
+ }
853
+ /** Query all entries intersecting the given AABB */
854
+ search(bounds) {
855
+ return this.tree.search(bounds);
856
+ }
857
+ /** Find the topmost entity at a point (by z-order — caller sorts) */
858
+ searchPoint(x, y, tolerance = 0) {
859
+ return this.tree.search({
860
+ minX: x - tolerance,
861
+ minY: y - tolerance,
862
+ maxX: x + tolerance,
863
+ maxY: y + tolerance
864
+ });
865
+ }
866
+ clear() {
867
+ this.tree.clear();
868
+ this.entries.clear();
869
+ }
870
+ get size() {
871
+ return this.entries.size;
872
+ }
873
+ };
874
+ //#endregion
875
+ //#region src/ecs/spatial/snap.ts
876
+ /**
877
+ * Compute snap guides for a dragged entity against reference entities.
878
+ */
879
+ function computeSnapGuides(dragged, references, threshold) {
880
+ const guides = [];
881
+ const spacings = [];
882
+ let snapDx = 0;
883
+ let snapDy = 0;
884
+ const dLeft = dragged.x;
885
+ const dRight = dragged.x + dragged.width;
886
+ const dCenterX = dragged.x + dragged.width / 2;
887
+ const dTop = dragged.y;
888
+ const dBottom = dragged.y + dragged.height;
889
+ const dCenterY = dragged.y + dragged.height / 2;
890
+ let bestSnapX = Number.POSITIVE_INFINITY;
891
+ let bestSnapY = Number.POSITIVE_INFINITY;
892
+ let bestDx = 0;
893
+ let bestDy = 0;
894
+ const xGuides = [];
895
+ const yGuides = [];
896
+ for (const ref of references) {
897
+ const rLeft = ref.x;
898
+ const rRight = ref.x + ref.width;
899
+ const rCenterX = ref.x + ref.width / 2;
900
+ const rTop = ref.y;
901
+ const rBottom = ref.y + ref.height;
902
+ const rCenterY = ref.y + ref.height / 2;
903
+ const xPairs = [
904
+ [
905
+ dLeft,
906
+ rLeft,
907
+ "edge"
908
+ ],
909
+ [
910
+ dLeft,
911
+ rRight,
912
+ "edge"
913
+ ],
914
+ [
915
+ dRight,
916
+ rLeft,
917
+ "edge"
918
+ ],
919
+ [
920
+ dRight,
921
+ rRight,
922
+ "edge"
923
+ ],
924
+ [
925
+ dCenterX,
926
+ rCenterX,
927
+ "center"
928
+ ],
929
+ [
930
+ dLeft,
931
+ rCenterX,
932
+ "edge"
933
+ ],
934
+ [
935
+ dRight,
936
+ rCenterX,
937
+ "edge"
938
+ ]
939
+ ];
940
+ for (const [dVal, rVal, type] of xPairs) {
941
+ const dist = Math.abs(dVal - rVal);
942
+ if (dist <= threshold) {
943
+ const dx = rVal - dVal;
944
+ if (dist < bestSnapX) {
945
+ bestSnapX = dist;
946
+ bestDx = dx;
947
+ xGuides.length = 0;
948
+ }
949
+ if (dist <= bestSnapX + .01) xGuides.push({
950
+ axis: "x",
951
+ position: rVal,
952
+ type
953
+ });
954
+ }
955
+ }
956
+ const yPairs = [
957
+ [
958
+ dTop,
959
+ rTop,
960
+ "edge"
961
+ ],
962
+ [
963
+ dTop,
964
+ rBottom,
965
+ "edge"
966
+ ],
967
+ [
968
+ dBottom,
969
+ rTop,
970
+ "edge"
971
+ ],
972
+ [
973
+ dBottom,
974
+ rBottom,
975
+ "edge"
976
+ ],
977
+ [
978
+ dCenterY,
979
+ rCenterY,
980
+ "center"
981
+ ],
982
+ [
983
+ dTop,
984
+ rCenterY,
985
+ "edge"
986
+ ],
987
+ [
988
+ dBottom,
989
+ rCenterY,
990
+ "edge"
991
+ ]
992
+ ];
993
+ for (const [dVal, rVal, type] of yPairs) {
994
+ const dist = Math.abs(dVal - rVal);
995
+ if (dist <= threshold) {
996
+ const dy = rVal - dVal;
997
+ if (dist < bestSnapY) {
998
+ bestSnapY = dist;
999
+ bestDy = dy;
1000
+ yGuides.length = 0;
1001
+ }
1002
+ if (dist <= bestSnapY + .01) yGuides.push({
1003
+ axis: "y",
1004
+ position: rVal,
1005
+ type
1006
+ });
1007
+ }
1008
+ }
1009
+ }
1010
+ const eqResult = computeEqualSpacing(dragged, references, threshold);
1011
+ if (bestSnapX <= threshold) snapDx = bestDx;
1012
+ else if (eqResult.snapDx !== void 0) snapDx = eqResult.snapDx;
1013
+ if (bestSnapY <= threshold) snapDy = bestDy;
1014
+ else if (eqResult.snapDy !== void 0) snapDy = eqResult.snapDy;
1015
+ if (bestSnapX <= threshold) {
1016
+ const seen = /* @__PURE__ */ new Set();
1017
+ for (const g of xGuides) if (!seen.has(g.position)) {
1018
+ seen.add(g.position);
1019
+ guides.push(g);
1020
+ }
1021
+ }
1022
+ if (bestSnapY <= threshold) {
1023
+ const seen = /* @__PURE__ */ new Set();
1024
+ for (const g of yGuides) if (!seen.has(g.position)) {
1025
+ seen.add(g.position);
1026
+ guides.push(g);
1027
+ }
1028
+ }
1029
+ const eqFinal = computeEqualSpacing({
1030
+ x: dragged.x + snapDx,
1031
+ y: dragged.y + snapDy,
1032
+ width: dragged.width,
1033
+ height: dragged.height
1034
+ }, references, threshold * .5);
1035
+ spacings.push(...eqFinal.indicators);
1036
+ return {
1037
+ snapDx,
1038
+ snapDy,
1039
+ guides,
1040
+ spacings
1041
+ };
1042
+ }
1043
+ function computeEqualSpacing(dragged, references, threshold) {
1044
+ const indicators = [];
1045
+ let snapDx;
1046
+ let snapDy;
1047
+ const xResult = checkAxisSpacing(dragged, references, threshold, "x");
1048
+ if (xResult) {
1049
+ snapDx = xResult.snap;
1050
+ indicators.push(...xResult.indicators);
1051
+ }
1052
+ const yResult = checkAxisSpacing(dragged, references, threshold, "y");
1053
+ if (yResult) {
1054
+ snapDy = yResult.snap;
1055
+ indicators.push(...yResult.indicators);
1056
+ }
1057
+ return {
1058
+ snapDx,
1059
+ snapDy,
1060
+ indicators
1061
+ };
1062
+ }
1063
+ function checkAxisSpacing(dragged, references, threshold, axis) {
1064
+ const isX = axis === "x";
1065
+ const pos = (b) => isX ? b.x : b.y;
1066
+ const size = (b) => isX ? b.width : b.height;
1067
+ const perpPos = (b) => isX ? b.y : b.x;
1068
+ const perpSize = (b) => isX ? b.height : b.width;
1069
+ const end = (b) => pos(b) + size(b);
1070
+ const neighbors = references.filter((ref) => perpPos(ref) < perpPos(dragged) + perpSize(dragged) && perpPos(ref) + perpSize(ref) > perpPos(dragged));
1071
+ if (neighbors.length < 1) return null;
1072
+ const sorted = [...neighbors].toSorted((a, b) => pos(a) - pos(b));
1073
+ const refGaps = [];
1074
+ for (let i = 0; i < sorted.length - 1; i++) {
1075
+ const gap = pos(sorted[i + 1]) - end(sorted[i]);
1076
+ if (gap > .1) refGaps.push({
1077
+ from: sorted[i],
1078
+ to: sorted[i + 1],
1079
+ gap
1080
+ });
1081
+ }
1082
+ let bestSnap = null;
1083
+ let bestIndicators = [];
1084
+ let bestDiff = Number.POSITIVE_INFINITY;
1085
+ let leftN = null;
1086
+ let rightN = null;
1087
+ for (const ref of sorted) {
1088
+ if (end(ref) <= pos(dragged) + threshold) {
1089
+ if (!leftN || end(ref) > end(leftN)) leftN = ref;
1090
+ }
1091
+ if (pos(ref) >= end(dragged) - threshold) {
1092
+ if (!rightN || pos(ref) < pos(rightN)) rightN = ref;
1093
+ }
1094
+ }
1095
+ if (leftN && rightN) {
1096
+ const lGap = pos(dragged) - end(leftN);
1097
+ const rGap = pos(rightN) - end(dragged);
1098
+ const diff = Math.abs(lGap - rGap);
1099
+ if (diff <= threshold && diff < bestDiff) {
1100
+ const idealPos = (end(leftN) + pos(rightN) - size(dragged)) / 2;
1101
+ const snap = idealPos - pos(dragged);
1102
+ const equalGap = (pos(rightN) - end(leftN) - size(dragged)) / 2;
1103
+ if (equalGap > .1) {
1104
+ const perpY = computePerpCenter(dragged, [leftN, rightN], isX);
1105
+ bestSnap = snap;
1106
+ bestDiff = diff;
1107
+ bestIndicators = [{
1108
+ axis,
1109
+ gap: equalGap,
1110
+ segments: [{
1111
+ from: end(leftN),
1112
+ to: idealPos
1113
+ }, {
1114
+ from: idealPos + size(dragged),
1115
+ to: pos(rightN)
1116
+ }],
1117
+ perpPosition: perpY
1118
+ }];
1119
+ }
1120
+ }
1121
+ }
1122
+ for (const refGap of refGaps) {
1123
+ const patternGap = refGap.gap;
1124
+ if (rightN === null || pos(refGap.to) >= end(dragged) - threshold * 2) {
1125
+ const chainEnd = refGap.to;
1126
+ const dragGap = pos(dragged) - end(chainEnd);
1127
+ const diff = Math.abs(dragGap - patternGap);
1128
+ if (diff <= threshold && diff < bestDiff) {
1129
+ const idealPos = end(chainEnd) + patternGap;
1130
+ const snap = idealPos - pos(dragged);
1131
+ const perpY = computePerpCenter(dragged, [refGap.from, refGap.to], isX);
1132
+ bestSnap = snap;
1133
+ bestDiff = diff;
1134
+ bestIndicators = [{
1135
+ axis,
1136
+ gap: patternGap,
1137
+ segments: [{
1138
+ from: end(refGap.from),
1139
+ to: pos(refGap.to)
1140
+ }, {
1141
+ from: end(chainEnd),
1142
+ to: idealPos
1143
+ }],
1144
+ perpPosition: perpY
1145
+ }];
1146
+ }
1147
+ }
1148
+ if (leftN === null || end(refGap.from) <= pos(dragged) + threshold * 2) {
1149
+ const chainStart = refGap.from;
1150
+ const dragGap = pos(chainStart) - end(dragged);
1151
+ const diff = Math.abs(dragGap - patternGap);
1152
+ if (diff <= threshold && diff < bestDiff) {
1153
+ const idealPos = pos(chainStart) - patternGap - size(dragged);
1154
+ const snap = idealPos - pos(dragged);
1155
+ const perpY = computePerpCenter(dragged, [refGap.from, refGap.to], isX);
1156
+ bestSnap = snap;
1157
+ bestDiff = diff;
1158
+ bestIndicators = [{
1159
+ axis,
1160
+ gap: patternGap,
1161
+ segments: [{
1162
+ from: idealPos + size(dragged),
1163
+ to: pos(chainStart)
1164
+ }, {
1165
+ from: end(refGap.from),
1166
+ to: pos(refGap.to)
1167
+ }],
1168
+ perpPosition: perpY
1169
+ }];
1170
+ }
1171
+ }
1172
+ }
1173
+ if (bestSnap !== null) return {
1174
+ snap: bestSnap,
1175
+ indicators: bestIndicators
1176
+ };
1177
+ return null;
1178
+ }
1179
+ function computePerpCenter(dragged, refs, isX) {
1180
+ const perpPos = (b) => isX ? b.y : b.x;
1181
+ const perpSize = (b) => isX ? b.height : b.width;
1182
+ const allBounds = [dragged, ...refs];
1183
+ const maxStart = Math.max(...allBounds.map(perpPos));
1184
+ const minEnd = Math.min(...allBounds.map((b) => perpPos(b) + perpSize(b)));
1185
+ if (minEnd < maxStart) return perpPos(allBounds[0]) + perpSize(allBounds[0]) / 2;
1186
+ return maxStart + (minEnd - maxStart) / 2;
1187
+ }
1188
+ //#endregion
1189
+ //#region src/react/context/container-ref-context.ts
1190
+ const ContainerRefContext = (0, react.createContext)(null);
1191
+ const ContainerRefProvider = ContainerRefContext.Provider;
1192
+ function useContainerRef() {
1193
+ return (0, react.useContext)(ContainerRefContext);
1194
+ }
1195
+ //#endregion
1196
+ //#region src/react/context/widget-resolver-context.ts
1197
+ const WidgetResolverContext = (0, react.createContext)(null);
1198
+ const WidgetResolverProvider = WidgetResolverContext.Provider;
1199
+ function useWidgetResolver() {
1200
+ return (0, react.useContext)(WidgetResolverContext);
1201
+ }
1202
+ //#endregion
1203
+ //#region src/react/input/debug.ts
1204
+ /**
1205
+ * RFC-008 input pipeline — diagnostic logger.
1206
+ *
1207
+ * Single toggle (`INPUT_DEBUG`) wires every layer's "what just happened"
1208
+ * decision into the browser console with colour-coded tags so the dispatch
1209
+ * flow is legible in real time:
1210
+ *
1211
+ * [Adapter] [InputManager] [Router] [R3F] [Recognizer] [Engine]
1212
+ *
1213
+ * `INPUT_DEBUG_VERBOSE` extends this to per-frame `move` events, which
1214
+ * fire at 60+ Hz and quickly drown the console — leave it `false` unless
1215
+ * you're chasing a hover or pan-update issue.
1216
+ *
1217
+ * Toggle either by editing the constants below or, more conveniently, by
1218
+ * setting `window.__INPUT_DEBUG__` / `window.__INPUT_DEBUG_VERBOSE__` from
1219
+ * the devtools console at runtime.
1220
+ */
1221
+ const DEFAULT_DEBUG = true;
1222
+ const DEFAULT_VERBOSE = false;
1223
+ function isDebug() {
1224
+ if (typeof window === "undefined") return DEFAULT_DEBUG;
1225
+ return window.__INPUT_DEBUG__ ?? DEFAULT_DEBUG;
1226
+ }
1227
+ function isVerbose() {
1228
+ if (typeof window === "undefined") return DEFAULT_VERBOSE;
1229
+ return window.__INPUT_DEBUG_VERBOSE__ ?? DEFAULT_VERBOSE;
1230
+ }
1231
+ const STYLES = {
1232
+ Adapter: "background:#0288d1; color:#fff; padding:1px 4px; border-radius:2px; font-weight:bold",
1233
+ InputManager: "background:#f9a825; color:#222; padding:1px 4px; border-radius:2px; font-weight:bold",
1234
+ Router: "background:#7b1fa2; color:#fff; padding:1px 4px; border-radius:2px; font-weight:bold",
1235
+ R3F: "background:#00838f; color:#fff; padding:1px 4px; border-radius:2px; font-weight:bold",
1236
+ Recognizer: "background:#558b2f; color:#fff; padding:1px 4px; border-radius:2px",
1237
+ Engine: "background:#d84315; color:#fff; padding:1px 4px; border-radius:2px; font-weight:bold"
1238
+ };
1239
+ /**
1240
+ * Log a single line tagged with a layer. `data` is appended as a single
1241
+ * object so devtools can expand it inline.
1242
+ *
1243
+ * `move`-typed events are gated on `INPUT_DEBUG_VERBOSE` so the default
1244
+ * trace stays readable.
1245
+ */
1246
+ function inputLog(layer, message, data) {
1247
+ if (!isDebug()) return;
1248
+ if (typeof data?.type === "string" && (data.type === "move" || data.type.endsWith("-update")) && !isVerbose()) return;
1249
+ if (data !== void 0) console.log(`%c${layer}%c ${message}`, STYLES[layer], "color:inherit", data);
1250
+ else console.log(`%c${layer}%c ${message}`, STYLES[layer], "color:inherit");
1251
+ }
1252
+ /**
1253
+ * Group all logs for a single InputManager.dispatch under one collapsible
1254
+ * heading. Pass the returned closer to console.groupEnd at the end of
1255
+ * dispatch. No-op when debug is off.
1256
+ */
1257
+ function inputGroupStart(label) {
1258
+ if (!isDebug()) return () => {};
1259
+ console.groupCollapsed(`%cInput · ${label}`, "color:#555; font-weight:bold");
1260
+ return () => console.groupEnd();
1261
+ }
1262
+ //#endregion
1263
+ //#region src/react/input/r3f/createR3FEventManager.ts
1264
+ function createR3FEventManager(engine, registry, onCreate) {
1265
+ let activeScene = null;
1266
+ function skipEvent(state) {
1267
+ activeScene = null;
1268
+ state.raycaster.camera = null;
1269
+ state.pointer.set(0, 0);
1270
+ }
1271
+ return (store) => {
1272
+ const base = (0, _react_three_fiber.events)(store);
1273
+ const compute = (event, state) => {
1274
+ const rect = state.gl.domElement.getBoundingClientRect();
1275
+ const screenX = event.clientX - rect.left;
1276
+ const screenY = event.clientY - rect.top;
1277
+ const entityId = engine.pickAt(screenX, screenY);
1278
+ if (entityId === null) {
1279
+ skipEvent(state);
1280
+ return;
1281
+ }
1282
+ if (engine.get(entityId, require_ecs.Widget)?.surface !== "webgl") {
1283
+ skipEvent(state);
1284
+ return;
1285
+ }
1286
+ const widget = registry.get(entityId);
1287
+ const t = engine.get(entityId, require_ecs.Transform2D);
1288
+ if (!widget || !t) {
1289
+ skipEvent(state);
1290
+ return;
1291
+ }
1292
+ const cam = engine.getCamera();
1293
+ const worldX = screenX / cam.zoom + cam.x;
1294
+ const worldY = screenY / cam.zoom + cam.y;
1295
+ const widgetCenterX = t.x + t.width / 2;
1296
+ const widgetCenterY = t.y + t.height / 2;
1297
+ const localX = worldX - widgetCenterX;
1298
+ const localY = -(worldY - widgetCenterY);
1299
+ const ndcX = 2 * localX / t.width;
1300
+ const ndcY = 2 * localY / t.height;
1301
+ activeScene = widget.scene;
1302
+ state.pointer.set(ndcX, ndcY);
1303
+ state.raycaster.setFromCamera(state.pointer, widget.camera);
1304
+ state.raycaster.camera = widget.camera;
1305
+ inputLog("R3F", `compute: ready raycaster for entity ${entityId}`, {
1306
+ type: event.type,
1307
+ entityId,
1308
+ ndc: {
1309
+ x: ndcX,
1310
+ y: ndcY
1311
+ },
1312
+ sceneChildren: widget.scene.children.length
1313
+ });
1314
+ };
1315
+ const filter = (items) => {
1316
+ const scene = activeScene;
1317
+ if (!scene) return [];
1318
+ return items.filter((hit) => isDescendantOf(hit.object, scene));
1319
+ };
1320
+ const isPointerCaptured = (pointerId) => {
1321
+ return store.getState().internal?.capturedMap?.has?.(pointerId) ?? false;
1322
+ };
1323
+ const manager = {
1324
+ ...base,
1325
+ compute,
1326
+ filter,
1327
+ isPointerCaptured,
1328
+ connect: (_target) => {
1329
+ inputLog("R3F", "createR3FEventManager.connect: no-op (driven by InputManager)");
1330
+ },
1331
+ disconnect: () => {
1332
+ inputLog("R3F", "createR3FEventManager.disconnect: no-op");
1333
+ }
1334
+ };
1335
+ onCreate?.(manager);
1336
+ return manager;
1337
+ };
1338
+ }
1339
+ function isDescendantOf(obj, ancestor) {
1340
+ let n = obj;
1341
+ while (n) {
1342
+ if (n === ancestor) return true;
1343
+ n = n.parent;
1344
+ }
1345
+ return false;
1346
+ }
1347
+ //#endregion
1348
+ //#region src/r3f/compositor/CompositionMaterial.ts
1349
+ /**
1350
+ * Module-level shared uniforms for the overlap glow.
1351
+ *
1352
+ * Every {@link CompositionMaterial} instance references THESE EXACT
1353
+ * Vector2 / Vector3 objects. Mutating any `.value` here propagates to
1354
+ * every R3F card simultaneously (Three.js sees them as the same uniform
1355
+ * binding). One call to `applyOverlapGlowShaderUniforms(config)` retunes
1356
+ * every card without iterating materials.
1357
+ */
1358
+ const sharedGlowUniforms = {
1359
+ uGlowColor: { value: new three$1.Vector3(.5, .5, .5) },
1360
+ uGlowAlpha: { value: new three$1.Vector2(.25, .45) },
1361
+ uGlowFalloff: { value: new three$1.Vector2(.3, .4) },
1362
+ uRimColor: { value: new three$1.Vector3(.5, .5, .5) },
1363
+ uRimAlpha: { value: new three$1.Vector2(.55, .85) },
1364
+ uRimRadius: { value: 3 }
1365
+ };
1366
+ /**
1367
+ * Shader pair for the composition pass — sample an sRGB-encoded widget
1368
+ * FBO and write it to the sRGB backbuffer unchanged. No tone mapping,
1369
+ * no output encoding (the FBO already holds display-ready values, see
1370
+ * RFC-002 § sRGB FBO fix).
1371
+ *
1372
+ * `uDraggedRect` + `uIsDragged` implement the RFC-003 drag-promote
1373
+ * clip: when an R3F widget is being dragged, every other widget's
1374
+ * fragments inside that widget's screen rect are discarded so the
1375
+ * promoted DOM CardChrome (now above the R3F canvas) shows through.
1376
+ *
1377
+ * `uHotPoint` + `uHotStrength` + `uIsOverlapTarget` drive the
1378
+ * single-layer overlap glow — a soft radial gradient at the
1379
+ * intersection centroid that fades out via `uGlowFalloff`. Color and
1380
+ * alpha are tunable via {@link sharedGlowUniforms}.
1381
+ */
1382
+ const VERTEX_SHADER = `
1383
+ varying vec2 vUv;
1384
+ void main() {
1385
+ vUv = uv;
1386
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1387
+ }
1388
+ `;
1389
+ const FRAGMENT_SHADER = `
1390
+ uniform sampler2D map;
1391
+ uniform vec4 uDraggedRect;
1392
+ uniform float uIsDragged;
1393
+ uniform vec2 uHotPoint;
1394
+ uniform float uHotStrength;
1395
+ uniform float uIsOverlapTarget;
1396
+
1397
+ uniform vec3 uGlowColor;
1398
+ uniform vec2 uGlowAlpha;
1399
+ uniform vec2 uGlowFalloff;
1400
+ uniform vec3 uRimColor;
1401
+ uniform vec2 uRimAlpha;
1402
+ uniform float uRimRadius;
1403
+
1404
+ varying vec2 vUv;
1405
+
1406
+ void main() {
1407
+ if (uIsDragged < 0.5) {
1408
+ vec2 sp = gl_FragCoord.xy;
1409
+ if (sp.x >= uDraggedRect.x && sp.x <= uDraggedRect.z &&
1410
+ sp.y >= uDraggedRect.y && sp.y <= uDraggedRect.w) {
1411
+ discard;
1412
+ }
1413
+ }
1414
+ vec4 c = texture2D(map, vUv);
1415
+ if (c.a < 0.001) discard;
1416
+
1417
+ // Two-part overlap highlight, both tinted with uGlowColor:
1418
+ // 1. Inner glow — soft radial at the hot point, falling off to
1419
+ // transparent at uGlowFalloff.
1420
+ // 2. Rim — a thin band along the card edge that pools toward the
1421
+ // hot point (rimBand AND rimFocus must both be high).
1422
+ if (uHotStrength > 0.001) {
1423
+ float d = length(vUv - uHotPoint);
1424
+
1425
+ // Inner glow.
1426
+ float falloff = mix(uGlowFalloff.x, uGlowFalloff.y, uIsOverlapTarget);
1427
+ float alpha = mix(uGlowAlpha.x, uGlowAlpha.y, uIsOverlapTarget);
1428
+ float glow = smoothstep(falloff, 0.0, d) * uHotStrength * alpha;
1429
+ c.rgb = mix(c.rgb, uGlowColor, glow);
1430
+
1431
+ // Rim — band thickness fixed at 0.028 in uv (~3% of card on each
1432
+ // axis); brightness pools toward the hot point via rimFocus, with
1433
+ // falloff distance derived from uRimRadius (matches the DOM
1434
+ // radial-gradient(uRimRadius circle ..., transparent 40%)).
1435
+ float edge = min(min(vUv.x, 1.0 - vUv.x), min(vUv.y, 1.0 - vUv.y));
1436
+ float rimBand = smoothstep(0.028, 0.0, edge);
1437
+ float rimFocus = smoothstep(uRimRadius * 0.4, 0.0, d);
1438
+ float rimA = mix(uRimAlpha.x, uRimAlpha.y, uIsOverlapTarget);
1439
+ float rim = rimBand * rimFocus * uHotStrength * rimA;
1440
+ c.rgb = mix(c.rgb, uRimColor, rim);
1441
+ }
1442
+
1443
+ gl_FragColor = c;
1444
+ #include <colorspace_fragment>
1445
+ }
1446
+ `;
1447
+ /**
1448
+ * Per-instance composition material. Each widget's quad gets its own
1449
+ * instance so the per-quad uniforms (`map`, `uIsDragged`,
1450
+ * `uDraggedRect`) are independent. Three.js compiles the shader once
1451
+ * and reuses the program across instances since they share
1452
+ * vertex/fragment source — verify in dev via
1453
+ * `renderer.info.programs.length === 1` for the composition shader.
1454
+ */
1455
+ var CompositionMaterial = class extends three$1.ShaderMaterial {
1456
+ constructor() {
1457
+ super({
1458
+ vertexShader: VERTEX_SHADER,
1459
+ fragmentShader: FRAGMENT_SHADER,
1460
+ uniforms: {
1461
+ map: { value: null },
1462
+ uDraggedRect: { value: new three$1.Vector4(0, 0, 0, 0) },
1463
+ uIsDragged: { value: 0 },
1464
+ uHotPoint: { value: new three$1.Vector2(.5, .5) },
1465
+ uHotStrength: { value: 0 },
1466
+ uIsOverlapTarget: { value: 0 },
1467
+ uGlowColor: sharedGlowUniforms.uGlowColor,
1468
+ uGlowAlpha: sharedGlowUniforms.uGlowAlpha,
1469
+ uGlowFalloff: sharedGlowUniforms.uGlowFalloff,
1470
+ uRimColor: sharedGlowUniforms.uRimColor,
1471
+ uRimAlpha: sharedGlowUniforms.uRimAlpha,
1472
+ uRimRadius: sharedGlowUniforms.uRimRadius
1473
+ },
1474
+ transparent: true,
1475
+ depthWrite: false
1476
+ });
1477
+ }
1478
+ setMap(map) {
1479
+ this.uniforms.map.value = map;
1480
+ }
1481
+ setDraggedRect(minX, minY, maxX, maxY) {
1482
+ this.uniforms.uDraggedRect.value.set(minX, minY, maxX, maxY);
1483
+ }
1484
+ setIsDragged(isDragged) {
1485
+ this.uniforms.uIsDragged.value = isDragged ? 1 : 0;
1486
+ }
1487
+ /** Set the radial-glow centre in uv space (0..1). */
1488
+ setHotPoint(x, y) {
1489
+ this.uniforms.uHotPoint.value.set(x, y);
1490
+ }
1491
+ /** 0..1 — glow opacity; set to 0 to disable. */
1492
+ setHotStrength(strength) {
1493
+ this.uniforms.uHotStrength.value = strength;
1494
+ }
1495
+ /** Toggle target state (mixes toward the target alpha / falloff). */
1496
+ setIsOverlapTarget(on) {
1497
+ this.uniforms.uIsOverlapTarget.value = on ? 1 : 0;
1498
+ }
1499
+ };
1500
+ //#endregion
1501
+ //#region src/r3f/compositor/CompositorContext.tsx
1502
+ const CompositorContext = (0, react.createContext)(null);
1503
+ function useCompositor() {
1504
+ const ctx = (0, react.useContext)(CompositorContext);
1505
+ if (!ctx) throw new Error("useCompositor must be used inside <Compositor>");
1506
+ return ctx;
1507
+ }
1508
+ //#endregion
1509
+ //#region src/r3f/compositor/eviction.ts
1510
+ /**
1511
+ * Eviction priority — lower numbers evict first. Hot and Waking are never
1512
+ * evicted because they are about to be (or are actively) painted.
1513
+ *
1514
+ * Cold 0 off-screen, eviction-eligible immediately
1515
+ * Warm 1 visible + idle, retained while budget allows
1516
+ * Dormant 2 inactive but eviction-protected — only released as a
1517
+ * last resort because losing the FBO costs the
1518
+ * "instant re-activation" guarantee
1519
+ * Waking ∞ scheduled to repaint imminently — never evict
1520
+ * Hot ∞ actively painted every frame — never evict
1521
+ */
1522
+ const PHASE_PRIORITY = {
1523
+ Cold: 0,
1524
+ Warm: 1,
1525
+ Dormant: 2,
1526
+ Waking: Number.POSITIVE_INFINITY,
1527
+ Hot: Number.POSITIVE_INFINITY
1528
+ };
1529
+ /**
1530
+ * Returns the ordered list of entity ids to release so total bytes ≤ budget.
1531
+ * Sort key: `(phasePriority, lastUsedMs)` — within a phase, oldest goes
1532
+ * first (LRU). Hot / Waking widgets are never returned even if the budget
1533
+ * is impossible to satisfy without them.
1534
+ *
1535
+ * Pure function — Compositor calls this in its useFrame and forwards each
1536
+ * id to `pool.release()`.
1537
+ */
1538
+ function selectEvictions(candidates, totalBytes, maxBytes) {
1539
+ if (totalBytes <= maxBytes) return [];
1540
+ const eligible = candidates.filter((c) => Number.isFinite(PHASE_PRIORITY[c.phase]));
1541
+ eligible.sort((a, b) => {
1542
+ const p = PHASE_PRIORITY[a.phase] - PHASE_PRIORITY[b.phase];
1543
+ if (p !== 0) return p;
1544
+ return a.lastUsedMs - b.lastUsedMs;
1545
+ });
1546
+ const toEvict = [];
1547
+ let remaining = totalBytes;
1548
+ for (const c of eligible) {
1549
+ if (remaining <= maxBytes) break;
1550
+ toEvict.push(c.entityId);
1551
+ remaining -= c.bytes;
1552
+ }
1553
+ return toEvict;
1554
+ }
1555
+ //#endregion
1556
+ //#region src/r3f/compositor/ResourceRegistry.ts
1557
+ /**
1558
+ * Archetype-keyed cache of GPU resources shared across per-widget scenes
1559
+ * (RFC-002 § Three.js resource sharing).
1560
+ *
1561
+ * Geometries, materials, and textures returned by `acquire*` are reference
1562
+ * counted. The registry disposes the underlying resource only after every
1563
+ * holder has called the matching `release*` — so 100 widgets of the same
1564
+ * card archetype share one geometry instance and one set of material
1565
+ * uniforms, rather than allocating 100 copies.
1566
+ */
1567
+ var ResourceRegistry = class {
1568
+ geometries = /* @__PURE__ */ new Map();
1569
+ materials = /* @__PURE__ */ new Map();
1570
+ textures = /* @__PURE__ */ new Map();
1571
+ disposed = false;
1572
+ acquireGeometry(key, factory) {
1573
+ const existing = this.geometries.get(key);
1574
+ if (existing) {
1575
+ existing.refCount++;
1576
+ return existing.resource;
1577
+ }
1578
+ const resource = factory();
1579
+ this.geometries.set(key, {
1580
+ resource,
1581
+ refCount: 1
1582
+ });
1583
+ return resource;
1584
+ }
1585
+ releaseGeometry(key) {
1586
+ this.release(this.geometries, key);
1587
+ }
1588
+ acquireMaterial(key, factory) {
1589
+ const existing = this.materials.get(key);
1590
+ if (existing) {
1591
+ existing.refCount++;
1592
+ return existing.resource;
1593
+ }
1594
+ const resource = factory();
1595
+ this.materials.set(key, {
1596
+ resource,
1597
+ refCount: 1
1598
+ });
1599
+ return resource;
1600
+ }
1601
+ releaseMaterial(key) {
1602
+ this.release(this.materials, key);
1603
+ }
1604
+ acquireTexture(key, factory) {
1605
+ const existing = this.textures.get(key);
1606
+ if (existing) {
1607
+ existing.refCount++;
1608
+ return existing.resource;
1609
+ }
1610
+ const resource = factory();
1611
+ this.textures.set(key, {
1612
+ resource,
1613
+ refCount: 1
1614
+ });
1615
+ return resource;
1616
+ }
1617
+ releaseTexture(key) {
1618
+ this.release(this.textures, key);
1619
+ }
1620
+ /** Number of distinct shared geometries currently held. */
1621
+ geometryCount() {
1622
+ return this.geometries.size;
1623
+ }
1624
+ /** Number of distinct shared materials currently held. */
1625
+ materialCount() {
1626
+ return this.materials.size;
1627
+ }
1628
+ /** Number of distinct shared textures currently held. */
1629
+ textureCount() {
1630
+ return this.textures.size;
1631
+ }
1632
+ /**
1633
+ * Estimated GPU bytes for shared geometry attribute buffers. Best-effort —
1634
+ * actual GPU footprint depends on driver alignment, but this is a useful
1635
+ * relative metric for the profiler.
1636
+ */
1637
+ geometryBytes() {
1638
+ let total = 0;
1639
+ for (const { resource } of this.geometries.values()) {
1640
+ for (const attr of Object.values(resource.attributes)) if ("array" in attr && attr.array.byteLength) total += attr.array.byteLength;
1641
+ if (resource.index) total += resource.index.array.byteLength;
1642
+ }
1643
+ return total;
1644
+ }
1645
+ /** Dispose every resource and clear the registry. */
1646
+ dispose() {
1647
+ if (this.disposed) return;
1648
+ for (const { resource } of this.geometries.values()) resource.dispose();
1649
+ for (const { resource } of this.materials.values()) resource.dispose();
1650
+ for (const { resource } of this.textures.values()) resource.dispose();
1651
+ this.geometries.clear();
1652
+ this.materials.clear();
1653
+ this.textures.clear();
1654
+ this.disposed = true;
1655
+ }
1656
+ /** True after `dispose()` — callers should re-create the registry instead of using it. */
1657
+ isDisposed() {
1658
+ return this.disposed;
1659
+ }
1660
+ release(map, key) {
1661
+ const entry = map.get(key);
1662
+ if (!entry) return;
1663
+ entry.refCount--;
1664
+ if (entry.refCount <= 0) queueMicrotask(() => {
1665
+ const current = map.get(key);
1666
+ if (current && current.refCount <= 0) {
1667
+ current.resource.dispose();
1668
+ map.delete(key);
1669
+ }
1670
+ });
1671
+ }
1672
+ };
1673
+ const R3FRenderState = (0, _jamesyong42_reactive_ecs.defineComponent)("R3FRenderState", {
1674
+ phase: "Cold",
1675
+ paintedAt: {
1676
+ width: 0,
1677
+ height: 0,
1678
+ dpr: 1,
1679
+ zoom: 1
1680
+ },
1681
+ animating: false,
1682
+ paintGeneration: 0,
1683
+ fboGeneration: -1
1684
+ });
1685
+ /**
1686
+ * Opt-in tag — a widget that wants per-frame ticking sets this. The state
1687
+ * machine treats `Visible + R3FAnimationSignal` as the trigger for `Hot`.
1688
+ * Removing the tag transitions back to `Warm` when the widget settles.
1689
+ */
1690
+ const R3FAnimationSignal = (0, _jamesyong42_reactive_ecs.defineTag)("R3FAnimationSignal");
1691
+ const R3FRenderBudget = (0, _jamesyong42_reactive_ecs.defineResource)("R3FRenderBudget", {
1692
+ maxBytes: 256 * 1024 * 1024,
1693
+ currentBytes: 0,
1694
+ maxRepaintsPerFrame: 4
1695
+ });
1696
+ //#endregion
1697
+ //#region src/r3f/compositor/WidgetRenderTargetPool.ts
1698
+ /**
1699
+ * Bytes per pixel for the default render target format (RGBA8 colour +
1700
+ * 24-bit depth + 8-bit stencil packed into 32 bits = 8 bytes/pixel).
1701
+ */
1702
+ const BYTES_PER_PIXEL = 8;
1703
+ /**
1704
+ * Allocates one persistent `WebGLRenderTarget` per R3F widget entity. Used
1705
+ * by the compositor (RFC-002 Phase 4) so each widget paints into its own
1706
+ * texture instead of into the main canvas backbuffer.
1707
+ *
1708
+ * Acquire returns an existing FBO if its pixel resolution matches the
1709
+ * request; otherwise the old FBO is disposed and a new one created (sized
1710
+ * to the new resolution). Eviction under memory pressure is Phase 6 — this
1711
+ * pool grows monotonically until widgets are released.
1712
+ */
1713
+ var WidgetRenderTargetPool = class {
1714
+ entries = /* @__PURE__ */ new Map();
1715
+ totalBytes = 0;
1716
+ disposed = false;
1717
+ /**
1718
+ * Get or create an FBO for `entityId` at the requested logical size and
1719
+ * device pixel ratio. If an FBO already exists at the same pixel
1720
+ * dimensions, returns it unchanged.
1721
+ */
1722
+ acquire(entityId, width, height, dpr) {
1723
+ if (this.disposed) throw new Error("WidgetRenderTargetPool: cannot acquire after dispose");
1724
+ const pixelWidth = Math.max(1, Math.round(width * dpr));
1725
+ const pixelHeight = Math.max(1, Math.round(height * dpr));
1726
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : 0;
1727
+ const existing = this.entries.get(entityId);
1728
+ if (existing && existing.pixelWidth === pixelWidth && existing.pixelHeight === pixelHeight) {
1729
+ existing.lastUsedMs = now;
1730
+ return existing.rt;
1731
+ }
1732
+ if (existing) {
1733
+ existing.rt.dispose();
1734
+ this.totalBytes -= existing.bytes;
1735
+ }
1736
+ const rt = new three$1.WebGLRenderTarget(pixelWidth, pixelHeight, { samples: 4 });
1737
+ rt.texture.colorSpace = three$1.SRGBColorSpace;
1738
+ const bytes = pixelWidth * pixelHeight * BYTES_PER_PIXEL;
1739
+ this.entries.set(entityId, {
1740
+ rt,
1741
+ pixelWidth,
1742
+ pixelHeight,
1743
+ dpr,
1744
+ bytes,
1745
+ lastUsedMs: now
1746
+ });
1747
+ this.totalBytes += bytes;
1748
+ return rt;
1749
+ }
1750
+ /** Look up an FBO without creating one. */
1751
+ get(entityId) {
1752
+ return this.entries.get(entityId)?.rt ?? null;
1753
+ }
1754
+ /**
1755
+ * Refresh `lastUsedMs` without re-acquiring. The Compositor calls this
1756
+ * for every widget it samples in the composition pass — without it,
1757
+ * Warm widgets that never repaint would freeze their `lastUsedMs` at
1758
+ * the time of their last paint, and eviction LRU would treat
1759
+ * still-visible widgets as stale.
1760
+ */
1761
+ touch(entityId) {
1762
+ const entry = this.entries.get(entityId);
1763
+ if (!entry) return;
1764
+ entry.lastUsedMs = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : 0;
1765
+ }
1766
+ /**
1767
+ * Release `entityId`'s FBO. Returns true if something was released.
1768
+ * Safe to call after dispose — returns false rather than corrupting the
1769
+ * byte counter or double-disposing the target.
1770
+ */
1771
+ release(entityId) {
1772
+ if (this.disposed) return false;
1773
+ const entry = this.entries.get(entityId);
1774
+ if (!entry) return false;
1775
+ entry.rt.dispose();
1776
+ this.totalBytes = Math.max(0, this.totalBytes - entry.bytes);
1777
+ this.entries.delete(entityId);
1778
+ return true;
1779
+ }
1780
+ /** Total GPU bytes consumed by the pool. */
1781
+ bytesUsed() {
1782
+ return this.totalBytes;
1783
+ }
1784
+ /** Number of FBOs currently held. */
1785
+ size() {
1786
+ return this.entries.size;
1787
+ }
1788
+ /** True after `dispose()` — callers should re-create the pool instead of using it. */
1789
+ isDisposed() {
1790
+ return this.disposed;
1791
+ }
1792
+ /** Iterate live entries. */
1793
+ forEach(cb) {
1794
+ for (const [id, entry] of this.entries) cb(id, entry.rt);
1795
+ }
1796
+ /**
1797
+ * Snapshot of every live entry's `bytes` + `lastUsedMs` — input for the
1798
+ * eviction algorithm in {@link selectEvictions}.
1799
+ */
1800
+ entryInfos() {
1801
+ const out = [];
1802
+ for (const [id, entry] of this.entries) out.push({
1803
+ entityId: id,
1804
+ bytes: entry.bytes,
1805
+ lastUsedMs: entry.lastUsedMs
1806
+ });
1807
+ return out;
1808
+ }
1809
+ /** Dispose every FBO. After this, acquire throws and release returns false. */
1810
+ dispose() {
1811
+ if (this.disposed) return;
1812
+ for (const entry of this.entries.values()) entry.rt.dispose();
1813
+ this.entries.clear();
1814
+ this.totalBytes = 0;
1815
+ this.disposed = true;
1816
+ }
1817
+ };
1818
+ //#endregion
1819
+ //#region src/r3f/compositor/ZoomBands.ts
1820
+ /**
1821
+ * Hysteresis-banded zoom-resolution policy (RFC-002 § Zoom handling).
1822
+ *
1823
+ * A widget's FBO is allocated at `widget bounds × dpr × band(zoom)` pixels.
1824
+ * As long as the camera zoom stays within `[band × 0.5, band × 2]` of the
1825
+ * band the widget was painted at, no repaint is needed — the composition
1826
+ * shader does a small up/down sample. Crossing that gap triggers a repaint
1827
+ * at the new band, snapping back to a 1:1 (or near-1:1) sample ratio.
1828
+ *
1829
+ * Bands are powers of 2 so each band covers a 4× display range. With the
1830
+ * default ladder (0.0625 ↔ 16) we span camera zooms 0.03125 ↔ 32 — covering
1831
+ * essentially every realistic infinite-canvas zoom level.
1832
+ */
1833
+ const ZOOM_BANDS = [
1834
+ .0625,
1835
+ .125,
1836
+ .25,
1837
+ .5,
1838
+ 1,
1839
+ 2,
1840
+ 4,
1841
+ 8,
1842
+ 16
1843
+ ];
1844
+ /**
1845
+ * Pick the band whose `[band × 0.5, band × 2]` range contains the current
1846
+ * zoom. The smallest band ≥ zoom (after rounding to a band edge) is the
1847
+ * canonical choice — keeps the texture resolution at or above what the
1848
+ * display needs.
1849
+ */
1850
+ function selectBand(zoom) {
1851
+ if (zoom <= ZOOM_BANDS[0]) return ZOOM_BANDS[0];
1852
+ if (zoom >= ZOOM_BANDS[ZOOM_BANDS.length - 1]) return ZOOM_BANDS[ZOOM_BANDS.length - 1];
1853
+ for (const b of ZOOM_BANDS) if (zoom <= b) return b;
1854
+ return ZOOM_BANDS[ZOOM_BANDS.length - 1];
1855
+ }
1856
+ /**
1857
+ * Returns true if the widget needs to be repainted because the camera
1858
+ * zoom has wandered outside the tolerance window of the band it was
1859
+ * painted at.
1860
+ *
1861
+ * `paintedBand` of 0 (or negative) means the widget has never been
1862
+ * painted yet — caller should treat that as "needs paint" through other
1863
+ * channels (Waking phase / paintGeneration > fboGeneration).
1864
+ */
1865
+ function isOutOfBand(currentZoom, paintedBand) {
1866
+ if (paintedBand <= 0) return false;
1867
+ const ratio = currentZoom / paintedBand;
1868
+ return ratio > 2 || ratio < .5;
1869
+ }
1870
+ //#endregion
1871
+ //#region src/r3f/compositor/Compositor.tsx
1872
+ /**
1873
+ * Drives the per-widget paint + composition render loop (RFC-002 Phase 4).
1874
+ *
1875
+ * Lifecycle each invalidation:
1876
+ * 1. For every registered widget whose phase is Hot or Waking (or whose
1877
+ * paintGeneration > fboGeneration), bind its FBO and render its scene
1878
+ * with its widget-local camera.
1879
+ * 2. Update each widget's composition quad (position, scale, texture).
1880
+ * 3. Render the composition scene to the canvas backbuffer with a
1881
+ * world-space orthographic camera matching the engine camera.
1882
+ *
1883
+ * Owns the `WidgetRenderTargetPool`. Children mount `<VirtualWidget />`
1884
+ * instances which register their scene+camera via context.
1885
+ *
1886
+ * Replaces the old `R3FWidgetSlot` + `CameraSync` pair.
1887
+ */
1888
+ function Compositor({ engine, widgetRegistry, children }) {
1889
+ const { gl, size, scene: defaultScene, set } = (0, _react_three_fiber.useThree)();
1890
+ const invalidate = (0, _react_three_fiber.useThree)((s) => s.invalidate);
1891
+ const poolRef = (0, react.useRef)(null);
1892
+ if (!poolRef.current || poolRef.current.isDisposed()) poolRef.current = new WidgetRenderTargetPool();
1893
+ const pool = poolRef.current;
1894
+ const registryRef = (0, react.useRef)(null);
1895
+ if (!registryRef.current || registryRef.current.isDisposed()) registryRef.current = new ResourceRegistry();
1896
+ const registry = registryRef.current;
1897
+ const quadGeometry = (0, react.useMemo)(() => new three.PlaneGeometry(1, 1), []);
1898
+ const quadsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
1899
+ const liftScaleRef = (0, react.useRef)(/* @__PURE__ */ new Map());
1900
+ const lastDprRef = (0, react.useRef)(-1);
1901
+ const idleDpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
1902
+ const gestureDpr = Math.min(idleDpr, 1);
1903
+ const compCamera = (0, react.useMemo)(() => new three.OrthographicCamera(0, 1, 0, -1, .1, 1e4), []);
1904
+ (0, react.useEffect)(() => {
1905
+ set({ camera: compCamera });
1906
+ }, [set, compCamera]);
1907
+ const register = (0, react.useCallback)((entityId, entry) => {
1908
+ const unregisterFromRegistry = widgetRegistry.register(entityId, entry);
1909
+ const mesh = new three.Mesh(quadGeometry, new CompositionMaterial());
1910
+ mesh.frustumCulled = false;
1911
+ mesh.visible = false;
1912
+ defaultScene.add(mesh);
1913
+ quadsRef.current.set(entityId, mesh);
1914
+ entry.requestRepaint();
1915
+ return () => {
1916
+ unregisterFromRegistry();
1917
+ const m = quadsRef.current.get(entityId);
1918
+ if (m) {
1919
+ defaultScene.remove(m);
1920
+ m.material.dispose();
1921
+ quadsRef.current.delete(entityId);
1922
+ }
1923
+ liftScaleRef.current.delete(entityId);
1924
+ pool.release(entityId);
1925
+ };
1926
+ }, [
1927
+ defaultScene,
1928
+ pool,
1929
+ quadGeometry,
1930
+ widgetRegistry
1931
+ ]);
1932
+ const ctxValue = (0, react.useMemo)(() => ({
1933
+ pool,
1934
+ registry,
1935
+ register
1936
+ }), [
1937
+ pool,
1938
+ registry,
1939
+ register
1940
+ ]);
1941
+ (0, react.useEffect)(() => {
1942
+ return () => {
1943
+ pool.dispose();
1944
+ registry.dispose();
1945
+ };
1946
+ }, [pool, registry]);
1947
+ (0, _react_three_fiber.useFrame)(() => {
1948
+ const cam = engine.getCamera();
1949
+ compCamera.left = 0;
1950
+ compCamera.right = size.width / cam.zoom;
1951
+ compCamera.top = 0;
1952
+ compCamera.bottom = -(size.height / cam.zoom);
1953
+ compCamera.position.set(cam.x, -cam.y, 1e3);
1954
+ compCamera.updateProjectionMatrix();
1955
+ const targetDpr = cam.gesturing ? gestureDpr : idleDpr;
1956
+ if (lastDprRef.current !== targetDpr) {
1957
+ gl.setPixelRatio(targetDpr);
1958
+ lastDprRef.current = targetDpr;
1959
+ }
1960
+ const dpr = gl.getPixelRatio();
1961
+ const world = engine.world;
1962
+ let sharedEnv = null;
1963
+ if (defaultScene.environment) sharedEnv = defaultScene.environment;
1964
+ else for (const [, entry] of widgetRegistry.all()) if (entry.scene.environment) {
1965
+ sharedEnv = entry.scene.environment;
1966
+ break;
1967
+ }
1968
+ if (sharedEnv) for (const [eid, entry] of widgetRegistry.all()) {
1969
+ if (entry.scene.environment === sharedEnv) continue;
1970
+ entry.scene.environment = sharedEnv;
1971
+ const s = world.getComponent(eid, R3FRenderState);
1972
+ if (s) world.setComponent(eid, R3FRenderState, {
1973
+ ...s,
1974
+ paintGeneration: s.paintGeneration + 1
1975
+ });
1976
+ }
1977
+ const band = selectBand(cam.zoom);
1978
+ const effectiveDpr = dpr * band;
1979
+ let widgetsRepainted = 0;
1980
+ for (const [entityId, entry] of widgetRegistry.all()) {
1981
+ const wt = world.getComponent(entityId, require_ecs.Transform2D);
1982
+ if (!wt) continue;
1983
+ const state = world.getComponent(entityId, R3FRenderState);
1984
+ if (!state) continue;
1985
+ const phaseWantsPaint = state.phase === "Hot" || state.phase === "Waking";
1986
+ const generationDirty = state.paintGeneration > state.fboGeneration;
1987
+ const bandChanged = !cam.gesturing && isOutOfBand(cam.zoom, state.paintedAt.zoom);
1988
+ if (!phaseWantsPaint && !generationDirty && !bandChanged && pool.get(entityId) !== null) continue;
1989
+ const fbo = pool.acquire(entityId, wt.width, wt.height, effectiveDpr);
1990
+ gl.setRenderTarget(fbo);
1991
+ try {
1992
+ gl.setClearColor(0, 0);
1993
+ gl.clear(true, true, false);
1994
+ gl.render(entry.scene, entry.camera);
1995
+ } finally {
1996
+ gl.setRenderTarget(null);
1997
+ }
1998
+ world.setComponent(entityId, R3FRenderState, {
1999
+ ...state,
2000
+ fboGeneration: state.paintGeneration,
2001
+ paintedAt: {
2002
+ width: wt.width,
2003
+ height: wt.height,
2004
+ dpr: effectiveDpr,
2005
+ zoom: band
2006
+ }
2007
+ });
2008
+ widgetsRepainted++;
2009
+ }
2010
+ const budget = world.getResource(R3FRenderBudget);
2011
+ if (pool.bytesUsed() > budget.maxBytes) {
2012
+ const candidates = [];
2013
+ for (const info of pool.entryInfos()) {
2014
+ const s = world.getComponent(info.entityId, R3FRenderState);
2015
+ if (!s) continue;
2016
+ candidates.push({
2017
+ entityId: info.entityId,
2018
+ phase: s.phase,
2019
+ bytes: info.bytes,
2020
+ lastUsedMs: info.lastUsedMs
2021
+ });
2022
+ }
2023
+ const toEvict = selectEvictions(candidates, pool.bytesUsed(), budget.maxBytes);
2024
+ for (const eid of toEvict) {
2025
+ const s = world.getComponent(eid, R3FRenderState);
2026
+ if (s?.phase === "Dormant") console.debug("[r3f-compositor] evicting Dormant widget", eid, "— consider raising R3FRenderBudget.maxBytes");
2027
+ pool.release(eid);
2028
+ if (s) world.setComponent(eid, R3FRenderState, {
2029
+ ...s,
2030
+ fboGeneration: -1
2031
+ });
2032
+ }
2033
+ }
2034
+ let draggedEntityId = null;
2035
+ let draggedRectMinX = 0;
2036
+ let draggedRectMinY = 0;
2037
+ let draggedRectMaxX = 0;
2038
+ let draggedRectMaxY = 0;
2039
+ const canvasHeightPx = size.height * dpr;
2040
+ for (const entityId of widgetRegistry.keys()) {
2041
+ if (!world.hasTag(entityId, require_ecs.Dragging)) continue;
2042
+ if (world.getComponent(entityId, require_ecs.Widget)?.surface !== "webgl") continue;
2043
+ draggedEntityId = entityId;
2044
+ if (!world.hasComponent(entityId, require_ecs.Card)) break;
2045
+ const dt = world.getComponent(entityId, require_ecs.Transform2D);
2046
+ if (!dt) break;
2047
+ const lift = liftScaleRef.current.get(entityId) ?? 1;
2048
+ const cx = dt.x + dt.width / 2;
2049
+ const cy = dt.y + dt.height / 2;
2050
+ const halfW = dt.width * lift / 2;
2051
+ const halfH = dt.height * lift / 2;
2052
+ const minWx = cx - halfW;
2053
+ const maxWx = cx + halfW;
2054
+ const minWy = cy - halfH;
2055
+ const maxWy = cy + halfH;
2056
+ const sxMin = (minWx - cam.x) * cam.zoom * dpr;
2057
+ const sxMax = (maxWx - cam.x) * cam.zoom * dpr;
2058
+ const syTop = (minWy - cam.y) * cam.zoom * dpr;
2059
+ const syBot = (maxWy - cam.y) * cam.zoom * dpr;
2060
+ draggedRectMinX = sxMin;
2061
+ draggedRectMinY = canvasHeightPx - syBot;
2062
+ draggedRectMaxX = sxMax;
2063
+ draggedRectMaxY = canvasHeightPx - syTop;
2064
+ break;
2065
+ }
2066
+ let liftSettling = false;
2067
+ for (const [entityId, mesh] of quadsRef.current) {
2068
+ const qt = world.getComponent(entityId, require_ecs.Transform2D);
2069
+ const state = world.getComponent(entityId, R3FRenderState);
2070
+ const fbo = pool.get(entityId);
2071
+ if (!qt || !fbo || !state || state.fboGeneration < 0) {
2072
+ mesh.visible = false;
2073
+ continue;
2074
+ }
2075
+ pool.touch(entityId);
2076
+ const targetScale = world.hasTag(entityId, require_ecs.Dragging) ? 1.05 : 1;
2077
+ let scale = liftScaleRef.current.get(entityId) ?? 1;
2078
+ scale += (targetScale - scale) * .2;
2079
+ if (Math.abs(targetScale - scale) > .001) liftSettling = true;
2080
+ else scale = targetScale;
2081
+ liftScaleRef.current.set(entityId, scale);
2082
+ mesh.visible = true;
2083
+ mesh.position.set(qt.x + qt.width / 2, -(qt.y + qt.height / 2), 0);
2084
+ mesh.scale.set(qt.width * scale, qt.height * scale, 1);
2085
+ mesh.renderOrder = entityId === draggedEntityId ? 99 : 1;
2086
+ const material = mesh.material;
2087
+ material.setMap(fbo.texture);
2088
+ material.setIsDragged(entityId === draggedEntityId);
2089
+ material.setDraggedRect(draggedRectMinX, draggedRectMinY, draggedRectMaxX, draggedRectMaxY);
2090
+ const hot = world.getComponent(entityId, require_ecs.CardOverlapHotPoint);
2091
+ if (hot && world.hasTag(entityId, require_ecs.OverlapCandidate)) {
2092
+ material.setHotPoint(hot.x, 1 - hot.y);
2093
+ material.setHotStrength(hot.strength);
2094
+ } else material.setHotStrength(0);
2095
+ material.setIsOverlapTarget(world.hasTag(entityId, require_ecs.OverlapTarget));
2096
+ }
2097
+ gl.setRenderTarget(null);
2098
+ gl.setClearColor(0, 0);
2099
+ gl.clear(true, true, false);
2100
+ gl.render(defaultScene, compCamera);
2101
+ COMPOSITOR_TELEMETRY.widgetsRepainted = widgetsRepainted;
2102
+ COMPOSITOR_TELEMETRY.fboBytes = pool.bytesUsed();
2103
+ let anyHot = false;
2104
+ for (const eid of widgetRegistry.keys()) if (world.getComponent(eid, R3FRenderState)?.phase === "Hot") {
2105
+ anyHot = true;
2106
+ break;
2107
+ }
2108
+ if (anyHot || liftSettling) invalidate();
2109
+ }, 1);
2110
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompositorContext.Provider, {
2111
+ value: ctxValue,
2112
+ children
2113
+ });
2114
+ }
2115
+ /**
2116
+ * Shared between Compositor and ProfilerProbe so the probe can record FBO
2117
+ * bytes and per-frame repaint counts without an extra subscription path.
2118
+ * Module-scoped because both components live in the same canvas.
2119
+ */
2120
+ const COMPOSITOR_TELEMETRY = {
2121
+ widgetsRepainted: 0,
2122
+ fboBytes: 0
2123
+ };
2124
+ //#endregion
2125
+ //#region src/r3f/compositor/VirtualWidget.tsx
2126
+ /**
2127
+ * Mounts one R3F widget into its own Three.js scene + ortho camera so it
2128
+ * can be painted into a private `WebGLRenderTarget` instead of the main
2129
+ * canvas backbuffer.
2130
+ *
2131
+ * The user component is rendered in widget-local space — origin at centre,
2132
+ * X right, Y up, dimensions = (Transform2D.width, Transform2D.height) in
2133
+ * frame-local world units.
2134
+ * That matches the contract the previous `R3FWidgetSlot` exposed, so user
2135
+ * widget code (e.g. `geometry-card`) needs no changes.
2136
+ *
2137
+ * VirtualWidget itself does not paint or composite — that's the
2138
+ * Compositor's job. We just create the scene/camera and register them so
2139
+ * the Compositor can iterate widgets in its render loop.
2140
+ */
2141
+ function VirtualWidget({ entityId, component: Component }) {
2142
+ const { register } = useCompositor();
2143
+ const invalidate = (0, _react_three_fiber.useThree)((s) => s.invalidate);
2144
+ const engine = require_ecs.useLayoutEngine();
2145
+ const scene = (0, react.useMemo)(() => new three.Scene(), []);
2146
+ const camera = (0, react.useMemo)(() => new three.OrthographicCamera(-1, 1, 1, -1, .1, 1e3), []);
2147
+ (0, react.useEffect)(() => {
2148
+ camera.position.set(0, 0, 100);
2149
+ camera.lookAt(0, 0, 0);
2150
+ }, [camera]);
2151
+ const t = require_ecs.useComponent(entityId, require_ecs.Transform2D);
2152
+ const w = t?.width ?? 0;
2153
+ const h = t?.height ?? 0;
2154
+ (0, react.useLayoutEffect)(() => {
2155
+ if (!w || !h) return;
2156
+ camera.left = -w / 2;
2157
+ camera.right = w / 2;
2158
+ camera.top = h / 2;
2159
+ camera.bottom = -h / 2;
2160
+ camera.updateProjectionMatrix();
2161
+ const current = engine.world.getComponent(entityId, R3FRenderState);
2162
+ if (current) engine.world.setComponent(entityId, R3FRenderState, {
2163
+ ...current,
2164
+ paintGeneration: current.paintGeneration + 1
2165
+ });
2166
+ invalidate();
2167
+ }, [
2168
+ camera,
2169
+ w,
2170
+ h,
2171
+ engine,
2172
+ entityId,
2173
+ invalidate
2174
+ ]);
2175
+ (0, react.useEffect)(() => {
2176
+ return register(entityId, {
2177
+ scene,
2178
+ camera,
2179
+ requestRepaint: invalidate
2180
+ });
2181
+ }, [
2182
+ entityId,
2183
+ register,
2184
+ scene,
2185
+ camera,
2186
+ invalidate
2187
+ ]);
2188
+ if (!t) return null;
2189
+ return (0, _react_three_fiber.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
2190
+ entityId,
2191
+ width: t.width,
2192
+ height: t.height
2193
+ }), scene);
2194
+ }
2195
+ //#endregion
2196
+ //#region src/r3f/compositor/WidgetRegistry.ts
2197
+ /**
2198
+ * Stable per-canvas registry of R3F widget scenes + cameras (RFC-006).
2199
+ *
2200
+ * Created in `R3FManager` so it's reachable both by the `Compositor`
2201
+ * (which adds/removes widgets as `VirtualWidget` mounts) and by the
2202
+ * R3F event factory (which resolves the active widget by entityId
2203
+ * returned from `engine.pickAt`, then looks up its scene + camera here).
2204
+ *
2205
+ * Plain Map under the hood — the wrapping class exists to give the
2206
+ * registry a stable identity across React renders and to keep the read
2207
+ * surface (`get`, `keys`, `all`) discoverable from both consumers.
2208
+ */
2209
+ var WidgetRegistry = class {
2210
+ entries = /* @__PURE__ */ new Map();
2211
+ register(entityId, entry) {
2212
+ this.entries.set(entityId, entry);
2213
+ return () => this.entries.delete(entityId);
2214
+ }
2215
+ get(entityId) {
2216
+ return this.entries.get(entityId);
2217
+ }
2218
+ all() {
2219
+ return this.entries.entries();
2220
+ }
2221
+ keys() {
2222
+ return this.entries.keys();
2223
+ }
2224
+ values() {
2225
+ return this.entries.values();
2226
+ }
2227
+ clear() {
2228
+ this.entries.clear();
2229
+ }
2230
+ };
2231
+ //#endregion
2232
+ //#region src/r3f/compositor/WidgetStateMachine.tsx
2233
+ /**
2234
+ * Updates `R3FRenderState` phases for every R3F (`surface === 'webgl'`)
2235
+ * widget after each engine tick.
2236
+ *
2237
+ * At this phase the state machine is bookkeeping only — it does not
2238
+ * allocate or paint render targets (that arrives in Phase 4). Consumers
2239
+ * such as widget `useFrame` callbacks read the phase to decide whether
2240
+ * to do work this frame.
2241
+ *
2242
+ * Transition rules match RFC-002 § State machine:
2243
+ *
2244
+ * Active + Visible + R3FAnimationSignal → Hot
2245
+ * Active + Visible (idle) → Warm (or Waking if texture invalid — irrelevant pre-Phase-4)
2246
+ * Active + Culled → Cold
2247
+ * !Active → Dormant
2248
+ */
2249
+ function WidgetStateMachine({ engine }) {
2250
+ (0, react.useEffect)(() => {
2251
+ const world = engine.world;
2252
+ function updatePhases() {
2253
+ for (const entity of world.query(require_ecs.Widget)) {
2254
+ const widget = world.getComponent(entity, require_ecs.Widget);
2255
+ if (!widget || widget.surface !== "webgl") continue;
2256
+ const current = world.getComponent(entity, R3FRenderState);
2257
+ const animating = world.hasTag(entity, R3FAnimationSignal);
2258
+ const hasFbo = (current?.fboGeneration ?? -1) >= 0;
2259
+ const nextPhase = computePhase(world.hasTag(entity, require_ecs.Active), world.hasTag(entity, require_ecs.Visible), world.hasTag(entity, require_ecs.Culled), animating, hasFbo);
2260
+ if (!current) world.addComponent(entity, R3FRenderState, {
2261
+ phase: nextPhase,
2262
+ paintedAt: {
2263
+ width: 0,
2264
+ height: 0,
2265
+ dpr: 1,
2266
+ zoom: 1
2267
+ },
2268
+ animating,
2269
+ paintGeneration: 0,
2270
+ fboGeneration: -1
2271
+ });
2272
+ else if (current.phase !== nextPhase || current.animating !== animating) world.setComponent(entity, R3FRenderState, {
2273
+ ...current,
2274
+ phase: nextPhase,
2275
+ animating
2276
+ });
2277
+ }
2278
+ }
2279
+ updatePhases();
2280
+ return engine.onFrame(updatePhases);
2281
+ }, [engine]);
2282
+ return null;
2283
+ }
2284
+ /**
2285
+ * Pure function version of the state-machine transition rule. Exported so
2286
+ * tests can pin the truth table without mounting React.
2287
+ *
2288
+ * `hasFbo` distinguishes Warm (texture present, can be sampled) from Waking
2289
+ * (Visible but no valid texture yet — the compositor must paint before the
2290
+ * widget is composited). After eviction (Phase 6) a Cold widget can lose
2291
+ * its texture and re-enter as Waking.
2292
+ */
2293
+ function computePhase(active, visible, culled, animationSignal, hasFbo) {
2294
+ if (!active) return "Dormant";
2295
+ if (visible) {
2296
+ if (animationSignal) return "Hot";
2297
+ return hasFbo ? "Warm" : "Waking";
2298
+ }
2299
+ if (culled) return "Cold";
2300
+ return "Cold";
2301
+ }
2302
+ //#endregion
2303
+ //#region src/r3f/EngineInvalidator.tsx
2304
+ /**
2305
+ * Subscribes to engine frame events and invalidates the R3F canvas only when
2306
+ * something has changed that affects R3F output. With `frameloop="demand"` on
2307
+ * the parent `<Canvas>`, the canvas only renders in response to these
2308
+ * invalidations — idle scenes produce zero R3F frames.
2309
+ *
2310
+ * Triggers invalidation on: camera change, position change (drag/resize),
2311
+ * widgets entering or exiting the visible set.
2312
+ */
2313
+ function EngineInvalidator({ engine }) {
2314
+ const invalidate = (0, _react_three_fiber.useThree)((s) => s.invalidate);
2315
+ (0, react.useEffect)(() => {
2316
+ return engine.onFrame(() => {
2317
+ const c = engine.getFrameChanges();
2318
+ if (c.cameraChanged || c.positionsChanged.length > 0 || c.entered.length > 0 || c.exited.length > 0) invalidate();
2319
+ });
2320
+ }, [engine, invalidate]);
2321
+ return null;
2322
+ }
2323
+ //#endregion
2324
+ //#region src/r3f/ProfilerProbe.tsx
2325
+ /**
2326
+ * Reports one R3F frame sample per animation frame to the engine profiler.
2327
+ * Reads `renderer.info` from three.js — draw calls / triangles / memory /
2328
+ * programs — which is maintained by R3F's default render loop regardless
2329
+ * of whether we opt in. Only samples when the profiler is enabled.
2330
+ */
2331
+ function ProfilerProbe({ engine, widgetCount }) {
2332
+ const { gl } = (0, _react_three_fiber.useThree)();
2333
+ const prevTimeRef = (0, react.useRef)(null);
2334
+ const prevCallsRef = (0, react.useRef)(0);
2335
+ const prevTrianglesRef = (0, react.useRef)(0);
2336
+ const prevPointsRef = (0, react.useRef)(0);
2337
+ const prevLinesRef = (0, react.useRef)(0);
2338
+ (0, _react_three_fiber.useFrame)(() => {
2339
+ const profiler = engine.profiler;
2340
+ if (!profiler.isEnabled()) {
2341
+ prevTimeRef.current = null;
2342
+ return;
2343
+ }
2344
+ const now = performance.now();
2345
+ const dtMs = prevTimeRef.current === null ? 0 : now - prevTimeRef.current;
2346
+ prevTimeRef.current = now;
2347
+ const info = gl.info;
2348
+ const calls = info.render.calls;
2349
+ const triangles = info.render.triangles;
2350
+ const points = info.render.points;
2351
+ const lines = info.render.lines;
2352
+ const frameCalls = info.autoReset ? calls : Math.max(0, calls - prevCallsRef.current);
2353
+ const frameTris = info.autoReset ? triangles : Math.max(0, triangles - prevTrianglesRef.current);
2354
+ const framePoints = info.autoReset ? points : Math.max(0, points - prevPointsRef.current);
2355
+ const frameLines = info.autoReset ? lines : Math.max(0, lines - prevLinesRef.current);
2356
+ prevCallsRef.current = calls;
2357
+ prevTrianglesRef.current = triangles;
2358
+ prevPointsRef.current = points;
2359
+ prevLinesRef.current = lines;
2360
+ const phases = {
2361
+ hot: 0,
2362
+ warm: 0,
2363
+ cold: 0,
2364
+ waking: 0,
2365
+ dormant: 0
2366
+ };
2367
+ const world = engine.world;
2368
+ for (const entity of world.query(require_ecs.Widget, R3FRenderState)) {
2369
+ const widget = world.getComponent(entity, require_ecs.Widget);
2370
+ if (!widget || widget.surface !== "webgl") continue;
2371
+ const state = world.getComponent(entity, R3FRenderState);
2372
+ if (!state) continue;
2373
+ switch (state.phase) {
2374
+ case "Hot":
2375
+ phases.hot++;
2376
+ break;
2377
+ case "Warm":
2378
+ phases.warm++;
2379
+ break;
2380
+ case "Cold":
2381
+ phases.cold++;
2382
+ break;
2383
+ case "Waking":
2384
+ phases.waking++;
2385
+ break;
2386
+ case "Dormant":
2387
+ phases.dormant++;
2388
+ break;
2389
+ }
2390
+ }
2391
+ profiler.recordR3FFrame({
2392
+ dtMs,
2393
+ drawCalls: frameCalls,
2394
+ triangles: frameTris,
2395
+ points: framePoints,
2396
+ lines: frameLines,
2397
+ programs: info.programs?.length ?? 0,
2398
+ geometries: info.memory.geometries,
2399
+ textures: info.memory.textures,
2400
+ activeWidgets: widgetCount,
2401
+ widgetsRepainted: COMPOSITOR_TELEMETRY.widgetsRepainted,
2402
+ fboBytes: COMPOSITOR_TELEMETRY.fboBytes,
2403
+ phases
2404
+ });
2405
+ }, 2);
2406
+ return null;
2407
+ }
2408
+ //#endregion
2409
+ //#region src/r3f/R3FManager.tsx
2410
+ /**
2411
+ * Top-level coordinator for the R3F (React Three Fiber) rendering layer.
2412
+ *
2413
+ * Mounts a single `<Canvas>` and lets the {@link Compositor} drive the
2414
+ * render loop — each R3F widget paints into its own `WebGLRenderTarget`
2415
+ * via {@link VirtualWidget} and a final composition pass samples those
2416
+ * textures into the visible canvas (RFC-002 Phase 4).
2417
+ */
2418
+ function R3FManager({ engine, entities, resolve, r3fRoot, eventManagerRef }) {
2419
+ const canvasRef = (0, react.useRef)(null);
2420
+ const containerRef = useContainerRef();
2421
+ const initialCamera = (0, react.useMemo)(() => {
2422
+ const cam = new three.OrthographicCamera(0, 1, 0, -1, .1, 1e4);
2423
+ cam.position.set(0, 0, 1e3);
2424
+ return cam;
2425
+ }, []);
2426
+ const widgetRegistry = (0, react.useMemo)(() => new WidgetRegistry(), []);
2427
+ const eventManager = (0, react.useMemo)(() => createR3FEventManager(engine, widgetRegistry, (manager) => {
2428
+ if (eventManagerRef) eventManagerRef.current = manager;
2429
+ }), [
2430
+ engine,
2431
+ widgetRegistry,
2432
+ eventManagerRef
2433
+ ]);
2434
+ const widgetEntries = (0, react.useMemo)(() => {
2435
+ const result = [];
2436
+ for (const id of entities) {
2437
+ const resolved = resolve(id);
2438
+ if (resolved && resolved.surface === "webgl") result.push({
2439
+ entityId: id,
2440
+ component: resolved.component
2441
+ });
2442
+ }
2443
+ return result;
2444
+ }, [entities, resolve]);
2445
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_three_fiber.Canvas, {
2446
+ ref: canvasRef,
2447
+ camera: initialCamera,
2448
+ frameloop: "demand",
2449
+ events: eventManager,
2450
+ eventSource: containerRef ?? void 0,
2451
+ gl: {
2452
+ alpha: true,
2453
+ antialias: true
2454
+ },
2455
+ style: {
2456
+ position: "absolute",
2457
+ inset: 0,
2458
+ pointerEvents: "none",
2459
+ zIndex: 1,
2460
+ display: widgetEntries.length === 0 ? "none" : "block"
2461
+ },
2462
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_ecs.EngineProvider, {
2463
+ value: engine,
2464
+ children: [
2465
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EngineInvalidator, { engine }),
2466
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetStateMachine, { engine }),
2467
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProfilerProbe, {
2468
+ engine,
2469
+ widgetCount: widgetEntries.length
2470
+ }),
2471
+ r3fRoot,
2472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Compositor, {
2473
+ engine,
2474
+ widgetRegistry,
2475
+ children: widgetEntries.map(({ entityId, component }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VirtualWidget, {
2476
+ entityId,
2477
+ component
2478
+ }, entityId))
2479
+ })
2480
+ ]
2481
+ })
2482
+ });
2483
+ }
2484
+ //#endregion
2485
+ //#region src/webgl/renderers/GridRenderer.ts
2486
+ const DEFAULT_GRID_CONFIG = {
2487
+ spacings: [
2488
+ 20,
2489
+ 100,
2490
+ 500
2491
+ ],
2492
+ dotColor: [
2493
+ .75,
2494
+ .77,
2495
+ .8
2496
+ ],
2497
+ dotAlpha: 1,
2498
+ fadeIn: [8, 16],
2499
+ fadeOut: [120, 200],
2500
+ dotRadius: [.75, .75],
2501
+ levelWeight: [1, 0]
2502
+ };
2503
+ const vertexShader$2 = `
2504
+ void main() {
2505
+ gl_Position = vec4(position.xy, 0.0, 1.0);
2506
+ }
2507
+ `;
2508
+ const fragmentShader$2 = `
2509
+ precision highp float;
2510
+
2511
+ uniform vec2 u_resolution; // device pixels
2512
+ uniform vec2 u_camera; // world-space top-left
2513
+ uniform float u_zoom; // CSS zoom
2514
+ uniform float u_dpr; // device pixel ratio
2515
+ uniform vec3 u_spacings; // world-unit grid spacings
2516
+ uniform vec3 u_dotColor; // dot RGB
2517
+ uniform float u_dotAlpha; // dot base alpha
2518
+ uniform vec2 u_fadeIn; // CSS-px [start, end]
2519
+ uniform vec2 u_fadeOut; // CSS-px [start, end]
2520
+ uniform vec2 u_dotRadius; // CSS-px [min, max]
2521
+ uniform vec2 u_levelWeight; // [base, step]
2522
+
2523
+ void main() {
2524
+ vec2 devicePos = gl_FragCoord.xy;
2525
+ devicePos.y = u_resolution.y - devicePos.y;
2526
+
2527
+ float effectiveZoom = u_zoom * u_dpr;
2528
+ vec2 worldPos = devicePos / effectiveZoom + u_camera;
2529
+
2530
+ float totalAlpha = 0.0;
2531
+
2532
+ for (int i = 0; i < 3; i++) {
2533
+ float spacing;
2534
+ if (i == 0) spacing = u_spacings.x;
2535
+ else if (i == 1) spacing = u_spacings.y;
2536
+ else spacing = u_spacings.z;
2537
+
2538
+ // Screen spacing in CSS pixels (DPR-independent for consistent fading)
2539
+ float cssSpacing = spacing * u_zoom;
2540
+
2541
+ // Fade curve
2542
+ float opacity = 0.0;
2543
+ if (cssSpacing >= u_fadeIn.x && cssSpacing < u_fadeIn.y) {
2544
+ opacity = (cssSpacing - u_fadeIn.x) / (u_fadeIn.y - u_fadeIn.x);
2545
+ } else if (cssSpacing >= u_fadeIn.y && cssSpacing < u_fadeOut.x) {
2546
+ opacity = 1.0;
2547
+ } else if (cssSpacing >= u_fadeOut.x && cssSpacing < u_fadeOut.y) {
2548
+ opacity = 1.0 - (cssSpacing - u_fadeOut.x) / (u_fadeOut.y - u_fadeOut.x);
2549
+ }
2550
+ if (opacity <= 0.001) continue;
2551
+
2552
+ // Distance to nearest grid intersection in device pixels
2553
+ vec2 f = fract(worldPos / spacing + 0.5) - 0.5;
2554
+ float dist = length(f) * spacing * effectiveZoom;
2555
+
2556
+ // Dot radius in device pixels — optionally grows for sparser levels
2557
+ // (set u_dotRadius.x == u_dotRadius.y for Freeform/FigJam-style
2558
+ // constant-size dots)
2559
+ float t = clamp((cssSpacing - u_fadeIn.x) / 40.0, 0.0, 1.0);
2560
+ float radius = mix(u_dotRadius.x, u_dotRadius.y, t) * u_dpr;
2561
+
2562
+ // Anti-aliased dot (0.5 device pixel smoothstep)
2563
+ float dot = 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist);
2564
+
2565
+ // Per-level weight: base + i * step. Step=0 keeps all levels at equal
2566
+ // intensity; positive step emphasizes coarser levels (CAD feel).
2567
+ float weight = u_levelWeight.x + float(i) * u_levelWeight.y;
2568
+
2569
+ // Composite with max, not sum. Additive compositing causes anti-
2570
+ // aliased dot rims to stack at joint intersections (every N-th dot
2571
+ // visibly fatter — a CAD tell). max() guarantees a joint intersection
2572
+ // looks identical to a single-level dot, matching Freeform / FigJam.
2573
+ totalAlpha = max(totalAlpha, dot * opacity * weight);
2574
+ }
2575
+
2576
+ gl_FragColor = vec4(u_dotColor, clamp(totalAlpha * u_dotAlpha, 0.0, 1.0));
2577
+ }
2578
+ `;
2579
+ /**
2580
+ * Draws the infinite dot-grid background into a THREE.WebGLRenderer.
2581
+ * The renderer is owned by the parent (see {@link WebGLManager}) — this class
2582
+ * only contributes a scene, camera, and shader material.
2583
+ */
2584
+ var GridRenderer = class {
2585
+ scene;
2586
+ camera;
2587
+ material;
2588
+ mesh;
2589
+ constructor() {
2590
+ this.scene = new three$1.Scene();
2591
+ this.camera = new three$1.OrthographicCamera(-1, 1, 1, -1, 0, 1);
2592
+ this.material = new three$1.ShaderMaterial({
2593
+ vertexShader: vertexShader$2,
2594
+ fragmentShader: fragmentShader$2,
2595
+ uniforms: {
2596
+ u_resolution: { value: new three$1.Vector2(1, 1) },
2597
+ u_camera: { value: new three$1.Vector2(0, 0) },
2598
+ u_zoom: { value: 1 },
2599
+ u_dpr: { value: 1 },
2600
+ u_spacings: { value: new three$1.Vector3(...DEFAULT_GRID_CONFIG.spacings) },
2601
+ u_dotColor: { value: new three$1.Vector3(...DEFAULT_GRID_CONFIG.dotColor) },
2602
+ u_dotAlpha: { value: DEFAULT_GRID_CONFIG.dotAlpha },
2603
+ u_fadeIn: { value: new three$1.Vector2(...DEFAULT_GRID_CONFIG.fadeIn) },
2604
+ u_fadeOut: { value: new three$1.Vector2(...DEFAULT_GRID_CONFIG.fadeOut) },
2605
+ u_dotRadius: { value: new three$1.Vector2(...DEFAULT_GRID_CONFIG.dotRadius) },
2606
+ u_levelWeight: { value: new three$1.Vector2(...DEFAULT_GRID_CONFIG.levelWeight) }
2607
+ },
2608
+ transparent: true,
2609
+ depthTest: false,
2610
+ depthWrite: false
2611
+ });
2612
+ const geometry = new three$1.BufferGeometry();
2613
+ const vertices = new Float32Array([
2614
+ -1,
2615
+ -1,
2616
+ 0,
2617
+ 3,
2618
+ -1,
2619
+ 0,
2620
+ -1,
2621
+ 3,
2622
+ 0
2623
+ ]);
2624
+ geometry.setAttribute("position", new three$1.BufferAttribute(vertices, 3));
2625
+ this.mesh = new three$1.Mesh(geometry, this.material);
2626
+ this.scene.add(this.mesh);
2627
+ }
2628
+ /** Apply a (partial) grid config. Only provided fields are updated. */
2629
+ setConfig(config) {
2630
+ const u = this.material.uniforms;
2631
+ if (config.spacings) u.u_spacings.value.set(...config.spacings);
2632
+ if (config.dotColor) u.u_dotColor.value.set(...config.dotColor);
2633
+ if (config.dotAlpha !== void 0) u.u_dotAlpha.value = config.dotAlpha;
2634
+ if (config.fadeIn) u.u_fadeIn.value.set(...config.fadeIn);
2635
+ if (config.fadeOut) u.u_fadeOut.value.set(...config.fadeOut);
2636
+ if (config.dotRadius) u.u_dotRadius.value.set(...config.dotRadius);
2637
+ if (config.levelWeight) u.u_levelWeight.value.set(...config.levelWeight);
2638
+ }
2639
+ setSize(width, height, dpr = 1) {
2640
+ const u = this.material.uniforms;
2641
+ u.u_resolution.value.set(width * dpr, height * dpr);
2642
+ u.u_dpr.value = dpr;
2643
+ }
2644
+ render(renderer, cameraX, cameraY, zoom) {
2645
+ const u = this.material.uniforms;
2646
+ u.u_camera.value.set(cameraX, cameraY);
2647
+ u.u_zoom.value = zoom;
2648
+ renderer.render(this.scene, this.camera);
2649
+ }
2650
+ dispose() {
2651
+ this.mesh.geometry.dispose();
2652
+ this.material.dispose();
2653
+ }
2654
+ };
2655
+ //#endregion
2656
+ //#region src/webgl/renderers/SelectionRenderer.ts
2657
+ const DEFAULT_SELECTION_CONFIG = {
2658
+ outlineColor: [
2659
+ .051,
2660
+ .6,
2661
+ 1
2662
+ ],
2663
+ outlineWidth: 1.5,
2664
+ hoverColor: [
2665
+ .051,
2666
+ .6,
2667
+ 1
2668
+ ],
2669
+ hoverWidth: 1,
2670
+ handleSize: 8,
2671
+ handleFill: [
2672
+ 1,
2673
+ 1,
2674
+ 1
2675
+ ],
2676
+ handleBorder: [
2677
+ .051,
2678
+ .6,
2679
+ 1
2680
+ ],
2681
+ handleBorderWidth: 1.5,
2682
+ groupDash: 4
2683
+ };
2684
+ const MAX_ENTITIES = 32;
2685
+ const vertexShader$1 = `
2686
+ void main() {
2687
+ gl_Position = vec4(position.xy, 0.0, 1.0);
2688
+ }
2689
+ `;
2690
+ const fragmentShader$1 = `
2691
+ precision highp float;
2692
+
2693
+ uniform vec2 u_resolution;
2694
+ uniform vec2 u_camera;
2695
+ uniform float u_zoom;
2696
+ uniform float u_dpr;
2697
+
2698
+ // Selection data
2699
+ uniform int u_count;
2700
+ uniform vec4 u_bounds[${MAX_ENTITIES}]; // (x, y, width, height) — frame-local Transform2D
2701
+ uniform int u_hoverIdx; // -1 = none
2702
+ uniform vec4 u_groupBounds; // group bbox (0 if count <= 1)
2703
+ uniform int u_hasGroup;
2704
+
2705
+ // Style
2706
+ uniform vec3 u_outlineColor;
2707
+ uniform float u_outlineWidth;
2708
+ uniform vec3 u_hoverColor;
2709
+ uniform float u_hoverWidth;
2710
+ uniform float u_handleSize;
2711
+ uniform vec3 u_handleFill;
2712
+ uniform vec3 u_handleBorder;
2713
+ uniform float u_handleBorderWidth;
2714
+ uniform float u_groupDash;
2715
+
2716
+ // SDF for axis-aligned rectangle outline (returns distance to edge)
2717
+ float sdRectOutline(vec2 p, vec2 center, vec2 halfSize) {
2718
+ vec2 d = abs(p - center) - halfSize;
2719
+ float outside = length(max(d, 0.0));
2720
+ float inside = min(max(d.x, d.y), 0.0);
2721
+ return abs(outside + inside);
2722
+ }
2723
+
2724
+ // SDF for filled square
2725
+ float sdSquare(vec2 p, vec2 center, float halfSize) {
2726
+ vec2 d = abs(p - center) - vec2(halfSize);
2727
+ return max(d.x, d.y);
2728
+ }
2729
+
2730
+ void main() {
2731
+ if (u_count == 0 && u_hoverIdx < 0) discard;
2732
+
2733
+ vec2 devicePos = gl_FragCoord.xy;
2734
+ devicePos.y = u_resolution.y - devicePos.y;
2735
+
2736
+ float effectiveZoom = u_zoom * u_dpr;
2737
+ vec2 worldPos = devicePos / effectiveZoom + u_camera;
2738
+
2739
+ // Screen-space conversion factor
2740
+ float pxToWorld = 1.0 / effectiveZoom;
2741
+
2742
+ vec4 color = vec4(0.0);
2743
+
2744
+ // --- Hover outline ---
2745
+ if (u_hoverIdx >= 0 && u_hoverIdx < ${MAX_ENTITIES}) {
2746
+ vec4 b = u_bounds[u_hoverIdx];
2747
+ vec2 center = vec2(b.x + b.z * 0.5, b.y + b.w * 0.5);
2748
+ vec2 halfSize = vec2(b.z, b.w) * 0.5;
2749
+ float dist = sdRectOutline(worldPos, center, halfSize);
2750
+ float width = u_hoverWidth * pxToWorld;
2751
+ float alpha = 1.0 - smoothstep(width - pxToWorld * 0.5, width + pxToWorld * 0.5, dist);
2752
+ color = max(color, vec4(u_hoverColor, alpha * 0.6));
2753
+ }
2754
+
2755
+ // --- Selection outlines ---
2756
+ for (int i = 0; i < ${MAX_ENTITIES}; i++) {
2757
+ if (i >= u_count) break;
2758
+ vec4 b = u_bounds[i];
2759
+ vec2 center = vec2(b.x + b.z * 0.5, b.y + b.w * 0.5);
2760
+ vec2 halfSize = vec2(b.z, b.w) * 0.5;
2761
+
2762
+ // Outline
2763
+ float dist = sdRectOutline(worldPos, center, halfSize);
2764
+ float width = u_outlineWidth * pxToWorld;
2765
+ float outlineAlpha = 1.0 - smoothstep(width - pxToWorld * 0.5, width + pxToWorld * 0.5, dist);
2766
+ color = max(color, vec4(u_outlineColor, outlineAlpha));
2767
+
2768
+ // 8 resize handles
2769
+ float hs = u_handleSize * 0.5 * pxToWorld;
2770
+ float bw = u_handleBorderWidth * pxToWorld;
2771
+ vec2 corners[8];
2772
+ corners[0] = vec2(b.x, b.y); // nw
2773
+ corners[1] = vec2(b.x + b.z * 0.5, b.y); // n
2774
+ corners[2] = vec2(b.x + b.z, b.y); // ne
2775
+ corners[3] = vec2(b.x + b.z, b.y + b.w * 0.5); // e
2776
+ corners[4] = vec2(b.x + b.z, b.y + b.w); // se
2777
+ corners[5] = vec2(b.x + b.z * 0.5, b.y + b.w); // s
2778
+ corners[6] = vec2(b.x, b.y + b.w); // sw
2779
+ corners[7] = vec2(b.x, b.y + b.w * 0.5); // w
2780
+
2781
+ for (int h = 0; h < 8; h++) {
2782
+ float d = sdSquare(worldPos, corners[h], hs);
2783
+ // Fill (white)
2784
+ float fillAlpha = 1.0 - smoothstep(-pxToWorld * 0.5, pxToWorld * 0.5, d);
2785
+ // Border
2786
+ float borderDist = abs(d + bw * 0.5) - bw * 0.5;
2787
+ float borderAlpha = 1.0 - smoothstep(-pxToWorld * 0.5, pxToWorld * 0.5, borderDist);
2788
+
2789
+ if (fillAlpha > 0.01) {
2790
+ // Composite: border color on top of fill
2791
+ vec3 handleColor = mix(u_handleFill, u_handleBorder, borderAlpha);
2792
+ color = vec4(handleColor, max(fillAlpha, color.a));
2793
+ }
2794
+ }
2795
+ }
2796
+
2797
+ // --- Group bounding box (dashed) ---
2798
+ if (u_hasGroup == 1 && u_count > 1) {
2799
+ vec4 gb = u_groupBounds;
2800
+ vec2 center = vec2(gb.x + gb.z * 0.5, gb.y + gb.w * 0.5);
2801
+ vec2 halfSize = vec2(gb.z, gb.w) * 0.5;
2802
+ float dist = sdRectOutline(worldPos, center, halfSize);
2803
+ float width = u_outlineWidth * 0.75 * pxToWorld;
2804
+ float lineAlpha = 1.0 - smoothstep(width - pxToWorld * 0.5, width + pxToWorld * 0.5, dist);
2805
+
2806
+ // Dash pattern along the rectangle perimeter
2807
+ if (u_groupDash > 0.0 && lineAlpha > 0.01) {
2808
+ // Proper perimeter arc-length from top-left corner going clockwise
2809
+ float perim;
2810
+ vec2 rel = worldPos - vec2(gb.x, gb.y);
2811
+ float w = gb.z;
2812
+ float h = gb.w;
2813
+
2814
+ // Determine which edge is nearest and compute cumulative arc length
2815
+ float dTop = abs(rel.y);
2816
+ float dRight = abs(rel.x - w);
2817
+ float dBottom = abs(rel.y - h);
2818
+ float dLeft = abs(rel.x);
2819
+
2820
+ if (dTop <= dBottom && dTop <= dLeft && dTop <= dRight) {
2821
+ perim = rel.x; // top edge: 0 to w
2822
+ } else if (dRight <= dLeft) {
2823
+ perim = w + rel.y; // right edge: w to w+h
2824
+ } else if (dBottom <= dTop) {
2825
+ perim = w + h + (w - rel.x); // bottom edge: w+h to 2w+h
2826
+ } else {
2827
+ perim = 2.0 * w + h + (h - rel.y); // left edge: 2w+h to 2w+2h
2828
+ }
2829
+
2830
+ float dashWorld = u_groupDash * pxToWorld;
2831
+ float dashPattern = step(0.5, fract(perim / (dashWorld * 2.0)));
2832
+ lineAlpha *= dashPattern;
2833
+ }
2834
+
2835
+ color = max(color, vec4(u_outlineColor, lineAlpha * 0.5));
2836
+ }
2837
+
2838
+ if (color.a < 0.01) discard;
2839
+ gl_FragColor = color;
2840
+ }
2841
+ `;
2842
+ /**
2843
+ * Draws selection outlines, 8 resize handles, group-bbox, and the hover
2844
+ * outline in a single SDF-based shader pass. Snap guides and equal-
2845
+ * spacing indicators are a separate concern — see {@link SnapGuideRenderer}.
2846
+ *
2847
+ * The `THREE.WebGLRenderer` is owned by the parent ({@link WebGLManager})
2848
+ * and passed to each render call so grid + selection can share a single
2849
+ * GL context and accumulate `renderer.info` counters for the same tick.
2850
+ */
2851
+ var SelectionRenderer = class {
2852
+ material;
2853
+ mesh;
2854
+ scene;
2855
+ camera;
2856
+ constructor() {
2857
+ this.scene = new three$1.Scene();
2858
+ this.camera = new three$1.OrthographicCamera(-1, 1, 1, -1, 0, 1);
2859
+ const boundsDefault = [];
2860
+ for (let i = 0; i < MAX_ENTITIES; i++) boundsDefault.push(new three$1.Vector4(0, 0, 0, 0));
2861
+ this.material = new three$1.ShaderMaterial({
2862
+ vertexShader: vertexShader$1,
2863
+ fragmentShader: fragmentShader$1,
2864
+ uniforms: {
2865
+ u_resolution: { value: new three$1.Vector2(1, 1) },
2866
+ u_camera: { value: new three$1.Vector2(0, 0) },
2867
+ u_zoom: { value: 1 },
2868
+ u_dpr: { value: 1 },
2869
+ u_count: { value: 0 },
2870
+ u_bounds: { value: boundsDefault },
2871
+ u_hoverIdx: { value: -1 },
2872
+ u_groupBounds: { value: new three$1.Vector4(0, 0, 0, 0) },
2873
+ u_hasGroup: { value: 0 },
2874
+ u_outlineColor: { value: new three$1.Vector3(...DEFAULT_SELECTION_CONFIG.outlineColor) },
2875
+ u_outlineWidth: { value: DEFAULT_SELECTION_CONFIG.outlineWidth },
2876
+ u_hoverColor: { value: new three$1.Vector3(...DEFAULT_SELECTION_CONFIG.hoverColor) },
2877
+ u_hoverWidth: { value: DEFAULT_SELECTION_CONFIG.hoverWidth },
2878
+ u_handleSize: { value: DEFAULT_SELECTION_CONFIG.handleSize },
2879
+ u_handleFill: { value: new three$1.Vector3(...DEFAULT_SELECTION_CONFIG.handleFill) },
2880
+ u_handleBorder: { value: new three$1.Vector3(...DEFAULT_SELECTION_CONFIG.handleBorder) },
2881
+ u_handleBorderWidth: { value: DEFAULT_SELECTION_CONFIG.handleBorderWidth },
2882
+ u_groupDash: { value: DEFAULT_SELECTION_CONFIG.groupDash }
2883
+ },
2884
+ transparent: true,
2885
+ depthTest: false,
2886
+ depthWrite: false
2887
+ });
2888
+ const geometry = new three$1.BufferGeometry();
2889
+ const vertices = new Float32Array([
2890
+ -1,
2891
+ -1,
2892
+ 0,
2893
+ 3,
2894
+ -1,
2895
+ 0,
2896
+ -1,
2897
+ 3,
2898
+ 0
2899
+ ]);
2900
+ geometry.setAttribute("position", new three$1.BufferAttribute(vertices, 3));
2901
+ this.mesh = new three$1.Mesh(geometry, this.material);
2902
+ this.scene.add(this.mesh);
2903
+ }
2904
+ setConfig(config) {
2905
+ const u = this.material.uniforms;
2906
+ if (config.outlineColor) u.u_outlineColor.value.set(...config.outlineColor);
2907
+ if (config.outlineWidth !== void 0) u.u_outlineWidth.value = config.outlineWidth;
2908
+ if (config.hoverColor) u.u_hoverColor.value.set(...config.hoverColor);
2909
+ if (config.hoverWidth !== void 0) u.u_hoverWidth.value = config.hoverWidth;
2910
+ if (config.handleSize !== void 0) u.u_handleSize.value = config.handleSize;
2911
+ if (config.handleFill) u.u_handleFill.value.set(...config.handleFill);
2912
+ if (config.handleBorder) u.u_handleBorder.value.set(...config.handleBorder);
2913
+ if (config.handleBorderWidth !== void 0) u.u_handleBorderWidth.value = config.handleBorderWidth;
2914
+ if (config.groupDash !== void 0) u.u_groupDash.value = config.groupDash;
2915
+ }
2916
+ setSize(resolution, dpr) {
2917
+ this.material.uniforms.u_resolution.value.copy(resolution);
2918
+ this.material.uniforms.u_dpr.value = dpr;
2919
+ }
2920
+ render(renderer, cameraX, cameraY, zoom, selected, hovered) {
2921
+ const u = this.material.uniforms;
2922
+ u.u_camera.value.set(cameraX, cameraY);
2923
+ u.u_zoom.value = zoom;
2924
+ const count = Math.min(selected.length, MAX_ENTITIES);
2925
+ u.u_count.value = count;
2926
+ for (let i = 0; i < count; i++) {
2927
+ const b = selected[i];
2928
+ u.u_bounds.value[i].set(b.x, b.y, b.width, b.height);
2929
+ }
2930
+ if (hovered && count < MAX_ENTITIES) {
2931
+ let hoverIdx = -1;
2932
+ for (let i = 0; i < count; i++) {
2933
+ const b = selected[i];
2934
+ if (b.x === hovered.x && b.y === hovered.y) {
2935
+ hoverIdx = i;
2936
+ break;
2937
+ }
2938
+ }
2939
+ if (hoverIdx < 0) {
2940
+ u.u_bounds.value[count].set(hovered.x, hovered.y, hovered.width, hovered.height);
2941
+ u.u_hoverIdx.value = count;
2942
+ } else u.u_hoverIdx.value = -1;
2943
+ } else u.u_hoverIdx.value = -1;
2944
+ if (count > 1) {
2945
+ let minX = Number.POSITIVE_INFINITY;
2946
+ let minY = Number.POSITIVE_INFINITY;
2947
+ let maxX = Number.NEGATIVE_INFINITY;
2948
+ let maxY = Number.NEGATIVE_INFINITY;
2949
+ for (let i = 0; i < count; i++) {
2950
+ const b = selected[i];
2951
+ minX = Math.min(minX, b.x);
2952
+ minY = Math.min(minY, b.y);
2953
+ maxX = Math.max(maxX, b.x + b.width);
2954
+ maxY = Math.max(maxY, b.y + b.height);
2955
+ }
2956
+ u.u_groupBounds.value.set(minX, minY, maxX - minX, maxY - minY);
2957
+ u.u_hasGroup.value = 1;
2958
+ } else u.u_hasGroup.value = 0;
2959
+ const prevAutoClear = renderer.autoClear;
2960
+ renderer.autoClear = false;
2961
+ renderer.render(this.scene, this.camera);
2962
+ renderer.autoClear = prevAutoClear;
2963
+ }
2964
+ dispose() {
2965
+ this.mesh.geometry.dispose();
2966
+ this.material.dispose();
2967
+ }
2968
+ };
2969
+ //#endregion
2970
+ //#region src/webgl/renderers/SnapGuideRenderer.ts
2971
+ const DEFAULT_SNAP_GUIDE_CONFIG = {
2972
+ color: [
2973
+ 1,
2974
+ 0,
2975
+ .55
2976
+ ],
2977
+ lineWidth: .5,
2978
+ guideAlpha: .8,
2979
+ spacingAlpha: .7
2980
+ };
2981
+ const MAX_GUIDES = 16;
2982
+ const MAX_SPACINGS = 8;
2983
+ const vertexShader = `
2984
+ void main() {
2985
+ gl_Position = vec4(position.xy, 0.0, 1.0);
2986
+ }
2987
+ `;
2988
+ const fragmentShader = `
2989
+ precision highp float;
2990
+
2991
+ uniform vec2 u_resolution;
2992
+ uniform vec2 u_camera;
2993
+ uniform float u_zoom;
2994
+ uniform float u_dpr;
2995
+
2996
+ uniform int u_guideCount;
2997
+ uniform vec4 u_guides[${MAX_GUIDES}]; // (axis: 0=x/1=y, position, 0, 0)
2998
+ uniform int u_spacingCount;
2999
+ uniform vec4 u_spacings[${MAX_SPACINGS}]; // (axis, from, to, perpPos)
3000
+
3001
+ uniform vec3 u_color;
3002
+ uniform float u_lineWidth;
3003
+ uniform float u_guideAlpha;
3004
+ uniform float u_spacingAlpha;
3005
+
3006
+ void main() {
3007
+ if (u_guideCount == 0 && u_spacingCount == 0) discard;
3008
+
3009
+ vec2 devicePos = gl_FragCoord.xy;
3010
+ devicePos.y = u_resolution.y - devicePos.y;
3011
+
3012
+ float effectiveZoom = u_zoom * u_dpr;
3013
+ vec2 worldPos = devicePos / effectiveZoom + u_camera;
3014
+ float pxToWorld = 1.0 / effectiveZoom;
3015
+ float lineHalf = u_lineWidth * pxToWorld;
3016
+
3017
+ vec4 color = vec4(0.0);
3018
+
3019
+ // --- Snap guide lines (full-canvas axis alignment) ---
3020
+ for (int i = 0; i < ${MAX_GUIDES}; i++) {
3021
+ if (i >= u_guideCount) break;
3022
+ vec4 g = u_guides[i];
3023
+ float dist = g.x < 0.5 ? abs(worldPos.x - g.y) : abs(worldPos.y - g.y);
3024
+ float a = 1.0 - smoothstep(lineHalf - pxToWorld * 0.3, lineHalf + pxToWorld * 0.3, dist);
3025
+ color = max(color, vec4(u_color, a * u_guideAlpha));
3026
+ }
3027
+
3028
+ // --- Equal spacing indicators (segment + end-bars) ---
3029
+ for (int i = 0; i < ${MAX_SPACINGS}; i++) {
3030
+ if (i >= u_spacingCount) break;
3031
+ vec4 s = u_spacings[i];
3032
+ float segAlpha = 0.0;
3033
+ if (s.x < 0.5) {
3034
+ // Horizontal gap segment
3035
+ float yDist = abs(worldPos.y - s.w);
3036
+ float xInRange = step(s.y, worldPos.x) * step(worldPos.x, s.z);
3037
+ segAlpha = (1.0 - smoothstep(lineHalf, lineHalf + pxToWorld, yDist)) * xInRange;
3038
+ float barHeight = 4.0 * pxToWorld;
3039
+ float barFrom = (1.0 - smoothstep(lineHalf, lineHalf + pxToWorld, abs(worldPos.x - s.y)))
3040
+ * (1.0 - smoothstep(barHeight, barHeight + pxToWorld, abs(worldPos.y - s.w)));
3041
+ float barTo = (1.0 - smoothstep(lineHalf, lineHalf + pxToWorld, abs(worldPos.x - s.z)))
3042
+ * (1.0 - smoothstep(barHeight, barHeight + pxToWorld, abs(worldPos.y - s.w)));
3043
+ segAlpha = max(segAlpha, max(barFrom, barTo));
3044
+ } else {
3045
+ // Vertical gap segment
3046
+ float xDist = abs(worldPos.x - s.w);
3047
+ float yInRange = step(s.y, worldPos.y) * step(worldPos.y, s.z);
3048
+ segAlpha = (1.0 - smoothstep(lineHalf, lineHalf + pxToWorld, xDist)) * yInRange;
3049
+ float barWidth = 4.0 * pxToWorld;
3050
+ float barFrom = (1.0 - smoothstep(lineHalf, lineHalf + pxToWorld, abs(worldPos.y - s.y)))
3051
+ * (1.0 - smoothstep(barWidth, barWidth + pxToWorld, abs(worldPos.x - s.w)));
3052
+ float barTo = (1.0 - smoothstep(lineHalf, lineHalf + pxToWorld, abs(worldPos.y - s.z)))
3053
+ * (1.0 - smoothstep(barWidth, barWidth + pxToWorld, abs(worldPos.x - s.w)));
3054
+ segAlpha = max(segAlpha, max(barFrom, barTo));
3055
+ }
3056
+ color = max(color, vec4(u_color, segAlpha * u_spacingAlpha));
3057
+ }
3058
+
3059
+ if (color.a < 0.01) discard;
3060
+ gl_FragColor = color;
3061
+ }
3062
+ `;
3063
+ /**
3064
+ * Renders alignment guide lines and equal-spacing indicators in a single
3065
+ * SDF pass. Independent of {@link SelectionRenderer} — guides participate
3066
+ * regardless of whether anything is selected, and the dragged entity may
3067
+ * have its own chrome that opts out of the engine-drawn selection frame.
3068
+ */
3069
+ var SnapGuideRenderer = class {
3070
+ material;
3071
+ mesh;
3072
+ scene;
3073
+ camera;
3074
+ constructor() {
3075
+ this.scene = new three$1.Scene();
3076
+ this.camera = new three$1.OrthographicCamera(-1, 1, 1, -1, 0, 1);
3077
+ this.material = new three$1.ShaderMaterial({
3078
+ vertexShader,
3079
+ fragmentShader,
3080
+ uniforms: {
3081
+ u_resolution: { value: new three$1.Vector2(1, 1) },
3082
+ u_camera: { value: new three$1.Vector2(0, 0) },
3083
+ u_zoom: { value: 1 },
3084
+ u_dpr: { value: 1 },
3085
+ u_guideCount: { value: 0 },
3086
+ u_guides: { value: Array.from({ length: MAX_GUIDES }, () => new three$1.Vector4(0, 0, 0, 0)) },
3087
+ u_spacingCount: { value: 0 },
3088
+ u_spacings: { value: Array.from({ length: MAX_SPACINGS }, () => new three$1.Vector4(0, 0, 0, 0)) },
3089
+ u_color: { value: new three$1.Vector3(...DEFAULT_SNAP_GUIDE_CONFIG.color) },
3090
+ u_lineWidth: { value: DEFAULT_SNAP_GUIDE_CONFIG.lineWidth },
3091
+ u_guideAlpha: { value: DEFAULT_SNAP_GUIDE_CONFIG.guideAlpha },
3092
+ u_spacingAlpha: { value: DEFAULT_SNAP_GUIDE_CONFIG.spacingAlpha }
3093
+ },
3094
+ transparent: true,
3095
+ depthTest: false,
3096
+ depthWrite: false
3097
+ });
3098
+ const geometry = new three$1.BufferGeometry();
3099
+ const vertices = new Float32Array([
3100
+ -1,
3101
+ -1,
3102
+ 0,
3103
+ 3,
3104
+ -1,
3105
+ 0,
3106
+ -1,
3107
+ 3,
3108
+ 0
3109
+ ]);
3110
+ geometry.setAttribute("position", new three$1.BufferAttribute(vertices, 3));
3111
+ this.mesh = new three$1.Mesh(geometry, this.material);
3112
+ this.scene.add(this.mesh);
3113
+ }
3114
+ setConfig(config) {
3115
+ const u = this.material.uniforms;
3116
+ if (config.color) u.u_color.value.set(...config.color);
3117
+ if (config.lineWidth !== void 0) u.u_lineWidth.value = config.lineWidth;
3118
+ if (config.guideAlpha !== void 0) u.u_guideAlpha.value = config.guideAlpha;
3119
+ if (config.spacingAlpha !== void 0) u.u_spacingAlpha.value = config.spacingAlpha;
3120
+ }
3121
+ setSize(resolution, dpr) {
3122
+ this.material.uniforms.u_resolution.value.copy(resolution);
3123
+ this.material.uniforms.u_dpr.value = dpr;
3124
+ }
3125
+ render(renderer, cameraX, cameraY, zoom, guides, spacings) {
3126
+ const u = this.material.uniforms;
3127
+ u.u_camera.value.set(cameraX, cameraY);
3128
+ u.u_zoom.value = zoom;
3129
+ const gCount = Math.min(guides.length, MAX_GUIDES);
3130
+ u.u_guideCount.value = gCount;
3131
+ for (let i = 0; i < gCount; i++) {
3132
+ const g = guides[i];
3133
+ u.u_guides.value[i].set(g.axis === "x" ? 0 : 1, g.position, 0, 0);
3134
+ }
3135
+ let sIdx = 0;
3136
+ for (const sp of spacings) for (const seg of sp.segments) {
3137
+ if (sIdx >= MAX_SPACINGS) break;
3138
+ u.u_spacings.value[sIdx].set(sp.axis === "x" ? 0 : 1, seg.from, seg.to, sp.perpPosition);
3139
+ sIdx++;
3140
+ }
3141
+ u.u_spacingCount.value = sIdx;
3142
+ const prevAutoClear = renderer.autoClear;
3143
+ renderer.autoClear = false;
3144
+ renderer.render(this.scene, this.camera);
3145
+ renderer.autoClear = prevAutoClear;
3146
+ }
3147
+ dispose() {
3148
+ this.mesh.geometry.dispose();
3149
+ this.material.dispose();
3150
+ }
3151
+ };
3152
+ //#endregion
3153
+ //#region src/webgl/WebGLManager.ts
3154
+ /**
3155
+ * Top-level coordinator for the library's vanilla-WebGL layer.
3156
+ *
3157
+ * Owns a single `THREE.WebGLRenderer` and drives the built-in renderers
3158
+ * (dot grid, selection overlay, snap guides) through it. This replaces
3159
+ * the previous pattern where `GridRenderer` owned the renderer and
3160
+ * `SelectionRenderer` piggy-backed on it via an implicit side-effect —
3161
+ * a manager makes the sharing explicit and gives {@link InfiniteCanvas}
3162
+ * a single surface to talk to instead of three.
3163
+ */
3164
+ var WebGLManager = class {
3165
+ renderer;
3166
+ grid = null;
3167
+ selection;
3168
+ snapGuides;
3169
+ constructor(canvas, opts = {}) {
3170
+ this.renderer = new three$1.WebGLRenderer({
3171
+ canvas,
3172
+ alpha: true,
3173
+ antialias: false,
3174
+ premultipliedAlpha: false
3175
+ });
3176
+ this.renderer.setClearColor(0, 0);
3177
+ this.renderer.info.autoReset = false;
3178
+ if (opts.grid !== false) {
3179
+ this.grid = new GridRenderer();
3180
+ if (opts.grid) this.grid.setConfig(opts.grid);
3181
+ }
3182
+ this.selection = new SelectionRenderer();
3183
+ if (opts.selection) this.selection.setConfig(opts.selection);
3184
+ this.snapGuides = new SnapGuideRenderer();
3185
+ if (opts.snapGuides) this.snapGuides.setConfig(opts.snapGuides);
3186
+ }
3187
+ /** Resize the drawing buffer. Call on mount and ResizeObserver events. */
3188
+ setSize(width, height, dpr = 1) {
3189
+ this.renderer.setSize(width, height, false);
3190
+ this.renderer.setPixelRatio(dpr);
3191
+ const resolution = new three$1.Vector2(width * dpr, height * dpr);
3192
+ this.grid?.setSize(width, height, dpr);
3193
+ this.selection.setSize(resolution, dpr);
3194
+ this.snapGuides.setSize(resolution, dpr);
3195
+ }
3196
+ /** Update grid visuals (colors, spacings, fade ranges, etc.). */
3197
+ setGridConfig(config) {
3198
+ this.grid?.setConfig(config);
3199
+ }
3200
+ /** Update selection overlay visuals (outline color, handle size, etc.). */
3201
+ setSelectionConfig(config) {
3202
+ this.selection.setConfig(config);
3203
+ }
3204
+ /** Update snap-guide visuals (color, line width, alpha). */
3205
+ setSnapGuideConfig(config) {
3206
+ this.snapGuides.setConfig(config);
3207
+ }
3208
+ /** Render one frame: grid → selection → snap guides, in stacking order. */
3209
+ render(input) {
3210
+ const { camera, selection, snap, profiler } = input;
3211
+ this.renderer.info.reset();
3212
+ if (this.grid) {
3213
+ profiler?.beginWebGL("grid");
3214
+ this.grid.render(this.renderer, camera.x, camera.y, camera.zoom);
3215
+ profiler?.endWebGL("grid");
3216
+ }
3217
+ profiler?.beginWebGL("selection");
3218
+ this.selection.render(this.renderer, camera.x, camera.y, camera.zoom, selection.bounds, selection.hovered);
3219
+ profiler?.endWebGL("selection");
3220
+ if (snap.visible) {
3221
+ profiler?.beginWebGL("snap-guides");
3222
+ this.snapGuides.render(this.renderer, camera.x, camera.y, camera.zoom, snap.guides, snap.spacings);
3223
+ profiler?.endWebGL("snap-guides");
3224
+ }
3225
+ if (profiler?.isEnabled()) {
3226
+ const info = this.renderer.info;
3227
+ profiler.recordWebGLStats({
3228
+ drawCalls: info.render.calls,
3229
+ triangles: info.render.triangles,
3230
+ selectionFrames: selection.bounds.length + (selection.hovered ? 1 : 0),
3231
+ snapGuides: snap.guides.length,
3232
+ spacingIndicators: snap.spacings.length,
3233
+ domPositionsUpdated: input.domPositionsUpdated ?? 0
3234
+ });
3235
+ }
3236
+ }
3237
+ /** Release GL resources — call on unmount. */
3238
+ dispose() {
3239
+ this.grid?.dispose();
3240
+ this.selection.dispose();
3241
+ this.snapGuides.dispose();
3242
+ this.renderer.dispose();
3243
+ }
3244
+ /** Escape hatch if an advanced consumer needs the underlying three renderer. */
3245
+ getWebGLRenderer() {
3246
+ return this.renderer;
3247
+ }
3248
+ };
3249
+ //#endregion
3250
+ //#region src/react/widgets/CardChrome.tsx
3251
+ /**
3252
+ * iOS-style card chrome — rounded background, hairline ring, soft drop
3253
+ * shadow, and a smooth lift (scale + stronger shadow) when `lifted` is
3254
+ * true. A single soft radial-gradient glow renders at the hot point
3255
+ * during overlap (no rim, no bloom, no backdrop-filter). Glow color,
3256
+ * alpha, and falloff are tunable via the `--ic-glow-*` CSS vars set by
3257
+ * `<InfiniteCanvas overlapGlow={…}>`.
3258
+ *
3259
+ * Pure presentational component with no ECS or compositor coupling. Used
3260
+ * by both the DOM `createCardWidget` (wrapping inner content) and the R3F
3261
+ * `createGeometryCardWidget` (rendered via a DOM slot beneath the WebGL
3262
+ * canvas, with the 3D content floating on top).
3263
+ */
3264
+ function CardChrome({ lifted = false, radius = 21.67, background, className, style, overlapCandidate = false, overlapTarget = false, hotX = .5, hotY = .5, hotStrength = 0, children }) {
3265
+ const baseShadow = lifted ? "0 30px 60px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.06)" : "0 20px 40px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.05)";
3266
+ const baseStyle = {
3267
+ position: "relative",
3268
+ width: "100%",
3269
+ height: "100%",
3270
+ borderRadius: `${radius}px`,
3271
+ overflow: "hidden",
3272
+ background,
3273
+ boxShadow: baseShadow,
3274
+ transform: lifted ? "scale(1.05)" : "scale(1)",
3275
+ transformOrigin: "center center",
3276
+ transition: "transform 180ms cubic-bezier(0.2, 0.9, 0.3, 1.2), box-shadow 220ms ease",
3277
+ willChange: lifted ? "transform, box-shadow" : void 0,
3278
+ ...style
3279
+ };
3280
+ const hotXPct = hotX * 100;
3281
+ const hotYPct = hotY * 100;
3282
+ const glowStyle = {
3283
+ position: "absolute",
3284
+ inset: 0,
3285
+ pointerEvents: "none",
3286
+ borderRadius: "inherit",
3287
+ boxShadow: `inset ${-(hotX - .5) * 16}px ${-(hotY - .5) * 16}px ${overlapTarget ? "var(--ic-glow-size-t, 80px)" : "var(--ic-glow-size-c, 60px)"} rgba(var(--ic-glow-color, 128, 128, 128), ${overlapTarget ? "var(--ic-glow-alpha-t, 0.45)" : "var(--ic-glow-alpha-c, 0.25)"})`,
3288
+ opacity: overlapCandidate ? hotStrength : 0,
3289
+ transition: "opacity 220ms ease, box-shadow 220ms ease"
3290
+ };
3291
+ const rimStyle = {
3292
+ position: "absolute",
3293
+ inset: 0,
3294
+ pointerEvents: "none",
3295
+ borderRadius: "inherit",
3296
+ padding: "var(--ic-rim-width, 1.5px)",
3297
+ background: `radial-gradient(var(--ic-rim-radius, 600px) circle at ${hotXPct}% ${hotYPct}%, ${`rgba(var(--ic-rim-color, 128, 128, 128), ${overlapTarget ? "var(--ic-rim-alpha-t, 0.85)" : "var(--ic-rim-alpha-c, 0.55)"})`}, transparent 40%)`,
3298
+ WebkitMask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
3299
+ WebkitMaskComposite: "xor",
3300
+ mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
3301
+ maskComposite: "exclude",
3302
+ opacity: overlapCandidate ? hotStrength : 0,
3303
+ transition: "opacity 220ms ease, background 220ms ease"
3304
+ };
3305
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3306
+ className,
3307
+ style: baseStyle,
3308
+ "data-overlap-candidate": overlapCandidate || void 0,
3309
+ "data-overlap-target": overlapTarget || void 0,
3310
+ children: [
3311
+ children,
3312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3313
+ "aria-hidden": true,
3314
+ style: glowStyle
3315
+ }),
3316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3317
+ "aria-hidden": true,
3318
+ style: rimStyle
3319
+ })
3320
+ ]
3321
+ });
3322
+ }
3323
+ //#endregion
3324
+ //#region src/react/overlays/SelectionOverlaySlot.tsx
3325
+ /**
3326
+ * DOM chrome overlay for R3F widgets — renders the selection frame /
3327
+ * card decoration and positions itself at the widget's world AABB.
3328
+ *
3329
+ * Decoration only. Pointer events bypass this wrapper
3330
+ * (`pointer-events: none`) so they reach the R3F canvas underneath,
3331
+ * where the `EventRouter` raycasts the widget's local scene (RFC-006).
3332
+ * Engine semantics — drag, select, resize, double-click — are dispatched
3333
+ * by the canvas-level `PointerEventBus` after the widget's R3F handlers
3334
+ * have had a chance to call `event.stopPropagation()`.
3335
+ */
3336
+ const SelectionOverlaySlot = (0, react.memo)(function SelectionOverlaySlot({ entityId, slotRef }) {
3337
+ const wrapperRef = (0, react.useRef)(null);
3338
+ const engine = require_ecs.useLayoutEngine();
3339
+ const dragging = require_ecs.useTag(entityId, require_ecs.Dragging);
3340
+ const card = require_ecs.useComponent(entityId, require_ecs.Card);
3341
+ const overlapCandidate = require_ecs.useTag(entityId, require_ecs.OverlapCandidate);
3342
+ const overlapTarget = require_ecs.useTag(entityId, require_ecs.OverlapTarget);
3343
+ const hot = require_ecs.useComponent(entityId, require_ecs.CardOverlapHotPoint);
3344
+ (0, react.useEffect)(() => {
3345
+ slotRef(entityId, wrapperRef.current);
3346
+ return () => slotRef(entityId, null);
3347
+ }, [entityId, slotRef]);
3348
+ const t = engine.get(entityId, require_ecs.Transform2D);
3349
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3350
+ ref: wrapperRef,
3351
+ className: "pointer-events-none absolute left-0 top-0 origin-top-left will-change-transform",
3352
+ "data-widget-slot": "",
3353
+ style: t ? {
3354
+ transform: `translate(${t.x}px, ${t.y}px)`,
3355
+ width: `${t.width}px`,
3356
+ height: `${t.height}px`
3357
+ } : {},
3358
+ children: card && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardChrome, {
3359
+ lifted: dragging,
3360
+ background: card.background,
3361
+ overlapCandidate,
3362
+ overlapTarget,
3363
+ hotX: hot?.x,
3364
+ hotY: hot?.y,
3365
+ hotStrength: hot?.strength
3366
+ })
3367
+ });
3368
+ });
3369
+ //#endregion
3370
+ //#region src/react/widgets/WidgetSlot.tsx
3371
+ /**
3372
+ * Wrapper for a DOM widget — owns the slot's positioning, registers its
3373
+ * ref with the rAF batcher, and renders the user's widget component.
3374
+ *
3375
+ * Pointer routing lives in the canvas-level `PointerEventBus` (RFC-006).
3376
+ * The slot does not call the engine; user widget React handlers run on
3377
+ * the natural DOM event path and bubble to the bus, which decides
3378
+ * whether to invoke engine semantics. Authors call `e.stopPropagation()`
3379
+ * from inside their widget to opt out of engine drag/select.
3380
+ */
3381
+ const WidgetSlot = (0, react.memo)(function WidgetSlot({ entityId, slotRef }) {
3382
+ const wrapperRef = (0, react.useRef)(null);
3383
+ const engine = require_ecs.useLayoutEngine();
3384
+ const resolve = useWidgetResolver();
3385
+ const widgetComp = require_ecs.useComponent(entityId, require_ecs.Widget);
3386
+ const resolved = resolve?.(entityId, widgetComp?.type ?? "");
3387
+ const WidgetComponent = resolved && resolved.surface === "dom" ? resolved.component : null;
3388
+ (0, react.useEffect)(() => {
3389
+ slotRef(entityId, wrapperRef.current);
3390
+ return () => slotRef(entityId, null);
3391
+ }, [entityId, slotRef]);
3392
+ const t = engine.get(entityId, require_ecs.Transform2D);
3393
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3394
+ ref: wrapperRef,
3395
+ "data-widget-slot": "",
3396
+ className: "absolute left-0 top-0 origin-top-left will-change-transform",
3397
+ style: t ? {
3398
+ transform: `translate(${t.x}px, ${t.y}px)`,
3399
+ width: `${t.width}px`,
3400
+ height: `${t.height}px`
3401
+ } : {},
3402
+ 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" })
3403
+ });
3404
+ });
3405
+ //#endregion
3406
+ //#region src/r3f/compositor/hooks.ts
3407
+ /**
3408
+ * Marks the current R3F widget as actively animating. While `active` is true,
3409
+ * the state machine places the widget in `Hot`; when false, it returns to
3410
+ * `Warm` on the next frame.
3411
+ *
3412
+ * Widgets should call this whenever they want per-frame ticking (e.g. during
3413
+ * a spring settle, hover lerp, or an external animation). Without this
3414
+ * signal, `useFrame` bodies may still fire when the canvas re-renders for
3415
+ * other reasons — check `useWidgetPhase() === 'Hot'` to early-exit work
3416
+ * that's only meaningful during the animation.
3417
+ */
3418
+ function useWidgetAnimation(entityId, active) {
3419
+ const engine = require_ecs.useLayoutEngine();
3420
+ (0, react.useEffect)(() => {
3421
+ if (active) {
3422
+ engine.world.addTag(entityId, R3FAnimationSignal);
3423
+ engine.markDirty();
3424
+ return () => {
3425
+ engine.world.removeTag(entityId, R3FAnimationSignal);
3426
+ engine.markDirty();
3427
+ };
3428
+ }
3429
+ engine.world.removeTag(entityId, R3FAnimationSignal);
3430
+ engine.markDirty();
3431
+ }, [
3432
+ engine,
3433
+ entityId,
3434
+ active
3435
+ ]);
3436
+ }
3437
+ /**
3438
+ * Returns the current compositor phase for the widget. Re-renders when the
3439
+ * phase changes.
3440
+ */
3441
+ function useWidgetPhase(entityId) {
3442
+ return require_ecs.useComponent(entityId, R3FRenderState)?.phase ?? null;
3443
+ }
3444
+ /**
3445
+ * Returns a function that schedules a one-shot repaint of the widget. Use
3446
+ * when widget content changes outside of React's render cycle (e.g., a
3447
+ * subscription to an external store, an imperative WebSocket message).
3448
+ *
3449
+ * Internally bumps `paintGeneration` so the compositor's dirty check picks
3450
+ * the widget up on the next frame, and invalidates the canvas so that
3451
+ * frame is actually scheduled.
3452
+ */
3453
+ function useWidgetInvalidate(entityId) {
3454
+ const engine = require_ecs.useLayoutEngine();
3455
+ const invalidate = (0, _react_three_fiber.useThree)((s) => s.invalidate);
3456
+ return (0, react.useCallback)(() => {
3457
+ const current = engine.world.getComponent(entityId, R3FRenderState);
3458
+ if (!current) {
3459
+ invalidate();
3460
+ return;
3461
+ }
3462
+ engine.world.setComponent(entityId, R3FRenderState, {
3463
+ ...current,
3464
+ paintGeneration: current.paintGeneration + 1
3465
+ });
3466
+ invalidate();
3467
+ }, [
3468
+ engine,
3469
+ entityId,
3470
+ invalidate
3471
+ ]);
3472
+ }
3473
+ /**
3474
+ * Acquires a shared geometry from the Compositor's `ResourceRegistry`,
3475
+ * keyed by `cacheKey`. The factory runs only on first acquisition; later
3476
+ * callers with the same key get the same instance. Released automatically
3477
+ * on unmount; the registry disposes when the last holder releases.
3478
+ *
3479
+ * Use for geometries that are expensive to build and frequently identical
3480
+ * across widget instances — e.g. preset card backs.
3481
+ */
3482
+ function useSharedGeometry(cacheKey, factory) {
3483
+ const { registry } = useCompositor();
3484
+ const factoryRef = (0, react.useRef)(factory);
3485
+ factoryRef.current = factory;
3486
+ const geometry = (0, react.useMemo)(() => registry.acquireGeometry(cacheKey, factoryRef.current), [registry, cacheKey]);
3487
+ (0, react.useEffect)(() => {
3488
+ return () => registry.releaseGeometry(cacheKey);
3489
+ }, [registry, cacheKey]);
3490
+ return geometry;
3491
+ }
3492
+ /** Same contract as {@link useSharedGeometry}, for materials. */
3493
+ function useSharedMaterial(cacheKey, factory) {
3494
+ const { registry } = useCompositor();
3495
+ const factoryRef = (0, react.useRef)(factory);
3496
+ factoryRef.current = factory;
3497
+ const material = (0, react.useMemo)(() => registry.acquireMaterial(cacheKey, factoryRef.current), [registry, cacheKey]);
3498
+ (0, react.useEffect)(() => {
3499
+ return () => registry.releaseMaterial(cacheKey);
3500
+ }, [registry, cacheKey]);
3501
+ return material;
3502
+ }
3503
+ /** Same contract as {@link useSharedGeometry}, for textures. */
3504
+ function useSharedTexture(cacheKey, factory) {
3505
+ const { registry } = useCompositor();
3506
+ const factoryRef = (0, react.useRef)(factory);
3507
+ factoryRef.current = factory;
3508
+ const texture = (0, react.useMemo)(() => registry.acquireTexture(cacheKey, factoryRef.current), [registry, cacheKey]);
3509
+ (0, react.useEffect)(() => {
3510
+ return () => registry.releaseTexture(cacheKey);
3511
+ }, [registry, cacheKey]);
3512
+ return texture;
3513
+ }
3514
+ //#endregion
3515
+ Object.defineProperty(exports, "CardChrome", {
3516
+ enumerable: true,
3517
+ get: function() {
3518
+ return CardChrome;
3519
+ }
3520
+ });
3521
+ Object.defineProperty(exports, "Compositor", {
3522
+ enumerable: true,
3523
+ get: function() {
3524
+ return Compositor;
3525
+ }
3526
+ });
3527
+ Object.defineProperty(exports, "CompositorContext", {
3528
+ enumerable: true,
3529
+ get: function() {
3530
+ return CompositorContext;
3531
+ }
3532
+ });
3533
+ Object.defineProperty(exports, "ContainerRefProvider", {
3534
+ enumerable: true,
3535
+ get: function() {
3536
+ return ContainerRefProvider;
3537
+ }
3538
+ });
3539
+ Object.defineProperty(exports, "DEFAULT_GRID_CONFIG", {
3540
+ enumerable: true,
3541
+ get: function() {
3542
+ return DEFAULT_GRID_CONFIG;
3543
+ }
3544
+ });
3545
+ Object.defineProperty(exports, "DEFAULT_SELECTION_CONFIG", {
3546
+ enumerable: true,
3547
+ get: function() {
3548
+ return DEFAULT_SELECTION_CONFIG;
3549
+ }
3550
+ });
3551
+ Object.defineProperty(exports, "DEFAULT_SNAP_GUIDE_CONFIG", {
3552
+ enumerable: true,
3553
+ get: function() {
3554
+ return DEFAULT_SNAP_GUIDE_CONFIG;
3555
+ }
3556
+ });
3557
+ Object.defineProperty(exports, "GridRenderer", {
3558
+ enumerable: true,
3559
+ get: function() {
3560
+ return GridRenderer;
3561
+ }
3562
+ });
3563
+ Object.defineProperty(exports, "Profiler", {
3564
+ enumerable: true,
3565
+ get: function() {
3566
+ return Profiler;
3567
+ }
3568
+ });
3569
+ Object.defineProperty(exports, "ProfilerProbe", {
3570
+ enumerable: true,
3571
+ get: function() {
3572
+ return ProfilerProbe;
3573
+ }
3574
+ });
3575
+ Object.defineProperty(exports, "R3FAnimationSignal", {
3576
+ enumerable: true,
3577
+ get: function() {
3578
+ return R3FAnimationSignal;
3579
+ }
3580
+ });
3581
+ Object.defineProperty(exports, "R3FManager", {
3582
+ enumerable: true,
3583
+ get: function() {
3584
+ return R3FManager;
3585
+ }
3586
+ });
3587
+ Object.defineProperty(exports, "R3FRenderBudget", {
3588
+ enumerable: true,
3589
+ get: function() {
3590
+ return R3FRenderBudget;
3591
+ }
3592
+ });
3593
+ Object.defineProperty(exports, "R3FRenderState", {
3594
+ enumerable: true,
3595
+ get: function() {
3596
+ return R3FRenderState;
3597
+ }
3598
+ });
3599
+ Object.defineProperty(exports, "ResourceRegistry", {
3600
+ enumerable: true,
3601
+ get: function() {
3602
+ return ResourceRegistry;
3603
+ }
3604
+ });
3605
+ Object.defineProperty(exports, "SelectionOverlaySlot", {
3606
+ enumerable: true,
3607
+ get: function() {
3608
+ return SelectionOverlaySlot;
3609
+ }
3610
+ });
3611
+ Object.defineProperty(exports, "SelectionRenderer", {
3612
+ enumerable: true,
3613
+ get: function() {
3614
+ return SelectionRenderer;
3615
+ }
3616
+ });
3617
+ Object.defineProperty(exports, "SpatialIndex", {
3618
+ enumerable: true,
3619
+ get: function() {
3620
+ return SpatialIndex;
3621
+ }
3622
+ });
3623
+ Object.defineProperty(exports, "VirtualWidget", {
3624
+ enumerable: true,
3625
+ get: function() {
3626
+ return VirtualWidget;
3627
+ }
3628
+ });
3629
+ Object.defineProperty(exports, "WebGLManager", {
3630
+ enumerable: true,
3631
+ get: function() {
3632
+ return WebGLManager;
3633
+ }
3634
+ });
3635
+ Object.defineProperty(exports, "WidgetRenderTargetPool", {
3636
+ enumerable: true,
3637
+ get: function() {
3638
+ return WidgetRenderTargetPool;
3639
+ }
3640
+ });
3641
+ Object.defineProperty(exports, "WidgetResolverProvider", {
3642
+ enumerable: true,
3643
+ get: function() {
3644
+ return WidgetResolverProvider;
3645
+ }
3646
+ });
3647
+ Object.defineProperty(exports, "WidgetSlot", {
3648
+ enumerable: true,
3649
+ get: function() {
3650
+ return WidgetSlot;
3651
+ }
3652
+ });
3653
+ Object.defineProperty(exports, "WidgetStateMachine", {
3654
+ enumerable: true,
3655
+ get: function() {
3656
+ return WidgetStateMachine;
3657
+ }
3658
+ });
3659
+ Object.defineProperty(exports, "ZOOM_BANDS", {
3660
+ enumerable: true,
3661
+ get: function() {
3662
+ return ZOOM_BANDS;
3663
+ }
3664
+ });
3665
+ Object.defineProperty(exports, "__toESM", {
3666
+ enumerable: true,
3667
+ get: function() {
3668
+ return __toESM;
3669
+ }
3670
+ });
3671
+ Object.defineProperty(exports, "computeSnapGuides", {
3672
+ enumerable: true,
3673
+ get: function() {
3674
+ return computeSnapGuides;
3675
+ }
3676
+ });
3677
+ Object.defineProperty(exports, "inputGroupStart", {
3678
+ enumerable: true,
3679
+ get: function() {
3680
+ return inputGroupStart;
3681
+ }
3682
+ });
3683
+ Object.defineProperty(exports, "inputLog", {
3684
+ enumerable: true,
3685
+ get: function() {
3686
+ return inputLog;
3687
+ }
3688
+ });
3689
+ Object.defineProperty(exports, "isOutOfBand", {
3690
+ enumerable: true,
3691
+ get: function() {
3692
+ return isOutOfBand;
3693
+ }
3694
+ });
3695
+ Object.defineProperty(exports, "selectBand", {
3696
+ enumerable: true,
3697
+ get: function() {
3698
+ return selectBand;
3699
+ }
3700
+ });
3701
+ Object.defineProperty(exports, "sharedGlowUniforms", {
3702
+ enumerable: true,
3703
+ get: function() {
3704
+ return sharedGlowUniforms;
3705
+ }
3706
+ });
3707
+ Object.defineProperty(exports, "useCompositor", {
3708
+ enumerable: true,
3709
+ get: function() {
3710
+ return useCompositor;
3711
+ }
3712
+ });
3713
+ Object.defineProperty(exports, "useContainerRef", {
3714
+ enumerable: true,
3715
+ get: function() {
3716
+ return useContainerRef;
3717
+ }
3718
+ });
3719
+ Object.defineProperty(exports, "useSharedGeometry", {
3720
+ enumerable: true,
3721
+ get: function() {
3722
+ return useSharedGeometry;
3723
+ }
3724
+ });
3725
+ Object.defineProperty(exports, "useSharedMaterial", {
3726
+ enumerable: true,
3727
+ get: function() {
3728
+ return useSharedMaterial;
3729
+ }
3730
+ });
3731
+ Object.defineProperty(exports, "useSharedTexture", {
3732
+ enumerable: true,
3733
+ get: function() {
3734
+ return useSharedTexture;
3735
+ }
3736
+ });
3737
+ Object.defineProperty(exports, "useWidgetAnimation", {
3738
+ enumerable: true,
3739
+ get: function() {
3740
+ return useWidgetAnimation;
3741
+ }
3742
+ });
3743
+ Object.defineProperty(exports, "useWidgetInvalidate", {
3744
+ enumerable: true,
3745
+ get: function() {
3746
+ return useWidgetInvalidate;
3747
+ }
3748
+ });
3749
+ Object.defineProperty(exports, "useWidgetPhase", {
3750
+ enumerable: true,
3751
+ get: function() {
3752
+ return useWidgetPhase;
3753
+ }
3754
+ });
3755
+ Object.defineProperty(exports, "useWidgetResolver", {
3756
+ enumerable: true,
3757
+ get: function() {
3758
+ return useWidgetResolver;
3759
+ }
3760
+ });
3761
+
3762
+ //# sourceMappingURL=hooks-CtP02JNt.cjs.map