ecspresso 0.14.6 → 0.14.7
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.
- package/dist/index.js +2 -2
- package/dist/index.js.map +2 -2
- package/dist/plugins/ai/behavior-tree.js +2 -2
- package/dist/plugins/ai/behavior-tree.js.map +2 -2
- package/dist/plugins/ai/detection.js +2 -2
- package/dist/plugins/ai/detection.js.map +2 -2
- package/dist/plugins/ai/flocking.js +2 -2
- package/dist/plugins/ai/flocking.js.map +2 -2
- package/dist/plugins/ai/pathfinding.js +2 -2
- package/dist/plugins/ai/pathfinding.js.map +2 -2
- package/dist/plugins/audio/audio.js +2 -2
- package/dist/plugins/audio/audio.js.map +2 -2
- package/dist/plugins/combat/health.js +2 -2
- package/dist/plugins/combat/health.js.map +2 -2
- package/dist/plugins/combat/projectile.js +2 -2
- package/dist/plugins/combat/projectile.js.map +2 -2
- package/dist/plugins/debug/diagnostics.js +3 -3
- package/dist/plugins/debug/diagnostics.js.map +2 -2
- package/dist/plugins/input/input.js +2 -2
- package/dist/plugins/input/input.js.map +2 -2
- package/dist/plugins/input/selection.js +2 -2
- package/dist/plugins/input/selection.js.map +2 -2
- package/dist/plugins/isometric/depth-sort.js +2 -2
- package/dist/plugins/isometric/depth-sort.js.map +2 -2
- package/dist/plugins/isometric/projection.js +2 -2
- package/dist/plugins/isometric/projection.js.map +2 -2
- package/dist/plugins/physics/collision.js +2 -2
- package/dist/plugins/physics/collision.js.map +2 -2
- package/dist/plugins/physics/collision3D.js +2 -2
- package/dist/plugins/physics/collision3D.js.map +2 -2
- package/dist/plugins/physics/physics2D.js +2 -2
- package/dist/plugins/physics/physics2D.js.map +2 -2
- package/dist/plugins/physics/physics3D.js +2 -2
- package/dist/plugins/physics/physics3D.js.map +2 -2
- package/dist/plugins/physics/steering.js +2 -2
- package/dist/plugins/physics/steering.js.map +2 -2
- package/dist/plugins/rendering/particles.js +2 -2
- package/dist/plugins/rendering/particles.js.map +2 -2
- package/dist/plugins/rendering/renderer2D.js +2 -2
- package/dist/plugins/rendering/renderer2D.js.map +2 -2
- package/dist/plugins/rendering/renderer3D.js +2 -4105
- package/dist/plugins/rendering/renderer3D.js.map +3 -5
- package/dist/plugins/rendering/sprite-animation.js +2 -2
- package/dist/plugins/rendering/sprite-animation.js.map +2 -2
- package/dist/plugins/rendering/tilemap.js +2 -2
- package/dist/plugins/rendering/tilemap.js.map +2 -2
- package/dist/plugins/scripting/coroutine.js +2 -2
- package/dist/plugins/scripting/coroutine.js.map +2 -2
- package/dist/plugins/scripting/state-machine.js +2 -2
- package/dist/plugins/scripting/state-machine.js.map +2 -2
- package/dist/plugins/scripting/timers.js +2 -2
- package/dist/plugins/scripting/timers.js.map +2 -2
- package/dist/plugins/scripting/tween.js +2 -2
- package/dist/plugins/scripting/tween.js.map +2 -2
- package/dist/plugins/spatial/bounds.js +2 -2
- package/dist/plugins/spatial/bounds.js.map +2 -2
- package/dist/plugins/spatial/camera.js +2 -2
- package/dist/plugins/spatial/camera.js.map +2 -2
- package/dist/plugins/spatial/camera3D.js +2 -2
- package/dist/plugins/spatial/camera3D.js.map +2 -2
- package/dist/plugins/spatial/spatial-index.js +2 -2
- package/dist/plugins/spatial/spatial-index.js.map +2 -2
- package/dist/plugins/spatial/spatial-index3D.js +2 -2
- package/dist/plugins/spatial/spatial-index3D.js.map +2 -2
- package/dist/plugins/spatial/transform.js +2 -2
- package/dist/plugins/spatial/transform.js.map +2 -2
- package/dist/plugins/spatial/transform3D.js +2 -2
- package/dist/plugins/spatial/transform3D.js.map +2 -2
- package/dist/plugins/ui/ui.d.ts +40 -8
- package/dist/plugins/ui/ui.js +2 -2
- package/dist/plugins/ui/ui.js.map +3 -3
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"/**\n * Tween Plugin for ECSpresso\n *\n * Declarative property animation within the ECS. Tween any numeric component\n * field over time with standard easing functions, sequences, and completion events.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { ComponentsOfWorld, AnyECSpresso } from 'ecspresso';\nimport { linear, type EasingFn } from '../../utils/easing';\n\n// ==================== Event Types ====================\n\n/**\n * Data structure published when a tween completes.\n * Use this type when defining tween completion events in your EventTypes interface.\n */\nexport interface TweenEventData {\n\t/** The entity ID the tween belongs to */\n\tentityId: number;\n\t/** Number of steps in the tween */\n\tstepCount: number;\n}\n\n\n// ==================== Component Types ====================\n\nexport interface TweenTarget {\n\t/** Component name on the entity */\n\tcomponent: string;\n\t/** Pre-split field path (e.g., ['position', 'x']) */\n\tpath: readonly string[];\n\t/** Starting value. null = resolve from current value on first tick */\n\tfrom: number | null;\n\t/** Target value */\n\tto: number;\n}\n\nexport interface TweenStep {\n\ttargets: TweenTarget[];\n\tduration: number;\n\teasing: EasingFn;\n}\n\nexport interface Tween {\n\tsteps: TweenStep[];\n\tcurrentStep: number;\n\telapsed: number;\n\tloop: LoopMode;\n\ttotalLoops: number;\n\tcompletedLoops: number;\n\tdirection: 1 | -1;\n\tstate: 'pending' | 'active' | 'complete';\n\tonComplete?: (data: TweenEventData) => void;\n\tjustFinished: boolean;\n}\n\nexport type LoopMode = 'once' | 'loop' | 'yoyo';\n\n/**\n * Component types provided by the tween plugin.\n */\nexport interface TweenComponentTypes {\n\ttween: Tween;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface TweenPluginOptions<G extends string = 'tweens'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nexport interface TweenOptions {\n\t/** Explicit starting value (default: captures current value on first tick) */\n\tfrom?: number;\n\t/** Easing function (default: linear) */\n\teasing?: EasingFn;\n\t/** Loop mode (default: 'once') */\n\tloop?: LoopMode;\n\t/** Number of loops. -1 = infinite (default: 1) */\n\tloops?: number;\n\t/** Callback invoked when tween completes */\n\tonComplete?: (data: TweenEventData) => void;\n}\n\n/**\n * Create a single-target tween component.\n *\n * @param component Component name on the entity\n * @param field Field path (dot-separated for nested, e.g. 'position.x')\n * @param to Target value\n * @param duration Duration in seconds\n * @param options Optional configuration\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createTween(\n\tcomponent: string,\n\tfield: string,\n\tto: number,\n\tduration: number,\n\toptions?: TweenOptions,\n): Pick<TweenComponentTypes, 'tween'> {\n\tconst {\n\t\tfrom,\n\t\teasing = linear,\n\t\tloop = 'once',\n\t\tloops = 1,\n\t\tonComplete,\n\t} = options ?? {};\n\n\treturn {\n\t\ttween: {\n\t\t\tsteps: [{\n\t\t\t\ttargets: [{\n\t\t\t\t\tcomponent,\n\t\t\t\t\tpath: field.split('.'),\n\t\t\t\t\tfrom: from ?? null,\n\t\t\t\t\tto,\n\t\t\t\t}],\n\t\t\t\tduration,\n\t\t\t\teasing,\n\t\t\t}],\n\t\t\tcurrentStep: 0,\n\t\t\telapsed: 0,\n\t\t\tloop,\n\t\t\ttotalLoops: loops,\n\t\t\tcompletedLoops: 0,\n\t\t\tdirection: 1,\n\t\t\tstate: 'pending',\n\t\t\tonComplete,\n\t\t\tjustFinished: false,\n\t\t},\n\t};\n}\n\nexport interface TweenSequenceStepInput {\n\ttargets: ReadonlyArray<{\n\t\tcomponent: string;\n\t\tfield: string;\n\t\tto: number;\n\t\tfrom?: number;\n\t}>;\n\tduration: number;\n\teasing?: EasingFn;\n}\n\nexport interface TweenSequenceOptions {\n\t/** Loop mode (default: 'once') */\n\tloop?: LoopMode;\n\t/** Number of loops. -1 = infinite (default: 1) */\n\tloops?: number;\n\t/** Callback invoked when tween completes */\n\tonComplete?: (data: TweenEventData) => void;\n}\n\n/**\n * Create a multi-step tween sequence. Each step can have parallel targets.\n *\n * @param steps Array of step definitions\n * @param options Optional configuration\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createTweenSequence(\n\tsteps: ReadonlyArray<TweenSequenceStepInput>,\n\toptions?: TweenSequenceOptions,\n): Pick<TweenComponentTypes, 'tween'> {\n\tconst {\n\t\tloop = 'once',\n\t\tloops = 1,\n\t\tonComplete,\n\t} = options ?? {};\n\n\treturn {\n\t\ttween: {\n\t\t\tsteps: steps.map((step) => ({\n\t\t\t\ttargets: step.targets.map((target) => ({\n\t\t\t\t\tcomponent: target.component,\n\t\t\t\t\tpath: target.field.split('.'),\n\t\t\t\t\tfrom: target.from ?? null,\n\t\t\t\t\tto: target.to,\n\t\t\t\t})),\n\t\t\t\tduration: step.duration,\n\t\t\t\teasing: step.easing ?? linear,\n\t\t\t})),\n\t\t\tcurrentStep: 0,\n\t\t\telapsed: 0,\n\t\t\tloop,\n\t\t\ttotalLoops: loops,\n\t\t\tcompletedLoops: 0,\n\t\t\tdirection: 1,\n\t\t\tstate: 'pending',\n\t\t\tonComplete,\n\t\t\tjustFinished: false,\n\t\t},\n\t};\n}\n\n// ==================== Kit Types ====================\n\n/**\n * Recursively produce a union of dot-separated paths that resolve to `number`\n * within type T. Depth-limited to 4 levels to prevent TS recursion errors.\n *\n * @example\n * NumericPaths<{ x: number; y: number }> // 'x' | 'y'\n * NumericPaths<{ position: { x: number }; rotation: number }> // 'position.x' | 'rotation'\n */\nexport type NumericPaths<T, Depth extends readonly unknown[] = []> =\n\tDepth['length'] extends 4 ? never :\n\tT extends readonly unknown[] ? never :\n\tT extends Record<string, unknown>\n\t\t? { [K in keyof T & string]:\n\t\t\tNonNullable<T[K]> extends number\n\t\t\t\t? K\n\t\t\t\t: NonNullable<T[K]> extends readonly unknown[]\n\t\t\t\t\t? never\n\t\t\t\t\t: NonNullable<T[K]> extends Record<string, unknown>\n\t\t\t\t\t\t? `${K}.${NumericPaths<NonNullable<T[K]>, [...Depth, unknown]>}`\n\t\t\t\t\t\t: never\n\t\t}[keyof T & string]\n\t\t: never;\n\n/**\n * Discriminated union over component names: each variant constrains `field`\n * to the numeric paths of that component. TS narrows inline object literals\n * by `component` discriminant — zero runtime overhead.\n */\nexport type TypedTweenTargetInput<C extends Record<string, any>> = {\n\t[K in keyof C & string]: {\n\t\tcomponent: K;\n\t\tfield: NumericPaths<C[K]>;\n\t\tto: number;\n\t\tfrom?: number;\n\t}\n}[keyof C & string];\n\nexport interface TypedTweenSequenceStepInput<C extends Record<string, any>> {\n\ttargets: ReadonlyArray<TypedTweenTargetInput<C>>;\n\tduration: number;\n\teasing?: EasingFn;\n}\n\nexport interface TweenHelpers<W extends AnyECSpresso> {\n\tcreateTween: <K extends keyof ComponentsOfWorld<W> & string>(\n\t\tcomponent: K,\n\t\tfield: NumericPaths<ComponentsOfWorld<W>[K]>,\n\t\tto: number,\n\t\tduration: number,\n\t\toptions?: {\n\t\t\tfrom?: number;\n\t\t\teasing?: EasingFn;\n\t\t\tloop?: LoopMode;\n\t\t\tloops?: number;\n\t\t\tonComplete?: (data: TweenEventData) => void;\n\t\t},\n\t) => Pick<TweenComponentTypes, 'tween'>;\n\tcreateTweenSequence: (\n\t\tsteps: ReadonlyArray<TypedTweenSequenceStepInput<ComponentsOfWorld<W>>>,\n\t\toptions?: {\n\t\t\tloop?: LoopMode;\n\t\t\tloops?: number;\n\t\t\tonComplete?: (data: TweenEventData) => void;\n\t\t},\n\t) => Pick<TweenComponentTypes, 'tween'>;\n}\n\nexport function createTweenHelpers<W extends AnyECSpresso>(_world?: W): TweenHelpers<W> {\n\treturn {\n\t\tcreateTween: createTween as TweenHelpers<W>['createTween'],\n\t\tcreateTweenSequence: createTweenSequence as TweenHelpers<W>['createTweenSequence'],\n\t};\n}\n\n// ==================== Field Path Resolution ====================\n\n/**\n * Module-scoped mutable result to avoid per-call allocation in hot path.\n */\nconst _fieldRef: { parent: Record<string, unknown>; key: string } = { parent: {} as Record<string, unknown>, key: '' };\n\n/**\n * Traverse an object by path segments. Returns the parent object and final key\n * for read/write, or null if any segment is missing.\n */\nfunction resolveField(obj: Record<string, unknown>, path: readonly string[]): typeof _fieldRef | null {\n\tconst lastIdx = path.length - 1;\n\tlet current: Record<string, unknown> = obj;\n\n\tfor (let i = 0; i < lastIdx; i++) {\n\t\tconst segment = path[i];\n\t\tif (segment === undefined) return null;\n\t\tconst next = current[segment];\n\t\tif (next === null || next === undefined || typeof next !== 'object') return null;\n\t\tcurrent = next as Record<string, unknown>;\n\t}\n\n\tconst finalKey = path[lastIdx];\n\tif (finalKey === undefined) return null;\n\tif (!(finalKey in current)) return null;\n\n\t_fieldRef.parent = current;\n\t_fieldRef.key = finalKey;\n\treturn _fieldRef;\n}\n\nfunction readField(obj: Record<string, unknown>, path: readonly string[]): number | null {\n\tconst ref = resolveField(obj, path);\n\tif (!ref) return null;\n\tconst val = ref.parent[ref.key];\n\treturn typeof val === 'number' ? val : null;\n}\n\nfunction writeField(obj: Record<string, unknown>, path: readonly string[], value: number): boolean {\n\tconst ref = resolveField(obj, path);\n\tif (!ref) return false;\n\tref.parent[ref.key] = value;\n\treturn true;\n}\n\n// ==================== System Logic ====================\n\nfunction clamp(value: number, min: number, max: number): number {\n\treturn value < min ? min : value > max ? max : value;\n}\n\n/**\n * Resolve all null `from` values by reading current component field values.\n */\nfunction resolveFromValues(\n\ttween: Tween,\n\tentityComponents: Record<string, unknown>,\n): void {\n\tfor (const step of tween.steps) {\n\t\tfor (const target of step.targets) {\n\t\t\tif (target.from !== null) continue;\n\t\t\tconst comp = entityComponents[target.component];\n\t\t\tif (!comp || typeof comp !== 'object') continue;\n\t\t\tconst val = readField(comp as Record<string, unknown>, target.path);\n\t\t\tif (val !== null) {\n\t\t\t\ttarget.from = val;\n\t\t\t} else {\n\t\t\t\ttarget.from = 0;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Apply interpolation for a step's targets at a given progress.\n */\nfunction applyStep(\n\tstep: TweenStep,\n\tprogress: number,\n\tentityComponents: Record<string, unknown>,\n\tentityId: number,\n\tecs: { markChanged: (entityId: number, componentName: any) => void },\n): void {\n\tconst easedT = step.easing(progress);\n\n\tfor (const target of step.targets) {\n\t\tconst comp = entityComponents[target.component];\n\t\tif (!comp || typeof comp !== 'object') continue;\n\t\tconst from = target.from ?? 0;\n\t\tconst value = from + (target.to - from) * easedT;\n\t\tconst written = writeField(comp as Record<string, unknown>, target.path, value);\n\t\tif (written) {\n\t\t\tecs.markChanged(entityId, target.component);\n\t\t}\n\t}\n}\n\n/**\n * Snap all targets in a step to their final values (from or to depending on direction).\n */\nfunction snapStepToEnd(\n\tstep: TweenStep,\n\tentityComponents: Record<string, unknown>,\n\tentityId: number,\n\tecs: { markChanged: (entityId: number, componentName: any) => void },\n): void {\n\tfor (const target of step.targets) {\n\t\tconst comp = entityComponents[target.component];\n\t\tif (!comp || typeof comp !== 'object') continue;\n\t\tconst written = writeField(comp as Record<string, unknown>, target.path, target.to);\n\t\tif (written) {\n\t\t\tecs.markChanged(entityId, target.component);\n\t\t}\n\t}\n}\n\n/**\n * Reverse all from/to values in every step (for yoyo).\n */\nfunction reverseAllTargets(tween: Tween): void {\n\tfor (const step of tween.steps) {\n\t\tfor (const target of step.targets) {\n\t\t\tconst tmp = target.from ?? 0;\n\t\t\ttarget.from = target.to;\n\t\t\ttarget.to = tmp;\n\t\t}\n\t}\n}\n\n// ==================== Tween Processing Helpers ====================\n\ntype TweenEcs = { markChanged: (entityId: number, componentName: any) => void; commands: { removeComponent: (entityId: number, componentName: any) => void } };\n\nfunction completeTween(\n\ttween: Tween,\n\tentityId: number,\n\tecs: { commands: { removeComponent: (entityId: number, componentName: any) => void } },\n): void {\n\ttween.state = 'complete';\n\ttween.justFinished = true;\n\n\ttween.onComplete?.({ entityId, stepCount: tween.steps.length });\n\tecs.commands.removeComponent(entityId, 'tween');\n}\n\nfunction handleTweenEnd(\n\ttween: Tween,\n\tentityId: number,\n\tecs: TweenEcs,\n): boolean {\n\ttween.completedLoops++;\n\n\tif (tween.loop === 'once') {\n\t\tcompleteTween(tween, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Check if finite loops exhausted\n\tif (tween.totalLoops > 0 && tween.completedLoops >= tween.totalLoops) {\n\t\tcompleteTween(tween, entityId, ecs);\n\t\treturn false;\n\t}\n\n\t// Loop continues\n\tif (tween.loop === 'yoyo') {\n\t\ttween.direction = tween.direction === 1 ? -1 : 1;\n\t\treverseAllTargets(tween);\n\t}\n\n\ttween.currentStep = 0;\n\n\t// For 'loop' mode, from values stay as-is so the animation replays identically.\n\t// For 'yoyo' mode, reverseAllTargets already swapped from/to.\n\n\treturn tween.elapsed > 0;\n}\n\n/**\n * Advance to next step. Returns true if there's more work to process,\n * false if the tween has completed or looped.\n */\nfunction advanceStep(\n\ttween: Tween,\n\tentityComponents: Record<string, unknown>,\n\tentityId: number,\n\tecs: TweenEcs,\n): boolean {\n\tconst nextStep = tween.currentStep + 1;\n\n\tif (nextStep < tween.steps.length) {\n\t\t// More steps — resolve from values for next step and continue\n\t\ttween.currentStep = nextStep;\n\t\tconst step = tween.steps[nextStep];\n\t\tif (step) {\n\t\t\tfor (const target of step.targets) {\n\t\t\t\tif (target.from !== null) continue;\n\t\t\t\tconst comp = entityComponents[target.component];\n\t\t\t\tif (!comp || typeof comp !== 'object') continue;\n\t\t\t\tconst val = readField(comp as Record<string, unknown>, target.path);\n\t\t\t\ttarget.from = val ?? 0;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t// All steps done — handle loop/complete\n\treturn handleTweenEnd(tween, entityId, ecs);\n}\n\nfunction processTweenProgress(\n\ttween: Tween,\n\tentityComponents: Record<string, unknown>,\n\tentityId: number,\n\tecs: TweenEcs,\n): void {\n\t// eslint-disable-next-line no-constant-condition\n\twhile (true) {\n\t\tconst currentStep = tween.steps[tween.currentStep];\n\t\tif (!currentStep) return;\n\n\t\t// Zero-duration steps complete immediately\n\t\tif (currentStep.duration <= 0) {\n\t\t\tsnapStepToEnd(currentStep, entityComponents, entityId, ecs);\n\t\t\ttween.elapsed = 0;\n\n\t\t\tif (!advanceStep(tween, entityComponents, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (tween.elapsed >= currentStep.duration) {\n\t\t\t// Step complete — snap to end and carry overflow\n\t\t\tsnapStepToEnd(currentStep, entityComponents, entityId, ecs);\n\t\t\tconst overflow = tween.elapsed - currentStep.duration;\n\t\t\ttween.elapsed = overflow;\n\n\t\t\tif (!advanceStep(tween, entityComponents, entityId, ecs)) return;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Step in progress — interpolate\n\t\tconst progress = clamp(tween.elapsed / currentStep.duration, 0, 1);\n\t\tapplyStep(currentStep, progress, entityComponents, entityId, ecs);\n\t\treturn;\n\t}\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a tween plugin for ECSpresso.\n *\n * This plugin provides:\n * - Tween system that processes all tween components each frame\n * - Support for single-field, multi-target, and multi-step sequences\n * - 31 standard easing functions\n * - Loop modes: once, loop, yoyo\n * - `justFinished` flag for one-frame completion detection\n * - `onComplete` callback on completion\n * - Change detection via markChanged\n */\nexport function createTweenPlugin<G extends string = 'tweens'>(\n\toptions?: TweenPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'tweens',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin('tweens')\n\t\t.withComponentTypes<TweenComponentTypes>()\n\t\t.withLabels<'tween-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('tween-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('tweens', {\n\t\t\t\t\twith: ['tween'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.tweens) {\n\t\t\t\t\t\tconst tween = entity.components.tween as Tween;\n\t\t\t\t\t\tconst entityComponents = entity.components as Record<string, unknown>;\n\n\t\t\t\t\t\t// Reset justFinished flag from previous frame\n\t\t\t\t\t\tif (tween.justFinished) {\n\t\t\t\t\t\t\ttween.justFinished = false;\n\t\t\t\t\t\t\t// Component removal was queued, skip processing\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Skip completed tweens\n\t\t\t\t\t\tif (tween.state === 'complete') continue;\n\n\t\t\t\t\t\t// Resolve pending state: capture null from values\n\t\t\t\t\t\tif (tween.state === 'pending') {\n\t\t\t\t\t\t\tresolveFromValues(tween, entityComponents);\n\t\t\t\t\t\t\ttween.state = 'active';\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Process active tween\n\t\t\t\t\t\tconst currentStep = tween.steps[tween.currentStep];\n\t\t\t\t\t\tif (!currentStep) continue;\n\n\t\t\t\t\t\ttween.elapsed += dt;\n\n\t\t\t\t\t\tprocessTweenProgress(tween, entityComponents, entity.id, ecs);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n",
|
|
6
6
|
"/**\n * Easing Functions\n *\n * 31 standard easing functions for animation. Pure math, no dependencies.\n */\n\nexport type EasingFn = (t: number) => number;\n\nexport function linear(t: number): number {\n\treturn t;\n}\n\n// Quad\nexport function easeInQuad(t: number): number {\n\treturn t * t;\n}\n\nexport function easeOutQuad(t: number): number {\n\treturn t * (2 - t);\n}\n\nexport function easeInOutQuad(t: number): number {\n\treturn t < 0.5\n\t\t? 2 * t * t\n\t\t: -1 + (4 - 2 * t) * t;\n}\n\n// Cubic\nexport function easeInCubic(t: number): number {\n\treturn t * t * t;\n}\n\nexport function easeOutCubic(t: number): number {\n\tconst t1 = t - 1;\n\treturn t1 * t1 * t1 + 1;\n}\n\nexport function easeInOutCubic(t: number): number {\n\treturn t < 0.5\n\t\t? 4 * t * t * t\n\t\t: 1 + (t - 1) * (2 * t - 2) * (2 * t - 2);\n}\n\n// Quart\nexport function easeInQuart(t: number): number {\n\treturn t * t * t * t;\n}\n\nexport function easeOutQuart(t: number): number {\n\tconst t1 = t - 1;\n\treturn 1 - t1 * t1 * t1 * t1;\n}\n\nexport function easeInOutQuart(t: number): number {\n\treturn t < 0.5\n\t\t? 8 * t * t * t * t\n\t\t: 1 - 8 * (t - 1) * (t - 1) * (t - 1) * (t - 1);\n}\n\n// Quint\nexport function easeInQuint(t: number): number {\n\treturn t * t * t * t * t;\n}\n\nexport function easeOutQuint(t: number): number {\n\tconst t1 = t - 1;\n\treturn 1 + t1 * t1 * t1 * t1 * t1;\n}\n\nexport function easeInOutQuint(t: number): number {\n\treturn t < 0.5\n\t\t? 16 * t * t * t * t * t\n\t\t: 1 + 16 * (t - 1) * (t - 1) * (t - 1) * (t - 1) * (t - 1);\n}\n\n// Sine\nexport function easeInSine(t: number): number {\n\treturn 1 - Math.cos((t * Math.PI) / 2);\n}\n\nexport function easeOutSine(t: number): number {\n\treturn Math.sin((t * Math.PI) / 2);\n}\n\nexport function easeInOutSine(t: number): number {\n\treturn -(Math.cos(Math.PI * t) - 1) / 2;\n}\n\n// Expo\nexport function easeInExpo(t: number): number {\n\treturn t === 0 ? 0 : Math.pow(2, 10 * (t - 1));\n}\n\nexport function easeOutExpo(t: number): number {\n\treturn t === 1 ? 1 : 1 - Math.pow(2, -10 * t);\n}\n\nexport function easeInOutExpo(t: number): number {\n\tif (t === 0) return 0;\n\tif (t === 1) return 1;\n\treturn t < 0.5\n\t\t? Math.pow(2, 20 * t - 10) / 2\n\t\t: (2 - Math.pow(2, -20 * t + 10)) / 2;\n}\n\n// Circ\nexport function easeInCirc(t: number): number {\n\treturn 1 - Math.sqrt(1 - t * t);\n}\n\nexport function easeOutCirc(t: number): number {\n\tconst t1 = t - 1;\n\treturn Math.sqrt(1 - t1 * t1);\n}\n\nexport function easeInOutCirc(t: number): number {\n\treturn t < 0.5\n\t\t? (1 - Math.sqrt(1 - 4 * t * t)) / 2\n\t\t: (Math.sqrt(1 - (-2 * t + 2) * (-2 * t + 2)) + 1) / 2;\n}\n\n// Back\nconst BACK_C1 = 1.70158;\nconst BACK_C2 = BACK_C1 * 1.525;\nconst BACK_C3 = BACK_C1 + 1;\n\nexport function easeInBack(t: number): number {\n\treturn BACK_C3 * t * t * t - BACK_C1 * t * t;\n}\n\nexport function easeOutBack(t: number): number {\n\tconst t1 = t - 1;\n\treturn 1 + BACK_C3 * t1 * t1 * t1 + BACK_C1 * t1 * t1;\n}\n\nexport function easeInOutBack(t: number): number {\n\treturn t < 0.5\n\t\t? ((2 * t) * (2 * t) * ((BACK_C2 + 1) * 2 * t - BACK_C2)) / 2\n\t\t: ((2 * t - 2) * (2 * t - 2) * ((BACK_C2 + 1) * (t * 2 - 2) + BACK_C2) + 2) / 2;\n}\n\n// Elastic\nconst ELASTIC_C4 = (2 * Math.PI) / 3;\nconst ELASTIC_C5 = (2 * Math.PI) / 4.5;\n\nexport function easeInElastic(t: number): number {\n\tif (t === 0) return 0;\n\tif (t === 1) return 1;\n\treturn -Math.pow(2, 10 * t - 10) * Math.sin((10 * t - 10.75) * ELASTIC_C4);\n}\n\nexport function easeOutElastic(t: number): number {\n\tif (t === 0) return 0;\n\tif (t === 1) return 1;\n\treturn Math.pow(2, -10 * t) * Math.sin((10 * t - 0.75) * ELASTIC_C4) + 1;\n}\n\nexport function easeInOutElastic(t: number): number {\n\tif (t === 0) return 0;\n\tif (t === 1) return 1;\n\treturn t < 0.5\n\t\t? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * ELASTIC_C5)) / 2\n\t\t: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * ELASTIC_C5)) / 2 + 1;\n}\n\n// Bounce\nexport function easeOutBounce(t: number): number {\n\tconst n1 = 7.5625;\n\tconst d1 = 2.75;\n\n\tif (t < 1 / d1) {\n\t\treturn n1 * t * t;\n\t} else if (t < 2 / d1) {\n\t\tconst t1 = t - 1.5 / d1;\n\t\treturn n1 * t1 * t1 + 0.75;\n\t} else if (t < 2.5 / d1) {\n\t\tconst t1 = t - 2.25 / d1;\n\t\treturn n1 * t1 * t1 + 0.9375;\n\t} else {\n\t\tconst t1 = t - 2.625 / d1;\n\t\treturn n1 * t1 * t1 + 0.984375;\n\t}\n}\n\nexport function easeInBounce(t: number): number {\n\treturn 1 - easeOutBounce(1 - t);\n}\n\nexport function easeInOutBounce(t: number): number {\n\treturn t < 0.5\n\t\t? (1 - easeOutBounce(1 - 2 * t)) / 2\n\t\t: (1 + easeOutBounce(2 * t - 1)) / 2;\n}\n\n/** Runtime lookup of all easing functions by name */\nexport const easings = {\n\tlinear,\n\teaseInQuad,\n\teaseOutQuad,\n\teaseInOutQuad,\n\teaseInCubic,\n\teaseOutCubic,\n\teaseInOutCubic,\n\teaseInQuart,\n\teaseOutQuart,\n\teaseInOutQuart,\n\teaseInQuint,\n\teaseOutQuint,\n\teaseInOutQuint,\n\teaseInSine,\n\teaseOutSine,\n\teaseInOutSine,\n\teaseInExpo,\n\teaseOutExpo,\n\teaseInOutExpo,\n\teaseInCirc,\n\teaseOutCirc,\n\teaseInOutCirc,\n\teaseInBack,\n\teaseOutBack,\n\teaseInOutBack,\n\teaseInElastic,\n\teaseOutElastic,\n\teaseInOutElastic,\n\teaseInBounce,\n\teaseOutBounce,\n\teaseInOutBounce,\n} as const;\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "2PAOA,uBAAS,kBCCF,SAAS,CAAM,CAAC,EAAmB,CACzC,OAAO,EAiHR,IAAM,EAAU,QACV,EAAU,EAAU,MACpB,EAAU,EAAU,EAkB1B,IAAM,EAAc,EAAI,KAAK,GAAM,EAC7B,EAAc,EAAI,KAAK,GAAM,IDhD5B,SAAS,CAAW,CAC1B,EACA,EACA,EACA,EACA,EACqC,CACrC,IACC,OACA,SAAS,EACT,OAAO,OACP,QAAQ,EACR,cACG,GAAW,CAAC,EAEhB,MAAO,CACN,MAAO,CACN,MAAO,CAAC,CACP,QAAS,CAAC,CACT,YACA,KAAM,EAAM,MAAM,GAAG,EACrB,KAAM,GAAQ,KACd,IACD,CAAC,EACD,WACA,QACD,CAAC,EACD,YAAa,EACb,QAAS,EACT,OACA,WAAY,EACZ,eAAgB,EAChB,UAAW,EACX,MAAO,UACP,aACA,aAAc,EACf,CACD,EA8BM,SAAS,CAAmB,CAClC,EACA,EACqC,CACrC,IACC,OAAO,OACP,QAAQ,EACR,cACG,GAAW,CAAC,EAEhB,MAAO,CACN,MAAO,CACN,MAAO,EAAM,IAAI,CAAC,KAAU,CAC3B,QAAS,EAAK,QAAQ,IAAI,CAAC,KAAY,CACtC,UAAW,EAAO,UAClB,KAAM,EAAO,MAAM,MAAM,GAAG,EAC5B,KAAM,EAAO,MAAQ,KACrB,GAAI,EAAO,EACZ,EAAE,EACF,SAAU,EAAK,SACf,OAAQ,EAAK,QAAU,CACxB,EAAE,EACF,YAAa,EACb,QAAS,EACT,OACA,WAAY,EACZ,eAAgB,EAChB,UAAW,EACX,MAAO,UACP,aACA,aAAc,EACf,CACD,EAwEM,SAAS,CAA0C,CAAC,EAA6B,CACvF,MAAO,CACN,YAAa,EACb,oBAAqB,CACtB,EAQD,IAAM,EAA8D,CAAE,OAAQ,CAAC,EAA8B,IAAK,EAAG,EAMrH,SAAS,CAAY,CAAC,EAA8B,EAAkD,CACrG,IAAM,EAAU,EAAK,OAAS,EAC1B,EAAmC,EAEvC,QAAS,EAAI,EAAG,EAAI,EAAS,IAAK,CACjC,IAAM,EAAU,EAAK,GACrB,GAAI,IAAY,OAAW,OAAO,KAClC,IAAM,EAAO,EAAQ,GACrB,GAAI,IAAS,MAAQ,IAAS,QAAa,OAAO,IAAS,SAAU,OAAO,KAC5E,EAAU,EAGX,IAAM,EAAW,EAAK,GACtB,GAAI,IAAa,OAAW,OAAO,KACnC,GAAI,EAAE,KAAY,GAAU,OAAO,KAInC,OAFA,EAAU,OAAS,EACnB,EAAU,IAAM,EACT,EAGR,SAAS,CAAS,CAAC,EAA8B,EAAwC,CACxF,IAAM,EAAM,EAAa,EAAK,CAAI,EAClC,GAAI,CAAC,EAAK,OAAO,KACjB,IAAM,EAAM,EAAI,OAAO,EAAI,KAC3B,OAAO,OAAO,IAAQ,SAAW,EAAM,KAGxC,SAAS,CAAU,CAAC,EAA8B,EAAyB,EAAwB,CAClG,IAAM,EAAM,EAAa,EAAK,CAAI,EAClC,GAAI,CAAC,EAAK,MAAO,GAEjB,OADA,EAAI,OAAO,EAAI,KAAO,EACf,GAKR,SAAS,CAAK,CAAC,EAAe,EAAa,EAAqB,CAC/D,OAAO,EAAQ,EAAM,EAAM,EAAQ,EAAM,EAAM,EAMhD,SAAS,CAAiB,CACzB,EACA,EACO,CACP,QAAW,KAAQ,EAAM,MACxB,QAAW,KAAU,EAAK,QAAS,CAClC,GAAI,EAAO,OAAS,KAAM,SAC1B,IAAM,EAAO,EAAiB,EAAO,WACrC,GAAI,CAAC,GAAQ,OAAO,IAAS,SAAU,SACvC,IAAM,EAAM,EAAU,EAAiC,EAAO,IAAI,EAClE,GAAI,IAAQ,KACX,EAAO,KAAO,EAEd,OAAO,KAAO,GASlB,SAAS,CAAS,CACjB,EACA,EACA,EACA,EACA,EACO,CACP,IAAM,EAAS,EAAK,OAAO,CAAQ,EAEnC,QAAW,KAAU,EAAK,QAAS,CAClC,IAAM,EAAO,EAAiB,EAAO,WACrC,GAAI,CAAC,GAAQ,OAAO,IAAS,SAAU,SACvC,IAAM,EAAO,EAAO,MAAQ,EACtB,EAAQ,GAAQ,EAAO,GAAK,GAAQ,EAE1C,GADgB,EAAW,EAAiC,EAAO,KAAM,CAAK,EAE7E,EAAI,YAAY,EAAU,EAAO,SAAS,GAQ7C,SAAS,CAAa,CACrB,EACA,EACA,EACA,EACO,CACP,QAAW,KAAU,EAAK,QAAS,CAClC,IAAM,EAAO,EAAiB,EAAO,WACrC,GAAI,CAAC,GAAQ,OAAO,IAAS,SAAU,SAEvC,GADgB,EAAW,EAAiC,EAAO,KAAM,EAAO,EAAE,EAEjF,EAAI,YAAY,EAAU,EAAO,SAAS,GAQ7C,SAAS,CAAiB,CAAC,EAAoB,CAC9C,QAAW,KAAQ,EAAM,MACxB,QAAW,KAAU,EAAK,QAAS,CAClC,IAAM,EAAM,EAAO,MAAQ,EAC3B,EAAO,KAAO,EAAO,GACrB,EAAO,GAAK,GASf,SAAS,CAAa,CACrB,EACA,EACA,EACO,CACP,EAAM,MAAQ,WACd,EAAM,aAAe,GAErB,EAAM,aAAa,CAAE,WAAU,UAAW,EAAM,MAAM,MAAO,CAAC,EAC9D,EAAI,SAAS,gBAAgB,EAAU,OAAO,EAG/C,SAAS,CAAc,CACtB,EACA,EACA,EACU,CAGV,GAFA,EAAM,iBAEF,EAAM,OAAS,OAElB,OADA,EAAc,EAAO,EAAU,CAAG,EAC3B,GAIR,GAAI,EAAM,WAAa,GAAK,EAAM,gBAAkB,EAAM,WAEzD,OADA,EAAc,EAAO,EAAU,CAAG,EAC3B,GAIR,GAAI,EAAM,OAAS,OAClB,EAAM,UAAY,EAAM,YAAc,EAAI,GAAK,EAC/C,EAAkB,CAAK,EAQxB,OALA,EAAM,YAAc,EAKb,EAAM,QAAU,EAOxB,SAAS,CAAW,CACnB,EACA,EACA,EACA,EACU,CACV,IAAM,EAAW,EAAM,YAAc,EAErC,GAAI,EAAW,EAAM,MAAM,OAAQ,CAElC,EAAM,YAAc,EACpB,IAAM,EAAO,EAAM,MAAM,GACzB,GAAI,EACH,QAAW,KAAU,EAAK,QAAS,CAClC,GAAI,EAAO,OAAS,KAAM,SAC1B,IAAM,EAAO,EAAiB,EAAO,WACrC,GAAI,CAAC,GAAQ,OAAO,IAAS,SAAU,SACvC,IAAM,EAAM,EAAU,EAAiC,EAAO,IAAI,EAClE,EAAO,KAAO,GAAO,EAGvB,MAAO,GAIR,OAAO,EAAe,EAAO,EAAU,CAAG,EAG3C,SAAS,CAAoB,CAC5B,EACA,EACA,EACA,EACO,CAEP,MAAO,GAAM,CACZ,IAAM,EAAc,EAAM,MAAM,EAAM,aACtC,GAAI,CAAC,EAAa,OAGlB,GAAI,EAAY,UAAY,EAAG,CAI9B,GAHA,EAAc,EAAa,EAAkB,EAAU,CAAG,EAC1D,EAAM,QAAU,EAEZ,CAAC,EAAY,EAAO,EAAkB,EAAU,CAAG,EAAG,OAC1D,SAGD,GAAI,EAAM,SAAW,EAAY,SAAU,CAE1C,EAAc,EAAa,EAAkB,EAAU,CAAG,EAC1D,IAAM,EAAW,EAAM,QAAU,EAAY,SAG7C,GAFA,EAAM,QAAU,EAEZ,CAAC,EAAY,EAAO,EAAkB,EAAU,CAAG,EAAG,OAC1D,SAID,IAAM,EAAW,EAAM,EAAM,QAAU,EAAY,SAAU,EAAG,CAAC,EACjE,EAAU,EAAa,EAAU,EAAkB,EAAU,CAAG,EAChE,QAkBK,SAAS,CAA8C,CAC7D,EACC,CACD,IACC,cAAc,SACd,WAAW,EACX,QAAQ,UACL,GAAW,CAAC,EAEhB,OAAO,EAAa,QAAQ,EAC1B,mBAAwC,EACxC,WAA2B,EAC3B,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,cAAc,EACxB,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,OAAO,CACf,CAAC,EACA,WAAW,EAAG,UAAS,KAAI,SAAU,CACrC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAM,EAAQ,EAAO,WAAW,MAC1B,EAAmB,EAAO,WAGhC,GAAI,EAAM,aAAc,CACvB,EAAM,aAAe,GAErB,SAID,GAAI,EAAM,QAAU,WAAY,SAGhC,GAAI,EAAM,QAAU,UACnB,EAAkB,EAAO,CAAgB,EACzC,EAAM,MAAQ,SAKf,GAAI,CADgB,EAAM,MAAM,EAAM,aACpB,SAElB,EAAM,SAAW,EAEjB,EAAqB,EAAO,EAAkB,EAAO,GAAI,CAAG,GAE7D,EACF",
|
|
9
|
+
"debugId": "52B5806729E5074564756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var R=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(I,F)=>(typeof require<"u"?require:I)[F]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as K}from"ecspresso";function h(j,I,F,S){let V={width:j,height:I};if(F!==void 0)V.x=F;if(S!==void 0)V.y=S;return V}function T(j){return{destroyOutOfBounds:j!==void 0?{padding:j}:{}}}function b(j){return{clampToBounds:j!==void 0?{margin:j}:{}}}function w(j){return{wrapAtBounds:j!==void 0?{padding:j}:{}}}function y(j){let{systemGroup:I="physics",priority:F=50,boundsResourceKey:S="bounds",autoRemove:V=!0,phase:U="postUpdate"}=j??{};return K("bounds").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install((G)=>{G.addSystem("bounds-destroy").setPriority(F).inPhase(U).inGroup(I).addQuery("entities",{with:["worldTransform","destroyOutOfBounds"]}).setProcess(({queries:D,ecs:J})=>{let C=J.getResource(S),L=C.x??0,N=C.y??0,v=L+C.width,A=N+C.height;for(let Q of D.entities){let{worldTransform:Z,destroyOutOfBounds:z}=Q.components,P=z.padding??0,k=E(Z,L,N,v,A,P);if(!k)continue;if(J.eventBus.publish("entityOutOfBounds",{entityId:Q.id,exitEdge:k}),V)J.commands.removeEntity(Q.id)}}),G.addSystem("bounds-clamp").setPriority(F-1).inPhase(U).inGroup(I).addQuery("entities",{with:["localTransform","worldTransform","clampToBounds"]}).setProcess(({queries:D,ecs:J})=>{let C=J.getResource(S),L=C.x??0,N=C.y??0,v=L+C.width,A=N+C.height;for(let Q of D.entities){let{localTransform:Z,worldTransform:z,clampToBounds:P}=Q.components,k=P.margin??0,_=L+k,$=N+k,H=v-k,O=A-k,W=0,B=0;if(z.x<_)W=_-z.x;if(z.x>H)W=H-z.x;if(z.y<$)B=$-z.y;if(z.y>O)B=O-z.y;if(W!==0||B!==0)Z.x+=W,Z.y+=B,J.markChanged(Q.id,"localTransform")}}),G.addSystem("bounds-wrap").setPriority(F-2).inPhase(U).inGroup(I).addQuery("entities",{with:["localTransform","worldTransform","wrapAtBounds"]}).setProcess(({queries:D,ecs:J})=>{let C=J.getResource(S),L=C.x??0,N=C.y??0,v=L+C.width,A=N+C.height;for(let Q of D.entities){let{localTransform:Z,worldTransform:z,wrapAtBounds:P}=Q.components,k=P.padding??0,_=0,$=0,H=v-L,O=A-N;if(z.x>v+k)_=-(H+2*k);else if(z.x<L-k)_=H+2*k;if(z.y>A+k)$=-(O+2*k);else if(z.y<N-k)$=O+2*k;if(_!==0||$!==0)Z.x+=_,Z.y+=$,J.markChanged(Q.id,"localTransform")}})})}function E(j,I,F,S,V,U){if(j.x>S+U)return"right";if(j.x<I-U)return"left";if(j.y>V+U)return"bottom";if(j.y<F-U)return"top";return null}export{w as createWrapAtBounds,T as createDestroyOutOfBounds,b as createClampToBounds,y as createBoundsPlugin,h as createBounds};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=C4210D075EB1DBA264756E2164756E21
|
|
4
4
|
//# sourceMappingURL=bounds.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Bounds Plugin for ECSpresso\n *\n * Provides screen bounds enforcement for entities with transforms.\n * Reads worldTransform for position checking; modifies localTransform for corrections.\n * Supports destroy, clamp, and wrap behaviors.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { TransformWorldConfig } from './transform';\n\n// ==================== Component Types ====================\n\n/**\n * Component that marks an entity for destruction when outside bounds.\n */\nexport interface DestroyOutOfBounds {\n\t/** Extra padding beyond bounds before destruction (default: 0) */\n\tpadding?: number;\n}\n\n/**\n * Component that clamps an entity's position to stay within bounds.\n */\nexport interface ClampToBounds {\n\t/** Margin to shrink the valid area (default: 0) */\n\tmargin?: number;\n}\n\n/**\n * Component that wraps an entity's position to the opposite edge.\n */\nexport interface WrapAtBounds {\n\t/** Padding beyond bounds before wrapping (default: 0) */\n\tpadding?: number;\n}\n\n/**\n * Component types provided by the bounds plugin.\n * Included automatically via `.withPlugin(createBoundsPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createBoundsPlugin({ width: 800, height: 600 }))\n * .withComponentTypes<{ sprite: Sprite }>()\n * .build();\n * ```\n */\nexport interface BoundsComponentTypes {\n\tdestroyOutOfBounds: DestroyOutOfBounds;\n\tclampToBounds: ClampToBounds;\n\twrapAtBounds: WrapAtBounds;\n}\n\n// ==================== Resource Types ====================\n\n/**\n * Bounds rectangle definition.\n */\nexport interface BoundsRect {\n\t/** Left edge x coordinate (default: 0) */\n\tx?: number;\n\t/** Top edge y coordinate (default: 0) */\n\ty?: number;\n\t/** Width of the bounds area */\n\twidth: number;\n\t/** Height of the bounds area */\n\theight: number;\n}\n\n/**\n * Resource types provided by the bounds plugin.\n */\nexport interface BoundsResourceTypes {\n\tbounds: BoundsRect;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when an entity exits bounds.\n */\nexport interface EntityOutOfBoundsEvent {\n\t/** The entity that exited bounds */\n\tentityId: number;\n\t/** The edge the entity exited through */\n\texitEdge: 'top' | 'bottom' | 'left' | 'right';\n}\n\n/**\n * Event types provided by the bounds plugin.\n */\nexport interface BoundsEventTypes {\n\tentityOutOfBounds: EntityOutOfBoundsEvent;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the bounds plugin.\n */\nexport interface BoundsPluginOptions<G extends string = 'physics'> extends BasePluginOptions<G> {\n\t/** Resource key for bounds rectangle (default: 'bounds') */\n\tboundsResourceKey?: string;\n\t/** Whether to auto-remove entities when out of bounds (default: true) */\n\tautoRemove?: boolean;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a bounds rectangle resource.\n *\n * @param width The width of the bounds area\n * @param height The height of the bounds area\n * @param x The left edge x coordinate (default: 0)\n * @param y The top edge y coordinate (default: 0)\n * @returns Bounds rectangle suitable for use as a resource\n *\n * @example\n * ```typescript\n * ECSpresso.create()\n * .withResource('bounds', createBounds(800, 600))\n * .build();\n * ```\n */\nexport function createBounds(width: number, height: number, x?: number, y?: number): BoundsRect {\n\tconst bounds: BoundsRect = { width, height };\n\tif (x !== undefined) bounds.x = x;\n\tif (y !== undefined) bounds.y = y;\n\treturn bounds;\n}\n\n/**\n * Create a destroyOutOfBounds component.\n *\n * @param padding Extra padding beyond bounds before destruction\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createDestroyOutOfBounds(20),\n * });\n * ```\n */\nexport function createDestroyOutOfBounds(padding?: number): Pick<BoundsComponentTypes, 'destroyOutOfBounds'> {\n\treturn {\n\t\tdestroyOutOfBounds: padding !== undefined ? { padding } : {},\n\t};\n}\n\n/**\n * Create a clampToBounds component.\n *\n * @param margin Margin to shrink the valid area\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createClampToBounds(30),\n * });\n * ```\n */\nexport function createClampToBounds(margin?: number): Pick<BoundsComponentTypes, 'clampToBounds'> {\n\treturn {\n\t\tclampToBounds: margin !== undefined ? { margin } : {},\n\t};\n}\n\n/**\n * Create a wrapAtBounds component.\n *\n * @param padding Padding beyond bounds before wrapping\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createWrapAtBounds(10),\n * });\n * ```\n */\nexport function createWrapAtBounds(padding?: number): Pick<BoundsComponentTypes, 'wrapAtBounds'> {\n\treturn {\n\t\twrapAtBounds: padding !== undefined ? { padding } : {},\n\t};\n}\n\n// ==================== Dependency Types ====================\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a bounds plugin for ECSpresso.\n *\n * This plugin provides:\n * - Destroy out of bounds system - removes entities that exit bounds\n * - Clamp to bounds system - constrains entities within bounds\n * - Wrap at bounds system - wraps entities to opposite edge\n *\n * Uses worldTransform for position checking (world-space) and modifies\n * localTransform for corrections. Works best with entities that don't\n * have parent transforms (orphan entities).\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withResource('bounds', createBounds(800, 600))\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createBoundsPlugin())\n * .build();\n *\n * // Entity that gets destroyed when leaving screen\n * ecs.spawn({\n * ...createTransform(100, 200),\n * ...createDestroyOutOfBounds(),\n * });\n * ```\n */\nexport function createBoundsPlugin<ResourceTypes extends BoundsResourceTypes = BoundsResourceTypes, G extends string = 'physics'>(\n\toptions?: BoundsPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'physics',\n\t\tpriority = 50,\n\t\tboundsResourceKey = 'bounds',\n\t\tautoRemove = true,\n\t\tphase = 'postUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('bounds')\n\t\t.withComponentTypes<BoundsComponentTypes>()\n\t\t.withEventTypes<BoundsEventTypes>()\n\t\t.withResourceTypes<ResourceTypes>()\n\t\t.withLabels<'bounds-destroy' | 'bounds-clamp' | 'bounds-wrap'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Destroy out of bounds system\n\t\t\tworld\n\t\t\t\t.addSystem('bounds-destroy')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('entities', {\n\t\t\t\t\twith: ['worldTransform', 'destroyOutOfBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst bounds = ecs.getResource(boundsResourceKey as keyof ResourceTypes) as BoundsRect;\n\t\t\t\t\tconst minX = bounds.x ?? 0;\n\t\t\t\t\tconst minY = bounds.y ?? 0;\n\t\t\t\t\tconst maxX = minX + bounds.width;\n\t\t\t\t\tconst maxY = minY + bounds.height;\n\n\t\t\t\t\tfor (const entity of queries.entities) {\n\t\t\t\t\t\tconst { worldTransform, destroyOutOfBounds } = entity.components;\n\t\t\t\t\t\tconst padding = destroyOutOfBounds.padding ?? 0;\n\n\t\t\t\t\t\tconst exitEdge = getExitEdge(worldTransform, minX, minY, maxX, maxY, padding);\n\t\t\t\t\t\tif (!exitEdge) continue;\n\n\t\t\t\t\t\tecs.eventBus.publish('entityOutOfBounds', {\n\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\texitEdge,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (autoRemove) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Clamp to bounds system\n\t\t\tworld\n\t\t\t\t.addSystem('bounds-clamp')\n\t\t\t\t.setPriority(priority - 1)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('entities', {\n\t\t\t\t\twith: ['localTransform', 'worldTransform', 'clampToBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst bounds = ecs.getResource(boundsResourceKey as keyof ResourceTypes) as BoundsRect;\n\t\t\t\t\tconst minX = bounds.x ?? 0;\n\t\t\t\t\tconst minY = bounds.y ?? 0;\n\t\t\t\t\tconst maxX = minX + bounds.width;\n\t\t\t\t\tconst maxY = minY + bounds.height;\n\n\t\t\t\t\tfor (const entity of queries.entities) {\n\t\t\t\t\t\tconst { localTransform, worldTransform, clampToBounds } = entity.components;\n\t\t\t\t\t\tconst margin = clampToBounds.margin ?? 0;\n\n\t\t\t\t\t\tconst clampedMinX = minX + margin;\n\t\t\t\t\t\tconst clampedMinY = minY + margin;\n\t\t\t\t\t\tconst clampedMaxX = maxX - margin;\n\t\t\t\t\t\tconst clampedMaxY = maxY - margin;\n\n\t\t\t\t\t\t// Calculate world-space correction and apply to local transform\n\t\t\t\t\t\t// For entities without parents, this is equivalent to direct position clamping\n\t\t\t\t\t\tlet deltaX = 0;\n\t\t\t\t\t\tlet deltaY = 0;\n\n\t\t\t\t\t\tif (worldTransform.x < clampedMinX) deltaX = clampedMinX - worldTransform.x;\n\t\t\t\t\t\tif (worldTransform.x > clampedMaxX) deltaX = clampedMaxX - worldTransform.x;\n\t\t\t\t\t\tif (worldTransform.y < clampedMinY) deltaY = clampedMinY - worldTransform.y;\n\t\t\t\t\t\tif (worldTransform.y > clampedMaxY) deltaY = clampedMaxY - worldTransform.y;\n\n\t\t\t\t\t\tif (deltaX !== 0 || deltaY !== 0) {\n\t\t\t\t\t\t\tlocalTransform.x += deltaX;\n\t\t\t\t\t\t\tlocalTransform.y += deltaY;\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// Wrap at bounds system\n\t\t\tworld\n\t\t\t\t.addSystem('bounds-wrap')\n\t\t\t\t.setPriority(priority - 2)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('entities', {\n\t\t\t\t\twith: ['localTransform', 'worldTransform', 'wrapAtBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst bounds = ecs.getResource(boundsResourceKey as keyof ResourceTypes) as BoundsRect;\n\t\t\t\t\tconst minX = bounds.x ?? 0;\n\t\t\t\t\tconst minY = bounds.y ?? 0;\n\t\t\t\t\tconst maxX = minX + bounds.width;\n\t\t\t\t\tconst maxY = minY + bounds.height;\n\n\t\t\t\t\tfor (const entity of queries.entities) {\n\t\t\t\t\t\tconst { localTransform, worldTransform, wrapAtBounds } = entity.components;\n\t\t\t\t\t\tconst padding = wrapAtBounds.padding ?? 0;\n\n\t\t\t\t\t\tlet deltaX = 0;\n\t\t\t\t\t\tlet deltaY = 0;\n\t\t\t\t\t\tconst boundsWidth = maxX - minX;\n\t\t\t\t\t\tconst boundsHeight = maxY - minY;\n\n\t\t\t\t\t\t// Wrap horizontally\n\t\t\t\t\t\tif (worldTransform.x > maxX + padding) {\n\t\t\t\t\t\t\tdeltaX = -(boundsWidth + 2 * padding);\n\t\t\t\t\t\t} else if (worldTransform.x < minX - padding) {\n\t\t\t\t\t\t\tdeltaX = boundsWidth + 2 * padding;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Wrap vertically\n\t\t\t\t\t\tif (worldTransform.y > maxY + padding) {\n\t\t\t\t\t\t\tdeltaY = -(boundsHeight + 2 * padding);\n\t\t\t\t\t\t} else if (worldTransform.y < minY - padding) {\n\t\t\t\t\t\t\tdeltaY = boundsHeight + 2 * padding;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (deltaX !== 0 || deltaY !== 0) {\n\t\t\t\t\t\t\tlocalTransform.x += deltaX;\n\t\t\t\t\t\t\tlocalTransform.y += deltaY;\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\t\t});\n}\n\n/**\n * Determine which edge an entity has exited through, if any.\n */\nfunction getExitEdge(\n\ttransform: { x: number; y: number },\n\tminX: number,\n\tminY: number,\n\tmaxX: number,\n\tmaxY: number,\n\tpadding: number\n): 'top' | 'bottom' | 'left' | 'right' | null {\n\tif (transform.x > maxX + padding) return 'right';\n\tif (transform.x < minX - padding) return 'left';\n\tif (transform.y > maxY + padding) return 'bottom';\n\tif (transform.y < minY - padding) return 'top';\n\treturn null;\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "C4210D075EB1DBA264756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var t=(($)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy($,{get:(z,R)=>(typeof require<"u"?require:z)[R]}):$)(function($){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+$+'" is not supported')});import{definePlugin as r}from"ecspresso";var v={traumaDecay:1,maxOffsetX:10,maxOffsetY:10,maxRotation:0.05},X={smoothing:5,deadzoneX:0,deadzoneY:0,offsetX:0,offsetY:0};function jj($,z,R){let H=$-(R.x+R.shakeOffsetX),B=z-(R.y+R.shakeOffsetY),b=-(R.rotation+R.shakeRotation),E=Math.cos(b),W=Math.sin(b),L=H*E-B*W,U=H*W+B*E;return{x:L*R.zoom+R.viewportWidth/2,y:U*R.zoom+R.viewportHeight/2}}function c($,z,R){let H=($-R.viewportWidth/2)/R.zoom,B=(z-R.viewportHeight/2)/R.zoom,b=R.rotation+R.shakeRotation,E=Math.cos(b),W=Math.sin(b),L=H*E-B*W,U=H*W+B*E;return{x:L+R.x+R.shakeOffsetX,y:U+R.y+R.shakeOffsetY}}function o($){return typeof $==="number"?$:$.id}function w($){let z=$===!0?{}:$;return{trauma:0,traumaDecay:z.traumaDecay??v.traumaDecay,maxOffsetX:z.maxOffsetX??v.maxOffsetX,maxOffsetY:z.maxOffsetY??v.maxOffsetY,maxRotation:z.maxRotation??v.maxRotation}}function s($){if(Array.isArray($))return{minX:$[0],minY:$[1],maxX:$[2],maxY:$[3]};return{...$}}function u($){return{smoothing:$?.smoothing??X.smoothing,deadzoneX:$?.deadzoneX??X.deadzoneX,deadzoneY:$?.deadzoneY??X.deadzoneY,offsetX:$?.offsetX??X.offsetX,offsetY:$?.offsetY??X.offsetY}}function Jj($){let{viewportWidth:z=800,viewportHeight:R=600,initial:H,follow:B,shake:b,bounds:E,zoom:W,pan:L,systemGroup:U="camera",phase:Y="postUpdate",randomFn:F=Math.random}=$??{};return r("camera").withComponentTypes().withResourceTypes().withLabels().withGroups().requires().install((D)=>{let j={x:H?.x??0,y:H?.y??0,zoom:H?.zoom??1,rotation:H?.rotation??0,shakeOffsetX:0,shakeOffsetY:0,shakeRotation:0,viewportWidth:z,viewportHeight:R,entityId:-1,follow:()=>{},unfollow:()=>{},setPosition:()=>{},setZoom:()=>{},setRotation:()=>{},setBounds:()=>{},clearBounds:()=>{},addTrauma:()=>{}};if(D.addResource("cameraState",j),D.addSystem("camera-init").inGroup(U).setOnInitialize((V)=>{let I=V.spawn({camera:{x:H?.x??0,y:H?.y??0,zoom:H?.zoom??1,rotation:H?.rotation??0}});if(B)V.addComponent(I.id,"cameraFollow",{target:-1,...u(B)});if(b)V.addComponent(I.id,"cameraShake",w(b));if(E)V.addComponent(I.id,"cameraBounds",s(E));j.entityId=I.id,j.follow=(Q,J)=>{let P={target:o(Q),...u(J)},N=V.getComponent(j.entityId,"cameraFollow");if(N)N.target=P.target,N.smoothing=P.smoothing,N.deadzoneX=P.deadzoneX,N.deadzoneY=P.deadzoneY,N.offsetX=P.offsetX,N.offsetY=P.offsetY;else V.addComponent(j.entityId,"cameraFollow",P)},j.unfollow=()=>{if(V.getComponent(j.entityId,"cameraFollow"))V.removeComponent(j.entityId,"cameraFollow")},j.setPosition=(Q,J)=>{let G=V.getComponent(j.entityId,"camera");if(!G)return;G.x=Q,G.y=J},j.setZoom=(Q)=>{let J=V.getComponent(j.entityId,"camera");if(!J)return;J.zoom=Q},j.setRotation=(Q)=>{let J=V.getComponent(j.entityId,"camera");if(!J)return;J.rotation=Q},j.setBounds=(Q,J,G,P)=>{let N=V.getComponent(j.entityId,"cameraBounds");if(N)N.minX=Q,N.minY=J,N.maxX=G,N.maxY=P;else V.addComponent(j.entityId,"cameraBounds",{minX:Q,minY:J,maxX:G,maxY:P})},j.clearBounds=()=>{if(V.getComponent(j.entityId,"cameraBounds"))V.removeComponent(j.entityId,"cameraBounds")},j.addTrauma=(Q)=>{let J=V.getComponent(j.entityId,"cameraShake");if(J)J.trauma=Math.min(1,Math.max(0,J.trauma+Q));else V.addComponent(j.entityId,"cameraShake",{...w(!0),trauma:Math.min(1,Math.max(0,Q))})}}),D.addSystem("camera-follow").setPriority(400).inPhase(Y).inGroup(U).addQuery("cameras",{with:["camera","cameraFollow"]}).setProcess(({queries:V,dt:I,ecs:Q})=>{let J=Math.min(1,I);for(let G of V.cameras){let{camera:P,cameraFollow:N}=G.components;if(N.target<0)continue;let Z=Q.getComponent(N.target,"worldTransform");if(!Z)continue;let q=Z.x+N.offsetX,M=Z.y+N.offsetY,_=q-P.x,C=M-P.y;if(Math.abs(_)>N.deadzoneX){let K=_>0?1:-1,T=_-K*N.deadzoneX,A=Math.min(1,N.smoothing*J);P.x+=T*A}if(Math.abs(C)>N.deadzoneY){let K=C>0?1:-1,T=C-K*N.deadzoneY,A=Math.min(1,N.smoothing*J);P.y+=T*A}}}),D.addSystem("camera-shake-update").setPriority(390).inPhase(Y).inGroup(U).addQuery("shakeCameras",{with:["camera","cameraShake"]}).setProcess(({queries:V,dt:I})=>{for(let Q of V.shakeCameras){let{cameraShake:J}=Q.components;J.trauma=Math.max(0,J.trauma-J.traumaDecay*I)}}),D.addSystem("camera-bounds").setPriority(380).inPhase(Y).inGroup(U).addQuery("boundedCameras",{with:["camera","cameraBounds"]}).setProcess(({queries:V})=>{for(let I of V.boundedCameras){let{camera:Q,cameraBounds:J}=I.components,G=j.viewportWidth/(2*Q.zoom),P=j.viewportHeight/(2*Q.zoom),N=J.minX+G,Z=J.maxX-G,q=J.minY+P,M=J.maxY-P;if(N>Z)Q.x=(J.minX+J.maxX)/2;else Q.x=Math.max(N,Math.min(Z,Q.x));if(q>M)Q.y=(J.minY+J.maxY)/2;else Q.y=Math.max(q,Math.min(M,Q.y))}}),D.addSystem("camera-state-sync").setPriority(370).inPhase(Y).inGroup(U).setProcess(({ecs:V})=>{let I=V.getComponent(j.entityId,"camera");if(!I){j.x=0,j.y=0,j.zoom=1,j.rotation=0,j.shakeOffsetX=0,j.shakeOffsetY=0,j.shakeRotation=0;return}j.x=I.x,j.y=I.y,j.zoom=I.zoom,j.rotation=I.rotation;let Q=V.getComponent(j.entityId,"cameraShake");if(Q&&Q.trauma>0){let J=Q.trauma*Q.trauma;j.shakeOffsetX=Q.maxOffsetX*J*(F()*2-1),j.shakeOffsetY=Q.maxOffsetY*J*(F()*2-1),j.shakeRotation=Q.maxRotation*J*(F()*2-1)}else j.shakeOffsetX=0,j.shakeOffsetY=0,j.shakeRotation=0}),W){let Z=function(q){q.preventDefault(),J+=Math.sign(q.deltaY)},{zoomStep:V=0.1,minZoom:I=0.1,maxZoom:Q=10}=W,J=0,G=!1,P,N;D.addSystem("camera-zoom").setPriority(410).inPhase("preUpdate").inGroup(U).addQuery("cameras",{with:["camera"]}).setOnInitialize((q)=>{let M=q.tryGetResource("inputState"),_=q.tryGetResource("pixiApp");if(!M||!_){console.error("[camera] zoom requires the input plugin and renderer2D plugin. Zoom will be disabled.");return}P=_.canvas,P.addEventListener("wheel",Z,{passive:!1}),N=q.tryGetResource("isoProjection"),G=!0}).setOnDetach(()=>{if(!G||!P)return;P.removeEventListener("wheel",Z)}).setProcess(({queries:q,ecs:M})=>{if(!G||J===0)return;let _=J;J=0;let[C]=q.cameras;if(!C)return;let K=C.components.camera,T=M.tryGetResource("inputState");if(!T)return;let A=_>0?1-V:1+V,k=Math.max(I,Math.min(Q,K.zoom*Math.pow(A,Math.abs(_))));if(N&&P){let O=P.getBoundingClientRect(),y=T.pointer.position.x-(O.left+O.width/2),h=T.pointer.position.y-(O.top+O.height/2),f=N.tileWidth/2,p=N.tileHeight/2,d=(K.x-K.y)*f+N.originX,S=(K.x+K.y)*p+N.originY,l=d+y/K.zoom,m=S+h/K.zoom;K.zoom=k;let i=l-y/k,n=m-h/k,x=i-N.originX,g=n-N.originY;K.x=x/N.tileWidth+g/N.tileHeight,K.y=-x/N.tileWidth+g/N.tileHeight}else{let O=c(T.pointer.position.x,T.pointer.position.y,j);K.zoom=k,K.x=O.x-(T.pointer.position.x-j.viewportWidth/2)/k,K.y=O.y-(T.pointer.position.y-j.viewportHeight/2)/k}})}if(L){let{speed:V,actions:I}=L,Q=I?.up??"panUp",J=I?.down??"panDown",G=I?.left??"panLeft",P=I?.right??"panRight",N=!1;D.addSystem("camera-pan").setPriority(420).inPhase("preUpdate").inGroup(U).setOnInitialize((Z)=>{if(!Z.tryGetResource("inputState")){console.error("[camera] pan requires the input plugin. Pan will be disabled.");return}N=!0}).setProcess(({ecs:Z,dt:q})=>{if(!N)return;let M=Z.tryGetResource("inputState");if(!M)return;let _=V/j.zoom*q,C=(M.actions.isActive(P)?1:0)-(M.actions.isActive(G)?1:0),K=(M.actions.isActive(J)?1:0)-(M.actions.isActive(Q)?1:0);if(C!==0||K!==0)j.setPosition(j.x+C*_,j.y+K*_)})}})}export{jj as worldToScreen,c as screenToWorld,Jj as createCameraPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=25B2568457374CA064756E2164756E21
|
|
4
4
|
//# sourceMappingURL=camera.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Camera / Viewport Plugin for ECSpresso\n *\n * Provides a declarative camera with world/screen coordinate conversion, smooth follow,\n * trauma-based shake, bounds clamping, cursor-centered zoom, and logical viewport dimensions.\n *\n * This plugin is renderer-agnostic. PixiJS or other renderer integration (applying\n * cameraState to a container/stage transform) is the consumer's responsibility.\n *\n * Camera uses its own x/y/zoom/rotation rather than localTransform/worldTransform.\n * It reads the target entity's worldTransform for follow, but doesn't participate\n * in the transform hierarchy itself.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom } from '../../type-utils';\nimport type { TransformWorldConfig } from './transform';\n\n// ==================== Component Types ====================\n\nexport interface Camera {\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n}\n\nexport interface CameraFollow {\n\ttarget: number;\n\tsmoothing: number;\n\tdeadzoneX: number;\n\tdeadzoneY: number;\n\toffsetX: number;\n\toffsetY: number;\n}\n\nexport interface CameraShake {\n\ttrauma: number;\n\ttraumaDecay: number;\n\tmaxOffsetX: number;\n\tmaxOffsetY: number;\n\tmaxRotation: number;\n}\n\nexport interface CameraBounds {\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\nexport interface CameraComponentTypes {\n\tcamera: Camera;\n\tcameraFollow: CameraFollow;\n\tcameraShake: CameraShake;\n\tcameraBounds: CameraBounds;\n}\n\n// ==================== Resource Types ====================\n\nexport interface FollowOptions {\n\tsmoothing?: number;\n\tdeadzoneX?: number;\n\tdeadzoneY?: number;\n\toffsetX?: number;\n\toffsetY?: number;\n}\n\nexport type EntityHandle = { id: number };\n\nexport interface CameraState {\n\t// Read-only data (synced from camera entity each frame)\n\tx: number;\n\ty: number;\n\tzoom: number;\n\trotation: number;\n\tshakeOffsetX: number;\n\tshakeOffsetY: number;\n\tshakeRotation: number;\n\tviewportWidth: number;\n\tviewportHeight: number;\n\tentityId: number;\n\n\t// Mutation methods\n\tfollow(target: number | EntityHandle, options?: FollowOptions): void;\n\tunfollow(): void;\n\tsetPosition(x: number, y: number): void;\n\tsetZoom(zoom: number): void;\n\tsetRotation(rotation: number): void;\n\tsetBounds(minX: number, minY: number, maxX: number, maxY: number): void;\n\tclearBounds(): void;\n\taddTrauma(amount: number): void;\n}\n\nexport interface CameraResourceTypes {\n\tcameraState: CameraState;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface CameraPluginOptions<G extends string = 'camera'> {\n\tviewportWidth?: number;\n\tviewportHeight?: number;\n\tinitial?: {\n\t\tx?: number;\n\t\ty?: number;\n\t\tzoom?: number;\n\t\trotation?: number;\n\t};\n\tfollow?: FollowOptions;\n\tshake?: boolean | Partial<Omit<CameraShake, 'trauma'>>;\n\tbounds?:\n\t\t| { minX: number; minY: number; maxX: number; maxY: number }\n\t\t| [number, number, number, number];\n\tzoom?: {\n\t\tzoomStep?: number;\n\t\tminZoom?: number;\n\t\tmaxZoom?: number;\n\t};\n\tpan?: {\n\t\tspeed: number;\n\t\tactions?: {\n\t\t\tup?: string;\n\t\t\tdown?: string;\n\t\t\tleft?: string;\n\t\t\tright?: string;\n\t\t};\n\t};\n\tsystemGroup?: G;\n\tphase?: SystemPhase;\n\trandomFn?: () => number;\n}\n\n// ==================== Default Values ====================\n\nconst DEFAULT_SHAKE: Readonly<Omit<CameraShake, 'trauma'>> = {\n\ttraumaDecay: 1,\n\tmaxOffsetX: 10,\n\tmaxOffsetY: 10,\n\tmaxRotation: 0.05,\n};\n\nconst DEFAULT_FOLLOW: Readonly<Omit<CameraFollow, 'target'>> = {\n\tsmoothing: 5,\n\tdeadzoneX: 0,\n\tdeadzoneY: 0,\n\toffsetX: 0,\n\toffsetY: 0,\n};\n\n// ==================== Coordinate Conversion ====================\n\nexport function worldToScreen(\n\tworldX: number,\n\tworldY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst dx = worldX - (state.x + state.shakeOffsetX);\n\tconst dy = worldY - (state.y + state.shakeOffsetY);\n\n\tconst angle = -(state.rotation + state.shakeRotation);\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = dx * cos - dy * sin;\n\tconst ry = dx * sin + dy * cos;\n\n\treturn {\n\t\tx: rx * state.zoom + state.viewportWidth / 2,\n\t\ty: ry * state.zoom + state.viewportHeight / 2,\n\t};\n}\n\nexport function screenToWorld(\n\tscreenX: number,\n\tscreenY: number,\n\tstate: CameraState,\n): { x: number; y: number } {\n\tconst cx = (screenX - state.viewportWidth / 2) / state.zoom;\n\tconst cy = (screenY - state.viewportHeight / 2) / state.zoom;\n\n\tconst angle = state.rotation + state.shakeRotation;\n\tconst cos = Math.cos(angle);\n\tconst sin = Math.sin(angle);\n\tconst rx = cx * cos - cy * sin;\n\tconst ry = cx * sin + cy * cos;\n\n\treturn {\n\t\tx: rx + state.x + state.shakeOffsetX,\n\t\ty: ry + state.y + state.shakeOffsetY,\n\t};\n}\n\n// ==================== Internal Helpers ====================\n\nfunction resolveTarget(target: number | EntityHandle): number {\n\treturn typeof target === 'number' ? target : target.id;\n}\n\nfunction resolveShakeOptions(shake: true | Partial<Omit<CameraShake, 'trauma'>>): CameraShake {\n\tconst opts = shake === true ? {} : shake;\n\treturn {\n\t\ttrauma: 0,\n\t\ttraumaDecay: opts.traumaDecay ?? DEFAULT_SHAKE.traumaDecay,\n\t\tmaxOffsetX: opts.maxOffsetX ?? DEFAULT_SHAKE.maxOffsetX,\n\t\tmaxOffsetY: opts.maxOffsetY ?? DEFAULT_SHAKE.maxOffsetY,\n\t\tmaxRotation: opts.maxRotation ?? DEFAULT_SHAKE.maxRotation,\n\t};\n}\n\nfunction resolveBounds(\n\tbounds: { minX: number; minY: number; maxX: number; maxY: number } | [number, number, number, number],\n): CameraBounds {\n\tif (Array.isArray(bounds)) {\n\t\treturn { minX: bounds[0], minY: bounds[1], maxX: bounds[2], maxY: bounds[3] };\n\t}\n\treturn { ...bounds };\n}\n\nfunction resolveFollowOptions(options?: FollowOptions): Omit<CameraFollow, 'target'> {\n\treturn {\n\t\tsmoothing: options?.smoothing ?? DEFAULT_FOLLOW.smoothing,\n\t\tdeadzoneX: options?.deadzoneX ?? DEFAULT_FOLLOW.deadzoneX,\n\t\tdeadzoneY: options?.deadzoneY ?? DEFAULT_FOLLOW.deadzoneY,\n\t\toffsetX: options?.offsetX ?? DEFAULT_FOLLOW.offsetX,\n\t\toffsetY: options?.offsetY ?? DEFAULT_FOLLOW.offsetY,\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\ntype CameraWorldConfig = WorldConfigFrom<CameraComponentTypes, {}, CameraResourceTypes>;\n\ntype CameraLabels =\n\t| 'camera-init'\n\t| 'camera-follow'\n\t| 'camera-shake-update'\n\t| 'camera-bounds'\n\t| 'camera-state-sync'\n\t| 'camera-zoom'\n\t| 'camera-pan';\n\nexport function createCameraPlugin<G extends string = 'camera'>(\n\toptions?: CameraPluginOptions<G>,\n) {\n\tconst {\n\t\tviewportWidth = 800,\n\t\tviewportHeight = 600,\n\t\tinitial,\n\t\tfollow: followConfig,\n\t\tshake: shakeConfig,\n\t\tbounds: boundsConfig,\n\t\tzoom: zoomConfig,\n\t\tpan: panConfig,\n\t\tsystemGroup = 'camera',\n\t\tphase = 'postUpdate',\n\t\trandomFn = Math.random,\n\t} = options ?? {};\n\n\treturn definePlugin('camera')\n\t\t.withComponentTypes<CameraComponentTypes>()\n\t\t.withResourceTypes<CameraResourceTypes>()\n\t\t.withLabels<CameraLabels>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig>()\n\t\t.install((world) => {\n\t\t\t// Build mutation methods as closures over the world reference.\n\t\t\t// The cameraState resource is created immediately with placeholder methods,\n\t\t\t// then the init system populates entityId and wires up real methods.\n\n\t\t\tconst cameraState: CameraState = {\n\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\tshakeOffsetX: 0,\n\t\t\t\tshakeOffsetY: 0,\n\t\t\t\tshakeRotation: 0,\n\t\t\t\tviewportWidth,\n\t\t\t\tviewportHeight,\n\t\t\t\tentityId: -1,\n\n\t\t\t\t// Mutation methods — wired up after camera entity is spawned\n\t\t\t\tfollow: () => {},\n\t\t\t\tunfollow: () => {},\n\t\t\t\tsetPosition: () => {},\n\t\t\t\tsetZoom: () => {},\n\t\t\t\tsetRotation: () => {},\n\t\t\t\tsetBounds: () => {},\n\t\t\t\tclearBounds: () => {},\n\t\t\t\taddTrauma: () => {},\n\t\t\t};\n\n\t\t\tworld.addResource('cameraState', cameraState);\n\n\t\t\t// camera-init: spawns camera entity and wires up mutation closures\n\t\t\tworld\n\t\t\t\t.addSystem('camera-init')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs: ECSpresso<CameraWorldConfig & TransformWorldConfig>) => {\n\t\t\t\t\t// Spawn with required camera component\n\t\t\t\t\tconst entity = ecs.spawn({\n\t\t\t\t\t\tcamera: {\n\t\t\t\t\t\t\tx: initial?.x ?? 0,\n\t\t\t\t\t\t\ty: initial?.y ?? 0,\n\t\t\t\t\t\t\tzoom: initial?.zoom ?? 1,\n\t\t\t\t\t\t\trotation: initial?.rotation ?? 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\t// Conditionally add optional components\n\t\t\t\t\tif (followConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraFollow', {\n\t\t\t\t\t\t\ttarget: -1,\n\t\t\t\t\t\t\t...resolveFollowOptions(followConfig),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (shakeConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraShake', resolveShakeOptions(shakeConfig));\n\t\t\t\t\t}\n\n\t\t\t\t\tif (boundsConfig) {\n\t\t\t\t\t\tecs.addComponent(entity.id, 'cameraBounds', resolveBounds(boundsConfig));\n\t\t\t\t\t}\n\t\t\t\t\tcameraState.entityId = entity.id;\n\n\t\t\t\t\t// Wire up mutation methods\n\t\t\t\t\tcameraState.follow = (target: number | EntityHandle, opts?: FollowOptions) => {\n\t\t\t\t\t\tconst targetId = resolveTarget(target);\n\t\t\t\t\t\tconst followData: CameraFollow = {\n\t\t\t\t\t\t\ttarget: targetId,\n\t\t\t\t\t\t\t...resolveFollowOptions(opts),\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.target = followData.target;\n\t\t\t\t\t\t\texisting.smoothing = followData.smoothing;\n\t\t\t\t\t\t\texisting.deadzoneX = followData.deadzoneX;\n\t\t\t\t\t\t\texisting.deadzoneY = followData.deadzoneY;\n\t\t\t\t\t\t\texisting.offsetX = followData.offsetX;\n\t\t\t\t\t\t\texisting.offsetY = followData.offsetY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraFollow', followData);\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.unfollow = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraFollow');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setPosition = (x: number, y: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.x = x;\n\t\t\t\t\t\tcamera.y = y;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setZoom = (zoom: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.zoom = zoom;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setRotation = (rotation: number) => {\n\t\t\t\t\t\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\t\tif (!camera) return;\n\t\t\t\t\t\tcamera.rotation = rotation;\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.setBounds = (minX: number, minY: number, maxX: number, maxY: number) => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\texisting.minX = minX;\n\t\t\t\t\t\t\texisting.minY = minY;\n\t\t\t\t\t\t\texisting.maxX = maxX;\n\t\t\t\t\t\t\texisting.maxY = maxY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraBounds', { minX, minY, maxX, maxY });\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.clearBounds = () => {\n\t\t\t\t\t\tconst existing = ecs.getComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tecs.removeComponent(cameraState.entityId, 'cameraBounds');\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tcameraState.addTrauma = (amount: number) => {\n\t\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\t\tif (shake) {\n\t\t\t\t\t\t\tshake.trauma = Math.min(1, Math.max(0, shake.trauma + amount));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(cameraState.entityId, 'cameraShake', {\n\t\t\t\t\t\t\t\t...resolveShakeOptions(true),\n\t\t\t\t\t\t\t\ttrauma: Math.min(1, Math.max(0, amount)),\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\n\t\t\t// camera-follow: priority 400 (after transform propagation at 500)\n\t\t\tworld\n\t\t\t\t.addSystem('camera-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.addQuery('cameras', {\n\t\t\t\t\twith: ['camera', 'cameraFollow'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tconst t = Math.min(1, dt);\n\t\t\t\t\tfor (const entity of queries.cameras) {\n\t\t\t\t\t\tconst { camera, cameraFollow } = entity.components;\n\t\t\t\t\t\tif (cameraFollow.target < 0) continue;\n\n\t\t\t\t\t\tconst targetWorld = ecs.getComponent(cameraFollow.target, 'worldTransform');\n\t\t\t\t\t\tif (!targetWorld) continue;\n\n\t\t\t\t\t\tconst goalX = targetWorld.x + cameraFollow.offsetX;\n\t\t\t\t\t\tconst goalY = targetWorld.y + cameraFollow.offsetY;\n\t\t\t\t\t\tconst dx = goalX - camera.x;\n\t\t\t\t\t\tconst dy = goalY - camera.y;\n\n\t\t\t\t\t\tif (Math.abs(dx) > cameraFollow.deadzoneX) {\n\t\t\t\t\t\t\tconst sign = dx > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessX = dx - sign * cameraFollow.deadzoneX;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.x += excessX * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (Math.abs(dy) > cameraFollow.deadzoneY) {\n\t\t\t\t\t\t\tconst sign = dy > 0 ? 1 : -1;\n\t\t\t\t\t\t\tconst excessY = dy - sign * cameraFollow.deadzoneY;\n\t\t\t\t\t\t\tconst factor = Math.min(1, cameraFollow.smoothing * t);\n\t\t\t\t\t\t\tcamera.y += excessY * factor;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-shake-update: priority 390\n\t\t\tworld\n\t\t\t\t.addSystem('camera-shake-update')\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.addQuery('shakeCameras', {\n\t\t\t\t\twith: ['camera', 'cameraShake'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt }) => {\n\t\t\t\t\tfor (const entity of queries.shakeCameras) {\n\t\t\t\t\t\tconst { cameraShake } = entity.components;\n\t\t\t\t\t\tcameraShake.trauma = Math.max(0, cameraShake.trauma - cameraShake.traumaDecay * dt);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-bounds: priority 380\n\t\t\tworld\n\t\t\t\t.addSystem('camera-bounds')\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.addQuery('boundedCameras', {\n\t\t\t\t\twith: ['camera', 'cameraBounds'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries }) => {\n\t\t\t\t\tfor (const entity of queries.boundedCameras) {\n\t\t\t\t\t\tconst { camera, cameraBounds } = entity.components;\n\t\t\t\t\t\tconst halfW = cameraState.viewportWidth / (2 * camera.zoom);\n\t\t\t\t\t\tconst halfH = cameraState.viewportHeight / (2 * camera.zoom);\n\n\t\t\t\t\t\tconst effectiveMinX = cameraBounds.minX + halfW;\n\t\t\t\t\t\tconst effectiveMaxX = cameraBounds.maxX - halfW;\n\t\t\t\t\t\tconst effectiveMinY = cameraBounds.minY + halfH;\n\t\t\t\t\t\tconst effectiveMaxY = cameraBounds.maxY - halfH;\n\n\t\t\t\t\t\tif (effectiveMinX > effectiveMaxX) {\n\t\t\t\t\t\t\tcamera.x = (cameraBounds.minX + cameraBounds.maxX) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.x = Math.max(effectiveMinX, Math.min(effectiveMaxX, camera.x));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (effectiveMinY > effectiveMaxY) {\n\t\t\t\t\t\t\tcamera.y = (cameraBounds.minY + cameraBounds.maxY) / 2;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcamera.y = Math.max(effectiveMinY, Math.min(effectiveMaxY, camera.y));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-state-sync: priority 370\n\t\t\tworld\n\t\t\t\t.addSystem('camera-state-sync')\n\t\t\t\t.setPriority(370)\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\tconst camera = ecs.getComponent(cameraState.entityId, 'camera');\n\t\t\t\t\tif (!camera) {\n\t\t\t\t\t\tcameraState.x = 0;\n\t\t\t\t\t\tcameraState.y = 0;\n\t\t\t\t\t\tcameraState.zoom = 1;\n\t\t\t\t\t\tcameraState.rotation = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tcameraState.x = camera.x;\n\t\t\t\t\tcameraState.y = camera.y;\n\t\t\t\t\tcameraState.zoom = camera.zoom;\n\t\t\t\t\tcameraState.rotation = camera.rotation;\n\n\t\t\t\t\tconst shake = ecs.getComponent(cameraState.entityId, 'cameraShake');\n\t\t\t\t\tif (shake && shake.trauma > 0) {\n\t\t\t\t\t\tconst intensity = shake.trauma * shake.trauma;\n\t\t\t\t\t\tcameraState.shakeOffsetX = shake.maxOffsetX * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeOffsetY = shake.maxOffsetY * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t\tcameraState.shakeRotation = shake.maxRotation * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcameraState.shakeOffsetX = 0;\n\t\t\t\t\t\tcameraState.shakeOffsetY = 0;\n\t\t\t\t\t\tcameraState.shakeRotation = 0;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// camera-zoom: conditionally registered when zoom option is provided\n\t\t\tif (zoomConfig) {\n\t\t\t\tconst {\n\t\t\t\t\tzoomStep = 0.1,\n\t\t\t\t\tminZoom = 0.1,\n\t\t\t\t\tmaxZoom = 10,\n\t\t\t\t} = zoomConfig;\n\n\t\t\t\ttype ZoomInputState = { pointer: { position: { x: number; y: number } } };\n\n\t\t\t\tlet pendingSteps = 0;\n\t\t\t\tlet zoomActive = false;\n\t\t\t\tlet canvas: HTMLCanvasElement | undefined;\n\t\t\t\tlet isoState: { tileWidth: number; tileHeight: number; originX: number; originY: number } | undefined;\n\n\t\t\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tpendingSteps += Math.sign(e.deltaY);\n\t\t\t\t}\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-zoom')\n\t\t\t\t\t.setPriority(410)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('cameras', {\n\t\t\t\t\t\twith: ['camera'],\n\t\t\t\t\t})\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\t// Check for required dependencies\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tconst pixiApp = ecs.tryGetResource<{ canvas: HTMLCanvasElement }>('pixiApp');\n\n\t\t\t\t\t\tif (!inputState || !pixiApp) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] zoom requires the input plugin and renderer2D plugin. ' +\n\t\t\t\t\t\t\t\t'Zoom will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcanvas = pixiApp.canvas;\n\t\t\t\t\t\tcanvas.addEventListener('wheel', onWheel as EventListener, { passive: false });\n\n\t\t\t\t\t\t// Detect isometric projection for iso-aware cursor-centered zoom\n\t\t\t\t\t\tisoState = ecs.tryGetResource<{\n\t\t\t\t\t\t\ttileWidth: number; tileHeight: number;\n\t\t\t\t\t\t\toriginX: number; originY: number;\n\t\t\t\t\t\t}>('isoProjection');\n\n\t\t\t\t\t\tzoomActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\t\tif (!zoomActive || !canvas) return;\n\t\t\t\t\t\tcanvas.removeEventListener('wheel', onWheel as EventListener);\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tif (!zoomActive || pendingSteps === 0) return;\n\n\t\t\t\t\t\tconst steps = pendingSteps;\n\t\t\t\t\t\tpendingSteps = 0;\n\n\t\t\t\t\t\tconst [cameraEntity] = queries.cameras;\n\t\t\t\t\t\tif (!cameraEntity) return;\n\n\t\t\t\t\t\tconst cam = cameraEntity.components.camera;\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<ZoomInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\t// Apply zoom — proportional to number of wheel steps\n\t\t\t\t\t\tconst direction = steps > 0 ? (1 - zoomStep) : (1 + zoomStep);\n\t\t\t\t\t\tconst newZoom = Math.max(minZoom, Math.min(maxZoom, cam.zoom * Math.pow(direction, Math.abs(steps))));\n\n\t\t\t\t\t\tif (isoState && canvas) {\n\t\t\t\t\t\t\t// Iso-aware cursor-centered zoom: work in iso-screen space\n\t\t\t\t\t\t\tconst rect = canvas.getBoundingClientRect();\n\t\t\t\t\t\t\tconst screenOffX = inputState.pointer.position.x - (rect.left + rect.width / 2);\n\t\t\t\t\t\t\tconst screenOffY = inputState.pointer.position.y - (rect.top + rect.height / 2);\n\n\t\t\t\t\t\t\t// Inlined worldToIso — avoids cross-plugin import\n\t\t\t\t\t\t\tconst halfW = isoState.tileWidth / 2;\n\t\t\t\t\t\t\tconst halfH = isoState.tileHeight / 2;\n\t\t\t\t\t\t\tconst camIsoX = (cam.x - cam.y) * halfW + isoState.originX;\n\t\t\t\t\t\t\tconst camIsoY = (cam.x + cam.y) * halfH + isoState.originY;\n\t\t\t\t\t\t\tconst isoBeforeX = camIsoX + screenOffX / cam.zoom;\n\t\t\t\t\t\t\tconst isoBeforeY = camIsoY + screenOffY / cam.zoom;\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\t// New camera iso position so the same point stays under cursor\n\t\t\t\t\t\t\tconst newCamIsoX = isoBeforeX - screenOffX / newZoom;\n\t\t\t\t\t\t\tconst newCamIsoY = isoBeforeY - screenOffY / newZoom;\n\n\t\t\t\t\t\t\t// Inlined isoToWorld\n\t\t\t\t\t\t\tconst relX = newCamIsoX - isoState.originX;\n\t\t\t\t\t\t\tconst relY = newCamIsoY - isoState.originY;\n\t\t\t\t\t\t\tcam.x = relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t\tcam.y = -relX / isoState.tileWidth + relY / isoState.tileHeight;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Pixel-space cursor-centered zoom\n\t\t\t\t\t\t\tconst worldBefore = screenToWorld(\n\t\t\t\t\t\t\t\tinputState.pointer.position.x,\n\t\t\t\t\t\t\t\tinputState.pointer.position.y,\n\t\t\t\t\t\t\t\tcameraState,\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\tcam.zoom = newZoom;\n\n\t\t\t\t\t\t\tcam.x = worldBefore.x - (inputState.pointer.position.x - cameraState.viewportWidth / 2) / newZoom;\n\t\t\t\t\t\t\tcam.y = worldBefore.y - (inputState.pointer.position.y - cameraState.viewportHeight / 2) / newZoom;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\n\t\t\t// camera-pan: conditionally registered when pan option is provided\n\t\t\tif (panConfig) {\n\t\t\t\ttype PanInputState = { actions: { isActive(action: string): boolean } };\n\n\t\t\t\tconst {\n\t\t\t\t\tspeed,\n\t\t\t\t\tactions: panActions,\n\t\t\t\t} = panConfig;\n\n\t\t\t\tconst actionUp = panActions?.up ?? 'panUp';\n\t\t\t\tconst actionDown = panActions?.down ?? 'panDown';\n\t\t\t\tconst actionLeft = panActions?.left ?? 'panLeft';\n\t\t\t\tconst actionRight = panActions?.right ?? 'panRight';\n\n\t\t\t\tlet panActive = false;\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem('camera-pan')\n\t\t\t\t\t.setPriority(420)\n\t\t\t\t\t.inPhase('preUpdate')\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) {\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t'[camera] pan requires the input plugin. Pan will be disabled.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpanActive = true;\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ ecs, dt }) => {\n\t\t\t\t\t\tif (!panActive) return;\n\n\t\t\t\t\t\tconst inputState = ecs.tryGetResource<PanInputState>('inputState');\n\t\t\t\t\t\tif (!inputState) return;\n\n\t\t\t\t\t\tconst delta = (speed / cameraState.zoom) * dt;\n\t\t\t\t\t\tconst dx = (inputState.actions.isActive(actionRight) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionLeft) ? 1 : 0);\n\t\t\t\t\t\tconst dy = (inputState.actions.isActive(actionDown) ? 1 : 0)\n\t\t\t\t\t\t\t- (inputState.actions.isActive(actionUp) ? 1 : 0);\n\n\t\t\t\t\t\tif (dx !== 0 || dy !== 0) {\n\t\t\t\t\t\t\tcameraState.setPosition(\n\t\t\t\t\t\t\t\tcameraState.x + dx * delta,\n\t\t\t\t\t\t\t\tcameraState.y + dy * delta,\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}\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "25B2568457374CA064756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var qq=((Q)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(Q,{get:(j,R)=>(typeof require<"u"?require:j)[R]}):Q)(function(Q){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+Q+'" is not supported')});import{definePlugin as e}from"ecspresso";var N={smoothing:5,offsetX:0,offsetY:0,offsetZ:0},H={traumaDecay:1,maxOffsetX:0.3,maxOffsetY:0.3,maxOffsetZ:0.3},E=Math.PI/2,L=0.001,K={x:0,y:0,z:0};function M(Q,j,R){return Math.max(j,Math.min(R,Q))}function a(Q){if(Q===!0)return{...H};return{traumaDecay:Q.traumaDecay??H.traumaDecay,maxOffsetX:Q.maxOffsetX??H.maxOffsetX,maxOffsetY:Q.maxOffsetY??H.maxOffsetY,maxOffsetZ:Q.maxOffsetZ??H.maxOffsetZ}}function f(Q,j,R,W){let z=Math.cos(j);W.x=R*z*Math.sin(Q),W.y=R*Math.sin(j),W.z=R*z*Math.cos(Q)}function Qq(Q){let{systemGroup:j="camera3d",phase:R="postUpdate",azimuth:W=0,elevation:z=0.5,distance:C=10,target:S,minDistance:k=1,maxDistance:x=100,minElevation:U=-E+L,maxElevation:u=E-L,orbitSensitivity:T=0.003,dollySensitivity:p=1.1,enableOrbit:D=!0,follow:G,shake:_,randomFn:w=Math.random}=Q??{},g=Q?.projection??"perspective",h=Q?.projection!=="orthographic"?Q?.fov??75:75,m=Q?.projection==="orthographic"?Q.zoom??1:1,y=_?a(_):H,l=y.traumaDecay,d=y.maxOffsetX,c=y.maxOffsetY,n=y.maxOffsetZ,i={targetX:S?.x??0,targetY:S?.y??0,targetZ:S?.z??0,azimuth:W,elevation:M(z,U,u),distance:M(C,k,x),followTarget:-1,followSmoothing:G?.smoothing??N.smoothing,followOffsetX:G?.offsetX??N.offsetX,followOffsetY:G?.offsetY??N.offsetY,followOffsetZ:G?.offsetZ??N.offsetZ,trauma:0,shakeOffsetX:0,shakeOffsetY:0,shakeOffsetZ:0},r={follow($,B){let I=typeof $==="number"?$:$.id;this.followTarget=I,this.followSmoothing=B?.smoothing??G?.smoothing??N.smoothing,this.followOffsetX=B?.offsetX??G?.offsetX??N.offsetX,this.followOffsetY=B?.offsetY??G?.offsetY??N.offsetY,this.followOffsetZ=B?.offsetZ??G?.offsetZ??N.offsetZ},unfollow(){this.followTarget=-1},setTarget($,B,I){this.targetX=$,this.targetY=B,this.targetZ=I},setOrbit($,B,I){this.azimuth=$,this.elevation=M(B,U,u),this.distance=M(I,k,x)},setDistance($){this.distance=M($,k,x)},addTrauma($){this.trauma=M(this.trauma+$,0,1)}};return e("camera3d").withResourceTypes().withLabels().withGroups().requires().install(($)=>{let B={active:!1,prevX:0,prevY:0,pendingDolly:0,el:null},q={...i,...r,...g==="orthographic"?{projection:"orthographic",zoom:m,setZoom(J){this.zoom=J}}:{projection:"perspective",fov:h,setFov(J){this.fov=J}}};$.addResource("camera3DState",q);function v(J){B.active=!0,B.prevX=J.clientX,B.prevY=J.clientY,B.el?.setPointerCapture(J.pointerId)}function P(J){if(!B.active)return;let Z=J.clientX-B.prevX,b=J.clientY-B.prevY;B.prevX=J.clientX,B.prevY=J.clientY,q.azimuth-=Z*T,q.elevation=M(q.elevation+b*T,U,u)}function F(J){B.active=!1,B.el?.releasePointerCapture(J.pointerId)}function O(J){J.preventDefault(),B.pendingDolly+=Math.sign(J.deltaY)}let V=null,X=null,Y=null;$.addSystem("camera3d-init").inGroup(j).setOnInitialize((J)=>{let Z=J.getResource("threeRenderer");if(V=J.getResource("camera"),V.isPerspectiveCamera)X=V;else if(V.isOrthographicCamera)Y=V;if(q.projection==="perspective"&&!X)throw Error("createCamera3DPlugin: configured as 'perspective' but the renderer's camera is not a PerspectiveCamera.");if(q.projection==="orthographic"&&!Y)throw Error("createCamera3DPlugin: configured as 'orthographic' but the renderer's camera is not an OrthographicCamera.");if(q.projection==="perspective"&&X)q.fov=X.fov;else if(q.projection==="orthographic"&&Y)q.zoom=Y.zoom;if(B.el=Z.domElement,D)B.el.addEventListener("pointerdown",v),B.el.addEventListener("pointermove",P),B.el.addEventListener("pointerup",F);B.el.addEventListener("wheel",O,{passive:!1}),f(q.azimuth,q.elevation,q.distance,K),V.position.set(q.targetX+K.x,q.targetY+K.y,q.targetZ+K.z),V.lookAt(q.targetX,q.targetY,q.targetZ)}).setOnDetach(()=>{if(!B.el)return;if(D)B.el.removeEventListener("pointerdown",v),B.el.removeEventListener("pointermove",P),B.el.removeEventListener("pointerup",F);B.el.removeEventListener("wheel",O),B.el=null,V=null,X=null,Y=null}),$.addSystem("camera3d-follow").setPriority(400).inPhase(R).inGroup(j).setProcess(({ecs:J,dt:Z})=>{if(q.followTarget<0)return;if(!J.getEntity(q.followTarget)){q.followTarget=-1;return}let b=J.getComponent(q.followTarget,"worldTransform3D");if(!b)return;let o=b.x+q.followOffsetX,s=b.y+q.followOffsetY,t=b.z+q.followOffsetZ,A=Math.min(1,q.followSmoothing*Z);q.targetX+=(o-q.targetX)*A,q.targetY+=(s-q.targetY)*A,q.targetZ+=(t-q.targetZ)*A}),$.addSystem("camera3d-shake").setPriority(390).inPhase(R).inGroup(j).setProcess(({dt:J})=>{if(q.trauma<=0){q.shakeOffsetX=0,q.shakeOffsetY=0,q.shakeOffsetZ=0;return}q.trauma=Math.max(0,q.trauma-l*J);let Z=q.trauma*q.trauma;q.shakeOffsetX=d*Z*(w()*2-1),q.shakeOffsetY=c*Z*(w()*2-1),q.shakeOffsetZ=n*Z*(w()*2-1)}),$.addSystem("camera3d-sync").setPriority(380).inPhase(R).inGroup(j).setProcess(()=>{if(!V)return;if(B.pendingDolly!==0)q.distance=M(q.distance*Math.pow(p,B.pendingDolly),k,x),B.pendingDolly=0;if(f(q.azimuth,q.elevation,q.distance,K),V.position.set(q.targetX+K.x+q.shakeOffsetX,q.targetY+K.y+q.shakeOffsetY,q.targetZ+K.z+q.shakeOffsetZ),V.lookAt(q.targetX+q.shakeOffsetX,q.targetY+q.shakeOffsetY,q.targetZ+q.shakeOffsetZ),q.projection==="perspective"&&X&&X.fov!==q.fov)X.fov=q.fov,X.updateProjectionMatrix();else if(q.projection==="orthographic"&&Y&&Y.zoom!==q.zoom)Y.zoom=q.zoom,Y.updateProjectionMatrix()})})}export{f as sphericalToCartesian,Qq as createCamera3DPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=E7D2111F51674CD764756E2164756E21
|
|
4
4
|
//# sourceMappingURL=camera3D.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
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 { WorldConfigFrom } 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 = WorldConfigFrom<\n\tTransform3DComponentTypes,\n\t{},\n\tRenderer3DResourceTypes\n>;\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 = WorldConfigFrom<{}, {}, 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"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "E7D2111F51674CD764756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var C=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(A,B)=>(typeof require<"u"?require:A)[B]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as W}from"ecspresso";function _(j,A){return j*73856093^A*19349663}function k(j){return{cellSize:j,invCellSize:1/j,cells:new Map,entries:new Map,_entriesPrev:new Map}}function v(j){j._entriesPrev.clear();let A=j.entries;j.entries=j._entriesPrev,j._entriesPrev=A;for(let B of j.cells.values())B.length=0}function Q(j,A,B,D,J,K){let L=j._entriesPrev.get(A);if(L)j._entriesPrev.delete(A),L.x=B,L.y=D,L.halfW=J,L.halfH=K,j.entries.set(A,L);else j.entries.set(A,{entityId:A,x:B,y:D,halfW:J,halfH:K});let O=j.invCellSize,F=Math.floor((B-J)*O),Z=Math.floor((B+J)*O),$=Math.floor((D-K)*O),U=Math.floor((D+K)*O);for(let M=F;M<=Z;M++)for(let V=$;V<=U;V++){let N=_(M,V),T=j.cells.get(N);if(T)T.push(A);else j.cells.set(N,[A])}}function G(j,A,B,D,J,K){let L=j.invCellSize,O=Math.floor(A*L),F=Math.floor(D*L),Z=Math.floor(B*L),$=Math.floor(J*L);for(let U=O;U<=F;U++)for(let M=Z;M<=$;M++){let V=j.cells.get(_(U,M));if(!V)continue;for(let N=0;N<V.length;N++){let T=V[N];if(T!==void 0)K.add(T)}}}var H=new Set;function R(j,A,B,D,J){let K=H;K.clear(),G(j,A-D,B-D,A+D,B+D,K);let L=D*D;for(let O of K){let F=j.entries.get(O);if(!F)continue;let Z=Math.max(F.x-F.halfW,Math.min(A,F.x+F.halfW)),$=Math.max(F.y-F.halfH,Math.min(B,F.y+F.halfH)),U=A-Z,M=B-$;if(U*U+M*M<=L)J.add(O)}}var E=new Set;function S(j){return{grid:j,queryRect(A,B,D,J){return E.clear(),G(j,A,B,D,J,E),Array.from(E)},queryRectInto(A,B,D,J,K){G(j,A,B,D,J,K)},queryRadius(A,B,D){return E.clear(),R(j,A,B,D,E),Array.from(E)},queryRadiusInto(A,B,D,J){R(j,A,B,D,J)},getEntry(A){return j.entries.get(A)}}}function b(j){let{cellSize:A=64,systemGroup:B="spatialIndex",priority:D=2000,phases:J=["fixedUpdate","postUpdate"]}=j??{},K=k(A),L=S(K);return W("spatialIndex").withComponentTypes().withResourceTypes().withLabels().withGroups().install((O)=>{O.addResource("spatialIndex",L);for(let F of J){let Z=F==="fixedUpdate"?"localTransform":"worldTransform";O.addSystem(`spatial-index-rebuild-${F}`).setPriority(D).inPhase(F).inGroup(B).addQuery("transforms",{with:[Z]}).setProcess(({queries:$,ecs:U})=>{v(K);for(let M of $.transforms){let V=M.components[Z],N=U.getComponent(M.id,"aabbCollider"),T=U.getComponent(M.id,"circleCollider");if(!N&&!T)continue;let{x:w,y:z}=V,P=0,q=0;if(N)w+=N.offsetX??0,z+=N.offsetY??0,P=N.width/2,q=N.height/2;if(T)w+=T.offsetX??0,z+=T.offsetY??0,P=Math.max(P,T.radius),q=Math.max(q,T.radius);Q(K,M.id,w,z,P,q)}})}})}export{b as createSpatialIndexPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=1BC7337B24E77C2464756E2164756E21
|
|
4
4
|
//# sourceMappingURL=spatial-index.js.map
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"/**\n * Spatial Index Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries. Replaces O(n²) brute-force with O(n·d) where\n * d = local density.\n *\n * Standalone usage: queryRect / queryRadius for proximity queries.\n * Automatic acceleration: collision and physics2D plugins detect the\n * spatialIndex resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { TransformComponentTypes } from './transform';\nimport type { CollisionComponentTypes } from '../physics/collision';\nimport {\n\ttype SpatialEntry,\n\ttype SpatialHashGrid,\n\ttype SpatialIndex,\n\tcreateGrid,\n\tclearGrid,\n\tinsertEntity,\n\tgridQueryRect,\n\tgridQueryRadius,\n} from '../../utils/spatial-hash';\n\n// Module-scoped reusable set to reduce GC pressure\nconst _reusableQuerySet = new Set<number>();\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndexResourceTypes {\n\tspatialIndex: SpatialIndex;\n}\n\nfunction createSpatialIndexResource(grid: SpatialHashGrid): SpatialIndex {\n\treturn {\n\t\tgrid,\n\t\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: Set<number>): void {\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, result);\n\t\t},\n\t\tqueryRadius(cx: number, cy: number, radius: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRadius(grid, cx, cy, radius, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRadiusInto(cx: number, cy: number, radius: number, result: Set<number>): void {\n\t\t\tgridQueryRadius(grid, cx, cy, radius, result);\n\t\t},\n\t\tgetEntry(entityId: number): SpatialEntry | undefined {\n\t\t\treturn grid.entries.get(entityId);\n\t\t},\n\t};\n}\n\n// ==================== Component Types ====================\n\ntype SpatialIndexComponentTypes =\n\tTransformComponentTypes & Pick<CollisionComponentTypes<string>, 'aabbCollider' | 'circleCollider'>;\n\n// ==================== Plugin Options ====================\n\nexport type SpatialIndexPhase = 'fixedUpdate' | 'postUpdate';\ntype SpatialIndexLabel = `spatial-index-rebuild-${SpatialIndexPhase}`;\n\nexport interface SpatialIndexPluginOptions<G extends string = 'spatialIndex'> {\n\t/** Cell size for the spatial hash grid (default: 64) */\n\tcellSize?: number;\n\t/** System group name (default: 'spatialIndex') */\n\tsystemGroup?: G;\n\t/** Priority for rebuild systems (default: 2000, before collision) */\n\tpriority?: number;\n\t/** Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']) */\n\tphases?: ReadonlyArray<SpatialIndexPhase>;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a spatial index plugin for ECSpresso.\n *\n * Provides a uniform-grid spatial hash that accelerates collision detection.\n * When installed alongside the collision or physics2D plugins, they\n * automatically use the spatial index for broadphase instead of O(n²)\n * brute-force.\n *\n * Also provides proximity query methods for game logic (e.g. \"find all\n * enemies within 200 units\").\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .withPlugin(createSpatialIndexPlugin({ cellSize: 128 }))\n * .build();\n *\n * // Proximity query in a system:\n * const si = ecs.getResource('spatialIndex');\n * const nearby = si.queryRadius(playerX, playerY, 200);\n * ```\n */\nexport function createSpatialIndexPlugin<G extends string = 'spatialIndex'>(\n\toptions?: SpatialIndexPluginOptions<G>,\n) {\n\tconst {\n\t\tcellSize = 64,\n\t\tsystemGroup = 'spatialIndex',\n\t\tpriority = 2000,\n\t\tphases = ['fixedUpdate', 'postUpdate'] as const,\n\t} = options ?? {};\n\n\tconst grid = createGrid(cellSize);\n\tconst resource = createSpatialIndexResource(grid);\n\n\treturn definePlugin('spatialIndex')\n\t\t.withComponentTypes<SpatialIndexComponentTypes>()\n\t\t.withResourceTypes<SpatialIndexResourceTypes>()\n\t\t.withLabels<SpatialIndexLabel>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('spatialIndex', resource);\n\n\t\t\t// Register a rebuild system for each requested phase\n\t\t\tfor (const phase of phases) {\n\t\t\t\tconst transformComponent = phase === 'fixedUpdate' ? 'localTransform' : 'worldTransform';\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem(`spatial-index-rebuild-${phase}`)\n\t\t\t\t\t.setPriority(priority)\n\t\t\t\t\t.inPhase(phase as SystemPhase)\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('transforms', {\n\t\t\t\t\t\twith: [transformComponent],\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tclearGrid(grid);\n\n\t\t\t\t\t\tfor (const entity of queries.transforms) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabbCollider');\n\t\t\t\t\t\t\tconst circle = ecs.getComponent(entity.id, 'circleCollider');\n\n\t\t\t\t\t\t\t// Only insert entities that have a collider\n\t\t\t\t\t\t\tif (!aabb && !circle) continue;\n\n\t\t\t\t\t\t\tlet x = transform.x;\n\t\t\t\t\t\t\tlet y = transform.y;\n\t\t\t\t\t\t\tlet halfW = 0;\n\t\t\t\t\t\t\tlet halfH = 0;\n\n\t\t\t\t\t\t\tif (aabb) {\n\t\t\t\t\t\t\t\tx += aabb.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += aabb.offsetY ?? 0;\n\t\t\t\t\t\t\t\thalfW = aabb.width / 2;\n\t\t\t\t\t\t\t\thalfH = aabb.height / 2;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (circle) {\n\t\t\t\t\t\t\t\tx += circle.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += circle.offsetY ?? 0;\n\t\t\t\t\t\t\t\t// Circle: use radius as half-extent in both dimensions\n\t\t\t\t\t\t\t\thalfW = Math.max(halfW, circle.radius);\n\t\t\t\t\t\t\t\thalfH = Math.max(halfH, circle.radius);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tinsertEntity(grid, entity.id, x, y, halfW, halfH);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t});\n}\n",
|
|
6
6
|
"/**\n * Spatial Hash Grid\n *\n * Uniform-grid spatial hash for broadphase collision detection and\n * proximity queries. Pure data structure, no ECS dependencies.\n */\n\n// ==================== Data Structure ====================\n\nexport interface SpatialEntry {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\thalfW: number;\n\thalfH: number;\n}\n\nexport interface SpatialHashGrid {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number, number[]>;\n\tentries: Map<number, SpatialEntry>;\n\t/** Previous-frame entries held for in-place reuse during rebuild. Internal. */\n\t_entriesPrev: Map<number, SpatialEntry>;\n}\n\n// ==================== Pure Functions ====================\n\n/**\n * Hash a cell coordinate pair to a single integer key.\n * Uses large-prime XOR to distribute values.\n */\nexport function hashCell(cx: number, cy: number): number {\n\t// Large primes for spatial hashing distribution\n\treturn (cx * 73856093) ^ (cy * 19349663);\n}\n\n/**\n * Create a new empty spatial hash grid.\n */\nexport function createGrid(cellSize: number): SpatialHashGrid {\n\treturn {\n\t\tcellSize,\n\t\tinvCellSize: 1 / cellSize,\n\t\tcells: new Map(),\n\t\tentries: new Map(),\n\t\t_entriesPrev: new Map(),\n\t};\n}\n\n/**\n * Prepare the grid for a rebuild.\n *\n * Swaps `entries` with `_entriesPrev` so `insertEntity` can reuse existing\n * `SpatialEntry` objects in place (steady-state rebuilds allocate zero\n * entries). Any stale entries left in `_entriesPrev` from the previous\n * rebuild are dropped here.\n *\n * Cell buckets are cleared in place — keys are retained so subsequent\n * inserts hit the existing array rather than allocating a fresh one.\n */\nexport function clearGrid(grid: SpatialHashGrid): void {\n\tgrid._entriesPrev.clear();\n\tconst tmp = grid.entries;\n\tgrid.entries = grid._entriesPrev;\n\tgrid._entriesPrev = tmp;\n\n\tfor (const bucket of grid.cells.values()) {\n\t\tbucket.length = 0;\n\t}\n}\n\n/**\n * Insert an entity into all overlapping cells of the grid.\n */\nexport function insertEntity(\n\tgrid: SpatialHashGrid,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\thalfW: number,\n\thalfH: number,\n): void {\n\tconst recycled = grid._entriesPrev.get(entityId);\n\tif (recycled) {\n\t\tgrid._entriesPrev.delete(entityId);\n\t\trecycled.x = x;\n\t\trecycled.y = y;\n\t\trecycled.halfW = halfW;\n\t\trecycled.halfH = halfH;\n\t\tgrid.entries.set(entityId, recycled);\n\t} else {\n\t\tgrid.entries.set(entityId, { entityId, x, y, halfW, halfH });\n\t}\n\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor((x - halfW) * inv);\n\tconst maxCX = Math.floor((x + halfW) * inv);\n\tconst minCY = Math.floor((y - halfH) * inv);\n\tconst maxCY = Math.floor((y + halfH) * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst key = hashCell(cx, cy);\n\t\t\tconst bucket = grid.cells.get(key);\n\t\t\tif (bucket) {\n\t\t\t\tbucket.push(entityId);\n\t\t\t} else {\n\t\t\t\tgrid.cells.set(key, [entityId]);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Collect entity IDs from all cells overlapping the given rectangle.\n */\nexport function gridQueryRect(\n\tgrid: SpatialHashGrid,\n\tminX: number,\n\tminY: number,\n\tmaxX: number,\n\tmaxY: number,\n\tresult: Set<number>,\n): void {\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor(minX * inv);\n\tconst maxCX = Math.floor(maxX * inv);\n\tconst minCY = Math.floor(minY * inv);\n\tconst maxCY = Math.floor(maxY * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst bucket = grid.cells.get(hashCell(cx, cy));\n\t\t\tif (!bucket) continue;\n\t\t\tfor (let i = 0; i < bucket.length; i++) {\n\t\t\t\tconst entry = bucket[i];\n\t\t\t\tif (entry !== undefined) result.add(entry);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Module-scoped reusable set to reduce GC pressure\nconst _radiusCandidates = new Set<number>();\n\n/**\n * Collect entity IDs within a circle. Uses rect broadphase then\n * AABB-to-point distance filter.\n */\nexport function gridQueryRadius(\n\tgrid: SpatialHashGrid,\n\tcx: number,\n\tcy: number,\n\tradius: number,\n\tresult: Set<number>,\n): void {\n\t// Broadphase: rect query for bounding box of circle\n\tconst candidates = _radiusCandidates;\n\tcandidates.clear();\n\tgridQueryRect(grid, cx - radius, cy - radius, cx + radius, cy + radius, candidates);\n\n\tconst rSq = radius * radius;\n\n\tfor (const entityId of candidates) {\n\t\tconst entry = grid.entries.get(entityId);\n\t\tif (!entry) continue;\n\n\t\t// Closest point on entity AABB to query center\n\t\tconst closestX = Math.max(entry.x - entry.halfW, Math.min(cx, entry.x + entry.halfW));\n\t\tconst closestY = Math.max(entry.y - entry.halfH, Math.min(cy, entry.y + entry.halfH));\n\t\tconst dx = cx - closestX;\n\t\tconst dy = cy - closestY;\n\n\t\tif (dx * dx + dy * dy <= rSq) {\n\t\t\tresult.add(entityId);\n\t\t}\n\t}\n}\n\n// ==================== Resource API ====================\n\n// TODO: Move SpatialIndex interface to src/plugins/spatial/spatial-index.ts.\n// It's a resource API concern, not a data structure concern. This file should\n// only contain the grid primitives (SpatialEntry, SpatialHashGrid, and the\n// pure functions that operate on them).\nexport interface SpatialIndex {\n\treadonly grid: SpatialHashGrid;\n\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[];\n\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: Set<number>): void;\n\tqueryRadius(cx: number, cy: number, radius: number): number[];\n\tqueryRadiusInto(cx: number, cy: number, radius: number, result: Set<number>): void;\n\tgetEntry(entityId: number): SpatialEntry | undefined;\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "1BC7337B24E77C2464756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var I=((F)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(F,{get:(J,K)=>(typeof require<"u"?require:J)[K]}):F)(function(F){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+F+'" is not supported')});import{definePlugin as Z}from"ecspresso";function v(F,J,K){return F*73856093^J*19349663^K*83492791}function S(F){return{cellSize:F,invCellSize:1/F,cells:new Map,entries:new Map,_entriesPrev:new Map}}function X(F){F._entriesPrev.clear();let J=F.entries;F.entries=F._entriesPrev,F._entriesPrev=J;for(let K of F.cells.values())K.length=0}function W(F,J,K,O,L,U,V,T){let N=F._entriesPrev.get(J);if(N)F._entriesPrev.delete(J),N.x=K,N.y=O,N.z=L,N.halfW=U,N.halfH=V,N.halfD=T,F.entries.set(J,N);else F.entries.set(J,{entityId:J,x:K,y:O,z:L,halfW:U,halfH:V,halfD:T});let M=F.invCellSize,R=Math.floor((K-U)*M),G=Math.floor((K+U)*M),A=Math.floor((O-V)*M),P=Math.floor((O+V)*M),$=Math.floor((L-T)*M),j=Math.floor((L+T)*M);for(let E=R;E<=G;E++)for(let q=A;q<=P;q++)for(let B=$;B<=j;B++){let _=v(E,q,B),Q=F.cells.get(_);if(Q)Q.push(J);else F.cells.set(_,[J])}}function w(F,J,K,O,L,U,V,T){let N=F.invCellSize,M=Math.floor(J*N),R=Math.floor(L*N),G=Math.floor(K*N),A=Math.floor(U*N),P=Math.floor(O*N),$=Math.floor(V*N);for(let j=M;j<=R;j++)for(let E=G;E<=A;E++)for(let q=P;q<=$;q++){let B=F.cells.get(v(j,E,q));if(!B)continue;for(let _ of B)T.add(_)}}var Y=new Set;function H(F,J,K,O,L,U){let V=Y;V.clear(),w(F,J-L,K-L,O-L,J+L,K+L,O+L,V);let T=L*L;for(let N of V){let M=F.entries.get(N);if(!M)continue;let R=Math.max(M.x-M.halfW,Math.min(J,M.x+M.halfW)),G=Math.max(M.y-M.halfH,Math.min(K,M.y+M.halfH)),A=Math.max(M.z-M.halfD,Math.min(O,M.z+M.halfD)),P=J-R,$=K-G,j=O-A;if(P*P+$*$+j*j<=T)U.add(N)}}var k=new Set;function C(F){return{grid:F,queryBox(J,K,O,L,U,V){return k.clear(),w(F,J,K,O,L,U,V,k),Array.from(k)},queryBoxInto(J,K,O,L,U,V,T){w(F,J,K,O,L,U,V,T)},queryRadius(J,K,O,L){return k.clear(),H(F,J,K,O,L,k),Array.from(k)},queryRadiusInto(J,K,O,L,U){H(F,J,K,O,L,U)},getEntry(J){return F.entries.get(J)}}}function f(F){let{cellSize:J=64,systemGroup:K="spatialIndex3D",priority:O=2000,phases:L=["fixedUpdate","postUpdate"]}=F??{},U=S(J),V=C(U);return Z("spatialIndex3D").withComponentTypes().withResourceTypes().withLabels().withGroups().install((T)=>{T.addResource("spatialIndex3D",V);for(let N of L){let M=N==="fixedUpdate"?"localTransform3D":"worldTransform3D";T.addSystem(`spatial-index3D-rebuild-${N}`).setPriority(O).inPhase(N).inGroup(K).addQuery("transforms",{with:[M]}).runWhenEmpty().setProcess(({queries:R,ecs:G})=>{X(U);for(let A of R.transforms){let P=A.components[M],$=G.getComponent(A.id,"aabb3DCollider"),j=$?void 0:G.getComponent(A.id,"sphereCollider");if($){W(U,A.id,P.x+($.offsetX??0),P.y+($.offsetY??0),P.z+($.offsetZ??0),$.width/2,$.height/2,$.depth/2);continue}if(j){let E=j.radius;W(U,A.id,P.x+(j.offsetX??0),P.y+(j.offsetY??0),P.z+(j.offsetZ??0),E,E,E)}}})}})}export{f as createSpatialIndex3DPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=7442E014E5F95DEB64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=spatial-index3D.js.map
|