ecspresso 0.17.0 → 0.18.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 (53) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +2 -0
  3. package/dist/asset-manager.d.ts +8 -8
  4. package/dist/asset-types.d.ts +9 -5
  5. package/dist/ecspresso-builder.d.ts +5 -5
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/index.js.map +7 -7
  9. package/dist/plugins/ai/behavior-tree.d.ts +2 -2
  10. package/dist/plugins/ai/behavior-tree.js.map +2 -2
  11. package/dist/plugins/ai/detection.d.ts +3 -3
  12. package/dist/plugins/ai/detection.js.map +2 -2
  13. package/dist/plugins/ai/flocking.d.ts +3 -3
  14. package/dist/plugins/ai/flocking.js.map +1 -1
  15. package/dist/plugins/ai/pathfinding.d.ts +3 -4
  16. package/dist/plugins/ai/pathfinding.js.map +2 -2
  17. package/dist/plugins/combat/health.d.ts +2 -2
  18. package/dist/plugins/combat/health.js.map +2 -2
  19. package/dist/plugins/combat/projectile.d.ts +3 -3
  20. package/dist/plugins/combat/projectile.js.map +2 -2
  21. package/dist/plugins/input/selection.d.ts +3 -3
  22. package/dist/plugins/input/selection.js.map +3 -3
  23. package/dist/plugins/isometric/depth-sort.d.ts +2 -2
  24. package/dist/plugins/isometric/depth-sort.js.map +2 -2
  25. package/dist/plugins/isometric/projection.d.ts +2 -2
  26. package/dist/plugins/isometric/projection.js.map +2 -2
  27. package/dist/plugins/physics/steering.d.ts +2 -2
  28. package/dist/plugins/physics/steering.js.map +2 -2
  29. package/dist/plugins/rendering/particles.d.ts +2 -2
  30. package/dist/plugins/rendering/particles.js.map +1 -1
  31. package/dist/plugins/rendering/renderer2D.d.ts +6 -5
  32. package/dist/plugins/rendering/renderer2D.js.map +2 -2
  33. package/dist/plugins/rendering/renderer3D.d.ts +3 -2
  34. package/dist/plugins/rendering/renderer3D.js.map +2 -2
  35. package/dist/plugins/rendering/sprite-animation.d.ts +110 -0
  36. package/dist/plugins/rendering/sprite-animation.js +2 -2
  37. package/dist/plugins/rendering/sprite-animation.js.map +3 -3
  38. package/dist/plugins/rendering/tilemap.d.ts +3 -3
  39. package/dist/plugins/rendering/tilemap.js.map +3 -3
  40. package/dist/plugins/spatial/camera.js.map +2 -2
  41. package/dist/plugins/spatial/camera3D.d.ts +3 -3
  42. package/dist/plugins/spatial/camera3D.js.map +2 -2
  43. package/dist/plugins/spatial/transform.d.ts +2 -2
  44. package/dist/plugins/spatial/transform.js.map +1 -1
  45. package/dist/plugins/spatial/transform3D.d.ts +2 -2
  46. package/dist/plugins/spatial/transform3D.js.map +1 -1
  47. package/dist/plugins/ui/ui.d.ts +2 -2
  48. package/dist/plugins/ui/ui.js.map +3 -3
  49. package/dist/screen-types.d.ts +7 -3
  50. package/dist/system-builder.d.ts +5 -5
  51. package/dist/type-utils.d.ts +20 -0
  52. package/dist/types.d.ts +1 -1
  53. package/package.json +5 -4
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { WebGLRenderer, WebGLRendererParameters, Scene, Camera, Object3D, Mesh, Group, ColorRepresentation } from 'three';
10
10
  import { type Plugin } from 'ecspresso';
11
- import type { WorldConfigFrom, EmptyConfig } from '../../type-utils';
11
+ import type { ComponentsConfig, EmptyConfig, EventsConfig, ResourcesConfig } from '../../type-utils';
12
12
  import { type LocalTransform3D, type WorldTransform3D, type Transform3DComponentTypes, type Transform3DPluginOptions } from 'ecspresso/plugins/spatial/transform3D';
13
13
  export type { LocalTransform3D, WorldTransform3D, Transform3DComponentTypes };
14
14
  export { createTransform3D, createLocalTransform3D, createWorldTransform3D, DEFAULT_LOCAL_TRANSFORM_3D, DEFAULT_WORLD_TRANSFORM_3D, } from 'ecspresso/plugins/spatial/transform3D';
