@jamesyong42/infinite-canvas 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +65 -0
  2. package/dist/advanced.cjs +61 -24
  3. package/dist/advanced.cjs.map +1 -1
  4. package/dist/advanced.d.cts +180 -64
  5. package/dist/advanced.d.cts.map +1 -1
  6. package/dist/advanced.d.mts +180 -64
  7. package/dist/advanced.d.mts.map +1 -1
  8. package/dist/advanced.mjs +29 -12
  9. package/dist/advanced.mjs.map +1 -1
  10. package/dist/devtools.cjs +22 -22
  11. package/dist/devtools.cjs.map +1 -1
  12. package/dist/devtools.d.cts +2 -2
  13. package/dist/devtools.d.cts.map +1 -1
  14. package/dist/devtools.d.mts +2 -2
  15. package/dist/devtools.d.mts.map +1 -1
  16. package/dist/devtools.mjs +2 -2
  17. package/dist/devtools.mjs.map +1 -1
  18. package/dist/{hooks-BwY7rRHg.mjs → ecs-3kimUV5Z.mjs} +238 -74
  19. package/dist/ecs-3kimUV5Z.mjs.map +1 -0
  20. package/dist/{hooks-DHShH86C.cjs → ecs-B4QrqfvQ.cjs} +320 -108
  21. package/dist/ecs-B4QrqfvQ.cjs.map +1 -0
  22. package/dist/hooks-CtP02JNt.cjs +3762 -0
  23. package/dist/hooks-CtP02JNt.cjs.map +1 -0
  24. package/dist/hooks-gsQDDE56.mjs +3494 -0
  25. package/dist/hooks-gsQDDE56.mjs.map +1 -0
  26. package/dist/index-3GY7T8JM.d.mts +480 -0
  27. package/dist/index-3GY7T8JM.d.mts.map +1 -0
  28. package/dist/index-B7B1tRPl.d.cts +480 -0
  29. package/dist/index-B7B1tRPl.d.cts.map +1 -0
  30. package/dist/index-DSdbSQ_t.d.cts +1451 -0
  31. package/dist/index-DSdbSQ_t.d.cts.map +1 -0
  32. package/dist/index-Dj9odADH.d.mts +1451 -0
  33. package/dist/index-Dj9odADH.d.mts.map +1 -0
  34. package/dist/index.cjs +3865 -643
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +315 -138
  37. package/dist/index.d.cts.map +1 -1
  38. package/dist/index.d.mts +315 -138
  39. package/dist/index.d.mts.map +1 -1
  40. package/dist/index.mjs +3767 -571
  41. package/dist/index.mjs.map +1 -1
  42. package/package.json +1 -1
  43. package/dist/SelectionRenderer-CR2PBQwx.d.cts +0 -105
  44. package/dist/SelectionRenderer-CR2PBQwx.d.cts.map +0 -1
  45. package/dist/SelectionRenderer-DlsBstAq.d.mts +0 -105
  46. package/dist/SelectionRenderer-DlsBstAq.d.mts.map +0 -1
  47. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs +0 -3560
  48. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs.map +0 -1
  49. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs +0 -3375
  50. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs.map +0 -1
  51. package/dist/engine-BfbvWXSk.d.mts +0 -982
  52. package/dist/engine-BfbvWXSk.d.mts.map +0 -1
  53. package/dist/engine-CCjuFMC-.d.cts +0 -982
  54. package/dist/engine-CCjuFMC-.d.cts.map +0 -1
  55. package/dist/hooks-BwY7rRHg.mjs.map +0 -1
  56. package/dist/hooks-DHShH86C.cjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { defineComponent, defineResource, defineTag } from "@jamesyong42/reactive-ecs";
2
2
  import { createContext, useContext, useEffect, useRef, useState } from "react";
3
- //#region src/components.ts
3
+ //#region src/ecs/components.ts
4
4
  /** Position, size, and rotation of an entity in local coordinates (world units). */
