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