@@ -234,6 +234,7 @@ export declare function createGroupComponents(group: Group, position?: PositionO
234
234
  export declare function createObject3DComponents(object3d: Object3D, position?: PositionOption3D, options?: TransformOptions3D): Pick<Renderer3DComponentTypes, 'object3d' | 'localTransform3D' | 'worldTransform3D' | 'visible3d'>;
235
235
  type Renderer3DLabels = 'renderer3d-sync' | 'renderer3d-scene-graph' | 'renderer3d-render' | 'transform3d-propagation';
236
236
  type Renderer3DReactiveQueryNames = 'renderer3d-meshes' | 'renderer3d-groups' | 'renderer3d-objects';
237
+ type Renderer3DWorldConfig = ComponentsConfig<Renderer3DComponentTypes> & EventsConfig<Renderer3DEventTypes> & ResourcesConfig<Renderer3DResourceTypes>;
237
238
  /**
238
239
  * Create a 3D rendering plugin for ECSpresso.
239
240
  *
@@ -244,4 +245,4 @@ type Renderer3DReactiveQueryNames = 'renderer3d-meshes' | 'renderer3d-groups' |
244
245
  * - Render call (renderer.render(scene, camera) each frame)
245
246
  * - Optional requestAnimationFrame loop
246
247
  */
247
- export declare function createRenderer3DPlugin<G extends string = 'renderer3d'>(options: Renderer3DPluginOptions<G>): Plugin<WorldConfigFrom<Renderer3DComponentTypes, Renderer3DEventTypes, Renderer3DResourceTypes>, EmptyConfig, Renderer3DLabels, G, never, Renderer3DReactiveQueryNames>;
248
+ export declare function createRenderer3DPlugin<G extends string = 'renderer3d'>(options: Renderer3DPluginOptions<G>): Plugin<Renderer3DWorldConfig, EmptyConfig, Renderer3DLabels, G, never, Renderer3DReactiveQueryNames>;
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/rendering/renderer3D.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * 3D Renderer Plugin for ECSpresso\n *\n * An opt-in Three.js-based 3D rendering plugin that automates scene graph wiring.\n * Import from 'ecspresso/plugins/rendering/renderer3D'\n *\n * This plugin includes 3D transform propagation automatically.\n */\n\nimport type {\n\tWebGLRenderer,\n\tWebGLRendererParameters,\n\tScene,\n\tCamera,\n\tPerspectiveCamera,\n\tOrthographicCamera,\n\tObject3D,\n\tMesh,\n\tGroup,\n\tColorRepresentation,\n\tVector3,\n\tEuler,\n\tQuaternion,\n} from 'three';\nimport { definePlugin, type Plugin } from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../../type-utils';\nimport type ECSpresso from 'ecspresso';\nimport {\n\tcreateTransform3DPlugin,\n\tcreateTransform3D,\n\ttype LocalTransform3D,\n\ttype WorldTransform3D,\n\ttype Transform3DComponentTypes,\n\ttype Transform3DPluginOptions,\n\tDEFAULT_LOCAL_TRANSFORM_3D,\n} from 'ecspresso/plugins/spatial/transform3D';\n\n// Re-export transform types for convenience\nexport type { LocalTransform3D, WorldTransform3D, Transform3DComponentTypes };\nexport {\n\tcreateTransform3D,\n\tcreateLocalTransform3D,\n\tcreateWorldTransform3D,\n\tDEFAULT_LOCAL_TRANSFORM_3D,\n\tDEFAULT_WORLD_TRANSFORM_3D,\n} from 'ecspresso/plugins/spatial/transform3D';\n\n// ==================== Component Types ====================\n\n/**\n * Visibility component for 3D entities.\n */\nexport interface Visible3D {\n\tvisible: boolean;\n}\n\n/**\n * Aggregate component types for the 3D renderer plugin.\n * Included automatically via `.withPlugin(createRenderer3DPlugin({ ... }))`.\n */\nexport interface Renderer3DComponentTypes extends Transform3DComponentTypes {\n\tmesh: Mesh;\n\tgroup: Group;\n\tobject3d: Object3D;\n\tvisible3d: Visible3D;\n\t/** Controls Three.js Object3D.renderOrder for manual z-ordering */\n\trenderOrder: number;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Events emitted by the 3D renderer plugin.\n */\nexport interface Renderer3DEventTypes {\n\thierarchyChanged: {\n\t\tentityId: number;\n\t\toldParent: number | null;\n\t\tnewParent: number | null;\n\t};\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Resources provided by the 3D renderer plugin.\n */\nexport interface Renderer3DResourceTypes {\n\tthreeRenderer: WebGLRenderer;\n\tscene: Scene;\n\tcamera: Camera;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Common options shared between both initialization modes.\n */\ninterface Renderer3DPluginCommonOptions<G extends string = 'renderer3d'> {\n\t/** System group name (default: 'renderer3d') */\n\tsystemGroup?: G;\n\t/** Priority for render sync system (default: 500) */\n\trenderSyncPriority?: number;\n\t/** Options for the included 3D transform plugin */\n\ttransform?: Transform3DPluginOptions;\n\t/** When true, starts a requestAnimationFrame loop to drive ecs.update() automatically (default: true) */\n\tstartLoop?: boolean;\n}\n\n/**\n * Options when providing pre-initialized Three.js objects.\n */\nexport interface Renderer3DPluginPreInitOptions<G extends string = 'renderer3d'> extends Renderer3DPluginCommonOptions<G> {\n\t/** Pre-initialized WebGLRenderer */\n\trenderer: WebGLRenderer;\n\t/** Pre-initialized Scene */\n\tscene: Scene;\n\t/** Pre-initialized Camera */\n\tcamera: Camera;\n\tcontainer?: never;\n\tbackground?: never;\n\twidth?: never;\n\theight?: never;\n\tantialias?: never;\n\tshadows?: never;\n\tcameraOptions?: never;\n\tthreeInit?: never;\n}\n\n/**\n * Camera configuration for managed mode.\n *\n * Discriminated on `projection`. Defaults to `'perspective'` when omitted.\n * Orthographic cameras use `viewSize` (world-unit height at zoom=1) to define\n * the base frustum; `zoom` maps directly to Three.js's `OrthographicCamera.zoom`.\n */\nexport type CameraOptions =\n\t| {\n\t\tprojection?: 'perspective';\n\t\tfov?: number;\n\t\tnear?: number;\n\t\tfar?: number;\n\t\tposition?: { x: number; y: number; z: number };\n\t\tlookAt?: { x: number; y: number; z: number };\n\t}\n\t| {\n\t\tprojection: 'orthographic';\n\t\tviewSize?: number;\n\t\tzoom?: number;\n\t\tnear?: number;\n\t\tfar?: number;\n\t\tposition?: { x: number; y: number; z: number };\n\t\tlookAt?: { x: number; y: number; z: number };\n\t};\n\n/**\n * Options when letting the plugin create and manage Three.js objects.\n */\nexport interface Renderer3DPluginManagedOptions<G extends string = 'renderer3d'> extends Renderer3DPluginCommonOptions<G> {\n\trenderer?: never;\n\tscene?: never;\n\tcamera?: never;\n\t/** Container element to append the canvas to (or CSS selector string). Defaults to `document.body`. */\n\tcontainer?: HTMLElement | string;\n\t/** Scene background color. */\n\tbackground?: ColorRepresentation;\n\t/** Canvas width. When omitted, auto-sizes to container. */\n\twidth?: number;\n\t/** Canvas height. When omitted, auto-sizes to container. */\n\theight?: number;\n\t/** Enable antialiasing (default: true) */\n\tantialias?: boolean;\n\t/** Enable shadow mapping (default: false) */\n\tshadows?: boolean;\n\t/** Camera configuration */\n\tcameraOptions?: CameraOptions;\n\t/** Escape hatch for raw WebGLRendererParameters not otherwise exposed. */\n\tthreeInit?: Partial<WebGLRendererParameters>;\n}\n\n/**\n * Configuration options for the 3D renderer plugin.\n *\n * Supports two modes:\n * 1. **Pre-initialized**: Pass already-initialized renderer, scene, camera\n * 2. **Managed**: Omit them and the plugin creates everything during `ecs.initialize()`\n *\n * This plugin includes 3D transform propagation automatically.\n *\n * @example Pre-initialized mode\n * ```typescript\n * const renderer = new WebGLRenderer({ antialias: true });\n * const scene = new Scene();\n * const camera = new PerspectiveCamera(75, w / h, 0.1, 1000);\n *\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer3DPlugin({ renderer, scene, camera }))\n * .build();\n * ```\n *\n * @example Managed mode\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer3DPlugin({\n * container: '#game',\n * background: 0x1099bb,\n * antialias: true,\n * cameraOptions: { fov: 75, position: { x: 0, y: 5, z: 10 } },\n * }))\n * .build();\n * await ecs.initialize();\n * ```\n */\nexport type Renderer3DPluginOptions<G extends string = 'renderer3d'> =\n\tRenderer3DPluginPreInitOptions<G> | Renderer3DPluginManagedOptions<G>;\n\n// ==================== Helper Utilities ====================\n\ninterface PositionOption3D {\n\tx?: number;\n\ty?: number;\n\tz?: number;\n}\n\ninterface TransformOptions3D {\n\trotation?: { x?: number; y?: number; z?: number };\n\tscale?: number | { x: number; y: number; z: number };\n\tvisible?: boolean;\n}\n\nfunction buildTransformComponents(\n\tposition?: PositionOption3D,\n\toptions?: TransformOptions3D,\n): Transform3DComponentTypes {\n\tconst scaleValue = options?.scale;\n\tconst scaleOpts = typeof scaleValue === 'number'\n\t\t? { scale: scaleValue }\n\t\t: scaleValue\n\t\t\t? { scaleX: scaleValue.x, scaleY: scaleValue.y, scaleZ: scaleValue.z }\n\t\t\t: undefined;\n\n\treturn createTransform3D(\n\t\tposition?.x ?? 0,\n\t\tposition?.y ?? 0,\n\t\tposition?.z ?? 0,\n\t\t{ rotation: options?.rotation, ...scaleOpts },\n\t);\n}\n\n/**\n * Create components for a mesh entity.\n * Returns an object suitable for spreading into spawn().\n *\n * @example\n * ```typescript\n * const player = ecs.spawn({\n * ...createMeshComponents(myMesh, { x: 10, y: 0, z: -5 }),\n * velocity: { x: 0, y: 0, z: 0 },\n * });\n * ```\n */\nexport function createMeshComponents(\n\tmesh: Mesh,\n\tposition?: PositionOption3D,\n\toptions?: TransformOptions3D,\n): Pick<Renderer3DComponentTypes, 'mesh' | 'localTransform3D' | 'worldTransform3D' | 'visible3d'> {\n\treturn {\n\t\tmesh,\n\t\t...buildTransformComponents(position, options),\n\t\tvisible3d: { visible: options?.visible ?? true },\n\t};\n}\n\n/**\n * Create components for a group entity.\n * Returns an object suitable for spreading into spawn().\n *\n * @example\n * ```typescript\n * const enemies = ecs.spawn({\n * ...createGroupComponents(enemyGroup, { x: 50, y: 0, z: -30 }),\n * });\n * ```\n */\nexport function createGroupComponents(\n\tgroup: Group,\n\tposition?: PositionOption3D,\n\toptions?: TransformOptions3D,\n): Pick<Renderer3DComponentTypes, 'group' | 'localTransform3D' | 'worldTransform3D' | 'visible3d'> {\n\treturn {\n\t\tgroup,\n\t\t...buildTransformComponents(position, options),\n\t\tvisible3d: { visible: options?.visible ?? true },\n\t};\n}\n\n/**\n * Create components for a generic Object3D entity.\n * Returns an object suitable for spreading into spawn().\n *\n * @example\n * ```typescript\n * const obj = ecs.spawn({\n * ...createObject3DComponents(myObject, { x: 0, y: 0, z: 0 }),\n * });\n * ```\n */\nexport function createObject3DComponents(\n\tobject3d: Object3D,\n\tposition?: PositionOption3D,\n\toptions?: TransformOptions3D,\n): Pick<Renderer3DComponentTypes, 'object3d' | 'localTransform3D' | 'worldTransform3D' | 'visible3d'> {\n\treturn {\n\t\tobject3d,\n\t\t...buildTransformComponents(position, options),\n\t\tvisible3d: { visible: options?.visible ?? true },\n\t};\n}\n\n// ==================== Sync Helper ====================\n\n/**\n * Apply worldTransform3D and visible3d to a Three.js Object3D.\n *\n * Managed objects have matrixAutoUpdate / matrixWorldAutoUpdate disabled\n * (see addToScene), so we recompose obj.matrix and refresh obj.matrixWorld\n * ourselves. Because the plugin keeps a flat scene graph AND assumes the scene\n * root stays at identity, world = local — so we skip a 4x4 matmul and copy.\n *\n * Note: obj.position / obj.rotation / obj.scale are NOT kept in sync — read\n * worldTransform3D from the ECS component if you need current values.\n */\nfunction syncObject3D(\n\tobj: Object3D,\n\twt: WorldTransform3D,\n\tvis: Visible3D,\n\tpos: Vector3,\n\teuler: Euler,\n\tquat: Quaternion,\n\tscale: Vector3,\n): void {\n\tpos.set(wt.x, wt.y, wt.z);\n\teuler.set(wt.rx, wt.ry, wt.rz, 'XYZ');\n\tquat.setFromEuler(euler);\n\tscale.set(wt.sx, wt.sy, wt.sz);\n\tobj.matrix.compose(pos, quat, scale);\n\tobj.matrixWorld.copy(obj.matrix);\n\tobj.visible = vis.visible;\n}\n\n// ==================== Plugin Factory ====================\n\ntype Renderer3DLabels = 'renderer3d-sync' | 'renderer3d-scene-graph' | 'renderer3d-render' | 'transform3d-propagation';\ntype Renderer3DReactiveQueryNames = 'renderer3d-meshes' | 'renderer3d-groups' | 'renderer3d-objects';\n\n/**\n * Create a 3D rendering plugin for ECSpresso.\n *\n * This plugin provides:\n * - 3D transform propagation (localTransform3D -> worldTransform3D)\n * - Render sync system (updates Three.js objects from ECS components)\n * - Scene graph management (auto-adds/removes Three.js objects)\n * - Render call (renderer.render(scene, camera) each frame)\n * - Optional requestAnimationFrame loop\n */\nexport function createRenderer3DPlugin<G extends string = 'renderer3d'>(\n\toptions: Renderer3DPluginOptions<G>,\n): Plugin<WorldConfigFrom<Renderer3DComponentTypes, Renderer3DEventTypes, Renderer3DResourceTypes>, EmptyConfig, Renderer3DLabels, G, never, Renderer3DReactiveQueryNames> {\n\tconst {\n\t\tsystemGroup = 'renderer3d',\n\t\trenderSyncPriority = 500,\n\t\ttransform: transformOptions,\n\t\tstartLoop = true,\n\t} = options;\n\n\t// Entity ID -> Three.js Object3D mapping for scene graph management\n\tconst entityToThreeObject = new Map<number, Object3D>();\n\n\t// Cached resource references, set during scene-graph init for hot-path access\n\tlet cachedRenderer: WebGLRenderer | null = null;\n\tlet cachedScene: Scene | null = null;\n\tlet cachedCamera: Camera | null = null;\n\n\t// Preallocated math temporaries for syncObject3D, allocated during scene-graph init.\n\tlet tmpPos: Vector3 | null = null;\n\tlet tmpEuler: Euler | null = null;\n\tlet tmpQuat: Quaternion | null = null;\n\tlet tmpScale: Vector3 | null = null;\n\n\t// Determine mode: pre-initialized if renderer was provided\n\tconst isManaged = !('renderer' in options && options.renderer !== undefined);\n\n\ttype PluginECS = ECSpresso<WorldConfigFrom<Renderer3DComponentTypes, Renderer3DEventTypes, Renderer3DResourceTypes>>;\n\n\treturn definePlugin('renderer3d')\n\t\t.withComponentTypes<Renderer3DComponentTypes>()\n\t\t.withEventTypes<Renderer3DEventTypes>()\n\t\t.withResourceTypes<Renderer3DResourceTypes>()\n\t\t.withLabels<Renderer3DLabels>()\n\t\t.withGroups<G>()\n\t\t.withReactiveQueryNames<Renderer3DReactiveQueryNames>()\n\t\t.install((world) => {\n\t\t\t// Install 3D transform plugin (deduplicates if already installed)\n\t\t\tworld.installPlugin(createTransform3DPlugin(transformOptions));\n\n\t\t\t// Register resources based on mode\n\t\t\tif (isManaged) {\n\t\t\t\tconst managedOptions = options as Renderer3DPluginManagedOptions<G>;\n\t\t\t\tconst {\n\t\t\t\t\tbackground,\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t\tantialias = true,\n\t\t\t\t\tshadows = false,\n\t\t\t\t\tcameraOptions,\n\t\t\t\t\tthreeInit,\n\t\t\t\t} = managedOptions;\n\t\t\t\tconst containerOption = managedOptions.container ?? document.body;\n\n\t\t\t\tworld.addResource('threeRenderer', async () => {\n\t\t\t\t\tconst { WebGLRenderer: WebGLRendererClass } = await import('three');\n\n\t\t\t\t\tconst containerEl: HTMLElement | null = typeof containerOption === 'string'\n\t\t\t\t\t\t? document.querySelector<HTMLElement>(containerOption)\n\t\t\t\t\t\t: containerOption;\n\n\t\t\t\t\tconst rendererParams: WebGLRendererParameters = {\n\t\t\t\t\t\tantialias,\n\t\t\t\t\t\tpowerPreference: 'high-performance',\n\t\t\t\t\t\t...threeInit,\n\t\t\t\t\t};\n\n\t\t\t\t\tconst renderer = new WebGLRendererClass(rendererParams);\n\n\t\t\t\t\tif (shadows) {\n\t\t\t\t\t\trenderer.shadowMap.enabled = true;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst w = width ?? containerEl?.clientWidth ?? window.innerWidth;\n\t\t\t\t\tconst h = height ?? containerEl?.clientHeight ?? window.innerHeight;\n\t\t\t\t\trenderer.setSize(w, h);\n\n\t\t\t\t\tif (containerEl) {\n\t\t\t\t\t\tcontainerEl.appendChild(renderer.domElement);\n\t\t\t\t\t} else if (typeof containerOption === 'string') {\n\t\t\t\t\t\tconsole.warn(`Renderer3D plugin: container selector \"${containerOption}\" not found`);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn renderer;\n\t\t\t\t});\n\n\t\t\t\tworld.addResource('scene', {\n\t\t\t\t\tdependsOn: ['threeRenderer'],\n\t\t\t\t\tfactory: async () => {\n\t\t\t\t\t\tconst { Scene: SceneClass, Color } = await import('three');\n\t\t\t\t\t\tconst scene = new SceneClass();\n\t\t\t\t\t\tif (background !== undefined) {\n\t\t\t\t\t\t\tscene.background = new Color(background);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn scene;\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tworld.addResource('camera', {\n\t\t\t\t\tdependsOn: ['threeRenderer'],\n\t\t\t\t\tfactory: async (ecs) => {\n\t\t\t\t\t\tconst renderer = ecs.getResource('threeRenderer');\n\t\t\t\t\t\tconst aspect = renderer.domElement.width / renderer.domElement.height;\n\t\t\t\t\t\tconst near = cameraOptions?.near ?? 0.1;\n\t\t\t\t\t\tconst far = cameraOptions?.far ?? 1000;\n\n\t\t\t\t\t\tlet cam: Camera;\n\t\t\t\t\t\tif (cameraOptions?.projection === 'orthographic') {\n\t\t\t\t\t\t\tconst { OrthographicCamera: OrthographicCameraClass } = await import('three');\n\t\t\t\t\t\t\tconst viewSize = cameraOptions.viewSize ?? 10;\n\t\t\t\t\t\t\tconst halfH = viewSize / 2;\n\t\t\t\t\t\t\tconst halfW = halfH * aspect;\n\t\t\t\t\t\t\tconst ortho = new OrthographicCameraClass(-halfW, halfW, halfH, -halfH, near, far);\n\t\t\t\t\t\t\tortho.zoom = cameraOptions.zoom ?? 1;\n\t\t\t\t\t\t\tortho.updateProjectionMatrix();\n\t\t\t\t\t\t\tcam = ortho;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst { PerspectiveCamera: PerspectiveCameraClass } = await import('three');\n\t\t\t\t\t\t\tconst fov = cameraOptions?.fov ?? 75;\n\t\t\t\t\t\t\tcam = new PerspectiveCameraClass(fov, aspect, near, far);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (cameraOptions?.position) {\n\t\t\t\t\t\t\tcam.position.set(\n\t\t\t\t\t\t\t\tcameraOptions.position.x,\n\t\t\t\t\t\t\t\tcameraOptions.position.y,\n\t\t\t\t\t\t\t\tcameraOptions.position.z,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (cameraOptions?.lookAt) {\n\t\t\t\t\t\t\tcam.lookAt(\n\t\t\t\t\t\t\t\tcameraOptions.lookAt.x,\n\t\t\t\t\t\t\t\tcameraOptions.lookAt.y,\n\t\t\t\t\t\t\t\tcameraOptions.lookAt.z,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn cam;\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst preInit = options as Renderer3DPluginPreInitOptions<G>;\n\t\t\t\tworld.addResource('threeRenderer', preInit.renderer);\n\t\t\t\tworld.addResource('scene', preInit.scene);\n\t\t\t\tworld.addResource('camera', preInit.camera);\n\t\t\t}\n\n\t\t\t// Register dispose callbacks for 3D object components\n\t\t\tworld.registerDispose('mesh', ({ value }) => {\n\t\t\t\tif (value.parent) value.parent.remove(value);\n\t\t\t});\n\t\t\tworld.registerDispose('group', ({ value }) => {\n\t\t\t\tif (value.parent) value.parent.remove(value);\n\t\t\t});\n\t\t\tworld.registerDispose('object3d', ({ value }) => {\n\t\t\t\tif (value.parent) value.parent.remove(value);\n\t\t\t});\n\n\t\t\t// 3D objects require localTransform3D and visible3d\n\t\t\tworld.registerRequired('mesh', 'localTransform3D', () => ({ ...DEFAULT_LOCAL_TRANSFORM_3D }));\n\t\t\tworld.registerRequired('mesh', 'visible3d', () => ({ visible: true }));\n\t\t\tworld.registerRequired('group', 'localTransform3D', () => ({ ...DEFAULT_LOCAL_TRANSFORM_3D }));\n\t\t\tworld.registerRequired('group', 'visible3d', () => ({ visible: true }));\n\t\t\tworld.registerRequired('object3d', 'localTransform3D', () => ({ ...DEFAULT_LOCAL_TRANSFORM_3D }));\n\t\t\tworld.registerRequired('object3d', 'visible3d', () => ({ visible: true }));\n\n\t\t\t// ==================== Render Sync System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('renderer3d-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('meshes', {\n\t\t\t\t\twith: ['mesh', 'worldTransform3D', 'visible3d'],\n\t\t\t\t\tchanged: ['worldTransform3D'],\n\t\t\t\t})\n\t\t\t\t.addQuery('groups', {\n\t\t\t\t\twith: ['group', 'worldTransform3D', 'visible3d'],\n\t\t\t\t\tchanged: ['worldTransform3D'],\n\t\t\t\t})\n\t\t\t\t.addQuery('objects', {\n\t\t\t\t\twith: ['object3d', 'worldTransform3D', 'visible3d'],\n\t\t\t\t\tchanged: ['worldTransform3D'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tconst pos = tmpPos;\n\t\t\t\t\tconst euler = tmpEuler;\n\t\t\t\t\tconst quat = tmpQuat;\n\t\t\t\t\tconst scale = tmpScale;\n\t\t\t\t\tif (!pos || !euler || !quat || !scale) return;\n\n\t\t\t\t\tfor (const entity of queries.meshes) {\n\t\t\t\t\t\tconst { mesh, worldTransform3D, visible3d } = entity.components;\n\t\t\t\t\t\tsyncObject3D(mesh, worldTransform3D, visible3d, pos, euler, quat, scale);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.groups) {\n\t\t\t\t\t\tconst { group, worldTransform3D, visible3d } = entity.components;\n\t\t\t\t\t\tsyncObject3D(group, worldTransform3D, visible3d, pos, euler, quat, scale);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.objects) {\n\t\t\t\t\t\tconst { object3d, worldTransform3D, visible3d } = entity.components;\n\t\t\t\t\t\tsyncObject3D(object3d, worldTransform3D, visible3d, pos, euler, quat, scale);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Scene Graph Manager System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('renderer3d-scene-graph')\n\t\t\t\t.setPriority(9999)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs: PluginECS) => {\n\t\t\t\t\tconst { Vector3: Vector3Class, Euler: EulerClass, Quaternion: QuaternionClass } = await import('three');\n\t\t\t\t\ttmpPos = new Vector3Class();\n\t\t\t\t\ttmpEuler = new EulerClass();\n\t\t\t\t\ttmpQuat = new QuaternionClass();\n\t\t\t\t\ttmpScale = new Vector3Class();\n\n\t\t\t\t\tconst scene = ecs.getResource('scene');\n\t\t\t\t\tconst threeRenderer = ecs.getResource('threeRenderer');\n\t\t\t\t\tconst camera = ecs.getResource('camera');\n\n\t\t\t\t\t// Cache for hot-path render system\n\t\t\t\t\tcachedRenderer = threeRenderer;\n\t\t\t\t\tcachedScene = scene;\n\t\t\t\t\tcachedCamera = camera;\n\n\t\t\t\t\t// Helper to add a Three.js object to the scene.\n\t\t\t\t\t// Disable Three.js's per-frame matrix bookkeeping for managed objects:\n\t\t\t\t\t// the sync system writes obj.matrix and obj.matrixWorld manually only when\n\t\t\t\t\t// worldTransform3D actually changes, skipping the work for static frames.\n\t\t\t\t\tfunction addToScene(entityId: number, obj: Object3D): void {\n\t\t\t\t\t\tobj.matrixAutoUpdate = false;\n\t\t\t\t\t\tobj.matrixWorldAutoUpdate = false;\n\t\t\t\t\t\tentityToThreeObject.set(entityId, obj);\n\t\t\t\t\t\tscene.add(obj);\n\t\t\t\t\t}\n\n\t\t\t\t\tecs.addReactiveQuery('renderer3d-meshes', {\n\t\t\t\t\t\twith: ['mesh'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\taddToScene(entity.id, entity.components.mesh);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tentityToThreeObject.delete(entityId);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.addReactiveQuery('renderer3d-groups', {\n\t\t\t\t\t\twith: ['group'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\taddToScene(entity.id, entity.components.group);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tentityToThreeObject.delete(entityId);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.addReactiveQuery('renderer3d-objects', {\n\t\t\t\t\t\twith: ['object3d'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\taddToScene(entity.id, entity.components.object3d);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tentityToThreeObject.delete(entityId);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.on('hierarchyChanged', ({ entityId }) => {\n\t\t\t\t\t\tconst obj = entityToThreeObject.get(entityId);\n\t\t\t\t\t\tif (!obj) return;\n\t\t\t\t\t\t// Scene graph stays flat — all objects are children of scene directly.\n\t\t\t\t\t\t// Re-add to scene if somehow removed.\n\t\t\t\t\t\tif (obj.parent !== scene) {\n\t\t\t\t\t\t\tscene.add(obj);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\t// Resize handler\n\t\t\t\t\tconst resizeHandler = () => {\n\t\t\t\t\t\tconst w = threeRenderer.domElement.parentElement?.clientWidth ?? window.innerWidth;\n\t\t\t\t\t\tconst h = threeRenderer.domElement.parentElement?.clientHeight ?? window.innerHeight;\n\t\t\t\t\t\tthreeRenderer.setSize(w, h);\n\t\t\t\t\t\tif ((camera as PerspectiveCamera).isPerspectiveCamera) {\n\t\t\t\t\t\t\tconst perspCam = camera as PerspectiveCamera;\n\t\t\t\t\t\t\tperspCam.aspect = w / h;\n\t\t\t\t\t\t\tperspCam.updateProjectionMatrix();\n\t\t\t\t\t\t} else if ((camera as OrthographicCamera).isOrthographicCamera) {\n\t\t\t\t\t\t\tconst orthoCam = camera as OrthographicCamera;\n\t\t\t\t\t\t\tconst halfH = (orthoCam.top - orthoCam.bottom) / 2;\n\t\t\t\t\t\t\tconst halfW = halfH * (w / h);\n\t\t\t\t\t\t\torthoCam.left = -halfW;\n\t\t\t\t\t\t\torthoCam.right = halfW;\n\t\t\t\t\t\t\torthoCam.top = halfH;\n\t\t\t\t\t\t\torthoCam.bottom = -halfH;\n\t\t\t\t\t\t\torthoCam.updateProjectionMatrix();\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\twindow.addEventListener('resize', resizeHandler);\n\n\t\t\t\t\t// Animation loop\n\t\t\t\t\tif (startLoop) {\n\t\t\t\t\t\tlet lastTime = 0;\n\t\t\t\t\t\tconst animate = (time: number) => {\n\t\t\t\t\t\t\trequestAnimationFrame(animate);\n\t\t\t\t\t\t\tconst dt = lastTime === 0 ? 0 : (time - lastTime) / 1000;\n\t\t\t\t\t\t\tlastTime = time;\n\t\t\t\t\t\t\tecs.update(dt);\n\t\t\t\t\t\t};\n\t\t\t\t\t\trequestAnimationFrame(animate);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Render System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('renderer3d-render')\n\t\t\t\t.setPriority(0)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\tif (cachedRenderer && cachedScene && cachedCamera) {\n\t\t\t\t\t\tcachedRenderer.render(cachedScene, cachedCamera);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
5
+ "/**\n * 3D Renderer Plugin for ECSpresso\n *\n * An opt-in Three.js-based 3D rendering plugin that automates scene graph wiring.\n * Import from 'ecspresso/plugins/rendering/renderer3D'\n *\n * This plugin includes 3D transform propagation automatically.\n */\n\nimport type {\n\tWebGLRenderer,\n\tWebGLRendererParameters,\n\tScene,\n\tCamera,\n\tPerspectiveCamera,\n\tOrthographicCamera,\n\tObject3D,\n\tMesh,\n\tGroup,\n\tColorRepresentation,\n\tVector3,\n\tEuler,\n\tQuaternion,\n} from 'three';\nimport { definePlugin, type Plugin } from 'ecspresso';\nimport type { ComponentsConfig, EmptyConfig, EventsConfig, ResourcesConfig } from '../../type-utils';\nimport type ECSpresso from 'ecspresso';\nimport {\n\tcreateTransform3DPlugin,\n\tcreateTransform3D,\n\ttype LocalTransform3D,\n\ttype WorldTransform3D,\n\ttype Transform3DComponentTypes,\n\ttype Transform3DPluginOptions,\n\tDEFAULT_LOCAL_TRANSFORM_3D,\n} from 'ecspresso/plugins/spatial/transform3D';\n\n// Re-export transform types for convenience\nexport type { LocalTransform3D, WorldTransform3D, Transform3DComponentTypes };\nexport {\n\tcreateTransform3D,\n\tcreateLocalTransform3D,\n\tcreateWorldTransform3D,\n\tDEFAULT_LOCAL_TRANSFORM_3D,\n\tDEFAULT_WORLD_TRANSFORM_3D,\n} from 'ecspresso/plugins/spatial/transform3D';\n\n// ==================== Component Types ====================\n\n/**\n * Visibility component for 3D entities.\n */\nexport interface Visible3D {\n\tvisible: boolean;\n}\n\n/**\n * Aggregate component types for the 3D renderer plugin.\n * Included automatically via `.withPlugin(createRenderer3DPlugin({ ... }))`.\n */\nexport interface Renderer3DComponentTypes extends Transform3DComponentTypes {\n\tmesh: Mesh;\n\tgroup: Group;\n\tobject3d: Object3D;\n\tvisible3d: Visible3D;\n\t/** Controls Three.js Object3D.renderOrder for manual z-ordering */\n\trenderOrder: number;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Events emitted by the 3D renderer plugin.\n */\nexport interface Renderer3DEventTypes {\n\thierarchyChanged: {\n\t\tentityId: number;\n\t\toldParent: number | null;\n\t\tnewParent: number | null;\n\t};\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Resources provided by the 3D renderer plugin.\n */\nexport interface Renderer3DResourceTypes {\n\tthreeRenderer: WebGLRenderer;\n\tscene: Scene;\n\tcamera: Camera;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Common options shared between both initialization modes.\n */\ninterface Renderer3DPluginCommonOptions<G extends string = 'renderer3d'> {\n\t/** System group name (default: 'renderer3d') */\n\tsystemGroup?: G;\n\t/** Priority for render sync system (default: 500) */\n\trenderSyncPriority?: number;\n\t/** Options for the included 3D transform plugin */\n\ttransform?: Transform3DPluginOptions;\n\t/** When true, starts a requestAnimationFrame loop to drive ecs.update() automatically (default: true) */\n\tstartLoop?: boolean;\n}\n\n/**\n * Options when providing pre-initialized Three.js objects.\n */\nexport interface Renderer3DPluginPreInitOptions<G extends string = 'renderer3d'> extends Renderer3DPluginCommonOptions<G> {\n\t/** Pre-initialized WebGLRenderer */\n\trenderer: WebGLRenderer;\n\t/** Pre-initialized Scene */\n\tscene: Scene;\n\t/** Pre-initialized Camera */\n\tcamera: Camera;\n\tcontainer?: never;\n\tbackground?: never;\n\twidth?: never;\n\theight?: never;\n\tantialias?: never;\n\tshadows?: never;\n\tcameraOptions?: never;\n\tthreeInit?: never;\n}\n\n/**\n * Camera configuration for managed mode.\n *\n * Discriminated on `projection`. Defaults to `'perspective'` when omitted.\n * Orthographic cameras use `viewSize` (world-unit height at zoom=1) to define\n * the base frustum; `zoom` maps directly to Three.js's `OrthographicCamera.zoom`.\n */\nexport type CameraOptions =\n\t| {\n\t\tprojection?: 'perspective';\n\t\tfov?: number;\n\t\tnear?: number;\n\t\tfar?: number;\n\t\tposition?: { x: number; y: number; z: number };\n\t\tlookAt?: { x: number; y: number; z: number };\n\t}\n\t| {\n\t\tprojection: 'orthographic';\n\t\tviewSize?: number;\n\t\tzoom?: number;\n\t\tnear?: number;\n\t\tfar?: number;\n\t\tposition?: { x: number; y: number; z: number };\n\t\tlookAt?: { x: number; y: number; z: number };\n\t};\n\n/**\n * Options when letting the plugin create and manage Three.js objects.\n */\nexport interface Renderer3DPluginManagedOptions<G extends string = 'renderer3d'> extends Renderer3DPluginCommonOptions<G> {\n\trenderer?: never;\n\tscene?: never;\n\tcamera?: never;\n\t/** Container element to append the canvas to (or CSS selector string). Defaults to `document.body`. */\n\tcontainer?: HTMLElement | string;\n\t/** Scene background color. */\n\tbackground?: ColorRepresentation;\n\t/** Canvas width. When omitted, auto-sizes to container. */\n\twidth?: number;\n\t/** Canvas height. When omitted, auto-sizes to container. */\n\theight?: number;\n\t/** Enable antialiasing (default: true) */\n\tantialias?: boolean;\n\t/** Enable shadow mapping (default: false) */\n\tshadows?: boolean;\n\t/** Camera configuration */\n\tcameraOptions?: CameraOptions;\n\t/** Escape hatch for raw WebGLRendererParameters not otherwise exposed. */\n\tthreeInit?: Partial<WebGLRendererParameters>;\n}\n\n/**\n * Configuration options for the 3D renderer plugin.\n *\n * Supports two modes:\n * 1. **Pre-initialized**: Pass already-initialized renderer, scene, camera\n * 2. **Managed**: Omit them and the plugin creates everything during `ecs.initialize()`\n *\n * This plugin includes 3D transform propagation automatically.\n *\n * @example Pre-initialized mode\n * ```typescript\n * const renderer = new WebGLRenderer({ antialias: true });\n * const scene = new Scene();\n * const camera = new PerspectiveCamera(75, w / h, 0.1, 1000);\n *\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer3DPlugin({ renderer, scene, camera }))\n * .build();\n * ```\n *\n * @example Managed mode\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createRenderer3DPlugin({\n * container: '#game',\n * background: 0x1099bb,\n * antialias: true,\n * cameraOptions: { fov: 75, position: { x: 0, y: 5, z: 10 } },\n * }))\n * .build();\n * await ecs.initialize();\n * ```\n */\nexport type Renderer3DPluginOptions<G extends string = 'renderer3d'> =\n\tRenderer3DPluginPreInitOptions<G> | Renderer3DPluginManagedOptions<G>;\n\n// ==================== Helper Utilities ====================\n\ninterface PositionOption3D {\n\tx?: number;\n\ty?: number;\n\tz?: number;\n}\n\ninterface TransformOptions3D {\n\trotation?: { x?: number; y?: number; z?: number };\n\tscale?: number | { x: number; y: number; z: number };\n\tvisible?: boolean;\n}\n\nfunction buildTransformComponents(\n\tposition?: PositionOption3D,\n\toptions?: TransformOptions3D,\n): Transform3DComponentTypes {\n\tconst scaleValue = options?.scale;\n\tconst scaleOpts = typeof scaleValue === 'number'\n\t\t? { scale: scaleValue }\n\t\t: scaleValue\n\t\t\t? { scaleX: scaleValue.x, scaleY: scaleValue.y, scaleZ: scaleValue.z }\n\t\t\t: undefined;\n\n\treturn createTransform3D(\n\t\tposition?.x ?? 0,\n\t\tposition?.y ?? 0,\n\t\tposition?.z ?? 0,\n\t\t{ rotation: options?.rotation, ...scaleOpts },\n\t);\n}\n\n/**\n * Create components for a mesh entity.\n * Returns an object suitable for spreading into spawn().\n *\n * @example\n * ```typescript\n * const player = ecs.spawn({\n * ...createMeshComponents(myMesh, { x: 10, y: 0, z: -5 }),\n * velocity: { x: 0, y: 0, z: 0 },\n * });\n * ```\n */\nexport function createMeshComponents(\n\tmesh: Mesh,\n\tposition?: PositionOption3D,\n\toptions?: TransformOptions3D,\n): Pick<Renderer3DComponentTypes, 'mesh' | 'localTransform3D' | 'worldTransform3D' | 'visible3d'> {\n\treturn {\n\t\tmesh,\n\t\t...buildTransformComponents(position, options),\n\t\tvisible3d: { visible: options?.visible ?? true },\n\t};\n}\n\n/**\n * Create components for a group entity.\n * Returns an object suitable for spreading into spawn().\n *\n * @example\n * ```typescript\n * const enemies = ecs.spawn({\n * ...createGroupComponents(enemyGroup, { x: 50, y: 0, z: -30 }),\n * });\n * ```\n */\nexport function createGroupComponents(\n\tgroup: Group,\n\tposition?: PositionOption3D,\n\toptions?: TransformOptions3D,\n): Pick<Renderer3DComponentTypes, 'group' | 'localTransform3D' | 'worldTransform3D' | 'visible3d'> {\n\treturn {\n\t\tgroup,\n\t\t...buildTransformComponents(position, options),\n\t\tvisible3d: { visible: options?.visible ?? true },\n\t};\n}\n\n/**\n * Create components for a generic Object3D entity.\n * Returns an object suitable for spreading into spawn().\n *\n * @example\n * ```typescript\n * const obj = ecs.spawn({\n * ...createObject3DComponents(myObject, { x: 0, y: 0, z: 0 }),\n * });\n * ```\n */\nexport function createObject3DComponents(\n\tobject3d: Object3D,\n\tposition?: PositionOption3D,\n\toptions?: TransformOptions3D,\n): Pick<Renderer3DComponentTypes, 'object3d' | 'localTransform3D' | 'worldTransform3D' | 'visible3d'> {\n\treturn {\n\t\tobject3d,\n\t\t...buildTransformComponents(position, options),\n\t\tvisible3d: { visible: options?.visible ?? true },\n\t};\n}\n\n// ==================== Sync Helper ====================\n\n/**\n * Apply worldTransform3D and visible3d to a Three.js Object3D.\n *\n * Managed objects have matrixAutoUpdate / matrixWorldAutoUpdate disabled\n * (see addToScene), so we recompose obj.matrix and refresh obj.matrixWorld\n * ourselves. Because the plugin keeps a flat scene graph AND assumes the scene\n * root stays at identity, world = local — so we skip a 4x4 matmul and copy.\n *\n * Note: obj.position / obj.rotation / obj.scale are NOT kept in sync — read\n * worldTransform3D from the ECS component if you need current values.\n */\nfunction syncObject3D(\n\tobj: Object3D,\n\twt: WorldTransform3D,\n\tvis: Visible3D,\n\tpos: Vector3,\n\teuler: Euler,\n\tquat: Quaternion,\n\tscale: Vector3,\n): void {\n\tpos.set(wt.x, wt.y, wt.z);\n\teuler.set(wt.rx, wt.ry, wt.rz, 'XYZ');\n\tquat.setFromEuler(euler);\n\tscale.set(wt.sx, wt.sy, wt.sz);\n\tobj.matrix.compose(pos, quat, scale);\n\tobj.matrixWorld.copy(obj.matrix);\n\tobj.visible = vis.visible;\n}\n\n// ==================== Plugin Factory ====================\n\ntype Renderer3DLabels = 'renderer3d-sync' | 'renderer3d-scene-graph' | 'renderer3d-render' | 'transform3d-propagation';\ntype Renderer3DReactiveQueryNames = 'renderer3d-meshes' | 'renderer3d-groups' | 'renderer3d-objects';\ntype Renderer3DWorldConfig =\n\tComponentsConfig<Renderer3DComponentTypes>\n\t& EventsConfig<Renderer3DEventTypes>\n\t& ResourcesConfig<Renderer3DResourceTypes>;\n\n/**\n * Create a 3D rendering plugin for ECSpresso.\n *\n * This plugin provides:\n * - 3D transform propagation (localTransform3D -> worldTransform3D)\n * - Render sync system (updates Three.js objects from ECS components)\n * - Scene graph management (auto-adds/removes Three.js objects)\n * - Render call (renderer.render(scene, camera) each frame)\n * - Optional requestAnimationFrame loop\n */\nexport function createRenderer3DPlugin<G extends string = 'renderer3d'>(\n\toptions: Renderer3DPluginOptions<G>,\n): Plugin<Renderer3DWorldConfig, EmptyConfig, Renderer3DLabels, G, never, Renderer3DReactiveQueryNames> {\n\tconst {\n\t\tsystemGroup = 'renderer3d',\n\t\trenderSyncPriority = 500,\n\t\ttransform: transformOptions,\n\t\tstartLoop = true,\n\t} = options;\n\n\t// Entity ID -> Three.js Object3D mapping for scene graph management\n\tconst entityToThreeObject = new Map<number, Object3D>();\n\n\t// Cached resource references, set during scene-graph init for hot-path access\n\tlet cachedRenderer: WebGLRenderer | null = null;\n\tlet cachedScene: Scene | null = null;\n\tlet cachedCamera: Camera | null = null;\n\n\t// Preallocated math temporaries for syncObject3D, allocated during scene-graph init.\n\tlet tmpPos: Vector3 | null = null;\n\tlet tmpEuler: Euler | null = null;\n\tlet tmpQuat: Quaternion | null = null;\n\tlet tmpScale: Vector3 | null = null;\n\n\t// Determine mode: pre-initialized if renderer was provided\n\tconst isManaged = !('renderer' in options && options.renderer !== undefined);\n\n\ttype PluginECS = ECSpresso<Renderer3DWorldConfig>;\n\n\treturn definePlugin('renderer3d')\n\t\t.withComponentTypes<Renderer3DComponentTypes>()\n\t\t.withEventTypes<Renderer3DEventTypes>()\n\t\t.withResourceTypes<Renderer3DResourceTypes>()\n\t\t.withLabels<Renderer3DLabels>()\n\t\t.withGroups<G>()\n\t\t.withReactiveQueryNames<Renderer3DReactiveQueryNames>()\n\t\t.install((world) => {\n\t\t\t// Install 3D transform plugin (deduplicates if already installed)\n\t\t\tworld.installPlugin(createTransform3DPlugin(transformOptions));\n\n\t\t\t// Register resources based on mode\n\t\t\tif (isManaged) {\n\t\t\t\tconst managedOptions = options as Renderer3DPluginManagedOptions<G>;\n\t\t\t\tconst {\n\t\t\t\t\tbackground,\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t\tantialias = true,\n\t\t\t\t\tshadows = false,\n\t\t\t\t\tcameraOptions,\n\t\t\t\t\tthreeInit,\n\t\t\t\t} = managedOptions;\n\t\t\t\tconst containerOption = managedOptions.container ?? document.body;\n\n\t\t\t\tworld.addResource('threeRenderer', async () => {\n\t\t\t\t\tconst { WebGLRenderer: WebGLRendererClass } = await import('three');\n\n\t\t\t\t\tconst containerEl: HTMLElement | null = typeof containerOption === 'string'\n\t\t\t\t\t\t? document.querySelector<HTMLElement>(containerOption)\n\t\t\t\t\t\t: containerOption;\n\n\t\t\t\t\tconst rendererParams: WebGLRendererParameters = {\n\t\t\t\t\t\tantialias,\n\t\t\t\t\t\tpowerPreference: 'high-performance',\n\t\t\t\t\t\t...threeInit,\n\t\t\t\t\t};\n\n\t\t\t\t\tconst renderer = new WebGLRendererClass(rendererParams);\n\n\t\t\t\t\tif (shadows) {\n\t\t\t\t\t\trenderer.shadowMap.enabled = true;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst w = width ?? containerEl?.clientWidth ?? window.innerWidth;\n\t\t\t\t\tconst h = height ?? containerEl?.clientHeight ?? window.innerHeight;\n\t\t\t\t\trenderer.setSize(w, h);\n\n\t\t\t\t\tif (containerEl) {\n\t\t\t\t\t\tcontainerEl.appendChild(renderer.domElement);\n\t\t\t\t\t} else if (typeof containerOption === 'string') {\n\t\t\t\t\t\tconsole.warn(`Renderer3D plugin: container selector \"${containerOption}\" not found`);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn renderer;\n\t\t\t\t});\n\n\t\t\t\tworld.addResource('scene', {\n\t\t\t\t\tdependsOn: ['threeRenderer'],\n\t\t\t\t\tfactory: async () => {\n\t\t\t\t\t\tconst { Scene: SceneClass, Color } = await import('three');\n\t\t\t\t\t\tconst scene = new SceneClass();\n\t\t\t\t\t\tif (background !== undefined) {\n\t\t\t\t\t\t\tscene.background = new Color(background);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn scene;\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tworld.addResource('camera', {\n\t\t\t\t\tdependsOn: ['threeRenderer'],\n\t\t\t\t\tfactory: async (ecs) => {\n\t\t\t\t\t\tconst renderer = ecs.getResource('threeRenderer');\n\t\t\t\t\t\tconst aspect = renderer.domElement.width / renderer.domElement.height;\n\t\t\t\t\t\tconst near = cameraOptions?.near ?? 0.1;\n\t\t\t\t\t\tconst far = cameraOptions?.far ?? 1000;\n\n\t\t\t\t\t\tlet cam: Camera;\n\t\t\t\t\t\tif (cameraOptions?.projection === 'orthographic') {\n\t\t\t\t\t\t\tconst { OrthographicCamera: OrthographicCameraClass } = await import('three');\n\t\t\t\t\t\t\tconst viewSize = cameraOptions.viewSize ?? 10;\n\t\t\t\t\t\t\tconst halfH = viewSize / 2;\n\t\t\t\t\t\t\tconst halfW = halfH * aspect;\n\t\t\t\t\t\t\tconst ortho = new OrthographicCameraClass(-halfW, halfW, halfH, -halfH, near, far);\n\t\t\t\t\t\t\tortho.zoom = cameraOptions.zoom ?? 1;\n\t\t\t\t\t\t\tortho.updateProjectionMatrix();\n\t\t\t\t\t\t\tcam = ortho;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst { PerspectiveCamera: PerspectiveCameraClass } = await import('three');\n\t\t\t\t\t\t\tconst fov = cameraOptions?.fov ?? 75;\n\t\t\t\t\t\t\tcam = new PerspectiveCameraClass(fov, aspect, near, far);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (cameraOptions?.position) {\n\t\t\t\t\t\t\tcam.position.set(\n\t\t\t\t\t\t\t\tcameraOptions.position.x,\n\t\t\t\t\t\t\t\tcameraOptions.position.y,\n\t\t\t\t\t\t\t\tcameraOptions.position.z,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (cameraOptions?.lookAt) {\n\t\t\t\t\t\t\tcam.lookAt(\n\t\t\t\t\t\t\t\tcameraOptions.lookAt.x,\n\t\t\t\t\t\t\t\tcameraOptions.lookAt.y,\n\t\t\t\t\t\t\t\tcameraOptions.lookAt.z,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn cam;\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst preInit = options as Renderer3DPluginPreInitOptions<G>;\n\t\t\t\tworld.addResource('threeRenderer', preInit.renderer);\n\t\t\t\tworld.addResource('scene', preInit.scene);\n\t\t\t\tworld.addResource('camera', preInit.camera);\n\t\t\t}\n\n\t\t\t// Register dispose callbacks for 3D object components\n\t\t\tworld.registerDispose('mesh', ({ value }) => {\n\t\t\t\tif (value.parent) value.parent.remove(value);\n\t\t\t});\n\t\t\tworld.registerDispose('group', ({ value }) => {\n\t\t\t\tif (value.parent) value.parent.remove(value);\n\t\t\t});\n\t\t\tworld.registerDispose('object3d', ({ value }) => {\n\t\t\t\tif (value.parent) value.parent.remove(value);\n\t\t\t});\n\n\t\t\t// 3D objects require localTransform3D and visible3d\n\t\t\tworld.registerRequired('mesh', 'localTransform3D', () => ({ ...DEFAULT_LOCAL_TRANSFORM_3D }));\n\t\t\tworld.registerRequired('mesh', 'visible3d', () => ({ visible: true }));\n\t\t\tworld.registerRequired('group', 'localTransform3D', () => ({ ...DEFAULT_LOCAL_TRANSFORM_3D }));\n\t\t\tworld.registerRequired('group', 'visible3d', () => ({ visible: true }));\n\t\t\tworld.registerRequired('object3d', 'localTransform3D', () => ({ ...DEFAULT_LOCAL_TRANSFORM_3D }));\n\t\t\tworld.registerRequired('object3d', 'visible3d', () => ({ visible: true }));\n\n\t\t\t// ==================== Render Sync System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('renderer3d-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('meshes', {\n\t\t\t\t\twith: ['mesh', 'worldTransform3D', 'visible3d'],\n\t\t\t\t\tchanged: ['worldTransform3D'],\n\t\t\t\t})\n\t\t\t\t.addQuery('groups', {\n\t\t\t\t\twith: ['group', 'worldTransform3D', 'visible3d'],\n\t\t\t\t\tchanged: ['worldTransform3D'],\n\t\t\t\t})\n\t\t\t\t.addQuery('objects', {\n\t\t\t\t\twith: ['object3d', 'worldTransform3D', 'visible3d'],\n\t\t\t\t\tchanged: ['worldTransform3D'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tconst pos = tmpPos;\n\t\t\t\t\tconst euler = tmpEuler;\n\t\t\t\t\tconst quat = tmpQuat;\n\t\t\t\t\tconst scale = tmpScale;\n\t\t\t\t\tif (!pos || !euler || !quat || !scale) return;\n\n\t\t\t\t\tfor (const entity of queries.meshes) {\n\t\t\t\t\t\tconst { mesh, worldTransform3D, visible3d } = entity.components;\n\t\t\t\t\t\tsyncObject3D(mesh, worldTransform3D, visible3d, pos, euler, quat, scale);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.groups) {\n\t\t\t\t\t\tconst { group, worldTransform3D, visible3d } = entity.components;\n\t\t\t\t\t\tsyncObject3D(group, worldTransform3D, visible3d, pos, euler, quat, scale);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const entity of queries.objects) {\n\t\t\t\t\t\tconst { object3d, worldTransform3D, visible3d } = entity.components;\n\t\t\t\t\t\tsyncObject3D(object3d, worldTransform3D, visible3d, pos, euler, quat, scale);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Scene Graph Manager System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('renderer3d-scene-graph')\n\t\t\t\t.setPriority(9999)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs: PluginECS) => {\n\t\t\t\t\tconst { Vector3: Vector3Class, Euler: EulerClass, Quaternion: QuaternionClass } = await import('three');\n\t\t\t\t\ttmpPos = new Vector3Class();\n\t\t\t\t\ttmpEuler = new EulerClass();\n\t\t\t\t\ttmpQuat = new QuaternionClass();\n\t\t\t\t\ttmpScale = new Vector3Class();\n\n\t\t\t\t\tconst scene = ecs.getResource('scene');\n\t\t\t\t\tconst threeRenderer = ecs.getResource('threeRenderer');\n\t\t\t\t\tconst camera = ecs.getResource('camera');\n\n\t\t\t\t\t// Cache for hot-path render system\n\t\t\t\t\tcachedRenderer = threeRenderer;\n\t\t\t\t\tcachedScene = scene;\n\t\t\t\t\tcachedCamera = camera;\n\n\t\t\t\t\t// Helper to add a Three.js object to the scene.\n\t\t\t\t\t// Disable Three.js's per-frame matrix bookkeeping for managed objects:\n\t\t\t\t\t// the sync system writes obj.matrix and obj.matrixWorld manually only when\n\t\t\t\t\t// worldTransform3D actually changes, skipping the work for static frames.\n\t\t\t\t\tfunction addToScene(entityId: number, obj: Object3D): void {\n\t\t\t\t\t\tobj.matrixAutoUpdate = false;\n\t\t\t\t\t\tobj.matrixWorldAutoUpdate = false;\n\t\t\t\t\t\tentityToThreeObject.set(entityId, obj);\n\t\t\t\t\t\tscene.add(obj);\n\t\t\t\t\t}\n\n\t\t\t\t\tecs.addReactiveQuery('renderer3d-meshes', {\n\t\t\t\t\t\twith: ['mesh'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\taddToScene(entity.id, entity.components.mesh);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tentityToThreeObject.delete(entityId);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.addReactiveQuery('renderer3d-groups', {\n\t\t\t\t\t\twith: ['group'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\taddToScene(entity.id, entity.components.group);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tentityToThreeObject.delete(entityId);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.addReactiveQuery('renderer3d-objects', {\n\t\t\t\t\t\twith: ['object3d'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\taddToScene(entity.id, entity.components.object3d);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tentityToThreeObject.delete(entityId);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tecs.on('hierarchyChanged', ({ entityId }) => {\n\t\t\t\t\t\tconst obj = entityToThreeObject.get(entityId);\n\t\t\t\t\t\tif (!obj) return;\n\t\t\t\t\t\t// Scene graph stays flat — all objects are children of scene directly.\n\t\t\t\t\t\t// Re-add to scene if somehow removed.\n\t\t\t\t\t\tif (obj.parent !== scene) {\n\t\t\t\t\t\t\tscene.add(obj);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\t// Resize handler\n\t\t\t\t\tconst resizeHandler = () => {\n\t\t\t\t\t\tconst w = threeRenderer.domElement.parentElement?.clientWidth ?? window.innerWidth;\n\t\t\t\t\t\tconst h = threeRenderer.domElement.parentElement?.clientHeight ?? window.innerHeight;\n\t\t\t\t\t\tthreeRenderer.setSize(w, h);\n\t\t\t\t\t\tif ((camera as PerspectiveCamera).isPerspectiveCamera) {\n\t\t\t\t\t\t\tconst perspCam = camera as PerspectiveCamera;\n\t\t\t\t\t\t\tperspCam.aspect = w / h;\n\t\t\t\t\t\t\tperspCam.updateProjectionMatrix();\n\t\t\t\t\t\t} else if ((camera as OrthographicCamera).isOrthographicCamera) {\n\t\t\t\t\t\t\tconst orthoCam = camera as OrthographicCamera;\n\t\t\t\t\t\t\tconst halfH = (orthoCam.top - orthoCam.bottom) / 2;\n\t\t\t\t\t\t\tconst halfW = halfH * (w / h);\n\t\t\t\t\t\t\torthoCam.left = -halfW;\n\t\t\t\t\t\t\torthoCam.right = halfW;\n\t\t\t\t\t\t\torthoCam.top = halfH;\n\t\t\t\t\t\t\torthoCam.bottom = -halfH;\n\t\t\t\t\t\t\torthoCam.updateProjectionMatrix();\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\twindow.addEventListener('resize', resizeHandler);\n\n\t\t\t\t\t// Animation loop\n\t\t\t\t\tif (startLoop) {\n\t\t\t\t\t\tlet lastTime = 0;\n\t\t\t\t\t\tconst animate = (time: number) => {\n\t\t\t\t\t\t\trequestAnimationFrame(animate);\n\t\t\t\t\t\t\tconst dt = lastTime === 0 ? 0 : (time - lastTime) / 1000;\n\t\t\t\t\t\t\tlastTime = time;\n\t\t\t\t\t\t\tecs.update(dt);\n\t\t\t\t\t\t};\n\t\t\t\t\t\trequestAnimationFrame(animate);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// ==================== Render System ====================\n\t\t\tworld\n\t\t\t\t.addSystem('renderer3d-render')\n\t\t\t\t.setPriority(0)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\tif (cachedRenderer && cachedScene && cachedCamera) {\n\t\t\t\t\t\tcachedRenderer.render(cachedScene, cachedCamera);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
6
6
  ],
