@jamesyong42/infinite-canvas 1.1.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 (63) hide show
  1. package/README.md +170 -8
  2. package/dist/advanced.cjs +148 -167
  3. package/dist/advanced.cjs.map +1 -1
  4. package/dist/advanced.d.cts +193 -61
  5. package/dist/advanced.d.cts.map +1 -0
  6. package/dist/advanced.d.mts +215 -0
  7. package/dist/advanced.d.mts.map +1 -0
  8. package/dist/advanced.mjs +122 -0
  9. package/dist/advanced.mjs.map +1 -0
  10. package/dist/devtools.cjs +378 -352
  11. package/dist/devtools.cjs.map +1 -1
  12. package/dist/devtools.d.cts +15 -11
  13. package/dist/devtools.d.cts.map +1 -0
  14. package/dist/devtools.d.mts +23 -0
  15. package/dist/devtools.d.mts.map +1 -0
  16. package/dist/devtools.mjs +652 -0
  17. package/dist/devtools.mjs.map +1 -0
  18. package/dist/ecs-3kimUV5Z.mjs +589 -0
  19. package/dist/ecs-3kimUV5Z.mjs.map +1 -0
  20. package/dist/ecs-B4QrqfvQ.cjs +919 -0
  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 +4127 -820
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +370 -86
  37. package/dist/index.d.cts.map +1 -0
  38. package/dist/index.d.mts +435 -0
  39. package/dist/index.d.mts.map +1 -0
  40. package/dist/index.mjs +4051 -0
  41. package/dist/index.mjs.map +1 -0
  42. package/package.json +44 -17
  43. package/dist/SelectionRenderer-DRtwHWJ0.d.cts +0 -102
  44. package/dist/SelectionRenderer-rGYPadnn.d.ts +0 -102
  45. package/dist/advanced.d.ts +0 -83
  46. package/dist/advanced.js +0 -125
  47. package/dist/advanced.js.map +0 -1
  48. package/dist/chunk-2KBYGER3.cjs +0 -336
  49. package/dist/chunk-2KBYGER3.cjs.map +0 -1
  50. package/dist/chunk-FUPKRQB2.cjs +0 -3025
  51. package/dist/chunk-FUPKRQB2.cjs.map +0 -1
  52. package/dist/chunk-NILAZG6O.js +0 -292
  53. package/dist/chunk-NILAZG6O.js.map +0 -1
  54. package/dist/chunk-W2ZNA7HP.js +0 -2977
  55. package/dist/chunk-W2ZNA7HP.js.map +0 -1
  56. package/dist/devtools.d.ts +0 -19
  57. package/dist/devtools.js +0 -626
  58. package/dist/devtools.js.map +0 -1
  59. package/dist/engine-DqgJ82tq.d.cts +0 -805
  60. package/dist/engine-DqgJ82tq.d.ts +0 -805
  61. package/dist/index.d.ts +0 -151
  62. package/dist/index.js +0 -604
  63. package/dist/index.js.map +0 -1
package/README.md CHANGED
@@ -17,6 +17,7 @@ Build Figma-style infinite canvases in React -- drag, resize, snap, zoom, nested
17
17
  - **Undo / redo** -- Command buffer with grouped operations (an entire drag is one undo step)
18
18
  - **Hierarchical navigation** -- Enter and exit nested containers with camera state preservation
19
19
  - **ECS architecture** -- Extensible via custom components, tags, and systems with topologically-sorted scheduling
20
+ - **Card widgets** -- iOS-style preset-sized tiles (small / medium / large / xl) with rounded corners, soft shadows, and lift-on-drag animation; DOM or R3F/PBR flavors via `createCardWidget` / `createGeometryCardWidget`
20
21
  - **Performance** -- SDF shaders for grid and selection chrome, RBush spatial indexing, viewport culling, per-system profiling
21
22
  - **Live ECS editor** -- Drop-in `<EcsDevtools>` panel for spawning, inspecting, and editing components and tags at runtime
22
23
  - **Dark mode** -- Full dark mode support across canvas, widgets, and UI chrome
@@ -215,6 +216,97 @@ Spawning is uniform: `engine.spawn(id, options)`. If `id` matches an archetype,
215
216
  | `parent` | Parent entity id for nesting. |
216
217
  | `rotation` | Initial rotation in radians. |
217
218
 
