ecspresso 0.12.2 → 0.12.4

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.
@@ -1,4 +1,4 @@
1
- var{defineProperty:F,getOwnPropertyNames:M,getOwnPropertyDescriptor:N}=Object,O=Object.prototype.hasOwnProperty;function Q(j){return this[j]}var $=(j)=>{var k=(J??=new WeakMap).get(j),q;if(k)return k;if(k=F({},"__esModule",{value:!0}),j&&typeof j==="object"||typeof j==="function"){for(var v of M(j))if(!O.call(k,v))F(k,v,{get:Q.bind(j,v),enumerable:!(q=N(j,v))||q.enumerable})}return J.set(j,k),k},J;var S=(j)=>j;function U(j,k){this[j]=S.bind(null,k)}var A=(j,k)=>{for(var q in k)F(j,q,{get:k[q],enumerable:!0,configurable:!0,set:U.bind(k,q)})};var E=(j,k)=>()=>(j&&(k=j(j=0)),k);var G=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,q)=>(typeof require<"u"?require:k)[q]}):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 b={x:0,y:0,rotation:0,scaleX:1,scaleY:1},g={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function h(j,k){return{localTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function u(j,k){return{worldTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function W(j,k,q){let v=q?.scale??q?.scaleX??1,C=q?.scale??q?.scaleY??1,z=q?.rotation??0,B={x:j,y:k,rotation:z,scaleX:v,scaleY:C};return{localTransform:{...B},worldTransform:{...B}}}function L(j){let{systemGroup:k="transform",priority:q=500,phase:v="postUpdate"}=j??{};return V({id:"transform",install(C){C.registerRequired("localTransform","worldTransform",(z)=>({x:z.x,y:z.y,rotation:z.rotation,scaleX:z.scaleX,scaleY:z.scaleY})),C.addSystem("transform-propagation").setPriority(q).inPhase(v).inGroup(k).setProcess(({ecs:z})=>{Z(z)})}})}function Z(j){let k=j.entityManager;j.forEachInHierarchy((v,C)=>{let z=k.getComponent(v,"localTransform"),B=k.getComponent(v,"worldTransform");if(!z||!B)return;if(C===null)H(z,B);else{let D=k.getComponent(C,"worldTransform");if(D)_(D,z,B);else H(z,B)}j.markChanged(v,"worldTransform")});let q=j.getEntitiesWithQuery(["localTransform","worldTransform"]);for(let v of q)if(j.getParent(v.id)===null&&j.getChildren(v.id).length===0){let{localTransform:z,worldTransform:B}=v.components;H(z,B),j.markChanged(v.id,"worldTransform")}}function H(j,k){k.x=j.x,k.y=j.y,k.rotation=j.rotation,k.scaleX=j.scaleX,k.scaleY=j.scaleY}function _(j,k,q){let v=k.x*j.scaleX,C=k.y*j.scaleY,z=Math.cos(j.rotation),B=Math.sin(j.rotation),D=v*z-C*B,K=v*B+C*z;q.x=j.x+D,q.y=j.y+K,q.rotation=j.rotation+k.rotation,q.scaleX=j.scaleX*k.scaleX,q.scaleY=j.scaleY*k.scaleY}export{u as createWorldTransform,L as createTransformPlugin,W as createTransform,h as createLocalTransform,g as DEFAULT_WORLD_TRANSFORM,b as DEFAULT_LOCAL_TRANSFORM};
1
+ var{defineProperty:F,getOwnPropertyNames:M,getOwnPropertyDescriptor:N}=Object,O=Object.prototype.hasOwnProperty;function Q(j){return this[j]}var $=(j)=>{var k=(J??=new WeakMap).get(j),q;if(k)return k;if(k=F({},"__esModule",{value:!0}),j&&typeof j==="object"||typeof j==="function"){for(var v of M(j))if(!O.call(k,v))F(k,v,{get:Q.bind(j,v),enumerable:!(q=N(j,v))||q.enumerable})}return J.set(j,k),k},J;var S=(j)=>j;function U(j,k){this[j]=S.bind(null,k)}var A=(j,k)=>{for(var q in k)F(j,q,{get:k[q],enumerable:!0,configurable:!0,set:U.bind(k,q)})};var G=(j,k)=>()=>(j&&(k=j(j=0)),k);var P=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,q)=>(typeof require<"u"?require:k)[q]}):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 g={x:0,y:0,rotation:0,scaleX:1,scaleY:1},W={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function L(j,k){return{localTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function X(j,k){return{worldTransform:{x:j,y:k,rotation:0,scaleX:1,scaleY:1}}}function Y(j,k,q){let v=q?.scale??q?.scaleX??1,D=q?.scale??q?.scaleY??1,C=q?.rotation??0,z={x:j,y:k,rotation:C,scaleX:v,scaleY:D};return{localTransform:{...z},worldTransform:{...z}}}function B(j){let{systemGroup:k="transform",priority:q=500,phase:v="postUpdate"}=j??{};return V({id:"transform",install(D){D.registerRequired("localTransform","worldTransform",(z)=>({x:z.x,y:z.y,rotation:z.rotation,scaleX:z.scaleX,scaleY:z.scaleY}));let C=[];D.addSystem("transform-propagation").setPriority(q).inPhase(v).inGroup(k).setProcess(({ecs:z})=>{Z(z,C)})}})}function Z(j,k){let q=j.entityManager;j.forEachInHierarchy((v,D)=>{let C=q.getComponent(v,"localTransform"),z=q.getComponent(v,"worldTransform");if(!C||!z)return;if(D===null)H(C,z);else{let E=q.getComponent(D,"worldTransform");if(E)_(E,C,z);else H(C,z)}j.markChanged(v,"worldTransform")}),q.getEntitiesWithQueryInto(k,["localTransform","worldTransform"]);for(let v of k)if(j.getParent(v.id)===null&&j.getChildren(v.id).length===0){let{localTransform:C,worldTransform:z}=v.components;H(C,z),j.markChanged(v.id,"worldTransform")}}function H(j,k){k.x=j.x,k.y=j.y,k.rotation=j.rotation,k.scaleX=j.scaleX,k.scaleY=j.scaleY}function _(j,k,q){let v=k.x*j.scaleX,D=k.y*j.scaleY,C=Math.cos(j.rotation),z=Math.sin(j.rotation),E=v*C-D*z,K=v*z+D*C;q.x=j.x+E,q.y=j.y+K,q.rotation=j.rotation+k.rotation,q.scaleX=j.scaleX*k.scaleX,q.scaleY=j.scaleY*k.scaleY}export{X as createWorldTransform,B as createTransformPlugin,Y as createTransform,L as createLocalTransform,W as DEFAULT_WORLD_TRANSFORM,g as DEFAULT_LOCAL_TRANSFORM};
2
2
 
3
- //# debugId=C63DD170F03CB2AB64756E2164756E21
3
+ //# debugId=BCC1F248D392AD5C64756E2164756E21
4
4
  //# sourceMappingURL=transform.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/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 Plugin, type BasePluginOptions } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } 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): Plugin<WorldConfigFrom<TransformComponentTypes>, EmptyConfig, 'transform-propagation', G> {\n\tconst {\n\t\tsystemGroup = 'transform',\n\t\tpriority = 500,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin<WorldConfigFrom<TransformComponentTypes>, EmptyConfig, 'transform-propagation', G>({\n\t\tid: 'transform',\n\t\tinstall(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\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);\n\t\t\t\t});\n\t\t},\n\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(ecs: ECSpresso<WorldConfigFrom<TransformComponentTypes>>): 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\tconst orphanedEntities = ecs.getEntitiesWithQuery(['localTransform', 'worldTransform']);\n\tfor (const entity of orphanedEntities) {\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 Plugin, type BasePluginOptions } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } 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): Plugin<WorldConfigFrom<TransformComponentTypes>, EmptyConfig, 'transform-propagation', G> {\n\tconst {\n\t\tsystemGroup = 'transform',\n\t\tpriority = 500,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin<WorldConfigFrom<TransformComponentTypes>, EmptyConfig, 'transform-propagation', G>({\n\t\tid: 'transform',\n\t\tinstall(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\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"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "C63DD170F03CB2AB64756E2164756E21",
7
+ "mappings": "i0BASA,uBAAS,kBAiEF,IAAM,EAAoD,CAChE,EAAG,EACH,EAAG,EACH,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,EAKa,EAAoD,CAChE,EAAG,EACH,EAAG,EACH,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,EAoBO,SAAS,CAAoB,CAAC,EAAW,EAA4D,CAC3G,MAAO,CACN,eAAgB,CACf,IACA,IACA,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,CACD,EAWM,SAAS,CAAoB,CAAC,EAAW,EAA4D,CAC3G,MAAO,CACN,eAAgB,CACf,IACA,IACA,SAAU,EACV,OAAQ,EACR,OAAQ,CACT,CACD,EAqCM,SAAS,CAAe,CAC9B,EACA,EACA,EAC0B,CAC1B,IAAM,EAAS,GAAS,OAAS,GAAS,QAAU,EAC9C,EAAS,GAAS,OAAS,GAAS,QAAU,EAC9C,EAAW,GAAS,UAAY,EAEhC,EAAY,CACjB,IACA,IACA,WACA,SACA,QACD,EAEA,MAAO,CACN,eAAgB,IAAK,CAAU,EAC/B,eAAgB,IAAK,CAAU,CAChC,EA4BM,SAAS,CAAqD,CACpE,EAC4F,CAC5F,IACC,cAAc,YACd,WAAW,IACX,QAAQ,cACL,GAAW,CAAC,EAEhB,OAAO,EAAgG,CACtG,GAAI,YACJ,OAAO,CAAC,EAAO,CAEd,EAAM,iBAAiB,iBAAkB,iBAAkB,CAAC,KAAQ,CACnE,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,SAAU,EAAG,SAAU,OAAQ,EAAG,OAAQ,OAAQ,EAAG,MACxE,EAAE,EAEF,IAAM,EAAuH,CAAC,EAE9H,EACE,UAAU,uBAAuB,EACjC,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,WAAW,EAAG,SAAU,CACxB,EAAoB,EAAK,CAAY,EACrC,EAEJ,CAAC,EAYF,SAAS,CAAmB,CAC3B,EACA,EACO,CACP,IAAM,EAAK,EAAI,cAGf,EAAI,mBAAmB,CAAC,EAAU,IAAa,CAC9C,IAAM,EAAiB,EAAG,aAAa,EAAU,gBAAgB,EAC3D,EAAiB,EAAG,aAAa,EAAU,gBAAgB,EAEjE,GAAI,CAAC,GAAkB,CAAC,EAAgB,OAExC,GAAI,IAAa,KAEhB,EAAc,EAAgB,CAAc,EACtC,KAEN,IAAM,EAAc,EAAG,aAAa,EAAU,gBAAgB,EAC9D,GAAI,EACH,EAAkB,EAAa,EAAgB,CAAc,EAG7D,OAAc,EAAgB,CAAc,EAI9C,EAAI,YAAY,EAAU,gBAAgB,EAC1C,EAGD,EAAG,yBAAyB,EAAc,CAAC,iBAAkB,gBAAgB,CAAC,EAC9E,QAAW,KAAU,EAGpB,GAFiB,EAAI,UAAU,EAAO,EAAE,IAEvB,MAAQ,EAAI,YAAY,EAAO,EAAE,EAAE,SAAW,EAAG,CACjE,IAAQ,iBAAgB,kBAAmB,EAAO,WAClD,EAAc,EAAgB,CAAc,EAC5C,EAAI,YAAY,EAAO,GAAI,gBAAgB,GAQ9C,SAAS,CAAa,CAAC,EAAqB,EAA4B,CACvE,EAAK,EAAI,EAAI,EACb,EAAK,EAAI,EAAI,EACb,EAAK,SAAW,EAAI,SACpB,EAAK,OAAS,EAAI,OAClB,EAAK,OAAS,EAAI,OAMnB,SAAS,CAAiB,CACzB,EACA,EACA,EACO,CAEP,IAAM,EAAe,EAAM,EAAI,EAAO,OAChC,EAAe,EAAM,EAAI,EAAO,OAGhC,EAAM,KAAK,IAAI,EAAO,QAAQ,EAC9B,EAAM,KAAK,IAAI,EAAO,QAAQ,EAC9B,EAAW,EAAe,EAAM,EAAe,EAC/C,EAAW,EAAe,EAAM,EAAe,EAGrD,EAAM,EAAI,EAAO,EAAI,EACrB,EAAM,EAAI,EAAO,EAAI,EACrB,EAAM,SAAW,EAAO,SAAW,EAAM,SACzC,EAAM,OAAS,EAAO,OAAS,EAAM,OACrC,EAAM,OAAS,EAAO,OAAS,EAAM",
8
+ "debugId": "BCC1F248D392AD5C64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -40,6 +40,7 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
40
40
  private resourceDependencies;
41
41
  private resourceDisposers;
42
42
  private initializedResourceKeys;
43
+ private _changeSubscribers;
43
44
  /**
44
45
  * Add a resource to the manager.
45
46
  *
@@ -129,6 +130,21 @@ export default class ResourceManager<ResourceTypes extends Record<string, any> =
129
130
  * @returns True if the resource existed and was disposed, false if it didn't exist
130
131
  */
131
132
  disposeResource<K extends keyof ResourceTypes>(label: K, ...args: ContextArgs<Context>): Promise<boolean>;
133
+ /**
134
+ * Subscribe to changes for a specific resource key.
135
+ * @param key The resource key to watch
136
+ * @param callback Function called with (newValue, oldValue) when the resource changes
137
+ * @returns Unsubscribe function
138
+ */
139
+ onResourceChange<K extends keyof ResourceTypes>(key: K, callback: (newValue: ResourceTypes[K], oldValue: ResourceTypes[K]) => void): () => void;
140
+ /**
141
+ * Notify subscribers of a resource value change.
142
+ * Skips notification if the value is unchanged (via Object.is).
143
+ * @param key The resource key that changed
144
+ * @param newValue The new resource value
145
+ * @param oldValue The previous resource value
146
+ */
147
+ notifyChange<K extends keyof ResourceTypes>(key: K, newValue: ResourceTypes[K], oldValue: ResourceTypes[K]): void;
132
148
  /**
133
149
  * Dispose all initialized resources in reverse dependency order.
134
150
  * Resources that depend on others are disposed first.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecspresso",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",