ecspresso 0.13.3 → 0.14.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 (82) hide show
  1. package/dist/bindings/react/index.d.ts +29 -0
  2. package/dist/ecspresso.d.ts +5 -0
  3. package/dist/entity-manager.d.ts +4 -0
  4. package/dist/hierarchy-manager.d.ts +7 -0
  5. package/dist/index.js +2 -2
  6. package/dist/index.js.map +8 -8
  7. package/dist/plugins/ai/behavior-tree.d.ts +369 -0
  8. package/dist/plugins/ai/behavior-tree.js +4 -0
  9. package/dist/plugins/ai/behavior-tree.js.map +10 -0
  10. package/dist/plugins/ai/detection.d.ts +17 -0
  11. package/dist/plugins/ai/detection.js +2 -2
  12. package/dist/plugins/ai/detection.js.map +3 -3
  13. package/dist/plugins/ai/flocking.d.ts +97 -0
  14. package/dist/plugins/ai/flocking.js +4 -0
  15. package/dist/plugins/ai/flocking.js.map +12 -0
  16. package/dist/plugins/audio/audio.js +2 -2
  17. package/dist/plugins/audio/audio.js.map +2 -2
  18. package/dist/plugins/combat/health.js +2 -2
  19. package/dist/plugins/combat/health.js.map +2 -2
  20. package/dist/plugins/combat/projectile.js +2 -2
  21. package/dist/plugins/combat/projectile.js.map +2 -2
  22. package/dist/plugins/debug/diagnostics.js +3 -3
  23. package/dist/plugins/debug/diagnostics.js.map +2 -2
  24. package/dist/plugins/input/input.js +2 -2
  25. package/dist/plugins/input/input.js.map +2 -2
  26. package/dist/plugins/input/selection.js +2 -2
  27. package/dist/plugins/input/selection.js.map +2 -2
  28. package/dist/plugins/isometric/depth-sort.js +2 -2
  29. package/dist/plugins/isometric/depth-sort.js.map +2 -2
  30. package/dist/plugins/isometric/projection.js +2 -2
  31. package/dist/plugins/isometric/projection.js.map +2 -2
  32. package/dist/plugins/physics/collision.js +2 -2
  33. package/dist/plugins/physics/collision.js.map +3 -3
  34. package/dist/plugins/physics/collision3D.d.ts +83 -0
  35. package/dist/plugins/physics/collision3D.js +4 -0
  36. package/dist/plugins/physics/collision3D.js.map +13 -0
  37. package/dist/plugins/physics/physics2D.js +2 -2
  38. package/dist/plugins/physics/physics2D.js.map +3 -3
  39. package/dist/plugins/physics/physics3D.d.ts +140 -0
  40. package/dist/plugins/physics/physics3D.js +4 -0
  41. package/dist/plugins/physics/physics3D.js.map +11 -0
  42. package/dist/plugins/physics/steering.js +2 -2
  43. package/dist/plugins/physics/steering.js.map +2 -2
  44. package/dist/plugins/rendering/particles.js +2 -2
  45. package/dist/plugins/rendering/particles.js.map +2 -2
  46. package/dist/plugins/rendering/renderer2D.js +2 -2
  47. package/dist/plugins/rendering/renderer2D.js.map +3 -3
  48. package/dist/plugins/rendering/renderer3D.d.ts +226 -0
  49. package/dist/plugins/rendering/renderer3D.js +4052 -0
  50. package/dist/plugins/rendering/renderer3D.js.map +12 -0
  51. package/dist/plugins/rendering/sprite-animation.js +2 -2
  52. package/dist/plugins/rendering/sprite-animation.js.map +2 -2
  53. package/dist/plugins/scripting/coroutine.js +2 -2
  54. package/dist/plugins/scripting/coroutine.js.map +2 -2
  55. package/dist/plugins/scripting/state-machine.js +2 -2
  56. package/dist/plugins/scripting/state-machine.js.map +2 -2
  57. package/dist/plugins/scripting/timers.js +2 -2
  58. package/dist/plugins/scripting/timers.js.map +2 -2
  59. package/dist/plugins/scripting/tween.js +2 -2
  60. package/dist/plugins/scripting/tween.js.map +2 -2
  61. package/dist/plugins/spatial/bounds.js +2 -2
  62. package/dist/plugins/spatial/bounds.js.map +2 -2
  63. package/dist/plugins/spatial/camera.js +2 -2
  64. package/dist/plugins/spatial/camera.js.map +2 -2
  65. package/dist/plugins/spatial/camera3D.d.ts +92 -0
  66. package/dist/plugins/spatial/camera3D.js +4 -0
  67. package/dist/plugins/spatial/camera3D.js.map +10 -0
  68. package/dist/plugins/spatial/spatial-index.js +2 -2
  69. package/dist/plugins/spatial/spatial-index.js.map +3 -3
  70. package/dist/plugins/spatial/spatial-index3D.d.ts +80 -0
  71. package/dist/plugins/spatial/spatial-index3D.js +4 -0
  72. package/dist/plugins/spatial/spatial-index3D.js.map +11 -0
  73. package/dist/plugins/spatial/transform.js +2 -2
  74. package/dist/plugins/spatial/transform.js.map +3 -3
  75. package/dist/plugins/spatial/transform3D.d.ts +148 -0
  76. package/dist/plugins/spatial/transform3D.js +4 -0
  77. package/dist/plugins/spatial/transform3D.js.map +10 -0
  78. package/dist/resource-manager.d.ts +24 -0
  79. package/dist/utils/math.d.ts +65 -1
  80. package/dist/utils/narrowphase3D.d.ts +120 -0
  81. package/dist/utils/spatial-hash3D.d.ts +72 -0
  82. package/package.json +42 -2
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/spatial/transform.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Transform Plugin for ECSpresso\n *\n * Provides hierarchical transform propagation following Bevy's Transform/GlobalTransform pattern.\n * LocalTransform is modified by user code; WorldTransform is computed automatically.\n *\n * @see https://docs.rs/bevy/latest/bevy/transform/components/struct.GlobalTransform.html\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\n\n// ==================== Component Types ====================\n\n/**\n * Local transform relative to parent (or world if no parent).\n * This is the transform you modify directly.\n */\nexport interface LocalTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Computed world transform (accumulated from parent chain).\n * Read-only - managed by the transform propagation system.\n */\nexport interface WorldTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Component types provided by the transform plugin.\n * Included automatically via `.withPlugin(createTransformPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withComponentTypes<{ sprite: Sprite; velocity: { x: number; y: number } }>()\n * .build();\n * ```\n */\nexport interface TransformComponentTypes {\n\tlocalTransform: LocalTransform;\n\tworldTransform: WorldTransform;\n}\n\n/**\n * WorldConfig representing the transform plugin's provided components.\n * Used as the `Requires` type parameter by plugins that depend on transform.\n */\nexport type TransformWorldConfig = WorldConfigFrom<TransformComponentTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the transform plugin.\n */\nexport interface TransformPluginOptions<G extends string = 'transform'> extends BasePluginOptions<G> {}\n\n// ==================== Default Values ====================\n\n/**\n * Default local transform values.\n */\nexport const DEFAULT_LOCAL_TRANSFORM: Readonly<LocalTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n/**\n * Default world transform values.\n */\nexport const DEFAULT_WORLD_TRANSFORM: Readonly<WorldTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a local transform component with position only.\n * Uses default rotation (0) and scale (1, 1).\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createLocalTransform(100, 200),\n * sprite,\n * });\n * ```\n */\nexport function createLocalTransform(x: number, y: number): Pick<TransformComponentTypes, 'localTransform'> {\n\treturn {\n\t\tlocalTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Create a world transform component with position only.\n * Typically used alongside createLocalTransform for initial state.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createWorldTransform(x: number, y: number): Pick<TransformComponentTypes, 'worldTransform'> {\n\treturn {\n\t\tworldTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Options for creating a full transform.\n */\nexport interface TransformOptions {\n\trotation?: number;\n\tscaleX?: number;\n\tscaleY?: number;\n\t/** Uniform scale (overrides scaleX/scaleY if provided) */\n\tscale?: number;\n}\n\n/**\n * Create both local and world transform components.\n * World transform is initialized to match local transform.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @param options Optional rotation and scale\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * });\n *\n * // With rotation and scale\n * ecs.spawn({\n * ...createTransform(100, 200, { rotation: Math.PI / 4, scale: 2 }),\n * sprite,\n * });\n * ```\n */\nexport function createTransform(\n\tx: number,\n\ty: number,\n\toptions?: TransformOptions\n): TransformComponentTypes {\n\tconst scaleX = options?.scale ?? options?.scaleX ?? 1;\n\tconst scaleY = options?.scale ?? options?.scaleY ?? 1;\n\tconst rotation = options?.rotation ?? 0;\n\n\tconst transform = {\n\t\tx,\n\t\ty,\n\t\trotation,\n\t\tscaleX,\n\t\tscaleY,\n\t};\n\n\treturn {\n\t\tlocalTransform: { ...transform },\n\t\tworldTransform: { ...transform },\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a transform plugin for ECSpresso.\n *\n * This plugin provides:\n * - Transform propagation system that computes world transforms from local transforms\n * - Parent-first traversal ensures parents are processed before children\n * - Supports full transform hierarchy (position, rotation, scale)\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createPhysics2DPlugin())\n * .build();\n *\n * // Spawn entity with transform\n * ecs.spawn({\n * ...createTransform(100, 200),\n * velocity: { x: 50, y: 0 },\n * });\n * ```\n */\nexport function createTransformPlugin<G extends string = 'transform'>(\n\toptions?: TransformPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'transform',\n\t\tpriority = 500,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('transform')\n\t\t.withComponentTypes<TransformComponentTypes>()\n\t\t.withLabels<'transform-propagation'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\t// localTransform requires worldTransform — initialize from localTransform values\n\t\t\tworld.registerRequired('localTransform', 'worldTransform', (lt) => ({\n\t\t\t\tx: lt.x, y: lt.y, rotation: lt.rotation, scaleX: lt.scaleX, scaleY: lt.scaleY,\n\t\t\t}));\n\n\t\t\tconst orphanBuffer: Array<import('../../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>> = [];\n\n\t\t\tworld\n\t\t\t\t.addSystem('transform-propagation')\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.setProcess(({ ecs }) => {\n\t\t\t\t\tpropagateTransforms(ecs, orphanBuffer);\n\t\t\t\t});\n\t\t});\n}\n\n/**\n * Propagate transforms through the hierarchy.\n * Parent-first traversal ensures parents are computed before children.\n *\n * Runs unconditionally for all entities with transforms — user code can\n * freely mutate localTransform without needing to call markChanged.\n * Marks worldTransform as changed so downstream systems (e.g. renderer\n * sync) pick up the updated values.\n */\nfunction propagateTransforms(\n\tecs: ECSpresso<WorldConfigFrom<TransformComponentTypes>>,\n\torphanBuffer: Array<import('../../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>>,\n): void {\n\tconst em = ecs.entityManager;\n\n\t// Use parent-first traversal for entities in hierarchy\n\tecs.forEachInHierarchy((entityId, parentId) => {\n\t\tconst localTransform = em.getComponent(entityId, 'localTransform');\n\t\tconst worldTransform = em.getComponent(entityId, 'worldTransform');\n\n\t\tif (!localTransform || !worldTransform) return;\n\n\t\tif (parentId === null) {\n\t\t\t// Root entity: world transform equals local transform\n\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t} else {\n\t\t\t// Child entity: combine with parent's world transform\n\t\t\tconst parentWorld = em.getComponent(parentId, 'worldTransform');\n\t\t\tif (parentWorld) {\n\t\t\t\tcombineTransforms(parentWorld, localTransform, worldTransform);\n\t\t\t} else {\n\t\t\t\t// Parent has no world transform, treat as root\n\t\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t\t}\n\t\t}\n\n\t\tecs.markChanged(entityId, 'worldTransform');\n\t});\n\n\t// Process orphaned entities (not in hierarchy but have transforms)\n\tem.getEntitiesWithQueryInto(orphanBuffer, ['localTransform', 'worldTransform']);\n\tfor (const entity of orphanBuffer) {\n\t\tconst parentId = ecs.getParent(entity.id);\n\t\t// Only process if truly orphaned (no parent and not a root with children)\n\t\tif (parentId === null && ecs.getChildren(entity.id).length === 0) {\n\t\t\tconst { localTransform, worldTransform } = entity.components;\n\t\t\tcopyTransform(localTransform, worldTransform);\n\t\t\tecs.markChanged(entity.id, 'worldTransform');\n\t\t}\n\t}\n}\n\n/**\n * Copy transform values from source to destination.\n */\nfunction copyTransform(src: LocalTransform, dest: WorldTransform): void {\n\tdest.x = src.x;\n\tdest.y = src.y;\n\tdest.rotation = src.rotation;\n\tdest.scaleX = src.scaleX;\n\tdest.scaleY = src.scaleY;\n}\n\n/**\n * Combine parent world transform with child local transform into child world transform.\n */\nfunction combineTransforms(\n\tparent: WorldTransform,\n\tlocal: LocalTransform,\n\tworld: WorldTransform\n): void {\n\t// Apply parent's scale to local position\n\tconst scaledLocalX = local.x * parent.scaleX;\n\tconst scaledLocalY = local.y * parent.scaleY;\n\n\t// Rotate local position by parent's rotation\n\tconst cos = Math.cos(parent.rotation);\n\tconst sin = Math.sin(parent.rotation);\n\tconst rotatedX = scaledLocalX * cos - scaledLocalY * sin;\n\tconst rotatedY = scaledLocalX * sin + scaledLocalY * cos;\n\n\t// Add to parent's position\n\tworld.x = parent.x + rotatedX;\n\tworld.y = parent.y + rotatedY;\n\tworld.rotation = parent.rotation + local.rotation;\n\tworld.scaleX = parent.scaleX * local.scaleX;\n\tworld.scaleY = parent.scaleY * local.scaleY;\n}\n"
5
+ "/**\n * Transform Plugin for ECSpresso\n *\n * Provides hierarchical transform propagation following Bevy's Transform/GlobalTransform pattern.\n * LocalTransform is modified by user code; WorldTransform is computed automatically.\n *\n * @see https://docs.rs/bevy/latest/bevy/transform/components/struct.GlobalTransform.html\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\n\n// ==================== Component Types ====================\n\n/**\n * Local transform relative to parent (or world if no parent).\n * This is the transform you modify directly.\n */\nexport interface LocalTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Computed world transform (accumulated from parent chain).\n * Read-only - managed by the transform propagation system.\n */\nexport interface WorldTransform {\n\tx: number;\n\ty: number;\n\trotation: number;\n\tscaleX: number;\n\tscaleY: number;\n}\n\n/**\n * Component types provided by the transform plugin.\n * Included automatically via `.withPlugin(createTransformPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withComponentTypes<{ sprite: Sprite; velocity: { x: number; y: number } }>()\n * .build();\n * ```\n */\nexport interface TransformComponentTypes {\n\tlocalTransform: LocalTransform;\n\tworldTransform: WorldTransform;\n}\n\n/**\n * WorldConfig representing the transform plugin's provided components.\n * Used as the `Requires` type parameter by plugins that depend on transform.\n */\nexport type TransformWorldConfig = WorldConfigFrom<TransformComponentTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the transform plugin.\n */\nexport interface TransformPluginOptions<G extends string = 'transform'> extends BasePluginOptions<G> {}\n\n// ==================== Default Values ====================\n\n/**\n * Default local transform values.\n */\nexport const DEFAULT_LOCAL_TRANSFORM: Readonly<LocalTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n/**\n * Default world transform values.\n */\nexport const DEFAULT_WORLD_TRANSFORM: Readonly<WorldTransform> = {\n\tx: 0,\n\ty: 0,\n\trotation: 0,\n\tscaleX: 1,\n\tscaleY: 1,\n};\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a local transform component with position only.\n * Uses default rotation (0) and scale (1, 1).\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createLocalTransform(100, 200),\n * sprite,\n * });\n * ```\n */\nexport function createLocalTransform(x: number, y: number): Pick<TransformComponentTypes, 'localTransform'> {\n\treturn {\n\t\tlocalTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Create a world transform component with position only.\n * Typically used alongside createLocalTransform for initial state.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createWorldTransform(x: number, y: number): Pick<TransformComponentTypes, 'worldTransform'> {\n\treturn {\n\t\tworldTransform: {\n\t\t\tx,\n\t\t\ty,\n\t\t\trotation: 0,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t},\n\t};\n}\n\n/**\n * Options for creating a full transform.\n */\nexport interface TransformOptions {\n\trotation?: number;\n\tscaleX?: number;\n\tscaleY?: number;\n\t/** Uniform scale (overrides scaleX/scaleY if provided) */\n\tscale?: number;\n}\n\n/**\n * Create both local and world transform components.\n * World transform is initialized to match local transform.\n *\n * @param x The x coordinate\n * @param y The y coordinate\n * @param options Optional rotation and scale\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * sprite,\n * });\n *\n * // With rotation and scale\n * ecs.spawn({\n * ...createTransform(100, 200, { rotation: Math.PI / 4, scale: 2 }),\n * sprite,\n * });\n * ```\n */\nexport function createTransform(\n\tx: number,\n\ty: number,\n\toptions?: TransformOptions\n): TransformComponentTypes {\n\tconst scaleX = options?.scale ?? options?.scaleX ?? 1;\n\tconst scaleY = options?.scale ?? options?.scaleY ?? 1;\n\tconst rotation = options?.rotation ?? 0;\n\n\tconst transform = {\n\t\tx,\n\t\ty,\n\t\trotation,\n\t\tscaleX,\n\t\tscaleY,\n\t};\n\n\treturn {\n\t\tlocalTransform: { ...transform },\n\t\tworldTransform: { ...transform },\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a transform plugin for ECSpresso.\n *\n * This plugin provides:\n * - Transform propagation system that computes world transforms from local transforms\n * - Parent-first traversal ensures parents are processed before children\n * - Supports full transform hierarchy (position, rotation, scale)\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createPhysics2DPlugin())\n * .build();\n *\n * // Spawn entity with transform\n * ecs.spawn({\n * ...createTransform(100, 200),\n * velocity: { x: 50, y: 0 },\n * });\n * ```\n */\nexport function createTransformPlugin<G extends string = 'transform'>(\n\toptions?: TransformPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'transform',\n\t\tpriority = 500,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('transform')\n\t\t.withComponentTypes<TransformComponentTypes>()\n\t\t.withLabels<'transform-propagation'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\t// localTransform requires worldTransform — initialize from localTransform values\n\t\t\tworld.registerRequired('localTransform', 'worldTransform', (lt) => ({\n\t\t\t\tx: lt.x, y: lt.y, rotation: lt.rotation, scaleX: lt.scaleX, scaleY: lt.scaleY,\n\t\t\t}));\n\n\t\t\tconst orphanBuffer: Array<import('../../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>> = [];\n\t\t\tconst hierarchyVisited = new Set<number>();\n\n\t\t\tworld\n\t\t\t\t.addSystem('transform-propagation')\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.setProcess(({ ecs }) => {\n\t\t\t\t\tpropagateTransforms(ecs, orphanBuffer, hierarchyVisited);\n\t\t\t\t});\n\t\t});\n}\n\n/**\n * Propagate transforms through the hierarchy.\n * Parent-first traversal ensures parents are computed before children.\n *\n * Runs unconditionally for all entities with transforms — user code can\n * freely mutate localTransform without needing to call markChanged.\n * Only marks worldTransform as changed when values actually differ,\n * so downstream systems (e.g. renderer sync) can skip static entities.\n */\nfunction propagateTransforms(\n\tecs: ECSpresso<WorldConfigFrom<TransformComponentTypes>>,\n\torphanBuffer: Array<import('../../types').FilteredEntity<TransformComponentTypes, 'localTransform' | 'worldTransform'>>,\n\thierarchyVisited: Set<number>,\n): void {\n\tconst em = ecs.entityManager;\n\n\t// Fast path: no hierarchy relationships exist — all entities are flat\n\tif (!em.hasHierarchy) {\n\t\tem.getEntitiesWithQueryInto(orphanBuffer, ['localTransform', 'worldTransform']);\n\t\tfor (const entity of orphanBuffer) {\n\t\t\tconst { localTransform, worldTransform } = entity.components;\n\t\t\tif (copyTransform(localTransform, worldTransform)) {\n\t\t\t\tecs.markChanged(entity.id, 'worldTransform');\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\n\t// Hierarchy exists — use parent-first traversal then process remaining orphans\n\thierarchyVisited.clear();\n\n\tecs.forEachInHierarchy((entityId, parentId) => {\n\t\thierarchyVisited.add(entityId);\n\t\tconst localTransform = em.getComponent(entityId, 'localTransform');\n\t\tconst worldTransform = em.getComponent(entityId, 'worldTransform');\n\n\t\tif (!localTransform || !worldTransform) return;\n\n\t\tconst parentWorld = parentId !== null\n\t\t\t? em.getComponent(parentId, 'worldTransform')\n\t\t\t: null;\n\n\t\tconst changed = parentWorld\n\t\t\t? combineTransforms(parentWorld, localTransform, worldTransform)\n\t\t\t: copyTransform(localTransform, worldTransform);\n\n\t\tif (changed) ecs.markChanged(entityId, 'worldTransform');\n\t});\n\n\tem.getEntitiesWithQueryInto(orphanBuffer, ['localTransform', 'worldTransform']);\n\tfor (const entity of orphanBuffer) {\n\t\tif (hierarchyVisited.has(entity.id)) continue;\n\t\tconst { localTransform, worldTransform } = entity.components;\n\t\tif (copyTransform(localTransform, worldTransform)) {\n\t\t\tecs.markChanged(entity.id, 'worldTransform');\n\t\t}\n\t}\n}\n\n/**\n * Copy transform values from source to destination.\n * Returns true if the destination was actually modified.\n */\nfunction copyTransform(src: LocalTransform, dest: WorldTransform): boolean {\n\tif (dest.x === src.x && dest.y === src.y &&\n\t\tdest.rotation === src.rotation &&\n\t\tdest.scaleX === src.scaleX && dest.scaleY === src.scaleY) {\n\t\treturn false;\n\t}\n\tdest.x = src.x;\n\tdest.y = src.y;\n\tdest.rotation = src.rotation;\n\tdest.scaleX = src.scaleX;\n\tdest.scaleY = src.scaleY;\n\treturn true;\n}\n\n/**\n * Combine parent world transform with child local transform into child world transform.\n * Returns true if the destination was actually modified.\n */\nfunction combineTransforms(\n\tparent: WorldTransform,\n\tlocal: LocalTransform,\n\tworld: WorldTransform\n): boolean {\n\t// Apply parent's scale to local position\n\tconst scaledLocalX = local.x * parent.scaleX;\n\tconst scaledLocalY = local.y * parent.scaleY;\n\n\t// Rotate local position by parent's rotation\n\tconst cos = Math.cos(parent.rotation);\n\tconst sin = Math.sin(parent.rotation);\n\tconst rotatedX = scaledLocalX * cos - scaledLocalY * sin;\n\tconst rotatedY = scaledLocalX * sin + scaledLocalY * cos;\n\n\t// Add to parent's position\n\tconst newX = parent.x + rotatedX;\n\tconst newY = parent.y + rotatedY;\n\tconst newRotation = parent.rotation + local.rotation;\n\tconst newScaleX = parent.scaleX * local.scaleX;\n\tconst newScaleY = parent.scaleY * local.scaleY;\n\n\tif (world.x === newX && world.y === newY &&\n\t\tworld.rotation === newRotation &&\n\t\tworld.scaleX === newScaleX && world.scaleY === newScaleY) {\n\t\treturn false;\n\t}\n\n\tworld.x = newX;\n\tworld.y = newY;\n\tworld.rotation = newRotation;\n\tworld.scaleX = newScaleX;\n\tworld.scaleY = newScaleY;\n\treturn true;\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "67B924D382E8AA1364756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "9368B362409A95F964756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * 3D Transform Plugin for ECSpresso
3
+ *
4
+ * Provides hierarchical 3D transform propagation following Bevy's Transform/GlobalTransform pattern.
5
+ * LocalTransform3D is modified by user code; WorldTransform3D is computed automatically.
6
+ *
7
+ * Rotation is stored as Euler angles (radians, XYZ intrinsic order matching Three.js defaults).
8
+ * Hierarchical composition converts to quaternions internally for correct rotation composition,
9
+ * then converts back to Euler for storage.
10
+ */
11
+ import { type BasePluginOptions } from 'ecspresso';
12
+ import type { WorldConfigFrom } from '../../type-utils';
13
+ /**
14
+ * 3D local transform relative to parent (or world if no parent).
15
+ * This is the transform you modify directly.
16
+ *
17
+ * Rotation is in radians, using XYZ intrinsic Euler order (Three.js default).
18
+ */
19
+ export interface LocalTransform3D {
20
+ x: number;
21
+ y: number;
22
+ z: number;
23
+ rx: number;
24
+ ry: number;
25
+ rz: number;
26
+ sx: number;
27
+ sy: number;
28
+ sz: number;
29
+ }
30
+ /**
31
+ * Computed 3D world transform (accumulated from parent chain).
32
+ * Read-only — managed by the transform propagation system.
33
+ */
34
+ export interface WorldTransform3D {
35
+ x: number;
36
+ y: number;
37
+ z: number;
38
+ rx: number;
39
+ ry: number;
40
+ rz: number;
41
+ sx: number;
42
+ sy: number;
43
+ sz: number;
44
+ }
45
+ /**
46
+ * Component types provided by the 3D transform plugin.
47
+ * Included automatically via `.withPlugin(createTransform3DPlugin())`.
48
+ */
49
+ export interface Transform3DComponentTypes {
50
+ localTransform3D: LocalTransform3D;
51
+ worldTransform3D: WorldTransform3D;
52
+ }
53
+ /**
54
+ * WorldConfig representing the 3D transform plugin's provided components.
55
+ * Used as the `Requires` type parameter by plugins that depend on transform3D.
56
+ */
57
+ export type Transform3DWorldConfig = WorldConfigFrom<Transform3DComponentTypes>;
58
+ /**
59
+ * Configuration options for the 3D transform plugin.
60
+ */
61
+ export interface Transform3DPluginOptions<G extends string = 'transform3d'> extends BasePluginOptions<G> {
62
+ }
63
+ /**
64
+ * Default local 3D transform values.
65
+ */
66
+ export declare const DEFAULT_LOCAL_TRANSFORM_3D: Readonly<LocalTransform3D>;
67
+ /**
68
+ * Default world 3D transform values.
69
+ */
70
+ export declare const DEFAULT_WORLD_TRANSFORM_3D: Readonly<WorldTransform3D>;
71
+ /**
72
+ * Create a local 3D transform component with position only.
73
+ * Uses default rotation (0, 0, 0) and scale (1, 1, 1).
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * ecs.spawn({
78
+ * ...createLocalTransform3D(10, 5, -20),
79
+ * mesh: myMesh,
80
+ * });
81
+ * ```
82
+ */
83
+ export declare function createLocalTransform3D(x: number, y: number, z: number): Pick<Transform3DComponentTypes, 'localTransform3D'>;
84
+ /**
85
+ * Create a world 3D transform component with position only.
86
+ * Typically used alongside createLocalTransform3D for initial state.
87
+ */
88
+ export declare function createWorldTransform3D(x: number, y: number, z: number): Pick<Transform3DComponentTypes, 'worldTransform3D'>;
89
+ /**
90
+ * Options for creating a full 3D transform.
91
+ */
92
+ export interface Transform3DOptions {
93
+ rotation?: {
94
+ x?: number;
95
+ y?: number;
96
+ z?: number;
97
+ };
98
+ scaleX?: number;
99
+ scaleY?: number;
100
+ scaleZ?: number;
101
+ /** Uniform scale (overrides scaleX/scaleY/scaleZ if provided) */
102
+ scale?: number;
103
+ }
104
+ /**
105
+ * Create both local and world 3D transform components.
106
+ * World transform is initialized to match local transform.
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * ecs.spawn({
111
+ * ...createTransform3D(10, 5, -20),
112
+ * mesh: myMesh,
113
+ * });
114
+ *
115
+ * // With rotation and scale
116
+ * ecs.spawn({
117
+ * ...createTransform3D(10, 5, -20, {
118
+ * rotation: { y: Math.PI / 4 },
119
+ * scale: 2,
120
+ * }),
121
+ * mesh: myMesh,
122
+ * });
123
+ * ```
124
+ */
125
+ export declare function createTransform3D(x: number, y: number, z: number, options?: Transform3DOptions): Transform3DComponentTypes;
126
+ /**
127
+ * Create a 3D transform plugin for ECSpresso.
128
+ *
129
+ * This plugin provides:
130
+ * - 3D transform propagation system that computes world transforms from local transforms
131
+ * - Parent-first traversal ensures parents are processed before children
132
+ * - Supports full 3D transform hierarchy (position, rotation, scale)
133
+ * - Rotation composed via quaternions internally for correctness
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const ecs = ECSpresso.create()
138
+ * .withPlugin(createTransform3DPlugin())
139
+ * .withComponentTypes<{ velocity: { x: number; y: number; z: number } }>()
140
+ * .build();
141
+ *
142
+ * ecs.spawn({
143
+ * ...createTransform3D(10, 5, -20),
144
+ * velocity: { x: 1, y: 0, z: 0 },
145
+ * });
146
+ * ```
147
+ */
148
+ export declare function createTransform3DPlugin<G extends string = 'transform3d'>(options?: Transform3DPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, Transform3DComponentTypes>, import("ecspresso").EmptyConfig, "transform3d-propagation", G, never, never>;
@@ -0,0 +1,4 @@
1
+ var O=Object.defineProperty;var D=(j)=>j;function T(j,k){this[j]=D.bind(null,k)}var z=(j,k)=>{for(var F in k)O(j,F,{get:k[F],enumerable:!0,configurable:!0,set:T.bind(k,F)})};var u=(j,k)=>()=>(j&&(k=j(j=0)),k);var w=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,F)=>(typeof require<"u"?require:k)[F]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as v}from"ecspresso";var y={x:0,y:0,z:0,rx:0,ry:0,rz:0,sx:1,sy:1,sz:1},p={x:0,y:0,z:0,rx:0,ry:0,rz:0,sx:1,sy:1,sz:1};function c(j,k,F){return{localTransform3D:{x:j,y:k,z:F,rx:0,ry:0,rz:0,sx:1,sy:1,sz:1}}}function d(j,k,F){return{worldTransform3D:{x:j,y:k,z:F,rx:0,ry:0,rz:0,sx:1,sy:1,sz:1}}}function n(j,k,F,H){let J=H?.scale??H?.scaleX??1,N=H?.scale??H?.scaleY??1,U=H?.scale??H?.scaleZ??1,K=H?.rotation?.x??0,_=H?.rotation?.y??0,$=H?.rotation?.z??0,A={x:j,y:k,z:F,rx:K,ry:_,rz:$,sx:J,sy:N,sz:U};return{localTransform3D:{...A},worldTransform3D:{...A}}}var G={x:0,y:0,z:0,w:1},g={x:0,y:0,z:0,w:1},L={x:0,y:0,z:0,w:1};function Q(j,k,F,H){let J=Math.cos(j*0.5),N=Math.sin(j*0.5),U=Math.cos(k*0.5),K=Math.sin(k*0.5),_=Math.cos(F*0.5),$=Math.sin(F*0.5);H.x=N*U*_+J*K*$,H.y=J*K*_-N*U*$,H.z=J*U*$+N*K*_,H.w=J*U*_-N*K*$}function h(j,k,F){F.x=j.w*k.x+j.x*k.w+j.y*k.z-j.z*k.y,F.y=j.w*k.y-j.x*k.z+j.y*k.w+j.z*k.x,F.z=j.w*k.z+j.x*k.y-j.y*k.x+j.z*k.w,F.w=j.w*k.w-j.x*k.x-j.y*k.y-j.z*k.z}var E={x:0,y:0,z:0},C={rx:0,ry:0,rz:0};function m(j,k,F,H){let J=2*(j.y*H-j.z*F),N=2*(j.z*k-j.x*H),U=2*(j.x*F-j.y*k);return E.x=k+j.w*J+(j.y*U-j.z*N),E.y=F+j.w*N+(j.z*J-j.x*U),E.z=H+j.w*U+(j.x*N-j.y*J),E}function b(j){let k=j.x+j.x,F=j.y+j.y,H=j.z+j.z,J=j.x*k,N=j.x*F,U=j.x*H,K=j.y*F,_=j.y*H,$=j.z*H,A=j.w*k,X=j.w*F,R=j.w*H,Z=1-(K+$),Y=N-R,P=U+X,W=1-(J+$),S=_-A,V=1-(J+K);C.ry=Math.asin(Math.max(-1,Math.min(1,P)));let B=Math.cos(C.ry);if(Math.abs(B)>0.000001)C.rx=Math.atan2(-S,V),C.rz=Math.atan2(-Y,Z);else{let I=N+R;C.rx=Math.atan2(I,W),C.rz=0}return C}function l(j){let{systemGroup:k="transform3d",priority:F=500,phase:H="postUpdate"}=j??{};return v("transform3d").withComponentTypes().withLabels().withGroups().install((J)=>{J.registerRequired("localTransform3D","worldTransform3D",(K)=>({x:K.x,y:K.y,z:K.z,rx:K.rx,ry:K.ry,rz:K.rz,sx:K.sx,sy:K.sy,sz:K.sz}));let N=[],U=new Set;J.addSystem("transform3d-propagation").setPriority(F).inPhase(H).inGroup(k).setProcess(({ecs:K})=>{f(K,N,U)})})}function f(j,k,F){let H=j.entityManager;if(!H.hasHierarchy){H.getEntitiesWithQueryInto(k,["localTransform3D","worldTransform3D"]);for(let J of k){let{localTransform3D:N,worldTransform3D:U}=J.components;if(M(N,U))j.markChanged(J.id,"worldTransform3D")}return}F.clear(),j.forEachInHierarchy((J,N)=>{F.add(J);let U=H.getComponent(J,"localTransform3D"),K=H.getComponent(J,"worldTransform3D");if(!U||!K)return;let _=N!==null?H.getComponent(N,"worldTransform3D"):null;if(_?x(_,U,K):M(U,K))j.markChanged(J,"worldTransform3D")}),H.getEntitiesWithQueryInto(k,["localTransform3D","worldTransform3D"]);for(let J of k){if(F.has(J.id))continue;let{localTransform3D:N,worldTransform3D:U}=J.components;if(M(N,U))j.markChanged(J.id,"worldTransform3D")}}function M(j,k){if(k.x===j.x&&k.y===j.y&&k.z===j.z&&k.rx===j.rx&&k.ry===j.ry&&k.rz===j.rz&&k.sx===j.sx&&k.sy===j.sy&&k.sz===j.sz)return!1;return k.x=j.x,k.y=j.y,k.z=j.z,k.rx=j.rx,k.ry=j.ry,k.rz=j.rz,k.sx=j.sx,k.sy=j.sy,k.sz=j.sz,!0}function x(j,k,F){Q(j.rx,j.ry,j.rz,G),Q(k.rx,k.ry,k.rz,g),h(G,g,L);let H=b(L),J=m(G,k.x*j.sx,k.y*j.sy,k.z*j.sz),N=j.x+J.x,U=j.y+J.y,K=j.z+J.z,_=j.sx*k.sx,$=j.sy*k.sy,A=j.sz*k.sz;if(F.x===N&&F.y===U&&F.z===K&&F.rx===H.rx&&F.ry===H.ry&&F.rz===H.rz&&F.sx===_&&F.sy===$&&F.sz===A)return!1;return F.x=N,F.y=U,F.z=K,F.rx=H.rx,F.ry=H.ry,F.rz=H.rz,F.sx=_,F.sy=$,F.sz=A,!0}export{d as createWorldTransform3D,l as createTransform3DPlugin,n as createTransform3D,c as createLocalTransform3D,p as DEFAULT_WORLD_TRANSFORM_3D,y as DEFAULT_LOCAL_TRANSFORM_3D};
2
+
3
+ //# debugId=CB864A7BB0F7E5CA64756E2164756E21
4
+ //# sourceMappingURL=transform3D.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/plugins/spatial/transform3D.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * 3D Transform Plugin for ECSpresso\n *\n * Provides hierarchical 3D transform propagation following Bevy's Transform/GlobalTransform pattern.\n * LocalTransform3D is modified by user code; WorldTransform3D is computed automatically.\n *\n * Rotation is stored as Euler angles (radians, XYZ intrinsic order matching Three.js defaults).\n * Hierarchical composition converts to quaternions internally for correct rotation composition,\n * then converts back to Euler for storage.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\n\n// ==================== Component Types ====================\n\n/**\n * 3D local transform relative to parent (or world if no parent).\n * This is the transform you modify directly.\n *\n * Rotation is in radians, using XYZ intrinsic Euler order (Three.js default).\n */\nexport interface LocalTransform3D {\n\tx: number;\n\ty: number;\n\tz: number;\n\trx: number;\n\try: number;\n\trz: number;\n\tsx: number;\n\tsy: number;\n\tsz: number;\n}\n\n/**\n * Computed 3D world transform (accumulated from parent chain).\n * Read-only — managed by the transform propagation system.\n */\nexport interface WorldTransform3D {\n\tx: number;\n\ty: number;\n\tz: number;\n\trx: number;\n\try: number;\n\trz: number;\n\tsx: number;\n\tsy: number;\n\tsz: number;\n}\n\n/**\n * Component types provided by the 3D transform plugin.\n * Included automatically via `.withPlugin(createTransform3DPlugin())`.\n */\nexport interface Transform3DComponentTypes {\n\tlocalTransform3D: LocalTransform3D;\n\tworldTransform3D: WorldTransform3D;\n}\n\n/**\n * WorldConfig representing the 3D transform plugin's provided components.\n * Used as the `Requires` type parameter by plugins that depend on transform3D.\n */\nexport type Transform3DWorldConfig = WorldConfigFrom<Transform3DComponentTypes>;\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the 3D transform plugin.\n */\nexport interface Transform3DPluginOptions<G extends string = 'transform3d'> extends BasePluginOptions<G> {}\n\n// ==================== Default Values ====================\n\n/**\n * Default local 3D transform values.\n */\nexport const DEFAULT_LOCAL_TRANSFORM_3D: Readonly<LocalTransform3D> = {\n\tx: 0, y: 0, z: 0,\n\trx: 0, ry: 0, rz: 0,\n\tsx: 1, sy: 1, sz: 1,\n};\n\n/**\n * Default world 3D transform values.\n */\nexport const DEFAULT_WORLD_TRANSFORM_3D: Readonly<WorldTransform3D> = {\n\tx: 0, y: 0, z: 0,\n\trx: 0, ry: 0, rz: 0,\n\tsx: 1, sy: 1, sz: 1,\n};\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a local 3D transform component with position only.\n * Uses default rotation (0, 0, 0) and scale (1, 1, 1).\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createLocalTransform3D(10, 5, -20),\n * mesh: myMesh,\n * });\n * ```\n */\nexport function createLocalTransform3D(\n\tx: number,\n\ty: number,\n\tz: number,\n): Pick<Transform3DComponentTypes, 'localTransform3D'> {\n\treturn {\n\t\tlocalTransform3D: { x, y, z, rx: 0, ry: 0, rz: 0, sx: 1, sy: 1, sz: 1 },\n\t};\n}\n\n/**\n * Create a world 3D transform component with position only.\n * Typically used alongside createLocalTransform3D for initial state.\n */\nexport function createWorldTransform3D(\n\tx: number,\n\ty: number,\n\tz: number,\n): Pick<Transform3DComponentTypes, 'worldTransform3D'> {\n\treturn {\n\t\tworldTransform3D: { x, y, z, rx: 0, ry: 0, rz: 0, sx: 1, sy: 1, sz: 1 },\n\t};\n}\n\n/**\n * Options for creating a full 3D transform.\n */\nexport interface Transform3DOptions {\n\trotation?: { x?: number; y?: number; z?: number };\n\tscaleX?: number;\n\tscaleY?: number;\n\tscaleZ?: number;\n\t/** Uniform scale (overrides scaleX/scaleY/scaleZ if provided) */\n\tscale?: number;\n}\n\n/**\n * Create both local and world 3D transform components.\n * World transform is initialized to match local transform.\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform3D(10, 5, -20),\n * mesh: myMesh,\n * });\n *\n * // With rotation and scale\n * ecs.spawn({\n * ...createTransform3D(10, 5, -20, {\n * rotation: { y: Math.PI / 4 },\n * scale: 2,\n * }),\n * mesh: myMesh,\n * });\n * ```\n */\nexport function createTransform3D(\n\tx: number,\n\ty: number,\n\tz: number,\n\toptions?: Transform3DOptions,\n): Transform3DComponentTypes {\n\tconst sx = options?.scale ?? options?.scaleX ?? 1;\n\tconst sy = options?.scale ?? options?.scaleY ?? 1;\n\tconst sz = options?.scale ?? options?.scaleZ ?? 1;\n\tconst rx = options?.rotation?.x ?? 0;\n\tconst ry = options?.rotation?.y ?? 0;\n\tconst rz = options?.rotation?.z ?? 0;\n\n\tconst transform = { x, y, z, rx, ry, rz, sx, sy, sz };\n\n\treturn {\n\t\tlocalTransform3D: { ...transform },\n\t\tworldTransform3D: { ...transform },\n\t};\n}\n\n// ==================== Quaternion Math (Internal) ====================\n// Inlined quaternion operations to avoid Three.js dependency in the transform plugin.\n// Uses XYZ intrinsic Euler order to match Three.js Object3D.rotation defaults.\n// Pre-allocated scratch arrays avoid GC pressure in the hot path.\n\n// Scratch quaternion struct to avoid allocation in hot path\ninterface Quat { x: number; y: number; z: number; w: number }\nconst qP: Quat = { x: 0, y: 0, z: 0, w: 1 }; // parent\nconst qL: Quat = { x: 0, y: 0, z: 0, w: 1 }; // local\nconst qW: Quat = { x: 0, y: 0, z: 0, w: 1 }; // world (result)\n\n/**\n * Convert Euler angles (XYZ intrinsic order) to quaternion.\n * Writes result into the `out` object.\n */\nfunction eulerToQuat(rx: number, ry: number, rz: number, out: Quat): void {\n\tconst cx = Math.cos(rx * 0.5);\n\tconst srx = Math.sin(rx * 0.5);\n\tconst cy = Math.cos(ry * 0.5);\n\tconst sy = Math.sin(ry * 0.5);\n\tconst cz = Math.cos(rz * 0.5);\n\tconst sz = Math.sin(rz * 0.5);\n\n\t// XYZ intrinsic = ZYX extrinsic\n\tout.x = srx * cy * cz + cx * sy * sz;\n\tout.y = cx * sy * cz - srx * cy * sz;\n\tout.z = cx * cy * sz + srx * sy * cz;\n\tout.w = cx * cy * cz - srx * sy * sz;\n}\n\n/**\n * Multiply two quaternions: out = a * b\n */\nfunction quatMultiply(a: Quat, b: Quat, out: Quat): void {\n\tout.x = a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y;\n\tout.y = a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x;\n\tout.z = a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w;\n\tout.w = a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z;\n}\n\n// Scratch vec3 for quatRotateVec to avoid per-call allocation\ninterface Vec3 { x: number; y: number; z: number }\nconst vecOut: Vec3 = { x: 0, y: 0, z: 0 };\n\n// Scratch euler for quatToEuler to avoid per-call allocation\ninterface Euler3 { rx: number; ry: number; rz: number }\nconst eulerOut: Euler3 = { rx: 0, ry: 0, rz: 0 };\n\n/**\n * Rotate a vector by quaternion q. Writes result into module-scoped `vecOut`.\n */\nfunction quatRotateVec(\n\tq: Quat,\n\tvx: number,\n\tvy: number,\n\tvz: number,\n): Vec3 {\n\tconst tx = 2 * (q.y * vz - q.z * vy);\n\tconst ty = 2 * (q.z * vx - q.x * vz);\n\tconst tz = 2 * (q.x * vy - q.y * vx);\n\n\tvecOut.x = vx + q.w * tx + (q.y * tz - q.z * ty);\n\tvecOut.y = vy + q.w * ty + (q.z * tx - q.x * tz);\n\tvecOut.z = vz + q.w * tz + (q.x * ty - q.y * tx);\n\treturn vecOut;\n}\n\n/**\n * Convert quaternion to Euler angles (XYZ intrinsic order).\n * Writes result into module-scoped `eulerOut`.\n */\nfunction quatToEuler(q: Quat): Euler3 {\n\tconst x2 = q.x + q.x, y2 = q.y + q.y, z2 = q.z + q.z;\n\tconst xx = q.x * x2, xy = q.x * y2, xz = q.x * z2;\n\tconst yy = q.y * y2, yz = q.y * z2, zz = q.z * z2;\n\tconst wx = q.w * x2, wy = q.w * y2, wz = q.w * z2;\n\n\tconst m11 = 1 - (yy + zz);\n\tconst m12 = xy - wz;\n\tconst m13 = xz + wy;\n\tconst m22 = 1 - (xx + zz);\n\tconst m23 = yz - wx;\n\tconst m33 = 1 - (xx + yy);\n\n\teulerOut.ry = Math.asin(Math.max(-1, Math.min(1, m13)));\n\tconst cosY = Math.cos(eulerOut.ry);\n\n\tif (Math.abs(cosY) > 1e-6) {\n\t\teulerOut.rx = Math.atan2(-m23, m33);\n\t\teulerOut.rz = Math.atan2(-m12, m11);\n\t} else {\n\t\t// Gimbal lock fallback\n\t\tconst m21 = xy + wz;\n\t\teulerOut.rx = Math.atan2(m21, m22);\n\t\teulerOut.rz = 0;\n\t}\n\treturn eulerOut;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a 3D transform plugin for ECSpresso.\n *\n * This plugin provides:\n * - 3D transform propagation system that computes world transforms from local transforms\n * - Parent-first traversal ensures parents are processed before children\n * - Supports full 3D transform hierarchy (position, rotation, scale)\n * - Rotation composed via quaternions internally for correctness\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransform3DPlugin())\n * .withComponentTypes<{ velocity: { x: number; y: number; z: number } }>()\n * .build();\n *\n * ecs.spawn({\n * ...createTransform3D(10, 5, -20),\n * velocity: { x: 1, y: 0, z: 0 },\n * });\n * ```\n */\nexport function createTransform3DPlugin<G extends string = 'transform3d'>(\n\toptions?: Transform3DPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'transform3d',\n\t\tpriority = 500,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('transform3d')\n\t\t.withComponentTypes<Transform3DComponentTypes>()\n\t\t.withLabels<'transform3d-propagation'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\t// localTransform3D requires worldTransform3D — initialize from localTransform3D values\n\t\t\tworld.registerRequired('localTransform3D', 'worldTransform3D', (lt) => ({\n\t\t\t\tx: lt.x, y: lt.y, z: lt.z,\n\t\t\t\trx: lt.rx, ry: lt.ry, rz: lt.rz,\n\t\t\t\tsx: lt.sx, sy: lt.sy, sz: lt.sz,\n\t\t\t}));\n\n\t\t\tconst orphanBuffer: Array<import('../../types').FilteredEntity<Transform3DComponentTypes, 'localTransform3D' | 'worldTransform3D'>> = [];\n\t\t\tconst hierarchyVisited = new Set<number>();\n\n\t\t\tworld\n\t\t\t\t.addSystem('transform3d-propagation')\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.setProcess(({ ecs }) => {\n\t\t\t\t\tpropagateTransforms3D(ecs, orphanBuffer, hierarchyVisited);\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Propagation ====================\n\n/**\n * Propagate 3D transforms through the hierarchy.\n * Parent-first traversal ensures parents are computed before children.\n */\nfunction propagateTransforms3D(\n\tecs: ECSpresso<WorldConfigFrom<Transform3DComponentTypes>>,\n\torphanBuffer: Array<import('../../types').FilteredEntity<Transform3DComponentTypes, 'localTransform3D' | 'worldTransform3D'>>,\n\thierarchyVisited: Set<number>,\n): void {\n\tconst em = ecs.entityManager;\n\n\t// Fast path: no hierarchy relationships exist — all entities are flat\n\tif (!em.hasHierarchy) {\n\t\tem.getEntitiesWithQueryInto(orphanBuffer, ['localTransform3D', 'worldTransform3D']);\n\t\tfor (const entity of orphanBuffer) {\n\t\t\tconst { localTransform3D, worldTransform3D } = entity.components;\n\t\t\tif (copyTransform3D(localTransform3D, worldTransform3D)) {\n\t\t\t\tecs.markChanged(entity.id, 'worldTransform3D');\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\n\t// Hierarchy exists — use parent-first traversal then process remaining orphans\n\thierarchyVisited.clear();\n\n\tecs.forEachInHierarchy((entityId, parentId) => {\n\t\thierarchyVisited.add(entityId);\n\t\tconst localTransform3D = em.getComponent(entityId, 'localTransform3D');\n\t\tconst worldTransform3D = em.getComponent(entityId, 'worldTransform3D');\n\n\t\tif (!localTransform3D || !worldTransform3D) return;\n\n\t\tconst parentWorld = parentId !== null\n\t\t\t? em.getComponent(parentId, 'worldTransform3D')\n\t\t\t: null;\n\n\t\tconst changed = parentWorld\n\t\t\t? combineTransforms3D(parentWorld, localTransform3D, worldTransform3D)\n\t\t\t: copyTransform3D(localTransform3D, worldTransform3D);\n\n\t\tif (changed) ecs.markChanged(entityId, 'worldTransform3D');\n\t});\n\n\tem.getEntitiesWithQueryInto(orphanBuffer, ['localTransform3D', 'worldTransform3D']);\n\tfor (const entity of orphanBuffer) {\n\t\tif (hierarchyVisited.has(entity.id)) continue;\n\t\tconst { localTransform3D, worldTransform3D } = entity.components;\n\t\tif (copyTransform3D(localTransform3D, worldTransform3D)) {\n\t\t\tecs.markChanged(entity.id, 'worldTransform3D');\n\t\t}\n\t}\n}\n\n/**\n * Copy 3D transform values from source to destination.\n * Returns true if the destination was actually modified.\n */\nfunction copyTransform3D(src: LocalTransform3D, dest: WorldTransform3D): boolean {\n\tif (\n\t\tdest.x === src.x && dest.y === src.y && dest.z === src.z &&\n\t\tdest.rx === src.rx && dest.ry === src.ry && dest.rz === src.rz &&\n\t\tdest.sx === src.sx && dest.sy === src.sy && dest.sz === src.sz\n\t) {\n\t\treturn false;\n\t}\n\tdest.x = src.x;\n\tdest.y = src.y;\n\tdest.z = src.z;\n\tdest.rx = src.rx;\n\tdest.ry = src.ry;\n\tdest.rz = src.rz;\n\tdest.sx = src.sx;\n\tdest.sy = src.sy;\n\tdest.sz = src.sz;\n\treturn true;\n}\n\n/**\n * Combine parent world transform with child local transform into child world transform.\n * Uses quaternion math internally for correct rotation composition.\n * Returns true if the destination was actually modified.\n */\nfunction combineTransforms3D(\n\tparent: WorldTransform3D,\n\tlocal: LocalTransform3D,\n\tworld: WorldTransform3D,\n): boolean {\n\t// Convert parent and local rotations to quaternions\n\teulerToQuat(parent.rx, parent.ry, parent.rz, qP);\n\teulerToQuat(local.rx, local.ry, local.rz, qL);\n\n\t// Compose rotations: worldQuat = parentQuat * localQuat\n\tquatMultiply(qP, qL, qW);\n\n\t// Convert back to Euler (writes into eulerOut scratch)\n\tconst euler = quatToEuler(qW);\n\n\t// Apply parent scale to local position, rotate by parent rotation (writes into vecOut scratch)\n\tconst rotated = quatRotateVec(qP, local.x * parent.sx, local.y * parent.sy, local.z * parent.sz);\n\n\t// Compute final world values\n\tconst newX = parent.x + rotated.x;\n\tconst newY = parent.y + rotated.y;\n\tconst newZ = parent.z + rotated.z;\n\tconst newSx = parent.sx * local.sx;\n\tconst newSy = parent.sy * local.sy;\n\tconst newSz = parent.sz * local.sz;\n\n\tif (\n\t\tworld.x === newX && world.y === newY && world.z === newZ &&\n\t\tworld.rx === euler.rx && world.ry === euler.ry && world.rz === euler.rz &&\n\t\tworld.sx === newSx && world.sy === newSy && world.sz === newSz\n\t) {\n\t\treturn false;\n\t}\n\n\tworld.x = newX;\n\tworld.y = newY;\n\tworld.z = newZ;\n\tworld.rx = euler.rx;\n\tworld.ry = euler.ry;\n\tworld.rz = euler.rz;\n\tworld.sx = newSx;\n\tworld.sy = newSy;\n\tworld.sz = newSz;\n\treturn true;\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "CB864A7BB0F7E5CA64756E2164756E21",
9
+ "names": []
10
+ }
@@ -41,6 +41,8 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
41
41
  private resourceDisposers;