219
+ The `interactive` field on `Archetype` accepts a boolean or an object. Pass `{ selectable, draggable, resizable, selectionFrame }` (any subset) when you want finer control. Cards that move, select, but never resize and render their own chrome: `{ selectable: true, draggable: true, resizable: false, selectionFrame: false }`. Omitted keys default to `false`, except `selectionFrame` which follows `selectable` (an entity you can select gets a frame unless you opt out).
220
+
221
+ ## Card Widgets
222
+
223
+ **Card widgets** are fixed-size, non-resizable widgets that sit on an iOS-style preset grid. They ship with rounded corners, a soft drop shadow, a hairline ring, and a subtle lift animation while dragging. Use them when you want dashboard-style tiles rather than free-form resizable surfaces.
224
+
225
+ ```tsx
226
+ import { createCardWidget } from '@jamesyong42/infinite-canvas';
227
+ import { z } from 'zod';
228
+
229
+ const schema = z.object({ label: z.string().default('Hi') });
230
+
231
+ export const Greeting = createCardWidget<{ label: string }>({
232
+ type: 'greeting-card',
233
+ size: 'small', // 'small' | 'medium' | 'large' | 'xl'
234
+ schema,
235
+ defaultData: { label: 'Hi' },
236
+ render: ({ data }) => (
237
+ <div className="flex h-full w-full items-center justify-center bg-white">
238
+ {data.label}
239
+ </div>
240
+ ),
241
+ });
242
+
243
+ // Register both the widget and the matching archetype.
244
+ const engine = createLayoutEngine({
245
+ widgets: [Greeting.widget],
246
+ archetypes: [Greeting.archetype],
247
+ });
248
+ engine.spawn('greeting-card', { at: { x: 50, y: 50 } });
249
+ ```
250
+
251
+ Preset sizes (default, matching iOS widget conventions on a 19 px grid):
252
+
253
+ | Preset | Width × Height |
254
+ |--------|----------------|
255
+ | `small` | 155 × 155 |
256
+ | `medium` | 329 × 155 |
257
+ | `large` | 329 × 345 |
258
+ | `xl` | 329 × 535 |
259
+
260
+ Override per-engine via `createLayoutEngine({ cardPresets: { presets: { small: { width: 200, height: 200 } }, gap: 24 } })`. Omitted presets keep their defaults.
261
+
262
+ Under the hood: the returned widget wraps your `render` in `<CardFrame>` (exported for manual composition), the archetype is non-resizable, and it bundles a `Card` component. A built-in `cardSystem` stamps `Transform2D.width/height` from `Card.preset` each tick — to change a card's size at runtime, update the preset: `engine.set(id, Card, { preset: 'large' })`. Reading the `Dragging` tag from the frame drives the lift affordance; you can read it elsewhere too via `useTag(entityId, Dragging)`.
263
+
264
+ Cards also opt out of the engine-drawn selection + hover outline (`selectionFrame: false` in their archetype) — the iOS rounded chrome in `<CardFrame>` is the card's visual contract, so the standard blue frame would fight it. If you need a selected/hover affordance inside a card, read `useIsSelected(entityId)` / `useTag(entityId, /* Hovered tag */)` from within `render` and style accordingly.
265
+
266
+ ### 3D Card Widgets
267
+
268
+ `createGeometryCardWidget` is the R3F counterpart — same preset sizes, non-resizable archetype, and drag-lift behavior, but the widget body is a three.js scene instead of DOM content. The helper pairs cleanly with PBR materials.
269
+
270
+ ```tsx
271
+ import { createGeometryCardWidget } from '@jamesyong42/infinite-canvas';
272
+ import { useFrame } from '@react-three/fiber';
273
+ import { useRef } from 'react';
274
+ import type { Mesh } from 'three';
275
+ import { z } from 'zod';
276
+
277
+ const schema = z.object({ color: z.string().default('#F5B8D0') });
278
+
279
+ export const Sphere = createGeometryCardWidget<{ color: string }>({
280
+ type: 'sphere',
281
+ size: 'small',
282
+ schema,
283
+ defaultData: { color: '#F5B8D0' },
284
+ background: 'card', // or 'transparent' — the geometry floats over the canvas
285
+ geometry: ({ data, width }) => {
286
+ const ref = useRef<Mesh>(null);
287
+ useFrame((_, dt) => { if (ref.current) ref.current.rotation.y += dt * 0.3; });
288
+ return (
289
+ <group>
290
+ <pointLight position={[80, 80, 120]} intensity={160} distance={300} decay={1.4} />
291
+ <ambientLight intensity={0.3} />
292
+ <mesh ref={ref}>
293
+ <sphereGeometry args={[width * 0.32, 48, 48]} />
294
+ <meshStandardMaterial color={data.color} roughness={0.35} />
295
+ </mesh>
296
+ </group>
297
+ );
298
+ },
299
+ });
300
+ ```
301
+
302
+ `background` options:
303
+
304
+ - `'card'` (default) — a rounded iOS-style card mesh sits behind the geometry in the same widget group.
305
+ - `'transparent'` — no back plane; the geometry floats over whatever's behind the widget.
306
+ - `{ color, roughness?, metalness? }` — card back with custom PBR parameters for tinted or glossy variants.
307
+
308
+ **Lighting caveat.** All R3F widgets share a single `<Canvas>`, so lights and `envMap`s you declare inside one widget's render function affect every other 3D widget. Keep per-widget lights `pointLight`s with `distance` scoped to the widget's size, or add one shared `<Environment>` at the app level (if you control the R3F canvas). The helper itself adds no lights — declare what you need inside your `geometry` component.
309
+
218
310
  ## WebGL Widgets (R3F)
219
311
 
220
312
  Define an `R3FWidget<T>` with `surface: 'webgl'` to render 3D content via React Three Fiber. R3F widget views receive `{ entityId, width, height }` and render in local coordinates (origin at widget centre):
@@ -260,6 +352,71 @@ engine.spawn('my-3d', { at: { x: 100, y: 100 } });
260
352
 
261
353
  WebGL widgets get a transparent R3F canvas layered between the grid and DOM layers. The R3F camera is synced with the engine camera every frame.
262
354
 
355
+ ## Pointer Events
356
+
357
+ Both DOM and R3F widgets dispatch pointer events naturally — write `onClick`, `onPointerOver`, `onPointerEnter` etc. the way you would in any React or R3F app. The canvas's engine state machine (drag, select, resize, marquee, double-click navigation) runs on the same event after your handlers, so the two layers compose without you wiring anything.
358
+
359
+ To opt a specific event out of engine semantics — e.g. a button inside a widget that should not start a drag — call `stopPropagation()` from your handler. Native HTML interactives (`<button>`, `<input>`, `<textarea>`, `<select>`, `[contenteditable]`) opt out automatically.
360
+
361
+ ### DOM widget
362
+
363
+ ```tsx
364
+ function MyDomWidget({ entityId }: DomWidgetProps) {
365
+ return (
366
+ <div>
367
+ <span>drag me anywhere — engine handles it</span>
368
+
369
+ <button onClick={(e) => { e.stopPropagation(); doThing(); }}>
370
+ clicking me runs doThing(), no drag
371
+ </button>
372
+ </div>
373
+ );
374
+ }
375
+ ```
376
+
377
+ `e.stopPropagation()` on the React synthetic event halts both widget-internal bubbling and the canvas-level engine call.
378
+
379
+ ### R3F widget
380
+
381
+ ```tsx
382
+ function MyR3FWidget({ entityId, width, height }: R3FWidgetProps) {
383
+ const [hover, setHover] = useState(false);
384
+ return (
385
+ <group>
386
+ {/* Drag handle: no stopPropagation — engine starts a drag normally. */}
387
+ <mesh
388
+ onPointerOver={() => setHover(true)}
389
+ onPointerOut={() => setHover(false)}>
390
+ <planeGeometry args={[width, height]} />
391
+ <meshBasicMaterial color={hover ? 'lightblue' : 'white'} />
392
+ </mesh>
393
+
394
+ {/* Button mesh: stopPropagation halts R3F bubble + nativeEvent halts engine. */}
395
+ <mesh
396
+ position={[0, 0, 1]}
397
+ onClick={(e) => {
398
+ e.stopPropagation(); // halts R3F bubble within widget
399
+ e.nativeEvent.stopPropagation(); // halts engine drag/select
400
+ doThing();
401
+ }}>
402
+ <boxGeometry args={[40, 20, 5]} />
403
+ <meshStandardMaterial color="orange" />
404
+ </mesh>
405
+ </group>
406
+ );
407
+ }
408
+ ```
409
+
410
+ R3F's `event.stopPropagation()` only halts further dispatch within the widget's own scene. To prevent the canvas-level engine routing from firing too — for example, a clickable mesh that should never start a drag — additionally call `event.nativeEvent.stopPropagation()`. The standard DOM idiom: stop the native event, the bus never sees it.
411
+
412
+ `event.point`, `event.uv`, `event.face`, `event.intersections` are populated by a widget-local raycast against the widget's own scene with widget-local coordinates — `event.point.x = 0` is the widget centre, not the canvas centre.
413
+
414
+ ### Hover and capture
415
+
416
+ Cross-widget hover transitions (cursor moves from widget A's mesh into widget B's mesh) fire `onPointerLeave` on A's last hit and `onPointerEnter` on B's first hit automatically. Within a widget, R3F handles enter/leave between meshes natively.
417
+
418
+ When the engine takes a drag (`capture-drag`), `capture-resize`, or `passthrough-track-drag` directive, the bus calls `setPointerCapture` on the canvas container. While captured, R3F's mesh-level events suspend — the engine owns the pointer until release. This matches the native browser pointer-capture model, so existing R3F patterns like `setPointerCapture` on a mesh continue to work for widget-internal drags that don't conflict with engine semantics.
419
+
263
420
  ## Configuration