7
- "mappings": "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",
7
+ "mappings": "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",
8
8
  "debugId": "4D8B1BD0246DFAAD64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { type BasePluginOptions } from 'ecspresso';
12
12
  import type { BaseWorld } from 'ecspresso';
13
+ import type { Spritesheet, SpritesheetData, TextureSource } from 'pixi.js';
13
14
  /** BaseWorld narrowed to sprite-animation components for typed access in helpers. */
14
15
  type SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;
15
16
  export type AnimationLoopMode = 'once' | 'loop' | 'pingPong';
@@ -146,4 +147,113 @@ export declare function resumeAnimation(ecs: SpriteAnimationWorld, entityId: num
146
147
  * - Change detection via markChanged
147
148
  */
148
149
  export declare function createSpriteAnimationPlugin<G extends string = 'spriteAnimation'>(options?: SpriteAnimationPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, SpriteAnimationComponentTypes<string>>, import("ecspresso").EmptyConfig, "sprite-animation-update", G, never, never>;
150
+ /**
151
+ * Per-clip timing/loop overrides keyed by animation name. Each entry tweaks
152
+ * a single clip; omitted entries fall back to the top-level defaults.
153
+ */
154
+ export type SheetClipOverrides<A extends string> = {
155
+ readonly [K in A]?: Omit<SpriteAnimationClipInput, 'frames'>;
156
+ };
157
+ /**
158
+ * Extract the animation-name union from a typed SpritesheetData. Falls back
159
+ * to `string` for untyped sheets.
160
+ */
161
+ export type SheetAnimationKeys<S extends SpritesheetData> = S extends {
162
+ animations: infer A;
163
+ } ? A extends Record<infer K, unknown> ? K extends string ? K : never : string : string;
164
+ /**
165
+ * Build a clip from a named animation in a loaded PixiJS Spritesheet.
166
+ *
167
+ * @example
168
+ * const sheet = await Assets.load<Spritesheet>('/hero.json');
169
+ * const idle = clipFromSheet(sheet, 'idle', { frameDuration: 1 / 12 });
170
+ */
171
+ export declare function clipFromSheet(sheet: Spritesheet, animationName: string, options?: Omit<SpriteAnimationClipInput, 'frames'>): SpriteAnimationClip;
172
+ /**
173
+ * Build an animation set from every named animation in a PixiJS Spritesheet.
174
+ * When the sheet is typed as `Spritesheet<MyData>`, animation names and
175
+ * `defaultClip` / `perClip` keys are inferred at compile time.
176
+ *
177
+ * @example
178
+ * const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>
179
+ * const set = animationSetFromSheet('hero', sheet, {
180
+ * defaultClip: 'idle',
181
+ * frameDuration: 1 / 12,
182
+ * perClip: { attack: { loop: 'once' } },
183
+ * });
184
+ */
185
+ export declare function animationSetFromSheet<S extends SpritesheetData = SpritesheetData>(id: string, sheet: Spritesheet<S>, options?: {
186
+ defaultClip?: SheetAnimationKeys<S>;
187
+ frameDuration?: number;
188
+ loop?: AnimationLoopMode;
189
+ perClip?: SheetClipOverrides<SheetAnimationKeys<S>>;
190
+ }): SpriteAnimationSet<SheetAnimationKeys<S>>;
191
+ /**
192
+ * Slice a grid-arranged sprite sheet into a clip. Use when you don't have a
193
+ * TexturePacker JSON — just an image and uniform cell dimensions. Cells are
194
+ * walked row-major.
195
+ *
196
+ * Specify exactly one of `rows`, `count`, or `indices` to define the cell set
197
+ * (along with `columns`). Combining `count` and `indices` is rejected.
198
+ *
199
+ * Returns a `Promise` because pixi.js is imported lazily — keeps the static
200
+ * module graph free of a runtime pixi dependency for consumers who only use
201
+ * the sheet-based helpers.
202
+ *
203
+ * @example
204
+ * const tex = await Assets.load<Texture>('/coin.png');
205
+ * const clip = await clipFromGrid({
206
+ * source: tex.source,
207
+ * frameWidth: 16, frameHeight: 16,
208
+ * columns: 8, count: 8,
209
+ * frameDuration: 1 / 10,
210
+ * });
211
+ */
212
+ export declare function clipFromGrid(input: {
213
+ source: TextureSource;
214
+ frameWidth: number;
215
+ frameHeight: number;
216
+ columns: number;
217
+ rows?: number;
218
+ /** Explicit row-major, 0-based cell indices. Mutually exclusive with `count`. */
219
+ indices?: readonly number[];
220
+ /** Number of cells to use, walked row-major. Mutually exclusive with `indices`. */
221
+ count?: number;
222
+ /** Pixels between cells. */
223
+ spacing?: number;
224
+ /** Pixels around the sheet edge. */
225
+ margin?: number;
226
+ frameDuration?: number;
227
+ frameDurations?: readonly number[];
228
+ loop?: AnimationLoopMode;
229
+ }): Promise<SpriteAnimationClip>;
230
+ /**
231
+ * Build an asset-manager-compatible loader for a PixiJS spritesheet atlas
232
+ * (TexturePacker JSON, etc.). Returns the fully-parsed `Spritesheet` object
233
+ * with `.animations` and `.textures` populated.
234
+ *
235
+ * The loader performs a runtime shape check on the resolved value — `Assets.load<T>`
236
+ * is purely nominal in PixiJS, so pointing this at a non-atlas URL would
237
+ * otherwise surface as a misleading 'animation not found' error deep in
238
+ * `clipFromSheet`/`animationSetFromSheet`. The shape check turns that into a
239
+ * load-time error with a clear message.
240
+ *
241
+ * To get literal animation-name inference, declare `S` as an
242
+ * `interface ... extends SpritesheetData` (a `type` alias re-widens via
243
+ * `SpritesheetData.animations`'s `Dict<string[]>` string index signature).
244
+ *
245
+ * @example
246
+ * interface HeroData extends SpritesheetData {
247
+ * animations: { idle: string[]; walk: string[]; attack: string[] };
248
+ * }
249
+ *
250
+ * ecs.builder.withAssets(a => a
251
+ * .add('hero', spritesheetLoader<HeroData>('/hero.json'))
252
+ * );
253
+ *
254
+ * // Later:
255
+ * const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>
256
+ * const set = animationSetFromSheet('hero', sheet); // names inferred
257
+ */
258
+ export declare function spritesheetLoader<S extends SpritesheetData = SpritesheetData>(url: string): () => Promise<Spritesheet<S>>;
149
259
  export {};
@@ -1,4 +1,4 @@
1
- var G=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(z,E)=>(typeof require<"u"?require:z)[E]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as Z}from"ecspresso";function Y(j){return Object.freeze({frames:Object.freeze([...j.frames]),frameDuration:j.frameDuration??0.1,frameDurations:j.frameDurations?Object.freeze([...j.frameDurations]):null,loop:j.loop??"loop"})}function U(j,z){return Object.freeze({id:j,clips:Object.freeze({default:Y(z)}),defaultClip:"default"})}function q(j,z,E){let J={},H=Object.keys(z);for(let N of H)J[N]=Y(z[N]);let M=H[0];if(!M)throw Error("defineSpriteAnimations: clips object must have at least one key");return Object.freeze({id:j,clips:Object.freeze(J),defaultClip:E?.defaultClip??M})}function D(j,z){let E=z?.initial??j.defaultClip;return{spriteAnimation:{set:j,current:E,currentFrame:0,elapsed:0,playing:!0,speed:z?.speed??1,direction:1,totalLoops:z?.totalLoops??-1,completedLoops:0,justFinished:!1,onComplete:z?.onComplete}}}function K(j,z,E,J){let H=j.getComponent(z,"spriteAnimation");if(!H)return!1;if(!(E in H.set.clips))return!1;if(E!==H.current||J?.restart===!0)H.current=E,H.currentFrame=0,H.elapsed=0,H.direction=1,H.completedLoops=0,H.justFinished=!1;if(H.playing=!0,J?.speed!==void 0)H.speed=J.speed;return j.markChanged(z,"spriteAnimation"),!0}function S(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!1,!0}function A(j,z){let E=j.getComponent(z,"spriteAnimation");if(!E)return!1;return E.playing=!0,!0}function W(j,z,E){j.playing=!1,j.justFinished=!0,j.onComplete?.({entityId:z,animation:j.current}),E.commands.removeComponent(z,"spriteAnimation")}function _(j,z,E,J){if(j.completedLoops++,z.loop==="once")return W(j,E,J),!1;if(j.totalLoops>0&&j.completedLoops>=j.totalLoops)return W(j,E,J),!1;if(z.loop==="pingPong")return j.direction=j.direction===1?-1:1,j.currentFrame+=j.direction,j.elapsed>0;return j.currentFrame=0,j.elapsed>0}function X(j,z,E,J){let H=j.currentFrame+j.direction;if(H>=z.frames.length||H<0)return _(j,z,E,J);return j.currentFrame=H,!0}function $(j,z,E,J){while(!0){let H=z.frameDurations!==null?z.frameDurations[j.currentFrame]??z.frameDuration:z.frameDuration;if(H<=0){if(!X(j,z,E,J))return;continue}let M=H-j.elapsed;if(M>0.000001)return;if(j.elapsed=M<0?-M:0,!X(j,z,E,J))return}}function b(j){let{systemGroup:z="spriteAnimation",priority:E=0,phase:J="update"}=j??{};return Z("spriteAnimation").withComponentTypes().withLabels().withGroups().install((H)=>{H.addSystem("sprite-animation-update").setPriority(E).inPhase(J).inGroup(z).addQuery("animations",{with:["spriteAnimation"]}).setProcess(({queries:M,dt:N,ecs:V})=>{for(let O of M.animations){let L=O.components.spriteAnimation,Q=L.set.clips[L.current];if(!Q)continue;if(L.justFinished){L.justFinished=!1;continue}if(!L.playing)continue;if(Q.frames.length<=1)continue;let R=L.currentFrame;if(L.elapsed+=N*L.speed,$(L,Q,O.id,V),L.currentFrame!==R||R===0)B(O.components,L,Q);if(L.currentFrame!==R)V.markChanged(O.id,"spriteAnimation")}})})}function B(j,z,E){let J=j.sprite;if(J&&typeof J==="object"&&"texture"in J)J.texture=E.frames[z.currentFrame]}export{S as stopAnimation,A as resumeAnimation,K as playAnimation,q as defineSpriteAnimations,U as defineSpriteAnimation,b as createSpriteAnimationPlugin,D as createSpriteAnimation};
1
+ var S=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(J,L)=>(typeof require<"u"?require:J)[L]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as A}from"ecspresso";function G(z){return Object.freeze({frames:Object.freeze([...z.frames]),frameDuration:z.frameDuration??0.1,frameDurations:z.frameDurations?Object.freeze([...z.frameDurations]):null,loop:z.loop??"loop"})}function v(z,J){return Object.freeze({id:z,clips:Object.freeze({default:G(J)}),defaultClip:"default"})}function f(z,J,L){let O={},M=Object.keys(J);for(let Y of M)O[Y]=G(J[Y]);let Q=M[0];if(!Q)throw Error("defineSpriteAnimations: clips object must have at least one key");return Object.freeze({id:z,clips:Object.freeze(O),defaultClip:L?.defaultClip??Q})}function x(z,J){let L=J?.initial??z.defaultClip;return{spriteAnimation:{set:z,current:L,currentFrame:0,elapsed:0,playing:!0,speed:J?.speed??1,direction:1,totalLoops:J?.totalLoops??-1,completedLoops:0,justFinished:!1,onComplete:J?.onComplete}}}function g(z,J,L,O){let M=z.getComponent(J,"spriteAnimation");if(!M)return!1;if(!(L in M.set.clips))return!1;if(L!==M.current||O?.restart===!0)M.current=L,M.currentFrame=0,M.elapsed=0,M.direction=1,M.completedLoops=0,M.justFinished=!1;if(M.playing=!0,O?.speed!==void 0)M.speed=O.speed;return z.markChanged(J,"spriteAnimation"),!0}function F(z,J){let L=z.getComponent(J,"spriteAnimation");if(!L)return!1;return L.playing=!1,!0}function I(z,J){let L=z.getComponent(J,"spriteAnimation");if(!L)return!1;return L.playing=!0,!0}function j(z,J,L){z.playing=!1,z.justFinished=!0,z.onComplete?.({entityId:J,animation:z.current}),L.commands.removeComponent(J,"spriteAnimation")}function N(z,J,L,O){if(z.completedLoops++,J.loop==="once")return j(z,L,O),!1;if(z.totalLoops>0&&z.completedLoops>=z.totalLoops)return j(z,L,O),!1;if(J.loop==="pingPong")return z.direction=z.direction===1?-1:1,z.currentFrame+=z.direction,z.elapsed>0;return z.currentFrame=0,z.elapsed>0}function q(z,J,L,O){let M=z.currentFrame+z.direction;if(M>=J.frames.length||M<0)return N(z,J,L,O);return z.currentFrame=M,!0}function T(z,J,L,O){while(!0){let M=J.frameDurations!==null?J.frameDurations[z.currentFrame]??J.frameDuration:J.frameDuration;if(M<=0){if(!q(z,J,L,O))return;continue}let Q=M-z.elapsed;if(Q>0.000001)return;if(z.elapsed=Q<0?-Q:0,!q(z,J,L,O))return}}function h(z){let{systemGroup:J="spriteAnimation",priority:L=0,phase:O="update"}=z??{};return A("spriteAnimation").withComponentTypes().withLabels().withGroups().install((M)=>{M.addSystem("sprite-animation-update").setPriority(L).inPhase(O).inGroup(J).addQuery("animations",{with:["spriteAnimation"]}).setProcess(({queries:Q,dt:Y,ecs:V})=>{for(let X of Q.animations){let U=X.components.spriteAnimation,Z=U.set.clips[U.current];if(!Z)continue;if(U.justFinished){U.justFinished=!1;continue}if(!U.playing)continue;if(Z.frames.length<=1)continue;let $=U.currentFrame;if(U.elapsed+=Y*U.speed,T(U,Z,X.id,V),U.currentFrame!==$||$===0)b(X.components,U,Z);if(U.currentFrame!==$)V.markChanged(X.id,"spriteAnimation")}})})}function b(z,J,L){let O=z.sprite;if(O&&typeof O==="object"&&"texture"in O)O.texture=L.frames[J.currentFrame]}function y(z,J,L){let O=z.animations[J];if(!O||O.length===0){let M=Object.keys(z.animations).join(", ")||"(none)";throw Error(`clipFromSheet: animation "${J}" not found on sheet (or has no frames). Available: ${M}`)}return G({...L,frames:O})}function u(z,J,L){let O=Object.entries(J.animations),M=O[0];if(!M)throw Error(`animationSetFromSheet: sheet "${z}" has no animations defined`);let Q=O.reduce((Y,[V,X])=>{if(!Array.isArray(X)||X.length===0)throw Error(`animationSetFromSheet: animation "${String(V)}" on sheet "${z}" has no frames (got ${X===null?"null":Array.isArray(X)?"empty array":typeof X})`);let U=L?.perClip?.[V];return Y[V]=G({frames:X,frameDuration:U?.frameDuration??L?.frameDuration,frameDurations:U?.frameDurations,loop:U?.loop??L?.loop}),Y},{});return Object.freeze({id:z,clips:Object.freeze(Q),defaultClip:L?.defaultClip??M[0]})}async function d(z){let{source:J,frameWidth:L,frameHeight:O,columns:M,rows:Q,indices:Y,count:V,spacing:X=0,margin:U=0}=z;if(!J)throw Error("clipFromGrid: source is required");if(!Number.isInteger(M)||M<=0)throw Error(`clipFromGrid: columns must be a positive integer, got ${M}`);if(Q!==void 0&&(!Number.isInteger(Q)||Q<=0))throw Error(`clipFromGrid: rows must be a positive integer, got ${Q}`);if(V!==void 0&&(!Number.isInteger(V)||V<0))throw Error(`clipFromGrid: count must be a non-negative integer, got ${V}`);if(Y!==void 0&&V!==void 0)throw Error("clipFromGrid: pass either 'indices' or 'count', not both");if(Y===void 0&&V===void 0&&Q===void 0)throw Error("clipFromGrid: specify 'rows', 'count', or 'indices' to define the cell set (only 'columns' is ambiguous)");let Z=Q!==void 0?M*Q:void 0,$=Y??Array.from({length:Z!==void 0&&V!==void 0?Math.min(V,Z):V??Z??0},(_,P)=>P);if($.length===0)throw Error("clipFromGrid: resolved to zero cells (empty indices array or count: 0)");let B=Z??1/0,R=$.find((_)=>!Number.isInteger(_)||_<0||_>=B);if(R!==void 0){let _=Z!==void 0?`[0, ${Z})`:"[0, ∞) — pass 'rows' to enable upper-bound checking";throw Error(`clipFromGrid: invalid cell index ${R}; expected integer in ${_}`)}let{Texture:D,Rectangle:E}=await import("pixi.js"),H=$.map((_)=>{let P=_%M,K=Math.floor(_/M),W=U+P*(L+X),k=U+K*(O+X);return new D({source:J,frame:new E(W,k,L,O)})});return G({frames:H,frameDuration:z.frameDuration,frameDurations:z.frameDurations,loop:z.loop})}function l(z){return async()=>{let{Assets:J}=await import("pixi.js"),L=await J.load(z);if(!L||typeof L!=="object"||typeof L.animations!=="object"||L.animations===null||typeof L.textures!=="object"||L.textures===null)throw Error(`spritesheetLoader: resource at "${z}" did not resolve to a Spritesheet (expected non-null 'animations' and 'textures' objects). Check that the URL points to a TexturePacker-style JSON atlas, not a raw image.`);return L}}export{F as stopAnimation,l as spritesheetLoader,I as resumeAnimation,g as playAnimation,f as defineSpriteAnimations,v as defineSpriteAnimation,h as createSpriteAnimationPlugin,x as createSpriteAnimation,y as clipFromSheet,d as clipFromGrid,u as animationSetFromSheet};
2
2
 
