@jamesyong42/infinite-canvas 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +170 -9
  2. package/dist/SelectionRenderer-CR2PBQwx.d.cts +105 -0
  3. package/dist/SelectionRenderer-CR2PBQwx.d.cts.map +1 -0
  4. package/dist/SelectionRenderer-DlsBstAq.d.mts +105 -0
  5. package/dist/SelectionRenderer-DlsBstAq.d.mts.map +1 -0
  6. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs +3560 -0
  7. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs.map +1 -0
  8. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs +3375 -0
  9. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs.map +1 -0
  10. package/dist/advanced.cjs +110 -165
  11. package/dist/advanced.cjs.map +1 -1
  12. package/dist/advanced.d.cts +58 -40
  13. package/dist/advanced.d.cts.map +1 -0
  14. package/dist/advanced.d.mts +99 -0
  15. package/dist/advanced.d.mts.map +1 -0
  16. package/dist/advanced.mjs +105 -0
  17. package/dist/advanced.mjs.map +1 -0
  18. package/dist/devtools.cjs +654 -0
  19. package/dist/devtools.cjs.map +1 -0
  20. package/dist/devtools.d.cts +23 -0
  21. package/dist/devtools.d.cts.map +1 -0
  22. package/dist/devtools.d.mts +23 -0
  23. package/dist/devtools.d.mts.map +1 -0
  24. package/dist/devtools.mjs +652 -0
  25. package/dist/devtools.mjs.map +1 -0
  26. package/dist/engine-BfbvWXSk.d.mts +982 -0
  27. package/dist/engine-BfbvWXSk.d.mts.map +1 -0
  28. package/dist/engine-CCjuFMC-.d.cts +982 -0
  29. package/dist/engine-CCjuFMC-.d.cts.map +1 -0
  30. package/dist/hooks-BwY7rRHg.mjs +425 -0
  31. package/dist/hooks-BwY7rRHg.mjs.map +1 -0
  32. package/dist/hooks-DHShH86C.cjs +707 -0
  33. package/dist/hooks-DHShH86C.cjs.map +1 -0
  34. package/dist/index.cjs +909 -803
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +199 -67
  37. package/dist/index.d.cts.map +1 -0
  38. package/dist/index.d.mts +258 -0
  39. package/dist/index.d.mts.map +1 -0
  40. package/dist/index.mjs +855 -0
  41. package/dist/index.mjs.map +1 -0
  42. package/package.json +47 -15
  43. package/dist/SelectionRenderer-CeWSNZT8.d.cts +0 -891
  44. package/dist/SelectionRenderer-CeWSNZT8.d.ts +0 -891
  45. package/dist/advanced.d.ts +0 -81
  46. package/dist/advanced.js +0 -124
  47. package/dist/advanced.js.map +0 -1
  48. package/dist/chunk-VSHXWTJH.cjs +0 -3228
  49. package/dist/chunk-VSHXWTJH.cjs.map +0 -1
  50. package/dist/chunk-Z6JQQOWL.js +0 -3142
  51. package/dist/chunk-Z6JQQOWL.js.map +0 -1
  52. package/dist/index.d.ts +0 -126
  53. package/dist/index.js +0 -602
  54. package/dist/index.js.map +0 -1
@@ -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