264
421
 
265
422
  ### Grid
@@ -525,20 +682,25 @@ z:3 UI chrome
525
682
  | `InteractionRole` | Interaction behavior (drag, select, resize, etc.) |
526
683
  | `HandleSet` | Child handle entity references |
527
684
  | `CursorHint` | Cursor style on hover/active |
685
+ | `Card` | Marks an iOS-style card; carries the preset (`small`/`medium`/`large`/`xl`) |
528
686
 
529
687
  ### ECS Tags
530
688
 
531
- `Selectable` `Draggable` `Resizable` `Locked` `Selected` `Active` `Visible`
689
+ `Selectable` `Draggable` `Resizable` `SelectionFrame` `Locked` `Selected` `Dragging` `Active` `Visible`
690
+
691
+ - `Dragging` — transient state tag (parallels `Selected`): added when the drag dead zone is crossed, removed on pointer up or cancel. Read via `useTag(entityId, Dragging)` to drive drag-time affordances.
692
+ - `SelectionFrame` — opts an entity into the engine-drawn selection + hover outline. Granted automatically to Selectable entities via the archetype's `interactive` caps; widgets that render their own chrome (e.g. cards) opt out.
532
693
 
533
694
  ### Systems (execution order)
534
695
 
535
- 1. `transformPropagate` -- Propagate transforms down hierarchy, compute WorldBounds
536
- 2. `handleSync` -- Synchronize resize handle entities with parent widgets
537
- 3. `hitboxWorldBounds` -- Compute world-space hitbox bounds
538
- 4. `navigationFilter` -- Filter entities to active navigation layer
539
- 5. `cull` -- Mark viewport-visible entities
540
- 6. `breakpoint` -- Compute responsive breakpoints
541
- 7. `sort` -- Z-index ordering
696
+ 1. `card` -- Stamp Transform2D size from Card preset
697
+ 2. `transformPropagate` -- Propagate transforms down hierarchy, compute WorldBounds
698
+ 3. `handleSync` -- Synchronize resize handle entities with parent widgets
699
+ 4. `hitboxWorldBounds` -- Compute world-space hitbox bounds
700
+ 5. `navigationFilter` -- Filter entities to active navigation layer
701
+ 6. `cull` -- Mark viewport-visible entities
702
+ 7. `breakpoint` -- Compute responsive breakpoints
703
+ 8. `sort` -- Z-index ordering
542
704
 
543
705
  ## Performance Profiling
544
706
 