3
- //# debugId=9EF329142BBA6E6864756E2164756E21
3
+ //# debugId=288D71B236D04EC264756E2164756E21
4
4
  //# sourceMappingURL=sprite-animation.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/rendering/sprite-animation.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Sprite Animation Plugin for ECSpresso\n *\n * ECS-native frame-based sprite animation. Advances through spritesheet frames,\n * handles loop modes (once, loop, pingPong), publishes completion events, and\n * syncs the current frame's texture to the PixiJS Sprite via structural access.\n *\n * Renderer2D is a required dependency — the `sprite` component comes from that plugin.\n * This plugin declares only `spriteAnimation` as its component type.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\n\n/** BaseWorld narrowed to sprite-animation components for typed access in helpers. */\ntype SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;\n\n// ==================== Loop Mode ====================\n\nexport type AnimationLoopMode = 'once' | 'loop' | 'pingPong';\n\n// ==================== Clip Types ====================\n\n/**\n * A single animation clip: an ordered sequence of texture frames with timing.\n * Immutable and shared across entities.\n */\nexport interface SpriteAnimationClip {\n\treadonly frames: readonly unknown[];\n\treadonly frameDuration: number;\n\treadonly frameDurations: readonly number[] | null;\n\treadonly loop: AnimationLoopMode;\n}\n\n/**\n * Input format for defining a clip. Accepts either uniform or per-frame timing.\n */\nexport interface SpriteAnimationClipInput {\n\t/** Array of PixiJS Texture objects */\n\tframes: readonly unknown[];\n\t/** Uniform seconds-per-frame (used when frameDurations is not provided) */\n\tframeDuration?: number;\n\t/** Per-frame durations in seconds (overrides frameDuration) */\n\tframeDurations?: readonly number[];\n\t/** Loop mode (default: 'loop') */\n\tloop?: AnimationLoopMode;\n}\n\n// ==================== Animation Set ====================\n\n/**\n * A named collection of animation clips. Immutable and shared across entities.\n * Parameterized by A (animation name union) for compile-time validation.\n */\nexport interface SpriteAnimationSet<A extends string = string> {\n\treadonly id: string;\n\treadonly clips: { readonly [K in A]: SpriteAnimationClip };\n\treadonly defaultClip: A;\n}\n\n// ==================== Component ====================\n\n/**\n * Per-entity runtime animation state.\n */\nexport interface SpriteAnimation<A extends string = string> {\n\treadonly set: SpriteAnimationSet<A>;\n\tcurrent: A;\n\tcurrentFrame: number;\n\telapsed: number;\n\tplaying: boolean;\n\tspeed: number;\n\tdirection: 1 | -1;\n\ttotalLoops: number;\n\tcompletedLoops: number;\n\tjustFinished: boolean;\n\tonComplete?: (data: SpriteAnimationEventData) => void;\n}\n\n/**\n * Component types provided by the sprite animation plugin.\n */\nexport interface SpriteAnimationComponentTypes<A extends string = string> {\n\tspriteAnimation: SpriteAnimation<A>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an animation completes.\n */\nexport interface SpriteAnimationEventData {\n\tentityId: number;\n\tanimation: string;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface SpriteAnimationPluginOptions<G extends string = 'spriteAnimation'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nfunction buildClip(input: SpriteAnimationClipInput): SpriteAnimationClip {\n\treturn Object.freeze({\n\t\tframes: Object.freeze([...input.frames]),\n\t\tframeDuration: input.frameDuration ?? (1 / 10),\n\t\tframeDurations: input.frameDurations\n\t\t\t? Object.freeze([...input.frameDurations])\n\t\t\t: null,\n\t\tloop: input.loop ?? 'loop',\n\t});\n}\n\n/**\n * Define a single-clip animation set named 'default'.\n * For simple use cases like spinning coins, pulsing effects, etc.\n *\n * @param id Unique identifier for this animation set\n * @param clip Clip definition\n * @returns A frozen SpriteAnimationSet with one clip named 'default'\n */\nexport function defineSpriteAnimation(\n\tid: string,\n\tclip: SpriteAnimationClipInput,\n): SpriteAnimationSet<'default'> {\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze({ default: buildClip(clip) }),\n\t\tdefaultClip: 'default' as const,\n\t});\n}\n\n/**\n * Define a multi-clip animation set with named animations.\n * Animation names are inferred from the keys of the clips object.\n *\n * @param id Unique identifier for this animation set\n * @param clips Object mapping animation names to clip definitions\n * @param options Optional configuration (defaultClip)\n * @returns A frozen SpriteAnimationSet with inferred animation name union\n */\nexport function defineSpriteAnimations<A extends string>(\n\tid: string,\n\tclips: Record<A, SpriteAnimationClipInput>,\n\toptions?: { defaultClip?: NoInfer<A> },\n): SpriteAnimationSet<A> {\n\tconst builtClips = {} as Record<A, SpriteAnimationClip>;\n\tconst keys = Object.keys(clips) as A[];\n\n\tfor (const key of keys) {\n\t\tbuiltClips[key] = buildClip(clips[key]);\n\t}\n\n\tconst firstKey = keys[0];\n\tif (!firstKey) {\n\t\tthrow new Error(`defineSpriteAnimations: clips object must have at least one key`);\n\t}\n\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze(builtClips),\n\t\tdefaultClip: options?.defaultClip ?? firstKey,\n\t});\n}\n\n/**\n * Create a spriteAnimation component from an animation set.\n *\n * @param set The animation set to use\n * @param options Optional configuration (initial clip, speed, onComplete event)\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createSpriteAnimation<A extends string>(\n\tset: SpriteAnimationSet<A>,\n\toptions?: {\n\t\tinitial?: A;\n\t\tspeed?: number;\n\t\ttotalLoops?: number;\n\t\tonComplete?: (data: SpriteAnimationEventData) => void;\n\t},\n): Pick<SpriteAnimationComponentTypes<A>, 'spriteAnimation'> {\n\tconst initial = options?.initial ?? set.defaultClip;\n\treturn {\n\t\tspriteAnimation: {\n\t\t\tset,\n\t\t\tcurrent: initial,\n\t\t\tcurrentFrame: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: true,\n\t\t\tspeed: options?.speed ?? 1,\n\t\t\tdirection: 1,\n\t\t\ttotalLoops: options?.totalLoops ?? -1,\n\t\t\tcompletedLoops: 0,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Switch an entity's current animation at runtime.\n * Resets state if switching to a different animation (or restart=true).\n *\n * @returns false if entity has no spriteAnimation or animation name doesn't exist\n */\nexport function playAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n\tanimation: string,\n\toptions?: { restart?: boolean; speed?: number },\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\tif (!(animation in anim.set.clips)) return false;\n\n\tconst shouldReset = animation !== anim.current || options?.restart === true;\n\n\tif (shouldReset) {\n\t\tanim.current = animation;\n\t\tanim.currentFrame = 0;\n\t\tanim.elapsed = 0;\n\t\tanim.direction = 1;\n\t\tanim.completedLoops = 0;\n\t\tanim.justFinished = false;\n\t}\n\n\tanim.playing = true;\n\n\tif (options?.speed !== undefined) {\n\t\tanim.speed = options.speed;\n\t}\n\n\tecs.markChanged(entityId, 'spriteAnimation');\n\treturn true;\n}\n\n/**\n * Pause an entity's animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function stopAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a paused animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function resumeAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = true;\n\treturn true;\n}\n\n// ==================== Animation Processing Helpers ====================\n\nfunction completeAnimation(\n\tanim: SpriteAnimation,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\tanim.playing = false;\n\tanim.justFinished = true;\n\n\tanim.onComplete?.({ entityId, animation: anim.current });\n\n\tecs.commands.removeComponent(entityId, 'spriteAnimation');\n}\n\nfunction handleBoundary(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tanim.completedLoops++;\n\n\tif (clip.loop === 'once') {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Check finite loop count\n\tif (anim.totalLoops > 0 && anim.completedLoops >= anim.totalLoops) {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\tif (clip.loop === 'pingPong') {\n\t\tanim.direction = anim.direction === 1 ? -1 : 1;\n\t\t// Step one frame in the new direction from the boundary\n\t\tanim.currentFrame += anim.direction;\n\t\treturn anim.elapsed > 0;\n\t}\n\n\t// loop mode: wrap to frame 0\n\tanim.currentFrame = 0;\n\treturn anim.elapsed > 0;\n}\n\n/**\n * Advance to next frame. Returns true if processing should continue (more overflow),\n * false if animation completed or reached a boundary.\n */\nfunction advanceFrame(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tconst nextFrame = anim.currentFrame + anim.direction;\n\n\t// Check boundary\n\tif (nextFrame >= clip.frames.length || nextFrame < 0) {\n\t\treturn handleBoundary(anim, clip, entityId, ecs);\n\t}\n\n\tanim.currentFrame = nextFrame;\n\treturn true;\n}\n\nfunction processFrameAdvancement(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\t// Process frame overflow\n\t// eslint-disable-next-line no-constant-condition\n\twhile (true) {\n\t\tconst frameDuration = clip.frameDurations !== null\n\t\t\t? (clip.frameDurations[anim.currentFrame] ?? clip.frameDuration)\n\t\t\t: clip.frameDuration;\n\n\t\tif (frameDuration <= 0) {\n\t\t\t// Zero-duration frame: advance immediately\n\t\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Floating-point-safe comparison: treat elapsed within 1μs of\n\t\t// frameDuration as having reached the boundary.\n\t\tconst remaining = frameDuration - anim.elapsed;\n\t\tif (remaining > 1e-6) return;\n\n\t\t// Frame complete — carry overflow (clamp negative remainders to 0)\n\t\tanim.elapsed = remaining < 0 ? -remaining : 0;\n\n\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t}\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a sprite animation plugin for ECSpresso.\n *\n * Provides:\n * - Frame-based animation system processing spriteAnimation components\n * - Loop modes: once, loop, pingPong\n * - justFinished one-frame flag for completion detection\n * - onComplete event publishing\n * - Sprite texture sync via structural cross-plugin access\n * - Change detection via markChanged\n */\nexport function createSpriteAnimationPlugin<\n\tG extends string = 'spriteAnimation',\n>(\n\toptions?: SpriteAnimationPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'spriteAnimation',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin('spriteAnimation')\n\t\t.withComponentTypes<SpriteAnimationComponentTypes>()\n\t\t.withLabels<'sprite-animation-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('sprite-animation-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('animations', {\n\t\t\t\t\twith: ['spriteAnimation'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.animations) {\n\t\t\t\t\t\tconst anim = entity.components.spriteAnimation as SpriteAnimation;\n\t\t\t\t\t\tconst clip = anim.set.clips[anim.current];\n\t\t\t\t\t\tif (!clip) continue;\n\n\t\t\t\t\t\t// Clear justFinished from previous frame\n\t\t\t\t\t\tif (anim.justFinished) {\n\t\t\t\t\t\t\tanim.justFinished = false;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Skip paused animations\n\t\t\t\t\t\tif (!anim.playing) continue;\n\n\t\t\t\t\t\t// Skip single-frame clips\n\t\t\t\t\t\tif (clip.frames.length <= 1) continue;\n\n\t\t\t\t\t\tconst previousFrame = anim.currentFrame;\n\t\t\t\t\t\tanim.elapsed += dt * anim.speed;\n\n\t\t\t\t\t\tprocessFrameAdvancement(anim, clip, entity.id, ecs);\n\n\t\t\t\t\t\t// Sync sprite texture if frame changed\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame || previousFrame === 0) {\n\t\t\t\t\t\t\tsyncSpriteTexture(entity.components as Record<string, unknown>, anim, clip);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame) {\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'spriteAnimation');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Internal: Sprite Texture Sync ====================\n\n/**\n * Sync the sprite's texture to the current frame. Uses structural access\n * following the tween plugin's cross-component pattern.\n */\nfunction syncSpriteTexture(\n\tentityComponents: Record<string, unknown>,\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n): void {\n\tconst sprite = entityComponents['sprite'];\n\tif (sprite && typeof sprite === 'object' && 'texture' in sprite) {\n\t\t(sprite as { texture: unknown }).texture = clip.frames[anim.currentFrame];\n\t}\n}\n"
