@jamesyong42/infinite-canvas 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +65 -0
  2. package/dist/advanced.cjs +61 -24
  3. package/dist/advanced.cjs.map +1 -1
  4. package/dist/advanced.d.cts +180 -64
  5. package/dist/advanced.d.cts.map +1 -1
  6. package/dist/advanced.d.mts +180 -64
  7. package/dist/advanced.d.mts.map +1 -1
  8. package/dist/advanced.mjs +29 -12
  9. package/dist/advanced.mjs.map +1 -1
  10. package/dist/devtools.cjs +22 -22
  11. package/dist/devtools.cjs.map +1 -1
  12. package/dist/devtools.d.cts +2 -2
  13. package/dist/devtools.d.cts.map +1 -1
  14. package/dist/devtools.d.mts +2 -2
  15. package/dist/devtools.d.mts.map +1 -1
  16. package/dist/devtools.mjs +2 -2
  17. package/dist/devtools.mjs.map +1 -1
  18. package/dist/{hooks-BwY7rRHg.mjs → ecs-3kimUV5Z.mjs} +238 -74
  19. package/dist/ecs-3kimUV5Z.mjs.map +1 -0
  20. package/dist/{hooks-DHShH86C.cjs → ecs-B4QrqfvQ.cjs} +320 -108
  21. package/dist/ecs-B4QrqfvQ.cjs.map +1 -0
  22. package/dist/hooks-CtP02JNt.cjs +3762 -0
  23. package/dist/hooks-CtP02JNt.cjs.map +1 -0
  24. package/dist/hooks-gsQDDE56.mjs +3494 -0
  25. package/dist/hooks-gsQDDE56.mjs.map +1 -0
  26. package/dist/index-3GY7T8JM.d.mts +480 -0
  27. package/dist/index-3GY7T8JM.d.mts.map +1 -0
  28. package/dist/index-B7B1tRPl.d.cts +480 -0
  29. package/dist/index-B7B1tRPl.d.cts.map +1 -0
  30. package/dist/index-DSdbSQ_t.d.cts +1451 -0
  31. package/dist/index-DSdbSQ_t.d.cts.map +1 -0
  32. package/dist/index-Dj9odADH.d.mts +1451 -0
  33. package/dist/index-Dj9odADH.d.mts.map +1 -0
  34. package/dist/index.cjs +3865 -643
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +315 -138
  37. package/dist/index.d.cts.map +1 -1
  38. package/dist/index.d.mts +315 -138
  39. package/dist/index.d.mts.map +1 -1
  40. package/dist/index.mjs +3767 -571
  41. package/dist/index.mjs.map +1 -1
  42. package/package.json +1 -1
  43. package/dist/SelectionRenderer-CR2PBQwx.d.cts +0 -105
  44. package/dist/SelectionRenderer-CR2PBQwx.d.cts.map +0 -1
  45. package/dist/SelectionRenderer-DlsBstAq.d.mts +0 -105
  46. package/dist/SelectionRenderer-DlsBstAq.d.mts.map +0 -1
  47. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs +0 -3560
  48. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs.map +0 -1
  49. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs +0 -3375
  50. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs.map +0 -1
  51. package/dist/engine-BfbvWXSk.d.mts +0 -982
  52. package/dist/engine-BfbvWXSk.d.mts.map +0 -1
  53. package/dist/engine-CCjuFMC-.d.cts +0 -982
  54. package/dist/engine-CCjuFMC-.d.cts.map +0 -1
  55. package/dist/hooks-BwY7rRHg.mjs.map +0 -1
  56. package/dist/hooks-DHShH86C.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,298 +1,3494 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_WebGLWidgetLayer = require("./WebGLWidgetLayer-BBMuwzHq.cjs");
3
- const require_hooks = require("./hooks-DHShH86C.cjs");
2
+ const require_hooks = require("./hooks-CtP02JNt.cjs");
3
+ const require_ecs = require("./ecs-B4QrqfvQ.cjs");
4
+ let _jamesyong42_reactive_ecs = require("@jamesyong42/reactive-ecs");
4
5
  let react = require("react");
5
- react = require_WebGLWidgetLayer.__toESM(react, 1);
6
+ react = require_hooks.__toESM(react, 1);
6
7
  let react_jsx_runtime = require("react/jsx-runtime");
