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