5
+ "/**\n * Sprite Animation Plugin for ECSpresso\n *\n * ECS-native frame-based sprite animation. Advances through spritesheet frames,\n * handles loop modes (once, loop, pingPong), publishes completion events, and\n * syncs the current frame's texture to the PixiJS Sprite via structural access.\n *\n * Renderer2D is a required dependency — the `sprite` component comes from that plugin.\n * This plugin declares only `spriteAnimation` as its component type.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { BaseWorld } from 'ecspresso';\nimport type { Spritesheet, SpritesheetData, Texture, TextureSource } from 'pixi.js';\n\n/** BaseWorld narrowed to sprite-animation components for typed access in helpers. */\ntype SpriteAnimationWorld = BaseWorld<SpriteAnimationComponentTypes>;\n\n// ==================== Loop Mode ====================\n\nexport type AnimationLoopMode = 'once' | 'loop' | 'pingPong';\n\n// ==================== Clip Types ====================\n\n/**\n * A single animation clip: an ordered sequence of texture frames with timing.\n * Immutable and shared across entities.\n */\nexport interface SpriteAnimationClip {\n\treadonly frames: readonly unknown[];\n\treadonly frameDuration: number;\n\treadonly frameDurations: readonly number[] | null;\n\treadonly loop: AnimationLoopMode;\n}\n\n/**\n * Input format for defining a clip. Accepts either uniform or per-frame timing.\n */\nexport interface SpriteAnimationClipInput {\n\t/** Array of PixiJS Texture objects */\n\tframes: readonly unknown[];\n\t/** Uniform seconds-per-frame (used when frameDurations is not provided) */\n\tframeDuration?: number;\n\t/** Per-frame durations in seconds (overrides frameDuration) */\n\tframeDurations?: readonly number[];\n\t/** Loop mode (default: 'loop') */\n\tloop?: AnimationLoopMode;\n}\n\n// ==================== Animation Set ====================\n\n/**\n * A named collection of animation clips. Immutable and shared across entities.\n * Parameterized by A (animation name union) for compile-time validation.\n */\nexport interface SpriteAnimationSet<A extends string = string> {\n\treadonly id: string;\n\treadonly clips: { readonly [K in A]: SpriteAnimationClip };\n\treadonly defaultClip: A;\n}\n\n// ==================== Component ====================\n\n/**\n * Per-entity runtime animation state.\n */\nexport interface SpriteAnimation<A extends string = string> {\n\treadonly set: SpriteAnimationSet<A>;\n\tcurrent: A;\n\tcurrentFrame: number;\n\telapsed: number;\n\tplaying: boolean;\n\tspeed: number;\n\tdirection: 1 | -1;\n\ttotalLoops: number;\n\tcompletedLoops: number;\n\tjustFinished: boolean;\n\tonComplete?: (data: SpriteAnimationEventData) => void;\n}\n\n/**\n * Component types provided by the sprite animation plugin.\n */\nexport interface SpriteAnimationComponentTypes<A extends string = string> {\n\tspriteAnimation: SpriteAnimation<A>;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Data published when an animation completes.\n */\nexport interface SpriteAnimationEventData {\n\tentityId: number;\n\tanimation: string;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface SpriteAnimationPluginOptions<G extends string = 'spriteAnimation'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nfunction buildClip(input: SpriteAnimationClipInput): SpriteAnimationClip {\n\treturn Object.freeze({\n\t\tframes: Object.freeze([...input.frames]),\n\t\tframeDuration: input.frameDuration ?? (1 / 10),\n\t\tframeDurations: input.frameDurations\n\t\t\t? Object.freeze([...input.frameDurations])\n\t\t\t: null,\n\t\tloop: input.loop ?? 'loop',\n\t});\n}\n\n/**\n * Define a single-clip animation set named 'default'.\n * For simple use cases like spinning coins, pulsing effects, etc.\n *\n * @param id Unique identifier for this animation set\n * @param clip Clip definition\n * @returns A frozen SpriteAnimationSet with one clip named 'default'\n */\nexport function defineSpriteAnimation(\n\tid: string,\n\tclip: SpriteAnimationClipInput,\n): SpriteAnimationSet<'default'> {\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze({ default: buildClip(clip) }),\n\t\tdefaultClip: 'default' as const,\n\t});\n}\n\n/**\n * Define a multi-clip animation set with named animations.\n * Animation names are inferred from the keys of the clips object.\n *\n * @param id Unique identifier for this animation set\n * @param clips Object mapping animation names to clip definitions\n * @param options Optional configuration (defaultClip)\n * @returns A frozen SpriteAnimationSet with inferred animation name union\n */\nexport function defineSpriteAnimations<A extends string>(\n\tid: string,\n\tclips: Record<A, SpriteAnimationClipInput>,\n\toptions?: { defaultClip?: NoInfer<A> },\n): SpriteAnimationSet<A> {\n\tconst builtClips = {} as Record<A, SpriteAnimationClip>;\n\tconst keys = Object.keys(clips) as A[];\n\n\tfor (const key of keys) {\n\t\tbuiltClips[key] = buildClip(clips[key]);\n\t}\n\n\tconst firstKey = keys[0];\n\tif (!firstKey) {\n\t\tthrow new Error(`defineSpriteAnimations: clips object must have at least one key`);\n\t}\n\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze(builtClips),\n\t\tdefaultClip: options?.defaultClip ?? firstKey,\n\t});\n}\n\n/**\n * Create a spriteAnimation component from an animation set.\n *\n * @param set The animation set to use\n * @param options Optional configuration (initial clip, speed, onComplete event)\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createSpriteAnimation<A extends string>(\n\tset: SpriteAnimationSet<A>,\n\toptions?: {\n\t\tinitial?: A;\n\t\tspeed?: number;\n\t\ttotalLoops?: number;\n\t\tonComplete?: (data: SpriteAnimationEventData) => void;\n\t},\n): Pick<SpriteAnimationComponentTypes<A>, 'spriteAnimation'> {\n\tconst initial = options?.initial ?? set.defaultClip;\n\treturn {\n\t\tspriteAnimation: {\n\t\t\tset,\n\t\t\tcurrent: initial,\n\t\t\tcurrentFrame: 0,\n\t\t\telapsed: 0,\n\t\t\tplaying: true,\n\t\t\tspeed: options?.speed ?? 1,\n\t\t\tdirection: 1,\n\t\t\ttotalLoops: options?.totalLoops ?? -1,\n\t\t\tcompletedLoops: 0,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Switch an entity's current animation at runtime.\n * Resets state if switching to a different animation (or restart=true).\n *\n * @returns false if entity has no spriteAnimation or animation name doesn't exist\n */\nexport function playAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n\tanimation: string,\n\toptions?: { restart?: boolean; speed?: number },\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\tif (!(animation in anim.set.clips)) return false;\n\n\tconst shouldReset = animation !== anim.current || options?.restart === true;\n\n\tif (shouldReset) {\n\t\tanim.current = animation;\n\t\tanim.currentFrame = 0;\n\t\tanim.elapsed = 0;\n\t\tanim.direction = 1;\n\t\tanim.completedLoops = 0;\n\t\tanim.justFinished = false;\n\t}\n\n\tanim.playing = true;\n\n\tif (options?.speed !== undefined) {\n\t\tanim.speed = options.speed;\n\t}\n\n\tecs.markChanged(entityId, 'spriteAnimation');\n\treturn true;\n}\n\n/**\n * Pause an entity's animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function stopAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = false;\n\treturn true;\n}\n\n/**\n * Resume a paused animation.\n *\n * @returns false if entity has no spriteAnimation\n */\nexport function resumeAnimation(\n\tecs: SpriteAnimationWorld,\n\tentityId: number,\n): boolean {\n\tconst anim = ecs.getComponent(entityId, 'spriteAnimation');\n\tif (!anim) return false;\n\n\tanim.playing = true;\n\treturn true;\n}\n\n// ==================== Animation Processing Helpers ====================\n\nfunction completeAnimation(\n\tanim: SpriteAnimation,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\tanim.playing = false;\n\tanim.justFinished = true;\n\n\tanim.onComplete?.({ entityId, animation: anim.current });\n\n\tecs.commands.removeComponent(entityId, 'spriteAnimation');\n}\n\nfunction handleBoundary(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tanim.completedLoops++;\n\n\tif (clip.loop === 'once') {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Check finite loop count\n\tif (anim.totalLoops > 0 && anim.completedLoops >= anim.totalLoops) {\n\t\tcompleteAnimation(anim, entityId, ecs);\n\t\treturn false;\n\t}\n\n\tif (clip.loop === 'pingPong') {\n\t\tanim.direction = anim.direction === 1 ? -1 : 1;\n\t\t// Step one frame in the new direction from the boundary\n\t\tanim.currentFrame += anim.direction;\n\t\treturn anim.elapsed > 0;\n\t}\n\n\t// loop mode: wrap to frame 0\n\tanim.currentFrame = 0;\n\treturn anim.elapsed > 0;\n}\n\n/**\n * Advance to next frame. Returns true if processing should continue (more overflow),\n * false if animation completed or reached a boundary.\n */\nfunction advanceFrame(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): boolean {\n\tconst nextFrame = anim.currentFrame + anim.direction;\n\n\t// Check boundary\n\tif (nextFrame >= clip.frames.length || nextFrame < 0) {\n\t\treturn handleBoundary(anim, clip, entityId, ecs);\n\t}\n\n\tanim.currentFrame = nextFrame;\n\treturn true;\n}\n\nfunction processFrameAdvancement(\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n\tentityId: number,\n\tecs: SpriteAnimationWorld,\n): void {\n\t// Process frame overflow\n\t// eslint-disable-next-line no-constant-condition\n\twhile (true) {\n\t\tconst frameDuration = clip.frameDurations !== null\n\t\t\t? (clip.frameDurations[anim.currentFrame] ?? clip.frameDuration)\n\t\t\t: clip.frameDuration;\n\n\t\tif (frameDuration <= 0) {\n\t\t\t// Zero-duration frame: advance immediately\n\t\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Floating-point-safe comparison: treat elapsed within 1μs of\n\t\t// frameDuration as having reached the boundary.\n\t\tconst remaining = frameDuration - anim.elapsed;\n\t\tif (remaining > 1e-6) return;\n\n\t\t// Frame complete — carry overflow (clamp negative remainders to 0)\n\t\tanim.elapsed = remaining < 0 ? -remaining : 0;\n\n\t\tif (!advanceFrame(anim, clip, entityId, ecs)) return;\n\t}\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a sprite animation plugin for ECSpresso.\n *\n * Provides:\n * - Frame-based animation system processing spriteAnimation components\n * - Loop modes: once, loop, pingPong\n * - justFinished one-frame flag for completion detection\n * - onComplete event publishing\n * - Sprite texture sync via structural cross-plugin access\n * - Change detection via markChanged\n */\nexport function createSpriteAnimationPlugin<\n\tG extends string = 'spriteAnimation',\n>(\n\toptions?: SpriteAnimationPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'spriteAnimation',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin('spriteAnimation')\n\t\t.withComponentTypes<SpriteAnimationComponentTypes>()\n\t\t.withLabels<'sprite-animation-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('sprite-animation-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('animations', {\n\t\t\t\t\twith: ['spriteAnimation'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.animations) {\n\t\t\t\t\t\tconst anim = entity.components.spriteAnimation as SpriteAnimation;\n\t\t\t\t\t\tconst clip = anim.set.clips[anim.current];\n\t\t\t\t\t\tif (!clip) continue;\n\n\t\t\t\t\t\t// Clear justFinished from previous frame\n\t\t\t\t\t\tif (anim.justFinished) {\n\t\t\t\t\t\t\tanim.justFinished = false;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Skip paused animations\n\t\t\t\t\t\tif (!anim.playing) continue;\n\n\t\t\t\t\t\t// Skip single-frame clips\n\t\t\t\t\t\tif (clip.frames.length <= 1) continue;\n\n\t\t\t\t\t\tconst previousFrame = anim.currentFrame;\n\t\t\t\t\t\tanim.elapsed += dt * anim.speed;\n\n\t\t\t\t\t\tprocessFrameAdvancement(anim, clip, entity.id, ecs);\n\n\t\t\t\t\t\t// Sync sprite texture if frame changed\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame || previousFrame === 0) {\n\t\t\t\t\t\t\tsyncSpriteTexture(entity.components as Record<string, unknown>, anim, clip);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (anim.currentFrame !== previousFrame) {\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'spriteAnimation');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Internal: Sprite Texture Sync ====================\n\n/**\n * Sync the sprite's texture to the current frame. Uses structural access\n * following the tween plugin's cross-component pattern.\n */\nfunction syncSpriteTexture(\n\tentityComponents: Record<string, unknown>,\n\tanim: SpriteAnimation,\n\tclip: SpriteAnimationClip,\n): void {\n\tconst sprite = entityComponents['sprite'];\n\tif (sprite && typeof sprite === 'object' && 'texture' in sprite) {\n\t\t(sprite as { texture: unknown }).texture = clip.frames[anim.currentFrame];\n\t}\n}\n\n// ==================== Spritesheet Helpers ====================\n\n/**\n * Per-clip timing/loop overrides keyed by animation name. Each entry tweaks\n * a single clip; omitted entries fall back to the top-level defaults.\n */\nexport type SheetClipOverrides<A extends string> = {\n\treadonly [K in A]?: Omit<SpriteAnimationClipInput, 'frames'>;\n};\n\n/**\n * Extract the animation-name union from a typed SpritesheetData. Falls back\n * to `string` for untyped sheets.\n */\nexport type SheetAnimationKeys<S extends SpritesheetData> =\n\tS extends { animations: infer A }\n\t\t? A extends Record<infer K, unknown>\n\t\t\t? K extends string ? K : never\n\t\t\t: string\n\t\t: string;\n\n/**\n * Build a clip from a named animation in a loaded PixiJS Spritesheet.\n *\n * @example\n * const sheet = await Assets.load<Spritesheet>('/hero.json');\n * const idle = clipFromSheet(sheet, 'idle', { frameDuration: 1 / 12 });\n */\nexport function clipFromSheet(\n\tsheet: Spritesheet,\n\tanimationName: string,\n\toptions?: Omit<SpriteAnimationClipInput, 'frames'>,\n): SpriteAnimationClip {\n\tconst frames = sheet.animations[animationName];\n\tif (!frames || frames.length === 0) {\n\t\tconst available = Object.keys(sheet.animations).join(', ') || '(none)';\n\t\tthrow new Error(\n\t\t\t`clipFromSheet: animation \"${animationName}\" not found on sheet (or has no frames). Available: ${available}`,\n\t\t);\n\t}\n\treturn buildClip({ ...options, frames });\n}\n\n/**\n * Build an animation set from every named animation in a PixiJS Spritesheet.\n * When the sheet is typed as `Spritesheet<MyData>`, animation names and\n * `defaultClip` / `perClip` keys are inferred at compile time.\n *\n * @example\n * const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>\n * const set = animationSetFromSheet('hero', sheet, {\n * defaultClip: 'idle',\n * frameDuration: 1 / 12,\n * perClip: { attack: { loop: 'once' } },\n * });\n */\nexport function animationSetFromSheet<S extends SpritesheetData = SpritesheetData>(\n\tid: string,\n\tsheet: Spritesheet<S>,\n\toptions?: {\n\t\tdefaultClip?: SheetAnimationKeys<S>;\n\t\tframeDuration?: number;\n\t\tloop?: AnimationLoopMode;\n\t\tperClip?: SheetClipOverrides<SheetAnimationKeys<S>>;\n\t},\n): SpriteAnimationSet<SheetAnimationKeys<S>> {\n\ttype A = SheetAnimationKeys<S>;\n\tconst entries = Object.entries(sheet.animations) as unknown as [A, readonly unknown[]][];\n\tconst firstEntry = entries[0];\n\tif (!firstEntry) {\n\t\tthrow new Error(`animationSetFromSheet: sheet \"${id}\" has no animations defined`);\n\t}\n\n\tconst clips = entries.reduce((acc, [name, sheetFrames]) => {\n\t\tif (!Array.isArray(sheetFrames) || sheetFrames.length === 0) {\n\t\t\tthrow new Error(`animationSetFromSheet: animation \"${String(name)}\" on sheet \"${id}\" has no frames (got ${sheetFrames === null ? 'null' : Array.isArray(sheetFrames) ? 'empty array' : typeof sheetFrames})`);\n\t\t}\n\t\tconst override = options?.perClip?.[name];\n\t\tacc[name] = buildClip({\n\t\t\tframes: sheetFrames,\n\t\t\tframeDuration: override?.frameDuration ?? options?.frameDuration,\n\t\t\tframeDurations: override?.frameDurations,\n\t\t\tloop: override?.loop ?? options?.loop,\n\t\t});\n\t\treturn acc;\n\t}, {} as Record<A, SpriteAnimationClip>);\n\n\treturn Object.freeze({\n\t\tid,\n\t\tclips: Object.freeze(clips),\n\t\tdefaultClip: options?.defaultClip ?? firstEntry[0],\n\t});\n}\n\n/**\n * Slice a grid-arranged sprite sheet into a clip. Use when you don't have a\n * TexturePacker JSON — just an image and uniform cell dimensions. Cells are\n * walked row-major.\n *\n * Specify exactly one of `rows`, `count`, or `indices` to define the cell set\n * (along with `columns`). Combining `count` and `indices` is rejected.\n *\n * Returns a `Promise` because pixi.js is imported lazily — keeps the static\n * module graph free of a runtime pixi dependency for consumers who only use\n * the sheet-based helpers.\n *\n * @example\n * const tex = await Assets.load<Texture>('/coin.png');\n * const clip = await clipFromGrid({\n * source: tex.source,\n * frameWidth: 16, frameHeight: 16,\n * columns: 8, count: 8,\n * frameDuration: 1 / 10,\n * });\n */\nexport async function clipFromGrid(input: {\n\tsource: TextureSource;\n\tframeWidth: number;\n\tframeHeight: number;\n\tcolumns: number;\n\trows?: number;\n\t/** Explicit row-major, 0-based cell indices. Mutually exclusive with `count`. */\n\tindices?: readonly number[];\n\t/** Number of cells to use, walked row-major. Mutually exclusive with `indices`. */\n\tcount?: number;\n\t/** Pixels between cells. */\n\tspacing?: number;\n\t/** Pixels around the sheet edge. */\n\tmargin?: number;\n\tframeDuration?: number;\n\tframeDurations?: readonly number[];\n\tloop?: AnimationLoopMode;\n}): Promise<SpriteAnimationClip> {\n\tconst { source, frameWidth, frameHeight, columns, rows, indices, count, spacing = 0, margin = 0 } = input;\n\n\tif (!source) {\n\t\tthrow new Error(`clipFromGrid: source is required`);\n\t}\n\tif (!Number.isInteger(columns) || columns <= 0) {\n\t\tthrow new Error(`clipFromGrid: columns must be a positive integer, got ${columns}`);\n\t}\n\tif (rows !== undefined && (!Number.isInteger(rows) || rows <= 0)) {\n\t\tthrow new Error(`clipFromGrid: rows must be a positive integer, got ${rows}`);\n\t}\n\tif (count !== undefined && (!Number.isInteger(count) || count < 0)) {\n\t\tthrow new Error(`clipFromGrid: count must be a non-negative integer, got ${count}`);\n\t}\n\tif (indices !== undefined && count !== undefined) {\n\t\tthrow new Error(`clipFromGrid: pass either 'indices' or 'count', not both`);\n\t}\n\tif (indices === undefined && count === undefined && rows === undefined) {\n\t\tthrow new Error(`clipFromGrid: specify 'rows', 'count', or 'indices' to define the cell set (only 'columns' is ambiguous)`);\n\t}\n\n\tconst gridTotal = rows !== undefined ? columns * rows : undefined;\n\tconst chosen: readonly number[] = indices ?? Array.from(\n\t\t{ length: gridTotal !== undefined && count !== undefined ? Math.min(count, gridTotal) : (count ?? gridTotal ?? 0) },\n\t\t(_, i) => i,\n\t);\n\n\tif (chosen.length === 0) {\n\t\tthrow new Error(`clipFromGrid: resolved to zero cells (empty indices array or count: 0)`);\n\t}\n\n\tconst upperBound = gridTotal ?? Infinity;\n\tconst invalid = chosen.find(idx => !Number.isInteger(idx) || idx < 0 || idx >= upperBound);\n\tif (invalid !== undefined) {\n\t\tconst bounds = gridTotal !== undefined ? `[0, ${gridTotal})` : `[0, ∞) — pass 'rows' to enable upper-bound checking`;\n\t\tthrow new Error(`clipFromGrid: invalid cell index ${invalid}; expected integer in ${bounds}`);\n\t}\n\n\tconst { Texture, Rectangle } = await import('pixi.js');\n\n\tconst frames: Texture[] = chosen.map(idx => {\n\t\tconst col = idx % columns;\n\t\tconst row = Math.floor(idx / columns);\n\t\tconst x = margin + col * (frameWidth + spacing);\n\t\tconst y = margin + row * (frameHeight + spacing);\n\t\treturn new Texture({\n\t\t\tsource,\n\t\t\tframe: new Rectangle(x, y, frameWidth, frameHeight),\n\t\t});\n\t});\n\n\treturn buildClip({\n\t\tframes,\n\t\tframeDuration: input.frameDuration,\n\t\tframeDurations: input.frameDurations,\n\t\tloop: input.loop,\n\t});\n}\n\n/**\n * Build an asset-manager-compatible loader for a PixiJS spritesheet atlas\n * (TexturePacker JSON, etc.). Returns the fully-parsed `Spritesheet` object\n * with `.animations` and `.textures` populated.\n *\n * The loader performs a runtime shape check on the resolved value — `Assets.load<T>`\n * is purely nominal in PixiJS, so pointing this at a non-atlas URL would\n * otherwise surface as a misleading 'animation not found' error deep in\n * `clipFromSheet`/`animationSetFromSheet`. The shape check turns that into a\n * load-time error with a clear message.\n *\n * To get literal animation-name inference, declare `S` as an\n * `interface ... extends SpritesheetData` (a `type` alias re-widens via\n * `SpritesheetData.animations`'s `Dict<string[]>` string index signature).\n *\n * @example\n * interface HeroData extends SpritesheetData {\n * animations: { idle: string[]; walk: string[]; attack: string[] };\n * }\n *\n * ecs.builder.withAssets(a => a\n * .add('hero', spritesheetLoader<HeroData>('/hero.json'))\n * );\n *\n * // Later:\n * const sheet = ecs.assets.get('hero'); // Spritesheet<HeroData>\n * const set = animationSetFromSheet('hero', sheet); // names inferred\n */\nexport function spritesheetLoader<S extends SpritesheetData = SpritesheetData>(\n\turl: string,\n): () => Promise<Spritesheet<S>> {\n\treturn async () => {\n\t\tconst { Assets } = await import('pixi.js');\n\t\tconst result = await Assets.load<Spritesheet<S>>(url);\n\t\t// Verify properties exist AND are non-null objects — `'animations' in result`\n\t\t// alone would pass for { animations: undefined }, defeating the load-time guard.\n\t\tif (\n\t\t\t!result || typeof result !== 'object'\n\t\t\t|| typeof (result as { animations?: unknown }).animations !== 'object'\n\t\t\t|| (result as { animations: unknown }).animations === null\n\t\t\t|| typeof (result as { textures?: unknown }).textures !== 'object'\n\t\t\t|| (result as { textures: unknown }).textures === null\n\t\t) {\n\t\t\tthrow new Error(\n\t\t\t\t`spritesheetLoader: resource at \"${url}\" did not resolve to a Spritesheet ` +\n\t\t\t\t`(expected non-null 'animations' and 'textures' objects). Check that the URL points to a TexturePacker-style JSON atlas, not a raw image.`,\n\t\t\t);\n\t\t}\n\t\treturn result;\n\t};\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "9EF329142BBA6E6864756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "288D71B236D04EC264756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -9,7 +9,7 @@
9
9
  * regardless of source.
