@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.
- package/README.md +65 -0
- package/dist/advanced.cjs +61 -24
- package/dist/advanced.cjs.map +1 -1
- package/dist/advanced.d.cts +180 -64
- package/dist/advanced.d.cts.map +1 -1
- package/dist/advanced.d.mts +180 -64
- package/dist/advanced.d.mts.map +1 -1
- package/dist/advanced.mjs +29 -12
- package/dist/advanced.mjs.map +1 -1
- package/dist/devtools.cjs +22 -22
- package/dist/devtools.cjs.map +1 -1
- package/dist/devtools.d.cts +2 -2
- package/dist/devtools.d.cts.map +1 -1
- package/dist/devtools.d.mts +2 -2
- package/dist/devtools.d.mts.map +1 -1
- package/dist/devtools.mjs +2 -2
- package/dist/devtools.mjs.map +1 -1
- package/dist/{hooks-BwY7rRHg.mjs → ecs-3kimUV5Z.mjs} +238 -74
- package/dist/ecs-3kimUV5Z.mjs.map +1 -0
- package/dist/{hooks-DHShH86C.cjs → ecs-B4QrqfvQ.cjs} +320 -108
- package/dist/ecs-B4QrqfvQ.cjs.map +1 -0
- package/dist/hooks-CtP02JNt.cjs +3762 -0
- package/dist/hooks-CtP02JNt.cjs.map +1 -0
- package/dist/hooks-gsQDDE56.mjs +3494 -0
- package/dist/hooks-gsQDDE56.mjs.map +1 -0
- package/dist/index-3GY7T8JM.d.mts +480 -0
- package/dist/index-3GY7T8JM.d.mts.map +1 -0
- package/dist/index-B7B1tRPl.d.cts +480 -0
- package/dist/index-B7B1tRPl.d.cts.map +1 -0
- package/dist/index-DSdbSQ_t.d.cts +1451 -0
- package/dist/index-DSdbSQ_t.d.cts.map +1 -0
- package/dist/index-Dj9odADH.d.mts +1451 -0
- package/dist/index-Dj9odADH.d.mts.map +1 -0
- package/dist/index.cjs +3865 -643
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +315 -138
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +315 -138
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3767 -571
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/SelectionRenderer-CR2PBQwx.d.cts +0 -105
- package/dist/SelectionRenderer-CR2PBQwx.d.cts.map +0 -1
- package/dist/SelectionRenderer-DlsBstAq.d.mts +0 -105
- package/dist/SelectionRenderer-DlsBstAq.d.mts.map +0 -1
- package/dist/WebGLWidgetLayer-BBMuwzHq.cjs +0 -3560
- package/dist/WebGLWidgetLayer-BBMuwzHq.cjs.map +0 -1
- package/dist/WebGLWidgetLayer-C3p1tnpm.mjs +0 -3375
- package/dist/WebGLWidgetLayer-C3p1tnpm.mjs.map +0 -1
- package/dist/engine-BfbvWXSk.d.mts +0 -982
- package/dist/engine-BfbvWXSk.d.mts.map +0 -1
- package/dist/engine-CCjuFMC-.d.cts +0 -982
- package/dist/engine-CCjuFMC-.d.cts.map +0 -1
- package/dist/hooks-BwY7rRHg.mjs.map +0 -1
- 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
|