42
42
  private initializedResourceKeys;
43
43
  private _changeSubscribers;
44
+ /** Shallow snapshots of observed resources, keyed by resource key */
45
+ private _observedSnapshots;
44
46
  /**
45
47
  * Add a resource to the manager.
46
48
  *
@@ -132,6 +134,15 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
132
134
  disposeResource<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): Promise<boolean>;
133
135
  /**
134
136
  * Subscribe to changes for a specific resource key.
137
+ *
138
+ * Subscribing marks the resource as "observed." Observed resources:
139
+ * - Are re-resolved each frame by `withResources` (no stale cache)
140
+ * - Are shallow-diffed at the end of each frame via `flushObserved()`,
141
+ * so in-place mutations are detected and subscribers notified
142
+ *
143
+ * When the last subscriber unsubscribes, the resource reverts to
144
+ * normal (cached, no per-frame diff).
145
+ *
135
146
  * @param key The resource key to watch
136
147
  * @param callback Function called with (newValue, oldValue) when the resource changes
137
148
  * @returns Unsubscribe function
@@ -145,6 +156,19 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
145
156
  * @param oldValue The previous resource value
146
157
  */
147
158
  notifyChange<K extends keyof ResourceTypes>(key: K, newValue: ResourceTypes[K], oldValue: ResourceTypes[K]): void;