package/dist/advanced.cjs CHANGED
@@ -1,176 +1,157 @@
1
- 'use strict';
2
-
3
- var chunkFUPKRQB2_cjs = require('./chunk-FUPKRQB2.cjs');
4
- var chunk2KBYGER3_cjs = require('./chunk-2KBYGER3.cjs');
5
-
6
- // src/serialization.ts
7
- function serializeWorld(world, componentTypes, tagTypes, camera, navigationFrames) {
8
- const entities = [];
9
- const allEntities = world.query();
10
- for (const entityId of allEntities) {
11
- const components = {};
12
- const tags = [];
13
- for (const type of componentTypes) {
14
- const data = world.getComponent(entityId, type);
15
- if (data !== void 0) {
16
- components[type.name] = structuredClone(data);
17
- }
18
- }
19
- for (const type of tagTypes) {
20
- if (world.hasTag(entityId, type)) {
21
- if (type.name !== "Active" && type.name !== "Visible") {
22
- tags.push(type.name);
23
- }
24
- }
25
- }
26
- if (Object.keys(components).length > 0 || tags.length > 0) {
27
- entities.push({ id: entityId, components, tags });
28
- }
29
- }
30
- return {
31
- version: 1,
32
- entities,
33
- resources: {
34
- camera: { ...camera },
35
- navigationStack: structuredClone(navigationFrames)
36
- }
37
- };
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_hooks = require("./hooks-CtP02JNt.cjs");
3
+ const require_ecs = require("./ecs-B4QrqfvQ.cjs");
4
+ //#region src/ecs/serialization.ts
5
+ /**
6
+ * Serializes all entities, components, and tags to a JSON-compatible document.
7
+ * Requires registries of known component and tag types for enumeration.
8
+ */
9
+ function serializeWorld(world, componentTypes, tagTypes, camera, rootCamera) {
10
+ const entities = [];
11
+ const allEntities = world.query();
12
+ for (const entityId of allEntities) {
13
+ const components = {};
14
+ const tags = [];
15
+ for (const type of componentTypes) {
16
+ if (type.name === "PreDragLayer" || type.name === "TransformTween" || type.name === "CardOverlapHotPoint") continue;
17
+ const data = world.getComponent(entityId, type);
18
+ if (data !== void 0) components[type.name] = structuredClone(data);
19
+ }
20
+ for (const type of tagTypes) if (world.hasTag(entityId, type)) {
21
+ if (type.name !== "Active" && type.name !== "Visible" && type.name !== "Culled" && type.name !== "OverlapCandidate" && type.name !== "OverlapTarget") tags.push(type.name);
22
+ }
23
+ if (Object.keys(components).length > 0 || tags.length > 0) entities.push({
24
+ id: entityId,
25
+ components,
26
+ tags
27
+ });
28
+ }
29
+ return {
30
+ version: 1,
31
+ entities,
32
+ resources: {
33
+ camera: { ...camera },
34
+ rootCamera: { ...rootCamera }
35
+ }
36
+ };
38
37
  }
38
+ /**
39
+ * Restores entities from a serialized document into the world.
40
+ * Clears existing state first and remaps entity IDs automatically.
41
+ */
39
42
  function deserializeWorld(world, doc, componentTypes, tagTypes) {
40
- if (doc.version !== 1) {
41
- throw new Error(`Unsupported canvas document version: ${doc.version}. Expected version 1.`);
42
- }
43
- const compByName = /* @__PURE__ */ new Map();
44
- for (const t of componentTypes) compByName.set(t.name, t);
45
- const tagByName = /* @__PURE__ */ new Map();
46
- for (const t of tagTypes) tagByName.set(t.name, t);
47
- for (const entityId of world.query()) {
48
- world.destroyEntity(entityId);
49
- }
50
- const idMap = /* @__PURE__ */ new Map();
51
- for (const entry of doc.entities) {
52
- const newId = world.createEntity();
53
- idMap.set(entry.id, newId);
54
- for (const [compName, data] of Object.entries(entry.components)) {
55
- const type = compByName.get(compName);
56
- if (type) {
57
- world.addComponent(newId, type, data);
58
- }
59
- }
60
- for (const tagName of entry.tags) {
61
- const type = tagByName.get(tagName);
62
- if (type) {
63
- world.addTag(newId, type);
64
- }
65
- }
66
- }
67
- for (const [_oldId, newId] of idMap) {
68
- const parent = world.getComponent(newId, chunk2KBYGER3_cjs.Parent);
69
- if (parent && idMap.has(parent.id)) {
70
- const mappedId = idMap.get(parent.id);
71
- if (mappedId !== void 0) {
72
- world.setComponent(newId, chunk2KBYGER3_cjs.Parent, { id: mappedId });
73
- }
74
- }
75
- const children = world.getComponent(newId, chunk2KBYGER3_cjs.Children);
76
- if (children) {
77
- world.setComponent(newId, chunk2KBYGER3_cjs.Children, {
78
- ids: children.ids.map((id) => idMap.get(id) ?? id)
79
- });
80
- }
81
- const handleSet = world.getComponent(newId, chunk2KBYGER3_cjs.HandleSet);
82
- if (handleSet) {
83
- world.setComponent(newId, chunk2KBYGER3_cjs.HandleSet, {
84
- ids: handleSet.ids.map((id) => idMap.get(id) ?? id)
85
- });
86
- }
87
- }
43
+ if (doc.version !== 1) throw new Error(`Unsupported canvas document version: ${doc.version}. Expected version 1.`);
44
+ const compByName = /* @__PURE__ */ new Map();
45
+ for (const t of componentTypes) compByName.set(t.name, t);
46
+ const tagByName = /* @__PURE__ */ new Map();
47
+ for (const t of tagTypes) tagByName.set(t.name, t);
48
+ for (const entityId of world.query()) world.destroyEntity(entityId);
49
+ const idMap = /* @__PURE__ */ new Map();
50
+ for (const entry of doc.entities) {
51
+ const newId = world.createEntity();
52
+ idMap.set(entry.id, newId);
53
+ for (const [compName, data] of Object.entries(entry.components)) {
54
+ const type = compByName.get(compName);
55
+ if (type) world.addComponent(newId, type, data);
56
+ }
57
+ for (const tagName of entry.tags) {
58
+ const type = tagByName.get(tagName);
59
+ if (type) world.addTag(newId, type);
60
+ }
61
+ }
62
+ for (const [_oldId, newId] of idMap) {
63
+ const parent = world.getComponent(newId, require_ecs.ParentFrame);
64
+ if (parent && idMap.has(parent.id)) {
65
+ const mappedId = idMap.get(parent.id);
66
+ if (mappedId !== void 0) world.setComponent(newId, require_ecs.ParentFrame, { id: mappedId });
67
+ }
68
+ const children = world.getComponent(newId, require_ecs.Children);
69
+ if (children) world.setComponent(newId, require_ecs.Children, { ids: children.ids.map((id) => idMap.get(id) ?? id) });
70
+ const containerChildren = world.getComponent(newId, require_ecs.ContainerChildren);
71
+ if (containerChildren) {
72
+ const mapped = [];
73
+ for (const id of containerChildren.ids) {
74
+ const remapped = idMap.get(id);
75
+ if (remapped !== void 0) mapped.push(remapped);
76
+ }
77
+ world.setComponent(newId, require_ecs.ContainerChildren, { ids: mapped });
78
+ }
79
+ }
80
+ const live = doc.resources.camera;
81
+ world.setResource(require_ecs.CameraResource, {
82
+ x: live.x,
83
+ y: live.y,
84
+ zoom: live.zoom,
85
+ gesturing: false
86
+ });
87
+ world.setResource(require_ecs.RootCameraResource, { ...doc.resources.rootCamera });
88
88
  }
89
+ /**
90
+ * Serializes a subset of entities (e.g., for copy/paste).
91
+ * Recursively includes children of the specified entities.
92
+ */
89
93
  function serializeEntities(world, entityIds, componentTypes, tagTypes) {
90
- const result = [];
91
- const visited = /* @__PURE__ */ new Set();
92
- function visit(entityId) {
93
- if (visited.has(entityId)) return;
94
- visited.add(entityId);
95
- const components = {};
96
- const tags = [];
97
- for (const type of componentTypes) {
98
- const data = world.getComponent(entityId, type);
99
- if (data !== void 0) {
100
- components[type.name] = structuredClone(data);
101
- }
102
- }
103
- for (const type of tagTypes) {
104
- if (world.hasTag(entityId, type)) {
105
- if (type.name !== "Active" && type.name !== "Visible") {
106
- tags.push(type.name);
107
- }
108
- }
109
- }
110
- result.push({ id: entityId, components, tags });
111
- const children = components.Children;
112
- if (children?.ids) {
113
- for (const childId of children.ids) {
114
- visit(childId);
115
- }
116
- }
117
- }
118
- for (const id of entityIds) {
119
- visit(id);
120
- }
121
- return result;
94
+ const result = [];
95
+ const visited = /* @__PURE__ */ new Set();
96
+ function visit(entityId) {
97
+ if (visited.has(entityId)) return;
98
+ visited.add(entityId);
99
+ const components = {};
100
+ const tags = [];
101
+ for (const type of componentTypes) {
102
+ if (type.name === "PreDragLayer" || type.name === "TransformTween" || type.name === "CardOverlapHotPoint") continue;
103
+ const data = world.getComponent(entityId, type);
104
+ if (data !== void 0) components[type.name] = structuredClone(data);
105
+ }
106
+ for (const type of tagTypes) if (world.hasTag(entityId, type)) {
107
+ if (type.name !== "Active" && type.name !== "Visible" && type.name !== "Culled") tags.push(type.name);
108
+ }
109
+ result.push({
110
+ id: entityId,
111
+ components,
112
+ tags
113
+ });
114
+ const children = components.Children;
115
+ if (children?.ids) for (const childId of children.ids) visit(childId);
116
+ }
117
+ for (const id of entityIds) visit(id);
118
+ return result;
122
119
  }
123
-
124
- Object.defineProperty(exports, "GridRenderer", {
125
- enumerable: true,
126
- get: function () { return chunkFUPKRQB2_cjs.GridRenderer; }
127
- });
128
- Object.defineProperty(exports, "Profiler", {
129
- enumerable: true,
130
- get: function () { return chunkFUPKRQB2_cjs.Profiler; }
131
- });
132
- Object.defineProperty(exports, "SelectionOverlaySlot", {
133
- enumerable: true,
134
- get: function () { return chunkFUPKRQB2_cjs.SelectionOverlaySlot; }
135
- });
136
- Object.defineProperty(exports, "SelectionRenderer", {
137
- enumerable: true,
138
- get: function () { return chunkFUPKRQB2_cjs.SelectionRenderer; }
139
- });
140
- Object.defineProperty(exports, "SpatialIndex", {
141
- enumerable: true,
142
- get: function () { return chunkFUPKRQB2_cjs.SpatialIndex; }
143
- });
144
- Object.defineProperty(exports, "SpatialIndexResource", {
145
- enumerable: true,
146
- get: function () { return chunkFUPKRQB2_cjs.SpatialIndexResource; }
147
- });
148
- Object.defineProperty(exports, "WebGLWidgetLayer", {
149
- enumerable: true,
150
- get: function () { return chunkFUPKRQB2_cjs.WebGLWidgetLayer; }
151
- });
152
- Object.defineProperty(exports, "WebGLWidgetSlot", {
153
- enumerable: true,
154
- get: function () { return chunkFUPKRQB2_cjs.WebGLWidgetSlot; }
155
- });
156
- Object.defineProperty(exports, "WidgetSlot", {
157
- enumerable: true,
158
- get: function () { return chunkFUPKRQB2_cjs.WidgetSlot; }
159
- });
160
- Object.defineProperty(exports, "computeSnapGuides", {
161
- enumerable: true,
162
- get: function () { return chunkFUPKRQB2_cjs.computeSnapGuides; }
163
- });
164
- Object.defineProperty(exports, "ContainerRefProvider", {
165
- enumerable: true,
166
- get: function () { return chunk2KBYGER3_cjs.ContainerRefProvider; }
167
- });
168
- Object.defineProperty(exports, "EngineProvider", {
169
- enumerable: true,
170
- get: function () { return chunk2KBYGER3_cjs.EngineProvider; }
171
- });
120
+ //#endregion
121
+ exports.Compositor = require_hooks.Compositor;
122
+ exports.CompositorContext = require_hooks.CompositorContext;
123
+ exports.ContainerRefProvider = require_hooks.ContainerRefProvider;
124
+ exports.EngineProvider = require_ecs.EngineProvider;
125
+ exports.GridRenderer = require_hooks.GridRenderer;
126
+ exports.Profiler = require_hooks.Profiler;
127
+ exports.ProfilerProbe = require_hooks.ProfilerProbe;
128
+ exports.R3FAnimationSignal = require_hooks.R3FAnimationSignal;
129
+ exports.R3FManager = require_hooks.R3FManager;
130
+ exports.R3FRenderBudget = require_hooks.R3FRenderBudget;
131
+ exports.R3FRenderState = require_hooks.R3FRenderState;
132
+ exports.ResourceRegistry = require_hooks.ResourceRegistry;
133
+ exports.SelectionOverlaySlot = require_hooks.SelectionOverlaySlot;
134
+ exports.SelectionRenderer = require_hooks.SelectionRenderer;
135
+ exports.SpatialIndex = require_hooks.SpatialIndex;
136
+ exports.SpatialIndexResource = require_ecs.SpatialIndexResource;
137
+ exports.VirtualWidget = require_hooks.VirtualWidget;
138
+ exports.WebGLManager = require_hooks.WebGLManager;
139
+ exports.WidgetRenderTargetPool = require_hooks.WidgetRenderTargetPool;
140
+ exports.WidgetSlot = require_hooks.WidgetSlot;
141
+ exports.WidgetStateMachine = require_hooks.WidgetStateMachine;
142
+ exports.ZOOM_BANDS = require_hooks.ZOOM_BANDS;
143
+ exports.computeSnapGuides = require_hooks.computeSnapGuides;
172
144
  exports.deserializeWorld = deserializeWorld;
145
+ exports.isOutOfBand = require_hooks.isOutOfBand;
146
+ exports.selectBand = require_hooks.selectBand;
173
147
  exports.serializeEntities = serializeEntities;
174
148
  exports.serializeWorld = serializeWorld;
175
- //# sourceMappingURL=advanced.cjs.map
149
+ exports.useCompositor = require_hooks.useCompositor;
150
+ exports.useSharedGeometry = require_hooks.useSharedGeometry;
151
+ exports.useSharedMaterial = require_hooks.useSharedMaterial;
152
+ exports.useSharedTexture = require_hooks.useSharedTexture;
153
+ exports.useWidgetAnimation = require_hooks.useWidgetAnimation;
154
+ exports.useWidgetInvalidate = require_hooks.useWidgetInvalidate;
155
+ exports.useWidgetPhase = require_hooks.useWidgetPhase;
156
+
176
157
  //# sourceMappingURL=advanced.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/serialization.ts"],"names":["Parent","Children","HandleSet"],"mappings":"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","file":"advanced.cjs","sourcesContent":["import type { ComponentType, EntityId, TagType, World } from '@jamesyong42/reactive-ecs';\nimport { Children, HandleSet, Parent } from './components.js';\nimport type { NavigationFrame } from './resources.js';\n\n// === Serialization Types ===\n\n/** JSON-serializable snapshot of the canvas state, including all entities and camera. */\nexport interface CanvasDocument {\n\tversion: number;\n\tentities: SerializedEntity[];\n\tresources: {\n\t\tcamera: { x: number; y: number; zoom: number };\n\t\tnavigationStack: NavigationFrame[];\n\t};\n}\n\n/** A single serialized entity with its components and tags. */\nexport interface SerializedEntity {\n\tid: EntityId;\n\tcomponents: Record<string, unknown>;\n\ttags: string[];\n}\n\n// === Serialize/Deserialize ===\n\n/**\n * Serializes all entities, components, and tags to a JSON-compatible document.\n * Requires registries of known component and tag types for enumeration.\n */\nexport function serializeWorld(\n\tworld: World,\n\tcomponentTypes: ComponentType[],\n\ttagTypes: TagType[],\n\tcamera: { x: number; y: number; zoom: number },\n\tnavigationFrames: NavigationFrame[],\n): CanvasDocument {\n\tconst entities: SerializedEntity[] = [];\n\n\t// Get all entity IDs (use a broad query)\n\tconst allEntities = world.query();\n\n\tfor (const entityId of allEntities) {\n\t\tconst components: Record<string, unknown> = {};\n\t\tconst tags: string[] = [];\n\n\t\tfor (const type of componentTypes) {\n\t\t\tconst data = world.getComponent(entityId, type);\n\t\t\tif (data !== undefined) {\n\t\t\t\tcomponents[type.name] = structuredClone(data);\n\t\t\t}\n\t\t}\n\n\t\tfor (const type of tagTypes) {\n\t\t\tif (world.hasTag(entityId, type)) {\n\t\t\t\t// Skip runtime-only tags (Active, Visible — they're recomputed)\n\t\t\t\tif (type.name !== 'Active' && type.name !== 'Visible') {\n\t\t\t\t\ttags.push(type.name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (Object.keys(components).length > 0 || tags.length > 0) {\n\t\t\tentities.push({ id: entityId, components, tags });\n\t\t}\n\t}\n\n\treturn {\n\t\tversion: 1,\n\t\tentities,\n\t\tresources: {\n\t\t\tcamera: { ...camera },\n\t\t\tnavigationStack: structuredClone(navigationFrames),\n\t\t},\n\t};\n}\n\n/**\n * Restores entities from a serialized document into the world.\n * Clears existing state first and remaps entity IDs automatically.\n */\nexport function deserializeWorld(\n\tworld: World,\n\tdoc: CanvasDocument,\n\tcomponentTypes: ComponentType[],\n\ttagTypes: TagType[],\n): void {\n\tif (doc.version !== 1) {\n\t\tthrow new Error(`Unsupported canvas document version: ${doc.version}. Expected version 1.`);\n\t}\n\n\t// Build lookup maps\n\tconst compByName = new Map<string, ComponentType>();\n\tfor (const t of componentTypes) compByName.set(t.name, t);\n\n\tconst tagByName = new Map<string, TagType>();\n\tfor (const t of tagTypes) tagByName.set(t.name, t);\n\n\t// Destroy all existing entities\n\tfor (const entityId of world.query()) {\n\t\tworld.destroyEntity(entityId);\n\t}\n\n\t// First pass: create entities and build old-to-new ID mapping\n\tconst idMap = new Map<EntityId, EntityId>();\n\n\tfor (const entry of doc.entities) {\n\t\tconst newId = world.createEntity();\n\t\tidMap.set(entry.id as EntityId, newId);\n\n\t\tfor (const [compName, data] of Object.entries(entry.components)) {\n\t\t\tconst type = compByName.get(compName);\n\t\t\tif (type) {\n\t\t\t\tworld.addComponent(newId, type, data);\n\t\t\t}\n\t\t}\n\n\t\tfor (const tagName of entry.tags) {\n\t\t\tconst type = tagByName.get(tagName);\n\t\t\tif (type) {\n\t\t\t\tworld.addTag(newId, type);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Second pass: remap cross-reference components (Parent, Children, HandleSet)\n\tfor (const [_oldId, newId] of idMap) {\n\t\tconst parent = world.getComponent(newId, Parent);\n\t\tif (parent && idMap.has(parent.id)) {\n\t\t\tconst mappedId = idMap.get(parent.id);\n\t\t\tif (mappedId !== undefined) {\n\t\t\t\tworld.setComponent(newId, Parent, { id: mappedId });\n\t\t\t}\n\t\t}\n\n\t\tconst children = world.getComponent(newId, Children);\n\t\tif (children) {\n\t\t\tworld.setComponent(newId, Children, {\n\t\t\t\tids: children.ids.map((id: EntityId) => idMap.get(id) ?? id),\n\t\t\t});\n\t\t}\n\n\t\tconst handleSet = world.getComponent(newId, HandleSet);\n\t\tif (handleSet) {\n\t\t\tworld.setComponent(newId, HandleSet, {\n\t\t\t\tids: handleSet.ids.map((id: EntityId) => idMap.get(id) ?? id),\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Serializes a subset of entities (e.g., for copy/paste).\n * Recursively includes children of the specified entities.\n */\nexport function serializeEntities(\n\tworld: World,\n\tentityIds: EntityId[],\n\tcomponentTypes: ComponentType[],\n\ttagTypes: TagType[],\n): SerializedEntity[] {\n\tconst result: SerializedEntity[] = [];\n\tconst visited = new Set<EntityId>();\n\n\tfunction visit(entityId: EntityId) {\n\t\tif (visited.has(entityId)) return;\n\t\tvisited.add(entityId);\n\n\t\tconst components: Record<string, unknown> = {};\n\t\tconst tags: string[] = [];\n\n\t\tfor (const type of componentTypes) {\n\t\t\tconst data = world.getComponent(entityId, type);\n\t\t\tif (data !== undefined) {\n\t\t\t\tcomponents[type.name] = structuredClone(data);\n\t\t\t}\n\t\t}\n\n\t\tfor (const type of tagTypes) {\n\t\t\tif (world.hasTag(entityId, type)) {\n\t\t\t\tif (type.name !== 'Active' && type.name !== 'Visible') {\n\t\t\t\t\ttags.push(type.name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresult.push({ id: entityId, components, tags });\n\n\t\t// Recurse into children. components.Children is typed as unknown via\n\t\t// the Record<string, unknown> shape, so narrow through a cast.\n\t\tconst children = components.Children as { ids?: EntityId[] } | undefined;\n\t\tif (children?.ids) {\n\t\t\tfor (const childId of children.ids) {\n\t\t\t\tvisit(childId);\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const id of entityIds) {\n\t\tvisit(id);\n\t}\n\n\treturn result;\n}\n"]}
1
+ {"version":3,"file":"advanced.cjs","names":["ParentFrame","Children","ContainerChildren","CameraResource","RootCameraResource"],"sources":["../src/ecs/serialization.ts"],"sourcesContent":["import type { ComponentType, EntityId, TagType, World } from '@jamesyong42/reactive-ecs';\nimport { Children, ContainerChildren, ParentFrame } from './components.js';\nimport type { FrameCameraState } from './resources.js';\nimport { CameraResource, RootCameraResource } from './resources.js';\n\n// === Serialization Types ===\n\n/**\n * JSON-serializable snapshot of the canvas state, including all entities,\n * the current camera, and the persisted root-frame camera.\n *\n * The navigation stack is **not** serialized — reloading a canvas always\n * drops the user at the root frame (RFC-004 § Phase 0c). Per-container\n * camera state is persisted via the `ContainerCamera` component on each\n * container entity (serialized as part of its component set).\n */\nexport interface CanvasDocument {\n\tversion: number;\n\tentities: SerializedEntity[];\n\tresources: {\n\t\t/** Current live camera (whatever frame the user was in at serialize time). */\n\t\tcamera: FrameCameraState;\n\t\t/** Persisted root-frame camera — restored on load. */\n\t\trootCamera: FrameCameraState;\n\t};\n}\n\n/** A single serialized entity with its components and tags. */\nexport interface SerializedEntity {\n\tid: EntityId;\n\tcomponents: Record<string, unknown>;\n\ttags: string[];\n}\n\n// === Serialize/Deserialize ===\n\n/**\n * Serializes all entities, components, and tags to a JSON-compatible document.\n * Requires registries of known component and tag types for enumeration.\n */\nexport function serializeWorld(\n\tworld: World,\n\tcomponentTypes: ComponentType[],\n\ttagTypes: TagType[],\n\tcamera: FrameCameraState,\n\trootCamera: FrameCameraState,\n): CanvasDocument {\n\tconst entities: SerializedEntity[] = [];\n\n\t// Get all entity IDs (use a broad query)\n\tconst allEntities = world.query();\n\n\tfor (const entityId of allEntities) {\n\t\tconst components: Record<string, unknown> = {};\n\t\tconst tags: string[] = [];\n\n\t\tfor (const type of componentTypes) {\n\t\t\t// Skip runtime-only components: `PreDragLayer` is recomputed by\n\t\t\t// dragPromoteSystem on the next Dragging flip; `TransformTween`\n\t\t\t// is an in-flight animation that should not be paused / resumed\n\t\t\t// across reloads (the destination `Transform2D` persists instead);\n\t\t\t// `CardOverlapHotPoint` is drag-scoped visual state with no\n\t\t\t// meaning outside an active drag.\n\t\t\tif (\n\t\t\t\ttype.name === 'PreDragLayer' ||\n\t\t\t\ttype.name === 'TransformTween' ||\n\t\t\t\ttype.name === 'CardOverlapHotPoint'\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst data = world.getComponent(entityId, type);\n\t\t\tif (data !== undefined) {\n\t\t\t\tcomponents[type.name] = structuredClone(data);\n\t\t\t}\n\t\t}\n\n\t\tfor (const type of tagTypes) {\n\t\t\tif (world.hasTag(entityId, type)) {\n\t\t\t\t// Skip runtime-only tags: Active/Visible/Culled are recomputed\n\t\t\t\t// by the cull pipeline; OverlapCandidate/OverlapTarget are\n\t\t\t\t// drag-scoped and meaningless outside an active drag.\n\t\t\t\tif (\n\t\t\t\t\ttype.name !== 'Active' &&\n\t\t\t\t\ttype.name !== 'Visible' &&\n\t\t\t\t\ttype.name !== 'Culled' &&\n\t\t\t\t\ttype.name !== 'OverlapCandidate' &&\n\t\t\t\t\ttype.name !== 'OverlapTarget'\n\t\t\t\t) {\n\t\t\t\t\ttags.push(type.name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (Object.keys(components).length > 0 || tags.length > 0) {\n\t\t\tentities.push({ id: entityId, components, tags });\n\t\t}\n\t}\n\n\treturn {\n\t\tversion: 1,\n\t\tentities,\n\t\tresources: {\n\t\t\tcamera: { ...camera },\n\t\t\trootCamera: { ...rootCamera },\n\t\t},\n\t};\n}\n\n/**\n * Restores entities from a serialized document into the world.\n * Clears existing state first and remaps entity IDs automatically.\n */\nexport function deserializeWorld(\n\tworld: World,\n\tdoc: CanvasDocument,\n\tcomponentTypes: ComponentType[],\n\ttagTypes: TagType[],\n): void {\n\tif (doc.version !== 1) {\n\t\tthrow new Error(`Unsupported canvas document version: ${doc.version}. Expected version 1.`);\n\t}\n\n\t// Build lookup maps\n\tconst compByName = new Map<string, ComponentType>();\n\tfor (const t of componentTypes) compByName.set(t.name, t);\n\n\tconst tagByName = new Map<string, TagType>();\n\tfor (const t of tagTypes) tagByName.set(t.name, t);\n\n\t// Destroy all existing entities\n\tfor (const entityId of world.query()) {\n\t\tworld.destroyEntity(entityId);\n\t}\n\n\t// First pass: create entities and build old-to-new ID mapping\n\tconst idMap = new Map<EntityId, EntityId>();\n\n\tfor (const entry of doc.entities) {\n\t\tconst newId = world.createEntity();\n\t\tidMap.set(entry.id as EntityId, newId);\n\n\t\tfor (const [compName, data] of Object.entries(entry.components)) {\n\t\t\tconst type = compByName.get(compName);\n\t\t\tif (type) {\n\t\t\t\tworld.addComponent(newId, type, data);\n\t\t\t}\n\t\t}\n\n\t\tfor (const tagName of entry.tags) {\n\t\t\tconst type = tagByName.get(tagName);\n\t\t\tif (type) {\n\t\t\t\tworld.addTag(newId, type);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Second pass: remap cross-reference components (ParentFrame, Children,\n\t// ContainerChildren).\n\tfor (const [_oldId, newId] of idMap) {\n\t\tconst parent = world.getComponent(newId, ParentFrame);\n\t\tif (parent && idMap.has(parent.id)) {\n\t\t\tconst mappedId = idMap.get(parent.id);\n\t\t\tif (mappedId !== undefined) {\n\t\t\t\tworld.setComponent(newId, ParentFrame, { id: mappedId });\n\t\t\t}\n\t\t}\n\n\t\tconst children = world.getComponent(newId, Children);\n\t\tif (children) {\n\t\t\tworld.setComponent(newId, Children, {\n\t\t\t\tids: children.ids.map((id: EntityId) => idMap.get(id) ?? id),\n\t\t\t});\n\t\t}\n\n\t\tconst containerChildren = world.getComponent(newId, ContainerChildren);\n\t\tif (containerChildren) {\n\t\t\t// Drop ids that didn't round-trip (child was destroyed before save\n\t\t\t// and still lingered in the list) rather than falling back to the\n\t\t\t// raw pre-save id — that id may have been recycled to an unrelated\n\t\t\t// entity by the post-load world and would leak into the container's\n\t\t\t// child count / navigation target list.\n\t\t\tconst mapped: EntityId[] = [];\n\t\t\tfor (const id of containerChildren.ids) {\n\t\t\t\tconst remapped = idMap.get(id);\n\t\t\t\tif (remapped !== undefined) mapped.push(remapped);\n\t\t\t}\n\t\t\tworld.setComponent(newId, ContainerChildren, { ids: mapped });\n\t\t}\n\t}\n\n\t// Restore camera resources. `gesturing` resets to false on load — it's\n\t// transient interaction state, not persisted view state.\n\tconst live = doc.resources.camera;\n\tworld.setResource(CameraResource, { x: live.x, y: live.y, zoom: live.zoom, gesturing: false });\n\tworld.setResource(RootCameraResource, { ...doc.resources.rootCamera });\n\n\t// NavigationStack is deliberately not restored — users always return\n\t// to the root frame on load (RFC-004 § Phase 0c).\n}\n\n/**\n * Serializes a subset of entities (e.g., for copy/paste).\n * Recursively includes children of the specified entities.\n */\nexport function serializeEntities(\n\tworld: World,\n\tentityIds: EntityId[],\n\tcomponentTypes: ComponentType[],\n\ttagTypes: TagType[],\n): SerializedEntity[] {\n\tconst result: SerializedEntity[] = [];\n\tconst visited = new Set<EntityId>();\n\n\tfunction visit(entityId: EntityId) {\n\t\tif (visited.has(entityId)) return;\n\t\tvisited.add(entityId);\n\n\t\tconst components: Record<string, unknown> = {};\n\t\tconst tags: string[] = [];\n\n\t\tfor (const type of componentTypes) {\n\t\t\t// Skip runtime-only components: `PreDragLayer` is recomputed by\n\t\t\t// dragPromoteSystem on the next Dragging flip; `TransformTween`\n\t\t\t// is an in-flight animation that should not be paused / resumed\n\t\t\t// across reloads (the destination `Transform2D` persists instead);\n\t\t\t// `CardOverlapHotPoint` is drag-scoped visual state with no\n\t\t\t// meaning outside an active drag.\n\t\t\tif (\n\t\t\t\ttype.name === 'PreDragLayer' ||\n\t\t\t\ttype.name === 'TransformTween' ||\n\t\t\t\ttype.name === 'CardOverlapHotPoint'\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst data = world.getComponent(entityId, type);\n\t\t\tif (data !== undefined) {\n\t\t\t\tcomponents[type.name] = structuredClone(data);\n\t\t\t}\n\t\t}\n\n\t\tfor (const type of tagTypes) {\n\t\t\tif (world.hasTag(entityId, type)) {\n\t\t\t\tif (type.name !== 'Active' && type.name !== 'Visible' && type.name !== 'Culled') {\n\t\t\t\t\ttags.push(type.name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresult.push({ id: entityId, components, tags });\n\n\t\t// Recurse into children. components.Children is typed as unknown via\n\t\t// the Record<string, unknown> shape, so narrow through a cast.\n\t\tconst children = components.Children as { ids?: EntityId[] } | undefined;\n\t\tif (children?.ids) {\n\t\t\tfor (const childId of children.ids) {\n\t\t\t\tvisit(childId);\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const id of entityIds) {\n\t\tvisit(id);\n\t}\n\n\treturn result;\n}\n"],"mappings":"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"}