ecspresso 0.14.2 → 0.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/ai/flocking.js +2 -2
- package/dist/plugins/ai/flocking.js.map +4 -4
- package/dist/plugins/physics/collision.js +2 -2
- package/dist/plugins/physics/collision.js.map +4 -4
- package/dist/plugins/physics/collision3D.js +2 -2
- package/dist/plugins/physics/collision3D.js.map +6 -6
- package/dist/plugins/physics/physics2D.js +2 -2
- package/dist/plugins/physics/physics2D.js.map +4 -4
- package/dist/plugins/physics/physics3D.js +2 -2
- package/dist/plugins/physics/physics3D.js.map +4 -4
- package/dist/plugins/rendering/renderer3D.js +20 -20
- package/dist/plugins/rendering/renderer3D.js.map +3 -3
- package/dist/plugins/spatial/camera3D.d.ts +1 -0
- package/dist/plugins/spatial/camera3D.js +2 -2
- package/dist/plugins/spatial/camera3D.js.map +3 -3
- package/dist/plugins/spatial/spatial-index3D.js +2 -2
- package/dist/plugins/spatial/spatial-index3D.js.map +3 -3
- package/package.json +14 -2
|
@@ -87,6 +87,7 @@ export interface Camera3DBasePluginOptions<G extends string = 'camera3d'> {
|
|
|
87
87
|
maxElevation?: number;
|
|
88
88
|
orbitSensitivity?: number;
|
|
89
89
|
dollySensitivity?: number;
|
|
90
|
+
enableOrbit?: boolean;
|
|
90
91
|
follow?: Camera3DFollowOptions;
|
|
91
92
|
shake?: boolean | Partial<Camera3DShakeOptions>;
|
|
92
93
|
randomFn?: () => number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var e=Object.defineProperty;var a=(J)=>J;function qq(J,$){this[J]=a.bind(null,$)}var Qq=(J,$)=>{for(var R in $)e(J,R,{get:$[R],enumerable:!0,configurable:!0,set:qq.bind($,R)})};var $q=(J,$)=>()=>(J&&($=J(J=0)),$);var jq=((J)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(J,{get:($,R)=>(typeof require<"u"?require:$)[R]}):J)(function(J){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+J+'" is not supported')});import{definePlugin as Bq}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(J,$,R){return Math.max($,Math.min(R,J))}function Jq(J){if(J===!0)return{...H};return{traumaDecay:J.traumaDecay??H.traumaDecay,maxOffsetX:J.maxOffsetX??H.maxOffsetX,maxOffsetY:J.maxOffsetY??H.maxOffsetY,maxOffsetZ:J.maxOffsetZ??H.maxOffsetZ}}function f(J,$,R,W){let z=Math.cos($);W.x=R*z*Math.sin(J),W.y=R*Math.sin($),W.z=R*z*Math.cos(J)}function Xq(J){let{systemGroup:$="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:D=0.003,dollySensitivity:p=1.1,enableOrbit:T=!0,follow:G,shake:_,randomFn:w=Math.random}=J??{},g=J?.projection??"perspective",h=J?.projection!=="orthographic"?J?.fov??75:75,m=J?.projection==="orthographic"?J.zoom??1:1,y=_?Jq(_):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(j,B){let b=typeof j==="number"?j:j.id;this.followTarget=b,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(j,B,b){this.targetX=j,this.targetY=B,this.targetZ=b},setOrbit(j,B,b){this.azimuth=j,this.elevation=M(B,U,u),this.distance=M(b,k,x)},setDistance(j){this.distance=M(j,k,x)},addTrauma(j){this.trauma=M(this.trauma+j,0,1)}};return Bq("camera3d").withResourceTypes().withLabels().withGroups().requires().install((j)=>{let B={active:!1,prevX:0,prevY:0,pendingDolly:0,el:null},q={...i,...r,...g==="orthographic"?{projection:"orthographic",zoom:m,setZoom(Q){this.zoom=Q}}:{projection:"perspective",fov:h,setFov(Q){this.fov=Q}}};j.addResource("camera3DState",q);function v(Q){B.active=!0,B.prevX=Q.clientX,B.prevY=Q.clientY,B.el?.setPointerCapture(Q.pointerId)}function P(Q){if(!B.active)return;let Z=Q.clientX-B.prevX,I=Q.clientY-B.prevY;B.prevX=Q.clientX,B.prevY=Q.clientY,q.azimuth-=Z*D,q.elevation=M(q.elevation+I*D,U,u)}function F(Q){B.active=!1,B.el?.releasePointerCapture(Q.pointerId)}function O(Q){Q.preventDefault(),B.pendingDolly+=Math.sign(Q.deltaY)}let V=null,X=null,Y=null;j.addSystem("camera3d-init").inGroup($).setOnInitialize((Q)=>{let Z=Q.getResource("threeRenderer");if(V=Q.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,T)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(T)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}),j.addSystem("camera3d-follow").setPriority(400).inPhase(R).inGroup($).setProcess(({ecs:Q,dt:Z})=>{if(q.followTarget<0)return;let I;try{I=Q.getComponent(q.followTarget,"worldTransform3D")}catch{q.followTarget=-1;return}if(!I)return;let o=I.x+q.followOffsetX,s=I.y+q.followOffsetY,t=I.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}),j.addSystem("camera3d-shake").setPriority(390).inPhase(R).inGroup($).setProcess(({dt:Q})=>{if(q.trauma<=0){q.shakeOffsetX=0,q.shakeOffsetY=0,q.shakeOffsetZ=0;return}q.trauma=Math.max(0,q.trauma-l*Q);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)}),j.addSystem("camera3d-sync").setPriority(380).inPhase(R).inGroup($).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,Xq as createCamera3DPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=65312DDE3969710964756E2164756E21
|
|
4
4
|
//# sourceMappingURL=camera3D.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/spatial/camera3D.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Camera 3D Plugin for ECSpresso\n *\n * Orbit/follow/shake camera controls for a Three.js PerspectiveCamera or\n * OrthographicCamera managed by renderer3D. Purely resource-based (no camera\n * entity). The renderer3D `camera` resource is the single camera target.\n * Orbit via pointer drag + scroll wheel, follow via entity tracking, shake\n * via trauma-based offsets.\n *\n * The plugin's `projection` option must match the underlying camera's kind;\n * a mismatch throws at init. State is a discriminated union — perspective\n * cameras expose `fov` / `setFov`, orthographic cameras expose `zoom` / `setZoom`.\n *\n * Import from 'ecspresso/plugins/spatial/camera3D'\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { 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// 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\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\tdrag.el.addEventListener('pointerdown', onPointerDown);\n\t\t\t\t\tdrag.el.addEventListener('pointermove', onPointerMove);\n\t\t\t\t\tdrag.el.addEventListener('pointerup', onPointerUp);\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\tdrag.el.removeEventListener('pointerdown', onPointerDown);\n\t\t\t\t\tdrag.el.removeEventListener('pointermove', onPointerMove);\n\t\t\t\t\tdrag.el.removeEventListener('pointerup', onPointerUp);\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\tlet worldTransform;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tworldTransform = ecs.getComponent(state.followTarget, 'worldTransform3D');\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Entity was destroyed — auto-unfollow to avoid repeated throws\n\t\t\t\t\t\tstate.followTarget = -1;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (!worldTransform) return;\n\n\t\t\t\t\tconst goalX = worldTransform.x + state.followOffsetX;\n\t\t\t\t\tconst goalY = worldTransform.y + state.followOffsetY;\n\t\t\t\t\tconst goalZ = worldTransform.z + state.followOffsetZ;\n\n\t\t\t\t\tconst factor = Math.min(1, state.followSmoothing * dt);\n\t\t\t\t\tstate.targetX += (goalX - state.targetX) * factor;\n\t\t\t\t\tstate.targetY += (goalY - state.targetY) * factor;\n\t\t\t\t\tstate.targetZ += (goalZ - state.targetZ) * factor;\n\t\t\t\t});\n\n\t\t\t// ==================== Shake System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-shake')\n\t\t\t\t.setPriority(390)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(({ dt }) => {\n\t\t\t\t\tif (state.trauma <= 0) {\n\t\t\t\t\t\tstate.shakeOffsetX = 0;\n\t\t\t\t\t\tstate.shakeOffsetY = 0;\n\t\t\t\t\t\tstate.shakeOffsetZ = 0;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tstate.trauma = Math.max(0, state.trauma - shakeDecay * dt);\n\n\t\t\t\t\tconst intensity = state.trauma * state.trauma;\n\t\t\t\t\tstate.shakeOffsetX = shakeMaxX * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\tstate.shakeOffsetY = shakeMaxY * intensity * (randomFn() * 2 - 1);\n\t\t\t\t\tstate.shakeOffsetZ = shakeMaxZ * intensity * (randomFn() * 2 - 1);\n\t\t\t\t});\n\n\t\t\t// ==================== Sync System ====================\n\n\t\t\tworld\n\t\t\t\t.addSystem('camera3d-sync')\n\t\t\t\t.setPriority(380)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\tif (!cachedCamera) return;\n\n\t\t\t\t\t// Process pending dolly\n\t\t\t\t\tif (drag.pendingDolly !== 0) {\n\t\t\t\t\t\tstate.distance = clamp(\n\t\t\t\t\t\t\tstate.distance * Math.pow(dollySensitivity, drag.pendingDolly),\n\t\t\t\t\t\t\tminDistance,\n\t\t\t\t\t\t\tmaxDistance,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tdrag.pendingDolly = 0;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Compute camera position from spherical coords. Shake is applied as a\n\t\t\t\t\t// pure view translation — both position and lookAt target shift by the\n\t\t\t\t\t// same offset so the view pans instead of rotating. This keeps the effect\n\t\t\t\t\t// visible under orthographic projection (which has no parallax) and also\n\t\t\t\t\t// makes perspective shake magnitudes feel consistent regardless of distance.\n\t\t\t\t\tsphericalToCartesian(state.azimuth, state.elevation, state.distance, _camPos);\n\t\t\t\t\tcachedCamera.position.set(\n\t\t\t\t\t\tstate.targetX + _camPos.x + state.shakeOffsetX,\n\t\t\t\t\t\tstate.targetY + _camPos.y + state.shakeOffsetY,\n\t\t\t\t\t\tstate.targetZ + _camPos.z + state.shakeOffsetZ,\n\t\t\t\t\t);\n\t\t\t\t\tcachedCamera.lookAt(\n\t\t\t\t\t\tstate.targetX + state.shakeOffsetX,\n\t\t\t\t\t\tstate.targetY + state.shakeOffsetY,\n\t\t\t\t\t\tstate.targetZ + state.shakeOffsetZ,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (state.projection === 'perspective' && cachedPerspCamera && cachedPerspCamera.fov !== state.fov) {\n\t\t\t\t\t\tcachedPerspCamera.fov = state.fov;\n\t\t\t\t\t\tcachedPerspCamera.updateProjectionMatrix();\n\t\t\t\t\t} else if (state.projection === 'orthographic' && cachedOrthoCamera && cachedOrthoCamera.zoom !== state.zoom) {\n\t\t\t\t\t\tcachedOrthoCamera.zoom = state.zoom;\n\t\t\t\t\t\tcachedOrthoCamera.updateProjectionMatrix();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
|
|
5
|
+
"/**\n * Camera 3D Plugin for ECSpresso\n *\n * Orbit/follow/shake camera controls for a Three.js PerspectiveCamera or\n * OrthographicCamera managed by renderer3D. Purely resource-based (no camera\n * entity). The renderer3D `camera` resource is the single camera target.\n * Orbit via pointer drag + scroll wheel, follow via entity tracking, shake\n * via trauma-based offsets.\n *\n * The plugin's `projection` option must match the underlying camera's kind;\n * a mismatch throws at init. State is a discriminated union — perspective\n * cameras expose `fov` / `setFov`, orthographic cameras expose `zoom` / `setZoom`.\n *\n * Import from 'ecspresso/plugins/spatial/camera3D'\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { 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\tlet worldTransform;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tworldTransform = ecs.getComponent(state.followTarget, 'worldTransform3D');\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Entity was destroyed — auto-unfollow to avoid repeated throws\n\t\t\t\t\t\tstate.followTarget = -1;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\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": "65312DDE3969710964756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Y=Object.defineProperty;var Z=(F)=>F;function C(F,J){this[F]=Z.bind(null,J)}var p=(F,J)=>{for(var K in J)Y(F,K,{get:J[K],enumerable:!0,configurable:!0,set:C.bind(J,K)})};var z=(F,J)=>()=>(F&&(J=F(F=0)),J);var f=((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 D}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 I=new Set;function H(F,J,K,O,L,U){let V=I;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 b(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 m(F){let{cellSize:J=64,systemGroup:K="spatialIndex3D",priority:O=2000,phases:L=["fixedUpdate","postUpdate"]}=F??{},U=S(J),V=b(U);return D("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{m as createSpatialIndex3DPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=304FFB5B6DD5D73864756E2164756E21
|
|
4
4
|
//# sourceMappingURL=spatial-index3D.js.map
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/spatial/spatial-index3D.ts", "../src/utils/spatial-hash3D.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Spatial Index 3D Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries in 3D. Rebuilds the grid each frame from entity\n * transforms. Replaces O(n²) brute-force with O(n·d) where d = local density.\n *\n * Standalone usage: queryBox / queryRadius for proximity queries.\n * Automatic acceleration: collision3D and physics3D plugins detect the\n * spatialIndex3D resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { Transform3DComponentTypes } from './transform3D';\nimport {\n\ttype SpatialEntry3D,\n\ttype SpatialHashGrid3D,\n\ttype SpatialIndex3D,\n\tcreateGrid3D,\n\tclearGrid3D,\n\tinsertEntity3D,\n\tgridQueryBox3D,\n\tgridQueryRadius3D,\n} from '../../utils/spatial-hash3D';\n\n// Module-scoped reusable set to reduce GC pressure\nconst _reusableQuerySet = new Set<number>();\n\n// ==================== Collider Component Types ====================\n\n/**\n * 3D axis-aligned bounding box collider component.\n * Defined here (spatial layer) so collision3D can import rather than redefine.\n */\nexport interface AABB3DCollider {\n\twidth: number;\n\theight: number;\n\tdepth: number;\n\toffsetX?: number;\n\toffsetY?: number;\n\toffsetZ?: number;\n}\n\n/**\n * Sphere collider component.\n * Defined here (spatial layer) so collision3D can import rather than redefine.\n */\nexport interface SphereCollider {\n\tradius: number;\n\toffsetX?: number;\n\toffsetY?: number;\n\toffsetZ?: number;\n}\n\nexport interface Spatial3DColliderComponentTypes {\n\taabb3DCollider: AABB3DCollider;\n\tsphereCollider: SphereCollider;\n}\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndex3DResourceTypes {\n\tspatialIndex3D: SpatialIndex3D;\n}\n\nfunction createSpatialIndex3DResource(grid: SpatialHashGrid3D): SpatialIndex3D {\n\treturn {\n\t\tgrid,\n\t\tqueryBox(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryBox3D(grid, minX, minY, minZ, maxX, maxY, maxZ, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryBoxInto(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result: Set<number>): void {\n\t\t\tgridQueryBox3D(grid, minX, minY, minZ, maxX, maxY, maxZ, result);\n\t\t},\n\t\tqueryRadius(cx: number, cy: number, cz: number, radius: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRadius3D(grid, cx, cy, cz, radius, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRadiusInto(cx: number, cy: number, cz: number, radius: number, result: Set<number>): void {\n\t\t\tgridQueryRadius3D(grid, cx, cy, cz, radius, result);\n\t\t},\n\t\tgetEntry(entityId: number): SpatialEntry3D | undefined {\n\t\t\treturn grid.entries.get(entityId);\n\t\t},\n\t};\n}\n\n// ==================== Component Types ====================\n\ntype SpatialIndex3DComponentTypes = Transform3DComponentTypes & Spatial3DColliderComponentTypes;\n\n// ==================== Plugin Options ====================\n\nexport type SpatialIndex3DPhase = 'fixedUpdate' | 'postUpdate';\ntype SpatialIndex3DLabel = `spatial-index3D-rebuild-${SpatialIndex3DPhase}`;\n\nexport interface SpatialIndex3DPluginOptions<G extends string = 'spatialIndex3D'> {\n\t/** Cell size for the spatial hash grid (default: 64) */\n\tcellSize?: number;\n\t/** System group name (default: 'spatialIndex3D') */\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<SpatialIndex3DPhase>;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a 3D spatial index plugin for ECSpresso.\n *\n * Provides a uniform-grid spatial hash that accelerates 3D collision detection.\n * When installed alongside the collision3D or physics3D 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(createTransform3DPlugin())\n * .withPlugin(createCollision3DPlugin({ layers }))\n * .withPlugin(createSpatialIndex3DPlugin({ cellSize: 128 }))\n * .build();\n *\n * // Proximity query in a system:\n * const si = ecs.getResource('spatialIndex3D');\n * const nearby = si.queryRadius(playerX, playerY, playerZ, 200);\n * ```\n */\nexport function createSpatialIndex3DPlugin<G extends string = 'spatialIndex3D'>(\n\toptions?: SpatialIndex3DPluginOptions<G>,\n) {\n\tconst {\n\t\tcellSize = 64,\n\t\tsystemGroup = 'spatialIndex3D',\n\t\tpriority = 2000,\n\t\tphases = ['fixedUpdate', 'postUpdate'] as const,\n\t} = options ?? {};\n\n\tconst grid = createGrid3D(cellSize);\n\tconst resource = createSpatialIndex3DResource(grid);\n\n\treturn definePlugin('spatialIndex3D')\n\t\t.withComponentTypes<SpatialIndex3DComponentTypes>()\n\t\t.withResourceTypes<SpatialIndex3DResourceTypes>()\n\t\t.withLabels<SpatialIndex3DLabel>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('spatialIndex3D', 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' ? 'localTransform3D' : 'worldTransform3D';\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem(`spatial-index3D-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.runWhenEmpty()\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tclearGrid3D(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, 'aabb3DCollider');\n\t\t\t\t\t\t\
|
|
5
|
+
"/**\n * Spatial Index 3D Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries in 3D. Rebuilds the grid each frame from entity\n * transforms. Replaces O(n²) brute-force with O(n·d) where d = local density.\n *\n * Standalone usage: queryBox / queryRadius for proximity queries.\n * Automatic acceleration: collision3D and physics3D plugins detect the\n * spatialIndex3D resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { Transform3DComponentTypes } from './transform3D';\nimport {\n\ttype SpatialEntry3D,\n\ttype SpatialHashGrid3D,\n\ttype SpatialIndex3D,\n\tcreateGrid3D,\n\tclearGrid3D,\n\tinsertEntity3D,\n\tgridQueryBox3D,\n\tgridQueryRadius3D,\n} from '../../utils/spatial-hash3D';\n\n// Module-scoped reusable set to reduce GC pressure\nconst _reusableQuerySet = new Set<number>();\n\n// ==================== Collider Component Types ====================\n\n/**\n * 3D axis-aligned bounding box collider component.\n * Defined here (spatial layer) so collision3D can import rather than redefine.\n */\nexport interface AABB3DCollider {\n\twidth: number;\n\theight: number;\n\tdepth: number;\n\toffsetX?: number;\n\toffsetY?: number;\n\toffsetZ?: number;\n}\n\n/**\n * Sphere collider component.\n * Defined here (spatial layer) so collision3D can import rather than redefine.\n */\nexport interface SphereCollider {\n\tradius: number;\n\toffsetX?: number;\n\toffsetY?: number;\n\toffsetZ?: number;\n}\n\nexport interface Spatial3DColliderComponentTypes {\n\taabb3DCollider: AABB3DCollider;\n\tsphereCollider: SphereCollider;\n}\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndex3DResourceTypes {\n\tspatialIndex3D: SpatialIndex3D;\n}\n\nfunction createSpatialIndex3DResource(grid: SpatialHashGrid3D): SpatialIndex3D {\n\treturn {\n\t\tgrid,\n\t\tqueryBox(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryBox3D(grid, minX, minY, minZ, maxX, maxY, maxZ, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryBoxInto(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result: Set<number>): void {\n\t\t\tgridQueryBox3D(grid, minX, minY, minZ, maxX, maxY, maxZ, result);\n\t\t},\n\t\tqueryRadius(cx: number, cy: number, cz: number, radius: number): number[] {\n\t\t\t_reusableQuerySet.clear();\n\t\t\tgridQueryRadius3D(grid, cx, cy, cz, radius, _reusableQuerySet);\n\t\t\treturn Array.from(_reusableQuerySet);\n\t\t},\n\t\tqueryRadiusInto(cx: number, cy: number, cz: number, radius: number, result: Set<number>): void {\n\t\t\tgridQueryRadius3D(grid, cx, cy, cz, radius, result);\n\t\t},\n\t\tgetEntry(entityId: number): SpatialEntry3D | undefined {\n\t\t\treturn grid.entries.get(entityId);\n\t\t},\n\t};\n}\n\n// ==================== Component Types ====================\n\ntype SpatialIndex3DComponentTypes = Transform3DComponentTypes & Spatial3DColliderComponentTypes;\n\n// ==================== Plugin Options ====================\n\nexport type SpatialIndex3DPhase = 'fixedUpdate' | 'postUpdate';\ntype SpatialIndex3DLabel = `spatial-index3D-rebuild-${SpatialIndex3DPhase}`;\n\nexport interface SpatialIndex3DPluginOptions<G extends string = 'spatialIndex3D'> {\n\t/** Cell size for the spatial hash grid (default: 64) */\n\tcellSize?: number;\n\t/** System group name (default: 'spatialIndex3D') */\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<SpatialIndex3DPhase>;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a 3D spatial index plugin for ECSpresso.\n *\n * Provides a uniform-grid spatial hash that accelerates 3D collision detection.\n * When installed alongside the collision3D or physics3D 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(createTransform3DPlugin())\n * .withPlugin(createCollision3DPlugin({ layers }))\n * .withPlugin(createSpatialIndex3DPlugin({ cellSize: 128 }))\n * .build();\n *\n * // Proximity query in a system:\n * const si = ecs.getResource('spatialIndex3D');\n * const nearby = si.queryRadius(playerX, playerY, playerZ, 200);\n * ```\n */\nexport function createSpatialIndex3DPlugin<G extends string = 'spatialIndex3D'>(\n\toptions?: SpatialIndex3DPluginOptions<G>,\n) {\n\tconst {\n\t\tcellSize = 64,\n\t\tsystemGroup = 'spatialIndex3D',\n\t\tpriority = 2000,\n\t\tphases = ['fixedUpdate', 'postUpdate'] as const,\n\t} = options ?? {};\n\n\tconst grid = createGrid3D(cellSize);\n\tconst resource = createSpatialIndex3DResource(grid);\n\n\treturn definePlugin('spatialIndex3D')\n\t\t.withComponentTypes<SpatialIndex3DComponentTypes>()\n\t\t.withResourceTypes<SpatialIndex3DResourceTypes>()\n\t\t.withLabels<SpatialIndex3DLabel>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('spatialIndex3D', 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' ? 'localTransform3D' : 'worldTransform3D';\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem(`spatial-index3D-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.runWhenEmpty()\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tclearGrid3D(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, 'aabb3DCollider');\n\t\t\t\t\t\t\t// AABB wins when both are present, matching collision3D / physics3D.\n\t\t\t\t\t\t\tconst sphere = aabb ? undefined : ecs.getComponent(entity.id, 'sphereCollider');\n\n\t\t\t\t\t\t\tif (aabb) {\n\t\t\t\t\t\t\t\tinsertEntity3D(\n\t\t\t\t\t\t\t\t\tgrid, entity.id,\n\t\t\t\t\t\t\t\t\ttransform.x + (aabb.offsetX ?? 0),\n\t\t\t\t\t\t\t\t\ttransform.y + (aabb.offsetY ?? 0),\n\t\t\t\t\t\t\t\t\ttransform.z + (aabb.offsetZ ?? 0),\n\t\t\t\t\t\t\t\t\taabb.width / 2, aabb.height / 2, aabb.depth / 2,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (sphere) {\n\t\t\t\t\t\t\t\tconst r = sphere.radius;\n\t\t\t\t\t\t\t\tinsertEntity3D(\n\t\t\t\t\t\t\t\t\tgrid, entity.id,\n\t\t\t\t\t\t\t\t\ttransform.x + (sphere.offsetX ?? 0),\n\t\t\t\t\t\t\t\t\ttransform.y + (sphere.offsetY ?? 0),\n\t\t\t\t\t\t\t\t\ttransform.z + (sphere.offsetZ ?? 0),\n\t\t\t\t\t\t\t\t\tr, r, r,\n\t\t\t\t\t\t\t\t);\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
|
"/**\n * Spatial Hash Grid 3D\n *\n * Uniform-grid spatial hash for broadphase collision detection and\n * proximity queries in 3D. Pure data structure, no ECS dependencies.\n */\n\n// ==================== Data Structures ====================\n\nexport interface SpatialEntry3D {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\tz: number;\n\thalfW: number;\n\thalfH: number;\n\thalfD: number;\n}\n\nexport interface SpatialHashGrid3D {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number, number[]>;\n\tentries: Map<number, SpatialEntry3D>;\n\t/** Previous-frame entries held for in-place reuse during rebuild. Internal. */\n\t_entriesPrev: Map<number, SpatialEntry3D>;\n}\n\n// ==================== Pure Functions ====================\n\n/**\n * Hash a cell coordinate triple to a single integer key.\n * Uses large-prime XOR to distribute values.\n */\nexport function hashCell3D(cx: number, cy: number, cz: number): number {\n\t// Large primes for spatial hashing distribution\n\treturn (cx * 73856093) ^ (cy * 19349663) ^ (cz * 83492791);\n}\n\n/**\n * Create a new empty 3D spatial hash grid.\n */\nexport function createGrid3D(cellSize: number): SpatialHashGrid3D {\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 `insertEntity3D` can reuse existing\n * `SpatialEntry3D` 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 clearGrid3D(grid: SpatialHashGrid3D): 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 insertEntity3D(\n\tgrid: SpatialHashGrid3D,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\tz: number,\n\thalfW: number,\n\thalfH: number,\n\thalfD: 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.z = z;\n\t\trecycled.halfW = halfW;\n\t\trecycled.halfH = halfH;\n\t\trecycled.halfD = halfD;\n\t\tgrid.entries.set(entityId, recycled);\n\t} else {\n\t\tgrid.entries.set(entityId, { entityId, x, y, z, halfW, halfH, halfD });\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\tconst minCZ = Math.floor((z - halfD) * inv);\n\tconst maxCZ = Math.floor((z + halfD) * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tfor (let cz = minCZ; cz <= maxCZ; cz++) {\n\t\t\t\tconst key = hashCell3D(cx, cy, cz);\n\t\t\t\tconst bucket = grid.cells.get(key);\n\t\t\t\tif (bucket) {\n\t\t\t\t\tbucket.push(entityId);\n\t\t\t\t} else {\n\t\t\t\t\tgrid.cells.set(key, [entityId]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Collect entity IDs from all cells overlapping the given 3D box.\n */\nexport function gridQueryBox3D(\n\tgrid: SpatialHashGrid3D,\n\tminX: number,\n\tminY: number,\n\tminZ: number,\n\tmaxX: number,\n\tmaxY: number,\n\tmaxZ: 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\tconst minCZ = Math.floor(minZ * inv);\n\tconst maxCZ = Math.floor(maxZ * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tfor (let cz = minCZ; cz <= maxCZ; cz++) {\n\t\t\t\tconst bucket = grid.cells.get(hashCell3D(cx, cy, cz));\n\t\t\t\tif (!bucket) continue;\n\t\t\t\tfor (const id of bucket) result.add(id);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Module-scoped reusable set to reduce GC pressure\nconst _radiusCandidates3D = new Set<number>();\n\n/**\n * Collect entity IDs within a sphere. Uses box broadphase then\n * 3D AABB-to-point distance filter.\n */\nexport function gridQueryRadius3D(\n\tgrid: SpatialHashGrid3D,\n\tcx: number,\n\tcy: number,\n\tcz: number,\n\tradius: number,\n\tresult: Set<number>,\n): void {\n\tconst candidates = _radiusCandidates3D;\n\tcandidates.clear();\n\tgridQueryBox3D(\n\t\tgrid,\n\t\tcx - radius, cy - radius, cz - radius,\n\t\tcx + radius, cy + radius, cz + radius,\n\t\tcandidates,\n\t);\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 closestZ = Math.max(entry.z - entry.halfD, Math.min(cz, entry.z + entry.halfD));\n\t\tconst dx = cx - closestX;\n\t\tconst dy = cy - closestY;\n\t\tconst dz = cz - closestZ;\n\n\t\tif (dx * dx + dy * dy + dz * dz <= rSq) {\n\t\t\tresult.add(entityId);\n\t\t}\n\t}\n}\n\n// ==================== SpatialIndex3D Interface ====================\n\n/**\n * High-level spatial index API for 3D broadphase queries.\n *\n * Defined here (the utility layer) so that narrowphase3D can accept it\n * without importing the ECS plugin. The spatial-index3D plugin creates\n * an object that implements this interface and registers it as a resource.\n */\nexport interface SpatialIndex3D {\n\treadonly grid: SpatialHashGrid3D;\n\tqueryBox(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number[];\n\tqueryBoxInto(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result: Set<number>): void;\n\tqueryRadius(cx: number, cy: number, cz: number, radius: number): number[];\n\tqueryRadiusInto(cx: number, cy: number, cz: number, radius: number, result: Set<number>): void;\n\tgetEntry(entityId: number): SpatialEntry3D | undefined;\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "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
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "304FFB5B6DD5D73864756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecspresso",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -186,7 +186,10 @@
|
|
|
186
186
|
"peerDependencies": {
|
|
187
187
|
"typescript": "^6.0.3",
|
|
188
188
|
"pixi.js": "^8.0.0",
|
|
189
|
-
"howler": "^2.2.0"
|
|
189
|
+
"howler": "^2.2.0",
|
|
190
|
+
"three": "^0.184.0",
|
|
191
|
+
"react": "^19.0.0",
|
|
192
|
+
"react-dom": "^19.0.0"
|
|
190
193
|
},
|
|
191
194
|
"peerDependenciesMeta": {
|
|
192
195
|
"pixi.js": {
|
|
@@ -194,6 +197,15 @@
|
|
|
194
197
|
},
|
|
195
198
|
"howler": {
|
|
196
199
|
"optional": true
|
|
200
|
+
},
|
|
201
|
+
"three": {
|
|
202
|
+
"optional": true
|
|
203
|
+
},
|
|
204
|
+
"react": {
|
|
205
|
+
"optional": true
|
|
206
|
+
},
|
|
207
|
+
"react-dom": {
|
|
208
|
+
"optional": true
|
|
197
209
|
}
|
|
198
210
|
},
|
|
199
211
|
"files": [
|