7
- let _react_three_fiber = require("@react-three/fiber");
8
- let three = require("three");
9
- //#region src/react/widget-hooks.ts
8
+ //#region src/ecs/archetype.ts
9
+ function createArchetypeRegistry(archetypes = []) {
10
+ const map = /* @__PURE__ */ new Map();
11
+ for (const a of archetypes) map.set(a.id, a);
12
+ return {
13
+ register(a) {
14
+ map.set(a.id, a);
15
+ },
16
+ get(id) {
17
+ return map.get(id) ?? null;
18
+ },
19
+ getAll() {
20
+ return [...map.values()];
21
+ }
22
+ };
23
+ }
24
+ //#endregion
25
+ //#region src/ecs/commands.ts
26
+ var CommandBuffer = class {
27
+ undoStack = [];
28
+ redoStack = [];
29
+ currentGroup = null;
30
+ /** Start grouping commands (e.g., on pointerdown). All commands until endGroup() are one undo step. */
31
+ beginGroup() {
32
+ if (this.currentGroup !== null) this.endGroup();
33
+ this.currentGroup = [];
34
+ }
35
+ /** Execute a command and record it for undo. */
36
+ execute(command, world) {
37
+ command.execute(world);
38
+ if (this.currentGroup) this.currentGroup.push(command);
39
+ else {
40
+ this.undoStack.push([command]);
41
+ this.redoStack.length = 0;
42
+ }
43
+ }
44
+ /** Close the current group — all commands since beginGroup() become one undo step. */
45
+ endGroup() {
46
+ if (this.currentGroup && this.currentGroup.length > 0) {
47
+ this.undoStack.push(this.currentGroup);
48
+ this.redoStack.length = 0;
49
+ }
50
+ this.currentGroup = null;
51
+ }
52
+ /** Undo the last command group. */
53
+ undo(world) {
54
+ if (this.currentGroup) this.endGroup();
55
+ const group = this.undoStack.pop();
56
+ if (!group) return false;
57
+ for (let i = group.length - 1; i >= 0; i--) group[i].undo(world);
58
+ this.redoStack.push(group);
59
+ return true;
60
+ }
61
+ /** Redo the last undone command group. */
62
+ redo(world) {
63
+ const group = this.redoStack.pop();
64
+ if (!group) return false;
65
+ for (const cmd of group) cmd.execute(world);
66
+ this.undoStack.push(group);
67
+ return true;
68
+ }
69
+ canUndo() {
70
+ return this.undoStack.length > 0 || this.currentGroup !== null && this.currentGroup.length > 0;
71
+ }
72
+ canRedo() {
73
+ return this.redoStack.length > 0;
74
+ }
75
+ clear() {
76
+ this.undoStack.length = 0;
77
+ this.redoStack.length = 0;
78
+ this.currentGroup = null;
79
+ }
80
+ get undoSize() {
81
+ return this.undoStack.length;
82
+ }
83
+ get redoSize() {
84
+ return this.redoStack.length;
85
+ }
86
+ };
87
+ var MoveCommand = class {
88
+ beforePositions = /* @__PURE__ */ new Map();
89
+ afterPositions = /* @__PURE__ */ new Map();
90
+ captured = false;
91
+ constructor(entityIds, dx, dy, transformType) {
92
+ this.entityIds = entityIds;
93
+ this.dx = dx;
94
+ this.dy = dy;
95
+ this.transformType = transformType;
96
+ }
97
+ execute(world) {
98
+ if (!this.captured) {
99
+ for (const id of this.entityIds) {
100
+ const t = world.getComponent(id, this.transformType);
101
+ if (t) {
102
+ this.beforePositions.set(id, {
103
+ x: t.x,
104
+ y: t.y
105
+ });
106
+ this.afterPositions.set(id, {
107
+ x: t.x + this.dx,
108
+ y: t.y + this.dy
109
+ });
110
+ }
111
+ }
112
+ this.captured = true;
113
+ }
114
+ for (const [id, pos] of this.afterPositions) world.setComponent(id, this.transformType, {
115
+ x: pos.x,
116
+ y: pos.y
117
+ });
118
+ }
119
+ undo(world) {
120
+ for (const [id, pos] of this.beforePositions) world.setComponent(id, this.transformType, {
121
+ x: pos.x,
122
+ y: pos.y
123
+ });
124
+ }
125
+ };
126
+ var ResizeCommand = class {
127
+ constructor(entityId, before, after, transformType) {
128
+ this.entityId = entityId;
129
+ this.before = before;
130
+ this.after = after;
131
+ this.transformType = transformType;
132
+ this.after = {
133
+ ...after,
134
+ width: Math.max(20, after.width),
135
+ height: Math.max(20, after.height)
136
+ };
137
+ }
138
+ execute(world) {
139
+ world.setComponent(this.entityId, this.transformType, this.after);
140
+ }
141
+ undo(world) {
142
+ world.setComponent(this.entityId, this.transformType, this.before);
143
+ }
144
+ };
145
+ /**
146
+ * Capture an entity's complete current state. Used by `ConsumeCommand`
147
+ * so `undo` can fully reconstitute a destroyed child. Relies on
148
+ * `world.getComponentsOf(entity)` / `world.getTagsOf(entity)` so no
149
+ * external type registries need to be threaded through.
150
+ */
151
+ function snapshotEntity(world, entity) {
152
+ const components = [];
153
+ for (const type of world.getComponentsOf(entity)) {
154
+ const data = world.getComponent(entity, type);
155
+ if (data !== void 0) components.push({
156
+ type,
157
+ data: structuredClone(data)
158
+ });
159
+ }
160
+ return {
161
+ entityId: entity,
162
+ components,
163
+ tags: [...world.getTagsOf(entity)]
164
+ };
165
+ }
166
+ /**
167
+ * Recreate an entity's state from a snapshot. Uses the same
168
+ * `EntityId` captured in the snapshot if the world allows
169
+ * reassignment; otherwise creates a fresh id and returns it.
170
+ */
171
+ function rehydrateEntity(world, snapshot) {
172
+ const id = world.createEntity();
173
+ for (const entry of snapshot.components) world.addComponent(id, entry.type, entry.data);
174
+ for (const tag of snapshot.tags) world.addTag(id, tag);
175
+ return id;
176
+ }
177
+ /**
178
+ * Drop-to-consume command (RFC-004 § Phase 4). Undo / redo are
179
+ * fully supported by:
180
+ * - capturing a full entity snapshot of the child at construction time,
181
+ * - delegating the forward mutation to the parent widget's
182
+ * `applyMutation` handler,
183
+ * - delegating the reverse mutation to `revertMutation`.
184
+ *
185
+ * Handlers are supplied at construction time (captured from the widget
186
+ * registry in the interaction runtime's pointerup path), so the
187
+ * command doesn't need to know about the registry shape itself.
188
+ */
189
+ var ConsumeCommand = class {
190
+ /**
191
+ * The entity id we currently target for destroy. Starts as the
192
+ * initial `childId` captured at construction; updated to the fresh
193
+ * id returned by `rehydrateEntity` on undo so that a subsequent
194
+ * `redo()` destroys the rehydrated entity (not a stale id from a
195
+ * previous execute cycle).
196
+ */
197
+ currentChildId;
198
+ constructor(parentId, childId, childSnapshot, mutation, applyMutation, revertMutation) {
199
+ this.parentId = parentId;
200
+ this.childId = childId;
201
+ this.childSnapshot = childSnapshot;
202
+ this.mutation = mutation;
203
+ this.applyMutation = applyMutation;
204
+ this.revertMutation = revertMutation;
205
+ this.currentChildId = childId;
206
+ }
207
+ execute(world) {
208
+ this.applyMutation?.(world, this.mutation);
209
+ if (this.applyMutation === void 0 && world.entityExists(this.currentChildId)) world.destroyEntity(this.currentChildId);
210
+ }
211
+ undo(world) {
212
+ this.revertMutation?.(world, this.mutation);
213
+ if (!world.entityExists(this.currentChildId)) this.currentChildId = rehydrateEntity(world, this.childSnapshot);
214
+ }
215
+ };
216
+ var SetComponentCommand = class {
217
+ constructor(entityId, type, before, after) {
218
+ this.entityId = entityId;
219
+ this.type = type;
220
+ this.before = before;
221
+ this.after = after;
222
+ }
223
+ execute(world) {
224
+ world.setComponent(this.entityId, this.type, this.after);
225
+ }
226
+ undo(world) {
227
+ world.setComponent(this.entityId, this.type, this.before);
228
+ }
229
+ };
230
+ //#endregion
231
+ //#region src/ecs/math.ts
232
+ /** Convert a Rect to AABB */
233
+ function rectToAABB(r) {
234
+ return {
235
+ minX: r.x,
236
+ minY: r.y,
237
+ maxX: r.x + r.width,
238
+ maxY: r.y + r.height
239
+ };
240
+ }
241
+ /** Convert AABB to Rect */
242
+ function aabbToRect(a) {
243
+ return {
244
+ x: a.minX,
245
+ y: a.minY,
246
+ width: a.maxX - a.minX,
247
+ height: a.maxY - a.minY
248
+ };
249
+ }
250
+ /** Test if two AABBs overlap */
251
+ function intersectsAABB(a, b) {
252
+ return a.maxX >= b.minX && a.minX <= b.maxX && a.maxY >= b.minY && a.minY <= b.maxY;
253
+ }
254
+ /** Test if a point is inside an AABB */
255
+ function pointInAABB(px, py, a) {
256
+ return px >= a.minX && px <= a.maxX && py >= a.minY && py <= a.maxY;
257
+ }
258
+ /** Convert screen coordinates to world coordinates */
259
+ function screenToWorld(screenX, screenY, camera) {
260
+ return {
261
+ x: screenX / camera.zoom + camera.x,
262
+ y: screenY / camera.zoom + camera.y
263
+ };
264
+ }
265
+ /** Convert world coordinates to screen coordinates */
266
+ function worldToScreen(worldX, worldY, camera) {
267
+ return {
268
+ x: (worldX - camera.x) * camera.zoom,
269
+ y: (worldY - camera.y) * camera.zoom
270
+ };
271
+ }
272
+ /** Clamp a value between min and max */
273
+ function clamp(value, min, max) {
274
+ return Math.max(min, Math.min(max, value));
275
+ }
276
+ //#endregion
277
+ //#region src/ecs/systems/breakpoint.ts
278
+ /**
279
+ * Compute breakpoints for visible widgets based on screen size.
280
+ * Fix #10: Always update screenWidth/screenHeight even if breakpoint tier doesn't change.
281
+ */
282
+ const breakpointSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
283
+ name: "breakpoint",
284
+ after: "cull",
285
+ execute: (world) => {
286
+ const camera = world.getResource(require_ecs.CameraResource);
287
+ const config = world.getResource(require_ecs.BreakpointConfigResource);
288
+ for (const entity of world.query(require_ecs.Widget, require_ecs.Visible)) {
289
+ const transform = world.getComponent(entity, require_ecs.Transform2D);
290
+ if (!transform) continue;
291
+ const screenWidth = transform.width * camera.zoom;
292
+ const screenHeight = transform.height * camera.zoom;
293
+ let bp;
294
+ if (screenWidth < config.micro) bp = "micro";
295
+ else if (screenWidth < config.compact) bp = "compact";
296
+ else if (screenWidth < config.normal) bp = "normal";
297
+ else if (screenWidth < config.expanded) bp = "expanded";
298
+ else bp = "detailed";
299
+ const existing = world.getComponent(entity, require_ecs.WidgetBreakpoint);
300
+ if (!existing) world.addComponent(entity, require_ecs.WidgetBreakpoint, {
301
+ current: bp,
302
+ screenWidth,
303
+ screenHeight
304
+ });
305
+ else {
306
+ const bpChanged = existing.current !== bp;
307
+ const sizeChanged = Math.round(existing.screenWidth) !== Math.round(screenWidth) || Math.round(existing.screenHeight) !== Math.round(screenHeight);
308
+ if (bpChanged || sizeChanged) world.setComponent(entity, require_ecs.WidgetBreakpoint, {
309
+ current: bp,
310
+ screenWidth,
311
+ screenHeight
312
+ });
313
+ }
314
+ }
315
+ }
316
+ });
317
+ //#endregion
318
+ //#region src/ecs/systems/card.ts
319
+ /**
320
+ * Stamp Transform2D width/height from Card.preset every tick. Manual
321
+ * writes to Transform2D.width/height on a card entity get overwritten
322
+ * — to change card size, update `Card.preset`. The spatial-index
323
+ * observer fires reactively on the Transform2D write, so no ordering
324
+ * constraint against other systems is needed.
325
+ */
326
+ const cardSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
327
+ name: "card",
328
+ execute: (world) => {
329
+ const resource = world.getResource(require_ecs.CardPresetsResource);
330
+ if (!resource) return;
331
+ const { presets } = resource;
332
+ for (const entity of world.query(require_ecs.Card, require_ecs.Transform2D)) {
333
+ const card = world.getComponent(entity, require_ecs.Card);
334
+ const transform = world.getComponent(entity, require_ecs.Transform2D);
335
+ if (!card || !transform) continue;
336
+ const size = presets[card.preset];
337
+ if (!size) continue;
338
+ if (transform.width !== size.width || transform.height !== size.height) world.setComponent(entity, require_ecs.Transform2D, {
339
+ width: size.width,
340
+ height: size.height
341
+ });
342
+ }
343
+ }
344
+ });
345
+ //#endregion
346
+ //#region src/ecs/systems/cull.ts
347
+ /**
348
+ * Viewport culling — for every `Active` entity, sets exactly one of `Visible`
349
+ * (intersects viewport+overscan) or `Culled` (outside it). Non-Active entities
350
+ * carry neither tag.
351
+ *
352
+ * The `Culled` tag is consumed by render layers that want to keep cached state
353
+ * without rendering — the R3F compositor (RFC-002) holds Culled widgets in its
354
+ * Cold pool and skips ticks/paints for them.
355
+ */
356
+ const cullSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
357
+ name: "cull",
358
+ after: "navigationFilter",
359
+ execute: (world) => {
360
+ const camera = world.getResource(require_ecs.CameraResource);
361
+ const viewport = world.getResource(require_ecs.ViewportResource);
362
+ if (viewport.width === 0 || viewport.height === 0) return;
363
+ const spatialIndex = world.getResource(require_ecs.SpatialIndexResource).instance;
364
+ const overscan = 200 / camera.zoom;
365
+ const vpWorldAABB = {
366
+ minX: camera.x - overscan,
367
+ minY: camera.y - overscan,
368
+ maxX: camera.x + viewport.width / camera.zoom + overscan,
369
+ maxY: camera.y + viewport.height / camera.zoom + overscan
370
+ };
371
+ for (const entity of world.queryTagged(require_ecs.Visible)) world.removeTag(entity, require_ecs.Visible);
372
+ for (const entity of world.queryTagged(require_ecs.Culled)) world.removeTag(entity, require_ecs.Culled);
373
+ const visibleIds = /* @__PURE__ */ new Set();
374
+ if (spatialIndex && spatialIndex.size > 0) {
375
+ const candidates = spatialIndex.search(vpWorldAABB);
376
+ for (const entry of candidates) if (world.hasTag(entry.entityId, require_ecs.Active)) {
377
+ world.addTag(entry.entityId, require_ecs.Visible);
378
+ visibleIds.add(entry.entityId);
379
+ }
380
+ } else for (const entity of world.queryTagged(require_ecs.Active)) {
381
+ const t = world.getComponent(entity, require_ecs.Transform2D);
382
+ if (t && intersectsAABB(rectToAABB(t), vpWorldAABB)) {
383
+ world.addTag(entity, require_ecs.Visible);
384
+ visibleIds.add(entity);
385
+ }
386
+ }
387
+ for (const entity of world.queryTagged(require_ecs.Active)) if (!visibleIds.has(entity)) world.addTag(entity, require_ecs.Culled);
388
+ }
389
+ });
390
+ //#endregion
391
+ //#region src/ecs/systems/drag-promote.ts
392
+ /**
393
+ * Promotes a dragged DOM card to the 'overlay' layer so it visually pops above
394
+ * its siblings; reverses on drag end. R3F cards opt out — the compositor
395
+ * handles their stacking via `uDraggedRect` clip + renderOrder bump
396
+ * (RFC-002), and a bare DOM widget without `Card` is a debug-style surface
397
+ * that shouldn't acquire card-shaped affordances.
398
+ *
399
+ * Diff signal: presence of `PreDragLayer` is the "currently promoted" flag.
400
+ * Dragging present, no PreDragLayer → promote (stash old layer, set overlay)
401
+ * PreDragLayer present, no Dragging → restore (write back stashed layer)
402
+ *
403
+ * RFC-010 Phase 1 — migrates the two `onTagAdded(Dragging)` /
404
+ * `onTagRemoved(Dragging)` observers out of `LayoutEngine.ts` into a
405
+ * `SystemScheduler` system. The `before: 'cull'` constraint is conservative
406
+ * and becomes implicit once RFC-010 Phase 2 stamps `phase: 'react'`.
407
+ */
408
+ const dragPromoteSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
409
+ name: "dragPromote",
410
+ before: "cull",
411
+ execute: (world) => {
412
+ for (const entity of world.queryTagged(require_ecs.Dragging)) {
413
+ if (world.hasComponent(entity, require_ecs.PreDragLayer)) continue;
414
+ if (!world.hasComponent(entity, require_ecs.Card)) continue;
415
+ if (world.getComponent(entity, require_ecs.Widget)?.surface === "webgl") continue;
416
+ const prev = world.getComponent(entity, require_ecs.Layer)?.name ?? "base";
417
+ world.addComponent(entity, require_ecs.PreDragLayer, { name: prev });
418
+ if (world.hasComponent(entity, require_ecs.Layer)) world.setComponent(entity, require_ecs.Layer, { name: "overlay" });
419
+ else world.addComponent(entity, require_ecs.Layer, { name: "overlay" });
420
+ }
421
+ for (const entity of world.query(require_ecs.PreDragLayer)) {
422
+ if (world.hasTag(entity, require_ecs.Dragging)) continue;
423
+ const stash = world.getComponent(entity, require_ecs.PreDragLayer);
424
+ if (!stash) continue;
425
+ world.setComponent(entity, require_ecs.Layer, { name: stash.name });
426
+ world.removeComponent(entity, require_ecs.PreDragLayer);
427
+ }
428
+ }
429
+ });
430
+ //#endregion
431
+ //#region src/ecs/systems/navigation-filter.ts
432
+ /**
433
+ * Reconcile `Active` for a single entity against the given nav frame.
434
+ * Exported so the engine can wire a reactive `ParentFrame` observer
435
+ * that keeps the consume / re-parent / undo paths in sync without
436
+ * waiting for a nav-stack change (RFC-004 § Phase 5).
437
+ */
438
+ function reconcileEntityActive(world, entity) {
439
+ const navStack = world.getResource(require_ecs.NavigationStackResource);
440
+ const activeContainer = navStack.frames[navStack.frames.length - 1].containerId;
441
+ const pf = world.getComponent(entity, require_ecs.ParentFrame);
442
+ const belongs = activeContainer === null ? pf === void 0 : pf?.id === activeContainer;
443
+ const isActive = world.hasTag(entity, require_ecs.Active);
444
+ if (belongs && !isActive) world.addTag(entity, require_ecs.Active);
445
+ else if (!belongs && isActive) world.removeTag(entity, require_ecs.Active);
446
+ }
447
+ /**
448
+ * Filter entities to the active navigation layer.
449
+ * Runs on nav-stack changes (full refilter) and incrementally whenever new
450
+ * Transform2D entities are added (so runtime spawns land in the active layer).
451
+ *
452
+ * Mid-session `ParentFrame` mutations (consume / undo) are handled out of
453
+ * band by a reactive observer in the engine — see
454
+ * {@link reconcileEntityActive}.
455
+ */
456
+ const navigationFilterSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
457
+ name: "navigationFilter",
458
+ execute: (world) => {
459
+ const navStack = world.getResource(require_ecs.NavigationStackResource);
460
+ const stackChanged = navStack.changed;
461
+ const newEntities = world.queryAdded(require_ecs.Transform2D);
462
+ if (!stackChanged && newEntities.length === 0) return;
463
+ const activeContainer = navStack.frames[navStack.frames.length - 1].containerId;
464
+ const belongsToCurrentFrame = (entity) => {
465
+ if (activeContainer === null) return !world.hasComponent(entity, require_ecs.ParentFrame);
466
+ return world.getComponent(entity, require_ecs.ParentFrame)?.id === activeContainer;
467
+ };
468
+ if (stackChanged) {
469
+ for (const entity of world.queryTagged(require_ecs.Active)) world.removeTag(entity, require_ecs.Active);
470
+ for (const entity of world.query(require_ecs.Transform2D)) if (belongsToCurrentFrame(entity)) world.addTag(entity, require_ecs.Active);
471
+ navStack.changed = false;
472
+ } else for (const entity of newEntities) if (belongsToCurrentFrame(entity) && !world.hasTag(entity, require_ecs.Active)) world.addTag(entity, require_ecs.Active);
473
+ }
474
+ });
475
+ //#endregion
476
+ //#region src/ecs/systems/sort.ts
477
+ /**
478
+ * Sort visible entities by z-index (handled in engine.tick()).
479
+ */
480
+ const sortSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
481
+ name: "sort",
482
+ after: "breakpoint",
483
+ execute: (_world) => {}
484
+ });
485
+ //#endregion
486
+ //#region src/ecs/systems/transform-tween.ts
487
+ /**
488
+ * Apply an easing curve to a linear progress value in `[0, 1]`.
489
+ *
490
+ * - `linear` — no curve (`p`).
491
+ * - `ease-out` — cubic ease-out (`1 - (1-p)^3`), default for fly-back.
492
+ * - `ease-in-out` — cubic ease-in-out; symmetric accelerate/decelerate.
493
+ * - `spring` — not yet implemented; falls back to `ease-out` for now.
494
+ *
495
+ * Input is clamped to `[0, 1]`; callers are expected to pass the tween's
496
+ * `elapsed / durationMs` before clamping themselves.
497
+ */
498
+ function applyEasing(p, easing) {
499
+ const t = p < 0 ? 0 : p > 1 ? 1 : p;
500
+ switch (easing) {
501
+ case "linear": return t;
502
+ case "ease-in-out": return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
503
+ case "ease-out":
504
+ case "spring": return 1 - (1 - t) ** 3;
505
+ }
506
+ }
507
+ /**
508
+ * Advance every active `TransformTween`, write the interpolated
509
+ * position back to the entity's `Transform2D`, and remove the tween
510
+ * component on completion (RFC-004 § Phase 2).
511
+ *
512
+ * No scheduler ordering constraint — the spatial-index observer fires
513
+ * reactively on every `Transform2D` change, so downstream consumers
514
+ * see the animated values within the same tick regardless of system
515
+ * order.
516
+ */
517
+ const transformTweenSystem = (0, _jamesyong42_reactive_ecs.defineSystem)({
518
+ name: "transformTween",
519
+ execute: (world) => {
520
+ const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
521
+ for (const entity of world.query(require_ecs.TransformTween)) {
522
+ const t = world.getComponent(entity, require_ecs.TransformTween);
523
+ const x2d = world.getComponent(entity, require_ecs.Transform2D);
524
+ if (!t) continue;
525
+ if (!x2d) {
526
+ world.removeComponent(entity, require_ecs.TransformTween);
527
+ continue;
528
+ }
529
+ const elapsed = nowMs - t.startMs;
530
+ if (elapsed >= t.durationMs) {
531
+ world.setComponent(entity, require_ecs.Transform2D, {
532
+ x: t.toX,
533
+ y: t.toY
534
+ });
535
+ world.removeComponent(entity, require_ecs.TransformTween);
536
+ continue;
537
+ }
538
+ const p = applyEasing(elapsed / t.durationMs, t.easing);
539
+ world.setComponent(entity, require_ecs.Transform2D, {
540
+ x: t.fromX + (t.toX - t.fromX) * p,
541
+ y: t.fromY + (t.toY - t.fromY) * p
542
+ });
543
+ }
544
+ }
545
+ });
546
+ //#endregion
547
+ //#region src/ecs/engine/interaction.ts
548
+ /**
549
+ * Hit-zone size for resize hotspots (full width, screen px). Deliberately
550
+ * larger than the visual handle size to give a generous clickable area.
551
+ * Do not reduce without a UX test.
552
+ *
553
+ * Private to `interaction.ts` post-RFC-005 — handle hotspots are not
554
+ * separate ECS entities, so this constant has no other consumer.
555
+ */
556
+ const HANDLE_HIT_SIZE_PX = 16;
557
+ /**
558
+ * Return the resize-handle hotspot a world-space point falls inside,
559
+ * if any. Corners are tested before edges so they win when hit zones
560
+ * overlap — this replicates today's `layer: 15` (corner) vs
561
+ * `layer: 10` (edge) priority encoded on handle entities.
562
+ *
563
+ * `zoom` converts the screen-pixel hit size to world units so the
564
+ * hotspot stays visually constant across zoom levels.
565
+ */
566
+ function detectResizeHandle(worldX, worldY, rect, zoom) {
567
+ const half = HANDLE_HIT_SIZE_PX / zoom / 2;
568
+ const xL = rect.x;
569
+ const xR = rect.x + rect.width;
570
+ const yT = rect.y;
571
+ const yB = rect.y + rect.height;
572
+ const corners = [
573
+ {
574
+ pos: "nw",
575
+ cx: xL,
576
+ cy: yT
577
+ },
578
+ {
579
+ pos: "ne",
580
+ cx: xR,
581
+ cy: yT
582
+ },
583
+ {
584
+ pos: "sw",
585
+ cx: xL,
586
+ cy: yB
587
+ },
588
+ {
589
+ pos: "se",
590
+ cx: xR,
591
+ cy: yB
592
+ }
593
+ ];
594
+ for (const c of corners) if (worldX >= c.cx - half && worldX <= c.cx + half && worldY >= c.cy - half && worldY <= c.cy + half) return c.pos;
595
+ const edges = [
596
+ {
597
+ pos: "n",
598
+ cx: (xL + xR) / 2,
599
+ cy: yT
600
+ },
601
+ {
602
+ pos: "s",
603
+ cx: (xL + xR) / 2,
604
+ cy: yB
605
+ },
606
+ {
607
+ pos: "w",
608
+ cx: xL,
609
+ cy: (yT + yB) / 2
610
+ },
611
+ {
612
+ pos: "e",
613
+ cx: xR,
614
+ cy: (yT + yB) / 2
615
+ }
616
+ ];
617
+ for (const e of edges) if (worldX >= e.cx - half && worldX <= e.cx + half && worldY >= e.cy - half && worldY <= e.cy + half) return e.pos;
618
+ return null;
619
+ }
620
+ /**
621
+ * Inline resize-handle hit test (RFC-005).
622
+ *
623
+ * Returns a `{ entityId: widgetId, role: resize-with-handle-pos }`
624
+ * result when the pointer falls inside one of the 8 hotspots around
625
+ * the single selected `Resizable` widget. Gated on exactly-one
626
+ * selected Resizable so that multi-select drag doesn't accidentally
627
+ * latch onto a resize hotspot — matches the historical
628
+ * handle-spawning rule.
629
+ */
630
+ function findInlineResizeHit(world, worldX, worldY, zoom) {
631
+ let selected = null;
632
+ let count = 0;
633
+ for (const e of world.queryTagged(require_ecs.Resizable)) if (world.hasTag(e, require_ecs.Selected)) {
634
+ selected = e;
635
+ if (++count > 1) return null;
636
+ }
637
+ if (!selected) return null;
638
+ const t = world.getComponent(selected, require_ecs.Transform2D);
639
+ if (!t) return null;
640
+ const handle = detectResizeHandle(worldX, worldY, t, zoom);
641
+ if (!handle) return null;
642
+ return {
643
+ entityId: selected,
644
+ role: {
645
+ layer: 15,
646
+ role: {
647
+ type: "resize",
648
+ handle
649
+ }
650
+ }
651
+ };
652
+ }
653
+ /**
654
+ * Map a resize-handle position to the CSS cursor value. Used by the
655
+ * cursor system in place of the per-handle-entity `CursorHint` reads
656
+ * the old model used.
657
+ */
658
+ function cursorForHandle(handle) {
659
+ return `${handle}-resize`;
660
+ }
661
+ /**
662
+ * The pointer state machine, hit testing, selection logic, and the
663
+ * root-container cursor resolution.
664
+ *
665
+ * Kept as one cohesive unit because every branch of the state machine needs
666
+ * access to the same closed-over state (inputState, hoveredEntity, snap
667
+ * result). Splitting further would require threading state refs through
668
+ * every callee, which hurts readability more than it helps.
669
+ */
670
+ function createInteractionRuntime(ctx) {
671
+ const { world, spatialIndex, commandBuffer, markDirty, notifySelectionChanged } = ctx;
672
+ let inputState = { mode: "idle" };
673
+ let hoveredEntity = null;
674
+ /**
675
+ * Cached handle position the pointer is currently over, iff the
676
+ * hovered entity is the selected `Resizable` widget. Updated by the
677
+ * idle-branch hover path in `handlePointerMove`; read by
678
+ * `runCursorSystem` to pick the right directional cursor without
679
+ * consulting a handle entity's `CursorHint` (RFC-005).
680
+ */
681
+ let hoveredHandle = null;
682
+ let currentSnap = {
683
+ snapDx: 0,
684
+ snapDy: 0,
685
+ guides: [],
686
+ spacings: []
687
+ };
688
+ let overlapCandidates = /* @__PURE__ */ new Set();
689
+ let overlapTarget = null;
690
+ function hitTest(screenX, screenY) {
691
+ const camera = world.getResource(require_ecs.CameraResource);
692
+ const worldPos = screenToWorld(screenX, screenY, camera);
693
+ const inlineHit = findInlineResizeHit(world, worldPos.x, worldPos.y, camera.zoom);
694
+ if (inlineHit) return inlineHit;
695
+ const candidates = spatialIndex.searchPoint(worldPos.x, worldPos.y, 0);
696
+ const interactable = [];
697
+ for (const c of candidates) {
698
+ if (!world.hasTag(c.entityId, require_ecs.Active)) continue;
699
+ const role = world.getComponent(c.entityId, require_ecs.InteractionRole);
700
+ if (!role) continue;
701
+ interactable.push({
702
+ entityId: c.entityId,
703
+ role
704
+ });
705
+ }
706
+ if (interactable.length === 0) return null;
707
+ interactable.sort((a, b) => {
708
+ if (b.role.layer !== a.role.layer) return b.role.layer - a.role.layer;
709
+ const zA = world.getComponent(a.entityId, require_ecs.ZIndex)?.value ?? 0;
710
+ return (world.getComponent(b.entityId, require_ecs.ZIndex)?.value ?? 0) - zA;
711
+ });
712
+ return interactable[0];
713
+ }
714
+ function selectEntity(entity, additive) {
715
+ if (!world.hasTag(entity, require_ecs.Selectable)) return;
716
+ if (additive) if (world.hasTag(entity, require_ecs.Selected)) world.removeTag(entity, require_ecs.Selected);
717
+ else world.addTag(entity, require_ecs.Selected);
718
+ else {
719
+ for (const e of world.queryTagged(require_ecs.Selected)) if (e !== entity) world.removeTag(e, require_ecs.Selected);
720
+ world.addTag(entity, require_ecs.Selected);
721
+ }
722
+ notifySelectionChanged();
723
+ }
724
+ function clearSelection() {
725
+ const selected = world.queryTagged(require_ecs.Selected);
726
+ if (selected.length > 0) {
727
+ for (const e of selected) world.removeTag(e, require_ecs.Selected);
728
+ notifySelectionChanged();
729
+ }
730
+ }
731
+ /**
732
+ * RFC-004 § Phase 3 — contract match between a dragged card (child)
733
+ * and a candidate parent. Returns true iff their `provides` /
734
+ * `accepts` arrays intersect AND the optional `canAccept` gate on
735
+ * the parent's widget type returns truthy.
736
+ */
737
+ function contractsMatch(childId, parentId) {
738
+ const childCard = world.getComponent(childId, require_ecs.Card);
739
+ const parentCard = world.getComponent(parentId, require_ecs.Card);
740
+ if (!childCard || !parentCard) return false;
741
+ if (childCard.provides.length === 0 || parentCard.accepts.length === 0) return false;
742
+ let intersects = false;
743
+ for (const p of childCard.provides) if (parentCard.accepts.includes(p)) {
744
+ intersects = true;
745
+ break;
746
+ }
747
+ if (!intersects) return false;
748
+ const gate = ctx.getWidgetInteraction?.(world.getComponent(parentId, require_ecs.Widget)?.type ?? "")?.canAccept;
749
+ if (!gate) return true;
750
+ return gate({
751
+ parent: parentId,
752
+ child: childId,
753
+ world
754
+ });
755
+ }
756
+ /**
757
+ * RFC-004 § Phase 3 — the overlap detection pass.
758
+ *
759
+ * Called from `handlePointerMove` whenever a Card-tagged entity is
760
+ * being dragged. Produces three pieces of visible state:
761
+ *
762
+ * - `OverlapCandidate` tag + `CardOverlapHotPoint` component on every
763
+ * other card whose AABB intersects the dragged card's AABB.
764
+ * `CardOverlapHotPoint.{x,y}` are the intersection centroid in the
765
+ * overlapped card's local (0..1) frame. Layer 1 of the visual state.
766
+ *
767
+ * - `OverlapTarget` tag on at most one card — the closest by centre
768
+ * distance — iff contracts match and any `canAccept` gate passes.
769
+ * Layer 2.
770
+ *
771
+ * `strength` is set to 1 on entry and 0 on exit; CSS / shader
772
+ * consumers own the actual fade transition (opacity transition at
773
+ * the rendering layer). Keeps the ECS pass cheap and stateless.
774
+ */
775
+ function updateCardOverlap(draggedId) {
776
+ if (!world.hasComponent(draggedId, require_ecs.Card)) {
777
+ clearOverlapState();
778
+ return;
779
+ }
780
+ const draggedT = world.getComponent(draggedId, require_ecs.Transform2D);
781
+ if (!draggedT) {
782
+ clearOverlapState();
783
+ return;
784
+ }
785
+ const dxMin = draggedT.x;
786
+ const dyMin = draggedT.y;
787
+ const dxMax = draggedT.x + draggedT.width;
788
+ const dyMax = draggedT.y + draggedT.height;
789
+ const dCenterX = draggedT.x + draggedT.width / 2;
790
+ const dCenterY = draggedT.y + draggedT.height / 2;
791
+ const next = /* @__PURE__ */ new Set();
792
+ const hits = spatialIndex.search({
793
+ minX: dxMin,
794
+ minY: dyMin,
795
+ maxX: dxMax,
796
+ maxY: dyMax
797
+ });
798
+ for (const entry of hits) {
799
+ if (entry.entityId === draggedId) continue;
800
+ if (!world.hasComponent(entry.entityId, require_ecs.Card)) continue;
801
+ if (!world.hasTag(entry.entityId, require_ecs.Active)) continue;
802
+ next.add(entry.entityId);
803
+ }
804
+ for (const prev of overlapCandidates) if (!next.has(prev)) {
805
+ world.removeTag(prev, require_ecs.OverlapCandidate);
806
+ world.removeComponent(prev, require_ecs.CardOverlapHotPoint);
807
+ }
808
+ for (const c of next) if (!overlapCandidates.has(c)) {
809
+ world.addTag(c, require_ecs.OverlapCandidate);
810
+ world.addComponent(c, require_ecs.CardOverlapHotPoint, {
811
+ x: .5,
812
+ y: .5,
813
+ strength: 0
814
+ });
815
+ }
816
+ let primary = null;
817
+ let bestDist = Number.POSITIVE_INFINITY;
818
+ let bestZ = Number.NEGATIVE_INFINITY;
819
+ let bestId = Number.POSITIVE_INFINITY;
820
+ for (const c of next) {
821
+ const ct = world.getComponent(c, require_ecs.Transform2D);
822
+ if (!ct) continue;
823
+ const cxMin = ct.x;
824
+ const cyMin = ct.y;
825
+ const cxMax = ct.x + ct.width;
826
+ const cyMax = ct.y + ct.height;
827
+ const ix = (Math.max(dxMin, cxMin) + Math.min(dxMax, cxMax)) / 2;
828
+ const iy = (Math.max(dyMin, cyMin) + Math.min(dyMax, cyMax)) / 2;
829
+ const hotX = ct.width > 0 ? (ix - ct.x) / ct.width : .5;
830
+ const hotY = ct.height > 0 ? (iy - ct.y) / ct.height : .5;
831
+ world.setComponent(c, require_ecs.CardOverlapHotPoint, {
832
+ x: hotX < 0 ? 0 : hotX > 1 ? 1 : hotX,
833
+ y: hotY < 0 ? 0 : hotY > 1 ? 1 : hotY,
834
+ strength: 1
835
+ });
836
+ const ccx = ct.x + ct.width / 2;
837
+ const ccy = ct.y + ct.height / 2;
838
+ const d = Math.hypot(ccx - dCenterX, ccy - dCenterY);
839
+ const z = world.getComponent(c, require_ecs.ZIndex)?.value ?? 0;
840
+ if (d < bestDist || d === bestDist && z > bestZ || d === bestDist && z === bestZ && c < bestId) {
841
+ bestDist = d;
842
+ bestZ = z;
843
+ bestId = c;
844
+ primary = c;
845
+ }
846
+ }
847
+ overlapCandidates = next;
848
+ const nextTarget = primary !== null && contractsMatch(draggedId, primary) ? primary : null;
849
+ if (nextTarget !== overlapTarget) {
850
+ if (overlapTarget !== null) world.removeTag(overlapTarget, require_ecs.OverlapTarget);
851
+ if (nextTarget !== null) world.addTag(nextTarget, require_ecs.OverlapTarget);
852
+ overlapTarget = nextTarget;
853
+ }
854
+ }
855
+ /**
856
+ * RFC-004 § Phase 3 — tear down all overlap state. Called on drag
857
+ * end (commit / cancel) and when the dragged entity stops being a
858
+ * Card (e.g. a non-card entity enters the drag state machine).
859
+ */
860
+ function clearOverlapState() {
861
+ for (const c of overlapCandidates) {
862
+ world.removeTag(c, require_ecs.OverlapCandidate);
863
+ world.removeComponent(c, require_ecs.CardOverlapHotPoint);
864
+ }
865
+ if (overlapTarget !== null) world.removeTag(overlapTarget, require_ecs.OverlapTarget);
866
+ overlapCandidates = /* @__PURE__ */ new Set();
867
+ overlapTarget = null;
868
+ }
869
+ /**
870
+ * RFC-004 § Phase 4 — fly-back completion poll. Called by the engine's
871
+ * frame runner after `scheduler.execute(world)` each tick. The tween
872
+ * system runs first, interpolates `Transform2D` toward the starting
873
+ * position, and auto-removes its `TransformTween` component on
874
+ * completion. When we see the tween component gone from the primary
875
+ * flying-back entity, the animation is done: remove `Dragging`,
876
+ * restore `ZIndex`, transition to idle.
877
+ */
878
+ function runFlyBackSystem() {
879
+ if (inputState.mode !== "flyingBack") return;
880
+ if (world.entityExists(inputState.entityId) && world.hasComponent(inputState.entityId, require_ecs.TransformTween)) return;
881
+ for (const e of inputState.startPositions.keys()) {
882
+ if (!world.entityExists(e)) continue;
883
+ if (world.hasTag(e, require_ecs.Dragging)) world.removeTag(e, require_ecs.Dragging);
884
+ }
885
+ for (const [entity, originalZ] of inputState.originalZIndices) {
886
+ if (!world.entityExists(entity)) continue;
887
+ world.setComponent(entity, require_ecs.ZIndex, { value: originalZ });
888
+ }
889
+ inputState = { mode: "idle" };
890
+ markDirty();
891
+ }
892
+ /**
893
+ * Derive the root-container cursor from input state + hover.
894
+ * Writes to CursorResource. Called from the frame runner after systems.
895
+ */
896
+ function runCursorSystem() {
897
+ let cursor = "default";
898
+ switch (inputState.mode) {
899
+ case "idle":
900
+ case "marquee":
901
+ if (hoveredHandle !== null) cursor = cursorForHandle(hoveredHandle);
902
+ else if (hoveredEntity !== null) cursor = world.getComponent(hoveredEntity, require_ecs.CursorHint)?.hover ?? "default";
903
+ break;
904
+ case "tracking":
905
+ cursor = world.getComponent(inputState.entityId, require_ecs.CursorHint)?.hover ?? "default";
906
+ break;
907
+ case "dragging":
908
+ cursor = world.getComponent(inputState.entityId, require_ecs.CursorHint)?.active ?? "grabbing";
909
+ break;
910
+ case "resizing":
911
+ cursor = cursorForHandle(inputState.handle);
912
+ break;
913
+ }
914
+ world.setResource(require_ecs.CursorResource, { cursor });
915
+ }
916
+ /**
917
+ * Transition to `dragging` mode. Snapshots ZIndex + Transform2D for every
918
+ * `Selected` entity, elevates them to `maxZ + 1`, tags `Dragging`, opens a
919
+ * command group. Caller is responsible for `Selected` set being correct.
920
+ */
921
+ function _beginDrag(entity, startWorldX, startWorldY) {
922
+ const originalZIndices = /* @__PURE__ */ new Map();
923
+ let maxZ = 0;
924
+ for (const e of world.queryTagged(require_ecs.Active)) {
925
+ const z = world.getComponent(e, require_ecs.ZIndex);
926
+ if (z && z.value > maxZ) maxZ = z.value;
927
+ }
928
+ for (const e of world.queryTagged(require_ecs.Selected)) {
929
+ const z = world.getComponent(e, require_ecs.ZIndex);
930
+ originalZIndices.set(e, z?.value ?? 0);
931
+ world.setComponent(e, require_ecs.ZIndex, { value: maxZ + 1 });
932
+ }
933
+ const startPositions = /* @__PURE__ */ new Map();
934
+ for (const e of world.queryTagged(require_ecs.Selected)) {
935
+ const t = world.getComponent(e, require_ecs.Transform2D);
936
+ if (t) startPositions.set(e, {
937
+ x: t.x,
938
+ y: t.y
939
+ });
940
+ }
941
+ for (const e of startPositions.keys()) world.addTag(e, require_ecs.Dragging);
942
+ commandBuffer.beginGroup();
943
+ inputState = {
944
+ mode: "dragging",
945
+ entityId: entity,
946
+ startWorldX,
947
+ startWorldY,
948
+ startPositions,
949
+ originalZIndices
950
+ };
951
+ markDirty();
952
+ }
953
+ /** Apply drag math + snap + overlap update for the current cursor world position. */
954
+ function _updateDrag(worldX, worldY) {
955
+ if (inputState.mode !== "dragging") return;
956
+ const camera = world.getResource(require_ecs.CameraResource);
957
+ const totalDx = worldX - inputState.startWorldX;
958
+ const totalDy = worldY - inputState.startWorldY;
959
+ const firstId = inputState.startPositions.keys().next().value;
960
+ if (ctx.getSnapEnabled() && firstId !== void 0 && world.hasTag(firstId, require_ecs.SnapSource)) {
961
+ const draggedIds = new Set(inputState.startPositions.keys());
962
+ const firstStart = inputState.startPositions.get(firstId);
963
+ const firstT = world.getComponent(firstId, require_ecs.Transform2D);
964
+ if (firstT && firstStart) {
965
+ const draggedBounds = {
966
+ x: firstStart.x + totalDx,
967
+ y: firstStart.y + totalDy,
968
+ width: firstT.width,
969
+ height: firstT.height
970
+ };
971
+ const refs = [];
972
+ for (const entity of world.queryTagged(require_ecs.SnapTarget)) {
973
+ if (draggedIds.has(entity)) continue;
974
+ if (!world.hasTag(entity, require_ecs.Active)) continue;
975
+ const rt = world.getComponent(entity, require_ecs.Transform2D);
976
+ if (rt) refs.push({
977
+ x: rt.x,
978
+ y: rt.y,
979
+ width: rt.width,
980
+ height: rt.height
981
+ });
982
+ }
983
+ currentSnap = require_hooks.computeSnapGuides(draggedBounds, refs, ctx.getSnapThreshold() / camera.zoom);
984
+ }
985
+ } else currentSnap = {
986
+ snapDx: 0,
987
+ snapDy: 0,
988
+ guides: [],
989
+ spacings: []
990
+ };
991
+ const finalDx = totalDx + currentSnap.snapDx;
992
+ const finalDy = totalDy + currentSnap.snapDy;
993
+ for (const [e, start] of inputState.startPositions) world.setComponent(e, require_ecs.Transform2D, {
994
+ x: start.x + finalDx,
995
+ y: start.y + finalDy
996
+ });
997
+ updateCardOverlap(inputState.entityId);
998
+ markDirty();
999
+ }
1000
+ /**
1001
+ * End an active drag. `cancelled: false` runs the commit/fly-back/consume
1002
+ * decision tree (RFC-004 § Phase 4); `cancelled: true` rolls back state
1003
+ * and discards the open command group.
1004
+ */
1005
+ function _endDrag(cancelled) {
1006
+ const prevState = inputState;
1007
+ if (prevState.mode !== "dragging") return;
1008
+ if (cancelled) {
1009
+ for (const [e, start] of prevState.startPositions) if (world.entityExists(e)) world.setComponent(e, require_ecs.Transform2D, {
1010
+ x: start.x,
1011
+ y: start.y
1012
+ });
1013
+ commandBuffer.endGroup();
1014
+ for (const e of prevState.startPositions.keys()) if (world.hasTag(e, require_ecs.Dragging)) world.removeTag(e, require_ecs.Dragging);
1015
+ for (const [entity, originalZ] of prevState.originalZIndices) if (world.entityExists(entity)) world.setComponent(entity, require_ecs.ZIndex, { value: originalZ });
1016
+ clearOverlapState();
1017
+ currentSnap = {
1018
+ snapDx: 0,
1019
+ snapDy: 0,
1020
+ guides: [],
1021
+ spacings: []
1022
+ };
1023
+ inputState = { mode: "idle" };
1024
+ markDirty();
1025
+ return;
1026
+ }
1027
+ const draggedId = prevState.entityId;
1028
+ const draggedHasCard = world.hasComponent(draggedId, require_ecs.Card);
1029
+ const hadOverlap = overlapCandidates.size > 0;
1030
+ const target = overlapTarget;
1031
+ const shouldConsume = draggedHasCard && target !== null;
1032
+ if (draggedHasCard && hadOverlap && target === null) {
1033
+ const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
1034
+ for (const [e, start] of prevState.startPositions) {
1035
+ const cur = world.getComponent(e, require_ecs.Transform2D);
1036
+ if (!cur) continue;
1037
+ world.addComponent(e, require_ecs.TransformTween, {
1038
+ fromX: cur.x,
1039
+ fromY: cur.y,
1040
+ toX: start.x,
1041
+ toY: start.y,
1042
+ startMs: nowMs,
1043
+ durationMs: 250,
1044
+ easing: "ease-out",
1045
+ kind: "flyback"
1046
+ });
1047
+ }
1048
+ commandBuffer.endGroup();
1049
+ currentSnap = {
1050
+ snapDx: 0,
1051
+ snapDy: 0,
1052
+ guides: [],
1053
+ spacings: []
1054
+ };
1055
+ clearOverlapState();
1056
+ inputState = {
1057
+ mode: "flyingBack",
1058
+ entityId: draggedId,
1059
+ startPositions: prevState.startPositions,
1060
+ originalZIndices: prevState.originalZIndices
1061
+ };
1062
+ markDirty();
1063
+ return;
1064
+ }
1065
+ for (const e of prevState.startPositions.keys()) if (world.hasTag(e, require_ecs.Dragging)) world.removeTag(e, require_ecs.Dragging);
1066
+ for (const [entity, originalZ] of prevState.originalZIndices) world.setComponent(entity, require_ecs.ZIndex, { value: originalZ });
1067
+ const entityIds = [...prevState.startPositions.keys()];
1068
+ let totalDx = 0;
1069
+ let totalDy = 0;
1070
+ let movedSomething = false;
1071
+ if (entityIds.length > 0) {
1072
+ const firstId = entityIds[0];
1073
+ const start = prevState.startPositions.get(firstId);
1074
+ const current = world.getComponent(firstId, require_ecs.Transform2D);
1075
+ if (current && start) {
1076
+ totalDx = current.x - start.x;
1077
+ totalDy = current.y - start.y;
1078
+ if (totalDx !== 0 || totalDy !== 0) {
1079
+ for (const [e, s] of prevState.startPositions) world.setComponent(e, require_ecs.Transform2D, {
1080
+ x: s.x,
1081
+ y: s.y
1082
+ });
1083
+ movedSomething = true;
1084
+ }
1085
+ }
1086
+ }
1087
+ let consumeSnapshot;
1088
+ let consumeHandlers;
1089
+ let consumeMutation;
1090
+ let shouldEmitConsume = false;
1091
+ if (shouldConsume && target !== null) {
1092
+ const parentType = world.getComponent(target, require_ecs.Widget)?.type ?? "";
1093
+ consumeHandlers = ctx.getWidgetInteraction?.(parentType);
1094
+ const result = consumeHandlers?.onReceiveChild?.({
1095
+ parent: target,
1096
+ child: draggedId,
1097
+ world
1098
+ }) ?? { consume: true };
1099
+ if (result.consume) {
1100
+ consumeSnapshot = snapshotEntity(world, draggedId);
1101
+ consumeMutation = result.mutation;
1102
+ shouldEmitConsume = true;
1103
+ }
1104
+ }
1105
+ if (movedSomething) commandBuffer.execute(new MoveCommand(entityIds, totalDx, totalDy, require_ecs.Transform2D), world);
1106
+ if (shouldEmitConsume && target !== null && consumeSnapshot) {
1107
+ commandBuffer.execute(new ConsumeCommand(target, draggedId, consumeSnapshot, consumeMutation, consumeHandlers?.applyMutation, consumeHandlers?.revertMutation), world);
1108
+ if (world.hasTag(draggedId, require_ecs.Selected)) {
1109
+ world.removeTag(draggedId, require_ecs.Selected);
1110
+ notifySelectionChanged();
1111
+ }
1112
+ }
1113
+ commandBuffer.endGroup();
1114
+ currentSnap = {
1115
+ snapDx: 0,
1116
+ snapDy: 0,
1117
+ guides: [],
1118
+ spacings: []
1119
+ };
1120
+ clearOverlapState();
1121
+ inputState = { mode: "idle" };
1122
+ markDirty();
1123
+ }
1124
+ function _beginResize(entity, handle, startWorldX, startWorldY) {
1125
+ const t = world.getComponent(entity, require_ecs.Transform2D);
1126
+ if (!t) return false;
1127
+ commandBuffer.beginGroup();
1128
+ inputState = {
1129
+ mode: "resizing",
1130
+ entityId: entity,
1131
+ handle,
1132
+ startWorldX,
1133
+ startWorldY,
1134
+ startBounds: {
1135
+ x: t.x,
1136
+ y: t.y,
1137
+ width: t.width,
1138
+ height: t.height
1139
+ }
1140
+ };
1141
+ markDirty();
1142
+ return true;
1143
+ }
1144
+ function _updateResize(worldX, worldY) {
1145
+ if (inputState.mode !== "resizing") return;
1146
+ const dx = worldX - inputState.startWorldX;
1147
+ const dy = worldY - inputState.startWorldY;
1148
+ const { x, y, width: w, height: h } = inputState.startBounds;
1149
+ const handle = inputState.handle;
1150
+ let newX = x;
1151
+ let newY = y;
1152
+ let newW = w;
1153
+ let newH = h;
1154
+ if (handle.includes("e")) newW = Math.max(20, w + dx);
1155
+ if (handle.includes("w")) {
1156
+ const clampedW = Math.max(20, w - dx);
1157
+ newX = x + w - clampedW;
1158
+ newW = clampedW;
1159
+ }
1160
+ if (handle.includes("s")) newH = Math.max(20, h + dy);
1161
+ if (handle.includes("n")) {
1162
+ const clampedH = Math.max(20, h - dy);
1163
+ newY = y + h - clampedH;
1164
+ newH = clampedH;
1165
+ }
1166
+ world.setComponent(inputState.entityId, require_ecs.Transform2D, {
1167
+ x: newX,
1168
+ y: newY,
1169
+ width: newW,
1170
+ height: newH
1171
+ });
1172
+ markDirty();
1173
+ }
1174
+ function _endResize(cancelled) {
1175
+ const prevState = inputState;
1176
+ if (prevState.mode !== "resizing") return;
1177
+ if (cancelled) {
1178
+ world.setComponent(prevState.entityId, require_ecs.Transform2D, prevState.startBounds);
1179
+ commandBuffer.endGroup();
1180
+ inputState = { mode: "idle" };
1181
+ markDirty();
1182
+ return;
1183
+ }
1184
+ const t = world.getComponent(prevState.entityId, require_ecs.Transform2D);
1185
+ if (t) {
1186
+ const finalBounds = {
1187
+ x: t.x,
1188
+ y: t.y,
1189
+ width: t.width,
1190
+ height: t.height
1191
+ };
1192
+ const sb = prevState.startBounds;
1193
+ world.setComponent(prevState.entityId, require_ecs.Transform2D, sb);
1194
+ commandBuffer.execute(new ResizeCommand(prevState.entityId, sb, finalBounds, require_ecs.Transform2D), world);
1195
+ }
1196
+ commandBuffer.endGroup();
1197
+ inputState = { mode: "idle" };
1198
+ markDirty();
1199
+ }
1200
+ function _beginMarquee(startWorldX, startWorldY) {
1201
+ inputState = {
1202
+ mode: "marquee",
1203
+ startWorldX,
1204
+ startWorldY
1205
+ };
1206
+ markDirty();
1207
+ }
1208
+ function _updateMarquee(_worldX, _worldY) {}
1209
+ function _endMarquee() {
1210
+ if (inputState.mode !== "marquee") return;
1211
+ inputState = { mode: "idle" };
1212
+ markDirty();
1213
+ }
1214
+ /**
1215
+ * Cancel any active drag/resize/flyingBack state. Used by both the
1216
+ * legacy `handlePointerCancel` and as the implementation of the
1217
+ * new public `endDrag(_, { cancelled: true })` / `endResize(_, …)`.
1218
+ */
1219
+ function _cancelAll() {
1220
+ if (inputState.mode === "dragging") _endDrag(true);
1221
+ else if (inputState.mode === "resizing") _endResize(true);
1222
+ else if (inputState.mode === "flyingBack") {
1223
+ for (const [e, start] of inputState.startPositions) {
1224
+ if (!world.entityExists(e)) continue;
1225
+ if (world.hasComponent(e, require_ecs.TransformTween)) world.removeComponent(e, require_ecs.TransformTween);
1226
+ if (world.hasTag(e, require_ecs.Dragging)) world.removeTag(e, require_ecs.Dragging);
1227
+ world.setComponent(e, require_ecs.Transform2D, {
1228
+ x: start.x,
1229
+ y: start.y
1230
+ });
1231
+ }
1232
+ for (const [entity, originalZ] of inputState.originalZIndices) {
1233
+ if (!world.entityExists(entity)) continue;
1234
+ world.setComponent(entity, require_ecs.ZIndex, { value: originalZ });
1235
+ }
1236
+ currentSnap = {
1237
+ snapDx: 0,
1238
+ snapDy: 0,
1239
+ guides: [],
1240
+ spacings: []
1241
+ };
1242
+ inputState = { mode: "idle" };
1243
+ markDirty();
1244
+ } else if (inputState.mode === "marquee") _endMarquee();
1245
+ else if (inputState.mode === "tracking") {
1246
+ inputState = { mode: "idle" };
1247
+ markDirty();
1248
+ }
1249
+ }
1250
+ function handlePointerDown(screenX, screenY, _button, modifiers) {
1251
+ const startWorld = screenToWorld(screenX, screenY, world.getResource(require_ecs.CameraResource));
1252
+ if (inputState.mode === "flyingBack") {
1253
+ const hit = hitTest(screenX, screenY);
1254
+ const target = hit?.entityId === inputState.entityId ? hit.entityId : null;
1255
+ if (target !== null) {
1256
+ world.removeComponent(target, require_ecs.TransformTween);
1257
+ const newStartPositions = /* @__PURE__ */ new Map();
1258
+ for (const e of inputState.startPositions.keys()) {
1259
+ const cur = world.getComponent(e, require_ecs.Transform2D);
1260
+ if (cur) newStartPositions.set(e, {
1261
+ x: cur.x,
1262
+ y: cur.y
1263
+ });
1264
+ if (world.hasComponent(e, require_ecs.TransformTween)) world.removeComponent(e, require_ecs.TransformTween);
1265
+ }
1266
+ commandBuffer.beginGroup();
1267
+ inputState = {
1268
+ mode: "dragging",
1269
+ entityId: target,
1270
+ startWorldX: startWorld.x,
1271
+ startWorldY: startWorld.y,
1272
+ startPositions: newStartPositions,
1273
+ originalZIndices: inputState.originalZIndices
1274
+ };
1275
+ markDirty();
1276
+ return { action: "capture-drag" };
1277
+ }
1278
+ }
1279
+ const hit = hitTest(screenX, screenY);
1280
+ if (!hit) {
1281
+ clearSelection();
1282
+ _beginMarquee(startWorld.x, startWorld.y);
1283
+ return { action: "capture-marquee" };
1284
+ }
1285
+ switch (hit.role.role.type) {
1286
+ case "resize":
1287
+ if (!_beginResize(hit.entityId, hit.role.role.handle, startWorld.x, startWorld.y)) return { action: "passthrough" };
1288
+ return {
1289
+ action: "capture-resize",
1290
+ handle: hit.role.role.handle
1291
+ };
1292
+ case "drag":
1293
+ selectEntity(hit.entityId, modifiers.shift);
1294
+ if (world.hasTag(hit.entityId, require_ecs.Draggable)) inputState = {
1295
+ mode: "tracking",
1296
+ entityId: hit.entityId,
1297
+ startScreenX: screenX,
1298
+ startScreenY: screenY
1299
+ };
1300
+ markDirty();
1301
+ return { action: "passthrough-track-drag" };
1302
+ case "select":
1303
+ selectEntity(hit.entityId, modifiers.shift);
1304
+ markDirty();
1305
+ return { action: "passthrough" };
1306
+ default: return { action: "passthrough" };
1307
+ }
1308
+ }
1309
+ function handlePointerMove(screenX, screenY, _modifiers) {
1310
+ const w = screenToWorld(screenX, screenY, world.getResource(require_ecs.CameraResource));
1311
+ if (inputState.mode === "tracking") {
1312
+ const dx = screenX - inputState.startScreenX;
1313
+ const dy = screenY - inputState.startScreenY;
1314
+ if (Math.abs(dx) > 4 || Math.abs(dy) > 4) {
1315
+ _beginDrag(inputState.entityId, w.x, w.y);
1316
+ return { action: "capture-drag" };
1317
+ }
1318
+ return { action: "passthrough" };
1319
+ }
1320
+ if (inputState.mode === "dragging") {
1321
+ _updateDrag(w.x, w.y);
1322
+ return { action: "capture-drag" };
1323
+ }
1324
+ if (inputState.mode === "resizing") {
1325
+ const handle = inputState.handle;
1326
+ _updateResize(w.x, w.y);
1327
+ return {
1328
+ action: "capture-resize",
1329
+ handle
1330
+ };
1331
+ }
1332
+ if (inputState.mode === "marquee") {
1333
+ _updateMarquee(w.x, w.y);
1334
+ return { action: "capture-marquee" };
1335
+ }
1336
+ if (inputState.mode === "idle") {
1337
+ const hit = hitTest(screenX, screenY);
1338
+ const hoverTarget = hit ? hit.entityId : null;
1339
+ const hoverHandle = hit?.role.role.type === "resize" ? hit.role.role.handle : null;
1340
+ if (hoverTarget !== hoveredEntity || hoverHandle !== hoveredHandle) {
1341
+ hoveredEntity = hoverTarget;
1342
+ hoveredHandle = hoverHandle;
1343
+ markDirty();
1344
+ }
1345
+ }
1346
+ return { action: "passthrough" };
1347
+ }
1348
+ function handlePointerUp() {
1349
+ const mode = inputState.mode;
1350
+ if (mode === "dragging") _endDrag(false);
1351
+ else if (mode === "resizing") _endResize(false);
1352
+ else if (mode === "marquee") _endMarquee();
1353
+ else if (mode === "tracking") inputState = { mode: "idle" };
1354
+ return { action: "passthrough" };
1355
+ }
1356
+ function handlePointerCancel() {
1357
+ _cancelAll();
1358
+ }
1359
+ function beginDrag(_entity, worldX, worldY) {
1360
+ _beginDrag(_entity, worldX, worldY);
1361
+ }
1362
+ function updateDrag(_entity, worldX, worldY) {
1363
+ _updateDrag(worldX, worldY);
1364
+ }
1365
+ function endDrag(_entity, opts) {
1366
+ _endDrag(opts.cancelled);
1367
+ }
1368
+ function beginResize(entity, handle, worldX, worldY) {
1369
+ return _beginResize(entity, handle, worldX, worldY);
1370
+ }
1371
+ function updateResize(_entity, worldX, worldY) {
1372
+ _updateResize(worldX, worldY);
1373
+ }
1374
+ function endResize(_entity, opts) {
1375
+ _endResize(opts.cancelled);
1376
+ }
1377
+ function beginMarquee(worldX, worldY) {
1378
+ _beginMarquee(worldX, worldY);
1379
+ }
1380
+ function updateMarquee(worldX, worldY) {
1381
+ _updateMarquee(worldX, worldY);
1382
+ }
1383
+ function endMarquee() {
1384
+ _endMarquee();
1385
+ }
1386
+ function isMarqueeActive() {
1387
+ return inputState.mode === "marquee";
1388
+ }
1389
+ function getDraggingEntity() {
1390
+ return inputState.mode === "dragging" ? inputState.entityId : null;
1391
+ }
1392
+ function isResizing() {
1393
+ return inputState.mode === "resizing";
1394
+ }
1395
+ function getResizingEntity() {
1396
+ return inputState.mode === "resizing" ? inputState.entityId : null;
1397
+ }
1398
+ function setHoveredEntity(entity) {
1399
+ if (entity === hoveredEntity) return;
1400
+ hoveredEntity = entity;
1401
+ hoveredHandle = null;
1402
+ markDirty();
1403
+ }
1404
+ /**
1405
+ * Update hover state from a screen-space pointer position. Runs the
1406
+ * full hit-test so both `hoveredEntity` and `hoveredHandle` (RFC-005
1407
+ * resize hotspot under the cursor, if any) reflect the current pixel.
1408
+ *
1409
+ * Gated on `idle` mode — hover doesn't refresh during drag / resize /
1410
+ * marquee / fly-back (matches the v1 `handlePointerMove` idle branch).
1411
+ *
1412
+ * Called from the InputManager pipeline's `move` engine handler on
1413
+ * every pointermove: HoverRecognizer's `hover-enter` / `hover-leave`
1414
+ * events only fire on entity transitions, but the cursor needs to
1415
+ * change between resize handles within the same entity, so this path
1416
+ * runs the hit-test per move.
1417
+ */
1418
+ function updateHover(screenX, screenY) {
1419
+ if (inputState.mode !== "idle") return;
1420
+ const hit = hitTest(screenX, screenY);
1421
+ const target = hit ? hit.entityId : null;
1422
+ const handle = hit?.role.role.type === "resize" ? hit.role.role.handle : null;
1423
+ if (target === hoveredEntity && handle === hoveredHandle) return;
1424
+ hoveredEntity = target;
1425
+ hoveredHandle = handle;
1426
+ markDirty();
1427
+ }
1428
+ /**
1429
+ * Cancel any active interaction state (drag / resize / marquee /
1430
+ * fly-back / tracking). Public surface for the InputManager pipeline's
1431
+ * `cancel` engine handler — `endDrag(.., {cancelled:true})` only handles
1432
+ * `dragging`, but a native `pointercancel` can land in any of the other
1433
+ * mid-gesture modes. `_cancelAll` covers them all.
1434
+ */
1435
+ function cancelInteraction() {
1436
+ _cancelAll();
1437
+ }
1438
+ return {
1439
+ handlePointerDown,
1440
+ handlePointerMove,
1441
+ handlePointerUp,
1442
+ handlePointerCancel,
1443
+ runCursorSystem,
1444
+ runFlyBackSystem,
1445
+ selectEntity,
1446
+ clearSelection,
1447
+ getHoveredEntity: () => hoveredEntity,
1448
+ setHoveredEntity,
1449
+ updateHover,
1450
+ getSnapGuides: () => currentSnap.guides,
1451
+ getEqualSpacing: () => currentSnap.spacings,
1452
+ /**
1453
+ * Topmost interactable entity under a screen-space point, or null
1454
+ * if nothing's there. Same hit-test the pointer state machine
1455
+ * uses, exposed for callers that need to resolve coords to an
1456
+ * entity without entering the state machine — used by
1457
+ * `installEngineHandlers` (tap, drag-start, double-tap routing) and
1458
+ * the InputManager dispatch loop (surface routing for R3F widgets).
1459
+ */
1460
+ pickAt: (screenX, screenY) => hitTest(screenX, screenY)?.entityId ?? null,
1461
+ beginDrag,
1462
+ updateDrag,
1463
+ endDrag,
1464
+ cancelInteraction,
1465
+ beginResize,
1466
+ updateResize,
1467
+ endResize,
1468
+ isResizing,
1469
+ getResizingEntity,
1470
+ /**
1471
+ * Rich variant of `pickAt` — returns the entity AND its
1472
+ * `InteractionRoleData` (role + layer) at a screen-space point.
1473
+ * Used by `installEngineHandlers` to role-branch on `drag-start`
1474
+ * (resize → `beginResize`, drag → `beginDrag`, etc.). External
1475
+ * callers that only need the entity id should use `pickAt`.
1476
+ */
1477
+ hitTest,
1478
+ beginMarquee,
1479
+ updateMarquee,
1480
+ endMarquee,
1481
+ isMarqueeActive,
1482
+ getDraggingEntity
1483
+ };
1484
+ }
1485
+ //#endregion
1486
+ //#region src/ecs/engine/widget-binding.ts
1487
+ function createWidgetRegistry(defs = []) {
1488
+ const map = /* @__PURE__ */ new Map();
1489
+ for (const def of defs) map.set(def.type, def);
1490
+ return {
1491
+ register(def) {
1492
+ map.set(def.type, def);
1493
+ },
1494
+ get(type) {
1495
+ return map.get(type) ?? null;
1496
+ },
1497
+ getAll() {
1498
+ return [...map.values()];
1499
+ }
1500
+ };
1501
+ }
1502
+ //#endregion
1503
+ //#region src/ecs/engine/LayoutEngine.ts
1504
+ /**
1505
+ * Creates a new LayoutEngine instance with the given configuration.
1506
+ * This is the main entry point for the infinite canvas library.
1507
+ */
1508
+ function createLayoutEngine(config) {
1509
+ const world = (0, _jamesyong42_reactive_ecs.createWorld)();
1510
+ const scheduler = new _jamesyong42_reactive_ecs.SystemScheduler();
1511
+ const spatialIndex = new require_hooks.SpatialIndex();
1512
+ const profiler = new require_hooks.Profiler();
1513
+ scheduler.profiler = profiler;
1514
+ world.setResource(require_ecs.SpatialIndexResource, { instance: spatialIndex });
1515
+ const commandBuffer = new CommandBuffer();
1516
+ const widgetRegistry = createWidgetRegistry();
1517
+ const archetypeRegistry = createArchetypeRegistry();
1518
+ if (config?.zoom) world.setResource(require_ecs.ZoomConfigResource, config.zoom);
1519
+ if (config?.breakpoints) world.setResource(require_ecs.BreakpointConfigResource, config.breakpoints);
1520
+ if (config?.cardPresets) {
1521
+ const current = world.getResource(require_ecs.CardPresetsResource);
1522
+ world.setResource(require_ecs.CardPresetsResource, {
1523
+ presets: {
1524
+ ...current.presets,
1525
+ ...config.cardPresets.presets
1526
+ },
1527
+ gap: config.cardPresets.gap ?? current.gap
1528
+ });
1529
+ }
1530
+ let snapEnabled = config?.snap?.enabled ?? true;
1531
+ let snapThreshold = config?.snap?.threshold ?? 5;
1532
+ let snapGuidesVisible = config?.snap?.guidesVisible ?? true;
1533
+ scheduler.register(cardSystem);
1534
+ scheduler.register(transformTweenSystem);
1535
+ scheduler.register(navigationFilterSystem);
1536
+ scheduler.register(cullSystem);
1537
+ scheduler.register(breakpointSystem);
1538
+ scheduler.register(sortSystem);
1539
+ scheduler.register(dragPromoteSystem);
1540
+ const unsubscribers = [];
1541
+ unsubscribers.push(world.onComponentChanged(require_ecs.Transform2D, (entityId, _prev, t) => {
1542
+ if (t) spatialIndex.upsert(entityId, rectToAABB(t));
1543
+ }));
1544
+ unsubscribers.push(world.onEntityDestroyed((entity) => {
1545
+ spatialIndex.remove(entity);
1546
+ }));
1547
+ unsubscribers.push(world.onComponentChanged(require_ecs.Container, (entityId, prev, next) => {
1548
+ if (prev === void 0 && next !== void 0) {
1549
+ if (!world.hasComponent(entityId, require_ecs.ContainerCamera)) world.addComponent(entityId, require_ecs.ContainerCamera, {
1550
+ x: 0,
1551
+ y: 0,
1552
+ zoom: 1
1553
+ });
1554
+ }
1555
+ }));
1556
+ unsubscribers.push(world.onComponentChanged(require_ecs.ParentFrame, (entityId) => {
1557
+ reconcileEntityActive(world, entityId);
1558
+ markDirtyInternal();
1559
+ }));
1560
+ function refreshInteractionRole(entity) {
1561
+ const current = world.getComponent(entity, require_ecs.InteractionRole);
1562
+ if (current && current.role.type !== "drag" && current.role.type !== "select" && current.role.type !== "canvas") return;
1563
+ const hasDraggable = world.hasTag(entity, require_ecs.Draggable);
1564
+ const hasSelectable = world.hasTag(entity, require_ecs.Selectable);
1565
+ const desiredRole = hasDraggable ? { type: "drag" } : hasSelectable ? { type: "select" } : null;
1566
+ if (desiredRole === null) {
1567
+ if (current) world.removeComponent(entity, require_ecs.InteractionRole);
1568
+ if (world.hasComponent(entity, require_ecs.CursorHint)) world.removeComponent(entity, require_ecs.CursorHint);
1569
+ return;
1570
+ }
1571
+ if (!current) world.addComponent(entity, require_ecs.InteractionRole, {
1572
+ layer: 5,
1573
+ role: desiredRole
1574
+ });
1575
+ else if (current.role.type !== desiredRole.type) world.setComponent(entity, require_ecs.InteractionRole, { role: desiredRole });
1576
+ if (desiredRole.type === "drag" && !world.hasComponent(entity, require_ecs.CursorHint)) world.addComponent(entity, require_ecs.CursorHint, {
1577
+ hover: "grab",
1578
+ active: "grabbing"
1579
+ });
1580
+ }
1581
+ unsubscribers.push(world.onTagAdded(require_ecs.Draggable, refreshInteractionRole));
1582
+ unsubscribers.push(world.onTagRemoved(require_ecs.Draggable, refreshInteractionRole));
1583
+ unsubscribers.push(world.onTagAdded(require_ecs.Selectable, refreshInteractionRole));
1584
+ unsubscribers.push(world.onTagRemoved(require_ecs.Selectable, refreshInteractionRole));
1585
+ if (config?.widgets) for (const w of config.widgets) widgetRegistry.register(w);
1586
+ if (config?.archetypes) for (const a of config.archetypes) archetypeRegistry.register(a);
1587
+ world.setResource(require_ecs.NavigationStackResource, { changed: true });
1588
+ let dirty = false;
1589
+ let cameraChangedThisTick = false;
1590
+ let selectionChangedThisTick = false;
1591
+ let prevVisible = /* @__PURE__ */ new Set();
1592
+ let currentVisible = [];
1593
+ let frameChanges = {
1594
+ positionsChanged: [],
1595
+ breakpointsChanged: [],
1596
+ zIndicesChanged: [],
1597
+ entered: [],
1598
+ exited: [],
1599
+ cameraChanged: false,
1600
+ navigationChanged: false,
1601
+ selectionChanged: false,
1602
+ layersChanged: false
1603
+ };
1604
+ function markDirtyInternal() {
1605
+ dirty = true;
1606
+ }
1607
+ const interaction = createInteractionRuntime({
1608
+ world,
1609
+ spatialIndex,
1610
+ commandBuffer,
1611
+ markDirty: markDirtyInternal,
1612
+ notifySelectionChanged: () => {
1613
+ selectionChangedThisTick = true;
1614
+ },
1615
+ getSnapEnabled: () => snapEnabled,
1616
+ getSnapThreshold: () => snapThreshold,
1617
+ getWidgetInteraction: (type) => widgetRegistry.get(type)?.interaction
1618
+ });
1619
+ const engine = {
1620
+ world,
1621
+ createEntity(inits) {
1622
+ const entity = world.createEntity();
1623
+ if (inits) for (const init of inits) {
1624
+ const type = init[0];
1625
+ if (type.__kind === "tag") world.addTag(entity, type);
1626
+ else world.addComponent(entity, type, init[1] ?? {});
1627
+ }
1628
+ markDirtyInternal();
1629
+ return entity;
1630
+ },
1631
+ spawn(id, opts = {}) {
1632
+ const archetype = archetypeRegistry.get(id);
1633
+ const widgetTypeId = archetype?.widget ?? id;
1634
+ const widget = widgetRegistry.get(widgetTypeId);
1635
+ const surface = widget?.surface ?? "dom";
1636
+ const defaultData = widget?.defaultData ?? {};
1637
+ const defaultSize = archetype?.defaultSize ?? widget?.defaultSize ?? {
1638
+ width: 100,
1639
+ height: 100
1640
+ };
1641
+ const position = opts.at ?? {
1642
+ x: 0,
1643
+ y: 0
1644
+ };
1645
+ const size = opts.size ?? defaultSize;
1646
+ const data = {
1647
+ ...defaultData,
1648
+ ...opts.data
1649
+ };
1650
+ const inits = [
1651
+ [require_ecs.Transform2D, {
1652
+ x: position.x,
1653
+ y: position.y,
1654
+ width: size.width,
1655
+ height: size.height,
1656
+ rotation: opts.rotation ?? 0
1657
+ }],
1658
+ [require_ecs.Widget, {
1659
+ surface,
1660
+ type: widgetTypeId
1661
+ }],
1662
+ [require_ecs.WidgetData, { data }],
1663
+ [require_ecs.ZIndex, { value: opts.zIndex ?? 0 }]
1664
+ ];
1665
+ if (archetype?.components) for (const init of archetype.components) inits.push(init);
1666
+ if (opts.parent !== void 0) inits.push([require_ecs.ParentFrame, { id: opts.parent }]);
1667
+ const interactiveConfig = archetype?.interactive;
1668
+ const caps = interactiveConfig === false ? {
1669
+ selectable: false,
1670
+ draggable: false,
1671
+ resizable: false,
1672
+ selectionFrame: false,
1673
+ snapSource: false,
1674
+ snapTarget: false
1675
+ } : interactiveConfig === void 0 || interactiveConfig === true ? {
1676
+ selectable: true,
1677
+ draggable: true,
1678
+ resizable: true,
1679
+ selectionFrame: true,
1680
+ snapSource: true,
1681
+ snapTarget: true
1682
+ } : (() => {
1683
+ const selectable = interactiveConfig.selectable ?? false;
1684
+ return {
1685
+ selectable,
1686
+ draggable: interactiveConfig.draggable ?? false,
1687
+ resizable: interactiveConfig.resizable ?? false,
1688
+ selectionFrame: interactiveConfig.selectionFrame ?? selectable,
1689
+ snapSource: interactiveConfig.snapSource ?? false,
1690
+ snapTarget: interactiveConfig.snapTarget ?? false
1691
+ };
1692
+ })();
1693
+ if (caps.selectable) inits.push([require_ecs.Selectable]);
1694
+ if (caps.draggable) inits.push([require_ecs.Draggable]);
1695
+ if (caps.resizable) inits.push([require_ecs.Resizable]);
1696
+ if (caps.selectionFrame) inits.push([require_ecs.SelectionFrame]);
1697
+ if (caps.snapSource) inits.push([require_ecs.SnapSource]);
1698
+ if (caps.snapTarget) inits.push([require_ecs.SnapTarget]);
1699
+ if (archetype?.tags) for (const tag of archetype.tags) inits.push([tag]);
1700
+ const entity = engine.createEntity(inits);
1701
+ if (opts.parent !== void 0 && world.hasComponent(opts.parent, require_ecs.ContainerChildren)) {
1702
+ const current = world.getComponent(opts.parent, require_ecs.ContainerChildren);
1703
+ if (current && !current.ids.includes(entity)) world.setComponent(opts.parent, require_ecs.ContainerChildren, { ids: [...current.ids, entity] });
1704
+ }
1705
+ return entity;
1706
+ },
1707
+ spawnAtCameraCenter(id, opts = {}) {
1708
+ const camera = world.getResource(require_ecs.CameraResource);
1709
+ const viewport = world.getResource(require_ecs.ViewportResource);
1710
+ const centerX = camera.x + viewport.width / (2 * camera.zoom);
1711
+ const centerY = camera.y + viewport.height / (2 * camera.zoom);
1712
+ const archetype = archetypeRegistry.get(id);
1713
+ const widget = widgetRegistry.get(archetype?.widget ?? id);
1714
+ const size = opts.size ?? archetype?.defaultSize ?? widget?.defaultSize ?? {
1715
+ width: 100,
1716
+ height: 100
1717
+ };
1718
+ return engine.spawn(id, {
1719
+ ...opts,
1720
+ at: {
1721
+ x: centerX - size.width / 2,
1722
+ y: centerY - size.height / 2
1723
+ }
1724
+ });
1725
+ },
1726
+ registerWidget(widget) {
1727
+ widgetRegistry.register(widget);
1728
+ },
1729
+ getWidget(type) {
1730
+ return widgetRegistry.get(type);
1731
+ },
1732
+ getWidgets() {
1733
+ return widgetRegistry.getAll();
1734
+ },
1735
+ registerArchetype(archetype) {
1736
+ archetypeRegistry.register(archetype);
1737
+ },
1738
+ getArchetype(id) {
1739
+ return archetypeRegistry.get(id);
1740
+ },
1741
+ destroyEntity(id) {
1742
+ spatialIndex.remove(id);
1743
+ world.destroyEntity(id);
1744
+ markDirtyInternal();
1745
+ },
1746
+ get(entity, type) {
1747
+ return world.getComponent(entity, type);
1748
+ },
1749
+ set(entity, type, data) {
1750
+ world.setComponent(entity, type, data);
1751
+ markDirtyInternal();
1752
+ },
1753
+ has(entity, type) {
1754
+ if (type.__kind === "tag") return world.hasTag(entity, type);
1755
+ return world.hasComponent(entity, type);
1756
+ },
1757
+ addComponent(entity, type, data) {
1758
+ world.addComponent(entity, type, data ?? type.defaults);
1759
+ markDirtyInternal();
1760
+ },
1761
+ removeComponent(entity, type) {
1762
+ world.removeComponent(entity, type);
1763
+ markDirtyInternal();
1764
+ },
1765
+ addTag(entity, type) {
1766
+ world.addTag(entity, type);
1767
+ markDirtyInternal();
1768
+ },
1769
+ removeTag(entity, type) {
1770
+ world.removeTag(entity, type);
1771
+ markDirtyInternal();
1772
+ },
1773
+ getSchemaFor(entity) {
1774
+ const w = world.getComponent(entity, require_ecs.Widget);
1775
+ if (!w) return void 0;
1776
+ return widgetRegistry.get(w.type)?.schema;
1777
+ },
1778
+ registerSystem(system) {
1779
+ scheduler.register(system);
1780
+ },
1781
+ removeSystem(name) {
1782
+ scheduler.remove(name);
1783
+ },
1784
+ getCamera() {
1785
+ return world.getResource(require_ecs.CameraResource);
1786
+ },
1787
+ panBy(dx, dy) {
1788
+ const camera = world.getResource(require_ecs.CameraResource);
1789
+ camera.x -= dx / camera.zoom;
1790
+ camera.y -= dy / camera.zoom;
1791
+ cameraChangedThisTick = true;
1792
+ markDirtyInternal();
1793
+ },
1794
+ panTo(worldX, worldY) {
1795
+ const camera = world.getResource(require_ecs.CameraResource);
1796
+ const viewport = world.getResource(require_ecs.ViewportResource);
1797
+ camera.x = worldX - viewport.width / (2 * camera.zoom);
1798
+ camera.y = worldY - viewport.height / (2 * camera.zoom);
1799
+ cameraChangedThisTick = true;
1800
+ markDirtyInternal();
1801
+ },
1802
+ zoomAtPoint(screenX, screenY, delta) {
1803
+ const camera = world.getResource(require_ecs.CameraResource);
1804
+ const zoomConfig = world.getResource(require_ecs.ZoomConfigResource);
1805
+ const worldBefore = screenToWorld(screenX, screenY, camera);
1806
+ const newZoom = clamp(camera.zoom * (1 + delta), zoomConfig.min, zoomConfig.max);
1807
+ camera.zoom = newZoom;
1808
+ camera.x = worldBefore.x - screenX / newZoom;
1809
+ camera.y = worldBefore.y - screenY / newZoom;
1810
+ cameraChangedThisTick = true;
1811
+ markDirtyInternal();
1812
+ },
1813
+ zoomTo(zoom) {
1814
+ const camera = world.getResource(require_ecs.CameraResource);
1815
+ const zoomConfig = world.getResource(require_ecs.ZoomConfigResource);
1816
+ const viewport = world.getResource(require_ecs.ViewportResource);
1817
+ const centerWorldX = camera.x + viewport.width / (2 * camera.zoom);
1818
+ const centerWorldY = camera.y + viewport.height / (2 * camera.zoom);
1819
+ camera.zoom = clamp(zoom, zoomConfig.min, zoomConfig.max);
1820
+ camera.x = centerWorldX - viewport.width / (2 * camera.zoom);
1821
+ camera.y = centerWorldY - viewport.height / (2 * camera.zoom);
1822
+ cameraChangedThisTick = true;
1823
+ markDirtyInternal();
1824
+ },
1825
+ /**
1826
+ * Toggle the camera's `gesturing` flag. Called by gesture handlers
1827
+ * (wheel debounced, touch pinch / pan start+end) so render layers
1828
+ * can defer expensive work — e.g. the R3F compositor skips zoom-band
1829
+ * repaints while gesturing is true so a continuous pinch doesn't
1830
+ * trigger a repaint storm across every visible widget.
1831
+ */
1832
+ setGesturing(active) {
1833
+ const camera = world.getResource(require_ecs.CameraResource);
1834
+ if (camera.gesturing === active) return;
1835
+ camera.gesturing = active;
1836
+ cameraChangedThisTick = true;
1837
+ markDirtyInternal();
1838
+ },
1839
+ zoomToFit(entityIds, padding = 50) {
1840
+ const viewport = world.getResource(require_ecs.ViewportResource);
1841
+ if (viewport.width === 0) return;
1842
+ const entities = entityIds ?? world.queryTagged(require_ecs.Active);
1843
+ if (entities.length === 0) return;
1844
+ let minX = Number.POSITIVE_INFINITY;
1845
+ let minY = Number.POSITIVE_INFINITY;
1846
+ let maxX = Number.NEGATIVE_INFINITY;
1847
+ let maxY = Number.NEGATIVE_INFINITY;
1848
+ for (const e of entities) {
1849
+ const t = world.getComponent(e, require_ecs.Transform2D);
1850
+ if (!t) continue;
1851
+ minX = Math.min(minX, t.x);
1852
+ minY = Math.min(minY, t.y);
1853
+ maxX = Math.max(maxX, t.x + t.width);
1854
+ maxY = Math.max(maxY, t.y + t.height);
1855
+ }
1856
+ if (!Number.isFinite(minX)) return;
1857
+ const contentWidth = maxX - minX + padding * 2;
1858
+ const contentHeight = maxY - minY + padding * 2;
1859
+ const zoomConfig = world.getResource(require_ecs.ZoomConfigResource);
1860
+ const zoom = clamp(Math.min(viewport.width / contentWidth, viewport.height / contentHeight), zoomConfig.min, zoomConfig.max);
1861
+ const camera = world.getResource(require_ecs.CameraResource);
1862
+ camera.zoom = zoom;
1863
+ camera.x = minX - padding - (viewport.width / zoom - contentWidth) / 2;
1864
+ camera.y = minY - padding - (viewport.height / zoom - contentHeight) / 2;
1865
+ cameraChangedThisTick = true;
1866
+ markDirtyInternal();
1867
+ },
1868
+ setViewport(width, height, dpr) {
1869
+ world.setResource(require_ecs.ViewportResource, {
1870
+ width,
1871
+ height,
1872
+ dpr: dpr ?? 1
1873
+ });
1874
+ markDirtyInternal();
1875
+ },
1876
+ execute(command) {
1877
+ commandBuffer.execute(command, world);
1878
+ markDirtyInternal();
1879
+ },
1880
+ beginCommandGroup() {
1881
+ commandBuffer.beginGroup();
1882
+ },
1883
+ endCommandGroup() {
1884
+ commandBuffer.endGroup();
1885
+ },
1886
+ undo() {
1887
+ const did = commandBuffer.undo(world);
1888
+ if (did) markDirtyInternal();
1889
+ return did;
1890
+ },
1891
+ redo() {
1892
+ const did = commandBuffer.redo(world);
1893
+ if (did) markDirtyInternal();
1894
+ return did;
1895
+ },
1896
+ canUndo() {
1897
+ return commandBuffer.canUndo();
1898
+ },
1899
+ canRedo() {
1900
+ return commandBuffer.canRedo();
1901
+ },
1902
+ handlePointerDown(screenX, screenY, button, modifiers) {
1903
+ return interaction.handlePointerDown(screenX, screenY, button, modifiers);
1904
+ },
1905
+ handlePointerMove(screenX, screenY, modifiers) {
1906
+ return interaction.handlePointerMove(screenX, screenY, modifiers);
1907
+ },
1908
+ handlePointerUp() {
1909
+ return interaction.handlePointerUp();
1910
+ },
1911
+ handlePointerCancel() {
1912
+ interaction.handlePointerCancel();
1913
+ },
1914
+ pickAt(screenX, screenY) {
1915
+ return interaction.pickAt(screenX, screenY);
1916
+ },
1917
+ hitTest(screenX, screenY) {
1918
+ return interaction.hitTest(screenX, screenY);
1919
+ },
1920
+ beginDrag(entity, worldX, worldY) {
1921
+ interaction.beginDrag(entity, worldX, worldY);
1922
+ },
1923
+ updateDrag(entity, worldX, worldY) {
1924
+ interaction.updateDrag(entity, worldX, worldY);
1925
+ },
1926
+ endDrag(entity, opts) {
1927
+ interaction.endDrag(entity, opts);
1928
+ },
1929
+ getDraggingEntity() {
1930
+ return interaction.getDraggingEntity();
1931
+ },
1932
+ cancelInteraction() {
1933
+ interaction.cancelInteraction();
1934
+ },
1935
+ beginResize(entity, handle, worldX, worldY) {
1936
+ return interaction.beginResize(entity, handle, worldX, worldY);
1937
+ },
1938
+ updateResize(entity, worldX, worldY) {
1939
+ interaction.updateResize(entity, worldX, worldY);
1940
+ },
1941
+ endResize(entity, opts) {
1942
+ interaction.endResize(entity, opts);
1943
+ },
1944
+ isResizing() {
1945
+ return interaction.isResizing();
1946
+ },
1947
+ getResizingEntity() {
1948
+ return interaction.getResizingEntity();
1949
+ },
1950
+ beginMarquee(worldX, worldY) {
1951
+ interaction.beginMarquee(worldX, worldY);
1952
+ },
1953
+ updateMarquee(worldX, worldY) {
1954
+ interaction.updateMarquee(worldX, worldY);
1955
+ },
1956
+ endMarquee() {
1957
+ interaction.endMarquee();
1958
+ },
1959
+ isMarqueeActive() {
1960
+ return interaction.isMarqueeActive();
1961
+ },
1962
+ getSelectedEntities() {
1963
+ return world.queryTagged(require_ecs.Selected);
1964
+ },
1965
+ selectEntity(entity, additive) {
1966
+ interaction.selectEntity(entity, additive);
1967
+ },
1968
+ clearSelection() {
1969
+ interaction.clearSelection();
1970
+ },
1971
+ getHoveredEntity() {
1972
+ return interaction.getHoveredEntity();
1973
+ },
1974
+ setHoveredEntity(entity) {
1975
+ interaction.setHoveredEntity(entity);
1976
+ },
1977
+ updateHover(screenX, screenY) {
1978
+ interaction.updateHover(screenX, screenY);
1979
+ },
1980
+ getSnapGuides() {
1981
+ return interaction.getSnapGuides();
1982
+ },
1983
+ getEqualSpacing() {
1984
+ return interaction.getEqualSpacing();
1985
+ },
1986
+ setSnapEnabled(on) {
1987
+ snapEnabled = on;
1988
+ markDirtyInternal();
1989
+ },
1990
+ setSnapThreshold(worldPx) {
1991
+ snapThreshold = worldPx;
1992
+ markDirtyInternal();
1993
+ },
1994
+ getSnapGuidesVisible() {
1995
+ return snapGuidesVisible;
1996
+ },
1997
+ setSnapGuidesVisible(on) {
1998
+ snapGuidesVisible = on;
1999
+ markDirtyInternal();
2000
+ },
2001
+ enterContainer(entity) {
2002
+ if (!world.hasComponent(entity, require_ecs.Container)) return;
2003
+ const navStack = world.getResource(require_ecs.NavigationStackResource);
2004
+ const camera = world.getResource(require_ecs.CameraResource);
2005
+ const outgoing = navStack.frames[navStack.frames.length - 1].containerId;
2006
+ if (outgoing === null) world.setResource(require_ecs.RootCameraResource, {
2007
+ x: camera.x,
2008
+ y: camera.y,
2009
+ zoom: camera.zoom
2010
+ });
2011
+ else world.setComponent(outgoing, require_ecs.ContainerCamera, {
2012
+ x: camera.x,
2013
+ y: camera.y,
2014
+ zoom: camera.zoom
2015
+ });
2016
+ navStack.frames.push({ containerId: entity });
2017
+ navStack.changed = true;
2018
+ const incoming = world.getComponent(entity, require_ecs.ContainerCamera) ?? {
2019
+ x: 0,
2020
+ y: 0,
2021
+ zoom: 1
2022
+ };
2023
+ camera.x = incoming.x;
2024
+ camera.y = incoming.y;
2025
+ camera.zoom = incoming.zoom;
2026
+ interaction.clearSelection();
2027
+ cameraChangedThisTick = true;
2028
+ markDirtyInternal();
2029
+ },
2030
+ exitContainer() {
2031
+ const navStack = world.getResource(require_ecs.NavigationStackResource);
2032
+ if (navStack.frames.length <= 1) return;
2033
+ const camera = world.getResource(require_ecs.CameraResource);
2034
+ const outgoing = navStack.frames[navStack.frames.length - 1].containerId;
2035
+ if (outgoing !== null) world.setComponent(outgoing, require_ecs.ContainerCamera, {
2036
+ x: camera.x,
2037
+ y: camera.y,
2038
+ zoom: camera.zoom
2039
+ });
2040
+ navStack.frames.pop();
2041
+ navStack.changed = true;
2042
+ const parent = navStack.frames[navStack.frames.length - 1].containerId;
2043
+ const incoming = parent === null ? world.getResource(require_ecs.RootCameraResource) : world.getComponent(parent, require_ecs.ContainerCamera) ?? {
2044
+ x: 0,
2045
+ y: 0,
2046
+ zoom: 1
2047
+ };
2048
+ camera.x = incoming.x;
2049
+ camera.y = incoming.y;
2050
+ camera.zoom = incoming.zoom;
2051
+ interaction.clearSelection();
2052
+ cameraChangedThisTick = true;
2053
+ markDirtyInternal();
2054
+ },
2055
+ getActiveContainer() {
2056
+ const navStack = world.getResource(require_ecs.NavigationStackResource);
2057
+ return navStack.frames[navStack.frames.length - 1].containerId;
2058
+ },
2059
+ getNavigationDepth() {
2060
+ return world.getResource(require_ecs.NavigationStackResource).frames.length - 1;
2061
+ },
2062
+ markDirty() {
2063
+ markDirtyInternal();
2064
+ },
2065
+ profiler,
2066
+ tick() {
2067
+ profiler.beginFrame(world.currentTick);
2068
+ const navigationChangedThisTick = world.getResource(require_ecs.NavigationStackResource)?.changed ?? false;
2069
+ scheduler.execute(world);
2070
+ interaction.runFlyBackSystem();
2071
+ interaction.runCursorSystem();
2072
+ profiler.beginVisibility();
2073
+ const newVisible = [];
2074
+ const newVisibleSet = /* @__PURE__ */ new Set();
2075
+ for (const entity of world.query(require_ecs.Widget, require_ecs.Visible)) {
2076
+ const t = world.getComponent(entity, require_ecs.Transform2D);
2077
+ const widget = world.getComponent(entity, require_ecs.Widget);
2078
+ const bp = world.getComponent(entity, require_ecs.WidgetBreakpoint);
2079
+ const zIdx = world.getComponent(entity, require_ecs.ZIndex);
2080
+ if (!t || !widget) continue;
2081
+ newVisibleSet.add(entity);
2082
+ newVisible.push({
2083
+ entityId: entity,
2084
+ x: t.x,
2085
+ y: t.y,
2086
+ width: t.width,
2087
+ height: t.height,
2088
+ breakpoint: bp?.current ?? "normal",
2089
+ zIndex: zIdx?.value ?? 0,
2090
+ surface: widget.surface,
2091
+ widgetType: widget.type
2092
+ });
2093
+ }
2094
+ newVisible.sort((a, b) => a.zIndex - b.zIndex);
2095
+ profiler.endVisibility();
2096
+ const entered = [];
2097
+ const exited = [];
2098
+ for (const entity of newVisibleSet) if (!prevVisible.has(entity)) entered.push(entity);
2099
+ for (const entity of prevVisible) if (!newVisibleSet.has(entity)) exited.push(entity);
2100
+ frameChanges = {
2101
+ positionsChanged: world.queryChanged(require_ecs.Transform2D),
2102
+ breakpointsChanged: world.queryChanged(require_ecs.WidgetBreakpoint),
2103
+ zIndicesChanged: world.queryChanged(require_ecs.ZIndex),
2104
+ entered,
2105
+ exited,
2106
+ cameraChanged: cameraChangedThisTick,
2107
+ navigationChanged: navigationChangedThisTick,
2108
+ selectionChanged: selectionChangedThisTick,
2109
+ layersChanged: world.queryChanged(require_ecs.Layer).length > 0
2110
+ };
2111
+ currentVisible = newVisible;
2112
+ prevVisible = newVisibleSet;
2113
+ cameraChangedThisTick = false;
2114
+ selectionChangedThisTick = false;
2115
+ profiler.endFrame(world.entityCount, newVisible.length);
2116
+ world.clearDirty();
2117
+ world.incrementTick();
2118
+ world.emitFrame();
2119
+ dirty = false;
2120
+ for (const _ of world.query(require_ecs.TransformTween)) {
2121
+ dirty = true;
2122
+ break;
2123
+ }
2124
+ },
2125
+ flushIfDirty() {
2126
+ if (!dirty) return false;
2127
+ engine.tick();
2128
+ return true;
2129
+ },
2130
+ getVisibleEntities() {
2131
+ return currentVisible;
2132
+ },
2133
+ getFrameChanges() {
2134
+ return frameChanges;
2135
+ },
2136
+ getSpatialIndex() {
2137
+ return spatialIndex;
2138
+ },
2139
+ onFrame(handler) {
2140
+ return world.onFrame(handler);
2141
+ },
2142
+ destroy() {
2143
+ for (const unsub of unsubscribers) unsub();
2144
+ unsubscribers.length = 0;
2145
+ commandBuffer.clear();
2146
+ profiler.setEnabled(false);
2147
+ profiler.clear();
2148
+ spatialIndex.clear();
2149
+ }
2150
+ };
2151
+ return engine;
2152
+ }
2153
+ //#endregion
2154
+ //#region src/ecs/hierarchy.ts
2155
+ /**
2156
+ * Walks the `ParentFrame` chain from `descendant` upward and returns
2157
+ * true iff `candidate` appears on that chain (inclusive check excluded:
2158
+ * an entity is not considered its own ancestor).
2159
+ *
2160
+ * Used by `CardContainer`'s default `canAccept` as a cycle-guard so
2161
+ * dragging a container onto one of its own descendants is rejected
2162
+ * before any mutation fires (RFC-004 § Phase 5). Cheap — the chain is
2163
+ * typically 0–3 deep. Capped at a sane depth to defend against
2164
+ * pathological / malformed worlds.
2165
+ */
2166
+ function isFrameAncestorOf(world, candidate, descendant) {
2167
+ if (candidate === descendant) return false;
2168
+ let current = descendant;
2169
+ for (let i = 0; i < 64; i++) {
2170
+ const pf = world.getComponent(current, require_ecs.ParentFrame);
2171
+ if (!pf) return false;
2172
+ if (pf.id === candidate) return true;
2173
+ current = pf.id;
2174
+ }
2175
+ return false;
2176
+ }
2177
+ //#endregion
2178
+ //#region src/react/hooks/widget.ts
10
2179
  /**
11
2180
  * Returns the custom data attached to a widget entity.
12
2181
  * Use the generic parameter for type safety: `useWidgetData<MyData>(entityId)`. Re-renders when data changes.
13
2182
  */