10
10
  */
11
11
  import { type BasePluginOptions } from 'ecspresso';
12
- import type { WorldConfigFrom, EmptyConfig } from '../../type-utils';
12
+ import type { ComponentsConfig, ResourcesConfig } from '../../type-utils';
13
13
  import { type NavGrid } from '../ai/pathfinding';
14
14
  import type { Vector2D } from '../../utils/math';
15
15
  export declare const TILE_FLIP_HORIZONTAL = 2147483648;
@@ -199,7 +199,7 @@ export interface TilemapRegistry {
199
199
  export interface TilemapResourceTypes {
200
200
  tilemaps: TilemapRegistry;
201
201
  }
202
- export type TilemapWorldConfig = WorldConfigFrom<TilemapComponentTypes, EmptyConfig['events'], TilemapResourceTypes>;
202
+ export type TilemapWorldConfig = ComponentsConfig<TilemapComponentTypes> & ResourcesConfig<TilemapResourceTypes>;
203
203
  export interface TilemapPluginOptions<G extends string = 'rendering'> extends BasePluginOptions<G> {
204
204
  /** Optional collision layer name. When set, solid tiles auto-spawn `aabbCollider` strips. */
205
205
  collisionLayer?: string;
@@ -208,7 +208,7 @@ export interface TilemapPluginOptions<G extends string = 'rendering'> extends Ba
208
208
  }
209
209
  export declare function createLoadedTilemap(data: TilemapRuntimeData): LoadedTilemap;
210
210
  export declare function parseTiledJSON(map: TiledMap, options: ParseTiledOptions): LoadedTilemap;
211
- export declare function createTilemapPlugin<G extends string = 'rendering'>(options?: TilemapPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithComponents<EmptyConfig, TilemapComponentTypes>, TilemapResourceTypes>, EmptyConfig, never, G, never, never>;
211
+ export declare function createTilemapPlugin<G extends string = 'rendering'>(options?: TilemapPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, TilemapComponentTypes>, TilemapResourceTypes>, import("ecspresso").EmptyConfig, never, G, never, never>;
212
212
  /**
213
213
  * Create a `tilemap` layer component for spreading into `spawn()`.
214
214
  *