ecspresso 0.18.0 → 0.19.1

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.
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/spatial/camera3D.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Camera 3D Plugin for ECSpresso\n *\n * Orbit/follow/shake camera controls for a Three.js PerspectiveCamera or\n * OrthographicCamera managed by renderer3D. Purely resource-based (no camera\n * entity). The renderer3D `camera` resource is the single camera target.\n * Orbit via pointer drag + scroll wheel, follow via entity tracking, shake\n * via trauma-based offsets.\n *\n * The plugin's `projection` option must match the underlying camera's kind;\n * a mismatch throws at init. State is a discriminated union — perspective\n * cameras expose `fov` / `setFov`, orthographic cameras expose `zoom` / `setZoom`.\n *\n * Import from 'ecspresso/plugins/spatial/camera3D'\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from '../../type-utils';\nimport type { Transform3DComponentTypes } from './transform3D';\nimport type { Renderer3DResourceTypes } from '../rendering/renderer3D';\nimport type { OrthographicCamera, PerspectiveCamera } from 'three';\n\n// ==================== Dependency Types ====================\n\ntype Camera3DRequiredConfig =\n\tComponentsConfig<Transform3DComponentTypes>\n\t& ResourcesConfig<Renderer3DResourceTypes>;\n\n// ==================== Resource Types ====================\n\nexport interface Camera3DFollowOptions {\n\tsmoothing?: number;\n\toffsetX?: number;\n\toffsetY?: number;\n\toffsetZ?: number;\n}\n\nexport interface Camera3DShakeOptions {\n\ttraumaDecay?: number;\n\tmaxOffsetX?: number;\n\tmaxOffsetY?: number;\n\tmaxOffsetZ?: number;\n}\n\nexport interface Camera3DBaseState {\n\t// Orbit / spherical state\n\ttargetX: number;\n\ttargetY: number;\n\ttargetZ: number;\n\tazimuth: number;\n\televation: number;\n\tdistance: number;\n\n\t// Follow\n\tfollowTarget: number;\n\tfollowSmoothing: number;\n\tfollowOffsetX: number;\n\tfollowOffsetY: number;\n\tfollowOffsetZ: number;\n\n\t// Shake (read by sync, written by shake system)\n\ttrauma: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeOffsetZ: number;\n\n\t// Mutation methods\n\tfollow(target: number | { id: number }, options?: Camera3DFollowOptions): void;\n\tunfollow(): void;\n\tsetTarget(x: number, y: number, z: number): void;\n\tsetOrbit(azimuth: number, elevation: number, distance: number): void;\n\tsetDistance(distance: number): void;\n\taddTrauma(amount: number): void;\n}\n\nexport interface PerspectiveCamera3DState extends Camera3DBaseState {\n\tprojection: 'perspective';\n\tfov: number;\n\tsetFov(fov: number): void;\n}\n\nexport interface OrthographicCamera3DState extends Camera3DBaseState {\n\tprojection: 'orthographic';\n\tzoom: number;\n\tsetZoom(zoom: number): void;\n}\n\nexport type Camera3DState = PerspectiveCamera3DState | OrthographicCamera3DState;\n\nexport interface Camera3DResourceTypes {\n\tcamera3DState: Camera3DState;\n}\n\nexport type Camera3DWorldConfig = ResourcesConfig<Camera3DResourceTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface Camera3DBasePluginOptions<G extends string = 'camera3d'> {\n\tsystemGroup?: G;\n\tphase?: SystemPhase;\n\n\t// Initial orbit state\n\tazimuth?: number;\n\televation?: number;\n\tdistance?: number;\n\ttarget?: { x: number; y: number; z: number };\n\n\t// Orbit constraints\n\tminDistance?: number;\n\tmaxDistance?: number;\n\tminElevation?: number;\n\tmaxElevation?: number;\n\n\t// Sensitivity\n\torbitSensitivity?: number;\n\tdollySensitivity?: number;\n\n\t// Disable pointer-drag orbit (zoom/dolly via wheel is unaffected)\n\tenableOrbit?: boolean;\n\n\t// Follow\n\tfollow?: Camera3DFollowOptions;\n\n\t// Shake\n\tshake?: boolean | Partial<Camera3DShakeOptions>;\n\n\t// Injectable RNG for deterministic shake\n\trandomFn?: () => number;\n}\n\nexport type Camera3DPluginOptions<G extends string = 'camera3d'> =\n\tCamera3DBasePluginOptions<G> & (\n\t\t| { projection?: 'perspective'; fov?: number }\n\t\t| { projection: 'orthographic'; zoom?: number }\n\t);\n\n// ==================== Labels ====================\n\nexport type Camera3DLabels =\n\t| 'camera3d-init'\n\t| 'camera3d-follow'\n\t| 'camera3d-shake'\n\t| 'camera3d-sync';\n\n// ==================== Constants ====================\n\nconst DEFAULT_FOLLOW: Readonly<Required<Camera3DFollowOptions>> = {\n\tsmoothing: 5,\n\toffsetX: 0,\n\toffsetY: 0,\n\toffsetZ: 0,\n};\n\nconst DEFAULT_SHAKE: Readonly<Required<Camera3DShakeOptions>> = {\n\ttraumaDecay: 1,\n\tmaxOffsetX: 0.3,\n\tmaxOffsetY: 0.3,\n\tmaxOffsetZ: 0.3,\n};\n\nconst HALF_PI = Math.PI / 2;\nconst ELEVATION_EPSILON = 0.001;\n\n// ==================== Scratch Objects ====================\n\nconst _camPos = { x: 0, y: 0, z: 0 };\n\n// ==================== Helpers ====================\n\nfunction clamp(value: number, min: number, max: number): number {\n\treturn Math.max(min, Math.min(max, value));\n}\n\nfunction resolveShakeOptions(config: true | Partial<Camera3DShakeOptions>): Required<Camera3DShakeOptions> {\n\tif (config === true) return { ...DEFAULT_SHAKE };\n\treturn {\n\t\ttraumaDecay: config.traumaDecay ?? DEFAULT_SHAKE.traumaDecay,\n\t\tmaxOffsetX: config.maxOffsetX ?? DEFAULT_SHAKE.maxOffsetX,\n\t\tmaxOffsetY: config.maxOffsetY ?? DEFAULT_SHAKE.maxOffsetY,\n\t\tmaxOffsetZ: config.maxOffsetZ ?? DEFAULT_SHAKE.maxOffsetZ,\n\t};\n}\n\n/**\n * Convert spherical coordinates to cartesian. Y-up convention (Three.js default).\n * Azimuth rotates in the XZ plane; elevation goes from XZ plane toward +Y.\n */\nexport function sphericalToCartesian(\n\tazimuth: number,\n\televation: number,\n\tdistance: number,\n\tout: { x: number; y: number; z: number },\n): void {\n\tconst cosElev = Math.cos(elevation);\n\tout.x = distance * cosElev * Math.sin(azimuth);\n\tout.y = distance * Math.sin(elevation);\n\tout.z = distance * cosElev * Math.cos(azimuth);\n}\n\n// ==================== Plugin Factory ====================\n\nexport function createCamera3DPlugin<G extends string = 'camera3d'>(\n\toptions?: Camera3DPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'camera3d',\n\t\tphase = 'postUpdate',\n\t\tazimuth: initialAzimuth = 0,\n\t\televation: initialElevation = 0.5,\n\t\tdistance: initialDistance = 10,\n\t\ttarget: initialTarget,\n\t\tminDistance = 1,\n\t\tmaxDistance = 100,\n\t\tminElevation = -HALF_PI + ELEVATION_EPSILON,\n\t\tmaxElevation = HALF_PI - ELEVATION_EPSILON,\n\t\torbitSensitivity = 0.003,\n\t\tdollySensitivity = 1.1,\n\t\tenableOrbit = true,\n\t\tfollow: followConfig,\n\t\tshake: shakeConfig,\n\t\trandomFn = Math.random,\n\t} = options ?? {};\n\n\tconst projection: 'perspective' | 'orthographic' = options?.projection ?? 'perspective';\n\tconst initialFov = options?.projection !== 'orthographic' ? (options?.fov ?? 75) : 75;\n\tconst initialZoom = options?.projection === 'orthographic' ? (options.zoom ?? 1) : 1;\n\n\tconst resolvedShake = shakeConfig ? resolveShakeOptions(shakeConfig) : DEFAULT_SHAKE;\n\tconst shakeDecay = resolvedShake.traumaDecay;\n\tconst shakeMaxX = resolvedShake.maxOffsetX;\n\tconst shakeMaxY = resolvedShake.maxOffsetY;\n\tconst shakeMaxZ = resolvedShake.maxOffsetZ;\n\n\t// Base fields + mutators shared between variants. Mutators use an explicit `this`\n\t// parameter so they type-check against `Camera3DBaseState` regardless of variant.\n\tconst baseFields = {\n\t\ttargetX: initialTarget?.x ?? 0,\n\t\ttargetY: initialTarget?.y ?? 0,\n\t\ttargetZ: initialTarget?.z ?? 0,\n\t\tazimuth: initialAzimuth,\n\t\televation: clamp(initialElevation, minElevation, maxElevation),\n\t\tdistance: clamp(initialDistance, minDistance, maxDistance),\n\n\t\tfollowTarget: -1,\n\t\tfollowSmoothing: followConfig?.smoothing ?? DEFAULT_FOLLOW.smoothing,\n\t\tfollowOffsetX: followConfig?.offsetX ?? DEFAULT_FOLLOW.offsetX,\n\t\tfollowOffsetY: followConfig?.offsetY ?? DEFAULT_FOLLOW.offsetY,\n\t\tfollowOffsetZ: followConfig?.offsetZ ?? DEFAULT_FOLLOW.offsetZ,\n\n\t\ttrauma: 0,\n\t\tshakeOffsetX: 0,\n\t\tshakeOffsetY: 0,\n\t\tshakeOffsetZ: 0,\n\t};\n\n\tconst baseMutators = {\n\t\tfollow(this: Camera3DBaseState, target: number | { id: number }, opts?: Camera3DFollowOptions) {\n\t\t\tconst targetId = typeof target === 'number' ? target : target.id;\n\t\t\tthis.followTarget = targetId;\n\t\t\tthis.followSmoothing = opts?.smoothing ?? followConfig?.smoothing ?? DEFAULT_FOLLOW.smoothing;\n\t\t\tthis.followOffsetX = opts?.offsetX ?? followConfig?.offsetX ?? DEFAULT_FOLLOW.offsetX;\n\t\t\tthis.followOffsetY = opts?.offsetY ?? followConfig?.offsetY ?? DEFAULT_FOLLOW.offsetY;\n\t\t\tthis.followOffsetZ = opts?.offsetZ ?? followConfig?.offsetZ ?? DEFAULT_FOLLOW.offsetZ;\n\t\t},\n\t\tunfollow(this: Camera3DBaseState) {\n\t\t\tthis.followTarget = -1;\n\t\t},\n\t\tsetTarget(this: Camera3DBaseState, x: number, y: number, z: number) {\n\t\t\tthis.targetX = x;\n\t\t\tthis.targetY = y;\n\t\t\tthis.targetZ = z;\n\t\t},\n\t\tsetOrbit(this: Camera3DBaseState, az: number, el: number, dist: number) {\n\t\t\tthis.azimuth = az;\n\t\t\tthis.elevation = clamp(el, minElevation, maxElevation);\n\t\t\tthis.distance = clamp(dist, minDistance, maxDistance);\n\t\t},\n\t\tsetDistance(this: Camera3DBaseState, d: number) {\n\t\t\tthis.distance = clamp(d, minDistance, maxDistance);\n\t\t},\n\t\taddTrauma(this: Camera3DBaseState, amount: number) {\n\t\t\tthis.trauma = clamp(this.trauma + amount, 0, 1);\n\t\t},\n\t};\n\n\treturn definePlugin('camera3d')\n\t\t.withResourceTypes<Camera3DResourceTypes>()\n\t\t.withLabels<Camera3DLabels>()\n\t\t.withGroups<G>()\n\t\t.requires<Camera3DRequiredConfig>()\n\t\t.install((world) => {\n\n\t\t\t// ==================== DOM State ====================\n\n\t\t\tconst drag = { active: false, prevX: 0, prevY: 0, pendingDolly: 0, el: null as HTMLElement | null };\n\n\t\t\t// ==================== Resource ====================\n\n\t\t\tconst variantFields = projection === 'orthographic'\n\t\t\t\t? {\n\t\t\t\t\tprojection: 'orthographic' as const,\n\t\t\t\t\tzoom: initialZoom,\n\t\t\t\t\tsetZoom(this: OrthographicCamera3DState, z: number) { this.zoom = z; },\n\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\tprojection: 'perspective' as const,\n\t\t\t\t\tfov: initialFov,\n\t\t\t\t\tsetFov(this: PerspectiveCamera3DState, f: number) { this.fov = f; },\n\t\t\t\t};\n\n\t\t\tconst state: Camera3DState = {\n\t\t\t\t...baseFields,\n\t\t\t\t...baseMutators,\n\t\t\t\t...variantFields,\n\t\t\t};\n\n\t\t\tworld.addResource('camera3DState', state);\n\n\t\t\t// ==================== DOM Handlers ====================\n\n\t\t\tfunction onPointerDown(e: PointerEvent) {\n\t\t\t\tdrag.active = true;\n\t\t\t\tdrag.prevX = e.clientX;\n\t\t\t\tdrag.prevY = e.clientY;\n\t\t\t\tdrag.el?.setPointerCapture(e.pointerId);\n\t\t\t}\n\n\t\t\tfunction onPointerMove(e: PointerEvent) {\n\t\t\t\tif (!drag.active) return;\n\t\t\t\tconst deltaX = e.clientX - drag.prevX;\n\t\t\t\tconst deltaY = e.clientY - drag.prevY;\n\t\t\t\tdrag.prevX = e.clientX;\n\t\t\t\tdrag.prevY = e.clientY;\n\n\t\t\t\tstate.azimuth -= deltaX * orbitSensitivity;\n\t\t\t\tstate.elevation = clamp(\n\t\t\t\t\tstate.elevation + deltaY * orbitSensitivity,\n\t\t\t\t\tminElevation,\n\t\t\t\t\tmaxElevation,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tfunction onPointerUp(e: PointerEvent) {\n\t\t\t\tdrag.active = false;\n\t\t\t\tdrag.el?.releasePointerCapture(e.pointerId);\n\t\t\t}\n\n\t\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t\te.preventDefault();\n\t\t\t\tdrag.pendingDolly += Math.sign(e.deltaY);\n\t\t\t}\n\n\t\t\t// ==================== Init System ====================\n\n\t\t\t// Camera ref cached once at init — never changes at runtime\n\t\t\tlet cachedCamera: Renderer3DResourceTypes['camera'] | null = null;\n\t\t\tlet cachedPerspCamera: PerspectiveCamera | null = null;\n\t\t\tlet cachedOrthoCamera: OrthographicCamera | null = null;\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tconst threeRenderer = ecs.getResource('threeRenderer');\n\t\t\t\t\tcachedCamera = ecs.getResource('camera');\n\n\t\t\t\t\t// Narrow to the concrete camera variant once\n\t\t\t\t\tif ((cachedCamera as PerspectiveCamera).isPerspectiveCamera) {\n\t\t\t\t\t\tcachedPerspCamera = cachedCamera as PerspectiveCamera;\n\t\t\t\t\t} else if ((cachedCamera as OrthographicCamera).isOrthographicCamera) {\n\t\t\t\t\t\tcachedOrthoCamera = cachedCamera as OrthographicCamera;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Guard: plugin `projection` option must match the resolved camera kind\n\t\t\t\t\tif (state.projection === 'perspective' && !cachedPerspCamera) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'createCamera3DPlugin: configured as \\'perspective\\' but the renderer\\'s camera is not a PerspectiveCamera.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (state.projection === 'orthographic' && !cachedOrthoCamera) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'createCamera3DPlugin: configured as \\'orthographic\\' but the renderer\\'s camera is not an OrthographicCamera.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Sync initial variant-specific value from the actual camera\n\t\t\t\t\tif (state.projection === 'perspective' && cachedPerspCamera) {\n\t\t\t\t\t\tstate.fov = cachedPerspCamera.fov;\n\t\t\t\t\t} else if (state.projection === 'orthographic' && cachedOrthoCamera) {\n\t\t\t\t\t\tstate.zoom = cachedOrthoCamera.zoom;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Attach DOM listeners\n\t\t\t\t\tdrag.el = threeRenderer.domElement;\n\t\t\t\t\tif (enableOrbit) {\n\t\t\t\t\t\tdrag.el.addEventListener('pointerdown', onPointerDown);\n\t\t\t\t\t\tdrag.el.addEventListener('pointermove', onPointerMove);\n\t\t\t\t\t\tdrag.el.addEventListener('pointerup', onPointerUp);\n\t\t\t\t\t}\n\t\t\t\t\tdrag.el.addEventListener('wheel', onWheel as EventListener, { passive: false });\n\n\t\t\t\t\t// Initial camera position sync\n\t\t\t\t\tsphericalToCartesian(state.azimuth, state.elevation, state.distance, _camPos);\n\t\t\t\t\tcachedCamera.position.set(\n\t\t\t\t\t\tstate.targetX + _camPos.x,\n\t\t\t\t\t\tstate.targetY + _camPos.y,\n\t\t\t\t\t\tstate.targetZ + _camPos.z,\n\t\t\t\t\t);\n\t\t\t\t\tcachedCamera.lookAt(state.targetX, state.targetY, state.targetZ);\n\t\t\t\t})\n\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\tif (!drag.el) return;\n\t\t\t\t\tif (enableOrbit) {\n\t\t\t\t\t\tdrag.el.removeEventListener('pointerdown', onPointerDown);\n\t\t\t\t\t\tdrag.el.removeEventListener('pointermove', onPointerMove);\n\t\t\t\t\t\tdrag.el.removeEventListener('pointerup', onPointerUp);\n\t\t\t\t\t}\n\t\t\t\t\tdrag.el.removeEventListener('wheel', onWheel as EventListener);\n\t\t\t\t\tdrag.el = null;\n\t\t\t\t\tcachedCamera = null;\n\t\t\t\t\tcachedPerspCamera = null;\n\t\t\t\t\tcachedOrthoCamera = null;\n\t\t\t\t});\n\n\t\t\t// ==================== Follow System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-follow')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ ecs, dt }) => {\n\t\t\t\t\tif (state.followTarget < 0) return;\n\n\t\t\t\t\tif (!ecs.getEntity(state.followTarget)) {\n\t\t\t\t\t\tstate.followTarget = -1;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst worldTransform = ecs.getComponent(state.followTarget, 'worldTransform3D');\n\t\t\t\t\tif (!worldTransform) return;\n\n\t\t\t\t\tconst goalX = worldTransform.x + state.followOffsetX;\n\t\t\t\t\tconst goalY = worldTransform.y + state.followOffsetY;\n\t\t\t\t\tconst goalZ = worldTransform.z + state.followOffsetZ;\n\n\t\t\t\t\tconst factor = Math.min(1, state.followSmoothing * dt);\n\t\t\t\t\tstate.targetX += (goalX - state.targetX) * factor;\n\t\t\t\t\tstate.targetY += (goalY - state.targetY) * factor;\n\t\t\t\t\tstate.targetZ += (goalZ - state.targetZ) * factor;\n\t\t\t\t});\n\n\t\t\t// ==================== Shake System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-shake')\n\t\t\t\t.setPriority(390)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ dt }) => {\n\t\t\t\t\tif (state.trauma <= 0) {\n\t\t\t\t\t\tstate.shakeOffsetX = 0;\n\t\t\t\t\t\tstate.shakeOffsetY = 0;\n\t\t\t\t\t\tstate.shakeOffsetZ = 0;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tstate.trauma = Math.max(0, state.trauma - shakeDecay * dt);\n\n\t\t\t\t\tconst intensity = state.trauma * state.trauma;\n\t\t\t\t\tstate.shakeOffsetX = shakeMaxX * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\tstate.shakeOffsetY = shakeMaxY * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\tstate.shakeOffsetZ = shakeMaxZ * intensity * (randomFn() * 2 - 1);\n\t\t\t\t});\n\n\t\t\t// ==================== Sync System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-sync')\n\t\t\t\t.setPriority(380)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\tif (!cachedCamera) return;\n\n\t\t\t\t\t// Process pending dolly\n\t\t\t\t\tif (drag.pendingDolly !== 0) {\n\t\t\t\t\t\tstate.distance = clamp(\n\t\t\t\t\t\t\tstate.distance * Math.pow(dollySensitivity, drag.pendingDolly),\n\t\t\t\t\t\t\tminDistance,\n\t\t\t\t\t\t\tmaxDistance,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tdrag.pendingDolly = 0;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Compute camera position from spherical coords. Shake is applied as a\n\t\t\t\t\t// pure view translation — both position and lookAt target shift by the\n\t\t\t\t\t// same offset so the view pans instead of rotating. This keeps the effect\n\t\t\t\t\t// visible under orthographic projection (which has no parallax) and also\n\t\t\t\t\t// makes perspective shake magnitudes feel consistent regardless of distance.\n\t\t\t\t\tsphericalToCartesian(state.azimuth, state.elevation, state.distance, _camPos);\n\t\t\t\t\tcachedCamera.position.set(\n\t\t\t\t\t\tstate.targetX + _camPos.x + state.shakeOffsetX,\n\t\t\t\t\t\tstate.targetY + _camPos.y + state.shakeOffsetY,\n\t\t\t\t\t\tstate.targetZ + _camPos.z + state.shakeOffsetZ,\n\t\t\t\t\t);\n\t\t\t\t\tcachedCamera.lookAt(\n\t\t\t\t\t\tstate.targetX + state.shakeOffsetX,\n\t\t\t\t\t\tstate.targetY + state.shakeOffsetY,\n\t\t\t\t\t\tstate.targetZ + state.shakeOffsetZ,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (state.projection === 'perspective' && cachedPerspCamera && cachedPerspCamera.fov !== state.fov) {\n\t\t\t\t\t\tcachedPerspCamera.fov = state.fov;\n\t\t\t\t\t\tcachedPerspCamera.updateProjectionMatrix();\n\t\t\t\t\t} else if (state.projection === 'orthographic' && cachedOrthoCamera && cachedOrthoCamera.zoom !== state.zoom) {\n\t\t\t\t\t\tcachedOrthoCamera.zoom = state.zoom;\n\t\t\t\t\t\tcachedOrthoCamera.updateProjectionMatrix();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
5
+ "/**\n * Camera 3D Plugin for ECSpresso\n *\n * Orbit/follow/shake camera controls for a Three.js PerspectiveCamera or\n * OrthographicCamera managed by renderer3D. Purely resource-based (no camera\n * entity). The renderer3D `camera` resource is the single camera target.\n * Orbit via pointer drag + scroll wheel, follow via entity tracking, shake\n * via trauma-based offsets.\n *\n * The plugin's `projection` option must match the underlying camera's kind;\n * a mismatch throws at init. State is a discriminated union — perspective\n * cameras expose `fov` / `setFov`, orthographic cameras expose `zoom` / `setZoom`.\n *\n * Import from 'ecspresso/plugins/spatial/camera3D'\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from '../../type-utils';\nimport type { Transform3DComponentTypes } from './transform3D';\nimport type { Renderer3DResourceTypes } from '../rendering/renderer3D';\nimport type { OrthographicCamera, PerspectiveCamera } from 'three';\n\n// ==================== Dependency Types ====================\n\ntype Camera3DRequiredConfig =\n\tComponentsConfig<Transform3DComponentTypes>\n\t& ResourcesConfig<Renderer3DResourceTypes>;\n\n// ==================== Resource Types ====================\n\nexport interface Camera3DFollowOptions {\n\tsmoothing?: number;\n\toffsetX?: number;\n\toffsetY?: number;\n\toffsetZ?: number;\n}\n\nexport interface Camera3DShakeOptions {\n\ttraumaDecay?: number;\n\tmaxOffsetX?: number;\n\tmaxOffsetY?: number;\n\tmaxOffsetZ?: number;\n}\n\nexport interface Camera3DBaseState {\n\t// Orbit / spherical state\n\ttargetX: number;\n\ttargetY: number;\n\ttargetZ: number;\n\tazimuth: number;\n\televation: number;\n\tdistance: number;\n\n\t// Follow\n\tfollowTarget: number;\n\tfollowSmoothing: number;\n\tfollowOffsetX: number;\n\tfollowOffsetY: number;\n\tfollowOffsetZ: number;\n\n\t// Shake (read by sync, written by shake system)\n\ttrauma: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeOffsetZ: number;\n\n\t// Mutation methods\n\tfollow(target: number | { id: number }, options?: Camera3DFollowOptions): void;\n\tunfollow(): void;\n\tsetTarget(x: number, y: number, z: number): void;\n\tsetOrbit(azimuth: number, elevation: number, distance: number): void;\n\tsetDistance(distance: number): void;\n\taddTrauma(amount: number): void;\n}\n\nexport interface PerspectiveCamera3DState extends Camera3DBaseState {\n\tprojection: 'perspective';\n\tfov: number;\n\tsetFov(fov: number): void;\n}\n\nexport interface OrthographicCamera3DState extends Camera3DBaseState {\n\tprojection: 'orthographic';\n\tzoom: number;\n\tsetZoom(zoom: number): void;\n}\n\nexport type Camera3DState = PerspectiveCamera3DState | OrthographicCamera3DState;\n\nexport interface Camera3DResourceTypes {\n\tcamera3DState: Camera3DState;\n}\n\nexport type Camera3DWorldConfig = ResourcesConfig<Camera3DResourceTypes>;\ntype Camera3DWheelMode = 'auto' | 'distance' | 'zoom' | 'disabled';\ntype ResolvedCamera3DWheelMode = Exclude<Camera3DWheelMode, 'auto'>;\n\n// ==================== Plugin Options ====================\n\nexport interface Camera3DBasePluginOptions<G extends string = 'camera3d'> {\n\tsystemGroup?: G;\n\tphase?: SystemPhase;\n\n\t// Initial orbit state\n\tazimuth?: number;\n\televation?: number;\n\tdistance?: number;\n\ttarget?: { x: number; y: number; z: number };\n\n\t// Orbit constraints\n\tminDistance?: number;\n\tmaxDistance?: number;\n\tminElevation?: number;\n\tmaxElevation?: number;\n\n\t// Sensitivity\n\torbitSensitivity?: number;\n\tdollySensitivity?: number;\n\twheelMode?: Camera3DWheelMode;\n\n\t// Disable pointer-drag orbit. Wheel input is controlled separately by wheelMode.\n\tenableOrbit?: boolean;\n\n\t// Follow\n\tfollow?: Camera3DFollowOptions;\n\n\t// Shake\n\tshake?: boolean | Partial<Camera3DShakeOptions>;\n\n\t// Injectable RNG for deterministic shake\n\trandomFn?: () => number;\n}\n\nexport type Camera3DPluginOptions<G extends string = 'camera3d'> =\n\tCamera3DBasePluginOptions<G> & (\n\t\t| { projection?: 'perspective'; fov?: number }\n\t\t| { projection: 'orthographic'; zoom?: number }\n\t);\n\n// ==================== Labels ====================\n\nexport type Camera3DLabels =\n\t| 'camera3d-init'\n\t| 'camera3d-follow'\n\t| 'camera3d-shake'\n\t| 'camera3d-sync';\n\n// ==================== Constants ====================\n\nconst DEFAULT_FOLLOW: Readonly<Required<Camera3DFollowOptions>> = {\n\tsmoothing: 5,\n\toffsetX: 0,\n\toffsetY: 0,\n\toffsetZ: 0,\n};\n\nconst DEFAULT_SHAKE: Readonly<Required<Camera3DShakeOptions>> = {\n\ttraumaDecay: 1,\n\tmaxOffsetX: 0.3,\n\tmaxOffsetY: 0.3,\n\tmaxOffsetZ: 0.3,\n};\n\nconst HALF_PI = Math.PI / 2;\nconst ELEVATION_EPSILON = 0.001;\nconst MIN_FOV = 1;\nconst MAX_FOV = 179;\n\n// ==================== Scratch Objects ====================\n\nconst _camPos = { x: 0, y: 0, z: 0 };\n\n// ==================== Helpers ====================\n\nfunction clamp(value: number, min: number, max: number): number {\n\treturn Math.max(min, Math.min(max, value));\n}\n\nfunction resolveWheelMode(mode: Camera3DWheelMode, projection: 'perspective' | 'orthographic'): ResolvedCamera3DWheelMode {\n\treturn mode === 'auto' ? (projection === 'orthographic' ? 'zoom' : 'distance') : mode;\n}\n\nfunction applyDistanceWheel(state: Camera3DState, wheelFactor: number): void {\n\tstate.setDistance(state.distance * wheelFactor);\n}\n\nfunction applyZoomWheel(state: Camera3DState, wheelFactor: number): void {\n\tif (state.projection === 'orthographic') {\n\t\tstate.setZoom(state.zoom / wheelFactor);\n\t\treturn;\n\t}\n\tstate.setFov(clamp(state.fov * wheelFactor, MIN_FOV, MAX_FOV));\n}\n\nconst wheelHandlers = {\n\tdistance: applyDistanceWheel,\n\tzoom: applyZoomWheel,\n\tdisabled: () => {},\n} satisfies Record<ResolvedCamera3DWheelMode, (state: Camera3DState, wheelFactor: number) => void>;\n\nfunction resolveShakeOptions(config: true | Partial<Camera3DShakeOptions>): Required<Camera3DShakeOptions> {\n\tif (config === true) return { ...DEFAULT_SHAKE };\n\treturn {\n\t\ttraumaDecay: config.traumaDecay ?? DEFAULT_SHAKE.traumaDecay,\n\t\tmaxOffsetX: config.maxOffsetX ?? DEFAULT_SHAKE.maxOffsetX,\n\t\tmaxOffsetY: config.maxOffsetY ?? DEFAULT_SHAKE.maxOffsetY,\n\t\tmaxOffsetZ: config.maxOffsetZ ?? DEFAULT_SHAKE.maxOffsetZ,\n\t};\n}\n\n/**\n * Convert spherical coordinates to cartesian. Y-up convention (Three.js default).\n * Azimuth rotates in the XZ plane; elevation goes from XZ plane toward +Y.\n */\nexport function sphericalToCartesian(\n\tazimuth: number,\n\televation: number,\n\tdistance: number,\n\tout: { x: number; y: number; z: number },\n): void {\n\tconst cosElev = Math.cos(elevation);\n\tout.x = distance * cosElev * Math.sin(azimuth);\n\tout.y = distance * Math.sin(elevation);\n\tout.z = distance * cosElev * Math.cos(azimuth);\n}\n\n// ==================== Plugin Factory ====================\n\nexport function createCamera3DPlugin<G extends string = 'camera3d'>(\n\toptions?: Camera3DPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'camera3d',\n\t\tphase = 'postUpdate',\n\t\tazimuth: initialAzimuth = 0,\n\t\televation: initialElevation = 0.5,\n\t\tdistance: initialDistance = 10,\n\t\ttarget: initialTarget,\n\t\tminDistance = 1,\n\t\tmaxDistance = 100,\n\t\tminElevation = -HALF_PI + ELEVATION_EPSILON,\n\t\tmaxElevation = HALF_PI - ELEVATION_EPSILON,\n\t\torbitSensitivity = 0.003,\n\t\tdollySensitivity = 1.1,\n\t\twheelMode = 'auto',\n\t\tenableOrbit = true,\n\t\tfollow: followConfig,\n\t\tshake: shakeConfig,\n\t\trandomFn = Math.random,\n\t} = options ?? {};\n\n\tconst projection: 'perspective' | 'orthographic' = options?.projection ?? 'perspective';\n\tconst initialFov = options?.projection !== 'orthographic' ? (options?.fov ?? 75) : 75;\n\tconst initialZoom = options?.projection === 'orthographic' ? (options.zoom ?? 1) : 1;\n\tconst resolvedWheelMode = resolveWheelMode(wheelMode, projection);\n\n\tconst resolvedShake = shakeConfig ? resolveShakeOptions(shakeConfig) : DEFAULT_SHAKE;\n\tconst shakeDecay = resolvedShake.traumaDecay;\n\tconst shakeMaxX = resolvedShake.maxOffsetX;\n\tconst shakeMaxY = resolvedShake.maxOffsetY;\n\tconst shakeMaxZ = resolvedShake.maxOffsetZ;\n\n\t// Base fields + mutators shared between variants. Mutators use an explicit `this`\n\t// parameter so they type-check against `Camera3DBaseState` regardless of variant.\n\tconst baseFields = {\n\t\ttargetX: initialTarget?.x ?? 0,\n\t\ttargetY: initialTarget?.y ?? 0,\n\t\ttargetZ: initialTarget?.z ?? 0,\n\t\tazimuth: initialAzimuth,\n\t\televation: clamp(initialElevation, minElevation, maxElevation),\n\t\tdistance: clamp(initialDistance, minDistance, maxDistance),\n\n\t\tfollowTarget: -1,\n\t\tfollowSmoothing: followConfig?.smoothing ?? DEFAULT_FOLLOW.smoothing,\n\t\tfollowOffsetX: followConfig?.offsetX ?? DEFAULT_FOLLOW.offsetX,\n\t\tfollowOffsetY: followConfig?.offsetY ?? DEFAULT_FOLLOW.offsetY,\n\t\tfollowOffsetZ: followConfig?.offsetZ ?? DEFAULT_FOLLOW.offsetZ,\n\n\t\ttrauma: 0,\n\t\tshakeOffsetX: 0,\n\t\tshakeOffsetY: 0,\n\t\tshakeOffsetZ: 0,\n\t};\n\n\tconst baseMutators = {\n\t\tfollow(this: Camera3DBaseState, target: number | { id: number }, opts?: Camera3DFollowOptions) {\n\t\t\tconst targetId = typeof target === 'number' ? target : target.id;\n\t\t\tthis.followTarget = targetId;\n\t\t\tthis.followSmoothing = opts?.smoothing ?? followConfig?.smoothing ?? DEFAULT_FOLLOW.smoothing;\n\t\t\tthis.followOffsetX = opts?.offsetX ?? followConfig?.offsetX ?? DEFAULT_FOLLOW.offsetX;\n\t\t\tthis.followOffsetY = opts?.offsetY ?? followConfig?.offsetY ?? DEFAULT_FOLLOW.offsetY;\n\t\t\tthis.followOffsetZ = opts?.offsetZ ?? followConfig?.offsetZ ?? DEFAULT_FOLLOW.offsetZ;\n\t\t},\n\t\tunfollow(this: Camera3DBaseState) {\n\t\t\tthis.followTarget = -1;\n\t\t},\n\t\tsetTarget(this: Camera3DBaseState, x: number, y: number, z: number) {\n\t\t\tthis.targetX = x;\n\t\t\tthis.targetY = y;\n\t\t\tthis.targetZ = z;\n\t\t},\n\t\tsetOrbit(this: Camera3DBaseState, az: number, el: number, dist: number) {\n\t\t\tthis.azimuth = az;\n\t\t\tthis.elevation = clamp(el, minElevation, maxElevation);\n\t\t\tthis.distance = clamp(dist, minDistance, maxDistance);\n\t\t},\n\t\tsetDistance(this: Camera3DBaseState, d: number) {\n\t\t\tthis.distance = clamp(d, minDistance, maxDistance);\n\t\t},\n\t\taddTrauma(this: Camera3DBaseState, amount: number) {\n\t\t\tthis.trauma = clamp(this.trauma + amount, 0, 1);\n\t\t},\n\t};\n\n\treturn definePlugin('camera3d')\n\t\t.withResourceTypes<Camera3DResourceTypes>()\n\t\t.withLabels<Camera3DLabels>()\n\t\t.withGroups<G>()\n\t\t.requires<Camera3DRequiredConfig>()\n\t\t.install((world) => {\n\n\t\t\t// ==================== DOM State ====================\n\n\t\t\tconst drag = { active: false, prevX: 0, prevY: 0, pendingWheel: 0, el: null as HTMLElement | null };\n\n\t\t\t// ==================== Resource ====================\n\n\t\t\tconst variantFields = projection === 'orthographic'\n\t\t\t\t? {\n\t\t\t\t\tprojection: 'orthographic' as const,\n\t\t\t\t\tzoom: initialZoom,\n\t\t\t\t\tsetZoom(this: OrthographicCamera3DState, z: number) { this.zoom = z; },\n\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\tprojection: 'perspective' as const,\n\t\t\t\t\tfov: initialFov,\n\t\t\t\t\tsetFov(this: PerspectiveCamera3DState, f: number) { this.fov = f; },\n\t\t\t\t};\n\n\t\t\tconst state: Camera3DState = {\n\t\t\t\t...baseFields,\n\t\t\t\t...baseMutators,\n\t\t\t\t...variantFields,\n\t\t\t};\n\n\t\t\tworld.addResource('camera3DState', state);\n\n\t\t\t// ==================== DOM Handlers ====================\n\n\t\t\tfunction onPointerDown(e: PointerEvent) {\n\t\t\t\tdrag.active = true;\n\t\t\t\tdrag.prevX = e.clientX;\n\t\t\t\tdrag.prevY = e.clientY;\n\t\t\t\tdrag.el?.setPointerCapture(e.pointerId);\n\t\t\t}\n\n\t\t\tfunction onPointerMove(e: PointerEvent) {\n\t\t\t\tif (!drag.active) return;\n\t\t\t\tconst deltaX = e.clientX - drag.prevX;\n\t\t\t\tconst deltaY = e.clientY - drag.prevY;\n\t\t\t\tdrag.prevX = e.clientX;\n\t\t\t\tdrag.prevY = e.clientY;\n\n\t\t\t\tstate.azimuth -= deltaX * orbitSensitivity;\n\t\t\t\tstate.elevation = clamp(\n\t\t\t\t\tstate.elevation + deltaY * orbitSensitivity,\n\t\t\t\t\tminElevation,\n\t\t\t\t\tmaxElevation,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tfunction onPointerUp(e: PointerEvent) {\n\t\t\t\tdrag.active = false;\n\t\t\t\tdrag.el?.releasePointerCapture(e.pointerId);\n\t\t\t}\n\n\t\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t\te.preventDefault();\n\t\t\t\tdrag.pendingWheel += Math.sign(e.deltaY);\n\t\t\t}\n\n\t\t\t// ==================== Init System ====================\n\n\t\t\t// Camera ref cached once at init — never changes at runtime\n\t\t\tlet cachedCamera: Renderer3DResourceTypes['camera'] | null = null;\n\t\t\tlet cachedPerspCamera: PerspectiveCamera | null = null;\n\t\t\tlet cachedOrthoCamera: OrthographicCamera | null = null;\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tconst threeRenderer = ecs.getResource('threeRenderer');\n\t\t\t\t\tcachedCamera = ecs.getResource('camera');\n\n\t\t\t\t\t// Narrow to the concrete camera variant once\n\t\t\t\t\tif ((cachedCamera as PerspectiveCamera).isPerspectiveCamera) {\n\t\t\t\t\t\tcachedPerspCamera = cachedCamera as PerspectiveCamera;\n\t\t\t\t\t} else if ((cachedCamera as OrthographicCamera).isOrthographicCamera) {\n\t\t\t\t\t\tcachedOrthoCamera = cachedCamera as OrthographicCamera;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Guard: plugin `projection` option must match the resolved camera kind\n\t\t\t\t\tif (state.projection === 'perspective' && !cachedPerspCamera) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'createCamera3DPlugin: configured as \\'perspective\\' but the renderer\\'s camera is not a PerspectiveCamera.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (state.projection === 'orthographic' && !cachedOrthoCamera) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'createCamera3DPlugin: configured as \\'orthographic\\' but the renderer\\'s camera is not an OrthographicCamera.',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Sync initial variant-specific value from the actual camera\n\t\t\t\t\tif (state.projection === 'perspective' && cachedPerspCamera) {\n\t\t\t\t\t\tstate.fov = cachedPerspCamera.fov;\n\t\t\t\t\t} else if (state.projection === 'orthographic' && cachedOrthoCamera) {\n\t\t\t\t\t\tstate.zoom = cachedOrthoCamera.zoom;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Attach DOM listeners\n\t\t\t\t\tdrag.el = threeRenderer.domElement;\n\t\t\t\t\tif (enableOrbit) {\n\t\t\t\t\t\tdrag.el.addEventListener('pointerdown', onPointerDown);\n\t\t\t\t\t\tdrag.el.addEventListener('pointermove', onPointerMove);\n\t\t\t\t\t\tdrag.el.addEventListener('pointerup', onPointerUp);\n\t\t\t\t\t}\n\t\t\t\t\tif (resolvedWheelMode !== 'disabled') {\n\t\t\t\t\t\tdrag.el.addEventListener('wheel', onWheel as EventListener, { passive: false });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Initial camera position sync\n\t\t\t\t\tsphericalToCartesian(state.azimuth, state.elevation, state.distance, _camPos);\n\t\t\t\t\tcachedCamera.position.set(\n\t\t\t\t\t\tstate.targetX + _camPos.x,\n\t\t\t\t\t\tstate.targetY + _camPos.y,\n\t\t\t\t\t\tstate.targetZ + _camPos.z,\n\t\t\t\t\t);\n\t\t\t\t\tcachedCamera.lookAt(state.targetX, state.targetY, state.targetZ);\n\t\t\t\t})\n\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\tif (!drag.el) return;\n\t\t\t\t\tif (enableOrbit) {\n\t\t\t\t\t\tdrag.el.removeEventListener('pointerdown', onPointerDown);\n\t\t\t\t\t\tdrag.el.removeEventListener('pointermove', onPointerMove);\n\t\t\t\t\t\tdrag.el.removeEventListener('pointerup', onPointerUp);\n\t\t\t\t\t}\n\t\t\t\t\tif (resolvedWheelMode !== 'disabled') {\n\t\t\t\t\t\tdrag.el.removeEventListener('wheel', onWheel as EventListener);\n\t\t\t\t\t}\n\t\t\t\t\tdrag.el = null;\n\t\t\t\t\tcachedCamera = null;\n\t\t\t\t\tcachedPerspCamera = null;\n\t\t\t\t\tcachedOrthoCamera = null;\n\t\t\t\t});\n\n\t\t\t// ==================== Follow System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-follow')\n\t\t\t\t.setPriority(400)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ ecs, dt }) => {\n\t\t\t\t\tif (state.followTarget < 0) return;\n\n\t\t\t\t\tif (!ecs.getEntity(state.followTarget)) {\n\t\t\t\t\t\tstate.followTarget = -1;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst worldTransform = ecs.getComponent(state.followTarget, 'worldTransform3D');\n\t\t\t\t\tif (!worldTransform) return;\n\n\t\t\t\t\tconst goalX = worldTransform.x + state.followOffsetX;\n\t\t\t\t\tconst goalY = worldTransform.y + state.followOffsetY;\n\t\t\t\t\tconst goalZ = worldTransform.z + state.followOffsetZ;\n\n\t\t\t\t\tconst factor = Math.min(1, state.followSmoothing * dt);\n\t\t\t\t\tstate.targetX += (goalX - state.targetX) * factor;\n\t\t\t\t\tstate.targetY += (goalY - state.targetY) * factor;\n\t\t\t\t\tstate.targetZ += (goalZ - state.targetZ) * factor;\n\t\t\t\t});\n\n\t\t\t// ==================== Shake System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-shake')\n\t\t\t\t.setPriority(390)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ dt }) => {\n\t\t\t\t\tif (state.trauma <= 0) {\n\t\t\t\t\t\tstate.shakeOffsetX = 0;\n\t\t\t\t\t\tstate.shakeOffsetY = 0;\n\t\t\t\t\t\tstate.shakeOffsetZ = 0;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tstate.trauma = Math.max(0, state.trauma - shakeDecay * dt);\n\n\t\t\t\t\tconst intensity = state.trauma * state.trauma;\n\t\t\t\t\tstate.shakeOffsetX = shakeMaxX * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\tstate.shakeOffsetY = shakeMaxY * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\tstate.shakeOffsetZ = shakeMaxZ * intensity * (randomFn() * 2 - 1);\n\t\t\t\t});\n\n\t\t\t// ==================== Sync System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-sync')\n\t\t\t\t.setPriority(380)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\tif (!cachedCamera) return;\n\n\t\t\t\t\t// Process pending wheel input\n\t\t\t\t\tif (drag.pendingWheel !== 0) {\n\t\t\t\t\t\tconst wheelFactor = Math.pow(dollySensitivity, drag.pendingWheel);\n\t\t\t\t\t\twheelHandlers[resolvedWheelMode](state, wheelFactor);\n\t\t\t\t\t\tdrag.pendingWheel = 0;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Compute camera position from spherical coords. Shake is applied as a\n\t\t\t\t\t// pure view translation — both position and lookAt target shift by the\n\t\t\t\t\t// same offset so the view pans instead of rotating. This keeps the effect\n\t\t\t\t\t// visible under orthographic projection (which has no parallax) and also\n\t\t\t\t\t// makes perspective shake magnitudes feel consistent regardless of distance.\n\t\t\t\t\tsphericalToCartesian(state.azimuth, state.elevation, state.distance, _camPos);\n\t\t\t\t\tcachedCamera.position.set(\n\t\t\t\t\t\tstate.targetX + _camPos.x + state.shakeOffsetX,\n\t\t\t\t\t\tstate.targetY + _camPos.y + state.shakeOffsetY,\n\t\t\t\t\t\tstate.targetZ + _camPos.z + state.shakeOffsetZ,\n\t\t\t\t\t);\n\t\t\t\t\tcachedCamera.lookAt(\n\t\t\t\t\t\tstate.targetX + state.shakeOffsetX,\n\t\t\t\t\t\tstate.targetY + state.shakeOffsetY,\n\t\t\t\t\t\tstate.targetZ + state.shakeOffsetZ,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (state.projection === 'perspective' && cachedPerspCamera && cachedPerspCamera.fov !== state.fov) {\n\t\t\t\t\t\tcachedPerspCamera.fov = state.fov;\n\t\t\t\t\t\tcachedPerspCamera.updateProjectionMatrix();\n\t\t\t\t\t} else if (state.projection === 'orthographic' && cachedOrthoCamera && cachedOrthoCamera.zoom !== state.zoom) {\n\t\t\t\t\t\tcachedOrthoCamera.zoom = state.zoom;\n\t\t\t\t\t\tcachedOrthoCamera.updateProjectionMatrix();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "E7D2111F51674CD764756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "CF36B77E5640BC3164756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
- var E=((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 x}from"ecspresso";var M={x:0,y:0,rotation:0,scaleX:1,scaleY:1},l={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function i(J,K){return{localTransform:{x:J,y:K,rotation:0,scaleX:1,scaleY:1}}}function r(J,K){return{worldTransform:{x:J,y:K,rotation:0,scaleX:1,scaleY:1}}}function n(J,K,Q){let Z=Q?.scale??Q?.scaleX??1,k=Q?.scale??Q?.scaleY??1,N=Q?.rotation??0,G={x:J,y:K,rotation:N,scaleX:Z,scaleY:k};return{localTransform:{...G},worldTransform:{...G}}}function t(J){let{systemGroup:K="transform",priority:Q=500,phase:Z="postUpdate"}=J??{};return x("transform").withComponentTypes().withLabels().withGroups().install((k)=>{k.registerRequired("localTransform","worldTransform",(q)=>({x:q.x,y:q.y,rotation:q.rotation,scaleX:q.scaleX,scaleY:q.scaleY}));let N=[],G=new Set;k.addSystem("transform-propagation").setPriority(Q).inPhase(Z).inGroup(K).setProcess(({ecs:q})=>{f(q,N,G)})})}function f(J,K,Q){let Z=J.entityManager;if(!Z.hasHierarchy){Z.getEntitiesWithQueryInto(K,["localTransform","worldTransform"]);for(let k of K){let{localTransform:N,worldTransform:G}=k.components;if(R(N,G))J.markChanged(k.id,"worldTransform")}return}Q.clear(),J.forEachInHierarchy((k,N)=>{Q.add(k);let G=Z.getComponent(k,"localTransform"),q=Z.getComponent(k,"worldTransform");if(!G||!q)return;let B=N!==null?Z.getComponent(N,"worldTransform"):null;if(B?h(B,G,q):R(G,q))J.markChanged(k,"worldTransform")}),Z.getEntitiesWithQueryInto(K,["localTransform","worldTransform"]);for(let k of K){if(Q.has(k.id))continue;let{localTransform:N,worldTransform:G}=k.components;if(R(N,G))J.markChanged(k.id,"worldTransform")}}function R(J,K){if(K.x===J.x&&K.y===J.y&&K.rotation===J.rotation&&K.scaleX===J.scaleX&&K.scaleY===J.scaleY)return!1;return K.x=J.x,K.y=J.y,K.rotation=J.rotation,K.scaleX=J.scaleX,K.scaleY=J.scaleY,!0}function h(J,K,Q){let Z=K.x*J.scaleX,k=K.y*J.scaleY,N=Math.cos(J.rotation),G=Math.sin(J.rotation),q=Z*N-k*G,B=Z*G+k*N,U=J.x+q,X=J.y+B,V=J.rotation+K.rotation,Y=J.scaleX*K.scaleX,H=J.scaleY*K.scaleY;if(Q.x===U&&Q.y===X&&Q.rotation===V&&Q.scaleX===Y&&Q.scaleY===H)return!1;return Q.x=U,Q.y=X,Q.rotation=V,Q.scaleX=Y,Q.scaleY=H,!0}import{definePlugin as w}from"ecspresso";var b=Object.freeze({"top-left":Object.freeze({x:0,y:0}),"top-center":Object.freeze({x:0.5,y:0}),"top-right":Object.freeze({x:1,y:0}),"center-left":Object.freeze({x:0,y:0.5}),center:Object.freeze({x:0.5,y:0.5}),"center-right":Object.freeze({x:1,y:0.5}),"bottom-left":Object.freeze({x:0,y:1}),"bottom-center":Object.freeze({x:0.5,y:1}),"bottom-right":Object.freeze({x:1,y:1})});function P(J){if(typeof J==="string"){let K=b[J];return{x:K.x,y:K.y}}return{x:J.x,y:J.y}}function g(J,K,Q,Z,k,N){N.x=J.x*Z.width+Q.x-K.x*k.width,N.y=J.y*Z.height+Q.y-K.y*k.height}function y(J,K){if(K<=0)return 0;if(J<=0)return 0;if(J>=K)return K;return J}var d={ltr:(J,K,Q,Z)=>{Z.x=0,Z.y=0,Z.width=J*Q,Z.height=K},rtl:(J,K,Q,Z)=>{Z.x=J*(1-Q),Z.y=0,Z.width=J*Q,Z.height=K},ttb:(J,K,Q,Z)=>{Z.x=0,Z.y=0,Z.width=J,Z.height=K*Q},btt:(J,K,Q,Z)=>{Z.x=0,Z.y=K*(1-Q),Z.width=J,Z.height=K*Q}};function p(J,K,Q,Z,k){d[Z](J,K,Q,k)}var T=Object.freeze({fontFamily:"sans-serif",fontSize:16,fill:16777215,align:"left"});function QJ(J){let K=P(J.anchor),Q=J.pivot===void 0?{x:K.x,y:K.y}:P(J.pivot),Z=J.offset===void 0?{x:0,y:0}:{x:J.offset.x,y:J.offset.y};return{uiElement:{anchor:K,pivot:Q,offset:Z,width:J.width,height:J.height}}}function ZJ(J,K){return{uiLabel:{text:J,style:{...T,...K}}}}function $J(J){return{uiPanel:{fillColor:J.fillColor,borderColor:J.borderColor,borderWidth:J.borderWidth??0}}}function kJ(J){return{uiProgressBar:{value:J.value,max:J.max,fillColor:J.fillColor,bgColor:J.bgColor,direction:J.direction??"ltr"}}}function HJ(J){return{uiMessageLog:{lines:J.initialLines===void 0?[]:[...J.initialLines],maxLines:J.maxLines,visibleLines:J.visibleLines,lineHeight:J.lineHeight,style:{...T,...J.style}}}}function WJ(){return{uiInteractive:{}}}function qJ(){return{uiButton:{}}}function zJ(){return{uiDisabled:{}}}function NJ(J,K,Q){J.commands.mutateComponent(K,"uiMessageLog",(Z)=>{let k=Math.max(0,Z.maxLines),N=[...Z.lines,Q];Z.lines=N.length>k?N.slice(N.length-k):N}),J.eventBus.publish("uiLogAppended",{entityId:K,line:Q})}function DJ(J){let{systemGroup:K="ui",anchorPriority:Q=0,interactionPriority:Z=200,renderSyncPriority:k=480}=J??{},N=new Map,G=new Map,q=new Map,B=new Map,U={x:0,y:0},X={x:0,y:0,width:0,height:0},V=null;return w("ui").withComponentTypes().withEventTypes().withLabels().withGroups().withReactiveQueryNames().requires().install((Y)=>{Y.registerRequired("uiElement","localTransform",()=>({x:M.x,y:M.y,rotation:M.rotation,scaleX:M.scaleX,scaleY:M.scaleY})),Y.registerRequired("uiButton","uiInteractive",()=>({})),Y.registerRequired("uiInteractive","uiInteraction",()=>({state:"none"})),Y.addSystem("ui-anchor-resolve").setPriority(Q).inPhase("preUpdate").inGroup(K).addQuery("uiElements",{with:["uiElement","localTransform"]}).setProcess(({queries:H,ecs:z})=>{let D=z.getResource("bounds");for(let W of H.uiElements){let{uiElement:$,localTransform:O}=W.components;if(g($.anchor,$.pivot,$.offset,D,$,U),O.x!==U.x||O.y!==U.y)O.x=U.x,O.y=U.y,z.markChanged(W.id,"localTransform")}}),Y.addSystem("ui-interaction").setPriority(Z).inPhase("preUpdate").inGroup(K).addQuery("interactables",{with:["uiInteractive","uiInteraction","uiElement","worldTransform"],without:["uiDisabled"]}).setProcess(({queries:H,ecs:z})=>{let D=z.getResource("inputState").pointer,W=D.position.x,$=D.position.y,O=D.isDown(0),I=D.justReleased(0);for(let j of H.interactables){let{uiElement:v,worldTransform:C,uiInteraction:F}=j.components,L=W>=C.x&&W<C.x+v.width&&$>=C.y&&$<C.y+v.height,_=F.state,S=!L?"none":O?_==="none"?"hover":"pressed":"hover";if(_==="pressed"&&S==="hover"&&I&&L)z.eventBus.publish("uiButtonPressed",{entityId:j.id});if(_==="none"&&S!=="none")z.eventBus.publish("uiButtonHovered",{entityId:j.id,entered:!0});if(_!=="none"&&S==="none")z.eventBus.publish("uiButtonHovered",{entityId:j.id,entered:!1});if(_!==S)F.state=S,z.markChanged(j.id,"uiInteraction")}}),Y.addSystem("ui-panel-sync").setPriority(k).inPhase("render").inGroup(K).setOnInitialize(async(H)=>{let z=await import("pixi.js"),D=H.tryGetResource("rootContainer");H.addReactiveQuery("ui-panels",{with:["uiPanel","uiElement"],onEnter:(W)=>{let $=new z.Graphics;if(G.set(W.id,{pixiGraphics:$,lastFillColor:Number.NaN,lastBorderColor:void 0,lastBorderWidth:Number.NaN,lastWidth:Number.NaN,lastHeight:Number.NaN}),D)D.addChild($)},onExit:(W)=>{let $=G.get(W);if($)$.pixiGraphics.removeFromParent(),$.pixiGraphics.destroy(),G.delete(W)}})}).setProcess(({ecs:H})=>{for(let[z,D]of G){let W=H.getComponent(z,"uiPanel"),$=H.getComponent(z,"uiElement");if(!W||!$)continue;m(D,W,$),A(D.pixiGraphics,H.getComponent(z,"worldTransform"))}}),Y.addSystem("ui-progress-sync").setPriority(k).inPhase("render").inGroup(K).setOnInitialize(async(H)=>{let z=await import("pixi.js"),D=H.tryGetResource("rootContainer");H.addReactiveQuery("ui-progress-bars",{with:["uiProgressBar","uiElement"],onEnter:(W)=>{let $=new z.Graphics;if(q.set(W.id,{pixiGraphics:$,lastValue:Number.NaN,lastMax:Number.NaN,lastFillColor:Number.NaN,lastBgColor:Number.NaN,lastDirection:"ltr",lastWidth:Number.NaN,lastHeight:Number.NaN}),D)D.addChild($)},onExit:(W)=>{let $=q.get(W);if($)$.pixiGraphics.removeFromParent(),$.pixiGraphics.destroy(),q.delete(W)}})}).setProcess(({ecs:H})=>{for(let[z,D]of q){let W=H.getComponent(z,"uiProgressBar"),$=H.getComponent(z,"uiElement");if(!W||!$)continue;u(D,W,$,X),A(D.pixiGraphics,H.getComponent(z,"worldTransform"))}}),Y.addSystem("ui-message-log-sync").setPriority(k).inPhase("render").inGroup(K).setOnInitialize(async(H)=>{let z=await import("pixi.js");V=z;let D=H.tryGetResource("rootContainer");H.addReactiveQuery("ui-message-logs",{with:["uiMessageLog","uiElement"],onEnter:(W)=>{let $=new z.Container;if(B.set(W.id,{rootContainer:$,lines:[],lastLinesRef:null,lastVisibleLines:-1,lastLineHeight:Number.NaN,lastFontFamily:"",lastFontSize:Number.NaN,lastAlign:""}),D)D.addChild($)},onExit:(W)=>{let $=B.get(W);if($)$.rootContainer.removeFromParent(),$.rootContainer.destroy({children:!0}),B.delete(W)}})}).setProcess(({ecs:H})=>{if(!V)return;for(let[z,D]of B){let W=H.getComponent(z,"uiMessageLog");if(!W)continue;s(D,W,V),A(D.rootContainer,H.getComponent(z,"worldTransform"))}}),Y.addSystem("ui-label-sync").setPriority(k).inPhase("render").inGroup(K).setOnInitialize(async(H)=>{let z=await import("pixi.js"),D=H.tryGetResource("rootContainer");H.addReactiveQuery("ui-labels",{with:["uiLabel"],onEnter:(W)=>{let $=W.components.uiLabel,O=new z.Text({text:$.text,style:{fontFamily:$.style.fontFamily,fontSize:$.style.fontSize,fill:$.style.fill,align:$.style.align}});if(N.set(W.id,{pixiText:O,lastText:$.text,lastFontSize:$.style.fontSize,lastFill:$.style.fill,lastAlign:$.style.align,lastFontFamily:$.style.fontFamily}),D)D.addChild(O)},onExit:(W)=>{let $=N.get(W);if($)$.pixiText.removeFromParent(),$.pixiText.destroy(),N.delete(W)}})}).setProcess(({ecs:H})=>{for(let[z,D]of N){let W=H.getComponent(z,"uiLabel");if(!W)continue;c(D,W),A(D.pixiText,H.getComponent(z,"worldTransform"))}})})}function A(J,K){if(!K)return;if(J.position.x!==K.x)J.position.x=K.x;if(J.position.y!==K.y)J.position.y=K.y}function c(J,K){if(J.lastText!==K.text)J.pixiText.text=K.text,J.lastText=K.text;let Q=J.pixiText.style;if(J.lastFontSize!==K.style.fontSize)Q.fontSize=K.style.fontSize,J.lastFontSize=K.style.fontSize;if(J.lastFill!==K.style.fill)Q.fill=K.style.fill,J.lastFill=K.style.fill;if(J.lastAlign!==K.style.align)Q.align=K.style.align,J.lastAlign=K.style.align;if(J.lastFontFamily!==K.style.fontFamily)Q.fontFamily=K.style.fontFamily,J.lastFontFamily=K.style.fontFamily}function m(J,K,Q){if(!(J.lastFillColor!==K.fillColor||J.lastBorderColor!==K.borderColor||J.lastBorderWidth!==K.borderWidth||J.lastWidth!==Q.width||J.lastHeight!==Q.height))return;let k=J.pixiGraphics;if(k.clear(),k.rect(0,0,Q.width,Q.height),k.fill({color:K.fillColor}),K.borderColor!==void 0&&K.borderWidth>0)k.stroke({color:K.borderColor,width:K.borderWidth});J.lastFillColor=K.fillColor,J.lastBorderColor=K.borderColor,J.lastBorderWidth=K.borderWidth,J.lastWidth=Q.width,J.lastHeight=Q.height}function u(J,K,Q,Z){if(!(J.lastValue!==K.value||J.lastMax!==K.max||J.lastFillColor!==K.fillColor||J.lastBgColor!==K.bgColor||J.lastDirection!==K.direction||J.lastWidth!==Q.width||J.lastHeight!==Q.height))return;let N=y(K.value,K.max),G=K.max>0?N/K.max:0;p(Q.width,Q.height,G,K.direction,Z);let q=J.pixiGraphics;if(q.clear(),q.rect(0,0,Q.width,Q.height),q.fill({color:K.bgColor}),Z.width>0&&Z.height>0)q.rect(Z.x,Z.y,Z.width,Z.height),q.fill({color:K.fillColor});J.lastValue=K.value,J.lastMax=K.max,J.lastFillColor=K.fillColor,J.lastBgColor=K.bgColor,J.lastDirection=K.direction,J.lastWidth=Q.width,J.lastHeight=Q.height}function s(J,K,Q){let Z=J.lastFontFamily!==K.style.fontFamily||J.lastFontSize!==K.style.fontSize||J.lastAlign!==K.style.align,k=J.lastLinesRef!==K.lines,N=J.lastVisibleLines!==K.visibleLines||J.lastLineHeight!==K.lineHeight;if(!k&&!N&&!Z)return;let G=K.lines.slice(-K.visibleLines);while(J.lines.length<G.length){let q=new Q.Container;J.rootContainer.addChild(q),J.lines.push({container:q,texts:[]})}if(J.lines.forEach((q,B)=>{if(B>=G.length)q.container.visible=!1}),G.forEach((q,B)=>{let U=J.lines[B];if(!U)return;U.container.visible=!0,U.container.position.y=B*K.lineHeight;while(U.texts.length<q.length){let V=new Q.Text({text:"",style:{fontFamily:K.style.fontFamily,fontSize:K.style.fontSize,fill:16777215,align:K.style.align}});U.container.addChild(V),U.texts.push(V)}let X=0;U.texts.forEach((V,Y)=>{let H=q[Y];if(!H){V.visible=!1;return}if(V.visible=!0,V.text!==H.text)V.text=H.text;if(V.style.fill!==H.color)V.style.fill=H.color;V.position.x=X,X+=V.width})}),Z)J.lines.forEach((q)=>{q.texts.forEach((B)=>{let U=B.style;if(U.fontFamily!==K.style.fontFamily)U.fontFamily=K.style.fontFamily;if(U.fontSize!==K.style.fontSize)U.fontSize=K.style.fontSize;if(U.align!==K.style.align)U.align=K.style.align})});J.lastLinesRef=K.lines,J.lastVisibleLines=K.visibleLines,J.lastLineHeight=K.lineHeight,J.lastFontFamily=K.style.fontFamily,J.lastFontSize=K.style.fontSize,J.lastAlign=K.style.align}export{P as resolveAnchorPreset,g as resolveAnchorPosition,kJ as createUIProgressBar,DJ as createUIPlugin,$J as createUIPanel,HJ as createUIMessageLog,ZJ as createUILabel,WJ as createUIInteractive,QJ as createUIElement,zJ as createUIDisabled,qJ as createUIButton,p as computeProgressFillRect,y as clampProgressValue,NJ as appendLogLine,b as ANCHOR_PRESETS};
1
+ var E=((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 x}from"ecspresso";var M={x:0,y:0,rotation:0,scaleX:1,scaleY:1},l={x:0,y:0,rotation:0,scaleX:1,scaleY:1};function i(J,K){return{localTransform:{x:J,y:K,rotation:0,scaleX:1,scaleY:1}}}function r(J,K){return{worldTransform:{x:J,y:K,rotation:0,scaleX:1,scaleY:1}}}function n(J,K,Q){let Z=Q?.scale??Q?.scaleX??1,k=Q?.scale??Q?.scaleY??1,N=Q?.rotation??0,G={x:J,y:K,rotation:N,scaleX:Z,scaleY:k};return{localTransform:{...G},worldTransform:{...G}}}function t(J){let{systemGroup:K="transform",priority:Q=500,phase:Z="postUpdate"}=J??{};return x("transform").withComponentTypes().withLabels().withGroups().install((k)=>{k.registerRequired("localTransform","worldTransform",(q)=>({x:q.x,y:q.y,rotation:q.rotation,scaleX:q.scaleX,scaleY:q.scaleY}));let N=[],G=new Set;k.addSystem("transform-propagation").setPriority(Q).inPhase(Z).inGroup(K).setProcess(({ecs:q})=>{f(q,N,G)})})}function f(J,K,Q){let Z=J.entityManager;if(!Z.hasHierarchy){Z.getEntitiesWithQueryInto(K,["localTransform","worldTransform"]);for(let k of K){let{localTransform:N,worldTransform:G}=k.components;if(R(N,G))J.markChanged(k.id,"worldTransform")}return}Q.clear(),J.forEachInHierarchy((k,N)=>{Q.add(k);let G=Z.getComponent(k,"localTransform"),q=Z.getComponent(k,"worldTransform");if(!G||!q)return;let B=N!==null?Z.getComponent(N,"worldTransform"):null;if(B?h(B,G,q):R(G,q))J.markChanged(k,"worldTransform")}),Z.getEntitiesWithQueryInto(K,["localTransform","worldTransform"]);for(let k of K){if(Q.has(k.id))continue;let{localTransform:N,worldTransform:G}=k.components;if(R(N,G))J.markChanged(k.id,"worldTransform")}}function R(J,K){if(K.x===J.x&&K.y===J.y&&K.rotation===J.rotation&&K.scaleX===J.scaleX&&K.scaleY===J.scaleY)return!1;return K.x=J.x,K.y=J.y,K.rotation=J.rotation,K.scaleX=J.scaleX,K.scaleY=J.scaleY,!0}function h(J,K,Q){let Z=K.x*J.scaleX,k=K.y*J.scaleY,N=Math.cos(J.rotation),G=Math.sin(J.rotation),q=Z*N-k*G,B=Z*G+k*N,U=J.x+q,X=J.y+B,V=J.rotation+K.rotation,Y=J.scaleX*K.scaleX,H=J.scaleY*K.scaleY;if(Q.x===U&&Q.y===X&&Q.rotation===V&&Q.scaleX===Y&&Q.scaleY===H)return!1;return Q.x=U,Q.y=X,Q.rotation=V,Q.scaleX=Y,Q.scaleY=H,!0}import{definePlugin as w}from"ecspresso";var b=Object.freeze({"top-left":Object.freeze({x:0,y:0}),"top-center":Object.freeze({x:0.5,y:0}),"top-right":Object.freeze({x:1,y:0}),"center-left":Object.freeze({x:0,y:0.5}),center:Object.freeze({x:0.5,y:0.5}),"center-right":Object.freeze({x:1,y:0.5}),"bottom-left":Object.freeze({x:0,y:1}),"bottom-center":Object.freeze({x:0.5,y:1}),"bottom-right":Object.freeze({x:1,y:1})});function P(J){if(typeof J==="string"){let K=b[J];return{x:K.x,y:K.y}}return{x:J.x,y:J.y}}function g(J,K,Q,Z,k,N){N.x=J.x*Z.width+Q.x-K.x*k.width,N.y=J.y*Z.height+Q.y-K.y*k.height}function y(J,K){if(K<=0)return 0;if(J<=0)return 0;if(J>=K)return K;return J}var d={ltr:(J,K,Q,Z)=>{Z.x=0,Z.y=0,Z.width=J*Q,Z.height=K},rtl:(J,K,Q,Z)=>{Z.x=J*(1-Q),Z.y=0,Z.width=J*Q,Z.height=K},ttb:(J,K,Q,Z)=>{Z.x=0,Z.y=0,Z.width=J,Z.height=K*Q},btt:(J,K,Q,Z)=>{Z.x=0,Z.y=K*(1-Q),Z.width=J,Z.height=K*Q}};function p(J,K,Q,Z,k){d[Z](J,K,Q,k)}var T=Object.freeze({fontFamily:"sans-serif",fontSize:16,fill:16777215,align:"left"});function QJ(J){let K=P(J.anchor),Q=J.pivot===void 0?{x:K.x,y:K.y}:P(J.pivot),Z=J.offset===void 0?{x:0,y:0}:{x:J.offset.x,y:J.offset.y};return{uiElement:{anchor:K,pivot:Q,offset:Z,width:J.width,height:J.height}}}function ZJ(J,K){return{uiLabel:{text:J,style:{...T,...K}}}}function $J(J){return{uiPanel:{fillColor:J.fillColor,borderColor:J.borderColor,borderWidth:J.borderWidth??0}}}function kJ(J){return{uiProgressBar:{value:J.value,max:J.max,fillColor:J.fillColor,bgColor:J.bgColor,direction:J.direction??"ltr"}}}function HJ(J){return{uiMessageLog:{lines:J.initialLines===void 0?[]:[...J.initialLines],maxLines:J.maxLines,visibleLines:J.visibleLines,lineHeight:J.lineHeight,style:{...T,...J.style}}}}function WJ(){return{uiInteractive:{}}}function qJ(){return{uiButton:{}}}function zJ(){return{uiDisabled:{}}}function NJ(J,K,Q){J.commands.mutateComponent(K,"uiMessageLog",(Z)=>{let k=Math.max(0,Z.maxLines),N=[...Z.lines,Q];Z.lines=N.length>k?N.slice(N.length-k):N}),J.eventBus.publish("uiLogAppended",{entityId:K,line:Q})}function DJ(J){let{systemGroup:K="ui",anchorPriority:Q=0,interactionPriority:Z=200,renderSyncPriority:k=480}=J??{},N=new Map,G=new Map,q=new Map,B=new Map,U={x:0,y:0},X={x:0,y:0,width:0,height:0},V=null;return w("ui").withComponentTypes().withEventTypes().withLabels().withGroups().withReactiveQueryNames().requires().install((Y)=>{Y.registerRequired("uiElement","localTransform",()=>({x:M.x,y:M.y,rotation:M.rotation,scaleX:M.scaleX,scaleY:M.scaleY})),Y.registerRequired("uiButton","uiInteractive",()=>({})),Y.registerRequired("uiInteractive","uiInteraction",()=>({state:"none"})),Y.addSystem("ui-anchor-resolve").setPriority(Q).inPhase("preUpdate").inGroup(K).addQuery("uiElements",{with:["uiElement","localTransform"]}).setProcess(({queries:H,ecs:z})=>{let D=z.getResource("bounds");for(let W of H.uiElements){let{uiElement:$,localTransform:O}=W.components;if(g($.anchor,$.pivot,$.offset,D,$,U),O.x!==U.x||O.y!==U.y)O.x=U.x,O.y=U.y,z.markChanged(W.id,"localTransform")}}),Y.addSystem("ui-interaction").setPriority(Z).inPhase("preUpdate").inGroup(K).addQuery("interactables",{with:["uiInteractive","uiInteraction","uiElement","worldTransform"],without:["uiDisabled"]}).setProcess(({queries:H,ecs:z})=>{let D=z.getResource("inputState").pointer,W=D.position.x,$=D.position.y,O=D.isDown(0),I=D.justReleased(0);for(let j of H.interactables){let{uiElement:v,worldTransform:C,uiInteraction:F}=j.components,L=W>=C.x&&W<C.x+v.width&&$>=C.y&&$<C.y+v.height,_=F.state,S=!L?"none":O?_==="none"?"hover":"pressed":"hover";if(_==="pressed"&&S==="hover"&&I&&L)z.eventBus.publish("uiButtonPressed",{entityId:j.id});if(_==="none"&&S!=="none")z.eventBus.publish("uiButtonHovered",{entityId:j.id,entered:!0});if(_!=="none"&&S==="none")z.eventBus.publish("uiButtonHovered",{entityId:j.id,entered:!1});if(_!==S)F.state=S,z.markChanged(j.id,"uiInteraction")}}),Y.addSystem("ui-panel-sync").setPriority(k).inPhase("render").inGroup(K).setOnInitialize(async(H)=>{let z=await import("pixi.js"),D=H.tryGetResource("rootContainer");H.addReactiveQuery("ui-panels",{with:["uiPanel","uiElement"],onEnter:({entity:W})=>{let $=new z.Graphics;if(G.set(W.id,{pixiGraphics:$,lastFillColor:Number.NaN,lastBorderColor:void 0,lastBorderWidth:Number.NaN,lastWidth:Number.NaN,lastHeight:Number.NaN}),D)D.addChild($)},onExit:({entityId:W})=>{let $=G.get(W);if($)$.pixiGraphics.removeFromParent(),$.pixiGraphics.destroy(),G.delete(W)}})}).setProcess(({ecs:H})=>{for(let[z,D]of G){let W=H.getComponent(z,"uiPanel"),$=H.getComponent(z,"uiElement");if(!W||!$)continue;m(D,W,$),A(D.pixiGraphics,H.getComponent(z,"worldTransform"))}}),Y.addSystem("ui-progress-sync").setPriority(k).inPhase("render").inGroup(K).setOnInitialize(async(H)=>{let z=await import("pixi.js"),D=H.tryGetResource("rootContainer");H.addReactiveQuery("ui-progress-bars",{with:["uiProgressBar","uiElement"],onEnter:({entity:W})=>{let $=new z.Graphics;if(q.set(W.id,{pixiGraphics:$,lastValue:Number.NaN,lastMax:Number.NaN,lastFillColor:Number.NaN,lastBgColor:Number.NaN,lastDirection:"ltr",lastWidth:Number.NaN,lastHeight:Number.NaN}),D)D.addChild($)},onExit:({entityId:W})=>{let $=q.get(W);if($)$.pixiGraphics.removeFromParent(),$.pixiGraphics.destroy(),q.delete(W)}})}).setProcess(({ecs:H})=>{for(let[z,D]of q){let W=H.getComponent(z,"uiProgressBar"),$=H.getComponent(z,"uiElement");if(!W||!$)continue;u(D,W,$,X),A(D.pixiGraphics,H.getComponent(z,"worldTransform"))}}),Y.addSystem("ui-message-log-sync").setPriority(k).inPhase("render").inGroup(K).setOnInitialize(async(H)=>{let z=await import("pixi.js");V=z;let D=H.tryGetResource("rootContainer");H.addReactiveQuery("ui-message-logs",{with:["uiMessageLog","uiElement"],onEnter:({entity:W})=>{let $=new z.Container;if(B.set(W.id,{rootContainer:$,lines:[],lastLinesRef:null,lastVisibleLines:-1,lastLineHeight:Number.NaN,lastFontFamily:"",lastFontSize:Number.NaN,lastAlign:""}),D)D.addChild($)},onExit:({entityId:W})=>{let $=B.get(W);if($)$.rootContainer.removeFromParent(),$.rootContainer.destroy({children:!0}),B.delete(W)}})}).setProcess(({ecs:H})=>{if(!V)return;for(let[z,D]of B){let W=H.getComponent(z,"uiMessageLog");if(!W)continue;s(D,W,V),A(D.rootContainer,H.getComponent(z,"worldTransform"))}}),Y.addSystem("ui-label-sync").setPriority(k).inPhase("render").inGroup(K).setOnInitialize(async(H)=>{let z=await import("pixi.js"),D=H.tryGetResource("rootContainer");H.addReactiveQuery("ui-labels",{with:["uiLabel"],onEnter:({entity:W})=>{let $=W.components.uiLabel,O=new z.Text({text:$.text,style:{fontFamily:$.style.fontFamily,fontSize:$.style.fontSize,fill:$.style.fill,align:$.style.align}});if(N.set(W.id,{pixiText:O,lastText:$.text,lastFontSize:$.style.fontSize,lastFill:$.style.fill,lastAlign:$.style.align,lastFontFamily:$.style.fontFamily}),D)D.addChild(O)},onExit:({entityId:W})=>{let $=N.get(W);if($)$.pixiText.removeFromParent(),$.pixiText.destroy(),N.delete(W)}})}).setProcess(({ecs:H})=>{for(let[z,D]of N){let W=H.getComponent(z,"uiLabel");if(!W)continue;c(D,W),A(D.pixiText,H.getComponent(z,"worldTransform"))}})})}function A(J,K){if(!K)return;if(J.position.x!==K.x)J.position.x=K.x;if(J.position.y!==K.y)J.position.y=K.y}function c(J,K){if(J.lastText!==K.text)J.pixiText.text=K.text,J.lastText=K.text;let Q=J.pixiText.style;if(J.lastFontSize!==K.style.fontSize)Q.fontSize=K.style.fontSize,J.lastFontSize=K.style.fontSize;if(J.lastFill!==K.style.fill)Q.fill=K.style.fill,J.lastFill=K.style.fill;if(J.lastAlign!==K.style.align)Q.align=K.style.align,J.lastAlign=K.style.align;if(J.lastFontFamily!==K.style.fontFamily)Q.fontFamily=K.style.fontFamily,J.lastFontFamily=K.style.fontFamily}function m(J,K,Q){if(!(J.lastFillColor!==K.fillColor||J.lastBorderColor!==K.borderColor||J.lastBorderWidth!==K.borderWidth||J.lastWidth!==Q.width||J.lastHeight!==Q.height))return;let k=J.pixiGraphics;if(k.clear(),k.rect(0,0,Q.width,Q.height),k.fill({color:K.fillColor}),K.borderColor!==void 0&&K.borderWidth>0)k.stroke({color:K.borderColor,width:K.borderWidth});J.lastFillColor=K.fillColor,J.lastBorderColor=K.borderColor,J.lastBorderWidth=K.borderWidth,J.lastWidth=Q.width,J.lastHeight=Q.height}function u(J,K,Q,Z){if(!(J.lastValue!==K.value||J.lastMax!==K.max||J.lastFillColor!==K.fillColor||J.lastBgColor!==K.bgColor||J.lastDirection!==K.direction||J.lastWidth!==Q.width||J.lastHeight!==Q.height))return;let N=y(K.value,K.max),G=K.max>0?N/K.max:0;p(Q.width,Q.height,G,K.direction,Z);let q=J.pixiGraphics;if(q.clear(),q.rect(0,0,Q.width,Q.height),q.fill({color:K.bgColor}),Z.width>0&&Z.height>0)q.rect(Z.x,Z.y,Z.width,Z.height),q.fill({color:K.fillColor});J.lastValue=K.value,J.lastMax=K.max,J.lastFillColor=K.fillColor,J.lastBgColor=K.bgColor,J.lastDirection=K.direction,J.lastWidth=Q.width,J.lastHeight=Q.height}function s(J,K,Q){let Z=J.lastFontFamily!==K.style.fontFamily||J.lastFontSize!==K.style.fontSize||J.lastAlign!==K.style.align,k=J.lastLinesRef!==K.lines,N=J.lastVisibleLines!==K.visibleLines||J.lastLineHeight!==K.lineHeight;if(!k&&!N&&!Z)return;let G=K.lines.slice(-K.visibleLines);while(J.lines.length<G.length){let q=new Q.Container;J.rootContainer.addChild(q),J.lines.push({container:q,texts:[]})}if(J.lines.forEach((q,B)=>{if(B>=G.length)q.container.visible=!1}),G.forEach((q,B)=>{let U=J.lines[B];if(!U)return;U.container.visible=!0,U.container.position.y=B*K.lineHeight;while(U.texts.length<q.length){let V=new Q.Text({text:"",style:{fontFamily:K.style.fontFamily,fontSize:K.style.fontSize,fill:16777215,align:K.style.align}});U.container.addChild(V),U.texts.push(V)}let X=0;U.texts.forEach((V,Y)=>{let H=q[Y];if(!H){V.visible=!1;return}if(V.visible=!0,V.text!==H.text)V.text=H.text;if(V.style.fill!==H.color)V.style.fill=H.color;V.position.x=X,X+=V.width})}),Z)J.lines.forEach((q)=>{q.texts.forEach((B)=>{let U=B.style;if(U.fontFamily!==K.style.fontFamily)U.fontFamily=K.style.fontFamily;if(U.fontSize!==K.style.fontSize)U.fontSize=K.style.fontSize;if(U.align!==K.style.align)U.align=K.style.align})});J.lastLinesRef=K.lines,J.lastVisibleLines=K.visibleLines,J.lastLineHeight=K.lineHeight,J.lastFontFamily=K.style.fontFamily,J.lastFontSize=K.style.fontSize,J.lastAlign=K.style.align}export{P as resolveAnchorPreset,g as resolveAnchorPosition,kJ as createUIProgressBar,DJ as createUIPlugin,$J as createUIPanel,HJ as createUIMessageLog,ZJ as createUILabel,WJ as createUIInteractive,QJ as createUIElement,zJ as createUIDisabled,qJ as createUIButton,p as computeProgressFillRect,y as clampProgressValue,NJ as appendLogLine,b as ANCHOR_PRESETS};
2
2
 
3
- //# debugId=B6E1260EDCFC611D64756E2164756E21
3
+ //# debugId=4973E7FC8B99283864756E2164756E21
4
4
  //# sourceMappingURL=ui.js.map
@@ -3,9 +3,9 @@
3
3
  "sources": ["../src/plugins/spatial/transform.ts", "../src/plugins/ui/ui.ts"],
4
4
  "sourcesContent": [
5
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 { ComponentsConfig } 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 = ComponentsConfig<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<ComponentsConfig<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
- "/**\n * UI / HUD Plugin for ECSpresso.\n *\n * Screen-space primitives:\n * - `uiElement` — anchor/pivot/offset positioning resolved against the `bounds` resource\n * - `uiLabel` — PixiJS Text\n * - `uiPanel` — PixiJS Graphics rectangle with optional border\n * - `uiProgressBar` — PixiJS Graphics value indicator with four fill directions\n *\n * Pointer interaction (buttons):\n * - `uiInteractive` (marker) opts an entity into hit-testing\n * - `uiInteraction.state` — `'none' | 'hover' | 'pressed'` (Bevy-style single enum)\n * - `uiButton` marker composes `uiInteractive` + `uiInteraction`\n * - `uiDisabled` skips hit-testing entirely\n * - Emits `uiButtonPressed` (confirmed down→up on same widget) and `uiButtonHovered`\n *\n * Depends on `renderer2D` (for the `bounds` resource + scene graph + screen-space layer),\n * the transform plugin (bundled by renderer2D), and the input plugin.\n *\n * Future phases will add the message log (Phase 3).\n */\n\nimport type { Container, Graphics, Text } from 'pixi.js';\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from '../../type-utils';\nimport type { Vector2D } from '../../utils/math';\nimport {\n\tDEFAULT_LOCAL_TRANSFORM,\n\ttype LocalTransform,\n\ttype TransformComponentTypes,\n\ttype WorldTransform,\n} from '../spatial/transform';\nimport type { BoundsResourceTypes } from '../spatial/bounds';\nimport type { InputResourceTypes } from '../input/input';\n\n// ==================== Anchor Presets ====================\n\nexport type AnchorPreset =\n\t| 'top-left' | 'top-center' | 'top-right'\n\t| 'center-left' | 'center' | 'center-right'\n\t| 'bottom-left' | 'bottom-center' | 'bottom-right';\n\nexport const ANCHOR_PRESETS: Readonly<Record<AnchorPreset, Readonly<Vector2D>>> = Object.freeze({\n\t'top-left': Object.freeze({ x: 0, y: 0 }),\n\t'top-center': Object.freeze({ x: 0.5, y: 0 }),\n\t'top-right': Object.freeze({ x: 1, y: 0 }),\n\t'center-left': Object.freeze({ x: 0, y: 0.5 }),\n\t'center': Object.freeze({ x: 0.5, y: 0.5 }),\n\t'center-right': Object.freeze({ x: 1, y: 0.5 }),\n\t'bottom-left': Object.freeze({ x: 0, y: 1 }),\n\t'bottom-center': Object.freeze({ x: 0.5, y: 1 }),\n\t'bottom-right': Object.freeze({ x: 1, y: 1 }),\n});\n\nexport type AnchorInput = AnchorPreset | Vector2D;\n\n/** Resolve a preset string or vec2 into a mutable Vector2D copy. */\nexport function resolveAnchorPreset(input: AnchorInput): Vector2D {\n\tif (typeof input === 'string') {\n\t\tconst preset = ANCHOR_PRESETS[input];\n\t\treturn { x: preset.x, y: preset.y };\n\t}\n\treturn { x: input.x, y: input.y };\n}\n\n/**\n * Write the top-left screen position of a widget into `out`.\n *\n * Formula: position = anchor * bounds + offset - pivot * size.\n * `anchor` specifies where on the canvas the widget attaches (0..1 normalized).\n * `pivot` specifies where on the widget that attachment point lands (0..1 normalized).\n * Writes in place to avoid per-frame allocation.\n */\nexport function resolveAnchorPosition(\n\tanchor: Readonly<Vector2D>,\n\tpivot: Readonly<Vector2D>,\n\toffset: Readonly<Vector2D>,\n\tbounds: Readonly<{ width: number; height: number }>,\n\tsize: Readonly<{ width: number; height: number }>,\n\tout: Vector2D,\n): void {\n\tout.x = anchor.x * bounds.width + offset.x - pivot.x * size.width;\n\tout.y = anchor.y * bounds.height + offset.y - pivot.y * size.height;\n}\n\n// ==================== Progress Bar Math ====================\n\nexport type ProgressDirection = 'ltr' | 'rtl' | 'ttb' | 'btt';\n\nexport interface FillRect {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n}\n\nexport function clampProgressValue(value: number, max: number): number {\n\tif (max <= 0) return 0;\n\tif (value <= 0) return 0;\n\tif (value >= max) return max;\n\treturn value;\n}\n\ntype FillComputer = (w: number, h: number, ratio: number, out: FillRect) => void;\n\nconst FILL_COMPUTERS: Readonly<Record<ProgressDirection, FillComputer>> = {\n\tltr: (w, h, r, o) => { o.x = 0; o.y = 0; o.width = w * r; o.height = h; },\n\trtl: (w, h, r, o) => { o.x = w * (1 - r); o.y = 0; o.width = w * r; o.height = h; },\n\tttb: (w, h, r, o) => { o.x = 0; o.y = 0; o.width = w; o.height = h * r; },\n\tbtt: (w, h, r, o) => { o.x = 0; o.y = h * (1 - r); o.width = w; o.height = h * r; },\n};\n\nexport function computeProgressFillRect(\n\twidth: number,\n\theight: number,\n\tratio: number,\n\tdirection: ProgressDirection,\n\tout: FillRect,\n): void {\n\tFILL_COMPUTERS[direction](width, height, ratio, out);\n}\n\n// ==================== Component Types ====================\n\nexport interface UIElement {\n\tanchor: Vector2D;\n\tpivot: Vector2D;\n\toffset: Vector2D;\n\twidth: number;\n\theight: number;\n}\n\nexport interface UITextStyle {\n\tfontFamily: string;\n\tfontSize: number;\n\tfill: number;\n\talign: 'left' | 'center' | 'right';\n}\n\nexport interface UILabel {\n\ttext: string;\n\tstyle: UITextStyle;\n}\n\nexport interface UIPanel {\n\tfillColor: number;\n\tborderColor?: number;\n\tborderWidth: number;\n}\n\nexport interface UIProgressBar {\n\tvalue: number;\n\tmax: number;\n\tfillColor: number;\n\tbgColor: number;\n\tdirection: ProgressDirection;\n}\n\nexport type UIInteractionState = 'none' | 'hover' | 'pressed';\n\nexport interface UIInteraction {\n\tstate: UIInteractionState;\n}\n\nexport interface LogFragment {\n\ttext: string;\n\tcolor: number;\n}\n\nexport interface UIMessageLog {\n\tlines: LogFragment[][];\n\tmaxLines: number;\n\tvisibleLines: number;\n\tlineHeight: number;\n\tstyle: UITextStyle;\n}\n\nexport interface UIComponentTypes {\n\tuiElement: UIElement;\n\tuiLabel: UILabel;\n\tuiPanel: UIPanel;\n\tuiProgressBar: UIProgressBar;\n\tuiButton: {};\n\tuiInteractive: {};\n\tuiInteraction: UIInteraction;\n\tuiDisabled: {};\n\tuiMessageLog: UIMessageLog;\n}\n\n// ==================== Event Types ====================\n\nexport interface UIButtonPressedEvent {\n\tentityId: number;\n}\n\nexport interface UIButtonHoveredEvent {\n\tentityId: number;\n\tentered: boolean;\n}\n\nexport interface UIMessageLogAppendedEvent {\n\tentityId: number;\n\tline: LogFragment[];\n}\n\nexport interface UIEventTypes {\n\tuiButtonPressed: UIButtonPressedEvent;\n\tuiButtonHovered: UIButtonHoveredEvent;\n\tuiLogAppended: UIMessageLogAppendedEvent;\n}\n\n// ==================== Component Factories ====================\n\nconst DEFAULT_TEXT_STYLE: Readonly<UITextStyle> = Object.freeze({\n\tfontFamily: 'sans-serif',\n\tfontSize: 16,\n\tfill: 0xffffff,\n\talign: 'left',\n});\n\nexport interface CreateUIElementInput {\n\tanchor: AnchorInput;\n\tpivot?: AnchorInput;\n\toffset?: Vector2D;\n\twidth: number;\n\theight: number;\n}\n\nexport function createUIElement(input: CreateUIElementInput): Pick<UIComponentTypes, 'uiElement'> {\n\tconst anchor = resolveAnchorPreset(input.anchor);\n\tconst pivot = input.pivot === undefined ? { x: anchor.x, y: anchor.y } : resolveAnchorPreset(input.pivot);\n\tconst offset = input.offset === undefined ? { x: 0, y: 0 } : { x: input.offset.x, y: input.offset.y };\n\treturn {\n\t\tuiElement: {\n\t\t\tanchor,\n\t\t\tpivot,\n\t\t\toffset,\n\t\t\twidth: input.width,\n\t\t\theight: input.height,\n\t\t},\n\t};\n}\n\nexport function createUILabel(\n\ttext: string,\n\tstyle?: Partial<UITextStyle>,\n): Pick<UIComponentTypes, 'uiLabel'> {\n\treturn {\n\t\tuiLabel: {\n\t\t\ttext,\n\t\t\tstyle: { ...DEFAULT_TEXT_STYLE, ...style },\n\t\t},\n\t};\n}\n\nexport interface CreateUIPanelInput {\n\tfillColor: number;\n\tborderColor?: number;\n\tborderWidth?: number;\n}\n\nexport function createUIPanel(input: CreateUIPanelInput): Pick<UIComponentTypes, 'uiPanel'> {\n\treturn {\n\t\tuiPanel: {\n\t\t\tfillColor: input.fillColor,\n\t\t\tborderColor: input.borderColor,\n\t\t\tborderWidth: input.borderWidth ?? 0,\n\t\t},\n\t};\n}\n\nexport interface CreateUIProgressBarInput {\n\tvalue: number;\n\tmax: number;\n\tfillColor: number;\n\tbgColor: number;\n\tdirection?: ProgressDirection;\n}\n\nexport function createUIProgressBar(input: CreateUIProgressBarInput): Pick<UIComponentTypes, 'uiProgressBar'> {\n\treturn {\n\t\tuiProgressBar: {\n\t\t\tvalue: input.value,\n\t\t\tmax: input.max,\n\t\t\tfillColor: input.fillColor,\n\t\t\tbgColor: input.bgColor,\n\t\t\tdirection: input.direction ?? 'ltr',\n\t\t},\n\t};\n}\n\nexport interface CreateUIMessageLogInput {\n\tmaxLines: number;\n\tvisibleLines: number;\n\tlineHeight: number;\n\tstyle?: Partial<UITextStyle>;\n\tinitialLines?: LogFragment[][];\n}\n\nexport function createUIMessageLog(input: CreateUIMessageLogInput): Pick<UIComponentTypes, 'uiMessageLog'> {\n\treturn {\n\t\tuiMessageLog: {\n\t\t\tlines: input.initialLines === undefined ? [] : [...input.initialLines],\n\t\t\tmaxLines: input.maxLines,\n\t\t\tvisibleLines: input.visibleLines,\n\t\t\tlineHeight: input.lineHeight,\n\t\t\tstyle: { ...DEFAULT_TEXT_STYLE, ...input.style },\n\t\t},\n\t};\n}\n\nexport function createUIInteractive(): Pick<UIComponentTypes, 'uiInteractive'> {\n\treturn { uiInteractive: {} };\n}\n\nexport function createUIButton(): Pick<UIComponentTypes, 'uiButton'> {\n\treturn { uiButton: {} };\n}\n\nexport function createUIDisabled(): Pick<UIComponentTypes, 'uiDisabled'> {\n\treturn { uiDisabled: {} };\n}\n\n// ==================== Message Log Helper ====================\n\n/** Structural ECS surface for `appendLogLine`; mirrors the `CoroutineWorld` pattern. */\nexport interface MessageLogWorld {\n\tcommands: {\n\t\tmutateComponent(\n\t\t\tentityId: number,\n\t\t\tcomponentName: 'uiMessageLog',\n\t\t\tmutator: (value: UIMessageLog) => void,\n\t\t): void;\n\t};\n\teventBus: {\n\t\tpublish(event: 'uiLogAppended', payload: UIMessageLogAppendedEvent): void;\n\t};\n}\n\n/**\n * Append a line (vector of fragments) to a `uiMessageLog` entity.\n *\n * Queues a buffered mutation that swaps `lines` for a fresh array (FIFO-truncated to\n * `maxLines`) — the array-identity change is the sync system's redraw signal — and\n * synchronously publishes `uiLogAppended` carrying the line for entry-animation\n * listeners. Safe to call from inside a system process callback.\n */\nexport function appendLogLine(\n\tecs: MessageLogWorld,\n\tentityId: number,\n\tline: LogFragment[],\n): void {\n\tecs.commands.mutateComponent(entityId, 'uiMessageLog', (log) => {\n\t\tconst cap = Math.max(0, log.maxLines);\n\t\tconst next = [...log.lines, line];\n\t\tlog.lines = next.length > cap ? next.slice(next.length - cap) : next;\n\t});\n\tecs.eventBus.publish('uiLogAppended', { entityId, line });\n}\n\n// ==================== Runtime Data (Side Storage) ====================\n\ninterface UILabelRuntime {\n\tpixiText: Text;\n\tlastText: string;\n\tlastFontSize: number;\n\tlastFill: number;\n\tlastAlign: string;\n\tlastFontFamily: string;\n}\n\ninterface UIPanelRuntime {\n\tpixiGraphics: Graphics;\n\tlastFillColor: number;\n\tlastBorderColor: number | undefined;\n\tlastBorderWidth: number;\n\tlastWidth: number;\n\tlastHeight: number;\n}\n\ninterface UIProgressRuntime {\n\tpixiGraphics: Graphics;\n\tlastValue: number;\n\tlastMax: number;\n\tlastFillColor: number;\n\tlastBgColor: number;\n\tlastDirection: ProgressDirection;\n\tlastWidth: number;\n\tlastHeight: number;\n}\n\ninterface UIMessageLogLineRuntime {\n\tcontainer: Container;\n\ttexts: Text[];\n}\n\ninterface UIMessageLogRuntime {\n\trootContainer: Container;\n\tlines: UIMessageLogLineRuntime[];\n\tlastLinesRef: LogFragment[][] | null;\n\tlastVisibleLines: number;\n\tlastLineHeight: number;\n\tlastFontFamily: string;\n\tlastFontSize: number;\n\tlastAlign: string;\n}\n\n// ==================== Plugin Factory ====================\n\ntype UIRequires =\n\tComponentsConfig<TransformComponentTypes>\n\t& ResourcesConfig<BoundsResourceTypes & InputResourceTypes>;\n\ntype UILabels =\n\t| 'ui-anchor-resolve'\n\t| 'ui-interaction'\n\t| 'ui-label-sync'\n\t| 'ui-panel-sync'\n\t| 'ui-progress-sync'\n\t| 'ui-message-log-sync';\n\nexport interface UIPluginOptions<G extends string = 'ui'> extends BasePluginOptions<G> {\n\t/** Priority for the anchor-resolve system in preUpdate (default: 0). */\n\tanchorPriority?: number;\n\t/** Priority for the pointer hit-test system in preUpdate (default: 200, after input's 100). */\n\tinteractionPriority?: number;\n\t/** Priority for render-sync systems (default: 480, just before renderer2D's 500). */\n\trenderSyncPriority?: number;\n}\n\nexport function createUIPlugin<G extends string = 'ui'>(\n\toptions?: UIPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'ui' as G,\n\t\tanchorPriority = 0,\n\t\tinteractionPriority = 200,\n\t\trenderSyncPriority = 480,\n\t} = options ?? {};\n\n\tconst labelPool = new Map<number, UILabelRuntime>();\n\tconst panelPool = new Map<number, UIPanelRuntime>();\n\tconst progressPool = new Map<number, UIProgressRuntime>();\n\tconst messageLogPool = new Map<number, UIMessageLogRuntime>();\n\tconst scratchPos: Vector2D = { x: 0, y: 0 };\n\tconst scratchFill: FillRect = { x: 0, y: 0, width: 0, height: 0 };\n\t// Captured at init for the message-log sync, which (unlike other sync systems) must create\n\t// new Text/Container nodes during process() as fragments grow via appendLogLine.\n\tlet pixiModuleForMessageLog: typeof import('pixi.js') | null = null;\n\n\treturn definePlugin('ui')\n\t\t.withComponentTypes<UIComponentTypes>()\n\t\t.withEventTypes<UIEventTypes>()\n\t\t.withLabels<UILabels>()\n\t\t.withGroups<G>()\n\t\t.withReactiveQueryNames<'ui-labels' | 'ui-panels' | 'ui-progress-bars' | 'ui-message-logs'>()\n\t\t.requires<UIRequires>()\n\t\t.install((world) => {\n\t\t\tworld.registerRequired('uiElement', 'localTransform', (): LocalTransform => ({\n\t\t\t\tx: DEFAULT_LOCAL_TRANSFORM.x,\n\t\t\t\ty: DEFAULT_LOCAL_TRANSFORM.y,\n\t\t\t\trotation: DEFAULT_LOCAL_TRANSFORM.rotation,\n\t\t\t\tscaleX: DEFAULT_LOCAL_TRANSFORM.scaleX,\n\t\t\t\tscaleY: DEFAULT_LOCAL_TRANSFORM.scaleY,\n\t\t\t}));\n\t\t\tworld.registerRequired('uiButton', 'uiInteractive', () => ({}));\n\t\t\tworld.registerRequired('uiInteractive', 'uiInteraction', (): UIInteraction => ({ state: 'none' }));\n\n\t\t\t// Anchor resolve: writes localTransform.{x,y} from uiElement + bounds.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-anchor-resolve')\n\t\t\t\t.setPriority(anchorPriority)\n\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('uiElements', {\n\t\t\t\t\twith: ['uiElement', 'localTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst bounds = ecs.getResource('bounds');\n\t\t\t\t\tfor (const entity of queries.uiElements) {\n\t\t\t\t\t\tconst { uiElement, localTransform } = entity.components;\n\t\t\t\t\t\tresolveAnchorPosition(\n\t\t\t\t\t\t\tuiElement.anchor,\n\t\t\t\t\t\t\tuiElement.pivot,\n\t\t\t\t\t\t\tuiElement.offset,\n\t\t\t\t\t\t\tbounds,\n\t\t\t\t\t\t\tuiElement,\n\t\t\t\t\t\t\tscratchPos,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (localTransform.x !== scratchPos.x || localTransform.y !== scratchPos.y) {\n\t\t\t\t\t\t\tlocalTransform.x = scratchPos.x;\n\t\t\t\t\t\t\tlocalTransform.y = scratchPos.y;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Pointer hit-test: reads inputState.pointer, updates uiInteraction.state, emits events.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-interaction')\n\t\t\t\t.setPriority(interactionPriority)\n\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('interactables', {\n\t\t\t\t\twith: ['uiInteractive', 'uiInteraction', 'uiElement', 'worldTransform'],\n\t\t\t\t\twithout: ['uiDisabled'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst pointer = ecs.getResource('inputState').pointer;\n\t\t\t\t\tconst px = pointer.position.x;\n\t\t\t\t\tconst py = pointer.position.y;\n\t\t\t\t\tconst down = pointer.isDown(0);\n\t\t\t\t\tconst justReleased = pointer.justReleased(0);\n\t\t\t\t\tfor (const entity of queries.interactables) {\n\t\t\t\t\t\tconst { uiElement, worldTransform, uiInteraction } = entity.components;\n\t\t\t\t\t\tconst hit =\n\t\t\t\t\t\t\tpx >= worldTransform.x &&\n\t\t\t\t\t\t\tpx < worldTransform.x + uiElement.width &&\n\t\t\t\t\t\t\tpy >= worldTransform.y &&\n\t\t\t\t\t\t\tpy < worldTransform.y + uiElement.height;\n\t\t\t\t\t\tconst prev = uiInteraction.state;\n\t\t\t\t\t\tconst next: UIInteractionState =\n\t\t\t\t\t\t\t!hit ? 'none'\n\t\t\t\t\t\t\t: down ? (prev === 'none' ? 'hover' : 'pressed')\n\t\t\t\t\t\t\t: 'hover';\n\n\t\t\t\t\t\tif (prev === 'pressed' && next === 'hover' && justReleased && hit) {\n\t\t\t\t\t\t\tecs.eventBus.publish('uiButtonPressed', { entityId: entity.id });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (prev === 'none' && next !== 'none') {\n\t\t\t\t\t\t\tecs.eventBus.publish('uiButtonHovered', { entityId: entity.id, entered: true });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (prev !== 'none' && next === 'none') {\n\t\t\t\t\t\t\tecs.eventBus.publish('uiButtonHovered', { entityId: entity.id, entered: false });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (prev !== next) {\n\t\t\t\t\t\t\tuiInteraction.state = next;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'uiInteraction');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Panel sync: lazy-initialize PixiJS Graphics, redraw when panel or size changes.\n\t\t\t// Registered before labels/progress so panel Graphics sits BEHIND text/fill when an\n\t\t\t// entity carries multiple visual components (e.g. a button with uiPanel + uiLabel).\n\t\t\tworld\n\t\t\t\t.addSystem('ui-panel-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<Container>('rootContainer');\n\t\t\t\t\tecs.addReactiveQuery('ui-panels', {\n\t\t\t\t\t\twith: ['uiPanel', 'uiElement'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst g = new pixi.Graphics();\n\t\t\t\t\t\t\tpanelPool.set(entity.id, {\n\t\t\t\t\t\t\t\tpixiGraphics: g,\n\t\t\t\t\t\t\t\tlastFillColor: Number.NaN,\n\t\t\t\t\t\t\t\tlastBorderColor: undefined,\n\t\t\t\t\t\t\t\tlastBorderWidth: Number.NaN,\n\t\t\t\t\t\t\t\tlastWidth: Number.NaN,\n\t\t\t\t\t\t\t\tlastHeight: Number.NaN,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (rootContainer) rootContainer.addChild(g);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tconst runtime = panelPool.get(entityId);\n\t\t\t\t\t\t\tif (runtime) {\n\t\t\t\t\t\t\t\truntime.pixiGraphics.removeFromParent();\n\t\t\t\t\t\t\t\truntime.pixiGraphics.destroy();\n\t\t\t\t\t\t\t\tpanelPool.delete(entityId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tfor (const [entityId, runtime] of panelPool) {\n\t\t\t\t\t\tconst panel = ecs.getComponent(entityId, 'uiPanel');\n\t\t\t\t\t\tconst element = ecs.getComponent(entityId, 'uiElement');\n\t\t\t\t\t\tif (!panel || !element) continue;\n\t\t\t\t\t\tsyncPanelRuntime(runtime, panel, element);\n\t\t\t\t\t\tapplyTransform(runtime.pixiGraphics, ecs.getComponent(entityId, 'worldTransform'));\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Progress bar sync.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-progress-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<Container>('rootContainer');\n\t\t\t\t\tecs.addReactiveQuery('ui-progress-bars', {\n\t\t\t\t\t\twith: ['uiProgressBar', 'uiElement'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst g = new pixi.Graphics();\n\t\t\t\t\t\t\tprogressPool.set(entity.id, {\n\t\t\t\t\t\t\t\tpixiGraphics: g,\n\t\t\t\t\t\t\t\tlastValue: Number.NaN,\n\t\t\t\t\t\t\t\tlastMax: Number.NaN,\n\t\t\t\t\t\t\t\tlastFillColor: Number.NaN,\n\t\t\t\t\t\t\t\tlastBgColor: Number.NaN,\n\t\t\t\t\t\t\t\tlastDirection: 'ltr',\n\t\t\t\t\t\t\t\tlastWidth: Number.NaN,\n\t\t\t\t\t\t\t\tlastHeight: Number.NaN,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (rootContainer) rootContainer.addChild(g);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tconst runtime = progressPool.get(entityId);\n\t\t\t\t\t\t\tif (runtime) {\n\t\t\t\t\t\t\t\truntime.pixiGraphics.removeFromParent();\n\t\t\t\t\t\t\t\truntime.pixiGraphics.destroy();\n\t\t\t\t\t\t\t\tprogressPool.delete(entityId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tfor (const [entityId, runtime] of progressPool) {\n\t\t\t\t\t\tconst bar = ecs.getComponent(entityId, 'uiProgressBar');\n\t\t\t\t\t\tconst element = ecs.getComponent(entityId, 'uiElement');\n\t\t\t\t\t\tif (!bar || !element) continue;\n\t\t\t\t\t\tsyncProgressRuntime(runtime, bar, element, scratchFill);\n\t\t\t\t\t\tapplyTransform(runtime.pixiGraphics, ecs.getComponent(entityId, 'worldTransform'));\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Message log sync: registered between progress and label so log text sits over panels\n\t\t\t// but under foreground labels when widgets overlap.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-message-log-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tpixiModuleForMessageLog = pixi;\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<Container>('rootContainer');\n\t\t\t\t\tecs.addReactiveQuery('ui-message-logs', {\n\t\t\t\t\t\twith: ['uiMessageLog', 'uiElement'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst root = new pixi.Container();\n\t\t\t\t\t\t\tmessageLogPool.set(entity.id, {\n\t\t\t\t\t\t\t\trootContainer: root,\n\t\t\t\t\t\t\t\tlines: [],\n\t\t\t\t\t\t\t\tlastLinesRef: null,\n\t\t\t\t\t\t\t\tlastVisibleLines: -1,\n\t\t\t\t\t\t\t\tlastLineHeight: Number.NaN,\n\t\t\t\t\t\t\t\tlastFontFamily: '',\n\t\t\t\t\t\t\t\tlastFontSize: Number.NaN,\n\t\t\t\t\t\t\t\tlastAlign: '',\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (rootContainer) rootContainer.addChild(root);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tconst runtime = messageLogPool.get(entityId);\n\t\t\t\t\t\t\tif (runtime) {\n\t\t\t\t\t\t\t\truntime.rootContainer.removeFromParent();\n\t\t\t\t\t\t\t\truntime.rootContainer.destroy({ children: true });\n\t\t\t\t\t\t\t\tmessageLogPool.delete(entityId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tif (!pixiModuleForMessageLog) return;\n\t\t\t\t\tfor (const [entityId, runtime] of messageLogPool) {\n\t\t\t\t\t\tconst log = ecs.getComponent(entityId, 'uiMessageLog');\n\t\t\t\t\t\tif (!log) continue;\n\t\t\t\t\t\tsyncMessageLogRuntime(runtime, log, pixiModuleForMessageLog);\n\t\t\t\t\t\tapplyTransform(runtime.rootContainer, ecs.getComponent(entityId, 'worldTransform'));\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Label sync: registered last so Text sits ON TOP of panels and progress bars when an\n\t\t\t// entity carries multiple visual components.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-label-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<Container>('rootContainer');\n\t\t\t\t\tecs.addReactiveQuery('ui-labels', {\n\t\t\t\t\t\twith: ['uiLabel'],\n\t\t\t\t\t\tonEnter: (entity) => {\n\t\t\t\t\t\t\tconst label = entity.components.uiLabel;\n\t\t\t\t\t\t\tconst text = new pixi.Text({\n\t\t\t\t\t\t\t\ttext: label.text,\n\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\tfontFamily: label.style.fontFamily,\n\t\t\t\t\t\t\t\t\tfontSize: label.style.fontSize,\n\t\t\t\t\t\t\t\t\tfill: label.style.fill,\n\t\t\t\t\t\t\t\t\talign: label.style.align,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tlabelPool.set(entity.id, {\n\t\t\t\t\t\t\t\tpixiText: text,\n\t\t\t\t\t\t\t\tlastText: label.text,\n\t\t\t\t\t\t\t\tlastFontSize: label.style.fontSize,\n\t\t\t\t\t\t\t\tlastFill: label.style.fill,\n\t\t\t\t\t\t\t\tlastAlign: label.style.align,\n\t\t\t\t\t\t\t\tlastFontFamily: label.style.fontFamily,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (rootContainer) rootContainer.addChild(text);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: (entityId) => {\n\t\t\t\t\t\t\tconst runtime = labelPool.get(entityId);\n\t\t\t\t\t\t\tif (runtime) {\n\t\t\t\t\t\t\t\truntime.pixiText.removeFromParent();\n\t\t\t\t\t\t\t\truntime.pixiText.destroy();\n\t\t\t\t\t\t\t\tlabelPool.delete(entityId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tfor (const [entityId, runtime] of labelPool) {\n\t\t\t\t\t\tconst label = ecs.getComponent(entityId, 'uiLabel');\n\t\t\t\t\t\tif (!label) continue;\n\t\t\t\t\t\tsyncLabelRuntime(runtime, label);\n\t\t\t\t\t\tapplyTransform(runtime.pixiText, ecs.getComponent(entityId, 'worldTransform'));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Sync Helpers ====================\n\nfunction applyTransform(\n\tobj: { position: { x: number; y: number } },\n\twt: WorldTransform | undefined,\n): void {\n\tif (!wt) return;\n\tif (obj.position.x !== wt.x) obj.position.x = wt.x;\n\tif (obj.position.y !== wt.y) obj.position.y = wt.y;\n}\n\nfunction syncLabelRuntime(runtime: UILabelRuntime, label: UILabel): void {\n\tif (runtime.lastText !== label.text) {\n\t\truntime.pixiText.text = label.text;\n\t\truntime.lastText = label.text;\n\t}\n\tconst style = runtime.pixiText.style;\n\tif (runtime.lastFontSize !== label.style.fontSize) {\n\t\tstyle.fontSize = label.style.fontSize;\n\t\truntime.lastFontSize = label.style.fontSize;\n\t}\n\tif (runtime.lastFill !== label.style.fill) {\n\t\tstyle.fill = label.style.fill;\n\t\truntime.lastFill = label.style.fill;\n\t}\n\tif (runtime.lastAlign !== label.style.align) {\n\t\tstyle.align = label.style.align;\n\t\truntime.lastAlign = label.style.align;\n\t}\n\tif (runtime.lastFontFamily !== label.style.fontFamily) {\n\t\tstyle.fontFamily = label.style.fontFamily;\n\t\truntime.lastFontFamily = label.style.fontFamily;\n\t}\n}\n\nfunction syncPanelRuntime(runtime: UIPanelRuntime, panel: UIPanel, element: UIElement): void {\n\tconst changed =\n\t\truntime.lastFillColor !== panel.fillColor ||\n\t\truntime.lastBorderColor !== panel.borderColor ||\n\t\truntime.lastBorderWidth !== panel.borderWidth ||\n\t\truntime.lastWidth !== element.width ||\n\t\truntime.lastHeight !== element.height;\n\tif (!changed) return;\n\n\tconst g = runtime.pixiGraphics;\n\tg.clear();\n\tg.rect(0, 0, element.width, element.height);\n\tg.fill({ color: panel.fillColor });\n\tif (panel.borderColor !== undefined && panel.borderWidth > 0) {\n\t\tg.stroke({ color: panel.borderColor, width: panel.borderWidth });\n\t}\n\truntime.lastFillColor = panel.fillColor;\n\truntime.lastBorderColor = panel.borderColor;\n\truntime.lastBorderWidth = panel.borderWidth;\n\truntime.lastWidth = element.width;\n\truntime.lastHeight = element.height;\n}\n\nfunction syncProgressRuntime(\n\truntime: UIProgressRuntime,\n\tbar: UIProgressBar,\n\telement: UIElement,\n\tscratchFill: FillRect,\n): void {\n\tconst changed =\n\t\truntime.lastValue !== bar.value ||\n\t\truntime.lastMax !== bar.max ||\n\t\truntime.lastFillColor !== bar.fillColor ||\n\t\truntime.lastBgColor !== bar.bgColor ||\n\t\truntime.lastDirection !== bar.direction ||\n\t\truntime.lastWidth !== element.width ||\n\t\truntime.lastHeight !== element.height;\n\tif (!changed) return;\n\n\tconst clamped = clampProgressValue(bar.value, bar.max);\n\tconst ratio = bar.max > 0 ? clamped / bar.max : 0;\n\tcomputeProgressFillRect(element.width, element.height, ratio, bar.direction, scratchFill);\n\n\tconst g = runtime.pixiGraphics;\n\tg.clear();\n\tg.rect(0, 0, element.width, element.height);\n\tg.fill({ color: bar.bgColor });\n\tif (scratchFill.width > 0 && scratchFill.height > 0) {\n\t\tg.rect(scratchFill.x, scratchFill.y, scratchFill.width, scratchFill.height);\n\t\tg.fill({ color: bar.fillColor });\n\t}\n\n\truntime.lastValue = bar.value;\n\truntime.lastMax = bar.max;\n\truntime.lastFillColor = bar.fillColor;\n\truntime.lastBgColor = bar.bgColor;\n\truntime.lastDirection = bar.direction;\n\truntime.lastWidth = element.width;\n\truntime.lastHeight = element.height;\n}\n\nfunction syncMessageLogRuntime(\n\truntime: UIMessageLogRuntime,\n\tlog: UIMessageLog,\n\tpixi: typeof import('pixi.js'),\n): void {\n\tconst styleChanged =\n\t\truntime.lastFontFamily !== log.style.fontFamily ||\n\t\truntime.lastFontSize !== log.style.fontSize ||\n\t\truntime.lastAlign !== log.style.align;\n\tconst linesChanged = runtime.lastLinesRef !== log.lines;\n\tconst layoutChanged =\n\t\truntime.lastVisibleLines !== log.visibleLines ||\n\t\truntime.lastLineHeight !== log.lineHeight;\n\tif (!linesChanged && !layoutChanged && !styleChanged) return;\n\n\tconst visible = log.lines.slice(-log.visibleLines);\n\n\twhile (runtime.lines.length < visible.length) {\n\t\tconst container = new pixi.Container();\n\t\truntime.rootContainer.addChild(container);\n\t\truntime.lines.push({ container, texts: [] });\n\t}\n\truntime.lines.forEach((line, index) => {\n\t\tif (index >= visible.length) line.container.visible = false;\n\t});\n\n\tvisible.forEach((fragments, lineIndex) => {\n\t\tconst line = runtime.lines[lineIndex];\n\t\tif (!line) return;\n\t\tline.container.visible = true;\n\t\tline.container.position.y = lineIndex * log.lineHeight;\n\n\t\twhile (line.texts.length < fragments.length) {\n\t\t\tconst t = new pixi.Text({\n\t\t\t\ttext: '',\n\t\t\t\tstyle: {\n\t\t\t\t\tfontFamily: log.style.fontFamily,\n\t\t\t\t\tfontSize: log.style.fontSize,\n\t\t\t\t\tfill: 0xffffff,\n\t\t\t\t\talign: log.style.align,\n\t\t\t\t},\n\t\t\t});\n\t\t\tline.container.addChild(t);\n\t\t\tline.texts.push(t);\n\t\t}\n\n\t\tlet cursorX = 0;\n\t\tline.texts.forEach((t, j) => {\n\t\t\tconst frag = fragments[j];\n\t\t\tif (!frag) {\n\t\t\t\tt.visible = false;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tt.visible = true;\n\t\t\tif (t.text !== frag.text) t.text = frag.text;\n\t\t\tif (t.style.fill !== frag.color) t.style.fill = frag.color;\n\t\t\tt.position.x = cursorX;\n\t\t\tcursorX += t.width;\n\t\t});\n\t});\n\n\t// Style fields are shared across all fragments — apply once per Text when they change,\n\t// keeping the per-fragment loop above limited to per-fragment text + color writes.\n\tif (styleChanged) {\n\t\truntime.lines.forEach((line) => {\n\t\t\tline.texts.forEach((t) => {\n\t\t\t\tconst s = t.style;\n\t\t\t\tif (s.fontFamily !== log.style.fontFamily) s.fontFamily = log.style.fontFamily;\n\t\t\t\tif (s.fontSize !== log.style.fontSize) s.fontSize = log.style.fontSize;\n\t\t\t\tif (s.align !== log.style.align) s.align = log.style.align;\n\t\t\t});\n\t\t});\n\t}\n\n\truntime.lastLinesRef = log.lines;\n\truntime.lastVisibleLines = log.visibleLines;\n\truntime.lastLineHeight = log.lineHeight;\n\truntime.lastFontFamily = log.style.fontFamily;\n\truntime.lastFontSize = log.style.fontSize;\n\truntime.lastAlign = log.style.align;\n}\n"
6
+ "/**\n * UI / HUD Plugin for ECSpresso.\n *\n * Screen-space primitives:\n * - `uiElement` — anchor/pivot/offset positioning resolved against the `bounds` resource\n * - `uiLabel` — PixiJS Text\n * - `uiPanel` — PixiJS Graphics rectangle with optional border\n * - `uiProgressBar` — PixiJS Graphics value indicator with four fill directions\n *\n * Pointer interaction (buttons):\n * - `uiInteractive` (marker) opts an entity into hit-testing\n * - `uiInteraction.state` — `'none' | 'hover' | 'pressed'` (Bevy-style single enum)\n * - `uiButton` marker composes `uiInteractive` + `uiInteraction`\n * - `uiDisabled` skips hit-testing entirely\n * - Emits `uiButtonPressed` (confirmed down→up on same widget) and `uiButtonHovered`\n *\n * Depends on `renderer2D` (for the `bounds` resource + scene graph + screen-space layer),\n * the transform plugin (bundled by renderer2D), and the input plugin.\n *\n * Future phases will add the message log (Phase 3).\n */\n\nimport type { Container, Graphics, Text } from 'pixi.js';\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsConfig, ResourcesConfig } from '../../type-utils';\nimport type { Vector2D } from '../../utils/math';\nimport {\n\tDEFAULT_LOCAL_TRANSFORM,\n\ttype LocalTransform,\n\ttype TransformComponentTypes,\n\ttype WorldTransform,\n} from '../spatial/transform';\nimport type { BoundsResourceTypes } from '../spatial/bounds';\nimport type { InputResourceTypes } from '../input/input';\n\n// ==================== Anchor Presets ====================\n\nexport type AnchorPreset =\n\t| 'top-left' | 'top-center' | 'top-right'\n\t| 'center-left' | 'center' | 'center-right'\n\t| 'bottom-left' | 'bottom-center' | 'bottom-right';\n\nexport const ANCHOR_PRESETS: Readonly<Record<AnchorPreset, Readonly<Vector2D>>> = Object.freeze({\n\t'top-left': Object.freeze({ x: 0, y: 0 }),\n\t'top-center': Object.freeze({ x: 0.5, y: 0 }),\n\t'top-right': Object.freeze({ x: 1, y: 0 }),\n\t'center-left': Object.freeze({ x: 0, y: 0.5 }),\n\t'center': Object.freeze({ x: 0.5, y: 0.5 }),\n\t'center-right': Object.freeze({ x: 1, y: 0.5 }),\n\t'bottom-left': Object.freeze({ x: 0, y: 1 }),\n\t'bottom-center': Object.freeze({ x: 0.5, y: 1 }),\n\t'bottom-right': Object.freeze({ x: 1, y: 1 }),\n});\n\nexport type AnchorInput = AnchorPreset | Vector2D;\n\n/** Resolve a preset string or vec2 into a mutable Vector2D copy. */\nexport function resolveAnchorPreset(input: AnchorInput): Vector2D {\n\tif (typeof input === 'string') {\n\t\tconst preset = ANCHOR_PRESETS[input];\n\t\treturn { x: preset.x, y: preset.y };\n\t}\n\treturn { x: input.x, y: input.y };\n}\n\n/**\n * Write the top-left screen position of a widget into `out`.\n *\n * Formula: position = anchor * bounds + offset - pivot * size.\n * `anchor` specifies where on the canvas the widget attaches (0..1 normalized).\n * `pivot` specifies where on the widget that attachment point lands (0..1 normalized).\n * Writes in place to avoid per-frame allocation.\n */\nexport function resolveAnchorPosition(\n\tanchor: Readonly<Vector2D>,\n\tpivot: Readonly<Vector2D>,\n\toffset: Readonly<Vector2D>,\n\tbounds: Readonly<{ width: number; height: number }>,\n\tsize: Readonly<{ width: number; height: number }>,\n\tout: Vector2D,\n): void {\n\tout.x = anchor.x * bounds.width + offset.x - pivot.x * size.width;\n\tout.y = anchor.y * bounds.height + offset.y - pivot.y * size.height;\n}\n\n// ==================== Progress Bar Math ====================\n\nexport type ProgressDirection = 'ltr' | 'rtl' | 'ttb' | 'btt';\n\nexport interface FillRect {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n}\n\nexport function clampProgressValue(value: number, max: number): number {\n\tif (max <= 0) return 0;\n\tif (value <= 0) return 0;\n\tif (value >= max) return max;\n\treturn value;\n}\n\ntype FillComputer = (w: number, h: number, ratio: number, out: FillRect) => void;\n\nconst FILL_COMPUTERS: Readonly<Record<ProgressDirection, FillComputer>> = {\n\tltr: (w, h, r, o) => { o.x = 0; o.y = 0; o.width = w * r; o.height = h; },\n\trtl: (w, h, r, o) => { o.x = w * (1 - r); o.y = 0; o.width = w * r; o.height = h; },\n\tttb: (w, h, r, o) => { o.x = 0; o.y = 0; o.width = w; o.height = h * r; },\n\tbtt: (w, h, r, o) => { o.x = 0; o.y = h * (1 - r); o.width = w; o.height = h * r; },\n};\n\nexport function computeProgressFillRect(\n\twidth: number,\n\theight: number,\n\tratio: number,\n\tdirection: ProgressDirection,\n\tout: FillRect,\n): void {\n\tFILL_COMPUTERS[direction](width, height, ratio, out);\n}\n\n// ==================== Component Types ====================\n\nexport interface UIElement {\n\tanchor: Vector2D;\n\tpivot: Vector2D;\n\toffset: Vector2D;\n\twidth: number;\n\theight: number;\n}\n\nexport interface UITextStyle {\n\tfontFamily: string;\n\tfontSize: number;\n\tfill: number;\n\talign: 'left' | 'center' | 'right';\n}\n\nexport interface UILabel {\n\ttext: string;\n\tstyle: UITextStyle;\n}\n\nexport interface UIPanel {\n\tfillColor: number;\n\tborderColor?: number;\n\tborderWidth: number;\n}\n\nexport interface UIProgressBar {\n\tvalue: number;\n\tmax: number;\n\tfillColor: number;\n\tbgColor: number;\n\tdirection: ProgressDirection;\n}\n\nexport type UIInteractionState = 'none' | 'hover' | 'pressed';\n\nexport interface UIInteraction {\n\tstate: UIInteractionState;\n}\n\nexport interface LogFragment {\n\ttext: string;\n\tcolor: number;\n}\n\nexport interface UIMessageLog {\n\tlines: LogFragment[][];\n\tmaxLines: number;\n\tvisibleLines: number;\n\tlineHeight: number;\n\tstyle: UITextStyle;\n}\n\nexport interface UIComponentTypes {\n\tuiElement: UIElement;\n\tuiLabel: UILabel;\n\tuiPanel: UIPanel;\n\tuiProgressBar: UIProgressBar;\n\tuiButton: {};\n\tuiInteractive: {};\n\tuiInteraction: UIInteraction;\n\tuiDisabled: {};\n\tuiMessageLog: UIMessageLog;\n}\n\n// ==================== Event Types ====================\n\nexport interface UIButtonPressedEvent {\n\tentityId: number;\n}\n\nexport interface UIButtonHoveredEvent {\n\tentityId: number;\n\tentered: boolean;\n}\n\nexport interface UIMessageLogAppendedEvent {\n\tentityId: number;\n\tline: LogFragment[];\n}\n\nexport interface UIEventTypes {\n\tuiButtonPressed: UIButtonPressedEvent;\n\tuiButtonHovered: UIButtonHoveredEvent;\n\tuiLogAppended: UIMessageLogAppendedEvent;\n}\n\n// ==================== Component Factories ====================\n\nconst DEFAULT_TEXT_STYLE: Readonly<UITextStyle> = Object.freeze({\n\tfontFamily: 'sans-serif',\n\tfontSize: 16,\n\tfill: 0xffffff,\n\talign: 'left',\n});\n\nexport interface CreateUIElementInput {\n\tanchor: AnchorInput;\n\tpivot?: AnchorInput;\n\toffset?: Vector2D;\n\twidth: number;\n\theight: number;\n}\n\nexport function createUIElement(input: CreateUIElementInput): Pick<UIComponentTypes, 'uiElement'> {\n\tconst anchor = resolveAnchorPreset(input.anchor);\n\tconst pivot = input.pivot === undefined ? { x: anchor.x, y: anchor.y } : resolveAnchorPreset(input.pivot);\n\tconst offset = input.offset === undefined ? { x: 0, y: 0 } : { x: input.offset.x, y: input.offset.y };\n\treturn {\n\t\tuiElement: {\n\t\t\tanchor,\n\t\t\tpivot,\n\t\t\toffset,\n\t\t\twidth: input.width,\n\t\t\theight: input.height,\n\t\t},\n\t};\n}\n\nexport function createUILabel(\n\ttext: string,\n\tstyle?: Partial<UITextStyle>,\n): Pick<UIComponentTypes, 'uiLabel'> {\n\treturn {\n\t\tuiLabel: {\n\t\t\ttext,\n\t\t\tstyle: { ...DEFAULT_TEXT_STYLE, ...style },\n\t\t},\n\t};\n}\n\nexport interface CreateUIPanelInput {\n\tfillColor: number;\n\tborderColor?: number;\n\tborderWidth?: number;\n}\n\nexport function createUIPanel(input: CreateUIPanelInput): Pick<UIComponentTypes, 'uiPanel'> {\n\treturn {\n\t\tuiPanel: {\n\t\t\tfillColor: input.fillColor,\n\t\t\tborderColor: input.borderColor,\n\t\t\tborderWidth: input.borderWidth ?? 0,\n\t\t},\n\t};\n}\n\nexport interface CreateUIProgressBarInput {\n\tvalue: number;\n\tmax: number;\n\tfillColor: number;\n\tbgColor: number;\n\tdirection?: ProgressDirection;\n}\n\nexport function createUIProgressBar(input: CreateUIProgressBarInput): Pick<UIComponentTypes, 'uiProgressBar'> {\n\treturn {\n\t\tuiProgressBar: {\n\t\t\tvalue: input.value,\n\t\t\tmax: input.max,\n\t\t\tfillColor: input.fillColor,\n\t\t\tbgColor: input.bgColor,\n\t\t\tdirection: input.direction ?? 'ltr',\n\t\t},\n\t};\n}\n\nexport interface CreateUIMessageLogInput {\n\tmaxLines: number;\n\tvisibleLines: number;\n\tlineHeight: number;\n\tstyle?: Partial<UITextStyle>;\n\tinitialLines?: LogFragment[][];\n}\n\nexport function createUIMessageLog(input: CreateUIMessageLogInput): Pick<UIComponentTypes, 'uiMessageLog'> {\n\treturn {\n\t\tuiMessageLog: {\n\t\t\tlines: input.initialLines === undefined ? [] : [...input.initialLines],\n\t\t\tmaxLines: input.maxLines,\n\t\t\tvisibleLines: input.visibleLines,\n\t\t\tlineHeight: input.lineHeight,\n\t\t\tstyle: { ...DEFAULT_TEXT_STYLE, ...input.style },\n\t\t},\n\t};\n}\n\nexport function createUIInteractive(): Pick<UIComponentTypes, 'uiInteractive'> {\n\treturn { uiInteractive: {} };\n}\n\nexport function createUIButton(): Pick<UIComponentTypes, 'uiButton'> {\n\treturn { uiButton: {} };\n}\n\nexport function createUIDisabled(): Pick<UIComponentTypes, 'uiDisabled'> {\n\treturn { uiDisabled: {} };\n}\n\n// ==================== Message Log Helper ====================\n\n/** Structural ECS surface for `appendLogLine`; mirrors the `CoroutineWorld` pattern. */\nexport interface MessageLogWorld {\n\tcommands: {\n\t\tmutateComponent(\n\t\t\tentityId: number,\n\t\t\tcomponentName: 'uiMessageLog',\n\t\t\tmutator: (value: UIMessageLog) => void,\n\t\t): void;\n\t};\n\teventBus: {\n\t\tpublish(event: 'uiLogAppended', payload: UIMessageLogAppendedEvent): void;\n\t};\n}\n\n/**\n * Append a line (vector of fragments) to a `uiMessageLog` entity.\n *\n * Queues a buffered mutation that swaps `lines` for a fresh array (FIFO-truncated to\n * `maxLines`) — the array-identity change is the sync system's redraw signal — and\n * synchronously publishes `uiLogAppended` carrying the line for entry-animation\n * listeners. Safe to call from inside a system process callback.\n */\nexport function appendLogLine(\n\tecs: MessageLogWorld,\n\tentityId: number,\n\tline: LogFragment[],\n): void {\n\tecs.commands.mutateComponent(entityId, 'uiMessageLog', (log) => {\n\t\tconst cap = Math.max(0, log.maxLines);\n\t\tconst next = [...log.lines, line];\n\t\tlog.lines = next.length > cap ? next.slice(next.length - cap) : next;\n\t});\n\tecs.eventBus.publish('uiLogAppended', { entityId, line });\n}\n\n// ==================== Runtime Data (Side Storage) ====================\n\ninterface UILabelRuntime {\n\tpixiText: Text;\n\tlastText: string;\n\tlastFontSize: number;\n\tlastFill: number;\n\tlastAlign: string;\n\tlastFontFamily: string;\n}\n\ninterface UIPanelRuntime {\n\tpixiGraphics: Graphics;\n\tlastFillColor: number;\n\tlastBorderColor: number | undefined;\n\tlastBorderWidth: number;\n\tlastWidth: number;\n\tlastHeight: number;\n}\n\ninterface UIProgressRuntime {\n\tpixiGraphics: Graphics;\n\tlastValue: number;\n\tlastMax: number;\n\tlastFillColor: number;\n\tlastBgColor: number;\n\tlastDirection: ProgressDirection;\n\tlastWidth: number;\n\tlastHeight: number;\n}\n\ninterface UIMessageLogLineRuntime {\n\tcontainer: Container;\n\ttexts: Text[];\n}\n\ninterface UIMessageLogRuntime {\n\trootContainer: Container;\n\tlines: UIMessageLogLineRuntime[];\n\tlastLinesRef: LogFragment[][] | null;\n\tlastVisibleLines: number;\n\tlastLineHeight: number;\n\tlastFontFamily: string;\n\tlastFontSize: number;\n\tlastAlign: string;\n}\n\n// ==================== Plugin Factory ====================\n\ntype UIRequires =\n\tComponentsConfig<TransformComponentTypes>\n\t& ResourcesConfig<BoundsResourceTypes & InputResourceTypes>;\n\ntype UILabels =\n\t| 'ui-anchor-resolve'\n\t| 'ui-interaction'\n\t| 'ui-label-sync'\n\t| 'ui-panel-sync'\n\t| 'ui-progress-sync'\n\t| 'ui-message-log-sync';\n\nexport interface UIPluginOptions<G extends string = 'ui'> extends BasePluginOptions<G> {\n\t/** Priority for the anchor-resolve system in preUpdate (default: 0). */\n\tanchorPriority?: number;\n\t/** Priority for the pointer hit-test system in preUpdate (default: 200, after input's 100). */\n\tinteractionPriority?: number;\n\t/** Priority for render-sync systems (default: 480, just before renderer2D's 500). */\n\trenderSyncPriority?: number;\n}\n\nexport function createUIPlugin<G extends string = 'ui'>(\n\toptions?: UIPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'ui' as G,\n\t\tanchorPriority = 0,\n\t\tinteractionPriority = 200,\n\t\trenderSyncPriority = 480,\n\t} = options ?? {};\n\n\tconst labelPool = new Map<number, UILabelRuntime>();\n\tconst panelPool = new Map<number, UIPanelRuntime>();\n\tconst progressPool = new Map<number, UIProgressRuntime>();\n\tconst messageLogPool = new Map<number, UIMessageLogRuntime>();\n\tconst scratchPos: Vector2D = { x: 0, y: 0 };\n\tconst scratchFill: FillRect = { x: 0, y: 0, width: 0, height: 0 };\n\t// Captured at init for the message-log sync, which (unlike other sync systems) must create\n\t// new Text/Container nodes during process() as fragments grow via appendLogLine.\n\tlet pixiModuleForMessageLog: typeof import('pixi.js') | null = null;\n\n\treturn definePlugin('ui')\n\t\t.withComponentTypes<UIComponentTypes>()\n\t\t.withEventTypes<UIEventTypes>()\n\t\t.withLabels<UILabels>()\n\t\t.withGroups<G>()\n\t\t.withReactiveQueryNames<'ui-labels' | 'ui-panels' | 'ui-progress-bars' | 'ui-message-logs'>()\n\t\t.requires<UIRequires>()\n\t\t.install((world) => {\n\t\t\tworld.registerRequired('uiElement', 'localTransform', (): LocalTransform => ({\n\t\t\t\tx: DEFAULT_LOCAL_TRANSFORM.x,\n\t\t\t\ty: DEFAULT_LOCAL_TRANSFORM.y,\n\t\t\t\trotation: DEFAULT_LOCAL_TRANSFORM.rotation,\n\t\t\t\tscaleX: DEFAULT_LOCAL_TRANSFORM.scaleX,\n\t\t\t\tscaleY: DEFAULT_LOCAL_TRANSFORM.scaleY,\n\t\t\t}));\n\t\t\tworld.registerRequired('uiButton', 'uiInteractive', () => ({}));\n\t\t\tworld.registerRequired('uiInteractive', 'uiInteraction', (): UIInteraction => ({ state: 'none' }));\n\n\t\t\t// Anchor resolve: writes localTransform.{x,y} from uiElement + bounds.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-anchor-resolve')\n\t\t\t\t.setPriority(anchorPriority)\n\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('uiElements', {\n\t\t\t\t\twith: ['uiElement', 'localTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst bounds = ecs.getResource('bounds');\n\t\t\t\t\tfor (const entity of queries.uiElements) {\n\t\t\t\t\t\tconst { uiElement, localTransform } = entity.components;\n\t\t\t\t\t\tresolveAnchorPosition(\n\t\t\t\t\t\t\tuiElement.anchor,\n\t\t\t\t\t\t\tuiElement.pivot,\n\t\t\t\t\t\t\tuiElement.offset,\n\t\t\t\t\t\t\tbounds,\n\t\t\t\t\t\t\tuiElement,\n\t\t\t\t\t\t\tscratchPos,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (localTransform.x !== scratchPos.x || localTransform.y !== scratchPos.y) {\n\t\t\t\t\t\t\tlocalTransform.x = scratchPos.x;\n\t\t\t\t\t\t\tlocalTransform.y = scratchPos.y;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Pointer hit-test: reads inputState.pointer, updates uiInteraction.state, emits events.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-interaction')\n\t\t\t\t.setPriority(interactionPriority)\n\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('interactables', {\n\t\t\t\t\twith: ['uiInteractive', 'uiInteraction', 'uiElement', 'worldTransform'],\n\t\t\t\t\twithout: ['uiDisabled'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst pointer = ecs.getResource('inputState').pointer;\n\t\t\t\t\tconst px = pointer.position.x;\n\t\t\t\t\tconst py = pointer.position.y;\n\t\t\t\t\tconst down = pointer.isDown(0);\n\t\t\t\t\tconst justReleased = pointer.justReleased(0);\n\t\t\t\t\tfor (const entity of queries.interactables) {\n\t\t\t\t\t\tconst { uiElement, worldTransform, uiInteraction } = entity.components;\n\t\t\t\t\t\tconst hit =\n\t\t\t\t\t\t\tpx >= worldTransform.x &&\n\t\t\t\t\t\t\tpx < worldTransform.x + uiElement.width &&\n\t\t\t\t\t\t\tpy >= worldTransform.y &&\n\t\t\t\t\t\t\tpy < worldTransform.y + uiElement.height;\n\t\t\t\t\t\tconst prev = uiInteraction.state;\n\t\t\t\t\t\tconst next: UIInteractionState =\n\t\t\t\t\t\t\t!hit ? 'none'\n\t\t\t\t\t\t\t: down ? (prev === 'none' ? 'hover' : 'pressed')\n\t\t\t\t\t\t\t: 'hover';\n\n\t\t\t\t\t\tif (prev === 'pressed' && next === 'hover' && justReleased && hit) {\n\t\t\t\t\t\t\tecs.eventBus.publish('uiButtonPressed', { entityId: entity.id });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (prev === 'none' && next !== 'none') {\n\t\t\t\t\t\t\tecs.eventBus.publish('uiButtonHovered', { entityId: entity.id, entered: true });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (prev !== 'none' && next === 'none') {\n\t\t\t\t\t\t\tecs.eventBus.publish('uiButtonHovered', { entityId: entity.id, entered: false });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (prev !== next) {\n\t\t\t\t\t\t\tuiInteraction.state = next;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'uiInteraction');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Panel sync: lazy-initialize PixiJS Graphics, redraw when panel or size changes.\n\t\t\t// Registered before labels/progress so panel Graphics sits BEHIND text/fill when an\n\t\t\t// entity carries multiple visual components (e.g. a button with uiPanel + uiLabel).\n\t\t\tworld\n\t\t\t\t.addSystem('ui-panel-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<Container>('rootContainer');\n\t\t\t\t\tecs.addReactiveQuery('ui-panels', {\n\t\t\t\t\t\twith: ['uiPanel', 'uiElement'],\n\t\t\t\t\t\tonEnter: ({ entity }) => {\n\t\t\t\t\t\t\tconst g = new pixi.Graphics();\n\t\t\t\t\t\t\tpanelPool.set(entity.id, {\n\t\t\t\t\t\t\t\tpixiGraphics: g,\n\t\t\t\t\t\t\t\tlastFillColor: Number.NaN,\n\t\t\t\t\t\t\t\tlastBorderColor: undefined,\n\t\t\t\t\t\t\t\tlastBorderWidth: Number.NaN,\n\t\t\t\t\t\t\t\tlastWidth: Number.NaN,\n\t\t\t\t\t\t\t\tlastHeight: Number.NaN,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (rootContainer) rootContainer.addChild(g);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: ({ entityId }) => {\n\t\t\t\t\t\t\tconst runtime = panelPool.get(entityId);\n\t\t\t\t\t\t\tif (runtime) {\n\t\t\t\t\t\t\t\truntime.pixiGraphics.removeFromParent();\n\t\t\t\t\t\t\t\truntime.pixiGraphics.destroy();\n\t\t\t\t\t\t\t\tpanelPool.delete(entityId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tfor (const [entityId, runtime] of panelPool) {\n\t\t\t\t\t\tconst panel = ecs.getComponent(entityId, 'uiPanel');\n\t\t\t\t\t\tconst element = ecs.getComponent(entityId, 'uiElement');\n\t\t\t\t\t\tif (!panel || !element) continue;\n\t\t\t\t\t\tsyncPanelRuntime(runtime, panel, element);\n\t\t\t\t\t\tapplyTransform(runtime.pixiGraphics, ecs.getComponent(entityId, 'worldTransform'));\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Progress bar sync.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-progress-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<Container>('rootContainer');\n\t\t\t\t\tecs.addReactiveQuery('ui-progress-bars', {\n\t\t\t\t\t\twith: ['uiProgressBar', 'uiElement'],\n\t\t\t\t\t\tonEnter: ({ entity }) => {\n\t\t\t\t\t\t\tconst g = new pixi.Graphics();\n\t\t\t\t\t\t\tprogressPool.set(entity.id, {\n\t\t\t\t\t\t\t\tpixiGraphics: g,\n\t\t\t\t\t\t\t\tlastValue: Number.NaN,\n\t\t\t\t\t\t\t\tlastMax: Number.NaN,\n\t\t\t\t\t\t\t\tlastFillColor: Number.NaN,\n\t\t\t\t\t\t\t\tlastBgColor: Number.NaN,\n\t\t\t\t\t\t\t\tlastDirection: 'ltr',\n\t\t\t\t\t\t\t\tlastWidth: Number.NaN,\n\t\t\t\t\t\t\t\tlastHeight: Number.NaN,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (rootContainer) rootContainer.addChild(g);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: ({ entityId }) => {\n\t\t\t\t\t\t\tconst runtime = progressPool.get(entityId);\n\t\t\t\t\t\t\tif (runtime) {\n\t\t\t\t\t\t\t\truntime.pixiGraphics.removeFromParent();\n\t\t\t\t\t\t\t\truntime.pixiGraphics.destroy();\n\t\t\t\t\t\t\t\tprogressPool.delete(entityId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tfor (const [entityId, runtime] of progressPool) {\n\t\t\t\t\t\tconst bar = ecs.getComponent(entityId, 'uiProgressBar');\n\t\t\t\t\t\tconst element = ecs.getComponent(entityId, 'uiElement');\n\t\t\t\t\t\tif (!bar || !element) continue;\n\t\t\t\t\t\tsyncProgressRuntime(runtime, bar, element, scratchFill);\n\t\t\t\t\t\tapplyTransform(runtime.pixiGraphics, ecs.getComponent(entityId, 'worldTransform'));\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Message log sync: registered between progress and label so log text sits over panels\n\t\t\t// but under foreground labels when widgets overlap.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-message-log-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tpixiModuleForMessageLog = pixi;\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<Container>('rootContainer');\n\t\t\t\t\tecs.addReactiveQuery('ui-message-logs', {\n\t\t\t\t\t\twith: ['uiMessageLog', 'uiElement'],\n\t\t\t\t\t\tonEnter: ({ entity }) => {\n\t\t\t\t\t\t\tconst root = new pixi.Container();\n\t\t\t\t\t\t\tmessageLogPool.set(entity.id, {\n\t\t\t\t\t\t\t\trootContainer: root,\n\t\t\t\t\t\t\t\tlines: [],\n\t\t\t\t\t\t\t\tlastLinesRef: null,\n\t\t\t\t\t\t\t\tlastVisibleLines: -1,\n\t\t\t\t\t\t\t\tlastLineHeight: Number.NaN,\n\t\t\t\t\t\t\t\tlastFontFamily: '',\n\t\t\t\t\t\t\t\tlastFontSize: Number.NaN,\n\t\t\t\t\t\t\t\tlastAlign: '',\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (rootContainer) rootContainer.addChild(root);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: ({ entityId }) => {\n\t\t\t\t\t\t\tconst runtime = messageLogPool.get(entityId);\n\t\t\t\t\t\t\tif (runtime) {\n\t\t\t\t\t\t\t\truntime.rootContainer.removeFromParent();\n\t\t\t\t\t\t\t\truntime.rootContainer.destroy({ children: true });\n\t\t\t\t\t\t\t\tmessageLogPool.delete(entityId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tif (!pixiModuleForMessageLog) return;\n\t\t\t\t\tfor (const [entityId, runtime] of messageLogPool) {\n\t\t\t\t\t\tconst log = ecs.getComponent(entityId, 'uiMessageLog');\n\t\t\t\t\t\tif (!log) continue;\n\t\t\t\t\t\tsyncMessageLogRuntime(runtime, log, pixiModuleForMessageLog);\n\t\t\t\t\t\tapplyTransform(runtime.rootContainer, ecs.getComponent(entityId, 'worldTransform'));\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Label sync: registered last so Text sits ON TOP of panels and progress bars when an\n\t\t\t// entity carries multiple visual components.\n\t\t\tworld\n\t\t\t\t.addSystem('ui-label-sync')\n\t\t\t\t.setPriority(renderSyncPriority)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(async (ecs) => {\n\t\t\t\t\tconst pixi = await import('pixi.js');\n\t\t\t\t\tconst rootContainer = ecs.tryGetResource<Container>('rootContainer');\n\t\t\t\t\tecs.addReactiveQuery('ui-labels', {\n\t\t\t\t\t\twith: ['uiLabel'],\n\t\t\t\t\t\tonEnter: ({ entity }) => {\n\t\t\t\t\t\t\tconst label = entity.components.uiLabel;\n\t\t\t\t\t\t\tconst text = new pixi.Text({\n\t\t\t\t\t\t\t\ttext: label.text,\n\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\tfontFamily: label.style.fontFamily,\n\t\t\t\t\t\t\t\t\tfontSize: label.style.fontSize,\n\t\t\t\t\t\t\t\t\tfill: label.style.fill,\n\t\t\t\t\t\t\t\t\talign: label.style.align,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tlabelPool.set(entity.id, {\n\t\t\t\t\t\t\t\tpixiText: text,\n\t\t\t\t\t\t\t\tlastText: label.text,\n\t\t\t\t\t\t\t\tlastFontSize: label.style.fontSize,\n\t\t\t\t\t\t\t\tlastFill: label.style.fill,\n\t\t\t\t\t\t\t\tlastAlign: label.style.align,\n\t\t\t\t\t\t\t\tlastFontFamily: label.style.fontFamily,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (rootContainer) rootContainer.addChild(text);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tonExit: ({ entityId }) => {\n\t\t\t\t\t\t\tconst runtime = labelPool.get(entityId);\n\t\t\t\t\t\t\tif (runtime) {\n\t\t\t\t\t\t\t\truntime.pixiText.removeFromParent();\n\t\t\t\t\t\t\t\truntime.pixiText.destroy();\n\t\t\t\t\t\t\t\tlabelPool.delete(entityId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tfor (const [entityId, runtime] of labelPool) {\n\t\t\t\t\t\tconst label = ecs.getComponent(entityId, 'uiLabel');\n\t\t\t\t\t\tif (!label) continue;\n\t\t\t\t\t\tsyncLabelRuntime(runtime, label);\n\t\t\t\t\t\tapplyTransform(runtime.pixiText, ecs.getComponent(entityId, 'worldTransform'));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Sync Helpers ====================\n\nfunction applyTransform(\n\tobj: { position: { x: number; y: number } },\n\twt: WorldTransform | undefined,\n): void {\n\tif (!wt) return;\n\tif (obj.position.x !== wt.x) obj.position.x = wt.x;\n\tif (obj.position.y !== wt.y) obj.position.y = wt.y;\n}\n\nfunction syncLabelRuntime(runtime: UILabelRuntime, label: UILabel): void {\n\tif (runtime.lastText !== label.text) {\n\t\truntime.pixiText.text = label.text;\n\t\truntime.lastText = label.text;\n\t}\n\tconst style = runtime.pixiText.style;\n\tif (runtime.lastFontSize !== label.style.fontSize) {\n\t\tstyle.fontSize = label.style.fontSize;\n\t\truntime.lastFontSize = label.style.fontSize;\n\t}\n\tif (runtime.lastFill !== label.style.fill) {\n\t\tstyle.fill = label.style.fill;\n\t\truntime.lastFill = label.style.fill;\n\t}\n\tif (runtime.lastAlign !== label.style.align) {\n\t\tstyle.align = label.style.align;\n\t\truntime.lastAlign = label.style.align;\n\t}\n\tif (runtime.lastFontFamily !== label.style.fontFamily) {\n\t\tstyle.fontFamily = label.style.fontFamily;\n\t\truntime.lastFontFamily = label.style.fontFamily;\n\t}\n}\n\nfunction syncPanelRuntime(runtime: UIPanelRuntime, panel: UIPanel, element: UIElement): void {\n\tconst changed =\n\t\truntime.lastFillColor !== panel.fillColor ||\n\t\truntime.lastBorderColor !== panel.borderColor ||\n\t\truntime.lastBorderWidth !== panel.borderWidth ||\n\t\truntime.lastWidth !== element.width ||\n\t\truntime.lastHeight !== element.height;\n\tif (!changed) return;\n\n\tconst g = runtime.pixiGraphics;\n\tg.clear();\n\tg.rect(0, 0, element.width, element.height);\n\tg.fill({ color: panel.fillColor });\n\tif (panel.borderColor !== undefined && panel.borderWidth > 0) {\n\t\tg.stroke({ color: panel.borderColor, width: panel.borderWidth });\n\t}\n\truntime.lastFillColor = panel.fillColor;\n\truntime.lastBorderColor = panel.borderColor;\n\truntime.lastBorderWidth = panel.borderWidth;\n\truntime.lastWidth = element.width;\n\truntime.lastHeight = element.height;\n}\n\nfunction syncProgressRuntime(\n\truntime: UIProgressRuntime,\n\tbar: UIProgressBar,\n\telement: UIElement,\n\tscratchFill: FillRect,\n): void {\n\tconst changed =\n\t\truntime.lastValue !== bar.value ||\n\t\truntime.lastMax !== bar.max ||\n\t\truntime.lastFillColor !== bar.fillColor ||\n\t\truntime.lastBgColor !== bar.bgColor ||\n\t\truntime.lastDirection !== bar.direction ||\n\t\truntime.lastWidth !== element.width ||\n\t\truntime.lastHeight !== element.height;\n\tif (!changed) return;\n\n\tconst clamped = clampProgressValue(bar.value, bar.max);\n\tconst ratio = bar.max > 0 ? clamped / bar.max : 0;\n\tcomputeProgressFillRect(element.width, element.height, ratio, bar.direction, scratchFill);\n\n\tconst g = runtime.pixiGraphics;\n\tg.clear();\n\tg.rect(0, 0, element.width, element.height);\n\tg.fill({ color: bar.bgColor });\n\tif (scratchFill.width > 0 && scratchFill.height > 0) {\n\t\tg.rect(scratchFill.x, scratchFill.y, scratchFill.width, scratchFill.height);\n\t\tg.fill({ color: bar.fillColor });\n\t}\n\n\truntime.lastValue = bar.value;\n\truntime.lastMax = bar.max;\n\truntime.lastFillColor = bar.fillColor;\n\truntime.lastBgColor = bar.bgColor;\n\truntime.lastDirection = bar.direction;\n\truntime.lastWidth = element.width;\n\truntime.lastHeight = element.height;\n}\n\nfunction syncMessageLogRuntime(\n\truntime: UIMessageLogRuntime,\n\tlog: UIMessageLog,\n\tpixi: typeof import('pixi.js'),\n): void {\n\tconst styleChanged =\n\t\truntime.lastFontFamily !== log.style.fontFamily ||\n\t\truntime.lastFontSize !== log.style.fontSize ||\n\t\truntime.lastAlign !== log.style.align;\n\tconst linesChanged = runtime.lastLinesRef !== log.lines;\n\tconst layoutChanged =\n\t\truntime.lastVisibleLines !== log.visibleLines ||\n\t\truntime.lastLineHeight !== log.lineHeight;\n\tif (!linesChanged && !layoutChanged && !styleChanged) return;\n\n\tconst visible = log.lines.slice(-log.visibleLines);\n\n\twhile (runtime.lines.length < visible.length) {\n\t\tconst container = new pixi.Container();\n\t\truntime.rootContainer.addChild(container);\n\t\truntime.lines.push({ container, texts: [] });\n\t}\n\truntime.lines.forEach((line, index) => {\n\t\tif (index >= visible.length) line.container.visible = false;\n\t});\n\n\tvisible.forEach((fragments, lineIndex) => {\n\t\tconst line = runtime.lines[lineIndex];\n\t\tif (!line) return;\n\t\tline.container.visible = true;\n\t\tline.container.position.y = lineIndex * log.lineHeight;\n\n\t\twhile (line.texts.length < fragments.length) {\n\t\t\tconst t = new pixi.Text({\n\t\t\t\ttext: '',\n\t\t\t\tstyle: {\n\t\t\t\t\tfontFamily: log.style.fontFamily,\n\t\t\t\t\tfontSize: log.style.fontSize,\n\t\t\t\t\tfill: 0xffffff,\n\t\t\t\t\talign: log.style.align,\n\t\t\t\t},\n\t\t\t});\n\t\t\tline.container.addChild(t);\n\t\t\tline.texts.push(t);\n\t\t}\n\n\t\tlet cursorX = 0;\n\t\tline.texts.forEach((t, j) => {\n\t\t\tconst frag = fragments[j];\n\t\t\tif (!frag) {\n\t\t\t\tt.visible = false;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tt.visible = true;\n\t\t\tif (t.text !== frag.text) t.text = frag.text;\n\t\t\tif (t.style.fill !== frag.color) t.style.fill = frag.color;\n\t\t\tt.position.x = cursorX;\n\t\t\tcursorX += t.width;\n\t\t});\n\t});\n\n\t// Style fields are shared across all fragments — apply once per Text when they change,\n\t// keeping the per-fragment loop above limited to per-fragment text + color writes.\n\tif (styleChanged) {\n\t\truntime.lines.forEach((line) => {\n\t\t\tline.texts.forEach((t) => {\n\t\t\t\tconst s = t.style;\n\t\t\t\tif (s.fontFamily !== log.style.fontFamily) s.fontFamily = log.style.fontFamily;\n\t\t\t\tif (s.fontSize !== log.style.fontSize) s.fontSize = log.style.fontSize;\n\t\t\t\tif (s.align !== log.style.align) s.align = log.style.align;\n\t\t\t});\n\t\t});\n\t}\n\n\truntime.lastLinesRef = log.lines;\n\truntime.lastVisibleLines = log.visibleLines;\n\truntime.lastLineHeight = log.lineHeight;\n\truntime.lastFontFamily = log.style.fontFamily;\n\truntime.lastFontSize = log.style.fontSize;\n\truntime.lastAlign = log.style.align;\n}\n"
7
7
  ],
8
- "mappings": "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",
9
- "debugId": "B6E1260EDCFC611D64756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "4973E7FC8B99283864756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -1,9 +1,20 @@
1
1
  import type { Entity, FilteredEntity } from "./types";
2
2
  import type EntityManager from "./entity-manager";
3
+ import type ECSpresso from "./ecspresso";
4
+ import type { WorldConfig } from "./type-utils";
5
+ type ComponentKey<Cfg extends WorldConfig> = keyof Cfg['components'];
6
+ export type ReactiveQueryEnterContext<Cfg extends WorldConfig, WithComponents extends ComponentKey<Cfg> = ComponentKey<Cfg>, WithoutComponents extends ComponentKey<Cfg> = never, OptionalComponents extends ComponentKey<Cfg> = never> = {
7
+ entity: FilteredEntity<Cfg['components'], WithComponents, WithoutComponents, OptionalComponents>;
8
+ ecs: ECSpresso<Cfg>;
9
+ };
10
+ export type ReactiveQueryExitContext<Cfg extends WorldConfig> = {
11
+ entityId: number;
12
+ ecs: ECSpresso<Cfg>;
13
+ };
3
14
  /**
4
15
  * Definition for a reactive query with enter/exit callbacks
5
16
  */
6
- export interface ReactiveQueryDefinition<ComponentTypes extends Record<string, any>, WithComponents extends keyof ComponentTypes = keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never, OptionalComponents extends keyof ComponentTypes = never> {
17
+ export interface ReactiveQueryDefinition<Cfg extends WorldConfig, WithComponents extends ComponentKey<Cfg> = ComponentKey<Cfg>, WithoutComponents extends ComponentKey<Cfg> = never, OptionalComponents extends ComponentKey<Cfg> = never> {
7
18
  /** Components the entity must have */
8
19
  with: ReadonlyArray<WithComponents>;
9
20
  /** Components the entity must not have */
@@ -11,21 +22,22 @@ export interface ReactiveQueryDefinition<ComponentTypes extends Record<string, a
11
22
  /** Components to include in the entity type but not require for matching */
12
23
  optional?: ReadonlyArray<OptionalComponents>;
13
24
  /** Components the entity's direct parent must have */
14
- parentHas?: ReadonlyArray<keyof ComponentTypes>;
25
+ parentHas?: ReadonlyArray<ComponentKey<Cfg>>;
15
26
  /** Called when an entity starts matching the query */
16
- onEnter?: (entity: FilteredEntity<ComponentTypes, WithComponents, WithoutComponents, OptionalComponents>) => void;
27
+ onEnter?: (ctx: ReactiveQueryEnterContext<Cfg, WithComponents, WithoutComponents, OptionalComponents>) => void;
17
28
  /** Called when an entity stops matching the query (receives just the ID since entity may be gone) */
18
- onExit?: (entityId: number) => void;
29
+ onExit?: (ctx: ReactiveQueryExitContext<Cfg>) => void;
19
30
  }
20
31
  /**
21
32
  * Manages reactive queries that trigger callbacks when entities enter/exit query matches
22
33
  */
23
- export default class ReactiveQueryManager<ComponentTypes extends Record<string, any>, QueryNames extends string = string> {
34
+ export default class ReactiveQueryManager<Cfg extends WorldConfig, QueryNames extends string = string> {
24
35
  private queries;
25
36
  private entityManager;
37
+ private ecs;
26
38
  /** Whether any registered query uses parentHas */
27
39
  private _hasParentHasQueries;
28
- constructor(entityManager: EntityManager<ComponentTypes>);
40
+ constructor(entityManager: EntityManager<Cfg['components']>, ecs: ECSpresso<Cfg>);
29
41
  /**
30
42
  * Whether any registered reactive query uses parentHas filters
31
43
  */
@@ -35,7 +47,7 @@ export default class ReactiveQueryManager<ComponentTypes extends Record<string,
35
47
  * @param name Unique name for the query
36
48
  * @param definition Query definition with callbacks
37
49
  */
38
- addQuery<WithComponents extends keyof ComponentTypes, WithoutComponents extends keyof ComponentTypes = never, OptionalComponents extends keyof ComponentTypes = never>(name: QueryNames, definition: ReactiveQueryDefinition<ComponentTypes, WithComponents, WithoutComponents, OptionalComponents>): void;
50
+ addQuery<WithComponents extends ComponentKey<Cfg>, WithoutComponents extends ComponentKey<Cfg> = never, OptionalComponents extends ComponentKey<Cfg> = never>(name: QueryNames, definition: ReactiveQueryDefinition<Cfg, WithComponents, WithoutComponents, OptionalComponents>): void;
39
51
  /**
40
52
  * Remove a reactive query
41
53
  * @param name Name of the query to remove
@@ -43,6 +55,8 @@ export default class ReactiveQueryManager<ComponentTypes extends Record<string,
43
55
  */
44
56
  removeQuery(name: QueryNames): boolean;
45
57
  private entityMatchesQuery;
58
+ private _fireEnter;
59
+ private _fireExit;
46
60
  /**
47
61
  * Apply enter/exit transitions for a single query against an entity.
48
62
  * Fires onEnter when entity starts matching, onExit when it stops.
@@ -52,12 +66,12 @@ export default class ReactiveQueryManager<ComponentTypes extends Record<string,
52
66
  * Called when a component is added to an entity
53
67
  * Checks all queries for potential enter/exit events
54
68
  */
55
- onComponentAdded(entity: Entity<ComponentTypes>, _componentName: keyof ComponentTypes): void;
69
+ onComponentAdded(entity: Entity<Cfg['components']>, _componentName: keyof Cfg['components']): void;
56
70
  /**
57
71
  * Called when a component is removed from an entity
58
72
  * Checks all queries for potential enter/exit events
59
73
  */
60
- onComponentRemoved(entity: Entity<ComponentTypes>, _componentName: keyof ComponentTypes): void;
74
+ onComponentRemoved(entity: Entity<Cfg['components']>, _componentName: keyof Cfg['components']): void;
61
75
  /**
62
76
  * Called when an entity is removed
63
77
  * Triggers onExit for all queries the entity was matching
@@ -67,13 +81,13 @@ export default class ReactiveQueryManager<ComponentTypes extends Record<string,
67
81
  * Recheck an entity against all queries (used after batch component additions)
68
82
  * Fires enter/exit callbacks as appropriate based on current state vs tracked state
69
83
  */
70
- recheckEntity(entity: Entity<ComponentTypes>): void;
84
+ recheckEntity(entity: Entity<Cfg['components']>): void;
71
85
  /**
72
86
  * Recheck an entity and its children against all queries.
73
87
  * Used after component mutations to handle both the entity's own queries
74
88
  * and parentHas queries on its children.
75
89
  */
76
- recheckEntityAndChildren(entity: Entity<ComponentTypes>): void;
90
+ recheckEntityAndChildren(entity: Entity<Cfg['components']>): void;
77
91
  /**
78
92
  * Recheck all children of a parent entity against parentHas queries.
79
93
  * Called when a component is added/removed from a parent entity.
@@ -84,3 +98,4 @@ export default class ReactiveQueryManager<ComponentTypes extends Record<string,
84
98
  */
85
99
  private _recalcParentHasFlag;
86
100
  }
101
+ export {};
@@ -21,6 +21,15 @@ export interface ScreenDefinition<Config extends Record<string, unknown> = Recor
21
21
  * Lifecycle hook called when exiting this screen
22
22
  */
23
23
  readonly onExit?: (ecs: World) => void | Promise<void>;
24
+ /**
25
+ * Lifecycle hook called when this stacked screen becomes current again
26
+ * after an overlay is popped.
27
+ */
28
+ readonly onResume?: (ctx: {
29
+ config: Config;
30
+ state: State;
31
+ ecs: World;
32
+ }) => void | Promise<void>;
24
33
  /**
25
34
  * Asset keys that must be loaded before entering this screen
26
35
  */
@@ -103,6 +112,11 @@ export interface ScreenEvents<S extends string = string> {
103
112
  screenPop: {
104
113
  screen: S;
105
114
  };
115
+ screenResume: {
116
+ screen: S;
117
+ config: unknown;
118
+ state: unknown;
119
+ };
106
120
  }
107
121
  /**
108
122
  * Configuration for screen definitions during builder setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecspresso",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",