14
2183
  function useWidgetData(entityId) {
15
- return require_hooks.useComponent(entityId, require_hooks.WidgetData)?.data ?? {};
2184
+ return require_ecs.useComponent(entityId, require_ecs.WidgetData)?.data ?? {};
16
2185
  }
17
2186
  /**
18
2187
  * Returns the current responsive breakpoint for a widget based on its screen-space size.
19
2188
  * Re-renders when the breakpoint changes.
20
2189
  */
21
2190
  function useBreakpoint(entityId) {
22
- return require_hooks.useComponent(entityId, require_hooks.WidgetBreakpoint)?.current ?? "normal";
2191
+ return require_ecs.useComponent(entityId, require_ecs.WidgetBreakpoint)?.current ?? "normal";
23
2192
  }
24
2193
  /**
25
2194
  * Returns child entity IDs of a container entity.
26
2195
  * Re-renders when children are added or removed.
27
2196
  */
28
2197
  function useChildren(entityId) {
29
- return require_hooks.useComponent(entityId, require_hooks.Children)?.ids ?? [];
2198
+ return require_ecs.useComponent(entityId, require_ecs.Children)?.ids ?? [];
30
2199
  }
31
2200
  /**
32
2201
  * Returns whether the entity is currently selected.
33
2202
  * Re-renders when the entity's selection state changes.
34
2203
  */