159
+ /**
160
+ * Whether a resource has active change subscribers.
161
+ * Observed resources should not be cached by systems — they need
162
+ * to be re-resolved each frame so external mutations are visible.
163
+ */
164
+ isObserved<K extends keyof ResourceTypes>(key: K): boolean;
165
+ /**
166
+ * Diff all observed resources against their snapshots.
167
+ * Fires subscribers for any resource whose shallow properties changed
168
+ * since the last snapshot, then updates the snapshot.
169
+ * Call once per frame after all systems have run.
170
+ */
171
+ flushObserved(): void;
148
172
  /**
149
173
  * Dispose all initialized resources in reverse dependency order.
150
174
  * Resources that depend on others are disposed first.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Shared 2D vector math utilities for ECSpresso bundles.
2
+ * Shared vector math utilities for ECSpresso bundles.
3
3
  * All functions are pure — they return new vectors, never mutate inputs.
4
4
  */
5
5
  /**
@@ -65,3 +65,67 @@ export declare function vec2Distance(a: Vector2D, b: Vector2D): number;
65
65
  * Check if two vectors are approximately equal within an epsilon tolerance.
66
66
  */
67
67
  export declare function vec2Equals(a: Vector2D, b: Vector2D, epsilon?: number): boolean;
68
+ /**
69
+ * A 3D vector with x, y, and z components.
70
+ */
71
+ export interface Vector3D {
72
+ x: number;
73
+ y: number;
74
+ z: number;
75
+ }
76
+ /**
77
+ * Create a Vector3D from x, y, and z components.
78
+ */
79
+ export declare function vec3(x: number, y: number, z: number): Vector3D;
80
+ /**
81
+ * Return a zero vector {x: 0, y: 0, z: 0}.
82
+ */
83
+ export declare function vec3Zero(): Vector3D;
84
+ /**
85
+ * Add two vectors component-wise.
86
+ */
87
+ export declare function vec3Add(a: Vector3D, b: Vector3D): Vector3D;
88
+ /**
89
+ * Subtract b from a component-wise.
90
+ */
91
+ export declare function vec3Sub(a: Vector3D, b: Vector3D): Vector3D;
92
+ /**
93
+ * Scale a vector by a scalar.
94
+ */
95
+ export declare function vec3Scale(v: Vector3D, scalar: number): Vector3D;
96
+ /**
97
+ * Negate a vector (flip all components).
98
+ */
99
+ export declare function vec3Negate(v: Vector3D): Vector3D;
100
+ /**
101
+ * Compute the dot product of two vectors.
102
+ */
103
+ export declare function vec3Dot(a: Vector3D, b: Vector3D): number;
104
+ /**
105
+ * Compute the cross product of two vectors.
106
+ */
107
+ export declare function vec3Cross(a: Vector3D, b: Vector3D): Vector3D;
108
+ /**
109
+ * Compute the squared length of a vector. Avoids sqrt when only comparing magnitudes.
110
+ */
111
+ export declare function vec3LengthSq(v: Vector3D): number;
112
+ /**
113
+ * Compute the length (magnitude) of a vector.
114
+ */
115
+ export declare function vec3Length(v: Vector3D): number;
116
+ /**
117
+ * Return a unit vector in the same direction. Returns zero vector if input is zero-length.
118
+ */
119
+ export declare function vec3Normalize(v: Vector3D): Vector3D;
120
+ /**
121
+ * Compute the squared distance between two points. Avoids sqrt when only comparing.
122
+ */
123
+ export declare function vec3DistanceSq(a: Vector3D, b: Vector3D): number;
124
+ /**
125
+ * Compute the distance between two points.
126
+ */
127
+ export declare function vec3Distance(a: Vector3D, b: Vector3D): number;
128
+ /**
129
+ * Check if two vectors are approximately equal within an epsilon tolerance.
130
+ */
131
+ export declare function vec3Equals(a: Vector3D, b: Vector3D, epsilon?: number): boolean;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Shared Narrowphase Module — 3D
3
+ *
4
+ * Provides contact-computing narrowphase tests and a generic collision
5
+ * iteration pipeline for 3D collider pairs (AABB3D + Sphere).
6
+ *
7
+ * Mirrors the 2D narrowphase (`narrowphase.ts`) with an added Z axis.
8
+ */
9
+ import type { SpatialIndex3D } from './spatial-hash3D';
10
+ /**
11
+ * Contact result from a 3D narrowphase test. Normal points from A toward B.
12
+ *
13
+ * Narrowphase functions use this as an out-parameter: the caller owns the
14
+ * struct, the function writes fields in place and returns `true` on hit.
15
+ * The `onContact` callback in `detectCollisions3D` receives a shared module-
16
+ * level instance — **subscribers must consume it synchronously and must not
17
+ * retain the reference across frames**.
18
+ */
19
+ export interface Contact3D {
20
+ normalX: number;
21
+ normalY: number;
22
+ normalZ: number;
23
+ /** Penetration depth (positive = overlapping) */
24
+ depth: number;
25
+ }
26
+ /** Collider shape discriminator for the flattened BaseColliderInfo3D layout. */
27
+ export declare const AABB3D_SHAPE = 0;
28
+ export declare const SPHERE_SHAPE = 1;
29
+ export type ColliderShape3D = typeof AABB3D_SHAPE | typeof SPHERE_SHAPE;
30
+ /**
31
+ * Minimum collider data shared by 3D collision and physics bundles.
32
+ *
33
+ * Flat layout (no nested sub-objects): the `shape` discriminator tells you
34
+ * whether to read `halfWidth`/`halfHeight`/`halfDepth` (AABB3D) or `radius`
35
+ * (Sphere). Unused fields are set to 0.
36
+ *
37
+ * Pool-friendly — all fields are assigned in place each frame.
38
+ */
39
+ export interface BaseColliderInfo3D<L extends string = string> {
40
+ entityId: number;
41
+ x: number;
42
+ y: number;
43
+ z: number;
44
+ layer: L;
45
+ collidesWith: readonly L[];
46
+ shape: ColliderShape3D;
47
+ halfWidth: number;
48
+ halfHeight: number;
49
+ halfDepth: number;
50
+ radius: number;
51
+ }
52
+ /**
53
+ * Populate a `BaseColliderInfo3D` slot in place from raw component data.
54
+ * Returns `true` if the slot was filled, `false` if the entity has no
55
+ * collider (caller should skip it).
56
+ *
57
+ * If an entity has both AABB3D and sphere colliders, AABB3D wins and only
58
+ * the AABB3D offset is applied.
59
+ */
60
+ export declare function fillBaseColliderInfo3D<L extends string>(info: BaseColliderInfo3D<L>, entityId: number, x: number, y: number, z: number, layer: L, collidesWith: readonly L[], aabb3D: {
61
+ width: number;
62
+ height: number;
63
+ depth: number;
64
+ offsetX?: number;
65
+ offsetY?: number;
66
+ offsetZ?: number;
67
+ } | undefined, sphere: {
68
+ radius: number;
69
+ offsetX?: number;
70
+ offsetY?: number;
71
+ offsetZ?: number;
72
+ } | undefined): boolean;
73
+ /**
74
+ * Retrieve the optional spatialIndex3D resource, returning undefined when absent.
75
+ * Centralizes the cross-plugin typed lookup so individual plugins don't each
76
+ * need to import SpatialIndex3D or repeat the tryGetResource pattern.
77
+ */
78
+ export declare function tryGetSpatialIndex3D(tryGetResource: <T>(key: string) => T | undefined): SpatialIndex3D | undefined;
79
+ /**
80
+ * Write an AABB3D-vs-AABB3D contact into `out`. Returns `true` if the
81
+ * shapes overlap (out was filled), `false` otherwise.
82
+ *
83
+ * Resolves along the axis with minimum penetration depth.
84
+ */
85
+ export declare function computeAABB3DvsAABB3D(ax: number, ay: number, az: number, ahw: number, ahh: number, ahd: number, bx: number, by: number, bz: number, bhw: number, bhh: number, bhd: number, out: Contact3D): boolean;
86
+ /**
87
+ * Write a sphere-vs-sphere contact into `out`. Returns `true` if the
88
+ * spheres overlap.
89
+ */
90
+ export declare function computeSphereVsSphere(ax: number, ay: number, az: number, ar: number, bx: number, by: number, bz: number, br: number, out: Contact3D): boolean;
91
+ /**
92
+ * Write an AABB3D-vs-sphere contact into `out`. Returns `true` if the
93
+ * shapes overlap.
94
+ *
95
+ * Uses closest-point-on-AABB to sphere center. When the sphere center
96
+ * is inside the AABB, resolves along the axis with minimum push distance.
97
+ */
98
+ export declare function computeAABB3DvsSphere(aabbX: number, aabbY: number, aabbZ: number, ahw: number, ahh: number, ahd: number, sphereX: number, sphereY: number, sphereZ: number, radius: number, out: Contact3D): boolean;
99
+ /**
100
+ * Dispatch to the correct narrowphase function for the given pair and
101
+ * write the contact into `out`. Returns `true` if the pair overlaps.
102
+ */
103
+ export declare function computeContact3D(a: BaseColliderInfo3D, b: BaseColliderInfo3D, out: Contact3D): boolean;
104
+ /**
105
+ * Generic 3D collision detection pipeline: brute-force or broadphase,
106
+ * with layer filtering and contact computation.
107
+ *
108
+ * `count` is the number of live entries at the front of `colliders`.
109
+ * The array itself may be a grow-only pool — only indices `[0, count)`
110
+ * are iterated, so trailing pool slots are ignored.
111
+ *
112
+ * `workingMap` is a caller-owned `Map<number, I>` used by the broadphase
113
+ * path as an entityId → collider lookup. It is cleared and repopulated on
114
+ * each call; callers should allocate it once and pass the same instance
115
+ * every frame.
116
+ *
117
+ * Uses a context parameter forwarded to the callback to avoid
118
+ * per-frame closure allocation.
119
+ */
120
+ export declare function detectCollisions3D<I extends BaseColliderInfo3D, C>(colliders: I[], count: number, workingMap: Map<number, I>, spatialIndex: SpatialIndex3D | undefined, onContact: (a: I, b: I, contact: Contact3D, context: C) => void, context: C): void;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Spatial Hash Grid 3D
3
+ *
4
+ * Uniform-grid spatial hash for broadphase collision detection and
5
+ * proximity queries in 3D. Pure data structure, no ECS dependencies.
6
+ */
7
+ export interface SpatialEntry3D {
8
+ entityId: number;
9
+ x: number;
10
+ y: number;
11
+ z: number;
12
+ halfW: number;
13
+ halfH: number;
14
+ halfD: number;
15
+ }
16
+ export interface SpatialHashGrid3D {
17
+ cellSize: number;
18
+ invCellSize: number;
19
+ cells: Map<number, number[]>;
20
+ entries: Map<number, SpatialEntry3D>;
21
+ /** Previous-frame entries held for in-place reuse during rebuild. Internal. */
22
+ _entriesPrev: Map<number, SpatialEntry3D>;
23
+ }
24
+ /**
25
+ * Hash a cell coordinate triple to a single integer key.
26
+ * Uses large-prime XOR to distribute values.
27
+ */
28
+ export declare function hashCell3D(cx: number, cy: number, cz: number): number;
29
+ /**
30
+ * Create a new empty 3D spatial hash grid.
31
+ */
32
+ export declare function createGrid3D(cellSize: number): SpatialHashGrid3D;
33
+ /**
34
+ * Prepare the grid for a rebuild.
35
+ *
36
+ * Swaps `entries` with `_entriesPrev` so `insertEntity3D` can reuse existing
37
+ * `SpatialEntry3D` objects in place (steady-state rebuilds allocate zero
38
+ * entries). Any stale entries left in `_entriesPrev` from the previous
39
+ * rebuild are dropped here.
40
+ *
41
+ * Cell buckets are cleared in place — keys are retained so subsequent
42
+ * inserts hit the existing array rather than allocating a fresh one.
43
+ */
44
+ export declare function clearGrid3D(grid: SpatialHashGrid3D): void;
45
+ /**
46
+ * Insert an entity into all overlapping cells of the grid.
47
+ */
48
+ export declare function insertEntity3D(grid: SpatialHashGrid3D, entityId: number, x: number, y: number, z: number, halfW: number, halfH: number, halfD: number): void;
49
+ /**
50
+ * Collect entity IDs from all cells overlapping the given 3D box.
51
+ */
52
+ export declare function gridQueryBox3D(grid: SpatialHashGrid3D, minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result: Set<number>): void;
53
+ /**
54
+ * Collect entity IDs within a sphere. Uses box broadphase then
55
+ * 3D AABB-to-point distance filter.
56
+ */
57
+ export declare function gridQueryRadius3D(grid: SpatialHashGrid3D, cx: number, cy: number, cz: number, radius: number, result: Set<number>): void;
58
+ /**
59
+ * High-level spatial index API for 3D broadphase queries.
60
+ *
61
+ * Defined here (the utility layer) so that narrowphase3D can accept it
62
+ * without importing the ECS plugin. The spatial-index3D plugin creates
63
+ * an object that implements this interface and registers it as a resource.
64
+ */
65
+ export interface SpatialIndex3D {
66
+ readonly grid: SpatialHashGrid3D;
67
+ queryBox(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number[];
68
+ queryBoxInto(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result: Set<number>): void;
69
+ queryRadius(cx: number, cy: number, cz: number, radius: number): number[];
70
+ queryRadiusInto(cx: number, cy: number, cz: number, radius: number, result: Set<number>): void;
71
+ getEntry(entityId: number): SpatialEntry3D | undefined;
72
+ }