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