5
5
  const Transform2D = defineComponent("Transform2D", {
6
6
  x: 0,
@@ -9,19 +9,57 @@ const Transform2D = defineComponent("Transform2D", {
9
9
  height: 100,
10
10
  rotation: 0
11
11
  });
12
- /** Computed world-space bounding box. Read-only -- updated by the transform propagation system. */
13
- const WorldBounds = defineComponent("WorldBounds", {
14
- worldX: 0,
15
- worldY: 0,
16
- worldWidth: 0,
17
- worldHeight: 0
18
- });
19
12
  /** Rendering and hit-test ordering. Higher values render on top. */
20
13
  const ZIndex = defineComponent("ZIndex", { value: 0 });
21
- /** Parent entity reference. Used for nested containers and handle sync. */
22
- const Parent = defineComponent("Parent", { id: 0 });
14
+ /**
15
+ * Generic animated transition of `Transform2D.x` / `.y` over time.
16
+ * Runtime-only: the tween component is auto-removed on completion,
17
+ * and in-flight tweens are not serialized (the destination Transform2D
18
+ * is what survives a save/load).
19
+ *
20
+ * Introduced for RFC-004 Phase 4 fly-back but designed to be reusable
21
+ * for any future position-animation need (snap, drop-in, consume-pop).
22
+ * Starting a new tween on an entity that already has one overwrites.
23
+ */
24
+ const TransformTween = defineComponent("TransformTween", {
25
+ fromX: 0,
26
+ fromY: 0,
27
+ toX: 0,
28
+ toY: 0,
29
+ /** `performance.now()`-ish timestamp captured at tween start. */
30
+ startMs: 0,
31
+ /** Total duration in ms. */
32
+ durationMs: 250,
33
+ easing: "ease-out",
34
+ /**
35
+ * Discriminator so downstream systems can react per kind
36
+ * (e.g. `'flyback'`, `'snap'`, `'spawn'`). The tween system
37
+ * itself is kind-agnostic.
38
+ */
39
+ kind: "generic"
40
+ });
41
+ /**
42
+ * Container-hierarchy parenthood: the entity lives inside another
43
+ * entity's sub-canvas. Read by `navigationFilterSystem` to filter the
44
+ * active widget set by the current navigation frame.
45
+ *
46
+ * Container children have their own `Transform2D` in the container's
47
+ * local coord space — **no coord accumulation** through this reference.
48
+ * Replaces the old `Parent` component's container-hierarchy usage
49
+ * (RFC-005).
50
+ */
51
+ const ParentFrame = defineComponent("ParentFrame", { id: 0 });
23
52
  /** Child entity IDs. Used for nested containers and handle sync. */
24
53
  const Children = defineComponent("Children", { ids: [] });
54
+ /**
55
+ * Ordered list of the entities a container owns (RFC-004 § Phase 5).
56
+ * Redundant with `ParentFrame` (the inverse relation) but materialised
57
+ * so UI / compositor paths can cheaply read "give me this container's
58
+ * children" without a reverse-index scan. `applyMutation` /
59
+ * `revertMutation` keep the two in sync. Serialized; IDs are remapped
60
+ * alongside `ParentFrame` on load.
61
+ */
62
+ const ContainerChildren = defineComponent("ContainerChildren", { ids: [] });
25
63
  /** Marks an entity as a renderable widget with a type identifier and rendering surface. */
26
64
  const Widget = defineComponent("Widget", {
27
65
  surface: "dom",
@@ -36,38 +74,100 @@ const WidgetBreakpoint = defineComponent("WidgetBreakpoint", {
36
74
  screenHeight: 0
37
75
  });
38
76
  /**
39
- * Marks an entity as an iOS-style card with a fixed preset size.
40
- * The `cardSystem` reconciles `Transform2D.width/height` from the preset
41
- * each tick, so cards cannot be resized freely — change `preset` instead.
77
+ * Marks an entity as an iOS-style card with a fixed preset size, AND
78
+ * opts the entity into the full card-shaped behavior bundle:
79
+ *
80
+ * - DOM `<CardChrome>` slot (rounded body, hairline ring, shadow,
81
+ * CSS lift transition on Dragging)
82
+ * - drag-promote to the 'overlay' layer for DOM cards (so dragged
83
+ * DOM cards visually pop above the R3F canvas)
84
+ * - composition discard rect for R3F cards (so other R3F widgets
85
+ * are clipped out of the dragged card's screen rect — defends
86
+ * the chrome from being painted over)
87
+ * - drop-to-consume contracts (`accepts` / `provides`) — RFC-004.
88
+ *
89
+ * Widgets *without* `Card` get none of this — they render bare (no
90
+ * chrome, no lift transition), and on drag they only get the
91
+ * compositor's renderOrder bump if they're R3F (so they stack on
92
+ * top of other R3F widgets they overlap).
93
+ *
94
+ * The `cardSystem` reconciles `Transform2D.width/height` from the
95
+ * preset each tick, so cards cannot be resized freely — change
96
+ * `preset` instead.
97
+ */
98
+ const Card = defineComponent("Card", {
99
+ preset: "small",
100
+ /**
101
+ * CSS background for the chrome's surface (any valid background value;
102
+ * defaults to the dark iOS card colour). Picked up by the
103
+ * `<CardChrome>` component rendered for this entity.
104
+ */
105
+ background: "#1C1C1E",
106
+ /**
107
+ * Drop-to-consume contract — what this card accepts as a *parent*.
108
+ * An incoming dragged card's `provides` must intersect this list
109
+ * for the consume mechanic to fire (RFC-004 § Phase 3). Empty
110
+ * array = card never consumes anything.
111
+ */
112
+ accepts: [],
113
+ /**
114
+ * Drop-to-consume contract — what this card offers when dropped
115
+ * as a *child*. Empty array = card is never consumed by anything.
116
+ */
117
+ provides: []
118
+ });
119
+ /**
120
+ * Transient — set by the card-overlap pass on every card whose AABB
121
+ * intersects the dragged card's AABB during drag. Layer 1 of the
122
+ * two-layer overlap visual state (RFC-004 § Phase 3). Cleared on drag
123
+ * end. Runtime-only — not serialized.
124
+ */
125
+ const OverlapCandidate = defineTag("OverlapCandidate");
126
+ /**
127
+ * Transient — set on the single primary candidate (closest centre
128
+ * distance) iff its `accepts` contract intersects the dragged card's
129
+ * `provides` contract AND the optional `canAccept` gate passes.
130
+ * Layer 2 of the two-layer overlap visual state. Cleared on drag end
131
+ * or when match becomes false. Runtime-only — not serialized.
132
+ */
133
+ const OverlapTarget = defineTag("OverlapTarget");
134
+ /**
135
+ * Per-candidate "hot point" for the position-dependent radial glow
136
+ * (RFC-004 § Phase 3). `x` and `y` are normalized [0..1] local coords
137
+ * within the overlapped card, pointing at the intersection centroid.
138
+ * `strength` ramps between 0 and 1 for the fade-in / fade-out.
139
+ * Runtime-only — not serialized.
42
140
  */
43
- const Card = defineComponent("Card", { preset: "small" });
141
+ const CardOverlapHotPoint = defineComponent("CardOverlapHotPoint", {
142
+ x: .5,
143
+ y: .5,
144
+ strength: 0
145
+ });
44
146
  /** Marks an entity as an enterable container (double-click/double-tap to enter). */
45
147
  const Container = defineComponent("Container", { enterable: true });
46
148
  /**
47
- * Rectangular interactable region anchored relative to the parent entity's WorldBounds.
48
- * Anchor values are in 0..1 space: 0 = parent min edge, 1 = parent max edge.
49
- * Widget bodies do NOT need Hitboxtheir WorldBounds is already their hit area.
50
- * Hitbox is only for sub-entities (handles, ports) whose position is parent-relative.
149
+ * Per-container persistent camera state. When the user navigates out of
150
+ * a container, their current pan/zoom is snapshotted here; when they
151
+ * navigate back in, it's restored. Serializedcontainers remember
152
+ * their view across save/load (RFC-004 § Phase 0c).
51
153
  */
52
- const Hitbox = defineComponent("Hitbox", {
53
- anchorX: 0,
54
- anchorY: 0,
55
- width: 0,
56
- height: 0
154
+ const ContainerCamera = defineComponent("ContainerCamera", {
155
+ x: 0,
156
+ y: 0,
157
+ zoom: 1
57
158
  });
58
159
  /**
59
160
  * Declares what happens when this entity is hit, plus its hit-test priority.
60
- * Canonical layers: 0=canvas, 5=widget body, 10=edge handles, 15=corner handles, 20=reserved.
161
+ * Canonical layers: 0=canvas, 5=widget body, 20=reserved.
162
+ *
163
+ * Resize corner/edge roles are emitted inline by `interaction.ts` from the
164
+ * selected `Resizable` widget's Transform2D; they are NOT stored as
165
+ * per-handle entities (RFC-005).
61
166
  */
62
167
  const InteractionRole = defineComponent("InteractionRole", {
63
168
  layer: 0,
64
169
  role: { type: "canvas" }
65
170
  });
66
- /**
67
- * Component on the parent entity listing the EntityIds of its spawned handle children.
68
- * Enables O(1) cascade destroy without a reverse-index scan of Parent components.
69
- */
70
- const HandleSet = defineComponent("HandleSet", { ids: [] });
71
171
  /** Declares the cursor this entity requests when hovered and when active. */
72
172
  const CursorHint = defineComponent("CursorHint", {
73
173
  hover: "default",
@@ -79,6 +179,32 @@ const Selectable = defineTag("Selectable");
79
179
  const Draggable = defineTag("Draggable");
80
180
  /** Marks an entity as resizable via edge/corner handles. */
81
181
  const Resizable = defineTag("Resizable");
182
+ /**
183
+ * Marks an entity that, when dragged, runs alignment-snap math against
184
+ * the set of `SnapTarget` entities. Without this tag, dragging proceeds
185
+ * with raw pointer deltas (no snapping). Independent of `SnapTarget`:
186
+ * an entity can be a source without being a target (rare) or a target
187
+ * without being a source (e.g. cards — they participate as references
188
+ * for other widgets but never snap themselves).
189
+ *
190
+ * Multi-select drag: only the first selected entity's bounds drive the
191
+ * snap calculation; followers receive the same correction delta. Tag a
192
+ * follower-only entity with `SnapSource` if you also want it to lead a
193
+ * single-entity drag, but be aware its geometry is ignored when it is
194
+ * being dragged as part of a multi-select group.
195
+ */
196
+ const SnapSource = defineTag("SnapSource");
197
+ /**
198
+ * Marks an entity whose bounds are usable as a snap reference when
199
+ * another `SnapSource` entity is being dragged. References are further
200
+ * filtered by `Active`, so only entities in the current navigation
201
+ * frame can pull a drag — a `SnapTarget` inside a closed container
202
+ * does not leak into a root-level drag. Visibility of the resulting
203
+ * guide lines is controlled separately by the engine's
204
+ * `snap.guidesVisible` config — this tag only governs participation
205
+ * in the math.
206
+ */
207
+ const SnapTarget = defineTag("SnapTarget");
82
208
  /** Prevents an entity from being moved or resized. */
83
209
  const Locked = defineTag("Locked");
84
210
  /** Indicates the entity is currently selected. */
@@ -100,18 +226,41 @@ const SelectionFrame = defineTag("SelectionFrame");
100
226
  const Active = defineTag("Active");
101
227
  /** Indicates the entity is within the visible viewport. Set by the cull system. */
102
228
  const Visible = defineTag("Visible");
229
+ /**
230
+ * Indicates the entity is `Active` but **outside** the visible viewport
231
+ * (+overscan). The complement of `Visible` for Active entities — the cull
232
+ * system maintains the invariant that every Active entity carries exactly
233
+ * one of `Visible` or `Culled`.
234
+ *
235
+ * Render layers consume this to keep state cached without rendering: DOM
236
+ * widgets may stay mounted-but-hidden for fast re-reveal, and the R3F
237
+ * compositor (RFC-002) holds widget render targets in its Cold pool.
238
+ */
239
+ const Culled = defineTag("Culled");
240
+ const Layer = defineComponent("Layer", { name: "base" });
241
+ const PreDragLayer = defineComponent("PreDragLayer", { name: "base" });
103
242
  //#endregion
104
- //#region src/resources.ts
243
+ //#region src/ecs/resources.ts
105
244
  /**
106
245
  * Output sink for the cursor system. Written by cursorSystem each tick;
107
246
  * read by the RAF loop to apply style.cursor on the root container div.
108
247
  */
109
248
  const CursorResource = defineResource("Cursor", { cursor: "default" });
110
- /** Camera state: world-space position (x, y) and zoom level. Updated by pan/zoom gestures. */
249
+ /**
250
+ * Camera state: world-space position (x, y) and zoom level.
251
+ *
252
+ * `gesturing` is true while the user is actively manipulating the camera
253
+ * (continuous wheel zoom, pinch, two-finger pan). Set/cleared by gesture
254
+ * handlers via {@link LayoutEngine.setGesturing}; render layers can use it
255
+ * to defer expensive work (e.g. the R3F compositor skips zoom-band
256
+ * repaints while gesturing so a continuous pinch doesn't trigger a
257
+ * repaint storm across every visible widget).
258
+ */
111
259
  const CameraResource = defineResource("Camera", {
112
260
  x: 0,
113
261
  y: 0,
114
- zoom: 1
262
+ zoom: 1,
263
+ gesturing: false
115
264
  });
116
265
  /** Viewport dimensions in CSS pixels and device pixel ratio. Updated on resize. */
117
266
  const ViewportResource = defineResource("Viewport", {
@@ -131,57 +280,77 @@ const BreakpointConfigResource = defineResource("BreakpointConfig", {
131
280
  normal: 500,
132
281
  expanded: 1200
133
282
  });
134
- /** Navigation stack for hierarchical container traversal. */
283
+ /**
284
+ * Navigation stack for hierarchical container traversal. Always has at
285
+ * least one frame (the root). The last element is the current frame;
286
+ * `containerId === null` means the user is at the root canvas.
287
+ *
288
+ * Runtime-only view state — deliberately not serialized, so reloading a
289
+ * saved canvas always drops the user at the root frame (RFC-004 § Phase 0c).
290
+ */
135
291
  const NavigationStackResource = defineResource("NavigationStack", {
136
- frames: [{
137
- containerId: null,
138
- camera: {
139
- x: 0,
140
- y: 0,
141
- zoom: 1
142
- }
143
- }],
292
+ frames: [{ containerId: null }],
144
293
  changed: false
145
294
  });
146
295
  /**
296
+ * Camera state for the root canvas. Persisted (serialized) so the root
297
+ * view returns to its previous pan/zoom across navigation push/pop and
298
+ * across save/load. Container frames use the `ContainerCamera` component
299
+ * on the container entity instead (RFC-004 § Phase 0c).
300
+ */
301
+ const RootCameraResource = defineResource("RootCamera", {
302
+ x: 0,
303
+ y: 0,
304
+ zoom: 1
305
+ });
306
+ /**
307
+ * Built-in card preset sizes (iOS widget conventions — 155×155 tile).
308
+ * Single source of truth consumed by:
309
+ * - {@link CardPresetsResource} — runtime lookup by the cardSystem.
310
+ * - `createCardWidget` / `createGeometryCardWidget` — widget-registration-
311
+ * time `defaultSize`, which must be known before the engine is built.
312
+ */
313
+ const DEFAULT_CARD_PRESET_SIZES = {
314
+ small: {
315
+ width: 155,
316
+ height: 155
317
+ },
318
+ medium: {
319
+ width: 329,
320
+ height: 155
321
+ },
322
+ large: {
323
+ width: 329,
324
+ height: 345
325
+ },
326
+ xl: {
327
+ width: 329,
328
+ height: 535
329
+ }
330
+ };
331
+ /**
147
332
  * iOS-style card preset size map. Lookup happens by `Card.preset`; the
148
333
  * `cardSystem` stamps `Transform2D.width/height` from the resolved size.
149
334
  *
150
- * Defaults mirror iOS widget conventions — 155×155 tile + 19px gap.
151
335
  * Override at `createLayoutEngine({ cardPresets })` for tablet-scale or
152
336
  * custom design systems.
153
337
  */
154
338
  const CardPresetsResource = defineResource("CardPresets", {
155
- presets: {
156
- small: {
157
- width: 155,
158
- height: 155
159
- },
160
- medium: {
161
- width: 329,
162
- height: 155
163
- },
164
- large: {
165
- width: 329,
166
- height: 345
167
- },
168
- xl: {
169
- width: 329,
170
- height: 535
171
- }
172
- },
339
+ presets: { ...DEFAULT_CARD_PRESET_SIZES },
173
340
  /** Gap between adjacent tiles (future tile-snap system reads this). */
174
341
  gap: 19
175
342
  });
343
+ /** ECS resource holding the SpatialIndex instance for viewport culling and hit testing. */
344
+ const SpatialIndexResource = defineResource("SpatialIndex", { instance: null });
345
+ const LayerOrderResource = defineResource("LayerOrder", { layers: [
346
+ "background",
347
+ "base",
348
+ "overlay"
349
+ ] });
176
350
  //#endregion
177
- //#region src/react/context.ts
351
+ //#region src/react/context/engine-context.ts
178
352
  const EngineContext = createContext(null);
179
353
  const EngineProvider = EngineContext.Provider;
180
- const ContainerRefContext = createContext(null);
181
- const ContainerRefProvider = ContainerRefContext.Provider;
182
- function useContainerRef() {
183
- return useContext(ContainerRefContext);
184
- }
185
354
  /**
186
355
  * Returns the LayoutEngine instance from the nearest InfiniteCanvas context.
187
356
  * Throws if used outside an InfiniteCanvas provider.
@@ -191,13 +360,8 @@ function useLayoutEngine() {
191
360
  if (!engine) throw new Error("useLayoutEngine must be used within an <InfiniteCanvas>");
192
361
  return engine;
193
362
  }
194
- const WidgetResolverContext = createContext(null);
195
- const WidgetResolverProvider = WidgetResolverContext.Provider;
196
- function useWidgetResolver() {
197
- return useContext(WidgetResolverContext);
198
- }
199
363
  //#endregion
200
- //#region src/react/hooks.ts
364
+ //#region src/react/hooks/ecs.ts
201
365
  function shallowEqual(a, b) {
202
366
  const keysA = Object.keys(a);
203
367
  const keysB = Object.keys(b);
@@ -420,6 +584,6 @@ function useRegisteredTags() {
420
584
  return types;
421
585
  }
422
586
  //#endregion
423
- export { Draggable as A, SelectionFrame as B, ViewportResource as C, Children as D, Card as E, Locked as F, WidgetData as G, Visible as H, Parent as I, WorldBounds as K, Resizable as L, HandleSet as M, Hitbox as N, Container as O, InteractionRole as P, Selectable as R, NavigationStackResource as S, Active as T, Widget as U, Transform2D as V, WidgetBreakpoint as W, useWidgetResolver as _, useEntityTags as a, CardPresetsResource as b, useRegisteredTags as c, useTaggedEntities as d, ContainerRefProvider as f, useLayoutEngine as g, useContainerRef as h, useEntityComponents as i, Dragging as j, CursorHint as k, useResource as l, WidgetResolverProvider as m, useCamera as n, useQuery as o, EngineProvider as p, ZIndex as q, useComponent as r, useRegisteredComponents as s, useAllEntities as t, useTag as u, BreakpointConfigResource as v, ZoomConfigResource as w, CursorResource as x, CameraResource as y, Selected as z };
587
+ export { WidgetBreakpoint as $, ContainerCamera as A, OverlapTarget as B, ViewportResource as C, CardOverlapHotPoint as D, Card as E, Dragging as F, Selected as G, PreDragLayer as H, InteractionRole as I, SnapTarget as J, SelectionFrame as K, Layer as L, Culled as M, CursorHint as N, Children as O, Draggable as P, Widget as Q, Locked as R, SpatialIndexResource as S, Active as T, Resizable as U, ParentFrame as V, Selectable as W, TransformTween as X, Transform2D as Y, Visible as Z, CursorResource as _, useEntityTags as a, NavigationStackResource as b, useRegisteredTags as c, useTaggedEntities as d, WidgetData as et, EngineProvider as f, CardPresetsResource as g, CameraResource as h, useEntityComponents as i, ContainerChildren as j, Container as k, useResource as l, BreakpointConfigResource as m, useCamera as n, useQuery as o, useLayoutEngine as p, SnapSource as q, useComponent as r, useRegisteredComponents as s, useAllEntities as t, ZIndex as tt, useTag as u, DEFAULT_CARD_PRESET_SIZES as v, ZoomConfigResource as w, RootCameraResource as x, LayerOrderResource as y, OverlapCandidate as z };
424
588
 
425
- //# sourceMappingURL=hooks-BwY7rRHg.mjs.map
589
+ //# sourceMappingURL=ecs-3kimUV5Z.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ecs-3kimUV5Z.mjs","names":[],"sources":["../src/ecs/components.ts","../src/ecs/resources.ts","../src/react/context/engine-context.ts","../src/react/hooks/ecs.ts"],"sourcesContent":["import type { EntityId } from '@jamesyong42/reactive-ecs';\nimport { defineComponent, defineTag } from '@jamesyong42/reactive-ecs';\n\n// === Spatial ===\n\n/** Position, size, and rotation of an entity in local coordinates (world units). */\nexport const Transform2D = defineComponent('Transform2D', {\n\tx: 0,\n\ty: 0,\n\twidth: 100,\n\theight: 100,\n\trotation: 0,\n});\n\n/** Rendering and hit-test ordering. Higher values render on top. */\nexport const ZIndex = defineComponent('ZIndex', { value: 0 });\n\n/** Easing curves supported by `transformTweenSystem` (RFC-004 § Phase 2). */\nexport type TweenEasing = 'linear' | 'ease-out' | 'ease-in-out' | 'spring';\n\n/**\n * Generic animated transition of `Transform2D.x` / `.y` over time.\n * Runtime-only: the tween component is auto-removed on completion,\n * and in-flight tweens are not serialized (the destination Transform2D\n * is what survives a save/load).\n *\n * Introduced for RFC-004 Phase 4 fly-back but designed to be reusable\n * for any future position-animation need (snap, drop-in, consume-pop).\n * Starting a new tween on an entity that already has one overwrites.\n */\nexport const TransformTween = defineComponent('TransformTween', {\n\tfromX: 0,\n\tfromY: 0,\n\ttoX: 0,\n\ttoY: 0,\n\t/** `performance.now()`-ish timestamp captured at tween start. */\n\tstartMs: 0,\n\t/** Total duration in ms. */\n\tdurationMs: 250,\n\teasing: 'ease-out' as TweenEasing,\n\t/**\n\t * Discriminator so downstream systems can react per kind\n\t * (e.g. `'flyback'`, `'snap'`, `'spawn'`). The tween system\n\t * itself is kind-agnostic.\n\t */\n\tkind: 'generic',\n});\n\n// === Hierarchy ===\n\n/**\n * Container-hierarchy parenthood: the entity lives inside another\n * entity's sub-canvas. Read by `navigationFilterSystem` to filter the\n * active widget set by the current navigation frame.\n *\n * Container children have their own `Transform2D` in the container's\n * local coord space — **no coord accumulation** through this reference.\n * Replaces the old `Parent` component's container-hierarchy usage\n * (RFC-005).\n */\nexport const ParentFrame = defineComponent('ParentFrame', { id: 0 as EntityId });\n\n/** Child entity IDs. Used for nested containers and handle sync. */\nexport const Children = defineComponent('Children', { ids: [] as EntityId[] });\n\n/**\n * Ordered list of the entities a container owns (RFC-004 § Phase 5).\n * Redundant with `ParentFrame` (the inverse relation) but materialised\n * so UI / compositor paths can cheaply read \"give me this container's\n * children\" without a reverse-index scan. `applyMutation` /\n * `revertMutation` keep the two in sync. Serialized; IDs are remapped\n * alongside `ParentFrame` on load.\n */\nexport const ContainerChildren = defineComponent('ContainerChildren', {\n\tids: [] as EntityId[],\n});\n\n// === Widget ===\n\n/** Marks an entity as a renderable widget with a type identifier and rendering surface. */\nexport const Widget = defineComponent('Widget', {\n\tsurface: 'dom' as 'dom' | 'webgl' | 'webview',\n\ttype: '' as string,\n});\n\n/** Arbitrary application data attached to a widget entity. Access via useWidgetData(). */\nexport const WidgetData = defineComponent('WidgetData', {\n\tdata: {} as Record<string, unknown>,\n});\n\n/** Computed responsive breakpoint based on screen-space size. Read-only. */\nexport const WidgetBreakpoint = defineComponent('WidgetBreakpoint', {\n\tcurrent: 'normal' as 'micro' | 'compact' | 'normal' | 'expanded' | 'detailed',\n\tscreenWidth: 0,\n\tscreenHeight: 0,\n});\n\n// === Card ===\n\n/** iOS-style card size presets. Actual dimensions live in CardPresetsResource. */\nexport type CardPreset = 'small' | 'medium' | 'large' | 'xl';\n\n/**\n * Marks an entity as an iOS-style card with a fixed preset size, AND\n * opts the entity into the full card-shaped behavior bundle:\n *\n * - DOM `<CardChrome>` slot (rounded body, hairline ring, shadow,\n * CSS lift transition on Dragging)\n * - drag-promote to the 'overlay' layer for DOM cards (so dragged\n * DOM cards visually pop above the R3F canvas)\n * - composition discard rect for R3F cards (so other R3F widgets\n * are clipped out of the dragged card's screen rect — defends\n * the chrome from being painted over)\n * - drop-to-consume contracts (`accepts` / `provides`) — RFC-004.\n *\n * Widgets *without* `Card` get none of this — they render bare (no\n * chrome, no lift transition), and on drag they only get the\n * compositor's renderOrder bump if they're R3F (so they stack on\n * top of other R3F widgets they overlap).\n *\n * The `cardSystem` reconciles `Transform2D.width/height` from the\n * preset each tick, so cards cannot be resized freely — change\n * `preset` instead.\n */\nexport const Card = defineComponent('Card', {\n\tpreset: 'small' as CardPreset,\n\t/**\n\t * CSS background for the chrome's surface (any valid background value;\n\t * defaults to the dark iOS card colour). Picked up by the\n\t * `<CardChrome>` component rendered for this entity.\n\t */\n\tbackground: '#1C1C1E',\n\t/**\n\t * Drop-to-consume contract — what this card accepts as a *parent*.\n\t * An incoming dragged card's `provides` must intersect this list\n\t * for the consume mechanic to fire (RFC-004 § Phase 3). Empty\n\t * array = card never consumes anything.\n\t */\n\taccepts: [] as readonly string[],\n\t/**\n\t * Drop-to-consume contract — what this card offers when dropped\n\t * as a *child*. Empty array = card is never consumed by anything.\n\t */\n\tprovides: [] as readonly string[],\n});\n\n/**\n * Transient — set by the card-overlap pass on every card whose AABB\n * intersects the dragged card's AABB during drag. Layer 1 of the\n * two-layer overlap visual state (RFC-004 § Phase 3). Cleared on drag\n * end. Runtime-only — not serialized.\n */\nexport const OverlapCandidate = defineTag('OverlapCandidate');\n\n/**\n * Transient — set on the single primary candidate (closest centre\n * distance) iff its `accepts` contract intersects the dragged card's\n * `provides` contract AND the optional `canAccept` gate passes.\n * Layer 2 of the two-layer overlap visual state. Cleared on drag end\n * or when match becomes false. Runtime-only — not serialized.\n */\nexport const OverlapTarget = defineTag('OverlapTarget');\n\n/**\n * Per-candidate \"hot point\" for the position-dependent radial glow\n * (RFC-004 § Phase 3). `x` and `y` are normalized [0..1] local coords\n * within the overlapped card, pointing at the intersection centroid.\n * `strength` ramps between 0 and 1 for the fade-in / fade-out.\n * Runtime-only — not serialized.\n */\nexport const CardOverlapHotPoint = defineComponent('CardOverlapHotPoint', {\n\tx: 0.5,\n\ty: 0.5,\n\tstrength: 0,\n});\n\n// === Container ===\n\n/** Marks an entity as an enterable container (double-click/double-tap to enter). */\nexport const Container = defineComponent('Container', { enterable: true });\n\n/**\n * Per-container persistent camera state. When the user navigates out of\n * a container, their current pan/zoom is snapshotted here; when they\n * navigate back in, it's restored. Serialized — containers remember\n * their view across save/load (RFC-004 § Phase 0c).\n */\nexport const ContainerCamera = defineComponent('ContainerCamera', {\n\tx: 0,\n\ty: 0,\n\tzoom: 1,\n});\n\n// === Interaction ===\n\n/** Resize handle positions — 4 edges + 4 corners. */\nexport type ResizeHandlePos = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';\n\n/** Discriminated union of interaction roles an entity can fulfil. */\nexport type InteractionRoleType =\n\t| { type: 'drag' }\n\t| { type: 'select' }\n\t| { type: 'resize'; handle: ResizeHandlePos }\n\t| { type: 'rotate' }\n\t| { type: 'connect' }\n\t| { type: 'canvas' };\n\nexport type InteractionRoleData = {\n\t/** Hit-test priority — higher wins when multiple entities contain the point. */\n\tlayer: number;\n\t/** Discriminated role + role-specific data. */\n\trole: InteractionRoleType;\n};\n\n/**\n * Declares what happens when this entity is hit, plus its hit-test priority.\n * Canonical layers: 0=canvas, 5=widget body, 20=reserved.\n *\n * Resize corner/edge roles are emitted inline by `interaction.ts` from the\n * selected `Resizable` widget's Transform2D; they are NOT stored as\n * per-handle entities (RFC-005).\n */\nexport const InteractionRole = defineComponent<InteractionRoleData>('InteractionRole', {\n\tlayer: 0,\n\trole: { type: 'canvas' },\n});\n\n/** CSS cursor values the canvas may request. */\nexport type CSSCursor =\n\t| 'default'\n\t| 'grab'\n\t| 'grabbing'\n\t| 'crosshair'\n\t| 'n-resize'\n\t| 's-resize'\n\t| 'e-resize'\n\t| 'w-resize'\n\t| 'ne-resize'\n\t| 'nw-resize'\n\t| 'se-resize'\n\t| 'sw-resize';\n\nexport type CursorHintData = {\n\t/** Cursor when this entity is hovered in idle state. */\n\thover: CSSCursor;\n\t/** Cursor while this entity is being dragged/resized. */\n\tactive: CSSCursor;\n};\n\n/** Declares the cursor this entity requests when hovered and when active. */\nexport const CursorHint = defineComponent<CursorHintData>('CursorHint', {\n\thover: 'default',\n\tactive: 'default',\n});\n\n// === Tags ===\n\n/** Marks an entity as selectable by click or marquee. */\nexport const Selectable = defineTag('Selectable');\n/** Marks an entity as draggable via pointer interaction. */\nexport const Draggable = defineTag('Draggable');\n/** Marks an entity as resizable via edge/corner handles. */\nexport const Resizable = defineTag('Resizable');\n/**\n * Marks an entity that, when dragged, runs alignment-snap math against\n * the set of `SnapTarget` entities. Without this tag, dragging proceeds\n * with raw pointer deltas (no snapping). Independent of `SnapTarget`:\n * an entity can be a source without being a target (rare) or a target\n * without being a source (e.g. cards — they participate as references\n * for other widgets but never snap themselves).\n *\n * Multi-select drag: only the first selected entity's bounds drive the\n * snap calculation; followers receive the same correction delta. Tag a\n * follower-only entity with `SnapSource` if you also want it to lead a\n * single-entity drag, but be aware its geometry is ignored when it is\n * being dragged as part of a multi-select group.\n */\nexport const SnapSource = defineTag('SnapSource');\n/**\n * Marks an entity whose bounds are usable as a snap reference when\n * another `SnapSource` entity is being dragged. References are further\n * filtered by `Active`, so only entities in the current navigation\n * frame can pull a drag — a `SnapTarget` inside a closed container\n * does not leak into a root-level drag. Visibility of the resulting\n * guide lines is controlled separately by the engine's\n * `snap.guidesVisible` config — this tag only governs participation\n * in the math.\n */\nexport const SnapTarget = defineTag('SnapTarget');\n/** Prevents an entity from being moved or resized. */\nexport const Locked = defineTag('Locked');\n/** Indicates the entity is currently selected. */\nexport const Selected = defineTag('Selected');\n/**\n * Indicates the entity is currently being dragged by the user.\n * Added after the drag dead-zone is crossed; removed on pointer up/cancel.\n * Renderers read this to apply transient drag affordances (e.g. scale/shadow lift).\n */\nexport const Dragging = defineTag('Dragging');\n/**\n * Entities with this tag get the engine-drawn selection + hover outline frame.\n * Granted automatically to Selectable entities unless explicitly disabled via\n * `Archetype.interactive.selectionFrame: false`. Widgets that render their own\n * selected/hover chrome (e.g. iOS-style cards) opt out.\n */\nexport const SelectionFrame = defineTag('SelectionFrame');\n/** Indicates the entity is currently being interacted with (drag, resize). */\nexport const Active = defineTag('Active');\n/** Indicates the entity is within the visible viewport. Set by the cull system. */\nexport const Visible = defineTag('Visible');\n/**\n * Indicates the entity is `Active` but **outside** the visible viewport\n * (+overscan). The complement of `Visible` for Active entities — the cull\n * system maintains the invariant that every Active entity carries exactly\n * one of `Visible` or `Culled`.\n *\n * Render layers consume this to keep state cached without rendering: DOM\n * widgets may stay mounted-but-hidden for fast re-reveal, and the R3F\n * compositor (RFC-002) holds widget render targets in its Cold pool.\n */\nexport const Culled = defineTag('Culled');\n\n// === Layer system (RFC-003) ===\n\n/**\n * Named DOM stacking layer a widget renders into. Three layers are\n * rendered out of the box by `<InfiniteCanvas>`:\n *\n * `'background'` — DOM widgets behind everything user-content.\n * `'base'` — default; DOM widgets and R3F card chrome.\n * `'overlay'` — DOM widgets and R3F chrome promoted above the R3F\n * canvas (e.g. dragged widget, future tooltips).\n *\n * Per-widget `ZIndex` continues to control intra-layer ordering;\n * `Layer.name` picks which layer container the widget mounts into.\n *\n * R3F widgets always render through the R3F canvas regardless of layer\n * — `Layer.name` only controls where their CSS chrome / interaction\n * surface mounts.\n */\nexport type LayerName = 'background' | 'base' | 'overlay';\n\nexport type LayerData = {\n\tname: LayerName;\n};\n\nexport const Layer = defineComponent<LayerData>('Layer', { name: 'base' });\n\n/**\n * Sidecar component on a widget that has been promoted to a higher\n * layer by `dragPromoteSystem`; stores the widget's pre-drag\n * `Layer.name` so it can be restored when `Dragging` is removed.\n *\n * Internal — not part of the public API. Serialization-skipped because\n * it only carries transient interaction state.\n */\nexport type PreDragLayerData = {\n\tname: LayerName;\n};\n\nexport const PreDragLayer = defineComponent<PreDragLayerData>('PreDragLayer', {\n\tname: 'base',\n});\n","import type { EntityId } from '@jamesyong42/reactive-ecs';\nimport { defineResource } from '@jamesyong42/reactive-ecs';\nimport type { CardPreset, CSSCursor } from './components.js';\nimport type { SpatialIndex } from './spatial/SpatialIndex.js';\n\n/**\n * A single frame in the navigation stack. `containerId === null` is the\n * root canvas; any other value is the entity id of the container whose\n * sub-canvas the user is currently inside. Camera state lives on a\n * `ContainerCamera` component per container (or `RootCameraResource`\n * for the root frame), not in the stack itself — see RFC-004 § Phase 0c.\n */\nexport interface NavigationFrame {\n\tcontainerId: EntityId | null;\n}\n\n/** Data shape for the CursorResource. */\nexport type CursorResourceData = {\n\tcursor: CSSCursor;\n};\n\n/**\n * Output sink for the cursor system. Written by cursorSystem each tick;\n * read by the RAF loop to apply style.cursor on the root container div.\n */\nexport const CursorResource = defineResource<CursorResourceData>('Cursor', {\n\tcursor: 'default',\n});\n\n/**\n * Camera state: world-space position (x, y) and zoom level.\n *\n * `gesturing` is true while the user is actively manipulating the camera\n * (continuous wheel zoom, pinch, two-finger pan). Set/cleared by gesture\n * handlers via {@link LayoutEngine.setGesturing}; render layers can use it\n * to defer expensive work (e.g. the R3F compositor skips zoom-band\n * repaints while gesturing so a continuous pinch doesn't trigger a\n * repaint storm across every visible widget).\n */\nexport const CameraResource = defineResource('Camera', {\n\tx: 0,\n\ty: 0,\n\tzoom: 1,\n\tgesturing: false,\n});\n\n/** Viewport dimensions in CSS pixels and device pixel ratio. Updated on resize. */\nexport const ViewportResource = defineResource('Viewport', {\n\twidth: 0,\n\theight: 0,\n\tdpr: 1,\n});\n\n/** Minimum and maximum zoom levels. */\nexport const ZoomConfigResource = defineResource('ZoomConfig', {\n\tmin: 0.1,\n\tmax: 5.0,\n});\n\n/** Screen-space pixel thresholds for responsive breakpoints (micro/compact/normal/expanded/detailed). */\nexport const BreakpointConfigResource = defineResource('BreakpointConfig', {\n\tmicro: 40,\n\tcompact: 120,\n\tnormal: 500,\n\texpanded: 1200,\n});\n\n/**\n * Navigation stack for hierarchical container traversal. Always has at\n * least one frame (the root). The last element is the current frame;\n * `containerId === null` means the user is at the root canvas.\n *\n * Runtime-only view state — deliberately not serialized, so reloading a\n * saved canvas always drops the user at the root frame (RFC-004 § Phase 0c).\n */\nexport const NavigationStackResource = defineResource('NavigationStack', {\n\tframes: [{ containerId: null }] as NavigationFrame[],\n\tchanged: false,\n});\n\n/** Shape shared by `ContainerCamera` components and `RootCameraResource`. */\nexport type FrameCameraState = { x: number; y: number; zoom: number };\n\n/**\n * Camera state for the root canvas. Persisted (serialized) so the root\n * view returns to its previous pan/zoom across navigation push/pop and\n * across save/load. Container frames use the `ContainerCamera` component\n * on the container entity instead (RFC-004 § Phase 0c).\n */\nexport const RootCameraResource = defineResource<FrameCameraState>('RootCamera', {\n\tx: 0,\n\ty: 0,\n\tzoom: 1,\n});\n\n/** Responsive breakpoint name derived from a widget's screen-space size. */\nexport type Breakpoint = 'micro' | 'compact' | 'normal' | 'expanded' | 'detailed';\n\n/**\n * Built-in card preset sizes (iOS widget conventions — 155×155 tile).\n * Single source of truth consumed by:\n * - {@link CardPresetsResource} — runtime lookup by the cardSystem.\n * - `createCardWidget` / `createGeometryCardWidget` — widget-registration-\n * time `defaultSize`, which must be known before the engine is built.\n */\nexport const DEFAULT_CARD_PRESET_SIZES: Record<CardPreset, { width: number; height: number }> = {\n\tsmall: { width: 155, height: 155 },\n\tmedium: { width: 329, height: 155 },\n\tlarge: { width: 329, height: 345 },\n\txl: { width: 329, height: 535 },\n};\n\n/**\n * iOS-style card preset size map. Lookup happens by `Card.preset`; the\n * `cardSystem` stamps `Transform2D.width/height` from the resolved size.\n *\n * Override at `createLayoutEngine({ cardPresets })` for tablet-scale or\n * custom design systems.\n */\nexport const CardPresetsResource = defineResource('CardPresets', {\n\tpresets: { ...DEFAULT_CARD_PRESET_SIZES },\n\t/** Gap between adjacent tiles (future tile-snap system reads this). */\n\tgap: 19,\n});\n\n/** ECS resource holding the SpatialIndex instance for viewport culling and hit testing. */\nexport const SpatialIndexResource = defineResource('SpatialIndex', {\n\tinstance: null as SpatialIndex | null,\n});\n\n/**\n * Render-layer order, low → high. `<InfiniteCanvas>` mounts a DOM\n * container for each entry; widgets render into the container that\n * matches their `Layer.name`. Per-widget `ZIndex` controls intra-layer\n * ordering. RFC-003.\n *\n * Default: `['background', 'base', 'overlay']`. Out-of-the-box the\n * three names map to fixed DOM positions in `<InfiniteCanvas>`'s\n * stacking sandwich: background and base sit beneath the R3F canvas\n * (zIndex < 1), overlay sits above it (zIndex 2). Custom layer names\n * are not yet rendered by the default `<InfiniteCanvas>`.\n */\nexport type LayerOrderData = {\n\tlayers: import('./components.js').LayerName[];\n};\n\nexport const LayerOrderResource = defineResource<LayerOrderData>('LayerOrder', {\n\tlayers: ['background', 'base', 'overlay'],\n});\n","import { createContext, useContext } from 'react';\nimport type { LayoutEngine } from '../../ecs/engine/index.js';\n\nconst EngineContext = createContext<LayoutEngine | null>(null);\n\nexport const EngineProvider = EngineContext.Provider;\n\n/**\n * Returns the LayoutEngine instance from the nearest InfiniteCanvas context.\n * Throws if used outside an InfiniteCanvas provider.\n */\nexport function useLayoutEngine(): LayoutEngine {\n\tconst engine = useContext(EngineContext);\n\tif (!engine) {\n\t\tthrow new Error('useLayoutEngine must be used within an <InfiniteCanvas>');\n\t}\n\treturn engine;\n}\n","import type { ComponentType, EntityId, ResourceType, TagType } from '@jamesyong42/reactive-ecs';\nimport { useEffect, useRef, useState } from 'react';\nimport { CameraResource } from '../../ecs/resources.js';\nimport { useLayoutEngine } from '../context/engine-context.js';\n\nfunction shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {\n\tconst keysA = Object.keys(a);\n\tconst keysB = Object.keys(b);\n\tif (keysA.length !== keysB.length) return false;\n\tfor (const key of keysA) {\n\t\tif (a[key] !== b[key]) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Reactively reads an ECS component from an entity.\n * Returns undefined if the entity doesn't have the component. Re-renders when the component changes.\n */\nexport function useComponent<T>(entity: EntityId, type: ComponentType<T>): T | undefined {\n\tconst engine = useLayoutEngine();\n\tconst [value, setValue] = useState<T | undefined>(() => engine.get(entity, type));\n\n\tuseEffect(() => {\n\t\tconst current = engine.get(entity, type);\n\t\tsetValue(current === undefined ? undefined : { ...current });\n\n\t\tconst unsub = engine.world.onComponentChanged(\n\t\t\ttype,\n\t\t\t(_id, _prev, next) => {\n\t\t\t\tsetValue(next === undefined ? undefined : { ...next });\n\t\t\t},\n\t\t\tentity,\n\t\t);\n\n\t\treturn unsub;\n\t}, [engine, entity, type]);\n\n\treturn value;\n}\n\n/**\n * Reactively checks whether an entity has a tag.\n * Re-renders when the tag is added or removed.\n */\nexport function useTag(entity: EntityId, type: TagType): boolean {\n\tconst engine = useLayoutEngine();\n\tconst [has, setHas] = useState(() => engine.world.hasTag(entity, type));\n\n\tuseEffect(() => {\n\t\tsetHas(engine.world.hasTag(entity, type));\n\n\t\tconst unsub1 = engine.world.onTagAdded(type, () => setHas(true), entity);\n\t\tconst unsub2 = engine.world.onTagRemoved(type, () => setHas(false), entity);\n\n\t\treturn () => {\n\t\t\tunsub1();\n\t\t\tunsub2();\n\t\t};\n\t}, [engine, entity, type]);\n\n\treturn has;\n}\n\n/**\n * Reactively reads an ECS resource (singleton data).\n * Re-renders when any field of the resource changes (shallow comparison).\n */\nexport function useResource<T>(type: ResourceType<T>): T {\n\tconst engine = useLayoutEngine();\n\tconst [value, setValue] = useState<T>(() => ({ ...engine.world.getResource(type) }));\n\tconst prevRef = useRef<T | undefined>(undefined);\n\n\tuseEffect(() => {\n\t\t// Immediately sync to current value on (re-)subscription\n\t\tconst current = engine.world.getResource(type);\n\t\tif (current !== undefined) {\n\t\t\tprevRef.current = current;\n\t\t\tsetValue({ ...current });\n\t\t}\n\n\t\tconst unsub = engine.onFrame(() => {\n\t\t\tconst current = engine.world.getResource(type);\n\t\t\tif (\n\t\t\t\tprevRef.current === undefined ||\n\t\t\t\t!shallowEqual(\n\t\t\t\t\tcurrent as unknown as Record<string, unknown>,\n\t\t\t\t\tprevRef.current as unknown as Record<string, unknown>,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tprevRef.current = current;\n\t\t\t\tsetValue({ ...current });\n\t\t\t}\n\t\t});\n\t\treturn unsub;\n\t}, [engine, type]);\n\n\treturn value;\n}\n\n/**\n * Returns entity IDs matching all specified component/tag types.\n * Re-renders when the result set changes.\n */\nexport function useQuery(...types: (ComponentType | TagType)[]): EntityId[] {\n\tconst engine = useLayoutEngine();\n\tconst typesRef = useRef(types);\n\ttypesRef.current = types;\n\tconst typesKey = types.map((t) => t.name).join('\\0');\n\n\tconst [result, setResult] = useState<EntityId[]>(() => engine.world.query(...types));\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: typesKey is a stable proxy for the types array\n\tuseEffect(() => {\n\t\tsetResult(engine.world.query(...typesRef.current));\n\n\t\tconst unsub = engine.onFrame(() => {\n\t\t\tconst next = engine.world.query(...typesRef.current);\n\t\t\tsetResult((prev) => {\n\t\t\t\tif (prev.length !== next.length) return next;\n\t\t\t\tfor (let i = 0; i < prev.length; i++) {\n\t\t\t\t\tif (prev[i] !== next[i]) return next;\n\t\t\t\t}\n\t\t\t\treturn prev;\n\t\t\t});\n\t\t});\n\t\treturn unsub;\n\t}, [engine, typesKey]);\n\n\treturn result;\n}\n\n/**\n * Returns all entity IDs that have the specified tag.\n * Re-renders when entities are tagged or untagged.\n */\nexport function useTaggedEntities(type: TagType): EntityId[] {\n\tconst engine = useLayoutEngine();\n\tconst [result, setResult] = useState<EntityId[]>(() => engine.world.queryTagged(type));\n\n\tuseEffect(() => {\n\t\tsetResult([...engine.world.queryTagged(type)]);\n\n\t\tconst update = () => setResult([...engine.world.queryTagged(type)]);\n\t\tconst unsub1 = engine.world.onTagAdded(type, update);\n\t\tconst unsub2 = engine.world.onTagRemoved(type, update);\n\t\treturn () => {\n\t\t\tunsub1();\n\t\t\tunsub2();\n\t\t};\n\t}, [engine, type]);\n\n\treturn result;\n}\n\n/**\n * Returns the current camera state {x, y, zoom}.\n * Shorthand for useResource(CameraResource).\n */\nexport function useCamera(): { x: number; y: number; zoom: number } {\n\tconst cam = useResource(CameraResource);\n\treturn { x: cam?.x ?? 0, y: cam?.y ?? 0, zoom: cam?.zoom ?? 1 };\n}\n\nfunction sameIdList(a: readonly number[], b: readonly number[]): boolean {\n\tif (a.length !== b.length) return false;\n\tfor (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n\treturn true;\n}\n\nfunction sameTypeList<T extends { name: string }>(a: readonly T[], b: readonly T[]): boolean {\n\tif (a.length !== b.length) return false;\n\tfor (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n\treturn true;\n}\n\n/**\n * Reactively returns the IDs of every live entity in the world.\n * Updates on entity create and destroy.\n */\nexport function useAllEntities(): EntityId[] {\n\tconst engine = useLayoutEngine();\n\tconst [entities, setEntities] = useState<EntityId[]>(() => engine.world.getAllEntities());\n\n\tuseEffect(() => {\n\t\tconst refresh = () => {\n\t\t\tconst next = engine.world.getAllEntities();\n\t\t\tsetEntities((prev) => (sameIdList(prev, next) ? prev : next));\n\t\t};\n\t\trefresh();\n\t\tconst unsub1 = engine.world.onEntityCreated(refresh);\n\t\tconst unsub2 = engine.world.onEntityDestroyed(refresh);\n\t\treturn () => {\n\t\t\tunsub1();\n\t\t\tunsub2();\n\t\t};\n\t}, [engine]);\n\n\treturn entities;\n}\n\n/**\n * Reactively returns the ComponentTypes currently attached to an entity.\n * Polls per frame (engine only ticks when dirty), but never re-renders unless the set changes.\n */\nexport function useEntityComponents(entity: EntityId): ComponentType[] {\n\tconst engine = useLayoutEngine();\n\tconst [types, setTypes] = useState<ComponentType[]>(() => engine.world.getComponentsOf(entity));\n\n\tuseEffect(() => {\n\t\tsetTypes(engine.world.getComponentsOf(entity));\n\t\tconst unsub = engine.onFrame(() => {\n\t\t\tconst next = engine.world.getComponentsOf(entity);\n\t\t\tsetTypes((prev) => (sameTypeList(prev, next) ? prev : next));\n\t\t});\n\t\treturn unsub;\n\t}, [engine, entity]);\n\n\treturn types;\n}\n\n/**\n * Reactively returns the TagTypes currently attached to an entity.\n */\nexport function useEntityTags(entity: EntityId): TagType[] {\n\tconst engine = useLayoutEngine();\n\tconst [types, setTypes] = useState<TagType[]>(() => engine.world.getTagsOf(entity));\n\n\tuseEffect(() => {\n\t\tsetTypes(engine.world.getTagsOf(entity));\n\t\tconst unsub = engine.onFrame(() => {\n\t\t\tconst next = engine.world.getTagsOf(entity);\n\t\t\tsetTypes((prev) => (sameTypeList(prev, next) ? prev : next));\n\t\t});\n\t\treturn unsub;\n\t}, [engine, entity]);\n\n\treturn types;\n}\n\n/**\n * Reactively returns every ComponentType the world has observed.\n * Grows over time as new component types are first used.\n */\nexport function useRegisteredComponents(): ComponentType[] {\n\tconst engine = useLayoutEngine();\n\tconst [types, setTypes] = useState<ComponentType[]>(() => engine.world.getRegisteredComponents());\n\n\tuseEffect(() => {\n\t\tsetTypes(engine.world.getRegisteredComponents());\n\t\tconst unsub = engine.onFrame(() => {\n\t\t\tconst next = engine.world.getRegisteredComponents();\n\t\t\tsetTypes((prev) => (sameTypeList(prev, next) ? prev : next));\n\t\t});\n\t\treturn unsub;\n\t}, [engine]);\n\n\treturn types;\n}\n\n/**\n * Reactively returns every TagType the world has observed.\n */\nexport function useRegisteredTags(): TagType[] {\n\tconst engine = useLayoutEngine();\n\tconst [types, setTypes] = useState<TagType[]>(() => engine.world.getRegisteredTags());\n\n\tuseEffect(() => {\n\t\tsetTypes(engine.world.getRegisteredTags());\n\t\tconst unsub = engine.onFrame(() => {\n\t\t\tconst next = engine.world.getRegisteredTags();\n\t\t\tsetTypes((prev) => (sameTypeList(prev, next) ? prev : next));\n\t\t});\n\t\treturn unsub;\n\t}, [engine]);\n\n\treturn types;\n}\n"],"mappings":"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"}