35
2204
  function useIsSelected(entityId) {
36
- return require_hooks.useTag(entityId, require_hooks.Selected);
2205
+ return require_ecs.useTag(entityId, require_ecs.Selected);
37
2206
  }
38
2207
  /**
39
2208
  * Returns a function to update the widget's custom data.
40
2209
  * Merges the patch into existing data via shallow spread.
41
2210
  */
42
2211
  function useUpdateWidget(entityId) {
43
- const engine = require_hooks.useLayoutEngine();
2212
+ const engine = require_ecs.useLayoutEngine();
44
2213
  return (0, react.useCallback)((patch) => {
45
- const existing = engine.get(entityId, require_hooks.WidgetData);
46
- if (existing) engine.set(entityId, require_hooks.WidgetData, { data: {
2214
+ const existing = engine.get(entityId, require_ecs.WidgetData);
2215
+ if (existing) engine.set(entityId, require_ecs.WidgetData, { data: {
47
2216
  ...existing.data,
48
2217
  ...patch
49
2218
  } });
50
2219
  }, [engine, entityId]);
51
2220
  }
52
2221
  //#endregion
53
- //#region src/react/card.tsx
54
- /**
55
- * Built-in preset sizes, matching `CardPresetsResource` defaults.
56
- * Used by `createCardWidget` to set `defaultSize` at widget-registration
57
- * time (before the engine is constructed).
58
- */
59
- const DEFAULT_CARD_PRESET_SIZES$1 = {
60
- small: {
61
- width: 155,
62
- height: 155
63
- },
64
- medium: {
65
- width: 329,
66
- height: 155
67
- },
68
- large: {
69
- width: 329,
70
- height: 345
71
- },
72
- xl: {
73
- width: 329,
74
- height: 535
2222
+ //#region src/react/input/adapters/ClickAdapter.ts
2223
+ /**
2224
+ * Click / dblclick / contextmenu adapter (RFC-008 v6).
2225
+ *
2226
+ * The browser fires click / dblclick / contextmenu through a native event
2227
+ * channel that's distinct from `pointerdown` / `pointerup` — they're
2228
+ * synthesised after the pointer cycle completes (down → up with little
2229
+ * movement, double-click within a short window, right-click). Pre-v6 the
2230
+ * canvas had no adapter for these: R3F registered its own listeners on
2231
+ * the container via `EventManager.connect`, and the InputManager pipeline
2232
+ * never saw them. Two side effects:
2233
+ *
2234
+ * 1. A click on a mesh that called `setPointerCapture` skipped the
2235
+ * `tap` recognizer (capture-claim) so the engine's selection logic
2236
+ * didn't run — the widget didn't select.
2237
+ * 2. Clicks couldn't be observed by recognizers / handlers / external
2238
+ * listeners through the manager, only via direct R3F mesh handlers.
2239
+ *
2240
+ * v6 makes click / dblclick / contextmenu first-class `InputEvent`s. The
2241
+ * adapter dispatches them through `InputManager.dispatch`, which routes
2242
+ * to the surface router (R3FRouter for webgl widgets — invokes the
2243
+ * mesh's `onClick` etc.) and fires engine handlers (which run
2244
+ * `selectEntity` for click, `enterContainer` for dblclick).
2245
+ *
2246
+ * `preventDefault` discipline:
2247
+ * - `click` / `dblclick`: never. Browser focus / activation must run.
2248
+ * - `contextmenu`: always. Canvas isn't a place for the browser context
2249
+ * menu.
2250
+ *
2251
+ * Native-interactive skip mirrors `PointerAdapter`: a click landing on a
2252
+ * `<button>` / `<input>` / etc. inside a DOM widget is suppressed at the
2253
+ * adapter so the widget body doesn't get re-selected over the user's
2254
+ * intent (typing, button activation, etc.). Authors who want canvas-level
2255
+ * coexistence with their own native interactive call
2256
+ * `e.stopPropagation()` so the click never reaches the container.
2257
+ */
2258
+ var ClickAdapter = class {
2259
+ attach(container, manager) {
2260
+ const make = (type, e) => {
2261
+ const rect = container.getBoundingClientRect();
2262
+ const screen = {
2263
+ x: e.clientX - rect.left,
2264
+ y: e.clientY - rect.top
2265
+ };
2266
+ const camera = manager.engine.getCamera();
2267
+ const world = screenToWorld(screen.x, screen.y, camera);
2268
+ const button = e.button ?? null;
2269
+ return {
2270
+ type,
2271
+ source: clickSource(e),
2272
+ pointerId: pointerIdOf(e),
2273
+ primary: true,
2274
+ screen,
2275
+ world,
2276
+ button,
2277
+ modifiers: {
2278
+ shift: e.shiftKey,
2279
+ ctrl: e.ctrlKey,
2280
+ alt: e.altKey,
2281
+ meta: e.metaKey
2282
+ },
2283
+ timestamp: e.timeStamp,
2284
+ native: e
2285
+ };
2286
+ };
2287
+ const onClick = (e) => {
2288
+ const target = e.target;
2289
+ const targetTag = target ? `${target.tagName}${target.id ? `#${target.id}` : ""}` : "null";
2290
+ if (target?.closest("button, input, textarea, select, [contenteditable]")) {
2291
+ require_hooks.inputLog("Adapter", `click → SKIPPED (native interactive)`, { target: targetTag });
2292
+ return;
2293
+ }
2294
+ require_hooks.inputLog("Adapter", `click → InputManager`, {
2295
+ screen: {
2296
+ x: e.clientX - container.getBoundingClientRect().left,
2297
+ y: e.clientY - container.getBoundingClientRect().top
2298
+ },
2299
+ target: targetTag
2300
+ });
2301
+ manager.dispatch(make("click", e));
2302
+ };
2303
+ const onDblClick = (e) => {
2304
+ if (e.target?.closest("button, input, textarea, select, [contenteditable]")) return;
2305
+ require_hooks.inputLog("Adapter", `dblclick → InputManager`);
2306
+ manager.dispatch(make("dblclick", e));
2307
+ };
2308
+ const onContextMenu = (e) => {
2309
+ e.preventDefault();
2310
+ if (e.target?.closest("button, input, textarea, select, [contenteditable]")) return;
2311
+ require_hooks.inputLog("Adapter", `contextmenu → InputManager`);
2312
+ manager.dispatch(make("contextmenu", e));
2313
+ };
2314
+ container.addEventListener("click", onClick);
2315
+ container.addEventListener("dblclick", onDblClick);
2316
+ container.addEventListener("contextmenu", onContextMenu);
2317
+ return () => {
2318
+ container.removeEventListener("click", onClick);
2319
+ container.removeEventListener("dblclick", onDblClick);
2320
+ container.removeEventListener("contextmenu", onContextMenu);
2321
+ };
75
2322
  }
76
2323
  };
2324
+ function pointerIdOf(e) {
2325
+ const pe = e;
2326
+ return typeof pe.pointerId === "number" ? pe.pointerId : 0;
2327
+ }
2328
+ function clickSource(e) {
2329
+ switch (e.pointerType) {
2330
+ case "mouse": return "mouse";
2331
+ case "pen": return "pen";
2332
+ case "touch": return "touch";
2333
+ default: return "mouse";
2334
+ }
2335
+ }
2336
+ //#endregion
2337
+ //#region src/react/input/adapters/PointerAdapter.ts
77
2338
  /**
78
- * Visual chrome for an iOS-style card: rounded corners, hairline ring,
79
- * soft drop shadow, and a subtle lift (scale + stronger shadow) while
80
- * the entity carries the `Dragging` tag.
2339
+ * Native pointer-event adapter (RFC-008). Listens for pointerdown / move /
2340
+ * up / cancel / pointerleave on the canvas container, normalises into
2341
+ * `InputEvent`s, and dispatches via `manager.dispatch(...)`.
2342
+ *
2343
+ * Single-finger and multi-finger touch flow through this adapter via the
2344
+ * browser's touch-to-pointer synthesis (`touch-action: none` on the
2345
+ * container is what makes synthesis reliable). PinchRecognizer counts
2346
+ * simultaneous active touch-source pointer IDs from these events.
2347
+ *
2348
+ * `preventDefault` discipline:
2349
+ * - Pointer events: never. Bubble must continue so widget React handlers
2350
+ * fire and so widgets can call `setPointerCapture` to claim drags.
2351
+ * - `contextmenu`: handled by `ClickAdapter`, not here.
2352
+ *
2353
+ * `pointerdown` on a native interactive target (button, input, etc.) is
2354
+ * suppressed at the adapter so a button click inside a DOM widget can't
2355
+ * spawn a marquee or drag. Move / up / cancel still flow so hover chrome
2356
+ * stays accurate.
81
2357
  *
82
- * Uses CSS transitions no animation library dependency.
2358
+ * `pointerleave` is dispatched as its own InputEvent type so engine hover
2359
+ * chrome can clear when the cursor exits the canvas — without an inline
2360
+ * listener outside the InputManager pipeline (RFC-008 v6 unification).
83
2361
  */
84
- function CardFrame({ entityId, children, className, style }) {
85
- const dragging = require_hooks.useTag(entityId, require_hooks.Dragging);
86
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
87
- className,
88
- style: {
89
- width: "100%",
90
- height: "100%",
91
- borderRadius: "21.67px",
92
- overflow: "hidden",
93
- boxShadow: dragging ? "0 30px 60px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.06)" : "0 20px 40px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.05)",
94
- transform: dragging ? "scale(1.05)" : "scale(1)",
95
- transformOrigin: "center center",
96
- transition: "transform 180ms cubic-bezier(0.2, 0.9, 0.3, 1.2), box-shadow 180ms cubic-bezier(0.2, 0.9, 0.3, 1.2)",
97
- willChange: dragging ? "transform, box-shadow" : void 0,
98
- ...style
99
- },
100
- children
101
- });
2362
+ var PointerAdapter = class {
2363
+ attach(container, manager) {
2364
+ const lastByPointerId = /* @__PURE__ */ new Map();
2365
+ const make = (type, e) => {
2366
+ const rect = container.getBoundingClientRect();
2367
+ const screen = {
2368
+ x: e.clientX - rect.left,
2369
+ y: e.clientY - rect.top
2370
+ };
2371
+ const camera = manager.engine.getCamera();
2372
+ const world = screenToWorld(screen.x, screen.y, camera);
2373
+ const last = lastByPointerId.get(e.pointerId);
2374
+ const delta = last ? {
2375
+ x: screen.x - last.x,
2376
+ y: screen.y - last.y
2377
+ } : void 0;
2378
+ const button = type === "down" || type === "up" ? e.button ?? null : null;
2379
+ return {
2380
+ type,
2381
+ source: pointerSource(e),
2382
+ pointerId: e.pointerId,
2383
+ primary: e.isPrimary,
2384
+ screen,
2385
+ world,
2386
+ delta,
2387
+ button,
2388
+ modifiers: {
2389
+ shift: e.shiftKey,
2390
+ ctrl: e.ctrlKey,
2391
+ alt: e.altKey,
2392
+ meta: e.metaKey
2393
+ },
2394
+ timestamp: e.timeStamp,
2395
+ native: e
2396
+ };
2397
+ };
2398
+ const onDown = (e) => {
2399
+ const rect = container.getBoundingClientRect();
2400
+ const screen = {
2401
+ x: e.clientX - rect.left,
2402
+ y: e.clientY - rect.top
2403
+ };
2404
+ lastByPointerId.set(e.pointerId, screen);
2405
+ const target = e.target;
2406
+ const targetTag = target ? `${target.tagName}${target.id ? `#${target.id}` : ""}` : "null";
2407
+ if (target?.closest("button, input, textarea, select, [contenteditable]")) {
2408
+ require_hooks.inputLog("Adapter", `pointerdown → SKIPPED (native interactive)`, {
2409
+ pointerId: e.pointerId,
2410
+ target: targetTag,
2411
+ source: e.pointerType
2412
+ });
2413
+ return;
2414
+ }
2415
+ require_hooks.inputLog("Adapter", `pointerdown id=${e.pointerId} → InputManager`, {
2416
+ pointerId: e.pointerId,
2417
+ screen,
2418
+ button: e.button,
2419
+ source: e.pointerType,
2420
+ target: targetTag
2421
+ });
2422
+ const event = make("down", e);
2423
+ manager.dispatch({
2424
+ ...event,
2425
+ delta: void 0
2426
+ });
2427
+ };
2428
+ const onMove = (e) => {
2429
+ const event = make("move", e);
2430
+ lastByPointerId.set(e.pointerId, {
2431
+ x: event.screen.x,
2432
+ y: event.screen.y
2433
+ });
2434
+ require_hooks.inputLog("Adapter", `pointermove id=${e.pointerId} → InputManager`, {
2435
+ type: "move",
2436
+ pointerId: e.pointerId,
2437
+ screen: event.screen
2438
+ });
2439
+ manager.dispatch(event);
2440
+ };
2441
+ const onUp = (e) => {
2442
+ require_hooks.inputLog("Adapter", `pointerup id=${e.pointerId} → InputManager`, {
2443
+ pointerId: e.pointerId,
2444
+ source: e.pointerType
2445
+ });
2446
+ manager.dispatch(make("up", e));
2447
+ lastByPointerId.delete(e.pointerId);
2448
+ };
2449
+ const onCancel = (e) => {
2450
+ require_hooks.inputLog("Adapter", `pointercancel id=${e.pointerId} → InputManager`, { pointerId: e.pointerId });
2451
+ manager.dispatch(make("cancel", e));
2452
+ lastByPointerId.delete(e.pointerId);
2453
+ };
2454
+ const onLeave = (e) => {
2455
+ require_hooks.inputLog("Adapter", `pointerleave id=${e.pointerId} → InputManager`, {
2456
+ pointerId: e.pointerId,
2457
+ source: e.pointerType
2458
+ });
2459
+ manager.dispatch(make("pointerleave", e));
2460
+ lastByPointerId.delete(e.pointerId);
2461
+ };
2462
+ container.addEventListener("pointerdown", onDown);
2463
+ container.addEventListener("pointermove", onMove);
2464
+ container.addEventListener("pointerup", onUp);
2465
+ container.addEventListener("pointercancel", onCancel);
2466
+ container.addEventListener("pointerleave", onLeave);
2467
+ return () => {
2468
+ container.removeEventListener("pointerdown", onDown);
2469
+ container.removeEventListener("pointermove", onMove);
2470
+ container.removeEventListener("pointerup", onUp);
2471
+ container.removeEventListener("pointercancel", onCancel);
2472
+ container.removeEventListener("pointerleave", onLeave);
2473
+ lastByPointerId.clear();
2474
+ };
2475
+ }
2476
+ };
2477
+ function pointerSource(e) {
2478
+ switch (e.pointerType) {
2479
+ case "mouse": return "mouse";
2480
+ case "pen": return "pen";
2481
+ case "touch": return "touch";
2482
+ default: return "mouse";
2483
+ }
102
2484
  }
2485
+ //#endregion
2486
+ //#region src/react/input/adapters/WheelAdapter.ts
103
2487
  /**
104
- * Returns a paired widget + archetype for an iOS-style card. Register both
105
- * with `createLayoutEngine({ widgets: [card.widget], archetypes: [card.archetype] })`
106
- * (or via `engine.registerWidget` / `engine.registerArchetype`) and spawn with
107
- * `engine.spawn('your-card-type', { at, data })`.
2488
+ * Wheel adapter (RFC-008). Listens for native wheel events on the canvas
2489
+ * container, normalises into `InputEvent`s with a `wheelDelta` payload,
2490
+ * and dispatches via `manager.dispatch(...)`.
108
2491
  *
109
- * The produced widget is non-resizable (Selectable + Draggable only), wrapped
110
- * in `<CardFrame>`, and spawns with a `Card` component so `cardSystem` enforces
111
- * the preset size each tick.
2492
+ * Always calls `preventDefault()` we always own canvas pan/zoom from
2493
+ * wheel input. Widgets that want internal scroll content call
2494
+ * `e.stopPropagation()` on `wheel` from inside their React tree, which
2495
+ * halts the bubble before it reaches this listener.
2496
+ *
2497
+ * `wheelDelta.pinch` is true when ctrl or meta is held — browsers
2498
+ * translate trackpad pinch into ctrl+wheel events; the engine wheel
2499
+ * handler interprets pinch as zoom and plain wheel as pan.
112
2500
  */
113
- function createCardWidget(opts) {
114
- const defaultSize = DEFAULT_CARD_PRESET_SIZES$1[opts.size];
115
- const Render = opts.render;
116
- const Component = ({ entityId }) => {
117
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFrame, {
118
- entityId,
119
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Render, {
120
- entityId,
121
- data: useWidgetData(entityId)
122
- })
2501
+ var WheelAdapter = class {
2502
+ attach(container, manager) {
2503
+ const onWheel = (e) => {
2504
+ e.preventDefault();
2505
+ const rect = container.getBoundingClientRect();
2506
+ const screen = {
2507
+ x: e.clientX - rect.left,
2508
+ y: e.clientY - rect.top
2509
+ };
2510
+ const camera = manager.engine.getCamera();
2511
+ const event = {
2512
+ type: "wheel",
2513
+ source: "wheel",
2514
+ pointerId: 0,
2515
+ primary: true,
2516
+ screen,
2517
+ world: screenToWorld(screen.x, screen.y, camera),
2518
+ wheelDelta: {
2519
+ dx: e.deltaX,
2520
+ dy: e.deltaY,
2521
+ pinch: e.ctrlKey || e.metaKey
2522
+ },
2523
+ modifiers: {
2524
+ shift: e.shiftKey,
2525
+ ctrl: e.ctrlKey,
2526
+ alt: e.altKey,
2527
+ meta: e.metaKey
2528
+ },
2529
+ timestamp: e.timeStamp,
2530
+ native: e
2531
+ };
2532
+ require_hooks.inputLog("Adapter", `wheel → InputManager`, {
2533
+ type: "wheel",
2534
+ screen,
2535
+ dx: e.deltaX,
2536
+ dy: e.deltaY,
2537
+ pinch: e.ctrlKey || e.metaKey
2538
+ });
2539
+ manager.dispatch(event);
2540
+ };
2541
+ container.addEventListener("wheel", onWheel, { passive: false });
2542
+ return () => {
2543
+ container.removeEventListener("wheel", onWheel);
2544
+ };
2545
+ }
2546
+ };
2547
+ //#endregion
2548
+ //#region src/react/input/constants.ts
2549
+ /** Double-tap zoom level cycle. */
2550
+ const ZOOM_TARGETS = [1, 2];
2551
+ /** Wheel deltaY → zoom delta multiplier. Matches today's wheel `useEffect`. */
2552
+ const WHEEL_ZOOM_FACTOR = .01;
2553
+ //#endregion
2554
+ //#region src/react/input/InputManager.ts
2555
+ /**
2556
+ * RFC-008 input pipeline core. Adapters dispatch normalised InputEvents;
2557
+ * routers deliver to widget surfaces; engine handlers and recognizers
2558
+ * react.
2559
+ *
2560
+ * Lifecycle:
2561
+ * 1. Construct: `new InputManager(engine, container, [adapter, ...])`.
2562
+ * 2. Register handlers / recognizers / routers as needed.
2563
+ * 3. `attach()` — mounts adapters; returns detacher.
2564
+ * 4. Dispatch flows through automatically as native events arrive.
2565
+ * 5. Detacher tears down adapters and clears the gesturing debounce.
2566
+ */
2567
+ var InputManager = class {
2568
+ handlers = /* @__PURE__ */ new Map();
2569
+ recognizers = [];
2570
+ routers = /* @__PURE__ */ new Map();
2571
+ gesturingClearTimer = null;
2572
+ constructor(engine, container, adapters) {
2573
+ this.engine = engine;
2574
+ this.container = container;
2575
+ this.adapters = adapters;
2576
+ }
2577
+ attach() {
2578
+ const detachers = this.adapters.map((a) => a.attach(this.container, this));
2579
+ return () => {
2580
+ for (const d of detachers) try {
2581
+ d();
2582
+ } catch (err) {
2583
+ console.error("[InputManager] adapter detach threw", err);
2584
+ }
2585
+ if (this.gesturingClearTimer !== null) {
2586
+ clearTimeout(this.gesturingClearTimer);
2587
+ this.gesturingClearTimer = null;
2588
+ }
2589
+ for (const r of this.recognizers) try {
2590
+ r.reset?.();
2591
+ } catch (err) {
2592
+ console.error("[InputManager] recognizer reset threw", err);
2593
+ }
2594
+ };
2595
+ }
2596
+ on(type, handler) {
2597
+ let set = this.handlers.get(type);
2598
+ if (!set) {
2599
+ set = /* @__PURE__ */ new Set();
2600
+ this.handlers.set(type, set);
2601
+ }
2602
+ set.add(handler);
2603
+ return () => {
2604
+ const s = this.handlers.get(type);
2605
+ if (s) {
2606
+ s.delete(handler);
2607
+ if (s.size === 0) this.handlers.delete(type);
2608
+ }
2609
+ };
2610
+ }
2611
+ addRecognizer(r) {
2612
+ this.recognizers.push(r);
2613
+ return () => {
2614
+ const i = this.recognizers.indexOf(r);
2615
+ if (i !== -1) this.recognizers.splice(i, 1);
2616
+ };
2617
+ }
2618
+ setRouter(router) {
2619
+ this.routers.set(router.surface, router);
2620
+ return () => {
2621
+ if (this.routers.get(router.surface) === router) this.routers.delete(router.surface);
2622
+ };
2623
+ }
2624
+ dispatch(event) {
2625
+ const closeGroup = require_hooks.inputGroupStart(`dispatch ${event.type} id=${event.pointerId}`);
2626
+ require_hooks.inputLog("InputManager", `dispatch ${event.type}`, {
2627
+ type: event.type,
2628
+ source: event.source,
2629
+ pointerId: event.pointerId,
2630
+ screen: event.screen,
2631
+ world: event.world
2632
+ });
2633
+ if ((event.type === "down" || event.type === "move" || event.type === "up" || event.type === "cancel" || event.type === "click" || event.type === "dblclick" || event.type === "contextmenu") && event.source !== "synthetic" && this.routers.size > 0) {
2634
+ const entityId = this.engine.pickAt(event.screen.x, event.screen.y);
2635
+ if (entityId !== null) {
2636
+ const surface = this.surfaceOf(entityId);
2637
+ const router = surface !== null ? this.routers.get(surface) : void 0;
2638
+ require_hooks.inputLog("InputManager", `routing → ${surface ?? "none"} entity=${entityId}`, {
2639
+ type: event.type,
2640
+ entityId,
2641
+ surface,
2642
+ hasRouter: !!router
2643
+ });
2644
+ if (router) try {
2645
+ router.route(event, entityId);
2646
+ } catch (err) {
2647
+ console.error("[InputManager] router threw", err);
2648
+ }
2649
+ } else require_hooks.inputLog("InputManager", `routing → empty space (pickAt null)`, {
2650
+ type: event.type,
2651
+ screen: event.screen
2652
+ });
2653
+ }
2654
+ let claimed = false;
2655
+ if (event.source !== "synthetic") {
2656
+ for (const router of this.routers.values()) if (router.isPointerClaimed?.(event.pointerId)) {
2657
+ claimed = true;
2658
+ require_hooks.inputLog("InputManager", `claim check → CLAIMED by ${router.surface} router (recognizers will skip)`, {
2659
+ type: event.type,
2660
+ pointerId: event.pointerId
2661
+ });
2662
+ break;
2663
+ }
2664
+ }
2665
+ const handlers = this.handlers.get(event.type);
2666
+ if (handlers && handlers.size > 0) {
2667
+ require_hooks.inputLog("InputManager", `handlers for ${event.type}: ${handlers.size} → firing`, {
2668
+ type: event.type,
2669
+ count: handlers.size
2670
+ });
2671
+ for (const h of handlers) try {
2672
+ h(event);
2673
+ } catch (err) {
2674
+ console.error("[InputManager] handler threw", err);
2675
+ }
2676
+ }
2677
+ if (!claimed) for (const r of this.recognizers) try {
2678
+ r.observe(event, this);
2679
+ } catch (err) {
2680
+ console.error("[InputManager] recognizer threw", err);
2681
+ }
2682
+ else require_hooks.inputLog("InputManager", `recognizers skipped (pointer claimed)`, {
2683
+ type: event.type,
2684
+ pointerId: event.pointerId
2685
+ });
2686
+ closeGroup();
2687
+ }
2688
+ pickAt(screen) {
2689
+ return this.engine.pickAt(screen.x, screen.y);
2690
+ }
2691
+ notifyGesturing() {
2692
+ this.engine.setGesturing(true);
2693
+ if (this.gesturingClearTimer !== null) clearTimeout(this.gesturingClearTimer);
2694
+ this.gesturingClearTimer = setTimeout(() => {
2695
+ this.engine.setGesturing(false);
2696
+ this.gesturingClearTimer = null;
2697
+ }, 200);
2698
+ }
2699
+ /** Resolve an entity's rendering surface via the `Widget` component. */
2700
+ surfaceOf(entityId) {
2701
+ const w = this.engine.get(entityId, require_ecs.Widget);
2702
+ if (!w) return null;
2703
+ if (w.surface === "dom" || w.surface === "webgl" || w.surface === "webview") return w.surface;
2704
+ return null;
2705
+ }
2706
+ };
2707
+ //#endregion
2708
+ //#region src/react/input/installEngineHandlers.ts
2709
+ /**
2710
+ * RFC-008 § Engine handler registration. Wires the engine's input
2711
+ * primitives (`beginDrag`, `selectEntity`, camera ops, hover state, etc.)
2712
+ * to the synthetic events emitted by recognizers and the raw events
2713
+ * emitted by adapters. Returns a teardown that removes every handler.
2714
+ *
2715
+ * The `container` is the canvas-container DOM element. Pointer-capture
2716
+ * for drags is anchored on it so the gesture survives the cursor leaving
2717
+ * the container's bounds.
2718
+ */
2719
+ function installEngineHandlers(manager, engine, container) {
2720
+ let lastPinchCenter = null;
2721
+ const offs = [];
2722
+ offs.push(manager.on("wheel", (e) => {
2723
+ const w = e.wheelDelta;
2724
+ if (!w) return;
2725
+ if (w.pinch) engine.zoomAtPoint(e.screen.x, e.screen.y, -w.dy * WHEEL_ZOOM_FACTOR);
2726
+ else engine.panBy(-w.dx, -w.dy);
2727
+ manager.notifyGesturing();
2728
+ }));
2729
+ offs.push(manager.on("pinch-start", (e) => {
2730
+ const g = e.gesture;
2731
+ lastPinchCenter = {
2732
+ x: g.center.x,
2733
+ y: g.center.y
2734
+ };
2735
+ manager.notifyGesturing();
2736
+ }));
2737
+ offs.push(manager.on("pinch-update", (e) => {
2738
+ const g = e.gesture;
2739
+ engine.zoomAtPoint(g.center.x, g.center.y, g.scale - 1);
2740
+ if (lastPinchCenter) engine.panBy(g.center.x - lastPinchCenter.x, g.center.y - lastPinchCenter.y);
2741
+ lastPinchCenter = {
2742
+ x: g.center.x,
2743
+ y: g.center.y
2744
+ };
2745
+ manager.notifyGesturing();
2746
+ }));
2747
+ offs.push(manager.on("pinch-end", () => {
2748
+ lastPinchCenter = null;
2749
+ }));
2750
+ offs.push(manager.on("pan-update", (e) => {
2751
+ const g = e.gesture;
2752
+ engine.panBy(g.delta.x, g.delta.y);
2753
+ manager.notifyGesturing();
2754
+ }));
2755
+ offs.push(manager.on("click", (e) => {
2756
+ if (e.button !== 0 && e.button !== null) return;
2757
+ const entity = engine.pickAt(e.screen.x, e.screen.y);
2758
+ if (entity !== null) {
2759
+ require_hooks.inputLog("Engine", `click on entity ${entity} → selectEntity (shift=${e.modifiers.shift})`);
2760
+ engine.selectEntity(entity, e.modifiers.shift);
2761
+ } else {
2762
+ require_hooks.inputLog("Engine", `click on empty space → clearSelection`);
2763
+ engine.clearSelection();
2764
+ }
2765
+ }));
2766
+ offs.push(manager.on("dblclick", (e) => {
2767
+ const entity = engine.pickAt(e.screen.x, e.screen.y);
2768
+ if (entity !== null) {
2769
+ require_hooks.inputLog("Engine", `dblclick on entity ${entity} → enterContainer`);
2770
+ engine.enterContainer(entity);
2771
+ return;
2772
+ }
2773
+ const camera = engine.getCamera();
2774
+ const target = camera.zoom < .9 ? ZOOM_TARGETS[0] : camera.zoom < 1.8 ? ZOOM_TARGETS[1] : ZOOM_TARGETS[0];
2775
+ require_hooks.inputLog("Engine", `dblclick on empty → zoomAtPoint target=${target}x`);
2776
+ engine.zoomAtPoint(e.screen.x, e.screen.y, (target - camera.zoom) / camera.zoom);
2777
+ }));
2778
+ offs.push(manager.on("drag-start", (e) => {
2779
+ const hit = engine.hitTest(e.screen.x, e.screen.y);
2780
+ container.setPointerCapture(e.pointerId);
2781
+ if (!hit) {
2782
+ if (e.source === "touch") {
2783
+ require_hooks.inputLog("Engine", `drag-start on empty (touch) → defer to PanRecognizer`, { pointerId: e.pointerId });
2784
+ return;
2785
+ }
2786
+ require_hooks.inputLog("Engine", `drag-start on empty (mouse/pen) → beginMarquee`, { pointerId: e.pointerId });
2787
+ engine.clearSelection();
2788
+ engine.beginMarquee(e.world.x, e.world.y);
2789
+ return;
2790
+ }
2791
+ if (hit.role.role.type === "resize") {
2792
+ require_hooks.inputLog("Engine", `drag-start on resize handle → beginResize ${hit.role.role.handle}`, {
2793
+ entityId: hit.entityId,
2794
+ handle: hit.role.role.handle
2795
+ });
2796
+ engine.beginResize(hit.entityId, hit.role.role.handle, e.world.x, e.world.y);
2797
+ return;
2798
+ }
2799
+ if (!engine.getSelectedEntities().includes(hit.entityId)) engine.selectEntity(hit.entityId, e.modifiers.shift);
2800
+ require_hooks.inputLog("Engine", `drag-start on entity → beginDrag ${hit.entityId}`, {
2801
+ entityId: hit.entityId,
2802
+ role: hit.role.role.type,
2803
+ shift: e.modifiers.shift
123
2804
  });
2805
+ engine.beginDrag(hit.entityId, e.world.x, e.world.y);
2806
+ }));
2807
+ offs.push(manager.on("drag-update", (e) => {
2808
+ if (engine.isMarqueeActive()) {
2809
+ engine.updateMarquee(e.world.x, e.world.y);
2810
+ return;
2811
+ }
2812
+ const resizing = engine.getResizingEntity();
2813
+ if (resizing !== null) {
2814
+ engine.updateResize(resizing, e.world.x, e.world.y);
2815
+ return;
2816
+ }
2817
+ const dragging = engine.getDraggingEntity();
2818
+ if (dragging !== null) engine.updateDrag(dragging, e.world.x, e.world.y);
2819
+ }));
2820
+ offs.push(manager.on("drag-end", (e) => {
2821
+ if (engine.isMarqueeActive()) {
2822
+ require_hooks.inputLog("Engine", `drag-end → endMarquee`);
2823
+ engine.endMarquee();
2824
+ } else {
2825
+ const resizing = engine.getResizingEntity();
2826
+ if (resizing !== null) {
2827
+ require_hooks.inputLog("Engine", `drag-end → endResize ${resizing} (commit)`);
2828
+ engine.endResize(resizing, { cancelled: false });
2829
+ } else {
2830
+ const dragging = engine.getDraggingEntity();
2831
+ if (dragging !== null) {
2832
+ require_hooks.inputLog("Engine", `drag-end → endDrag ${dragging} (commit)`);
2833
+ engine.endDrag(dragging, { cancelled: false });
2834
+ }
2835
+ }
2836
+ }
2837
+ if (container.hasPointerCapture(e.pointerId)) container.releasePointerCapture(e.pointerId);
2838
+ }));
2839
+ offs.push(manager.on("cancel", (e) => {
2840
+ require_hooks.inputLog("Engine", `cancel → cancelInteraction (covers all mid-gesture modes)`, { pointerId: e.pointerId });
2841
+ engine.cancelInteraction();
2842
+ if (container.hasPointerCapture(e.pointerId)) container.releasePointerCapture(e.pointerId);
2843
+ }));
2844
+ offs.push(manager.on("move", (e) => {
2845
+ engine.updateHover(e.screen.x, e.screen.y);
2846
+ }));
2847
+ offs.push(manager.on("pointerleave", () => {
2848
+ require_hooks.inputLog("Engine", `pointerleave → setHoveredEntity(null)`);
2849
+ engine.setHoveredEntity(null);
2850
+ }));
2851
+ return () => {
2852
+ for (const off of offs) off();
2853
+ lastPinchCenter = null;
124
2854
  };
2855
+ }
2856
+ //#endregion
2857
+ //#region src/react/input/synthetic.ts
2858
+ /**
2859
+ * Helper for constructing recognizer-emitted synthetic events. The
2860
+ * recognizer copies pointerId / modifiers / coords from the base raw
2861
+ * event, sets `source: 'synthetic'`, and attaches a `gesture` payload.
2862
+ */
2863
+ function makeSynthetic(type, base, gesture, overrides) {
125
2864
  return {
126
- widget: {
127
- type: opts.type,
128
- schema: opts.schema,
129
- defaultData: opts.defaultData,
130
- defaultSize,
131
- component: Component
132
- },
133
- archetype: {
134
- id: opts.type,
135
- widget: opts.type,
136
- components: [[require_hooks.Card, { preset: opts.size }]],
137
- interactive: {
138
- selectable: true,
139
- draggable: true,
140
- resizable: false,
141
- selectionFrame: false
142
- },
143
- defaultSize
2865
+ type,
2866
+ source: "synthetic",
2867
+ pointerId: base.pointerId,
2868
+ primary: base.primary,
2869
+ screen: overrides?.screen ?? base.screen,
2870
+ world: overrides?.world ?? base.world,
2871
+ delta: overrides?.delta,
2872
+ modifiers: base.modifiers,
2873
+ timestamp: base.timestamp,
2874
+ gesture
2875
+ };
2876
+ }
2877
+ /** Squared distance — avoids a sqrt for threshold comparisons. */
2878
+ function distSq(a, b) {
2879
+ const dx = a.x - b.x;
2880
+ const dy = a.y - b.y;
2881
+ return dx * dx + dy * dy;
2882
+ }
2883
+ /** Returns true if the linear distance between two points exceeds `threshold` px. */
2884
+ function exceedsThreshold(a, b, threshold) {
2885
+ return distSq(a, b) > threshold * threshold;
2886
+ }
2887
+ //#endregion
2888
+ //#region src/react/input/recognizers/DoubleTapRecognizer.ts
2889
+ /**
2890
+ * Double-tap recognizer (RFC-008). Observes the `tap` events emitted by
2891
+ * `TapRecognizer`. When two consecutive taps land within
2892
+ * `DOUBLE_TAP_WINDOW_MS` and `DOUBLE_TAP_DIST_PX`, emits a synthetic
2893
+ * `'double-tap'`.
2894
+ *
2895
+ * The first tap continues to fire normally — handlers that listen to
2896
+ * `'tap'` (e.g., engine selection) react to it. The second tap fires
2897
+ * both a `'tap'` AND a `'double-tap'`. Engine handlers can decide which
2898
+ * to respond to based on what they care about.
2899
+ */
2900
+ var DoubleTapRecognizer = class {
2901
+ last = null;
2902
+ observe(event, manager) {
2903
+ if (event.type !== "tap" || event.gesture?.kind !== "tap" || event.gesture.count !== 1) return;
2904
+ const now = event.timestamp;
2905
+ const here = event.screen;
2906
+ if (this.last !== null && now - this.last.time <= 300 && !exceedsThreshold(here, this.last.at, 30)) {
2907
+ require_hooks.inputLog("Recognizer", `DoubleTapRecognizer: 2nd tap within window → double-tap`, {
2908
+ pointerId: event.pointerId,
2909
+ dtMs: now - this.last.time
2910
+ });
2911
+ manager.dispatch(makeSynthetic("double-tap", event, {
2912
+ kind: "tap",
2913
+ count: 2
2914
+ }));
2915
+ this.last = null;
2916
+ return;
2917
+ }
2918
+ this.last = {
2919
+ at: here,
2920
+ time: now
2921
+ };
2922
+ }
2923
+ reset() {
2924
+ this.last = null;
2925
+ }
2926
+ };
2927
+ //#endregion
2928
+ //#region src/react/input/recognizers/DragRecognizer.ts
2929
+ /**
2930
+ * Drag recognizer (RFC-008). Per-pointerId state on `down`. On `move`
2931
+ * past the source-appropriate dead zone, emits a synthetic `drag-start`
2932
+ * and transitions to `dragging`. Subsequent moves emit `drag-update`s
2933
+ * with screen + world deltas. `up` or `cancel` after dragging emits
2934
+ * `drag-end`.
2935
+ *
2936
+ * Single-finger only: PinchRecognizer dispatches a synthetic `cancel`
2937
+ * when a 2nd touch lands, which DragRecognizer observes and uses to
2938
+ * abort tracking for the first finger.
2939
+ *
2940
+ * Note: synthetic `drag-update` events carry the SCREEN delta in
2941
+ * `gesture.delta` (not world delta) — the engine handler needs world
2942
+ * coords to update entity positions, but it gets those from the raw
2943
+ * `move` event's `world`. The gesture's `delta` is informational, not
2944
+ * used by the engine drag handler.
2945
+ */
2946
+ var DragRecognizer = class {
2947
+ tracking = /* @__PURE__ */ new Map();
2948
+ observe(event, manager) {
2949
+ switch (event.type) {
2950
+ case "down":
2951
+ if (event.button !== 0 && event.button !== null) return;
2952
+ if (this.tracking.size > 0) return;
2953
+ this.tracking.set(event.pointerId, {
2954
+ downAt: {
2955
+ screen: event.screen,
2956
+ world: event.world
2957
+ },
2958
+ last: {
2959
+ screen: event.screen,
2960
+ world: event.world
2961
+ },
2962
+ status: "tracking"
2963
+ });
2964
+ return;
2965
+ case "move": {
2966
+ const t = this.tracking.get(event.pointerId);
2967
+ if (!t) return;
2968
+ if (t.status === "tracking") {
2969
+ const dz = event.source === "touch" ? 8 : 4;
2970
+ if (!exceedsThreshold(event.screen, t.downAt.screen, dz)) return;
2971
+ t.status = "dragging";
2972
+ require_hooks.inputLog("Recognizer", `DragRecognizer: dead-zone crossed → drag-start`, {
2973
+ pointerId: event.pointerId,
2974
+ source: event.source,
2975
+ deadZone: dz
2976
+ });
2977
+ manager.dispatch(makeSynthetic("drag-start", event, {
2978
+ kind: "drag",
2979
+ phase: "start",
2980
+ total: {
2981
+ x: event.screen.x - t.downAt.screen.x,
2982
+ y: event.screen.y - t.downAt.screen.y
2983
+ },
2984
+ delta: {
2985
+ x: event.screen.x - t.last.screen.x,
2986
+ y: event.screen.y - t.last.screen.y
2987
+ }
2988
+ }));
2989
+ t.last = {
2990
+ screen: event.screen,
2991
+ world: event.world
2992
+ };
2993
+ return;
2994
+ }
2995
+ manager.dispatch(makeSynthetic("drag-update", event, {
2996
+ kind: "drag",
2997
+ phase: "update",
2998
+ total: {
2999
+ x: event.screen.x - t.downAt.screen.x,
3000
+ y: event.screen.y - t.downAt.screen.y
3001
+ },
3002
+ delta: {
3003
+ x: event.screen.x - t.last.screen.x,
3004
+ y: event.screen.y - t.last.screen.y
3005
+ }
3006
+ }));
3007
+ t.last = {
3008
+ screen: event.screen,
3009
+ world: event.world
3010
+ };
3011
+ return;
3012
+ }
3013
+ case "up": {
3014
+ const t = this.tracking.get(event.pointerId);
3015
+ if (!t) return;
3016
+ this.tracking.delete(event.pointerId);
3017
+ if (t.status !== "dragging") return;
3018
+ require_hooks.inputLog("Recognizer", `DragRecognizer: up after dragging → drag-end`, { pointerId: event.pointerId });
3019
+ manager.dispatch(makeSynthetic("drag-end", event, {
3020
+ kind: "drag",
3021
+ phase: "end",
3022
+ total: {
3023
+ x: event.screen.x - t.downAt.screen.x,
3024
+ y: event.screen.y - t.downAt.screen.y
3025
+ },
3026
+ delta: {
3027
+ x: event.screen.x - t.last.screen.x,
3028
+ y: event.screen.y - t.last.screen.y
3029
+ }
3030
+ }));
3031
+ return;
3032
+ }
3033
+ case "cancel":
3034
+ this.tracking.delete(event.pointerId);
3035
+ return;
3036
+ }
3037
+ }
3038
+ reset() {
3039
+ this.tracking.clear();
3040
+ }
3041
+ };
3042
+ //#endregion
3043
+ //#region src/react/input/recognizers/HoverRecognizer.ts
3044
+ /**
3045
+ * Hover recognizer (RFC-008). Observes `move` events; tracks the last
3046
+ * leaf entity under each pointer via `manager.pickAt`. On change, emits
3047
+ * `hover-leave` (with the previous entity in `gesture.entityId`) and
3048
+ * `hover-enter` (with the new entity).
3049
+ *
3050
+ * Hover is informational — recognizers and handlers downstream cannot
3051
+ * "consume" it. Engine hover chrome (selection ring, cursor hint) is
3052
+ * driven entirely by these events.
3053
+ *
3054
+ * Pen + mouse hover normally; touch fires hover only while a finger is
3055
+ * pressed, so touch-source hover events fire as a side-effect of drag
3056
+ * tracking. Authors who want touch hover-on-hover should listen for
3057
+ * `move` events themselves.
3058
+ */
3059
+ var HoverRecognizer = class {
3060
+ lastByPointer = /* @__PURE__ */ new Map();
3061
+ observe(event, manager) {
3062
+ switch (event.type) {
3063
+ case "move": {
3064
+ const current = manager.pickAt(event.screen);
3065
+ const prev = this.lastByPointer.get(event.pointerId) ?? null;
3066
+ if (current === prev) return;
3067
+ require_hooks.inputLog("Recognizer", `HoverRecognizer: entity changed ${prev} → ${current}`, {
3068
+ pointerId: event.pointerId,
3069
+ prev,
3070
+ current
3071
+ });
3072
+ if (prev !== null) manager.dispatch(makeSynthetic("hover-leave", event, {
3073
+ kind: "hover",
3074
+ entityId: prev
3075
+ }));
3076
+ this.lastByPointer.set(event.pointerId, current);
3077
+ if (current !== null) manager.dispatch(makeSynthetic("hover-enter", event, {
3078
+ kind: "hover",
3079
+ entityId: current
3080
+ }));
3081
+ return;
3082
+ }
3083
+ case "up":
3084
+ case "cancel": {
3085
+ const prev = this.lastByPointer.get(event.pointerId);
3086
+ this.lastByPointer.delete(event.pointerId);
3087
+ if (prev != null) manager.dispatch(makeSynthetic("hover-leave", event, {
3088
+ kind: "hover",
3089
+ entityId: prev
3090
+ }));
3091
+ return;
3092
+ }
3093
+ }
3094
+ }
3095
+ reset() {
3096
+ this.lastByPointer.clear();
3097
+ }
3098
+ };
3099
+ //#endregion
3100
+ //#region src/react/input/recognizers/PanRecognizer.ts
3101
+ /**
3102
+ * Pan recognizer (RFC-008). Single-finger touch on empty space only —
3103
+ * `engine.pickAt(world) === null` at down-time. Emits `pan-update` with
3104
+ * screen deltas. The engine pan handler translates them into camera
3105
+ * `panBy` calls.
3106
+ *
3107
+ * Pinch + pan run simultaneously when two fingers are down on empty
3108
+ * space (matches iOS Maps): PinchRecognizer cancels this recognizer's
3109
+ * tracking via synthetic `cancel`, then a fresh PanRecognizer state
3110
+ * doesn't restart until both fingers release and a new single-finger
3111
+ * touch begins on empty space.
3112
+ *
3113
+ * Mouse / pen drags on empty space are NOT pans here — they're marquee
3114
+ * selection, handled by the engine drag handler responding to
3115
+ * DragRecognizer's `drag-start` for empty-space hits.
3116
+ */
3117
+ var PanRecognizer = class {
3118
+ tracking = /* @__PURE__ */ new Map();
3119
+ observe(event, manager) {
3120
+ switch (event.type) {
3121
+ case "down":
3122
+ if (event.source !== "touch") return;
3123
+ if (event.button !== 0 && event.button !== null) return;
3124
+ if (manager.pickAt(event.screen) !== null) return;
3125
+ this.tracking.set(event.pointerId, {
3126
+ downAt: event.screen,
3127
+ last: event.screen,
3128
+ status: "tracking"
3129
+ });
3130
+ return;
3131
+ case "move": {
3132
+ const t = this.tracking.get(event.pointerId);
3133
+ if (!t) return;
3134
+ if (t.status === "tracking") {
3135
+ if (!exceedsThreshold(event.screen, t.downAt, 8)) return;
3136
+ t.status = "panning";
3137
+ t.last = event.screen;
3138
+ require_hooks.inputLog("Recognizer", `PanRecognizer: dead-zone crossed → panning (touch empty-space)`, { pointerId: event.pointerId });
3139
+ return;
3140
+ }
3141
+ const delta = {
3142
+ x: event.screen.x - t.last.x,
3143
+ y: event.screen.y - t.last.y
3144
+ };
3145
+ t.last = event.screen;
3146
+ manager.dispatch(makeSynthetic("pan-update", event, {
3147
+ kind: "pan",
3148
+ delta
3149
+ }));
3150
+ return;
3151
+ }
3152
+ case "up":
3153
+ case "cancel":
3154
+ this.tracking.delete(event.pointerId);
3155
+ return;
3156
+ }
3157
+ }
3158
+ reset() {
3159
+ this.tracking.clear();
3160
+ }
3161
+ };
3162
+ //#endregion
3163
+ //#region src/react/input/recognizers/PinchRecognizer.ts
3164
+ /**
3165
+ * Pinch recognizer (RFC-008). Counts simultaneous active touch-source
3166
+ * pointers; emits `pinch-start` when count reaches 2, `pinch-update` on
3167
+ * either finger move, `pinch-end` when count drops below 2.
3168
+ *
3169
+ * On `pinch-start`, dispatches a synthetic `cancel` for any tracked
3170
+ * single-finger gesture so DragRecognizer / TapRecognizer / PanRecognizer
3171
+ * can abort cleanly. This is the cancel-then-pinch ordering — engine
3172
+ * drag (if active) runs `endDrag(entity, { cancelled: true })` before
3173
+ * pinch math takes over.
3174
+ *
3175
+ * Coexists with PanRecognizer: empty-space pan (single finger that
3176
+ * already crossed dead zone before the 2nd finger arrives) gets
3177
+ * cancelled by the synthetic `cancel`. After `pinch-end`, the user must
3178
+ * lift and re-place to start a new gesture (matches iOS).
3179
+ */
3180
+ var PinchRecognizer = class {
3181
+ /** Active touch-source pointers and their latest positions (screen). */
3182
+ active = /* @__PURE__ */ new Map();
3183
+ state = null;
3184
+ observe(event, manager) {
3185
+ if (event.source !== "touch") return;
3186
+ switch (event.type) {
3187
+ case "down":
3188
+ this.active.set(event.pointerId, event.screen);
3189
+ if (this.state === null && this.active.size === 2) {
3190
+ const ids = [...this.active.keys()];
3191
+ const camera = manager.engine.getCamera();
3192
+ for (const id of ids) {
3193
+ const screen = this.active.get(id);
3194
+ manager.dispatch({
3195
+ type: "cancel",
3196
+ source: "synthetic",
3197
+ pointerId: id,
3198
+ primary: false,
3199
+ screen,
3200
+ world: screenToWorld(screen.x, screen.y, camera),
3201
+ modifiers: event.modifiers,
3202
+ timestamp: event.timestamp
3203
+ });
3204
+ }
3205
+ const positions = [...this.active.values()];
3206
+ const dist = pointDistance(positions[0], positions[1]);
3207
+ const center = midpoint(positions[0], positions[1]);
3208
+ this.state = {
3209
+ pointerIds: ids,
3210
+ last: {
3211
+ dist,
3212
+ center
3213
+ }
3214
+ };
3215
+ require_hooks.inputLog("Recognizer", `PinchRecognizer: 2nd touch → pinch-start (cancel sent for both)`, { pointerIds: ids });
3216
+ manager.dispatch(makeSynthetic("pinch-start", event, {
3217
+ kind: "pinch",
3218
+ phase: "start",
3219
+ scale: 1,
3220
+ center
3221
+ }));
3222
+ }
3223
+ return;
3224
+ case "move": {
3225
+ if (this.state === null || !this.active.has(event.pointerId)) return;
3226
+ this.active.set(event.pointerId, event.screen);
3227
+ const a = this.active.get(this.state.pointerIds[0]);
3228
+ const b = this.active.get(this.state.pointerIds[1]);
3229
+ if (!a || !b) return;
3230
+ const dist = pointDistance(a, b);
3231
+ const center = midpoint(a, b);
3232
+ const scale = this.state.last.dist > 0 ? dist / this.state.last.dist : 1;
3233
+ this.state.last = {
3234
+ dist,
3235
+ center
3236
+ };
3237
+ manager.dispatch(makeSynthetic("pinch-update", event, {
3238
+ kind: "pinch",
3239
+ phase: "update",
3240
+ scale,
3241
+ center
3242
+ }, { screen: center }));
3243
+ return;
3244
+ }
3245
+ case "up":
3246
+ case "cancel":
3247
+ this.active.delete(event.pointerId);
3248
+ if (this.state !== null && this.active.size < 2) {
3249
+ require_hooks.inputLog("Recognizer", `PinchRecognizer: finger lifted → pinch-end`, { remainingActive: this.active.size });
3250
+ manager.dispatch(makeSynthetic("pinch-end", event, {
3251
+ kind: "pinch",
3252
+ phase: "end",
3253
+ scale: 1,
3254
+ center: this.state.last.center
3255
+ }));
3256
+ this.state = null;
3257
+ }
3258
+ return;
144
3259
  }
3260
+ }
3261
+ reset() {
3262
+ this.active.clear();
3263
+ this.state = null;
3264
+ }
3265
+ };
3266
+ function pointDistance(a, b) {
3267
+ const dx = a.x - b.x;
3268
+ const dy = a.y - b.y;
3269
+ return Math.sqrt(dx * dx + dy * dy);
3270
+ }
3271
+ function midpoint(a, b) {
3272
+ return {
3273
+ x: (a.x + b.x) / 2,
3274
+ y: (a.y + b.y) / 2
145
3275
  };
146
3276
  }
147
3277
  //#endregion
148
- //#region src/react/geometry-card.tsx
149
- /** Must match {@link CardPresetsResource} defaults. */
150
- const DEFAULT_CARD_PRESET_SIZES = {
151
- small: {
152
- width: 155,
153
- height: 155
154
- },
155
- medium: {
156
- width: 329,
157
- height: 155
158
- },
159
- large: {
160
- width: 329,
161
- height: 345
162
- },
163
- xl: {
164
- width: 329,
165
- height: 535
3278
+ //#region src/react/input/recognizers/TapRecognizer.ts
3279
+ /**
3280
+ * Tap recognizer (RFC-008). Per-pointerId state on `down`; emits a
3281
+ * synthetic `'tap'` (count 1) on `up` within `TAP_WINDOW_MS` and the
3282
+ * source-appropriate dead zone.
3283
+ *
3284
+ * Pairwise relationships:
3285
+ * - Cancels its pending tap on observed `drag-start` or `pinch-start`
3286
+ * for the same pointerId.
3287
+ * - Cancels its pending tap on observed `cancel`.
3288
+ */
3289
+ var TapRecognizer = class {
3290
+ pending = /* @__PURE__ */ new Map();
3291
+ observe(event, manager) {
3292
+ switch (event.type) {
3293
+ case "down":
3294
+ if (event.button !== 0 && event.button !== null) return;
3295
+ this.pending.set(event.pointerId, {
3296
+ downAt: event.screen,
3297
+ time: event.timestamp
3298
+ });
3299
+ return;
3300
+ case "up": {
3301
+ const p = this.pending.get(event.pointerId);
3302
+ if (!p) return;
3303
+ this.pending.delete(event.pointerId);
3304
+ const elapsed = event.timestamp - p.time;
3305
+ const dz = event.source === "touch" ? 8 : 4;
3306
+ if (elapsed <= 250 && !exceedsThreshold(event.screen, p.downAt, dz)) {
3307
+ require_hooks.inputLog("Recognizer", `TapRecognizer: up within tap window → tap`, {
3308
+ pointerId: event.pointerId,
3309
+ elapsedMs: elapsed
3310
+ });
3311
+ manager.dispatch(makeSynthetic("tap", event, {
3312
+ kind: "tap",
3313
+ count: 1
3314
+ }));
3315
+ }
3316
+ return;
3317
+ }
3318
+ case "cancel":
3319
+ case "drag-start":
3320
+ case "pinch-start":
3321
+ this.pending.delete(event.pointerId);
3322
+ return;
3323
+ }
3324
+ }
3325
+ reset() {
3326
+ this.pending.clear();
166
3327
  }
167
3328
  };
3329
+ //#endregion
3330
+ //#region src/react/input/routers/R3FRouter.ts
168
3331
  /**
169
- * Pure-three rounded-rect extrude geometry avoids a drei dependency.
170
- * Rounded corners match the DOM CardFrame radius (21.67 px).
171
- */
172
- function makeRoundedCardGeometry(width, height, radius, depth) {
173
- const shape = new three.Shape();
174
- const r = Math.min(radius, Math.min(width, height) / 2);
175
- const x = -width / 2;
176
- const y = -height / 2;
177
- shape.moveTo(x, y + r);
178
- shape.lineTo(x, y + height - r);
179
- shape.quadraticCurveTo(x, y + height, x + r, y + height);
180
- shape.lineTo(x + width - r, y + height);
181
- shape.quadraticCurveTo(x + width, y + height, x + width, y + height - r);
182
- shape.lineTo(x + width, y + r);
183
- shape.quadraticCurveTo(x + width, y, x + width - r, y);
184
- shape.lineTo(x + r, y);
185
- shape.quadraticCurveTo(x, y, x, y + r);
186
- return new three.ExtrudeGeometry(shape, {
187
- depth,
188
- bevelEnabled: true,
189
- bevelSegments: 3,
190
- bevelSize: .6,
191
- bevelThickness: .6
192
- });
193
- }
194
- function CardBack({ width, height, color, roughness, metalness }) {
195
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("mesh", {
196
- geometry: (0, react.useMemo)(() => makeRoundedCardGeometry(width, height, 21.67, 3), [width, height]),
197
- position: [
198
- 0,
199
- 0,
200
- -6
201
- ],
202
- receiveShadow: true,
203
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meshStandardMaterial", {
204
- color,
205
- roughness,
206
- metalness
207
- })
208
- });
209
- }
3332
+ * Map `InputEventType`s to the R3F handler name that drives R3F's
3333
+ * mesh-dispatch machinery. Pointer types feed R3F's hover-diff + capture
3334
+ * pipeline; click family feeds R3F's click synthesis (`onClick`,
3335
+ * `onDoubleClick`, `onContextMenu`). v6 routes clicks through this table
3336
+ * so the InputManager pipeline is the single source for both engine and
3337
+ * widget no parallel `connect` listener registration on the container.
3338
+ */
3339
+ const HANDLER_BY_TYPE = {
3340
+ down: "onPointerDown",
3341
+ move: "onPointerMove",
3342
+ up: "onPointerUp",
3343
+ cancel: "onPointerCancel",
3344
+ click: "onClick",
3345
+ dblclick: "onDoubleClick",
3346
+ contextmenu: "onContextMenu"
3347
+ };
210
3348
  /**
211
- * Returns a paired R3F widget + archetype for a card-shaped 3D widget.
212
- * Behaves like {@link createCardWidget} — fixed preset size, non-resizable,
213
- * no engine-drawn selection frame, and lifts on drag (scale + z) — but
214
- * renders a three.js scene instead of DOM content.
3349
+ * RFC-008 v5 `WidgetSurfaceRouter` for the WebGL surface.
215
3350
  *
216
- * Lighting: this helper adds no lights. Declare your own in the `geometry`
217
- * component (typically a local `pointLight` scoped with `distance`).
3351
+ * Invoked by `InputManager.dispatch` whenever a raw pointer event falls
3352
+ * over an entity whose `Widget.surface === 'webgl'`. Looks up the matching
3353
+ * R3F mesh-dispatch handler and invokes it with the underlying native
3354
+ * event, letting R3F run its raycast → bubble → handler pipeline (with
3355
+ * `compute` + `filter` from `createR3FEventManager` targeting the right
3356
+ * per-widget scene).
3357
+ *
3358
+ * The InputManager dispatches the router BEFORE engine handlers, so widget
3359
+ * mesh handlers that call `setPointerCapture` (claiming exclusive ownership
3360
+ * of the gesture) take effect before the engine's drag/marquee logic could
3361
+ * react. Mesh handlers that call `e.stopPropagation()` halt R3F's bubble
3362
+ * but NOT the engine's handlers — those listen on the InputManager, not on
3363
+ * R3F's bubble. This is the coexistence model: R3F handles widget-internal
3364
+ * logic, engine handles canvas-level logic, both react to the same event
3365
+ * unless a widget explicitly claims it (RFC-008 § Default coexistence).
218
3366
  */
219
- function createGeometryCardWidget(opts) {
220
- const defaultSize = DEFAULT_CARD_PRESET_SIZES[opts.size];
221
- const Render = opts.geometry;
222
- const backgroundConfig = opts.background ?? "card";
223
- const resolvedBack = backgroundConfig === "transparent" ? null : backgroundConfig === "card" ? {
224
- color: "#1C1C1E",
225
- roughness: .55,
226
- metalness: 0
227
- } : {
228
- color: backgroundConfig.color,
229
- roughness: backgroundConfig.roughness ?? .55,
230
- metalness: backgroundConfig.metalness ?? 0
231
- };
232
- const Component = ({ entityId, width, height }) => {
233
- const data = useWidgetData(entityId);
234
- const dragging = require_hooks.useTag(entityId, require_hooks.Dragging);
235
- const groupRef = (0, react.useRef)(null);
236
- (0, _react_three_fiber.useFrame)(() => {
237
- const g = groupRef.current;
238
- if (!g) return;
239
- const targetScale = dragging ? 1.05 : 1;
240
- const targetZ = dragging ? 8 : 0;
241
- const s = g.scale.x;
242
- g.scale.setScalar(s + (targetScale - s) * .2);
243
- g.position.z += (targetZ - g.position.z) * .2;
244
- });
245
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("group", {
246
- ref: groupRef,
247
- children: [resolvedBack && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBack, {
248
- width,
249
- height,
250
- color: resolvedBack.color,
251
- roughness: resolvedBack.roughness,
252
- metalness: resolvedBack.metalness
253
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Render, {
254
- entityId,
255
- data,
256
- width,
257
- height
258
- })]
3367
+ var R3FRouter = class {
3368
+ surface = "webgl";
3369
+ /**
3370
+ * @param getEventManager Returns the R3F event manager whose `handlers`
3371
+ * we invoke. Wrapped in a getter because R3F creates the manager
3372
+ * inside the React component tree, so the router (typically constructed
3373
+ * alongside the InputManager) needs late-bound access.
3374
+ */
3375
+ constructor(getEventManager) {
3376
+ this.getEventManager = getEventManager;
3377
+ }
3378
+ route(event, entityId) {
3379
+ if (!event.native) return;
3380
+ const handlerName = HANDLER_BY_TYPE[event.type];
3381
+ if (!handlerName) return;
3382
+ const handler = this.getEventManager()?.handlers?.[handlerName];
3383
+ if (!handler) {
3384
+ require_hooks.inputLog("Router", `R3FRouter: no R3F manager / handler for ${handlerName}`, {
3385
+ type: event.type,
3386
+ entityId
3387
+ });
3388
+ return;
3389
+ }
3390
+ require_hooks.inputLog("Router", `R3FRouter R3F.${handlerName} for entity ${entityId}`, {
3391
+ type: event.type,
3392
+ entityId,
3393
+ handlerName
259
3394
  });
260
- };
3395
+ handler(event.native);
3396
+ }
3397
+ isPointerClaimed(pointerId) {
3398
+ const claimed = this.getEventManager()?.isPointerCaptured?.(pointerId) ?? false;
3399
+ if (claimed) require_hooks.inputLog("Router", `R3FRouter: pointer ${pointerId} CLAIMED via setPointerCapture`, { pointerId });
3400
+ return claimed;
3401
+ }
3402
+ };
3403
+ //#endregion
3404
+ //#region src/react/widgets/overlap-glow.ts
3405
+ /**
3406
+ * Overlap glow — visual config for the drag-over highlight applied to
3407
+ * cards (DOM `CardChrome` + R3F `CompositionMaterial`).
3408
+ *
3409
+ * Single-layer design: a soft radial gradient at the hot point in a
3410
+ * neutral color, blended normally over the card content. Deliberately
3411
+ * simple — no backdrop-filter, no saturation/contrast tricks, no rim,
3412
+ * no bloom. Three tunables: color, alpha (per state), falloff radius.
3413
+ *
3414
+ * Flows two ways from `<InfiniteCanvas overlapGlow={...} />`:
3415
+ * 1. CSS custom properties on the canvas container — read by
3416
+ * `CardChrome` via `var(--ic-glow-…, fallback)` for DOM widgets.
3417
+ * 2. Shared shader uniforms — every `CompositionMaterial` instance
3418
+ * references the same uniform objects, so mutating
3419
+ * {@link sharedGlowUniforms} updates all R3F cards at once.
3420
+ *
3421
+ * Each `[candidate, target]` pair holds the value used while the card
3422
+ * is just an overlap candidate vs. when the drop will actually consume.
3423
+ */
3424
+ const DEFAULT_OVERLAP_GLOW_CONFIG = {
3425
+ glowColor: [
3426
+ .5,
3427
+ .5,
3428
+ .5
3429
+ ],
3430
+ glowAlpha: [.25, .45],
3431
+ glowSize: [60, 80],
3432
+ rimColor: [
3433
+ .5,
3434
+ .5,
3435
+ .5
3436
+ ],
3437
+ rimWidth: 1.5,
3438
+ rimAlpha: [.55, .85],
3439
+ rimRadius: 600
3440
+ };
3441
+ function rgbTriplet(color) {
3442
+ const [r, g, b] = color;
3443
+ return `${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}`;
3444
+ }
3445
+ /** Write the config as CSS custom properties on `target`. CardChrome reads them. */
3446
+ function applyOverlapGlowVars(target, c) {
3447
+ const s = target.style;
3448
+ s.setProperty("--ic-glow-color", rgbTriplet(c.glowColor));
3449
+ s.setProperty("--ic-glow-alpha-c", String(c.glowAlpha[0]));
3450
+ s.setProperty("--ic-glow-alpha-t", String(c.glowAlpha[1]));
3451
+ s.setProperty("--ic-glow-size-c", `${c.glowSize[0]}px`);
3452
+ s.setProperty("--ic-glow-size-t", `${c.glowSize[1]}px`);
3453
+ s.setProperty("--ic-rim-color", rgbTriplet(c.rimColor));
3454
+ s.setProperty("--ic-rim-width", `${c.rimWidth}px`);
3455
+ s.setProperty("--ic-rim-alpha-c", String(c.rimAlpha[0]));
3456
+ s.setProperty("--ic-rim-alpha-t", String(c.rimAlpha[1]));
3457
+ s.setProperty("--ic-rim-radius", `${c.rimRadius}px`);
3458
+ }
3459
+ /** Push the same config into the shared shader uniforms (mutates in place). */
3460
+ function applyOverlapGlowShaderUniforms(c) {
3461
+ require_hooks.sharedGlowUniforms.uGlowColor.value.set(c.glowColor[0], c.glowColor[1], c.glowColor[2]);
3462
+ require_hooks.sharedGlowUniforms.uGlowAlpha.value.set(c.glowAlpha[0], c.glowAlpha[1]);
3463
+ require_hooks.sharedGlowUniforms.uGlowFalloff.value.set(c.glowSize[0] / 200, c.glowSize[1] / 200);
3464
+ require_hooks.sharedGlowUniforms.uRimColor.value.set(c.rimColor[0], c.rimColor[1], c.rimColor[2]);
3465
+ require_hooks.sharedGlowUniforms.uRimAlpha.value.set(c.rimAlpha[0], c.rimAlpha[1]);
3466
+ require_hooks.sharedGlowUniforms.uRimRadius.value = c.rimRadius / 200;
3467
+ }
3468
+ function mergeOverlapGlow(override) {
261
3469
  return {
262
- widget: {
263
- type: opts.type,
264
- surface: "webgl",
265
- schema: opts.schema,
266
- defaultData: opts.defaultData,
267
- defaultSize,
268
- component: Component
269
- },
270
- archetype: {
271
- id: opts.type,
272
- widget: opts.type,
273
- components: [[require_hooks.Card, { preset: opts.size }]],
274
- interactive: {
275
- selectable: true,
276
- draggable: true,
277
- resizable: false,
278
- selectionFrame: false
279
- },
280
- defaultSize
281
- }
3470
+ ...DEFAULT_OVERLAP_GLOW_CONFIG,
3471
+ ...override
282
3472
  };
283
3473
  }
284
3474
  //#endregion
285
- //#region src/react/WidgetProvider.tsx
3475
+ //#region src/react/widgets/registry.ts
3476
+ /** Narrows to the R3F variant. */
3477
+ function isR3FWidget(widget) {
3478
+ return widget.surface === "webgl";
3479
+ }
3480
+ //#endregion
3481
+ //#region src/react/widgets/WidgetProvider.tsx
286
3482
  /**
287
3483
  * Bridges the engine's widget registry to React context so WidgetSlot /
288
- * WebGLWidgetLayer can resolve components by type.
3484
+ * R3FManager can resolve components by type.
289
3485
  */
290
3486
  function WidgetProvider({ engine, children }) {
291
3487
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_hooks.WidgetResolverProvider, {
292
3488
  value: (0, react.useCallback)((_entityId, widgetType) => {
293
3489
  const def = engine.getWidget(widgetType);
294
3490
  if (!def) return null;
295
- if (require_WebGLWidgetLayer.isR3FWidget(def)) return {
3491
+ if (isR3FWidget(def)) return {
296
3492
  surface: "webgl",
297
3493
  component: def.component
298
3494
  };
@@ -306,8 +3502,12 @@ function WidgetProvider({ engine, children }) {
306
3502
  }
307
3503
  //#endregion
308
3504
  //#region src/react/InfiniteCanvas.tsx
309
- const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine, grid, selection, onSelectionChange, onCameraChange, onNavigationChange, className, style, children }, ref) {
3505
+ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine, grid, overlapGlow, selection, snapGuides, onSelectionChange, onCameraChange, onNavigationChange, className, style, children, r3fRoot }, ref) {
310
3506
  const containerRef = (0, react.useRef)(null);
3507
+ const [containerMounted, setContainerMounted] = (0, react.useState)(false);
3508
+ (0, react.useLayoutEffect)(() => {
3509
+ setContainerMounted(true);
3510
+ }, []);
311
3511
  const onSelectionChangeRef = (0, react.useRef)(onSelectionChange);
312
3512
  const onCameraChangeRef = (0, react.useRef)(onCameraChange);
313
3513
  const onNavigationChangeRef = (0, react.useRef)(onNavigationChange);
@@ -320,6 +3520,14 @@ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine
320
3520
  (0, react.useEffect)(() => {
321
3521
  onNavigationChangeRef.current = onNavigationChange;
322
3522
  }, [onNavigationChange]);
3523
+ const selectionRef = (0, react.useRef)(selection);
3524
+ const snapGuidesRef = (0, react.useRef)(snapGuides);
3525
+ (0, react.useEffect)(() => {
3526
+ selectionRef.current = selection;
3527
+ }, [selection]);
3528
+ (0, react.useEffect)(() => {
3529
+ snapGuidesRef.current = snapGuides;
3530
+ }, [snapGuides]);
323
3531
  (0, react.useImperativeHandle)(ref, () => ({
324
3532
  panTo: (x, y) => {
325
3533
  engine.panTo(x, y);
@@ -344,9 +3552,10 @@ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine
344
3552
  getEngine: () => engine
345
3553
  }), [engine]);
346
3554
  const webglCanvasRef = (0, react.useRef)(null);
347
- const gridRendererRef = (0, react.useRef)(null);
348
- const selectionRendererRef = (0, react.useRef)(null);
349
- const cameraLayerRef = (0, react.useRef)(null);
3555
+ const webglManagerRef = (0, react.useRef)(null);
3556
+ const backgroundLayerRef = (0, react.useRef)(null);
3557
+ const baseLayerRef = (0, react.useRef)(null);
3558
+ const overlayLayerRef = (0, react.useRef)(null);
350
3559
  const slotRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
351
3560
  const [visibleEntities, setVisibleEntities] = (0, react.useState)([]);
352
3561
  const registerSlotRef = (0, react.useCallback)((entityId, el) => {
@@ -357,310 +3566,88 @@ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine
357
3566
  const container = containerRef.current;
358
3567
  const canvas = webglCanvasRef.current;
359
3568
  if (!container || !canvas) return;
360
- const gridEnabled = grid !== false;
361
- let gridInst = null;
362
- if (gridEnabled) {
363
- gridInst = new require_WebGLWidgetLayer.GridRenderer(canvas);
364
- gridRendererRef.current = gridInst;
365
- }
366
- const selInst = new require_WebGLWidgetLayer.SelectionRenderer();
367
- selectionRendererRef.current = selInst;
3569
+ const manager = new require_hooks.WebGLManager(canvas, {
3570
+ grid,
3571
+ selection: selectionRef.current,
3572
+ snapGuides: snapGuidesRef.current
3573
+ });
3574
+ webglManagerRef.current = manager;
368
3575
  const updateSize = () => {
369
3576
  const rect = container.getBoundingClientRect();
370
3577
  const dpr = window.devicePixelRatio;
371
3578
  engine.setViewport(rect.width, rect.height, dpr);
372
3579
  canvas.style.width = `${rect.width}px`;
373
3580
  canvas.style.height = `${rect.height}px`;
374
- if (gridInst) gridInst.setSize(rect.width, rect.height, dpr);
375
- selInst.setSize(new three.Vector2(rect.width * dpr, rect.height * dpr), dpr);
3581
+ manager.setSize(rect.width, rect.height, dpr);
376
3582
  };
377
3583
  updateSize();
378
3584
  const observer = new ResizeObserver(updateSize);
379
3585
  observer.observe(container);
380
3586
  return () => {
381
3587
  observer.disconnect();
382
- if (gridInst) {
383
- gridInst.dispose();
384
- gridRendererRef.current = null;
385
- }
386
- selInst.dispose();
387
- selectionRendererRef.current = null;
3588
+ manager.dispose();
3589
+ webglManagerRef.current = null;
388
3590
  };
389
3591
  }, [engine, grid]);
390
3592
  (0, react.useEffect)(() => {
391
- const gridR = gridRendererRef.current;
392
- if (gridR && grid !== false) {
3593
+ const manager = webglManagerRef.current;
3594
+ if (!manager) return;
3595
+ if (grid !== false) {
393
3596
  const isDark = document.documentElement.classList.contains("dark");
394
- gridR.setConfig({
3597
+ manager.setGridConfig({
395
3598
  dotColor: isDark ? [
396
- 1,
397
- 1,
398
- 1
3599
+ .35,
3600
+ .37,
3601
+ .4
399
3602
  ] : [
400
- 0,
401
- 0,
402
- 0
3603
+ .75,
3604
+ .77,
3605
+ .8
403
3606
  ],
404
- dotAlpha: isDark ? .12 : .18,
3607
+ dotAlpha: 1,
405
3608
  ...grid
406
3609
  });
407
3610
  }
408
- const selR = selectionRendererRef.current;
409
- if (selR && selection) selR.setConfig(selection);
3611
+ if (selection) manager.setSelectionConfig(selection);
3612
+ if (snapGuides) manager.setSnapGuideConfig(snapGuides);
410
3613
  engine.markDirty();
411
3614
  }, [
412
3615
  engine,
413
3616
  grid,
414
- selection
3617
+ selection,
3618
+ snapGuides
415
3619
  ]);
416
3620
  (0, react.useEffect)(() => {
3621
+ const merged = mergeOverlapGlow(overlapGlow);
417
3622
  const container = containerRef.current;
418
- if (!container) return;
419
- const onWheel = (e) => {
420
- e.preventDefault();
421
- if (e.ctrlKey || e.metaKey) {
422
- const rect = container.getBoundingClientRect();
423
- engine.zoomAtPoint(e.clientX - rect.left, e.clientY - rect.top, -e.deltaY * .01);
424
- } else engine.panBy(-e.deltaX, -e.deltaY);
425
- };
426
- container.addEventListener("wheel", onWheel, { passive: false });
427
- return () => container.removeEventListener("wheel", onWheel);
428
- }, [engine]);
3623
+ if (container) applyOverlapGlowVars(container, merged);
3624
+ applyOverlapGlowShaderUniforms(merged);
3625
+ engine.markDirty();
3626
+ }, [engine, overlapGlow]);
3627
+ const r3fEventManagerRef = (0, react.useRef)(null);
429
3628
  (0, react.useEffect)(() => {
430
3629
  const container = containerRef.current;
431
3630
  if (!container) return;
432
- let gesture = { type: "idle" };
433
- let lastTapTime = 0;
434
- let lastTapX = 0;
435
- let lastTapY = 0;
436
- const DOUBLE_TAP_MS = 300;
437
- const DOUBLE_TAP_DIST = 30;
438
- function isOnWidget(target) {
439
- let el = target;
440
- while (el && el !== container) {
441
- if (el.hasAttribute("data-widget-slot")) return true;
442
- el = el.parentElement;
443
- }
444
- return false;
445
- }
446
- function isInteractive(target) {
447
- const el = target;
448
- if (!el) return false;
449
- const tag = el.tagName;
450
- return tag === "INPUT" || tag === "TEXTAREA" || tag === "BUTTON" || tag === "SELECT" || el.isContentEditable || el.closest("button") !== null;
451
- }
452
- function getRect() {
453
- return container?.getBoundingClientRect() ?? new DOMRect();
454
- }
455
- function touchDist(t1, t2) {
456
- const dx = t1.clientX - t2.clientX;
457
- const dy = t1.clientY - t2.clientY;
458
- return Math.sqrt(dx * dx + dy * dy);
459
- }
460
- function touchCenter(t1, t2, rect) {
461
- return {
462
- x: (t1.clientX + t2.clientX) / 2 - rect.left,
463
- y: (t1.clientY + t2.clientY) / 2 - rect.top
464
- };
465
- }
466
- function cancelEngineGesture() {
467
- if (gesture.type === "pending-entity" || gesture.type === "entity-dragging") engine.handlePointerUp();
468
- }
469
- const noMods = {
470
- shift: false,
471
- ctrl: false,
472
- alt: false,
473
- meta: false
474
- };
475
- function onTouchStart(e) {
476
- const rect = getRect();
477
- const touches = e.touches;
478
- if (touches.length >= 2) {
479
- e.preventDefault();
480
- cancelEngineGesture();
481
- const dist = touchDist(touches[0], touches[1]);
482
- const center = touchCenter(touches[0], touches[1], rect);
483
- gesture = {
484
- type: "pinching",
485
- lastDist: dist,
486
- lastCx: center.x,
487
- lastCy: center.y
488
- };
489
- return;
490
- }
491
- const touch = touches[0];
492
- const x = touch.clientX - rect.left;
493
- const y = touch.clientY - rect.top;
494
- if (isInteractive(e.target)) return;
495
- e.preventDefault();
496
- const now = Date.now();
497
- if (now - lastTapTime < DOUBLE_TAP_MS && Math.abs(x - lastTapX) < DOUBLE_TAP_DIST && Math.abs(y - lastTapY) < DOUBLE_TAP_DIST) {
498
- lastTapTime = 0;
499
- const directive = engine.handlePointerDown(x, y, 0, noMods);
500
- try {
501
- if (directive.action === "passthrough-track-drag") {
502
- const selected = engine.getSelectedEntities();
503
- if (selected.length === 1) engine.enterContainer(selected[0]);
504
- } else {
505
- const camera = engine.getCamera();
506
- const target = camera.zoom < .9 ? 1 : camera.zoom < 1.8 ? 2 : 1;
507
- engine.zoomAtPoint(x, y, (target - camera.zoom) / camera.zoom);
508
- }
509
- } finally {
510
- engine.handlePointerUp();
511
- engine.markDirty();
512
- }
513
- gesture = { type: "idle" };
514
- return;
515
- }
516
- if (isOnWidget(e.target)) {
517
- engine.handlePointerDown(x, y, 0, noMods);
518
- gesture = {
519
- type: "pending-entity",
520
- x,
521
- y,
522
- time: now
523
- };
524
- } else gesture = {
525
- type: "pending-pan",
526
- x,
527
- y,
528
- time: now
529
- };
530
- }
531
- function onTouchMove(e) {
532
- e.preventDefault();
533
- const rect = getRect();
534
- const touches = e.touches;
535
- if (gesture.type === "pinching" && touches.length >= 2) {
536
- const dist = touchDist(touches[0], touches[1]);
537
- const center = touchCenter(touches[0], touches[1], rect);
538
- const scale = dist / gesture.lastDist;
539
- engine.zoomAtPoint(center.x, center.y, scale - 1);
540
- engine.panBy(center.x - gesture.lastCx, center.y - gesture.lastCy);
541
- gesture.lastDist = dist;
542
- gesture.lastCx = center.x;
543
- gesture.lastCy = center.y;
544
- return;
545
- }
546
- if (touches.length >= 2) {
547
- cancelEngineGesture();
548
- const dist = touchDist(touches[0], touches[1]);
549
- const center = touchCenter(touches[0], touches[1], rect);
550
- gesture = {
551
- type: "pinching",
552
- lastDist: dist,
553
- lastCx: center.x,
554
- lastCy: center.y
555
- };
556
- return;
557
- }
558
- if (touches.length < 1) return;
559
- const touch = touches[0];
560
- const x = touch.clientX - rect.left;
561
- const y = touch.clientY - rect.top;
562
- if (gesture.type === "pending-pan") {
563
- if (Math.abs(x - gesture.x) > 8 || Math.abs(y - gesture.y) > 8) gesture = {
564
- type: "panning",
565
- lastX: x,
566
- lastY: y
567
- };
568
- return;
569
- }
570
- if (gesture.type === "panning") {
571
- engine.panBy(x - gesture.lastX, y - gesture.lastY);
572
- gesture.lastX = x;
573
- gesture.lastY = y;
574
- return;
575
- }
576
- if (gesture.type === "pending-entity" || gesture.type === "entity-dragging") {
577
- engine.handlePointerMove(x, y, noMods);
578
- if (gesture.type === "pending-entity") {
579
- if (Math.abs(x - gesture.x) > 8 || Math.abs(y - gesture.y) > 8) gesture = { type: "entity-dragging" };
580
- }
581
- }
582
- }
583
- function onTouchEnd(e) {
584
- e.preventDefault();
585
- const remaining = e.touches.length;
586
- const rect = getRect();
587
- if (gesture.type === "pinching") {
588
- if (remaining === 1) {
589
- const t = e.touches[0];
590
- gesture = {
591
- type: "panning",
592
- lastX: t.clientX - rect.left,
593
- lastY: t.clientY - rect.top
594
- };
595
- } else if (remaining === 0) gesture = { type: "idle" };
596
- return;
597
- }
598
- if (remaining > 0) return;
599
- if (gesture.type === "pending-pan") {
600
- engine.handlePointerDown(gesture.x, gesture.y, 0, noMods);
601
- engine.handlePointerUp();
602
- engine.markDirty();
603
- lastTapTime = Date.now();
604
- lastTapX = gesture.x;
605
- lastTapY = gesture.y;
606
- }
607
- if (gesture.type === "pending-entity") {
608
- engine.handlePointerUp();
609
- engine.markDirty();
610
- lastTapTime = Date.now();
611
- lastTapX = gesture.x;
612
- lastTapY = gesture.y;
613
- }
614
- if (gesture.type === "entity-dragging") {
615
- engine.handlePointerUp();
616
- engine.markDirty();
617
- }
618
- gesture = { type: "idle" };
619
- }
620
- function onTouchCancel(_e) {
621
- gesture = { type: "idle" };
622
- engine.handlePointerCancel();
623
- }
624
- container.addEventListener("touchstart", onTouchStart, { passive: false });
625
- container.addEventListener("touchmove", onTouchMove, { passive: false });
626
- container.addEventListener("touchend", onTouchEnd, { passive: false });
627
- container.addEventListener("touchcancel", onTouchCancel, { passive: true });
3631
+ const manager = new InputManager(engine, container, [
3632
+ new PointerAdapter(),
3633
+ new WheelAdapter(),
3634
+ new ClickAdapter()
3635
+ ]);
3636
+ manager.addRecognizer(new HoverRecognizer());
3637
+ manager.addRecognizer(new TapRecognizer());
3638
+ manager.addRecognizer(new DoubleTapRecognizer());
3639
+ manager.addRecognizer(new DragRecognizer());
3640
+ manager.addRecognizer(new PinchRecognizer());
3641
+ manager.addRecognizer(new PanRecognizer());
3642
+ manager.setRouter(new R3FRouter(() => r3fEventManagerRef.current));
3643
+ const offHandlers = installEngineHandlers(manager, engine, container);
3644
+ const detach = manager.attach();
628
3645
  return () => {
629
- container.removeEventListener("touchstart", onTouchStart);
630
- container.removeEventListener("touchmove", onTouchMove);
631
- container.removeEventListener("touchend", onTouchEnd);
632
- container.removeEventListener("touchcancel", onTouchCancel);
3646
+ offHandlers();
3647
+ detach();
3648
+ r3fEventManagerRef.current = null;
633
3649
  };
634
3650
  }, [engine]);
635
- const onCanvasPointerDown = (0, react.useCallback)((e) => {
636
- if (e.target?.closest("button, input, textarea, select, [contenteditable]")) return;
637
- const rect = containerRef.current?.getBoundingClientRect();
638
- if (!rect) return;
639
- const directive = engine.handlePointerDown(e.clientX - rect.left, e.clientY - rect.top, e.button, {
640
- shift: e.shiftKey,
641
- ctrl: e.ctrlKey,
642
- alt: e.altKey,
643
- meta: e.metaKey
644
- });
645
- if (directive.action === "capture-resize" || directive.action === "passthrough-track-drag") containerRef.current?.setPointerCapture(e.pointerId);
646
- if (directive.action === "capture-resize") e.preventDefault();
647
- }, [engine]);
648
- const onCanvasPointerMove = (0, react.useCallback)((e) => {
649
- const target = e.target;
650
- if (target.closest?.("[data-widget-slot]") && target !== containerRef.current) return;
651
- const rect = containerRef.current?.getBoundingClientRect();
652
- if (!rect) return;
653
- engine.handlePointerMove(e.clientX - rect.left, e.clientY - rect.top, {
654
- shift: e.shiftKey,
655
- ctrl: e.ctrlKey,
656
- alt: e.altKey,
657
- meta: e.metaKey
658
- });
659
- }, [engine]);
660
- const onCanvasPointerUp = (0, react.useCallback)((e) => {
661
- if (containerRef.current?.hasPointerCapture(e.pointerId)) containerRef.current.releasePointerCapture(e.pointerId);
662
- engine.handlePointerUp();
663
- }, [engine]);
664
3651
  (0, react.useEffect)(() => {
665
3652
  let rafId;
666
3653
  let running = true;
@@ -669,74 +3656,72 @@ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine
669
3656
  if (engine.flushIfDirty()) {
670
3657
  const camera = engine.getCamera();
671
3658
  const changes = engine.getFrameChanges();
672
- if (cameraLayerRef.current) cameraLayerRef.current.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
673
- const cursor = engine.world.getResource(require_hooks.CursorResource).cursor;
3659
+ const transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
3660
+ if (backgroundLayerRef.current) backgroundLayerRef.current.style.transform = transform;
3661
+ if (baseLayerRef.current) baseLayerRef.current.style.transform = transform;
3662
+ if (overlayLayerRef.current) overlayLayerRef.current.style.transform = transform;
3663
+ const cursor = engine.world.getResource(require_ecs.CursorResource).cursor;
674
3664
  if (containerRef.current && containerRef.current.style.cursor !== cursor) containerRef.current.style.cursor = cursor;
675
- const profiler = engine.profiler;
676
- const profilerOn = profiler.isEnabled();
677
- let selectionFramesDrawn = 0;
678
- let snapGuidesDrawn = 0;
679
- let spacingIndicatorsDrawn = 0;
680
- if (gridRendererRef.current) gridRendererRef.current.getWebGLRenderer().info.reset();
681
- if (gridRendererRef.current) {
682
- profiler.beginWebGL("grid");
683
- gridRendererRef.current.render(camera.x, camera.y, camera.zoom);
684
- profiler.endWebGL("grid");
685
- }
686
- if (selectionRendererRef.current && gridRendererRef.current) {
687
- const selected = engine.getSelectedEntities();
3665
+ const manager = webglManagerRef.current;
3666
+ if (manager) {
3667
+ const selectedIds = engine.getSelectedEntities();
688
3668
  const selBounds = [];
689
- for (const id of selected) {
690
- if (!engine.has(id, require_hooks.SelectionFrame)) continue;
691
- const wb = engine.get(id, require_hooks.WorldBounds);
692
- if (wb) selBounds.push({
693
- x: wb.worldX,
694
- y: wb.worldY,
695
- width: wb.worldWidth,
696
- height: wb.worldHeight
3669
+ for (const id of selectedIds) {
3670
+ if (!engine.has(id, require_ecs.SelectionFrame)) continue;
3671
+ const t = engine.get(id, require_ecs.Transform2D);
3672
+ if (t) selBounds.push({
3673
+ x: t.x,
3674
+ y: t.y,
3675
+ width: t.width,
3676
+ height: t.height
697
3677
  });
698
3678
  }
699
3679
  const hovId = engine.getHoveredEntity();
700
3680
  let hovBounds = null;
701
- if (hovId !== null && engine.has(hovId, require_hooks.SelectionFrame)) {
702
- const wb = engine.get(hovId, require_hooks.WorldBounds);
703
- if (wb) hovBounds = {
704
- x: wb.worldX,
705
- y: wb.worldY,
706
- width: wb.worldWidth,
707
- height: wb.worldHeight
3681
+ if (hovId !== null && engine.has(hovId, require_ecs.SelectionFrame)) {
3682
+ const t = engine.get(hovId, require_ecs.Transform2D);
3683
+ if (t) hovBounds = {
3684
+ x: t.x,
3685
+ y: t.y,
3686
+ width: t.width,
3687
+ height: t.height
708
3688
  };
709
3689
  }
710
- const snapGuides = engine.getSnapGuides();
711
- const equalSpacing = engine.getEqualSpacing();
712
- selectionFramesDrawn = selBounds.length + (hovBounds ? 1 : 0);
713
- snapGuidesDrawn = snapGuides.length;
714
- spacingIndicatorsDrawn = equalSpacing.length;
715
- profiler.beginWebGL("selection");
716
- selectionRendererRef.current.render(gridRendererRef.current.getWebGLRenderer(), camera.x, camera.y, camera.zoom, selBounds, hovBounds, snapGuides, equalSpacing);
717
- profiler.endWebGL("selection");
718
- }
719
- if (profilerOn && gridRendererRef.current) {
720
- const info = gridRendererRef.current.getWebGLRenderer().info;
721
- profiler.recordWebGLStats({
722
- drawCalls: info.render.calls,
723
- triangles: info.render.triangles,
724
- selectionFrames: selectionFramesDrawn,
725
- snapGuides: snapGuidesDrawn,
726
- spacingIndicators: spacingIndicatorsDrawn,
3690
+ manager.render({
3691
+ camera: {
3692
+ x: camera.x,
3693
+ y: camera.y,
3694
+ zoom: camera.zoom
3695
+ },
3696
+ selection: {
3697
+ bounds: selBounds,
3698
+ hovered: hovBounds
3699
+ },
3700
+ snap: {
3701
+ guides: engine.getSnapGuides(),
3702
+ spacings: engine.getEqualSpacing(),
3703
+ visible: engine.getSnapGuidesVisible()
3704
+ },
3705
+ profiler: engine.profiler,
727
3706
  domPositionsUpdated: changes.positionsChanged.length
728
3707
  });
729
3708
  }
730
3709
  for (const entityId of changes.positionsChanged) {
731
3710
  const el = slotRefs.current.get(entityId);
732
3711
  if (!el) continue;
733
- const wb = engine.get(entityId, require_hooks.WorldBounds);
734
- if (!wb) continue;
735
- el.style.transform = `translate(${wb.worldX}px, ${wb.worldY}px)`;
736
- el.style.width = `${wb.worldWidth}px`;
737
- el.style.height = `${wb.worldHeight}px`;
3712
+ const t = engine.get(entityId, require_ecs.Transform2D);
3713
+ if (!t) continue;
3714
+ el.style.transform = `translate(${t.x}px, ${t.y}px)`;
3715
+ el.style.width = `${t.width}px`;
3716
+ el.style.height = `${t.height}px`;
738
3717
  }
739
- if (changes.entered.length > 0 || changes.exited.length > 0) setVisibleEntities(engine.getVisibleEntities().map((v) => v.entityId));
3718
+ for (const entityId of changes.zIndicesChanged) {
3719
+ const el = slotRefs.current.get(entityId);
3720
+ if (!el) continue;
3721
+ const z = engine.get(entityId, require_ecs.ZIndex);
3722
+ el.style.zIndex = z ? String(z.value) : "";
3723
+ }
3724
+ if (changes.entered.length > 0 || changes.exited.length > 0 || changes.layersChanged) setVisibleEntities(engine.getVisibleEntities().map((v) => v.entityId));
740
3725
  if (changes.selectionChanged && onSelectionChangeRef.current) onSelectionChangeRef.current(engine.getSelectedEntities());
741
3726
  if (changes.cameraChanged && onCameraChangeRef.current) onCameraChangeRef.current({
742
3727
  x: camera.x,
@@ -744,7 +3729,7 @@ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine
744
3729
  zoom: camera.zoom
745
3730
  });
746
3731
  if (changes.navigationChanged && onNavigationChangeRef.current) {
747
- const navStack = engine.world.getResource(require_hooks.NavigationStackResource);
3732
+ const navStack = engine.world.getResource(require_ecs.NavigationStackResource);
748
3733
  const depth = navStack.frames.length - 1;
749
3734
  const containerId = navStack.frames[navStack.frames.length - 1].containerId;
750
3735
  onNavigationChangeRef.current(depth, containerId);
@@ -756,14 +3741,34 @@ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine
756
3741
  const visible = engine.getVisibleEntities();
757
3742
  setVisibleEntities(visible.map((v) => v.entityId));
758
3743
  const camera = engine.getCamera();
759
- if (cameraLayerRef.current) cameraLayerRef.current.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
760
- if (gridRendererRef.current) gridRendererRef.current.render(camera.x, camera.y, camera.zoom);
3744
+ const initTransform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
3745
+ if (backgroundLayerRef.current) backgroundLayerRef.current.style.transform = initTransform;
3746
+ if (baseLayerRef.current) baseLayerRef.current.style.transform = initTransform;
3747
+ if (overlayLayerRef.current) overlayLayerRef.current.style.transform = initTransform;
3748
+ const manager = webglManagerRef.current;
3749
+ if (manager) manager.render({
3750
+ camera: {
3751
+ x: camera.x,
3752
+ y: camera.y,
3753
+ zoom: camera.zoom
3754
+ },
3755
+ selection: {
3756
+ bounds: [],
3757
+ hovered: null
3758
+ },
3759
+ snap: {
3760
+ guides: [],
3761
+ spacings: [],
3762
+ visible: false
3763
+ }
3764
+ });
761
3765
  for (const v of visible) {
762
3766
  const el = slotRefs.current.get(v.entityId);
763
3767
  if (!el) continue;
764
- el.style.transform = `translate(${v.worldX}px, ${v.worldY}px)`;
765
- el.style.width = `${v.worldWidth}px`;
766
- el.style.height = `${v.worldHeight}px`;
3768
+ el.style.transform = `translate(${v.x}px, ${v.y}px)`;
3769
+ el.style.width = `${v.width}px`;
3770
+ el.style.height = `${v.height}px`;
3771
+ el.style.zIndex = String(v.zIndex);
767
3772
  }
768
3773
  rafId = requestAnimationFrame(loop);
769
3774
  return () => {
@@ -775,24 +3780,33 @@ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine
775
3780
  for (const entityId of visibleEntities) {
776
3781
  const el = slotRefs.current.get(entityId);
777
3782
  if (!el) continue;
778
- const wb = engine.get(entityId, require_hooks.WorldBounds);
779
- if (!wb) continue;
780
- el.style.transform = `translate(${wb.worldX}px, ${wb.worldY}px)`;
781
- el.style.width = `${wb.worldWidth}px`;
782
- el.style.height = `${wb.worldHeight}px`;
3783
+ const t = engine.get(entityId, require_ecs.Transform2D);
3784
+ if (!t) continue;
3785
+ el.style.transform = `translate(${t.x}px, ${t.y}px)`;
3786
+ el.style.width = `${t.width}px`;
3787
+ el.style.height = `${t.height}px`;
3788
+ const z = engine.get(entityId, require_ecs.ZIndex);
3789
+ if (z) el.style.zIndex = String(z.value);
783
3790
  }
784
3791
  }, [visibleEntities, engine]);
785
- const { domEntities, webglEntities } = (0, react.useMemo)(() => {
786
- const dom = [];
3792
+ const { backgroundDom, baseDom, overlayDom, webglEntities } = (0, react.useMemo)(() => {
3793
+ const background = [];
3794
+ const base = [];
3795
+ const overlay = [];
787
3796
  const webgl = [];
788
- for (const id of visibleEntities) if (engine.get(id, require_hooks.Widget)?.surface === "webgl") webgl.push(id);
789
- else dom.push(id);
3797
+ for (const id of visibleEntities) {
3798
+ if (engine.get(id, require_ecs.Widget)?.surface === "webgl") webgl.push(id);
3799
+ const layerName = engine.get(id, require_ecs.Layer)?.name ?? "base";
3800
+ (layerName === "background" ? background : layerName === "overlay" ? overlay : base).push(id);
3801
+ }
790
3802
  return {
791
- domEntities: dom,
3803
+ backgroundDom: background,
3804
+ baseDom: base,
3805
+ overlayDom: overlay,
792
3806
  webglEntities: webgl
793
3807
  };
794
3808
  }, [visibleEntities, engine]);
795
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_hooks.EngineProvider, {
3809
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ecs.EngineProvider, {
796
3810
  value: engine,
797
3811
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_hooks.ContainerRefProvider, {
798
3812
  value: containerRef,
@@ -806,29 +3820,30 @@ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine
806
3820
  touchAction: "none",
807
3821
  backgroundColor: "var(--canvas-bg, #fafafa)"
808
3822
  },
809
- onPointerDown: onCanvasPointerDown,
810
- onPointerMove: onCanvasPointerMove,
811
- onPointerUp: onCanvasPointerUp,
812
3823
  children: [
813
3824
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
814
3825
  ref: webglCanvasRef,
815
3826
  className: "absolute inset-0 pointer-events-none"
816
3827
  }),
817
- webglEntities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WebGLWidgetBridge, {
3828
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "absolute inset-0 pointer-events-none" }),
3829
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayerContainer, {
3830
+ layerRef: backgroundLayerRef,
3831
+ children: bucketSlots(backgroundDom, engine, registerSlotRef)
3832
+ }),
3833
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayerContainer, {
3834
+ layerRef: baseLayerRef,
3835
+ children: bucketSlots(baseDom, engine, registerSlotRef)
3836
+ }),
3837
+ containerMounted && webglEntities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(R3FBridge, {
818
3838
  engine,
819
- entities: webglEntities
3839
+ entities: webglEntities,
3840
+ r3fRoot,
3841
+ eventManagerRef: r3fEventManagerRef
820
3842
  }),
821
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "absolute inset-0 pointer-events-none" }),
822
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
823
- ref: cameraLayerRef,
824
- className: "absolute left-0 top-0 origin-top-left will-change-transform",
825
- children: [domEntities.map((entityId) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_WebGLWidgetLayer.WidgetSlot, {
826
- entityId,
827
- slotRef: registerSlotRef
828
- }, entityId)), webglEntities.map((entityId) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_WebGLWidgetLayer.SelectionOverlaySlot, {
829
- entityId,
830
- slotRef: registerSlotRef
831
- }, entityId))]
3843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayerContainer, {
3844
+ layerRef: overlayLayerRef,
3845
+ zIndex: 2,
3846
+ children: bucketSlots(overlayDom, engine, registerSlotRef)
832
3847
  }),
833
3848
  children
834
3849
  ]
@@ -837,91 +3852,298 @@ const InfiniteCanvas = react.default.forwardRef(function InfiniteCanvas({ engine
837
3852
  })
838
3853
  });
839
3854
  });
840
- /** Bridge component — reads widget resolver from context and passes to WebGLWidgetLayer */
841
- function WebGLWidgetBridge({ engine, entities }) {
3855
+ /** Bridge component — reads widget resolver from context and passes to R3FManager. */
3856
+ function R3FBridge({ engine, entities, r3fRoot, eventManagerRef }) {
842
3857
  const resolver = require_hooks.useWidgetResolver();
843
3858
  const resolve = (0, react.useCallback)((entityId) => {
844
3859
  if (!resolver) return null;
845
- return resolver(entityId, engine.get(entityId, require_hooks.Widget)?.type ?? "");
3860
+ return resolver(entityId, engine.get(entityId, require_ecs.Widget)?.type ?? "");
846
3861
  }, [resolver, engine]);
847
3862
  if (!resolver) return null;
848
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_WebGLWidgetLayer.WebGLWidgetLayer, {
3863
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_hooks.R3FManager, {
849
3864
  engine,
850
3865
  entities,
851
- resolve
3866
+ resolve,
3867
+ r3fRoot,
3868
+ eventManagerRef
3869
+ });
3870
+ }
3871
+ /**
3872
+ * One DOM container for a render layer (RFC-003). Each container holds
3873
+ * the WidgetSlot / SelectionOverlaySlot elements for the entities
3874
+ * bucketed into its layer; the container's CSS transform is driven by
3875
+ * the rAF loop so all layers pan / zoom in lockstep.
3876
+ *
3877
+ * `zIndex` is applied via a one-shot effect (not via React's `style`
3878
+ * prop) so it doesn't fight the rAF loop's direct `style.transform`
3879
+ * writes — and so it doesn't depend on Tailwind's content scanner
3880
+ * picking up the class from inside the library bundle (which it
3881
+ * doesn't, since the library lives in node_modules of the consumer).
3882
+ */
3883
+ function LayerContainer({ layerRef, zIndex, children }) {
3884
+ (0, react.useEffect)(() => {
3885
+ if (zIndex !== void 0 && layerRef.current) layerRef.current.style.zIndex = String(zIndex);
3886
+ }, [layerRef, zIndex]);
3887
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3888
+ ref: layerRef,
3889
+ className: "absolute left-0 top-0 origin-top-left will-change-transform",
3890
+ children
3891
+ });
3892
+ }
3893
+ /**
3894
+ * Renders the right slot per entity in a layer container — DOM widgets
3895
+ * get a `WidgetSlot`, R3F widgets get a `SelectionOverlaySlot` (chrome
3896
+ * + interaction surface; the actual 3D content renders through the R3F
3897
+ * canvas). Pure helper so the JSX in the main component stays compact.
3898
+ */
3899
+ function bucketSlots(entities, engine, registerSlotRef) {
3900
+ return entities.map((entityId) => {
3901
+ return engine.get(entityId, require_ecs.Widget)?.surface === "webgl" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_hooks.SelectionOverlaySlot, {
3902
+ entityId,
3903
+ slotRef: registerSlotRef
3904
+ }, entityId) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_hooks.WidgetSlot, {
3905
+ entityId,
3906
+ slotRef: registerSlotRef
3907
+ }, entityId);
3908
+ });
3909
+ }
3910
+ //#endregion
3911
+ //#region src/react/widgets/card.tsx
3912
+ /**
3913
+ * Visual chrome for an iOS-style card. Reads the entity's `Dragging` tag
3914
+ * and forwards `lifted` to {@link CardChrome}, which owns the actual
3915
+ * appearance (rounded corners, hairline ring, soft drop shadow, lift
3916
+ * transition). Same chrome is used by R3F geometry cards via a DOM slot
3917
+ * beneath the WebGL canvas, so DOM and 3D cards stay visually identical.
3918
+ */
3919
+ function CardFrame({ entityId, children, className, style }) {
3920
+ const dragging = require_ecs.useTag(entityId, require_ecs.Dragging);
3921
+ const overlapCandidate = require_ecs.useTag(entityId, require_ecs.OverlapCandidate);
3922
+ const overlapTarget = require_ecs.useTag(entityId, require_ecs.OverlapTarget);
3923
+ const hot = require_ecs.useComponent(entityId, require_ecs.CardOverlapHotPoint);
3924
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_hooks.CardChrome, {
3925
+ lifted: dragging,
3926
+ className,
3927
+ style,
3928
+ overlapCandidate,
3929
+ overlapTarget,
3930
+ hotX: hot?.x,
3931
+ hotY: hot?.y,
3932
+ hotStrength: hot?.strength,
3933
+ children
852
3934
  });
853
3935
  }
3936
+ /**
3937
+ * Returns a paired widget + archetype for an iOS-style card. Register both
3938
+ * with `createLayoutEngine({ widgets: [card.widget], archetypes: [card.archetype] })`
3939
+ * (or via `engine.registerWidget` / `engine.registerArchetype`) and spawn with
3940
+ * `engine.spawn('your-card-type', { at, data })`.
3941
+ *
3942
+ * The produced widget is non-resizable (Selectable + Draggable only), wrapped
3943
+ * in `<CardFrame>`, and spawns with a `Card` component so `cardSystem` enforces
3944
+ * the preset size each tick.
3945
+ */
3946
+ function createCardWidget(opts) {
3947
+ const defaultSize = require_ecs.DEFAULT_CARD_PRESET_SIZES[opts.size];
3948
+ const Render = opts.render;
3949
+ const Component = ({ entityId }) => {
3950
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFrame, {
3951
+ entityId,
3952
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Render, {
3953
+ entityId,
3954
+ data: useWidgetData(entityId)
3955
+ })
3956
+ });
3957
+ };
3958
+ return {
3959
+ widget: {
3960
+ type: opts.type,
3961
+ schema: opts.schema,
3962
+ defaultData: opts.defaultData,
3963
+ defaultSize,
3964
+ component: Component,
3965
+ interaction: opts.interaction
3966
+ },
3967
+ archetype: {
3968
+ id: opts.type,
3969
+ widget: opts.type,
3970
+ components: [[require_ecs.Card, {
3971
+ preset: opts.size,
3972
+ accepts: opts.accepts ?? [],
3973
+ provides: opts.provides ?? []
3974
+ }]],
3975
+ interactive: {
3976
+ selectable: true,
3977
+ draggable: true,
3978
+ resizable: false,
3979
+ selectionFrame: false,
3980
+ snapSource: false,
3981
+ snapTarget: true
3982
+ },
3983
+ defaultSize
3984
+ }
3985
+ };
3986
+ }
3987
+ //#endregion
3988
+ //#region src/r3f/widgets/geometry-card.tsx
3989
+ /**
3990
+ * Returns a paired R3F widget + archetype for a card-shaped 3D widget.
3991
+ * Behaves like {@link createCardWidget} — fixed preset size, non-resizable,
3992
+ * no engine-drawn selection frame, and lifts on drag (scale + z) — but
3993
+ * renders a three.js scene instead of DOM content.
3994
+ *
3995
+ * The card body and drop shadow are rendered as DOM `<CardChrome>`
3996
+ * beneath the WebGL canvas, not inside the FBO. The user's `geometry`
3997
+ * component renders ONLY the 3D content; chrome is provided by the
3998
+ * `Card` ECS component (the source of truth for all card-shaped
3999
+ * behavior — chrome, lift, drag-promote, compositor discard).
4000
+ *
4001
+ * Pass `withCard: false` to skip card behavior entirely (bare 3D
4002
+ * widget — no chrome, no lift, no discard).
4003
+ *
4004
+ * Lighting: this helper adds no lights. Declare your own in the `geometry`
4005
+ * component (typically a local `pointLight` scoped with `distance`).
4006
+ */
4007
+ function createGeometryCardWidget(opts) {
4008
+ const defaultSize = require_ecs.DEFAULT_CARD_PRESET_SIZES[opts.size];
4009
+ const Render = opts.geometry;
4010
+ const withCard = opts.withCard ?? true;
4011
+ const Component = ({ entityId, width, height }) => {
4012
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Render, {
4013
+ entityId,
4014
+ data: useWidgetData(entityId),
4015
+ width,
4016
+ height
4017
+ });
4018
+ };
4019
+ return {
4020
+ widget: {
4021
+ type: opts.type,
4022
+ surface: "webgl",
4023
+ schema: opts.schema,
4024
+ defaultData: opts.defaultData,
4025
+ defaultSize,
4026
+ component: Component,
4027
+ interaction: opts.interaction
4028
+ },
4029
+ archetype: {
4030
+ id: opts.type,
4031
+ widget: opts.type,
4032
+ components: withCard ? [[require_ecs.Card, {
4033
+ preset: opts.size,
4034
+ background: opts.background ?? "#1C1C1E",
4035
+ accepts: opts.accepts ?? [],
4036
+ provides: opts.provides ?? []
4037
+ }]] : [],
4038
+ interactive: {
4039
+ selectable: true,
4040
+ draggable: true,
4041
+ resizable: false,
4042
+ selectionFrame: false,
4043
+ snapSource: false,
4044
+ snapTarget: true
4045
+ },
4046
+ defaultSize
4047
+ }
4048
+ };
4049
+ }
854
4050
  //#endregion
855
- exports.Active = require_hooks.Active;
856
- exports.BreakpointConfigResource = require_hooks.BreakpointConfigResource;
857
- exports.CameraResource = require_hooks.CameraResource;
858
- exports.Card = require_hooks.Card;
4051
+ exports.Active = require_ecs.Active;
4052
+ exports.BreakpointConfigResource = require_ecs.BreakpointConfigResource;
4053
+ exports.CameraResource = require_ecs.CameraResource;
4054
+ exports.Card = require_ecs.Card;
4055
+ exports.CardChrome = require_hooks.CardChrome;
859
4056
  exports.CardFrame = CardFrame;
860
- exports.CardPresetsResource = require_hooks.CardPresetsResource;
861
- exports.Children = require_hooks.Children;
862
- exports.CommandBuffer = require_WebGLWidgetLayer.CommandBuffer;
863
- exports.Container = require_hooks.Container;
864
- exports.CursorHint = require_hooks.CursorHint;
865
- exports.CursorResource = require_hooks.CursorResource;
866
- exports.DEFAULT_GRID_CONFIG = require_WebGLWidgetLayer.DEFAULT_GRID_CONFIG;
867
- exports.DEFAULT_SELECTION_CONFIG = require_WebGLWidgetLayer.DEFAULT_SELECTION_CONFIG;
868
- exports.Draggable = require_hooks.Draggable;
869
- exports.Dragging = require_hooks.Dragging;
870
- exports.HandleSet = require_hooks.HandleSet;
871
- exports.Hitbox = require_hooks.Hitbox;
4057
+ exports.CardOverlapHotPoint = require_ecs.CardOverlapHotPoint;
4058
+ exports.CardPresetsResource = require_ecs.CardPresetsResource;
4059
+ exports.Children = require_ecs.Children;
4060
+ exports.CommandBuffer = CommandBuffer;
4061
+ exports.ConsumeCommand = ConsumeCommand;
4062
+ exports.Container = require_ecs.Container;
4063
+ exports.ContainerCamera = require_ecs.ContainerCamera;
4064
+ exports.ContainerChildren = require_ecs.ContainerChildren;
4065
+ exports.Culled = require_ecs.Culled;
4066
+ exports.CursorHint = require_ecs.CursorHint;
4067
+ exports.CursorResource = require_ecs.CursorResource;
4068
+ exports.DEFAULT_GRID_CONFIG = require_hooks.DEFAULT_GRID_CONFIG;
4069
+ exports.DEFAULT_OVERLAP_GLOW_CONFIG = DEFAULT_OVERLAP_GLOW_CONFIG;
4070
+ exports.DEFAULT_SELECTION_CONFIG = require_hooks.DEFAULT_SELECTION_CONFIG;
4071
+ exports.DEFAULT_SNAP_GUIDE_CONFIG = require_hooks.DEFAULT_SNAP_GUIDE_CONFIG;
4072
+ exports.Draggable = require_ecs.Draggable;
4073
+ exports.Dragging = require_ecs.Dragging;
872
4074
  exports.InfiniteCanvas = InfiniteCanvas;
873
- exports.InteractionRole = require_hooks.InteractionRole;
874
- exports.Locked = require_hooks.Locked;
875
- exports.MoveCommand = require_WebGLWidgetLayer.MoveCommand;
876
- exports.NavigationStackResource = require_hooks.NavigationStackResource;
877
- exports.Parent = require_hooks.Parent;
878
- exports.Resizable = require_hooks.Resizable;
879
- exports.ResizeCommand = require_WebGLWidgetLayer.ResizeCommand;
880
- exports.Selectable = require_hooks.Selectable;
881
- exports.Selected = require_hooks.Selected;
882
- exports.SelectionFrame = require_hooks.SelectionFrame;
883
- exports.SetComponentCommand = require_WebGLWidgetLayer.SetComponentCommand;
884
- exports.Transform2D = require_hooks.Transform2D;
885
- exports.ViewportResource = require_hooks.ViewportResource;
886
- exports.Visible = require_hooks.Visible;
887
- exports.Widget = require_hooks.Widget;
888
- exports.WidgetBreakpoint = require_hooks.WidgetBreakpoint;
889
- exports.WidgetData = require_hooks.WidgetData;
4075
+ exports.InteractionRole = require_ecs.InteractionRole;
4076
+ exports.Layer = require_ecs.Layer;
4077
+ exports.LayerOrderResource = require_ecs.LayerOrderResource;
4078
+ exports.Locked = require_ecs.Locked;
4079
+ exports.MoveCommand = MoveCommand;
4080
+ exports.NavigationStackResource = require_ecs.NavigationStackResource;
4081
+ exports.OverlapCandidate = require_ecs.OverlapCandidate;
4082
+ exports.OverlapTarget = require_ecs.OverlapTarget;
4083
+ exports.ParentFrame = require_ecs.ParentFrame;
4084
+ exports.Resizable = require_ecs.Resizable;
4085
+ exports.ResizeCommand = ResizeCommand;
4086
+ exports.RootCameraResource = require_ecs.RootCameraResource;
4087
+ exports.Selectable = require_ecs.Selectable;
4088
+ exports.Selected = require_ecs.Selected;
4089
+ exports.SelectionFrame = require_ecs.SelectionFrame;
4090
+ exports.SetComponentCommand = SetComponentCommand;
4091
+ exports.SnapSource = require_ecs.SnapSource;
4092
+ exports.SnapTarget = require_ecs.SnapTarget;
4093
+ exports.Transform2D = require_ecs.Transform2D;
4094
+ exports.TransformTween = require_ecs.TransformTween;
4095
+ exports.ViewportResource = require_ecs.ViewportResource;
4096
+ exports.Visible = require_ecs.Visible;
4097
+ exports.Widget = require_ecs.Widget;
4098
+ exports.WidgetBreakpoint = require_ecs.WidgetBreakpoint;
4099
+ exports.WidgetData = require_ecs.WidgetData;
890
4100
  exports.WidgetProvider = WidgetProvider;
891
4101
  exports.WidgetResolverProvider = require_hooks.WidgetResolverProvider;
892
- exports.WorldBounds = require_hooks.WorldBounds;
893
- exports.ZIndex = require_hooks.ZIndex;
894
- exports.ZoomConfigResource = require_hooks.ZoomConfigResource;
895
- exports.clamp = require_WebGLWidgetLayer.clamp;
896
- exports.createArchetypeRegistry = require_WebGLWidgetLayer.createArchetypeRegistry;
4102
+ exports.ZIndex = require_ecs.ZIndex;
4103
+ exports.ZOOM_BANDS = require_hooks.ZOOM_BANDS;
4104
+ exports.ZoomConfigResource = require_ecs.ZoomConfigResource;
4105
+ exports.aabbToRect = aabbToRect;
4106
+ exports.clamp = clamp;
4107
+ exports.createArchetypeRegistry = createArchetypeRegistry;
897
4108
  exports.createCardWidget = createCardWidget;
898
4109
  exports.createGeometryCardWidget = createGeometryCardWidget;
899
- exports.createLayoutEngine = require_WebGLWidgetLayer.createLayoutEngine;
900
- exports.createWidgetRegistry = require_WebGLWidgetLayer.createWidgetRegistry;
901
- exports.intersectsAABB = require_WebGLWidgetLayer.intersectsAABB;
902
- exports.isR3FWidget = require_WebGLWidgetLayer.isR3FWidget;
903
- exports.pointInAABB = require_WebGLWidgetLayer.pointInAABB;
904
- exports.screenToWorld = require_WebGLWidgetLayer.screenToWorld;
905
- exports.useAllEntities = require_hooks.useAllEntities;
4110
+ exports.createLayoutEngine = createLayoutEngine;
4111
+ exports.createWidgetRegistry = createWidgetRegistry;
4112
+ exports.intersectsAABB = intersectsAABB;
4113
+ exports.isFrameAncestorOf = isFrameAncestorOf;
4114
+ exports.isOutOfBand = require_hooks.isOutOfBand;
4115
+ exports.isR3FWidget = isR3FWidget;
4116
+ exports.pointInAABB = pointInAABB;
4117
+ exports.rectToAABB = rectToAABB;
4118
+ exports.rehydrateEntity = rehydrateEntity;
4119
+ exports.screenToWorld = screenToWorld;
4120
+ exports.selectBand = require_hooks.selectBand;
4121
+ exports.snapshotEntity = snapshotEntity;
4122
+ exports.useAllEntities = require_ecs.useAllEntities;
906
4123
  exports.useBreakpoint = useBreakpoint;
907
- exports.useCamera = require_hooks.useCamera;
4124
+ exports.useCamera = require_ecs.useCamera;
908
4125
  exports.useChildren = useChildren;
909
- exports.useComponent = require_hooks.useComponent;
4126
+ exports.useComponent = require_ecs.useComponent;
910
4127
  exports.useContainerRef = require_hooks.useContainerRef;
911
- exports.useEntityComponents = require_hooks.useEntityComponents;
912
- exports.useEntityTags = require_hooks.useEntityTags;
4128
+ exports.useEntityComponents = require_ecs.useEntityComponents;
4129
+ exports.useEntityTags = require_ecs.useEntityTags;
913
4130
  exports.useIsSelected = useIsSelected;
914
- exports.useLayoutEngine = require_hooks.useLayoutEngine;
915
- exports.useQuery = require_hooks.useQuery;
916
- exports.useRegisteredComponents = require_hooks.useRegisteredComponents;
917
- exports.useRegisteredTags = require_hooks.useRegisteredTags;
918
- exports.useResource = require_hooks.useResource;
919
- exports.useTag = require_hooks.useTag;
920
- exports.useTaggedEntities = require_hooks.useTaggedEntities;
4131
+ exports.useLayoutEngine = require_ecs.useLayoutEngine;
4132
+ exports.useQuery = require_ecs.useQuery;
4133
+ exports.useRegisteredComponents = require_ecs.useRegisteredComponents;
4134
+ exports.useRegisteredTags = require_ecs.useRegisteredTags;
4135
+ exports.useResource = require_ecs.useResource;
4136
+ exports.useSharedGeometry = require_hooks.useSharedGeometry;
4137
+ exports.useSharedMaterial = require_hooks.useSharedMaterial;
4138
+ exports.useSharedTexture = require_hooks.useSharedTexture;
4139
+ exports.useTag = require_ecs.useTag;
4140
+ exports.useTaggedEntities = require_ecs.useTaggedEntities;
921
4141
  exports.useUpdateWidget = useUpdateWidget;
4142
+ exports.useWidgetAnimation = require_hooks.useWidgetAnimation;
922
4143
  exports.useWidgetData = useWidgetData;
4144
+ exports.useWidgetInvalidate = require_hooks.useWidgetInvalidate;
4145
+ exports.useWidgetPhase = require_hooks.useWidgetPhase;
923
4146
  exports.useWidgetResolver = require_hooks.useWidgetResolver;
924
- exports.worldBoundsToAABB = require_WebGLWidgetLayer.worldBoundsToAABB;
925
- exports.worldToScreen = require_WebGLWidgetLayer.worldToScreen;
4147
+ exports.worldToScreen = worldToScreen;
926
4148
 
927
4149
  //# sourceMappingURL=index.cjs.map