@hology/core 0.0.265 → 0.0.266
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/effects/sequence/sequence-player.js +1 -1
- package/dist/effects/vfx/gpu-material-uniforms.d.ts +23 -0
- package/dist/effects/vfx/gpu-material-uniforms.js +4 -0
- package/dist/effects/vfx/gpu-sprite-runtime.js +1 -1
- package/dist/effects/vfx/vfx-actor.d.ts +6 -0
- package/dist/effects/vfx/vfx-actor.js +1 -1
- package/dist/effects/vfx/vfx-materializer.d.ts +1 -0
- package/dist/effects/vfx/vfx-materializer.js +1 -1
- package/dist/effects/vfx/vfx-param.js +1 -1
- package/dist/effects/vfx/vfx-render-preparation.d.ts +8 -0
- package/dist/effects/vfx/vfx-render-preparation.js +4 -0
- package/dist/gameplay/actors/builtin/components/character/character-animation.d.ts +53 -0
- package/dist/gameplay/actors/builtin/components/character/character-animation.js +1 -1
- package/dist/gameplay/actors/builtin/components/character/character-movement.js +1 -1
- package/dist/gameplay/ai/behavior-tree/behavior-tree.d.ts +8 -1
- package/dist/gameplay/ai/behavior-tree/behavior-tree.js +1 -1
- package/dist/gameplay/ai/behavior-tree/behavior-tree.test.js +1 -1
- package/dist/gameplay/ai/behavior-tree/bt.d.ts +9 -0
- package/dist/gameplay/ai/behavior-tree/bt.js +1 -1
- package/dist/gameplay/ai/behavior-tree/bt.test.js +1 -1
- package/dist/gameplay/animation/foot-contact-baker.d.ts +23 -0
- package/dist/gameplay/animation/foot-contact-baker.js +4 -0
- package/dist/gameplay/animation/foot-contact.d.ts +27 -0
- package/dist/gameplay/animation/foot-contact.js +4 -0
- package/dist/gameplay/animation/locomotion-body.d.ts +62 -0
- package/dist/gameplay/animation/locomotion-body.js +4 -0
- package/dist/gameplay/animation/pose-inertialization.d.ts +46 -0
- package/dist/gameplay/animation/pose-inertialization.js +4 -0
- package/dist/gameplay/animation/root-motion.js +1 -1
- package/dist/gameplay/index.d.ts +4 -0
- package/dist/gameplay/index.js +1 -1
- package/dist/gameplay/initiate.js +1 -1
- package/dist/gameplay/input/input-service.d.ts +4 -0
- package/dist/gameplay/input/input-service.js +1 -1
- package/dist/gameplay/rendering-host.d.ts +2 -1
- package/dist/gameplay/rendering-host.js +1 -1
- package/dist/gameplay/services/physics/physics-system.d.ts +7 -0
- package/dist/gameplay/services/physics/physics-system.js +1 -1
- package/dist/gameplay/services/render.d.ts +2 -0
- package/dist/gameplay/services/render.js +1 -1
- package/dist/rendering/webgpu-experimental-backend.d.ts +1 -1
- package/dist/rendering/webgpu-experimental-backend.js +1 -1
- package/dist/rendering.d.ts +4 -2
- package/dist/rendering.js +1 -1
- package/dist/scene/animation-binary.d.ts +6 -0
- package/dist/scene/animation-binary.js +4 -0
- package/dist/scene/asset-resource-loader.d.ts +3 -1
- package/dist/scene/asset-resource-loader.js +1 -1
- package/dist/scene/landscape/landscape-manager.d.ts +4 -1
- package/dist/scene/landscape/landscape-manager.js +1 -1
- package/dist/scene/landscape/landscape-static-draw-sets.d.ts +18 -0
- package/dist/scene/landscape/landscape-static-draw-sets.js +4 -0
- package/dist/scene/materializer.d.ts +2 -0
- package/dist/scene/materializer.js +1 -1
- package/dist/shader/shader.d.ts +8 -0
- package/dist/test/animation-binary.test.d.ts +2 -0
- package/dist/test/animation-binary.test.js +4 -0
- package/dist/test/character-foot-contact.test.d.ts +2 -0
- package/dist/test/character-foot-contact.test.js +4 -0
- package/dist/test/character-inertialization.test.d.ts +2 -0
- package/dist/test/character-inertialization.test.js +4 -0
- package/dist/test/character-movement-solve.test.d.ts +2 -0
- package/dist/test/character-movement-solve.test.js +4 -0
- package/dist/test/foot-contact.test.d.ts +2 -0
- package/dist/test/foot-contact.test.js +4 -0
- package/dist/test/gpu-material-uniforms.test.d.ts +2 -0
- package/dist/test/gpu-material-uniforms.test.js +4 -0
- package/dist/test/gpu-sprite-pipeline-cache.test.d.ts +2 -0
- package/dist/test/gpu-sprite-pipeline-cache.test.js +4 -0
- package/dist/test/input.test.js +1 -1
- package/dist/test/landscape-static-draw-sets.test.d.ts +2 -0
- package/dist/test/landscape-static-draw-sets.test.js +4 -0
- package/dist/test/locomotion-body.test.d.ts +2 -0
- package/dist/test/locomotion-body.test.js +4 -0
- package/dist/test/physics-ray-object.test.d.ts +2 -0
- package/dist/test/physics-ray-object.test.js +4 -0
- package/dist/test/pose-inertialization.test.d.ts +2 -0
- package/dist/test/pose-inertialization.test.js +4 -0
- package/dist/test/renderer-loading.test.d.ts +2 -0
- package/dist/test/renderer-loading.test.js +4 -0
- package/dist/test/rendering-host.test.js +1 -1
- package/dist/test/sequence-property-parameters.test.js +1 -1
- package/dist/test/sequence-vfx.test.js +1 -1
- package/dist/test/vfx-render-preparation.test.d.ts +2 -0
- package/dist/test/vfx-render-preparation.test.js +4 -0
- package/dist/utils/structs.d.ts +4 -0
- package/dist/utils/structs.js +4 -0
- package/package.json +4 -2
- package/tsconfig.tsbuildinfo +1 -1
package/dist/shader/shader.d.ts
CHANGED
|
@@ -12,9 +12,17 @@ export declare abstract class Shader {
|
|
|
12
12
|
export type ShaderType = Type<Shader>;
|
|
13
13
|
export type ActorType = Type<BaseActor>;
|
|
14
14
|
export type ActorComponentType = Type<ActorComponent>;
|
|
15
|
+
/** Original TypeScript definition; line and column are one-based. */
|
|
16
|
+
export interface ClassSource {
|
|
17
|
+
/** Project-relative path using forward slashes. */
|
|
18
|
+
file: string;
|
|
19
|
+
line: number;
|
|
20
|
+
column: number;
|
|
21
|
+
}
|
|
15
22
|
export type ClassImpl<T> = {
|
|
16
23
|
name: string;
|
|
17
24
|
type: Type<T>;
|
|
25
|
+
source?: ClassSource;
|
|
18
26
|
};
|
|
19
27
|
export type ActorImpl = ClassImpl<BaseActor>;
|
|
20
28
|
export type ShaderImpl = ClassImpl<Shader>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as n}from"vitest";import{AnimationClip as o,VectorKeyframeTrack as a,QuaternionKeyframeTrack as r,NumberKeyframeTrack as s,InterpolateDiscrete as i,InterpolateSmooth as l,AdditiveAnimationBlendMode as c}from"three";import{encodeAnimationClips as m,parseAnimationClips as f}from"../scene/animation-binary.js";e(".anim",()=>{n("preserves samples, names, blend mode and interpolation with shared buffer views",()=>{const e=[0,.5,1],n=[new a("骨.position",e,[0,1,2,3,4,5,6,7,8],l),new r("bone.quaternion",e,[0,0,0,1,0,.5,0,.8660254,0,1,0,0]),new s("mesh.morphTargetInfluences[0]",e,[0,1,0],i)],u=new o("歩行",1,n,c),d=m([u]),p=f(d)[0];t(p.name).toBe(u.name),t(p.blendMode).toBe(u.blendMode);for(let e=0;e<n.length;e++){const o=p.tracks[e],a=n[e];t(o.times.buffer).toBe(d),t(o.values.buffer).toBe(d),t(o.times.byteOffset).toBe(p.tracks[0].times.byteOffset),t(o.values).toEqual(a.values);for(const e of[0,.1,.5,.9,1])t(Array.from(o.createInterpolant().evaluate(e))).toEqual(Array.from(a.createInterpolant().evaluate(e)))}}),n("rejects truncated and unaligned data",()=>{const e=m([new o("x",1,[new s("a",[0,1],[0,1])])]);t(()=>f(e.slice(0,-1))).toThrow(),new DataView(e).setUint32(4,2,!0),t(()=>f(e)).toThrow()}),n("preserves glTF cubic tangent layout and endpoint behavior",()=>{const e=new s("mesh.morphTargetInfluences[0]",[0,1],[0,0,1,1,1,0]),n=()=>{throw new Error("Not evaluated during encoding")};n.isInterpolantFactoryMethodGLTFCubicSpline=!0,e.createInterpolant=n;const a=m([new o("cubic",1,[e])]),r=f(a)[0].tracks[0].createInterpolant();for(const e of[-1,0,.25,.5,.75,1,2])t(r.evaluate(e)[0]).toBeCloseTo(Math.max(0,Math.min(1,e)))})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,test as o,vi as n}from"vitest";import{AnimationClip as s,Bone as a,BufferGeometry as i,Object3D as r,Quaternion as l,Skeleton as c,SkinnedMesh as p,Vector3 as d}from"three";n.mock("../gameplay/actors/component.js",()=>({ActorComponent:class{},Component:()=>e=>e})),n.mock("../gameplay/inject.js",()=>({inject:()=>{}})),n.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),n.mock("../gameplay/services/world.js",()=>({World:class{}})),n.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{},RayTestResult:class{}}));import{CharacterAnimationComponent as m}from"../gameplay/actors/builtin/components/character/character-animation.js";import{setAnimationFootContacts as g}from"../gameplay/animation/foot-contact.js";function f(){const e=new r,t=new r;e.add(t);const o=new a;o.name="mixamorigHips",o.position.y=.9,t.add(o);const n=["Left","Right"].map((e,t)=>{const n=new a;n.name=`mixamorig${e}UpLeg`,n.position.x=0===t?-.12:.12;const s=new a;s.name=`mixamorig${e}Leg`,s.position.set(0,-.4,.06);const i=new a;i.name=`mixamorig${e}Foot`,i.position.set(0,-.4,-.06);const r=new a;return r.name=`mixamorig${e}ToeBase`,r.position.z=.15,o.add(n),n.add(s),s.add(i),i.add(r),{upper:n,lower:s,foot:i,toe:r}}),l=new p(new i);t.add(l),t.updateMatrixWorld(!0),l.bind(new c([o,...n.flatMap(e=>[e.upper,e.lower,e.foot,e.toe])]));const f=new m;let h=0;Object.assign(f,{actor:{object:e},physicsSystem:{rayTest:e=>(h++,{hasHit:!0,hitPoint:new d(e.x,0,e.z),hitNormal:new d(0,1,0)})}}),f.footIkEnabled=!0,f.footIkLocomotionEnabled=!0,f.movementSpeed=4,f.setup(t,[],o);const w=new s("locomotion",1,[]);g(w,{left:[{start:0,end:.4}],right:[{start:0,end:1}]}),f.play(w);return{actor:e,hips:o,legs:n,animation:f,tick:()=>f.updateInternal(1/60),getRayCount:()=>h}}e("character contact-driven IK pipeline",()=>{o("holds a foot on flat ground using masked clip metadata while retaining ankle orientation",()=>{const{actor:e,legs:o,animation:n,tick:s}=f();s();const a=o[0].foot.getWorldPosition(new d);for(let t=0;t<10;t++)e.position.z+=.006,s();const i=o[0].foot.getWorldPosition(new d);t(Math.abs(i.z-a.z)).toBeLessThan(.005),t(i.y).toBeCloseTo(n.footIkFootOffset+.01,2),t(o[0].foot.getWorldQuaternion(new l).angleTo(new l)).toBeLessThan(1e-6)}),o("restores untracked joints and pelvis instead of accumulating IK, and releases on swing",()=>{const{actor:e,hips:o,legs:n,animation:s,tick:a,getRayCount:i}=f();for(let e=0;e<12;e++)a();t(o.position.y).toBeGreaterThan(.85),s.getFullBodyAction().time=.5,e.position.z=.1;const r=i();a(),t(i()-r).toBe(4);for(let e=0;e<10;e++)a();const c=n[0].foot.getWorldPosition(new d);t(c.z).toBeCloseTo(.1,2),s.footIkLocomotionActive=!1,a(),t(o.position.y).toBeCloseTo(.9,8),t(n[0].upper.quaternion.angleTo(new l)).toBeLessThan(1e-6),t(n[0].lower.quaternion.angleTo(new l)).toBeLessThan(1e-6)}),o("keeps support stable while the before-IK body processor leans the pelvis",()=>{const{hips:e,legs:o,animation:n,tick:s}=f(),a=new d(0,0,1);let i=0;n.addPoseProcessor({id:"test-body-restore",phase:"beforeMixer",update:()=>e.quaternion.identity()}),n.addPoseProcessor({id:"test-body-lean",phase:"beforeIk",update:()=>e.quaternion.setFromAxisAngle(a,i)}),s();const r=o[0].foot.getWorldPosition(new d);for(let e=0;e<10;e++)i+=.006,s();const c=o[0].foot.getWorldPosition(new d);t(Math.abs(c.x-r.x)).toBeLessThan(.005),t(Math.abs(c.z-r.z)).toBeLessThan(.005),t(o[0].foot.getWorldQuaternion(new l).angleTo(new l)).toBeLessThan(1e-6),t(o[0].lower.getWorldPosition(new d).z).toBeGreaterThan(c.z)}),o("clears contact correction during clip transitions and external sequence evaluation",()=>{const{legs:e,animation:o,tick:n}=f();n();const a=new s("next",1,[]);g(a,{left:[{start:0,end:1}],right:[{start:0,end:1}]}),o.play(a),n(),t(e[0].upper.quaternion.angleTo(new l)).toBeLessThan(1e-6);for(let e=0;e<15;e++)n();o.beginExternalControl(),o.evaluateExternalPose(),t(e[0].upper.quaternion.angleTo(new l)).toBeLessThan(1e-6)}),o("preserves support through inertia and waits for legs to settle before adopting idle contacts",()=>{const{animation:e,tick:o,getRayCount:n}=f();g(e.getFullBodyAction().getClip(),{left:[{start:0,end:1}],right:[]});for(let e=0;e<6;e++)o();const a=new s("idle",1,[]);g(a,{left:[{start:0,end:1}],right:[{start:0,end:1}]}),e.addTransitionProcessor({id:"test-inertia",prepare:t=>(e.preserveFootIkContacts(t.targetPoseClip,.12),!0)}),e.play(a);let i=n();o(),t(n()-i).toBe(4);for(let e=0;e<7;e++)o();i=n(),o(),t(n()-i).toBe(8)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,test as n,vi as o}from"vitest";import{AnimationClip as a,Bone as i,NumberKeyframeTrack as r,Object3D as s,Quaternion as l,QuaternionKeyframeTrack as c,Vector3 as d}from"three";import{PoseInertializer as p}from"../gameplay/animation/pose-inertialization.js";o.mock("../gameplay/actors/component.js",()=>({ActorComponent:class{},Component:()=>e=>e})),o.mock("../gameplay/inject.js",()=>({inject:()=>{}})),o.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),o.mock("../gameplay/services/world.js",()=>({World:class{}})),o.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{},RayTestResult:class{}}));import{CharacterAnimationComponent as m}from"../gameplay/actors/builtin/components/character/character-animation.js";function u(){const e=new s,t=new i;t.name="hips";const n=new i;n.name="arm";const o=new i;o.name="blink",e.add(t),t.add(n,o);const r=new m;Object.assign(r,{actor:{object:e}}),r.setup(e,[],t);const u=new d(0,0,1),g=e=>(new l).setFromAxisAngle(u,e).toArray(),y=new a("run",1,[new c("arm.quaternion",[0,.5,1],[...g(-.4),...g(.4),...g(-.4)])]),h=new a("idle",1,[new c("arm.quaternion",[0,1],[...g(0),...g(0)])]),w=new p([{bone:n,duration:.3,maxRotation:Math.PI}]);r.addPoseProcessor({id:"restore",phase:"beforeMixer",order:50,update:()=>w.restore()}),r.addPoseProcessor({id:"inertia",phase:"afterMixer",order:-100,update:({deltaTime:e})=>{r.isSequenceControlled||r.hasUpperBodyOverride?w.reset():w.update(e)}});let B=0;const f=r.addTransitionProcessor({id:"inertia",prepare:e=>(B++,e.targetClip===h&&w.transitionTo(e.targetPoseClip,e.options.offset??0,e.targetTimeScale))}),S=()=>r.updateInternal(1/60);r.play(y);for(let e=0;e<18;e++)S();return{animation:r,arm:n,run:y,idle:h,pose:w,tick:S,unregister:f,getPreparations:()=>B}}e("character inertialization playback",()=>{n("replaces crossfade with a target action while preserving forward arm motion",()=>{const{animation:e,arm:n,run:o,idle:a,tick:i}=u(),r=e.getFullBodyAction(),s=2*Math.atan2(n.quaternion.z,n.quaternion.w);e.play(a),t(e.getFullBodySourceClip()).toBe(a),t(r.isScheduled()).toBe(!1),i(),t(e.getFullBodyAction().getEffectiveWeight()).toBe(1),t(2*Math.atan2(n.quaternion.z,n.quaternion.w)).toBeGreaterThan(s),t(e.getFullBodySourceClip()).not.toBe(o);for(let e=0;e<20;e++)i();t(n.quaternion.angleTo(new l)).toBeLessThan(1e-7)}),n("stops all managed fading sources while retaining an unrelated mixer action",()=>{const{animation:e,run:n,idle:o,tick:i}=u(),s=e.getFullBodyAction(),l=n.clone();l.name="sprint",e.play(l),i();const c=e.getFullBodyAction();t(s.isScheduled()).toBe(!0);const d=e.getMixer().clipAction(new a("face",1,[new r("blink.scale[x]",[0,1],[1,1.2])])).play();e.play(o),i(),t(s.isScheduled()).toBe(!1),t(c.isScheduled()).toBe(!1),t(d.isScheduled()).toBe(!0)}),n("keeps the original crossfade when no transition processor accepts the change",()=>{const{animation:e,idle:n,tick:o,unregister:a}=u();a();const i=e.getFullBodyAction();e.play(n),o(),t(i.isScheduled()).toBe(!0),t(e.getFullBodyAction().getEffectiveWeight()).toBeLessThan(1)}),n("never requests inertia for upper-body overrides or authored sequences",()=>{const e=u();e.animation.playUpper(new a("ability",1,[]));const n=e.getPreparations();e.animation.play(e.idle),t(e.getPreparations()).toBe(n);const o=u(),i=o.getPreparations();o.animation.play(o.idle,{sequenceControlled:!0}),t(o.getPreparations()).toBe(i),o.animation.beginExternalAnimationControl(new a("external",1,[])),t(o.animation.getFullBodySourceClip()).toBeUndefined()}),n("removes a fading external sequence action when subsequent locomotion stops",()=>{const{animation:e,run:n,idle:o,tick:a}=u(),i=n.clone();i.name="external";const r=e.beginExternalAnimationControl(i);e.evaluateExternalPose(),e.stopSequenceAnimation(),e.play(n),a(),a(),t(r.isScheduled()).toBe(!0),e.play(o),a(),t(r.isScheduled()).toBe(!1)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{afterAll as e,beforeAll as t,describe as o,expect as n,test as s,vi as a}from"vitest";import{Vector3 as i}from"three";import{CharacterMovementMode as l}from"../gameplay/actors/builtin/components/character/modes.js";let r;function c(e,t,o=l.walking){const n=new i,s=[],r={getActorComputedMovement:a.fn((e,t,o)=>(s.push(o.clone()),n.copy(o).multiplyScalar(.5))),setNextKinematicPosition:a.fn(),updateActorTransform:a.fn(),flushModifiedBodyPositionsToColliders:a.fn()};return{component:{actor:{position:new i},physicsSystem:r,state:{grounded:e,mode:o,position:new i},rayTestResult:{distance:1/0},suppressUncrouchLift:!1,getCollisionGroup:()=>null,getEffectiveGravity:()=>-10,cc:{offset:()=>.01,maxSlopeClimbAngle:()=>Math.PI/4,computedCollision:a.fn((e,o)=>{o.normal1=t})}},requests:s,physicsSystem:r}}e(()=>a.restoreAllMocks()),t(async()=>{"undefined"!=typeof HTMLCanvasElement&&a.spyOn(HTMLCanvasElement.prototype,"getContext").mockImplementation(()=>({fillStyle:"",fillRect(){},getImageData:()=>({data:new Uint8ClampedArray(4)})}));const{CharacterMovementComponent:e}=await import("../gameplay/actors/builtin/components/character/character-movement.js");r=e.prototype.applyMovement},3e4),o("character movement solve reuse",()=>{s.each([l.walking,l.sliding])("solves ordinary grounded movement once in mode %s",e=>{const{component:t,requests:o,physicsSystem:s}=c(!0,new i(0,1,0),e);r.call(t,new i(2,-1,0),.016,!1),n(o).toHaveLength(1),n(t.state.position.toArray()).toEqual([.016,-.008,0]),n(s.setNextKinematicPosition).toHaveBeenCalledWith(t.actor,t.state.position)}),s("solves again when steep-slope handling changes the requested translation",()=>{const{component:e,requests:t}=c(!0,new i(1,0,0));r.call(e,new i(2,0,0),.016,!1),n(t).toHaveLength(2),n(t[0].y).toBe(0),n(t[1].y).toBe(-.08),n(e.state.position.toArray()).toEqual([.016,-.04,0])}),s("reuses steep-slope results when the requested translation already matches",()=>{const{component:e,requests:t}=c(!0,new i(1,0,0));r.call(e,new i(2,-.08,0),1,!1),n(t).toHaveLength(1),n(e.state.position.toArray()).toEqual([1,-.04,0])}),s("retains the first movement result when no collision normal is available",()=>{const{component:e,requests:t}=c(!0,null);r.call(e,new i(2,0,0),.016,!1),n(t).toHaveLength(1),n(e.state.position.x).toBe(.016)}),s("keeps airborne solves and immediate physics synchronization",()=>{const{component:e,requests:t,physicsSystem:o}=c(!1,null);r.call(e,new i(2,-1,0),.016,!0),n(t).toHaveLength(1),n(e.cc.computedCollision).not.toHaveBeenCalled(),n(e.actor.position.toArray()).toEqual(e.state.position.toArray()),n(o.updateActorTransform).toHaveBeenCalledWith(e.actor),n(o.flushModifiedBodyPositionsToColliders).toHaveBeenCalledOnce()}),s("does not solve stationary grounded movement",()=>{const{component:e,requests:t}=c(!0,null);r.call(e,new i,.016,!1),n(t).toHaveLength(0),n(e.state.position.toArray()).toEqual([0,0,0])})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as t,expect as e,test as o}from"vitest";import{AnimationClip as n,Bone as a,Object3D as i,Vector3 as s,VectorKeyframeTrack as r}from"three";import{FootContactAnchor as p,getAnimationFootContacts as c,isFootInContact as l,setAnimationFootContacts as d}from"../gameplay/animation/foot-contact.js";import{FootContactBaker as h}from"../gameplay/animation/foot-contact-baker.js";import{RootMotionClip as m}from"../gameplay/animation/root-motion.js";const f=new s(0,0,1);t("foot contact phase data",()=>{o("samples loop seams and nonlooping phases without creating another contact at clip end",()=>{const t=[{start:.8,end:.2}];e(l(t,.1,1,!0)).toBe(!0),e(l(t,.9,1,!0)).toBe(!0),e(l(t,1.1,1,!0)).toBe(!0),e(l(t,-.1,1,!0)).toBe(!0),e(l(t,.5,1,!0)).toBe(!1),e(l([{start:0,end:.2}],1.1,1,!1)).toBe(!1),e(l(t,.1,0,!0)).toBe(!1)}),o("validates authored overrides and carries them through root motion wrapping/cloning",()=>{const t=new n("walk",1,[]),o={left:[{start:.1,end:.4}],right:[{start:.6,end:.9}]};d(t,o);const a=m.fromClipWithDistance(t,1);e(c(a)).toBe(o),d(a,{left:[],right:[]}),e(c(t)).toBe(o),e(()=>d(t,{left:[{start:-.1,end:.2}],right:[]})).toThrow()})}),t("stance anchors",()=>{function t(){const t=new p,e=new s(0,.08,0),o=e.clone(),n=new s(0,.9,0);return{anchor:t,animated:e,ground:o,hip:n,update:(a=!0,i=!0,s=f)=>t.update(a,i,e,o,s,n,.9,.12,.18,Math.PI/5)}}o("holds the original world contact while the animation and hips move",()=>{const{anchor:o,animated:n,ground:a,hip:i,update:s}=t();e(s()).toBe(!0),n.z=.1,a.z=.1,i.z=.1,e(s()).toBe(!0),e(o.position.toArray()).toEqual([0,.08,0])}),o("releases on swing and captures the next stance at its new position",()=>{const{anchor:o,animated:n,ground:a,update:i}=t();i(),e(i(!1)).toBe(!1),n.x=a.x=.12,e(i()).toBe(!0),e(o.position.x).toBeCloseTo(.12)}),o("excessive drift cannot repeatedly recapture within the same stance",()=>{const{anchor:o,animated:n,ground:a,update:i}=t();i(),n.x=a.x=.3,e(i()).toBe(!1),n.x=a.x=0,e(i()).toBe(!1),i(!1),e(i()).toBe(!0),e(o.locked).toBe(!0)}),o("releases before excessive turn, unreachable hip displacement or lost support",()=>{const o=t();o.update(),e(o.update(!0,!0,new s(1,0,0))).toBe(!1);const n=t();n.update(),n.hip.z=1,e(n.update()).toBe(!1);const a=t();a.update(),e(a.update(!0,!1)).toBe(!1)}),o("permits only the configured pelvis drop and resets history after teleport",()=>{const{anchor:o,hip:n,update:a}=t();n.y=1.05,e(a()).toBe(!0),n.y=1.2,e(a()).toBe(!1),o.reset(),n.y=.9,e(a()).toBe(!0)})}),t("isolated clip contact generation",()=>{function t(t=!1){const e=new i,o=new a;o.name="hips";const s=new a;s.name="left";const p=new a;p.name="right",e.add(o),o.add(s,p),t?(e.rotation.x=-Math.PI/2,e.scale.setScalar(.01),o.position.z=100,s.position.z=p.position.z=-90):(o.position.y=1,s.position.y=p.position.y=-.9);const c=[0,.25,.4,.6,.75,1],l=e=>e.flatMap(e=>t?[0,0,100*e]:[0,e,0]);return{root:e,hips:o,left:s,right:p,clip:new n("walk",1,[new r("left.position",c,l([-.9,-.9,-.65,-.65,-.9,-.9])),new r("right.position",c,l([-.65,-.65,-.9,-.9,-.65,-.65]))])}}o("finds alternating stance intervals and merges a contact across phase zero",()=>{const{root:o,clip:n}=t(),a=new h(o,"left","right").bake(n);e(l(a.left,.1,1,!0)).toBe(!0),e(l(a.left,.5,1,!0)).toBe(!1),e(l(a.right,.1,1,!0)).toBe(!1),e(l(a.right,.5,1,!0)).toBe(!0),e(a.left).toHaveLength(1),e(a.left[0].start).toBeGreaterThan(a.left[0].end)}),o("honors imported model scale/up axes without modifying the live pose",()=>{const{root:o,hips:n,left:a,right:i,clip:s}=t(!0),r=[n.position.clone(),a.position.clone(),i.position.clone()],p=new h(o,"left","right").bake(s);e(l(p.left,.1,1,!0)).toBe(!0),e(l(p.left,.5,1,!0)).toBe(!1),e(n.position.equals(r[0])).toBe(!0),e(a.position.equals(r[1])).toBe(!0),e(i.position.equals(r[2])).toBe(!0)}),o("generates full-cycle idle contacts and skips unsupported rigs",()=>{const{root:o}=t(),a=new n("idle",1,[]),i=new h(o,"left","right");e(i.bake(a)).toEqual({left:[{start:0,end:1}],right:[{start:0,end:1}]}),e(new h(o,"missing","right").bake(a)).toBeUndefined()}),o("preserves overrides through root-motion clone and samples restored root travel separately",()=>{const{root:o,hips:a,clip:i}=t(),s=new r("hips.position",[0,1],[0,1,0,0,1,2]),p=new n(i.name,1,[...i.tracks,s]),f=m.fromClip(p,!0,a,o),u=new h(o,"left","right").bake(f,{rootMotionTrack:f.motionTrack});d(f,u),e(c(f.clone())).toBe(u),e(l(u.left,.1,1,!0)).toBe(!0),e(a.position.toArray()).toEqual([0,1,0])})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as a}from"vitest";import{Color as r,Matrix3 as o,Matrix4 as n,Vector2 as l,Vector3 as i,Vector4 as u}from"three";import{GpuMaterialUniformData as p,materialUniformLayout as v}from"../effects/vfx/gpu-material-uniforms.js";e("GPU VFX uniform packing",()=>{a("packs WGSL alignment, integer bits, vectors and padded matrices",()=>{const e={scalar:{type:"f32",value:.5},pair:{type:"vec2<f32>",value:new l(2,3)},color:{type:"vec3<f32>",value:new r(.25,.5,.75)},signed:{type:"i32",value:-7},unsigned:{type:"u32",value:4294967295},triple:{type:"vec3<f32>",value:new i(4,5,6)},quad:{type:"vec4<f32>",value:new u(7,8,9,10)},matrix3:{type:"mat3x3<f32>",value:(new o).fromArray([1,2,3,4,5,6,7,8,9])},matrix4:{type:"mat4x4<f32>",value:new n},array:{type:"vec4<f32>",value:[11,12,13,14]},flag:{type:"u32",value:!0}},a=new p(e),c=v(e),f=e=>c.entries.find(t=>t.name===e).offset,s=new Float32Array(a.data),d=new DataView(a.data);t(f("pair")).toBe(8),t(f("color")).toBe(16),t(d.getInt32(f("signed"),!0)).toBe(-7),t(d.getUint32(f("unsigned"),!0)).toBe(4294967295),t(d.getUint32(f("flag"),!0)).toBe(1);for(const[e,a]of Object.entries({pair:[2,3],color:[.25,.5,.75],triple:[4,5,6],quad:[7,8,9,10],array:[11,12,13,14],matrix3:[1,2,3,0,4,5,6,0,7,8,9,0],matrix4:(new n).elements}))t(Array.from(s.subarray(f(e)/4,f(e)/4+a.length))).toEqual(a);t(a.update(e)).toBe(!1)}),a("reuses storage while detecting in-place changes and replacement live values",()=>{const e={vector:{type:"vec4<f32>",value:[1,2,3,4]},scalar:{type:"f32",value:.1}},a=new p(e),r=a.data;t(a.update(e)).toBe(!1),e.vector.value[0]=5,t(a.update(e)).toBe(!0),t(a.update(e)).toBe(!1),e.vector={type:"vec4<f32>",value:[6,7]},t(a.update(e)).toBe(!0),t(Array.from(new Float32Array(a.data,0,4))).toEqual([6,7,0,0]),t(a.data).toBe(r);const o=new p(e);t(o.data).not.toBe(r),t(o.update(e)).toBe(!1)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{afterEach as e,describe as t,expect as i,it as r,vi as n}from"vitest";import{Texture as o,Vector3 as p}from"three";import{compileGpuSpriteEmitters as l}from"../effects/vfx/gpu-sprite-compiler.js";import{GpuSpriteRuntime as s}from"../effects/vfx/gpu-sprite-runtime.js";e(()=>n.unstubAllGlobals()),t("GPU sprite pipeline sharing",()=>{r("shares pipelines across classes and worlds while keeping their bindings independent",()=>{n.stubGlobal("GPUBufferUsage",{STORAGE:1,COPY_DST:2,UNIFORM:4,INDIRECT:8,COPY_SRC:16,MAP_READ:32}),n.stubGlobal("GPUShaderStage",{COMPUTE:1,VERTEX:2,FRAGMENT:4});const e=()=>({createBuffer:()=>({destroy:n.fn()}),createShaderModule:n.fn(()=>({})),createBindGroupLayout:n.fn(()=>({})),createPipelineLayout:()=>({}),createComputePipeline:()=>({}),createRenderPipeline:n.fn(e=>({...e})),createBindGroup:n.fn(e=>({...e})),queue:{writeBuffer:n.fn()}}),t=new o,r=new o,a=new Map([t,r].map(e=>[e,{sampler:{},view:{}}])),d=l({id:"sprite",name:"sprite",type:"vfx",vfx:{emitters:[{id:"emitter",name:"emitter",position:[0,0,0],rotation:[0,0,0],rate:{type:"once",count:1,delay:0},initializers:[],behaviours:[],output:{type:"sprite",texture:"dot",color:"#fff",intensity:1,blendingMode:"additive",space:"world"}}]}},[new p]).emitters,u=e(),c={device:u,colorFormat:"rgba16float",depthView:{},getExternalTexture:e=>a.get(e)},f=[],m=(e=c)=>{const i=new s(d,[{texture:t},{texture:r},{texture:t,localSpace:!0}]).createResource(e);return f.push(i),i.renderResources};try{const n=m();i(u.createRenderPipeline).toHaveBeenCalledTimes(2),i(n[0].pipeline).toBe(n[1].pipeline),i(n[2].pipeline).not.toBe(n[0].pipeline),i(n[0].renderClassBuffer).not.toBe(n[1].renderClassBuffer);const o=n[0].getBindGroup(c),p=n[1].getBindGroup(c);i(o).not.toBe(p),i(o.layout).toBe(p.layout),i(o.entries.some(e=>e.resource===a.get(t).view)).toBe(!0),i(p.entries.some(e=>e.resource===a.get(r).view)).toBe(!0);const l=m();i(u.createRenderPipeline).toHaveBeenCalledTimes(2),i(l[0].pipeline).toBe(n[0].pipeline),i(l[0].renderClassBuffer).not.toBe(n[0].renderClassBuffer);const s=m({...c,colorFormat:"rgba8unorm"});i(u.createRenderPipeline).toHaveBeenCalledTimes(4),i(s[0].pipeline).not.toBe(n[0].pipeline),i(s[0].pipeline.vertex.module).toBe(n[0].pipeline.vertex.module),m({...c,depthView:null}),i(u.createRenderPipeline).toHaveBeenCalledTimes(6);const d=e(),f=m({...c,device:d});i(d.createRenderPipeline).toHaveBeenCalledTimes(2),i(f[0].pipeline).not.toBe(n[0].pipeline)}finally{for(const e of f)e.dispose()}})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
package/dist/test/input.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{expect as e,test as
|
|
1
|
+
import{expect as e,test as a,vi as t}from"vitest";import s from"typedi";import{ActionInput as n,ButtonInput as o,ToggleInput as d}from"../gameplay/input/input.js";t.mock("../gameplay/services/render.js",()=>({ViewController:class{}}));import{InputService as p}from"../gameplay/input/input-service.js";import{GamepadButtonBind as l,Keybind as u}from"../gameplay/input/keybind.js";import{ViewController as i}from"../gameplay/services/render.js";function r(){const e=Object.create(p.prototype);return Object.assign(e,{view:{paused:!1},inputs:new Map,deltaCallbacks:new Map,toggleCallbacks:new Map,keybinds:new Map,gamepadButtonBinds:new Map,gamepadAxisBinds:new Map,activeToggleCallbacks:[],prevGamepadButtonStates:new Map,suppressedGamepadButtons:new Map,sampledGamepadButtonStates:new Map,pendingGamepadButtonTransitions:new Map,frameGamepadButtonTransitions:new Map,lastActiveDevice:"kbm",onDeviceChanged:{next:()=>{}}}),e}a("button input uses hold mode by default",()=>{const a=new o,t=[];a.onStart(()=>t.push(a.activated)),a.onEnd(()=>t.push(a.activated)),a.toggle(!0),e(a.activated).toBe(!0),a.toggle(!0),e(t).toEqual([!0]),a.toggle(!1),e(a.activated).toBe(!1),e(t).toEqual([!0,!1])}),a("removed start listeners do not run on subsequent presses",()=>{const a=new o,s=t.fn(),n=t.fn();a.onStart(s),a.onStart(n),a.removeOnStart(s),a.removeOnStart(s),a.toggle(!0),e(s).not.toHaveBeenCalled(),e(n).toHaveBeenCalledTimes(1)}),a("button input can switch to toggle mode at runtime",()=>{const a=new o,t=[];a.onStart(()=>t.push(a.activated)),a.onEnd(()=>t.push(a.activated)),a.mode="toggle",a.toggle(!0),e(a.activated).toBe(!1),a.toggle(!1),e(a.activated).toBe(!0),a.toggle(!1),e(a.activated).toBe(!1),e(t).toEqual([!0,!1])}),a("action and toggle inputs remain compatibility presets",()=>{const a=new n;a.toggle(!0),e(a.activated).toBe(!0),a.toggle(!1),e(a.activated).toBe(!1);const t=new d;t.toggle(!0),e(t.activated).toBe(!1),t.toggle(!1),e(t.activated).toBe(!0)}),a("held gamepad buttons can be suppressed until released",()=>{const a={pressed:!0,value:1},t={connected:!0,mapping:"standard",buttons:[a],axes:[]},s=r(),n=[];s.gamepadButtonBinds.set("jump",[new l(0)]),s.toggleCallbacks.set("jump",e=>n.push(e)),Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[t]}),s.suppressHeldGamepadButtonsUntilRelease(),s.update(0),a.pressed=!1,a.value=0,s.update(0),a.pressed=!0,a.value=1,s.update(0),e(n).toEqual([!0])}),a("mouse button gameplay input requires pointer lock",()=>{let a=null;Object.defineProperty(document,"pointerLockElement",{configurable:!0,get:()=>a}),s.set(i,{paused:!1,htmlElement:document.body});const t=new p,n=[];t.keybinds.set("attack",[new u("MouseLeft")]),t.toggleCallbacks.set("attack",e=>n.push(e)),t.onKeyDown({button:0}),a=document.body,t.onKeyDown({button:0}),e(n).toEqual([!0])}),a("suppression consumes the release for all actions sharing a gamepad button",()=>{const a={pressed:!0,value:1},s={mapping:"standard",buttons:[a],axes:[]},n=r(),o=t.fn(),p=new d;n.setGamepadButtonBind("jump",new l(0)),n.setGamepadButtonBind("crouch",new l(0)),n.bindToggle("jump",o),n.bindToggle("crouch",p.toggle),Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[s]}),n.suppressHeldGamepadButtonsUntilRelease(),n.update(0),Object.assign(a,{pressed:!1,value:0}),n.update(0),e(o).not.toHaveBeenCalled(),e(p.activated).toBe(!1),Object.assign(a,{pressed:!0,value:1}),n.update(0),Object.assign(a,{pressed:!1,value:0}),n.update(0),e(o.mock.calls).toEqual([[!0],[!1]]),e(p.activated).toBe(!0)}),a("suppression clears when an unbound gamepad button is released",()=>{const a={pressed:!0,value:1},s={mapping:"standard",buttons:[a],axes:[]},n=r(),o=t.fn();Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[s]}),n.suppressHeldGamepadButtonsUntilRelease(),Object.assign(a,{pressed:!1,value:0}),n.update(0),n.setGamepadButtonBind("jump",new l(0)),n.bindToggle("jump",o),Object.assign(a,{pressed:!0,value:1}),n.update(0),e(o).toHaveBeenCalledWith(!0)}),a("a jump tap between rendered frames remains active for one gameplay update",()=>{const a={pressed:!1,value:0},s={mapping:"standard",buttons:[a],axes:[]},o=r(),d=new n,p=t.fn();d.onStart(p),o.bindToggle("jump",d.toggle),o.setGamepadButtonBind("jump",new l(0)),Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[s]}),o.update(1/30),Object.assign(a,{pressed:!0,value:1}),o.sampleGamepadButtons(!1),Object.assign(a,{pressed:!1,value:0}),o.sampleGamepadButtons(!1),o.update(1/30),e(p).toHaveBeenCalledOnce(),e(d.activated).toBe(!0),o.update(1/30),e(d.activated).toBe(!1)}),a("an attack release and repress between rendered frames emits another start",()=>{const a={pressed:!0,value:1},s={mapping:"standard",buttons:[a],axes:[]},o=r(),d=new n,p=t.fn();d.onStart(p),o.bindToggle("attack",d.toggle),o.setGamepadButtonBind("attack",new l(0)),Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[s]}),o.update(1/30),Object.assign(a,{pressed:!1,value:0}),o.sampleGamepadButtons(!1),Object.assign(a,{pressed:!0,value:1}),o.sampleGamepadButtons(!1),o.update(1/30),e(p).toHaveBeenCalledTimes(2),e(d.activated).toBe(!0)}),a("paused sampling discards taps and releases an action that was already held",()=>{const a={pressed:!0,value:1},s={mapping:"standard",buttons:[a],axes:[]},o=r(),d=new n,p=t.fn();d.onStart(p),o.bindToggle("attack",d.toggle),o.setGamepadButtonBind("attack",new l(0)),Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[s]}),o.update(0),o.sampleGamepadButtons(!0),o.update(0),e(d.activated).toBe(!1),e(p).toHaveBeenCalledOnce(),Object.assign(a,{pressed:!1,value:0}),o.sampleGamepadButtons(!0),Object.assign(a,{pressed:!0,value:1}),o.sampleGamepadButtons(!0),Object.assign(a,{pressed:!1,value:0}),o.sampleGamepadButtons(!0),o.update(0),e(p).toHaveBeenCalledOnce(),Object.assign(a,{pressed:!0,value:1}),o.update(0),e(p).toHaveBeenCalledTimes(2)}),a("modal suppression discards buffered taps and clears on a sampled release",()=>{const a={pressed:!0,value:1},s={mapping:"standard",buttons:[a],axes:[]},n=r(),o=t.fn();n.bindToggle("jump",o),n.setGamepadButtonBind("jump",new l(0)),Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[s]}),n.sampleGamepadButtons(!1),Object.assign(a,{pressed:!1,value:0}),n.sampleGamepadButtons(!1),n.suppressHeldGamepadButtonsUntilRelease(),n.update(0),e(o).not.toHaveBeenCalled(),Object.assign(a,{pressed:!0,value:1}),n.suppressHeldGamepadButtonsUntilRelease(),Object.assign(a,{pressed:!1,value:0}),n.sampleGamepadButtons(!1),Object.assign(a,{pressed:!0,value:1}),n.sampleGamepadButtons(!1),n.update(0),e(o.mock.calls).toEqual([[!0]])}),a("a callback can suppress other buffered buttons without leaving stale delivered states",()=>{const a=[{pressed:!1,value:0},{pressed:!1,value:0}],s={mapping:"standard",buttons:a,axes:[]},n=r(),o=t.fn();n.bindToggle("modal",e=>{e&&n.suppressHeldGamepadButtonsUntilRelease()}),n.bindToggle("jump",o),n.setGamepadButtonBind("modal",new l(0)),n.setGamepadButtonBind("jump",new l(1)),Object.defineProperty(navigator,"getGamepads",{configurable:!0,value:()=>[s]}),n.update(0),Object.assign(a[0],{pressed:!0,value:1}),Object.assign(a[1],{pressed:!0,value:1}),n.sampleGamepadButtons(!1),Object.assign(a[1],{pressed:!1,value:0}),n.sampleGamepadButtons(!1),n.update(0),e(o).not.toHaveBeenCalled(),Object.assign(a[1],{pressed:!0,value:1}),n.update(0),e(o.mock.calls).toEqual([[!0]])});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{expect as e,test as s,vi as t}from"vitest";import{Group as a,Mesh as i,Vector3 as n}from"three";import{LandscapeStaticDrawSets as o}from"../scene/landscape/landscape-static-draw-sets.js";function r(){const e=new a,s=new a,n=new i;n.x=2,n.y=3,s.add(n);const r=t.fn(()=>{n.visible=!0}),l=t.fn(e=>(e.visible=!1,r)),d=new o({registerExperimentalStaticDrawSet:l},s,"terrain");return{scene:e,landscape:s,section:n,sets:d,dispose:r,registerExperimentalStaticDrawSet:l}}s("waits for attachment and captures the final ancestor transform using strict registration",()=>{const{scene:s,landscape:t,section:a,sets:i,registerExperimentalStaticDrawSet:o}=r();i.invalidate(a),i.flush(),e(o).not.toHaveBeenCalled(),s.position.set(10,0,0),t.position.set(20,0,0),a.position.set(5,0,0),s.add(t),i.flush(),e((new n).setFromMatrixPosition(a.matrixWorld).x).toBe(35),e(o).toHaveBeenCalledWith(a,"terrain-2,3",!1,"throw"),i.flush(),e(o).toHaveBeenCalledTimes(1)}),s("unloading disposes the GPU snapshot and reloading can register again",()=>{const{scene:s,landscape:t,section:a,sets:i,dispose:n,registerExperimentalStaticDrawSet:o}=r();s.add(t),i.invalidate(a),i.flush(),i.remove(a),t.remove(a),e(n).toHaveBeenCalledTimes(1),e(a.visible).toBe(!0),t.add(a),i.invalidate(a),i.flush(),e(o).toHaveBeenCalledTimes(2)}),s("invalidating a snapshot restores its source and coalesces rebuilds",()=>{const{scene:s,landscape:t,section:a,sets:i,dispose:n,registerExperimentalStaticDrawSet:o}=r();s.add(t),i.invalidate(a),i.flush(),i.invalidate(a),i.invalidate(a),e(n).toHaveBeenCalledTimes(1),e(a.visible).toBe(!0),i.flush(),e(o).toHaveBeenCalledTimes(2)}),s("does not snapshot detached sections or hidden ancestors",()=>{const{scene:s,landscape:t,section:a,sets:i,registerExperimentalStaticDrawSet:n}=r();s.add(t),s.visible=!1,i.invalidate(a),i.flush(),e(n).not.toHaveBeenCalled(),s.visible=!0,t.remove(a),i.flush(),e(n).not.toHaveBeenCalled()}),s("disposal releases snapshots and prevents late asynchronous work from registering",()=>{const{scene:s,landscape:t,section:a,sets:i,dispose:n,registerExperimentalStaticDrawSet:o}=r();s.add(t),i.invalidate(a),i.flush(),i.dispose(),i.dispose(),i.invalidate(a),i.flush(),e(n).toHaveBeenCalledTimes(1),e(o).toHaveBeenCalledTimes(1)});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,test as n}from"vitest";import{Bone as o,MathUtils as a,Object3D as s,Quaternion as l,Vector3 as r}from"three";import{LocomotionBodyPose as i,LocomotionBodyResponse as h}from"../gameplay/animation/locomotion-body.js";const c=new r,d=new r(0,0,1);function p(e){const t=new h,n=new r;t.update(1/e,c,n,d);for(let o=1;o<=e;o++)n.set(3*o/e,0,2*o/e),t.update(1/e,c,n,d);return t}e("locomotion body motion sampling",()=>{n("leans toward lateral acceleration and pitches into forward acceleration",()=>{const e=p(60);t(e.roll).toBeLessThan(0),t(e.pitch).toBeGreaterThan(0),t(e.acceleration.x).toBeCloseTo(3,3),t(e.acceleration.z).toBeCloseTo(2,3),t(e.yaw).toBe(0)}),n("produces comparable response at 30, 60 and 120 FPS",()=>{const e=p(30),n=p(120);t(Math.abs(e.roll-n.roll)).toBeLessThan(.001),t(Math.abs(e.pitch-n.pitch)).toBeLessThan(.001),t(e.acceleration.distanceTo(n.acceleration)).toBeLessThan(1e-10)}),n("facing changes without world acceleration do not create lean",()=>{const e=new h,n=new r(0,0,3),o=d.clone();e.update(1/60,c,n,o);for(let t=0;t<60;t++)o.set(Math.sin(t/60),0,Math.cos(t/60)),e.update(1/60,c,n,o);t(e.roll).toBe(0),t(e.pitch).toBe(0),t(e.yaw).toBeLessThan(0)}),n("unwraps facing across the -pi/pi boundary",()=>{const e=new h,n=new r(0,0,3),o=new r(Math.sin(Math.PI-.01),0,Math.cos(Math.PI-.01));e.update(1/60,c,n,o),o.set(Math.sin(.01-Math.PI),0,Math.cos(.01-Math.PI)),e.update(1/60,c,n,o),t(e.yaw).toBeLessThan(0),t(Math.abs(e.yaw)).toBeLessThan(.01)}),n("bounds abrupt reversals and settles after an instantaneous stop",()=>{const e=new h,n=new r(0,0,7);e.update(1/60,c,n,d);for(let o=0;o<30;o++)n.set(o%2==0?100:-100,0,o%2==0?100:-100),e.update(1/60,c,n,d),t(Math.abs(e.roll)).toBeLessThanOrEqual(e.maxLean),t(Math.abs(e.pitch)).toBeLessThanOrEqual(e.maxPitch);n.set(0,0,0);for(let t=0;t<180;t++)e.update(1/60,c,n,d);t(Math.abs(e.roll)).toBeLessThan(1e-6),t(Math.abs(e.pitch)).toBeLessThan(1e-6)}),n("holds during pause and resets on teleports, hitches and inactive states",()=>{const e=p(60),n=e.roll;e.update(0,c,new r(100,0,0),d),t(e.roll).toBe(n),e.update(1/60,new r(10,0,0),new r(3,0,2),d),t(e.roll).toBe(0),t(e.acceleration.lengthSq()).toBe(0),e.update(.5,c,new r,d),t(e.pitch).toBe(0),e.update(1/60,c,new r(3,0,2),d),t(e.roll).not.toBe(0),e.update(1/60,c,new r,d,!1),t(e.roll).toBe(0),e.update(1/60,c,new r(3,0,2),d),t(e.acceleration.lengthSq()).toBe(0)})}),e("locomotion pose ownership",()=>{n("uses presentation axes with rotated bind bones and an imported model basis",()=>{const e=new s,n=new s;n.rotation.x=-Math.PI/2,e.add(n);const a=new o;a.rotation.y=Math.PI/3,n.add(a);const h=p(60);h.maxPitch=0;const w=new r(3,0,0);h.reset(),h.update(1/60,c,new r,d),h.update(1/60,c,w,d);const u=a.getWorldQuaternion(new l),g=(new l).setFromAxisAngle(d,h.roll).multiply(u);new i(e,[{bone:a,leanWeight:1,twistWeight:0}]).apply(h),t(a.getWorldQuaternion(new l).angleTo(g)).toBeLessThan(1e-7)}),n("distributes lean along a hierarchy and counter-rotates the head",()=>{const e=new s,n=new o,a=new o,p=new o;e.add(n),n.add(a),a.add(p);const w=new h;w.update(1/60,c,new r,d),w.update(1/60,c,new r(3,0,0),d);new i(e,[{bone:p,leanWeight:-.35,twistWeight:-.35},{bone:a,leanWeight:.85,twistWeight:.75},{bone:n,leanWeight:.15,twistWeight:.25}]).apply(w);const u=(new l).setFromAxisAngle(d,w.roll),g=(new l).setFromAxisAngle(d,.65*w.roll);t(a.getWorldQuaternion(new l).angleTo(u)).toBeLessThan(1e-7),t(p.getWorldQuaternion(new l).angleTo(g)).toBeLessThan(1e-7)}),n("restores the animated pose without accumulating on untracked bones or touching translations",()=>{const e=new s,n=new o;e.add(n),n.rotation.x=a.degToRad(15),n.position.set(0,.5,0);const l=n.quaternion.clone(),h=new i(e,[{bone:n,leanWeight:1,twistWeight:1}]),c=p(60);h.apply(c);const d=n.quaternion.clone();for(let e=0;e<120;e++)h.restore(),h.apply(c);t(n.quaternion.angleTo(d)).toBeLessThan(1e-7),t(n.position.equals(new r(0,.5,0))).toBe(!0),h.apply(c,0),t(n.quaternion.angleTo(l)).toBeLessThan(1e-7)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import"reflect-metadata";import*as e from"@dimforge/rapier3d-simd-compat";import{afterEach as t,beforeAll as n,expect as o,test as s,vi as a}from"vitest";import{BatchedMesh as r,BoxGeometry as i,InstancedMesh as c,Matrix4 as d,Mesh as w,MeshBasicMaterial as l,Object3D as h,Vector3 as p}from"three";import{PhysicsSystem as m,RayTestResult as y}from"../gameplay/services/physics/physics-system.js";import{BoxCollisionShape as b}from"../scene/collision/collision-shape.js";a.hoisted(()=>{const e=new Proxy({},{get:(e,t)=>e[t]??a.fn(),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),a.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),a.mock("../gameplay/services/world.js",()=>({World:class{}})),n(async()=>{await e.init()});const f=[];function B(){const t=Object.create(m.prototype),n=new e.World({x:0,y:0,z:0});return f.push(n),Object.assign(t,{world:n,rapier:e,colliderObjects:new WeakMap,actorBodies:new Map,bodyActors:new Map,nonFixedBodies:new Set,_raytestDiff:new p,_raytestDirection:new p,invalidateStaticDebug:()=>{}}),{system:t,world:n}}t(()=>{for(const e of f.splice(0))e.free()}),s("ray hits retain the source object without resolving an actor, and clear it on misses",()=>{const{system:e,world:t}=B(),n=new w(new i,new l),s=new b(new p(1,1,1)),a=e.addShape(void 0,s,n);t.step();const r=new y;e.rayTest(new p(0,2,0),new p(0,-2,0),r,{resolveActor:!1}),o(r.hasHit).toBe(!0),o(r.object).toBe(n),o(r.actor).toBeUndefined(),t.removeCollider(a,!1),t.step(),e.rayTest(new p(0,2,0),new p(0,-2,0),r),o(r.hasHit).toBe(!1),o(r.object).toBeUndefined(),r.hasHit=!0,r.object=n,r.instanceId=3,r.batchId=4,e.rayTest(new p,new p,r),o(r.hasHit).toBe(!1),o(r.object).toBeUndefined(),o(r.instanceId).toBeUndefined(),o(r.batchId).toBeUndefined()}),s("instanced colliders return the rendered mesh and the hit instance index",()=>{const{system:e,world:t}=B(),n=new c(new i,new l,2);n.setMatrixAt(0,new d),n.setMatrixAt(1,(new d).makeTranslation(3,0,0)),e.createForInstancedMesh(n,[new b(new p(1,1,1))]),t.step();const s=e.rayTest(new p(3,2,0),new p(3,-2,0),new y);o(s.hasHit).toBe(!0),o(s.object).toBe(n),o(s.instanceId).toBe(1),o(s.batchId).toBeUndefined()}),s("standalone colliders without a scene object return no object",()=>{const{system:e,world:t}=B();e.addShape(void 0,new b(new p(1,1,1))),t.step();const n=new y;n.object=new h,e.rayTest(new p(0,2,0),new p(0,-2,0),n),o(n.hasHit).toBe(!0),o(n.object).toBeUndefined()}),s("batched colliders return the batch instance ID rather than the temporary physics object",()=>{const{system:e,world:t}=B(),n=new r(2,48,72,new l),s=n.addGeometry(new i),a=n.addInstance(s),c=n.addInstance(s);n.setMatrixAt(a,new d),n.setMatrixAt(c,(new d).makeTranslation(3,0,0)),n.userData.collisionShapes=[[new b(new p(1,1,1))],[new b(new p(1,1,1))]],e.createForInstancedMesh(n,[]),t.step();const w=e.rayTest(new p(3,2,0),new p(3,-2,0),new y);o(w.hasHit).toBe(!0),o(w.object).toBe(n),o(w.batchId).toBe(c),o(w.instanceId).toBeUndefined()});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as n,test as t}from"vitest";import{AnimationClip as o,Bone as a,Quaternion as i,QuaternionKeyframeTrack as r,Vector3 as s,VectorKeyframeTrack as u}from"three";import{PoseInertializer as p}from"../gameplay/animation/pose-inertialization.js";const l=new s(0,0,1);function c(e=.3){const n=new a;n.name="arm";const t={bone:n,duration:e,maxRotation:Math.PI,maxAngularSpeed:20,maxPosition:.05},i=new p([t]),s=new o("idle",1,[new r("arm.quaternion",[0,1],[0,0,0,1,0,0,0,1]),new u("arm.position",[0,1],[0,0,0,0,0,0])]);return{bone:n,profile:t,pose:i,target:s,sample:(e,t)=>{i.restore(),n.quaternion.setFromAxisAngle(l,e),i.update(t)}}}e("transition pose inertialization",()=>{t("preserves outgoing pose and angular velocity at the transition boundary",()=>{const{bone:e,pose:t,target:o,sample:a}=c();a(.3,1/60),a(.32,1/60);const i=e.quaternion.clone();n(t.transitionTo(o)).toBe(!0),e.quaternion.identity(),t.update(0),n(e.quaternion.angleTo(i)).toBeLessThan(1e-7),t.restore(),e.quaternion.identity(),t.update(1e-4);const r=2*Math.atan2(e.quaternion.z,e.quaternion.w);n((r-.32)/1e-4).toBeCloseTo(1.2,3)}),t("accounts for incoming motion rather than retaining it twice",()=>{const{bone:e,pose:t,sample:a}=c();a(.3,1/60),a(.32,1/60);const s=(new i).setFromAxisAngle(l,-.2),u=(new i).setFromAxisAngle(l,.4),p=new o("moving-target",1,[new r("arm.quaternion",[0,1],[...s.toArray(),...u.toArray()])]);t.transitionTo(p,0,2),e.quaternion.copy(s).slerp(u,2e-4),t.update(1e-4);const d=2*Math.atan2(e.quaternion.z,e.quaternion.w);n((d-.32)/1e-4).toBeCloseTo(1.2,3)}),t("maintains angular velocity when source and target rotate on different axes",()=>{const{bone:e,pose:t}=c(),a=(new i).setFromAxisAngle(new s(1,0,0),.5),u=(new i).setFromAxisAngle(l,.02);e.quaternion.copy(a),t.update(1/60),e.quaternion.premultiply(u),t.update(1/60);const p=e.quaternion.clone(),d=(new i).setFromAxisAngle(new s(0,1,0),-.3),m=(new i).setFromAxisAngle(new s(1,0,0),.4).multiply(d),g=new o("idle",1,[new r("arm.quaternion",[0,1],[...d.toArray(),...m.toArray()])]);t.transitionTo(g),e.quaternion.copy(d).slerp(m,1e-4),t.update(1e-4);const q=(new i).setFromAxisAngle(l,12e-5).multiply(p);n(e.quaternion.angleTo(q)).toBeLessThan(1e-6)}),t("settles exactly into idle and preserves translation within its cap",()=>{const{bone:e,pose:t,target:o}=c();e.position.y=.02,e.quaternion.setFromAxisAngle(l,.3),t.update(1/60),e.position.y=.025,t.update(1/60),t.transitionTo(o);for(let o=0;o<20;o++)t.restore(),e.position.set(0,0,0),e.quaternion.identity(),t.update(1/60),n(e.position.length()).toBeLessThanOrEqual(.05+1e-10);n(e.quaternion.angleTo(new i)).toBeLessThan(1e-7),n(e.position.length()).toBe(0),n(t.isTransitioning).toBe(!1)}),t("uses the shortest quaternion path and bounds large corrections",()=>{const{bone:e,profile:t,pose:a,sample:s}=c();t.maxRotation=.2,s(Math.PI-.03,1/60),s(Math.PI-.02,1/60);const u=(new i).setFromAxisAngle(l,.02-Math.PI),p=new o("idle",1,[new r("arm.quaternion",[0,1],[...u.toArray(),...u.toArray()])]);a.transitionTo(p),e.quaternion.copy(u),a.update(1/60),n(e.quaternion.angleTo(u)).toBeLessThan(.05);const d=new o("opposite",1,[new r("arm.quaternion",[0,1],[0,0,0,1,0,0,0,1])]);a.transitionTo(d),a.restore(),e.quaternion.identity(),a.update(1/60),n(e.quaternion.angleTo(new i)).toBeLessThanOrEqual(.2+1e-7)}),t("retargets an interrupted transition from its current output",()=>{const{bone:e,pose:t,target:a,sample:s}=c();s(.3,1/60),s(.32,1/60),t.transitionTo(a),e.quaternion.identity(),t.update(1/60);const u=e.quaternion.clone(),p=(new i).setFromAxisAngle(l,-.4),d=new o("restart",1,[new r("arm.quaternion",[0,1],[...p.toArray(),...p.toArray()])]);t.transitionTo(d),t.restore(),e.quaternion.copy(p),t.update(0),n(e.quaternion.angleTo(u)).toBeLessThan(1e-7)}),t("pause does not advance decay and reset retains the before-mixer cleanup",()=>{const{bone:e,pose:t,target:o,sample:a}=c();a(.3,1/60),a(.32,1/60),t.transitionTo(o),e.quaternion.identity(),t.update(1/60);const r=e.quaternion.clone();for(let n=0;n<10;n++)t.restore(),e.quaternion.identity(),t.update(0);n(e.quaternion.angleTo(r)).toBeLessThan(1e-7),t.reset(),t.restore(),n(e.quaternion.angleTo(new i)).toBeLessThan(1e-7),n(t.transitionTo(o)).toBe(!1)}),t("decay is independent of update frequency for constant target poses",()=>{const e=e=>{const{bone:n,pose:t,target:o,sample:a}=c();a(.3,1/60),a(.32,1/60),t.transitionTo(o);for(let o=0;o<e/10;o++)t.restore(),n.quaternion.identity(),t.update(1/e);return n.quaternion.clone()};n(e(30).angleTo(e(120))).toBeLessThan(1e-7)}),t("restores an existing translation correction when its profile is disabled",()=>{const{bone:e,profile:t,pose:o,target:a,sample:i}=c();e.position.y=.03,i(.3,1/60),i(.32,1/60),o.transitionTo(a),e.position.set(0,0,0),e.quaternion.identity(),o.update(1/60),n(e.position.y).toBeGreaterThan(0),t.maxPosition=0,o.reset(),o.restore(),n(e.position.length()).toBe(0)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{afterEach as e,describe as t,expect as n,it as a,vi as o}from"vitest";import{FrameGraphBuilder as r,GTAOEffect as s}from"@hology/webgpu-renderer";e(()=>o.unstubAllGlobals()),t("renderer loading preparation",()=>{a("compiles fullscreen pipelines without consuming frame uniforms or rendering",async()=>{let e;o.stubGlobal("GPUShaderStage",{FRAGMENT:1}),o.stubGlobal("GPUBufferUsage",{UNIFORM:1,COPY_DST:2});const t=new r({addCustomPass:t=>{e=t}}),a=o.fn(()=>new Float32Array(4));t.addFullscreenPass({name:"test",inputs:{color:"sceneColor"},outputs:"output",uniformBufferSize:16,fragmentShader:"",updateUniforms:a});const s={createShaderModule:()=>({}),createBindGroupLayout:()=>({}),createPipelineLayout:()=>({}),createRenderPipeline:o.fn(()=>({})),createRenderPipelineAsync:o.fn(async()=>({})),createBuffer:()=>({}),createBindGroup:()=>({}),queue:{writeBuffer:o.fn()}},i={beginRenderPass:o.fn(()=>({setPipeline(){},setBindGroup(){},draw(){},end(){}}))},l={device:s,encoder:i,getColorFormat:()=>"rgba16float",getColorView:()=>({}),linearClampSampler:{}};await e.compile(l),n(s.createRenderPipelineAsync).toHaveBeenCalledTimes(1),n(s.createRenderPipeline).not.toHaveBeenCalled(),n(a).not.toHaveBeenCalled(),n(i.beginRenderPass).not.toHaveBeenCalled(),e.execute(l),n(s.createRenderPipeline).not.toHaveBeenCalled(),n(a).toHaveBeenCalledTimes(1),n(i.beginRenderPass).toHaveBeenCalledTimes(1)}),a("keeps AO passes and history when equivalent quality settings are reapplied",()=>{const e=new s({directions:2,denoise:{samples:8},temporal:{historyWeight:.9}}),t=[],a={createColorTarget:e=>e,addFullscreenPass:e=>t.push(e)},o={depth:"depth",normal:"normal",velocity:"velocity"};e.addPasses(a,o);const r=t[0];n(e.consumeHistoryValid()).toBe(!1),e.setOptions({directions:2,denoise:{samples:8},temporal:{historyWeight:.9}}),e.addPasses(a,o),n(t[3]).toBe(r),n(e.consumeHistoryValid()).toBe(!0),e.setOptions({directions:3}),e.addPasses(a,o),n(t[6]).not.toBe(r),n(e.consumeHistoryValid()).toBe(!1)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{describe as e,expect as t,it as n,vi as a}from"vitest";import{RenderingHost as
|
|
1
|
+
import{describe as e,expect as t,it as n,vi as a}from"vitest";import{RenderingHost as o}from"../gameplay/rendering-host.js";function s(){let e;return{simulationTime:12,webGpuDevice:{label:"device"},loop:a.fn(e=>e(.25)),stop:a.fn(),stopAnimationLoop:a.fn(),suspendPresentation:a.fn(),activatePresentation:a.fn(),registerWebGpuPresentationExtension:a.fn(t=>(e=t,a.fn())),get extension(){return e}}}e("RenderingHost",()=>{n("installs the input sampler on each active view separately from gameplay frames",()=>{const e=s(),n=s(),i=new o(e,!1,()=>n),r=a.fn(),l=a.fn();i.start(r,l),t(e.loop).toHaveBeenCalledWith(t.any(Function),!1,l),i.activate(i.createView()),t(n.loop).toHaveBeenCalledWith(t.any(Function),!1,l)}),n("owns frame scheduling and preserves a caller-owned view on shutdown",()=>{const e=s(),n=new o(e,!1,()=>s()),i=a.fn();n.start(i),n.shutdown(),n.shutdown(),t(i).toHaveBeenCalledWith(.25),t(e.suspendPresentation).toHaveBeenCalledOnce(),t(e.stop).not.toHaveBeenCalled()}),n("disposes a host-owned view",()=>{const e=s();new o(e,!0,()=>s()).shutdown(),t(e.stop).toHaveBeenCalledOnce()}),n("creates and switches distinct scene views",()=>{const e=s(),n=s(),a=new o(e,!1,()=>n);a.start(()=>{});const i=a.createView();t(i).toBe(n),t(n.suspendPresentation).toHaveBeenCalledOnce(),a.activate(i),t(e.suspendPresentation).toHaveBeenCalledOnce(),t(n.activatePresentation).toHaveBeenCalledOnce(),a.activate(e),a.releaseView(i),t(n.stop).toHaveBeenCalledOnce()}),n("adds a persistent native-resolution presentation pass with frame timing",()=>{const e=s(),n=new o(e,!1,()=>s());n.start(()=>{});const i=a.fn();n.registerStage(i);const r={},l={device:{},encoder:{},targetView:r,targetFormat:"bgra8unorm",presentationWidth:1920,presentationHeight:1080};e.extension(l),t(i).toHaveBeenCalledWith(t.objectContaining({deltaTime:.25,elapsedTime:12,targetView:r,targetFormat:"bgra8unorm",presentationWidth:1920}))})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{beforeAll as e,describe as a,expect as t,it as o,vi as r}from"vitest";import{BoxGeometry as s,Color as l,Euler as i,Mesh as n,Object3D as m,ShaderMaterial as u,Vector3 as p}from"three";import{createMaterialSubTrack as c,createPropertyKeyframe as y,createPropertySubTrack as f,createSpawnTrack as v,materialOutlineVisibilityParameterName as d}from"../effects/sequence/sequence-data.js";import{canTrackAddSubTrack as b}from"../effects/sequence/sequence-definitions.js";import{evaluateCustomParamValueKeyframes as B}from"../effects/sequence/sequence-value-lane.js";import{SerializedParamType as g}from"../scene/model.js";import{shaderParameterUniformName as w}from"../shader/parameter.js";let N;function T(){return new N}r.mock("../gameplay/actors/builtin/index.js",()=>({builtInActors:{},default:{}})),r.mock("../gameplay/actors/index.js",()=>({CharacterAnimationComponent:class{},CharacterMovementComponent:class{}})),r.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),r.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),r.mock("../effects/vfx/vfx-service.js",()=>({VfxService:class{}})),r.mock("../gameplay/services/render.js",()=>({})),r.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,a)=>(a in e||(e[a]=r.fn()),e[a]),set:(e,a,t)=>(e[a]=t,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),e(async()=>{const e=await import("../effects/sequence/sequence-player.js");N=e.SequencePlayer},1e4),a("sequence property parameter tracks",()=>{o("allows multiple property sub-tracks on spawn tracks but rejects duplicate property paths",()=>{const e=v(),a=f({propertyPath:"settings.globalGain",propertyType:g.Vector3});t(b(e,"property",{propertyPath:"settings.globalGain"})).toBe(!0),e.subTracks.push(a),t(b(e,"property",{propertyPath:"settings.globalGain"})).toBe(!1),t(b(e,"property",{propertyPath:"settings.globalGamma"})).toBe(!0)}),o("creates material sub-tracks for uniform-backed parameters and rejects duplicates",()=>{const e=v(),a={materialAssetId:"mat-1",parameterName:"dissolveAmount",uniformName:w("dissolveAmount"),uniformType:g.FloatNode,valueType:g.Number,label:"Dissolve Amount",defaultValue:{type:g.Number,value:0}},o=c(a);t(o).toMatchObject({type:"material",target:"shaderUniform",materialMatchMode:"asset",materialAssetId:"mat-1",parameterName:"dissolveAmount",uniformName:"pu_dissolveAmount",uniformType:g.FloatNode,valueType:g.Number,label:"Dissolve Amount"}),t(b(e,"material",{material:a})).toBe(!0),e.subTracks.push(o),t(b(e,"material",{material:a})).toBe(!1),t(b(e,"material",{material:{...a,parameterName:"rimPower",uniformName:w("rimPower")}})).toBe(!0)}),o("creates material sub-tracks for outline visibility and rejects duplicate outline controls",()=>{const e=v(),a={target:"outlineVisibility",materialAssetId:"mat-1",parameterName:d,uniformName:d,uniformType:g.Boolean,valueType:g.Boolean,label:"Outline Visibility",defaultValue:{type:g.Boolean,value:!0}},o=c(a);t(o).toMatchObject({type:"material",target:"outlineVisibility",materialMatchMode:"asset",materialAssetId:"mat-1",parameterName:d,uniformName:d,uniformType:g.Boolean,valueType:g.Boolean,label:"Outline Visibility"}),t(b(e,"material",{material:a})).toBe(!0),e.subTracks.push(o),t(b(e,"material",{material:a})).toBe(!1),t(b(e,"material",{material:{...a,materialAssetId:"mat-2"}})).toBe(!0),t(b(e,"material",{material:{...a,materialMatchMode:"parameterName",materialAssetId:null}})).toBe(!1)}),o("allows name-matched material sub-tracks as an alternative to asset-specific material tracks",()=>{const e=v(),a={materialAssetId:"mat-1",parameterName:"hitGlow",uniformName:w("hitGlow"),uniformType:g.FloatNode,valueType:g.Number},o={...a,materialAssetId:null,materialMatchMode:"parameterName"},r=c(o);t(r).toMatchObject({type:"material",materialMatchMode:"parameterName",materialAssetId:null,parameterName:"hitGlow"}),t(b(e,"material",{material:o})).toBe(!0),e.subTracks.push(r),t(b(e,"material",{material:a})).toBe(!1),t(b(e,"material",{material:{...a,parameterName:"rimPower",uniformName:w("rimPower")}})).toBe(!0);const s=v();s.subTracks.push(c(a)),t(b(s,"material",{material:{...a,materialAssetId:"mat-2"}})).toBe(!0),t(b(s,"material",{material:o})).toBe(!1)}),o("interpolates number, vector, color, and Euler property keyframes",()=>{const e=B([y(0,{type:g.Number,value:0}),y(1,{type:g.Number,value:10})],.5);t(e?.value).toBeCloseTo(5);const a=B([y(0,{type:g.Vector3,value:[0,0,0]}),y(1,{type:g.Vector3,value:[2,4,6]})],.5,{min:0,max:3});t(a?.value).toEqual([1,2,3]);const o=B([y(0,{type:g.Color,value:"#000000"}),y(1,{type:g.Color,value:"#ffffff"})],.5);t(o?.value).toBe(`#${new l("#000000").lerp(new l("#ffffff"),.5).getHexString()}`);const r=B([y(0,{type:g.Euler,value:[0,0,0,"XYZ"]}),y(1,{type:g.Euler,value:[1,2,3,"XYZ"]})],.5);t(r?.value).toEqual([.5,1,1.5,"XYZ"])}),o("uses step evaluation for booleans and options-backed values",()=>{const e=B([y(0,{type:g.Boolean,value:!1}),y(1,{type:g.Boolean,value:!0})],.5,{stepOnly:!0});t(e?.value).toBe(!1);const a=B([y(0,{type:g.Number,value:1}),y(1,{type:g.Number,value:2})],.5,{stepOnly:!0});t(a?.value).toBe(1)}),o("applies nested property paths and restores an original undefined value",async()=>{const e={settings:{globalGain:void 0}},a=f({propertyPath:"settings.globalGain",propertyType:g.Vector3}),o=await T();o.applyResolvedPropertyValue(a,e,{type:g.Vector3,value:[1,2,3]}),t(e.settings.globalGain).toBeInstanceOf(p),t(e.settings.globalGain?.toArray()).toEqual([1,2,3]),o.stop(),t(Object.prototype.hasOwnProperty.call(e.settings,"globalGain")).toBe(!0),t(e.settings.globalGain).toBeUndefined()}),o("mutates existing mutable values and restores their original value",async()=>{const e=new p(9,8,7),a=new l("#112233"),o=new i(.1,.2,.3,"XYZ"),r={settings:{globalGain:e,colorTint:a,rotation:o}},s=await T();s.applyResolvedPropertyValue(f({propertyPath:"settings.globalGain",propertyType:g.Vector3}),r,{type:g.Vector3,value:[1,2,3]}),s.applyResolvedPropertyValue(f({propertyPath:"settings.colorTint",propertyType:g.Color}),r,{type:g.Color,value:"#ffffff"}),s.applyResolvedPropertyValue(f({propertyPath:"settings.rotation",propertyType:g.Euler}),r,{type:g.Euler,value:[1,2,3,"XYZ"]}),t(r.settings.globalGain).toBe(e),t(r.settings.globalGain.toArray()).toEqual([1,2,3]),t(r.settings.colorTint).toBe(a),t(r.settings.colorTint.getHexString()).toBe("ffffff"),t(r.settings.rotation).toBe(o),t(r.settings.rotation.toArray()).toEqual([1,2,3,"XYZ"]),s.stop(),t(r.settings.globalGain).toBe(e),t(r.settings.globalGain.toArray()).toEqual([9,8,7]),t(r.settings.colorTint).toBe(a),t(r.settings.colorTint.getHexString()).toBe("112233"),t(r.settings.rotation).toBe(o),t(r.settings.rotation.toArray()).toEqual([.1,.2,.3,"XYZ"])}),o("clones matched materials before animating uniforms and restores originals",async()=>{const e=w("dissolveAmount"),a=new u({uniforms:{[e]:{value:0}}});a.userData.assetId="mat-1";const o=new n(new s,a),r=c({materialAssetId:"mat-1",parameterName:"dissolveAmount",uniformName:e,uniformType:g.FloatNode,valueType:g.Number});r.keyframes=[y(0,{type:g.Number,value:0}),y(1,{type:g.Number,value:1})];const l=await T();l.evaluateParameterSubTrack(r,o,.5),t(o.material).not.toBe(a),t(a.uniforms[e].value).toBe(0),t(o.material.uniforms[e].value).toBeCloseTo(.5),l.stop(),t(o.material).toBe(a)}),o("clones outline materials before toggling outline visibility and restores originals",async()=>{const e=new u;e.userData.assetId="mat-1",e.userData.outlineParameters={thickness:.01,color:[0,0,0]};const a=new n(new s,e),o=c({target:"outlineVisibility",materialAssetId:"mat-1",parameterName:d,uniformName:d,uniformType:g.Boolean,valueType:g.Boolean}),r=await T();r.applyResolvedMaterialValue(o,a,{type:g.Boolean,value:!1}),t(a.material).not.toBe(e),t(e.userData.outlineParameters.visible).toBeUndefined(),t(a.material.userData.outlineParameters).not.toBe(e.userData.outlineParameters),t(a.material.userData.outlineParameters).toMatchObject({thickness:.01,visible:!1}),r.stop(),t(a.material).toBe(e),t(e.userData.outlineParameters.visible).toBeUndefined()}),o("can match outline visibility tracks by parameter name across outlined materials",async()=>{const e=new u;e.userData.assetId="mat-1",e.userData.outlineParameters={};const a=new u;a.userData.assetId="mat-2",a.userData.outlineParameters={};const o=new u;o.userData.assetId="mat-3";const r=new n(new s,e),l=new n(new s,a),i=new n(new s,o),p=new m;p.add(r,l,i);const y=c({target:"outlineVisibility",materialMatchMode:"parameterName",materialAssetId:null,parameterName:d,uniformName:d,uniformType:g.Boolean,valueType:g.Boolean}),f=await T();f.applyResolvedMaterialValue(y,p,{type:g.Boolean,value:!1}),t(r.material).not.toBe(e),t(l.material).not.toBe(a),t(i.material).toBe(o),t(r.material.userData.outlineParameters.visible).toBe(!1),t(l.material.userData.outlineParameters.visible).toBe(!1),f.stop(),t(r.material).toBe(e),t(l.material).toBe(a)}),o("can match material tracks by parameter name across different material assets",async()=>{const e=w("hitGlow"),a=new u({uniforms:{[e]:{value:0}}});a.userData.assetId="mat-1";const o=new u({uniforms:{[e]:{value:0}}});o.userData.assetId="mat-2";const r=new u({uniforms:{[w("otherGlow")]:{value:0}}});r.userData.assetId="mat-3";const l=new n(new s,a),i=new n(new s,o),p=new n(new s,r),y=new m;y.add(l,i,p);const f=c({materialMatchMode:"parameterName",parameterName:"hitGlow",uniformName:e,uniformType:g.FloatNode,valueType:g.Number}),v=await T();v.applyResolvedMaterialValue(f,y,{type:g.Number,value:.75}),t(l.material).not.toBe(a),t(i.material).not.toBe(o),t(p.material).toBe(r),t(a.uniforms[e].value).toBe(0),t(o.uniforms[e].value).toBe(0),t(l.material.uniforms[e].value).toBeCloseTo(.75),t(i.material.uniforms[e].value).toBeCloseTo(.75),v.stop(),t(l.material).toBe(a),t(i.material).toBe(o)}),o("interpolates string-backed number material keyframes before applying uniforms",async()=>{const e=w("dissolveAmount"),a=new u({uniforms:{[e]:{value:0}}});a.userData.assetId="mat-1";const o=new n(new s,a),r=c({materialAssetId:"mat-1",parameterName:"dissolveAmount",uniformName:e,uniformType:g.FloatNode,valueType:g.Number});r.keyframes=[y(0,{type:g.Number,value:"0"}),y(1,{type:g.Number,value:"1"})];(await T()).evaluateParameterSubTrack(r,o,.5),t(o.material.uniforms[e].value).toBeCloseTo(.5)}),o("applies color material keyframes to vec3 uniform storage",async()=>{const e=w("rimColor"),a=new u({uniforms:{[e]:{value:new p(0,0,0)}}});a.userData.assetId="mat-1";const o=new n(new s,a),r=c({materialAssetId:"mat-1",parameterName:"rimColor",uniformName:e,uniformType:g.RgbNode,valueType:g.Color});(await T()).applyResolvedMaterialValue(r,o,{type:g.Color,value:"#336699"});const i=o.material.uniforms[e].value,m=new l("#336699");t(i.x).toBeCloseTo(m.r),t(i.y).toBeCloseTo(m.g),t(i.z).toBeCloseTo(m.b)}),o("step-evaluates boolean material keyframes",async()=>{const e=w("enabled"),a=new u({uniforms:{[e]:{value:!1}}});a.userData.assetId="mat-1";const o=new n(new s,a),r=c({materialAssetId:"mat-1",parameterName:"enabled",uniformName:e,uniformType:g.BooleanNode,valueType:g.Boolean});r.keyframes=[y(0,{type:g.Boolean,value:!1}),y(1,{type:g.Boolean,value:!0})];(await T()).evaluateParameterSubTrack(r,o,.5),t(o.material.uniforms[e].value).toBe(!1)})});/*
|
|
1
|
+
import{beforeAll as e,describe as a,expect as t,it as o,vi as r}from"vitest";import{BoxGeometry as s,Color as l,Euler as i,Mesh as n,Object3D as m,ShaderMaterial as u,Vector3 as p}from"three";import{createMaterialSubTrack as c,createPropertyKeyframe as y,createPropertySubTrack as f,createSpawnTrack as v,materialOutlineVisibilityParameterName as d}from"../effects/sequence/sequence-data.js";import{canTrackAddSubTrack as b}from"../effects/sequence/sequence-definitions.js";import{evaluateCustomParamValueKeyframes as B}from"../effects/sequence/sequence-value-lane.js";import{SerializedParamType as g}from"../scene/model.js";import{shaderParameterUniformName as w}from"../shader/parameter.js";let N;function T(){return new N}r.mock("../gameplay/actors/builtin/index.js",()=>({builtInActors:{},default:{}})),r.mock("../gameplay/actors/index.js",()=>({CharacterAnimationComponent:class{},CharacterMovementComponent:class{}})),r.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),r.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),r.mock("../effects/vfx/vfx-service.js",()=>({VfxService:class{}})),r.mock("../gameplay/services/render.js",()=>({})),r.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,a)=>(a in e||(e[a]=r.fn()),e[a]),set:(e,a,t)=>(e[a]=t,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),e(async()=>{const e=await import("../effects/sequence/sequence-player.js");N=e.SequencePlayer},1e4),a("sequence property parameter tracks",()=>{o("allows multiple property sub-tracks on spawn tracks but rejects duplicate property paths",()=>{const e=v(),a=f({propertyPath:"settings.globalGain",propertyType:g.Vector3});t(b(e,"property",{propertyPath:"settings.globalGain"})).toBe(!0),e.subTracks.push(a),t(b(e,"property",{propertyPath:"settings.globalGain"})).toBe(!1),t(b(e,"property",{propertyPath:"settings.globalGamma"})).toBe(!0)}),o("creates material sub-tracks for uniform-backed parameters and rejects duplicates",()=>{const e=v(),a={materialAssetId:"mat-1",parameterName:"dissolveAmount",uniformName:w("dissolveAmount"),uniformType:g.FloatNode,valueType:g.Number,label:"Dissolve Amount",defaultValue:{type:g.Number,value:0}},o=c(a);t(o).toMatchObject({type:"material",target:"shaderUniform",materialMatchMode:"asset",materialAssetId:"mat-1",parameterName:"dissolveAmount",uniformName:"pu_dissolveAmount",uniformType:g.FloatNode,valueType:g.Number,label:"Dissolve Amount"}),t(b(e,"material",{material:a})).toBe(!0),e.subTracks.push(o),t(b(e,"material",{material:a})).toBe(!1),t(b(e,"material",{material:{...a,parameterName:"rimPower",uniformName:w("rimPower")}})).toBe(!0)}),o("creates material sub-tracks for outline visibility and rejects duplicate outline controls",()=>{const e=v(),a={target:"outlineVisibility",materialAssetId:"mat-1",parameterName:d,uniformName:d,uniformType:g.Boolean,valueType:g.Boolean,label:"Outline Visibility",defaultValue:{type:g.Boolean,value:!0}},o=c(a);t(o).toMatchObject({type:"material",target:"outlineVisibility",materialMatchMode:"asset",materialAssetId:"mat-1",parameterName:d,uniformName:d,uniformType:g.Boolean,valueType:g.Boolean,label:"Outline Visibility"}),t(b(e,"material",{material:a})).toBe(!0),e.subTracks.push(o),t(b(e,"material",{material:a})).toBe(!1),t(b(e,"material",{material:{...a,materialAssetId:"mat-2"}})).toBe(!0),t(b(e,"material",{material:{...a,materialMatchMode:"parameterName",materialAssetId:null}})).toBe(!1)}),o("allows name-matched material sub-tracks as an alternative to asset-specific material tracks",()=>{const e=v(),a={materialAssetId:"mat-1",parameterName:"hitGlow",uniformName:w("hitGlow"),uniformType:g.FloatNode,valueType:g.Number},o={...a,materialAssetId:null,materialMatchMode:"parameterName"},r=c(o);t(r).toMatchObject({type:"material",materialMatchMode:"parameterName",materialAssetId:null,parameterName:"hitGlow"}),t(b(e,"material",{material:o})).toBe(!0),e.subTracks.push(r),t(b(e,"material",{material:a})).toBe(!1),t(b(e,"material",{material:{...a,parameterName:"rimPower",uniformName:w("rimPower")}})).toBe(!0);const s=v();s.subTracks.push(c(a)),t(b(s,"material",{material:{...a,materialAssetId:"mat-2"}})).toBe(!0),t(b(s,"material",{material:o})).toBe(!1)}),o("interpolates number, vector, color, and Euler property keyframes",()=>{const e=B([y(0,{type:g.Number,value:0}),y(1,{type:g.Number,value:10})],.5);t(e?.value).toBeCloseTo(5);const a=B([y(0,{type:g.Vector3,value:[0,0,0]}),y(1,{type:g.Vector3,value:[2,4,6]})],.5,{min:0,max:3});t(a?.value).toEqual([1,2,3]);const o=B([y(0,{type:g.Color,value:"#000000"}),y(1,{type:g.Color,value:"#ffffff"})],.5);t(o?.value).toBe(`#${new l("#000000").lerp(new l("#ffffff"),.5).getHexString()}`);const r=B([y(0,{type:g.Euler,value:[0,0,0,"XYZ"]}),y(1,{type:g.Euler,value:[1,2,3,"XYZ"]})],.5);t(r?.value).toEqual([.5,1,1.5,"XYZ"])}),o("uses step evaluation for booleans and options-backed values",()=>{const e=B([y(0,{type:g.Boolean,value:!1}),y(1,{type:g.Boolean,value:!0})],.5,{stepOnly:!0});t(e?.value).toBe(!1);const a=B([y(0,{type:g.Number,value:1}),y(1,{type:g.Number,value:2})],.5,{stepOnly:!0});t(a?.value).toBe(1)}),o("applies nested property paths and restores an original undefined value",async()=>{const e={settings:{globalGain:void 0}},a=f({propertyPath:"settings.globalGain",propertyType:g.Vector3}),o=await T();o.applyResolvedPropertyValue(a,e,{type:g.Vector3,value:[1,2,3]}),t(e.settings.globalGain).toBeInstanceOf(p),t(e.settings.globalGain?.toArray()).toEqual([1,2,3]),o.stop(),t(Object.prototype.hasOwnProperty.call(e.settings,"globalGain")).toBe(!0),t(e.settings.globalGain).toBeUndefined()}),o("mutates existing mutable values and restores their original value",async()=>{const e=new p(9,8,7),a=new l("#112233"),o=new i(.1,.2,.3,"XYZ"),r={settings:{globalGain:e,colorTint:a,rotation:o}},s=await T();s.applyResolvedPropertyValue(f({propertyPath:"settings.globalGain",propertyType:g.Vector3}),r,{type:g.Vector3,value:[1,2,3]}),s.applyResolvedPropertyValue(f({propertyPath:"settings.colorTint",propertyType:g.Color}),r,{type:g.Color,value:"#ffffff"}),s.applyResolvedPropertyValue(f({propertyPath:"settings.rotation",propertyType:g.Euler}),r,{type:g.Euler,value:[1,2,3,"XYZ"]}),t(r.settings.globalGain).toBe(e),t(r.settings.globalGain.toArray()).toEqual([1,2,3]),t(r.settings.colorTint).toBe(a),t(r.settings.colorTint.getHexString()).toBe("ffffff"),t(r.settings.rotation).toBe(o),t(r.settings.rotation.toArray()).toEqual([1,2,3,"XYZ"]),s.stop(),t(r.settings.globalGain).toBe(e),t(r.settings.globalGain.toArray()).toEqual([9,8,7]),t(r.settings.colorTint).toBe(a),t(r.settings.colorTint.getHexString()).toBe("112233"),t(r.settings.rotation).toBe(o),t(r.settings.rotation.toArray()).toEqual([.1,.2,.3,"XYZ"])}),o("clones matched materials before animating uniforms and restores originals",async()=>{const e=w("dissolveAmount"),a=new u({uniforms:{[e]:{value:0}}});Object.assign(a,{uniformValuesAutoUpdate:!1,uniformValuesAutoVersion:0}),a.userData.assetId="mat-1";const o=new n(new s,a),r=c({materialAssetId:"mat-1",parameterName:"dissolveAmount",uniformName:e,uniformType:g.FloatNode,valueType:g.Number});r.keyframes=[y(0,{type:g.Number,value:0}),y(1,{type:g.Number,value:1})];const l=await T();l.evaluateParameterSubTrack(r,o,.5),t(o.material).not.toBe(a),t(a.uniforms[e].value).toBe(0),t(o.material.uniforms[e].value).toBeCloseTo(.5),t(o.material.uniformValuesAutoUpdate).toBe(!1),t(o.material.uniformValuesAutoVersion).toBe(1),l.stop(),t(o.material).toBe(a)}),o("clones outline materials before toggling outline visibility and restores originals",async()=>{const e=new u;e.userData.assetId="mat-1",e.userData.outlineParameters={thickness:.01,color:[0,0,0]};const a=new n(new s,e),o=c({target:"outlineVisibility",materialAssetId:"mat-1",parameterName:d,uniformName:d,uniformType:g.Boolean,valueType:g.Boolean}),r=await T();r.applyResolvedMaterialValue(o,a,{type:g.Boolean,value:!1}),t(a.material).not.toBe(e),t(e.userData.outlineParameters.visible).toBeUndefined(),t(a.material.userData.outlineParameters).not.toBe(e.userData.outlineParameters),t(a.material.userData.outlineParameters).toMatchObject({thickness:.01,visible:!1}),r.stop(),t(a.material).toBe(e),t(e.userData.outlineParameters.visible).toBeUndefined()}),o("can match outline visibility tracks by parameter name across outlined materials",async()=>{const e=new u;e.userData.assetId="mat-1",e.userData.outlineParameters={};const a=new u;a.userData.assetId="mat-2",a.userData.outlineParameters={};const o=new u;o.userData.assetId="mat-3";const r=new n(new s,e),l=new n(new s,a),i=new n(new s,o),p=new m;p.add(r,l,i);const y=c({target:"outlineVisibility",materialMatchMode:"parameterName",materialAssetId:null,parameterName:d,uniformName:d,uniformType:g.Boolean,valueType:g.Boolean}),f=await T();f.applyResolvedMaterialValue(y,p,{type:g.Boolean,value:!1}),t(r.material).not.toBe(e),t(l.material).not.toBe(a),t(i.material).toBe(o),t(r.material.userData.outlineParameters.visible).toBe(!1),t(l.material.userData.outlineParameters.visible).toBe(!1),f.stop(),t(r.material).toBe(e),t(l.material).toBe(a)}),o("can match material tracks by parameter name across different material assets",async()=>{const e=w("hitGlow"),a=new u({uniforms:{[e]:{value:0}}});a.userData.assetId="mat-1";const o=new u({uniforms:{[e]:{value:0}}});o.userData.assetId="mat-2";const r=new u({uniforms:{[w("otherGlow")]:{value:0}}});r.userData.assetId="mat-3";const l=new n(new s,a),i=new n(new s,o),p=new n(new s,r),y=new m;y.add(l,i,p);const f=c({materialMatchMode:"parameterName",parameterName:"hitGlow",uniformName:e,uniformType:g.FloatNode,valueType:g.Number}),v=await T();v.applyResolvedMaterialValue(f,y,{type:g.Number,value:.75}),t(l.material).not.toBe(a),t(i.material).not.toBe(o),t(p.material).toBe(r),t(a.uniforms[e].value).toBe(0),t(o.uniforms[e].value).toBe(0),t(l.material.uniforms[e].value).toBeCloseTo(.75),t(i.material.uniforms[e].value).toBeCloseTo(.75),v.stop(),t(l.material).toBe(a),t(i.material).toBe(o)}),o("interpolates string-backed number material keyframes before applying uniforms",async()=>{const e=w("dissolveAmount"),a=new u({uniforms:{[e]:{value:0}}});a.userData.assetId="mat-1";const o=new n(new s,a),r=c({materialAssetId:"mat-1",parameterName:"dissolveAmount",uniformName:e,uniformType:g.FloatNode,valueType:g.Number});r.keyframes=[y(0,{type:g.Number,value:"0"}),y(1,{type:g.Number,value:"1"})];(await T()).evaluateParameterSubTrack(r,o,.5),t(o.material.uniforms[e].value).toBeCloseTo(.5)}),o("applies color material keyframes to vec3 uniform storage",async()=>{const e=w("rimColor"),a=new u({uniforms:{[e]:{value:new p(0,0,0)}}});a.userData.assetId="mat-1";const o=new n(new s,a),r=c({materialAssetId:"mat-1",parameterName:"rimColor",uniformName:e,uniformType:g.RgbNode,valueType:g.Color});(await T()).applyResolvedMaterialValue(r,o,{type:g.Color,value:"#336699"});const i=o.material.uniforms[e].value,m=new l("#336699");t(i.x).toBeCloseTo(m.r),t(i.y).toBeCloseTo(m.g),t(i.z).toBeCloseTo(m.b)}),o("step-evaluates boolean material keyframes",async()=>{const e=w("enabled"),a=new u({uniforms:{[e]:{value:!1}}});a.userData.assetId="mat-1";const o=new n(new s,a),r=c({materialAssetId:"mat-1",parameterName:"enabled",uniformName:e,uniformType:g.BooleanNode,valueType:g.Boolean});r.keyframes=[y(0,{type:g.Boolean,value:!1}),y(1,{type:g.Boolean,value:!0})];(await T()).evaluateParameterSubTrack(r,o,.5),t(o.material.uniforms[e].value).toBe(!1)})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as e from"three";import{describe as s,expect as a,it as t,vi as n}from"vitest";import{createSequenceData as c,createVfxClipInstance as r,createVfxTrack as o}from"../effects/sequence/sequence-data.js";import{SequencePlayer as l}from"../effects/sequence/sequence-player.js";function i(){return{object:new e.Object3D,restart:n.fn(),play:n.fn(),pause:n.fn(),previewAt:n.fn(),clearPreview:n.fn(),stop:n.fn(),getParticleCount:n.fn(()=>0)}}async function p(){await Promise.resolve(),await Promise.resolve()}n.mock("../gameplay/actors/builtin/index.js",()=>({builtInActors:{},default:{}})),n.mock("../gameplay/actors/index.js",()=>({CharacterAnimationComponent:class{},CharacterMovementComponent:class{}})),n.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),n.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),n.mock("../effects/vfx/vfx-service.js",()=>({VfxService:class{}})),n.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,s)=>(s in e||(e[s]=("string"!=typeof s||!s.startsWith("is"))&&n.fn()),e[s]),set:(e,s,a)=>(e[s]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),s("sequence VFX tracks",()=>{t("preloads unique VFX assets when preparing a sequence",async()=>{const s=new l,t=n.fn(async e=>{});s.setWorld({scene:new e.Scene,preloadVisualEffects:t}),s.setActorFactory({}),s.setAssetLoader({getAsset:n.fn(async e=>({id:e,vfx:{emitters:[]}}))});const i=c(),p=o();p.clips.push(r("vfx-asset",0,1),r("vfx-asset",1,1),r("other-vfx-asset",2,1)),i.tracks.push(p),s.load(i),await s.prepare(),a(t).toHaveBeenCalledTimes(1),a([...t.mock.calls[0][0]]).toHaveLength(2)}),t("creates and retains gameplay VFX actors while preparing a sequence",async()=>{const s=i(),t=n.fn(async()=>s),d=new l;d.setWorld({scene:new e.Scene,preloadVisualEffects:n.fn(async()=>{})}),d.setActorFactory({}),d.setAssetLoader({getAsset:n.fn(async e=>({id:e,vfx:{emitters:[]}}))}),d.setVfxService({createFromAssetId:t});const f=c(),v=o();v.clips.push(r("vfx-asset",0,1)),f.tracks.push(v),d.load(f),await d.prepare(),a(t).toHaveBeenCalledTimes(1),a(s.play).not.toHaveBeenCalled(),d.play(),await p(),a(t).toHaveBeenCalledTimes(1)}),t("restarts an already-active VFX clip when seeking backwards within the clip",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=2;const f=o(),v=r("vfx-asset",0,1.5);f.clips.push(v),d.tracks.push(f),t.load(d),t.play(),await p(),t.seek(1),s.restart.mockClear(),s.play.mockClear(),t.seek(.25),a(s.restart).toHaveBeenCalledTimes(1),a(s.play).toHaveBeenCalledTimes(1)}),t("restarts active VFX clips when a looping sequence wraps to the start",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=1,d.loop=!0;const f=o(),v=r("vfx-asset",0,1);f.clips.push(v),d.tracks.push(f),t.load(d),t.play(),await p(),s.restart.mockClear(),s.play.mockClear(),t.update(1.1),a(s.restart).toHaveBeenCalledTimes(1),a(s.play).toHaveBeenCalledTimes(1)}),t("does not replay a VFX while its sequence clip remains active",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=1;const f=o();f.clips.push(r("vfx-asset",0,.8)),d.tracks.push(f),t.load(d),t.play(),await p(),a(s.play).toHaveBeenCalledTimes(1),t.update(.3),t.update(.3),a(s.play).toHaveBeenCalledTimes(1)}),t("clears VFX particles when seeking backwards before the clip starts",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=2;const f=o(),v=r("vfx-asset",.5,1);f.clips.push(v),d.tracks.push(f),t.load(d),t.play(),t.seek(.75),await p(),s.restart.mockClear(),s.stop.mockClear(),s.pause.mockClear(),t.seek(.25),a(s.restart).toHaveBeenCalledTimes(1),a(s.stop).toHaveBeenCalled(),a(s.pause).toHaveBeenCalledTimes(1)}),t("rebuilds VFX at the playhead when an editor preview seeks while paused",async()=>{const s=i(),t=n.fn(async()=>s),d=new l;d.setWorld({scene:new e.Scene}),d.setVfxService({createFromAssetId:t}),d.externalTimeControl=!0,d.inEditor=!0;const f=c();f.duration=2;const v=o();v.clips.push(r("vfx-asset",.5,1)),f.tracks.push(v),d.load(f),d.seek(.75),await p(),a(t).toHaveBeenCalledWith("vfx-asset",a.any(e.Scene),{backend:"cpu"}),a(s.previewAt).toHaveBeenCalledWith(.25,1,"finish",.25,a.any(Number)),a(s.play).not.toHaveBeenCalled()}),t("keeps gameplay sequence VFX on the configured world backend",async()=>{const s=i(),t=n.fn(async()=>s),d=new l;d.setWorld({scene:new e.Scene}),d.setVfxService({createFromAssetId:t});const f=c();f.duration=1;const v=o();v.clips.push(r("vfx-asset",0,1)),f.tracks.push(v),d.load(f),d.play(),await p(),a(t).toHaveBeenCalledWith("vfx-asset",a.any(e.Scene),void 0)}),t("clears an editor VFX preview when seeking before its clip",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)}),t.externalTimeControl=!0,t.inEditor=!0;const d=c();d.duration=2;const f=o();f.clips.push(r("vfx-asset",.5,1)),d.tracks.push(f),t.load(d),t.seek(.75),await p(),s.clearPreview.mockClear(),t.seek(.25),a(s.clearPreview).toHaveBeenCalledTimes(1)})});/*
|
|
1
|
+
import*as e from"three";import{describe as s,expect as a,it as t,vi as n}from"vitest";import{createSequenceData as c,createVfxClipInstance as r,createVfxTrack as o}from"../effects/sequence/sequence-data.js";import{SequencePlayer as l}from"../effects/sequence/sequence-player.js";function i(){return{object:new e.Object3D,restart:n.fn(),play:n.fn(),pause:n.fn(),prepareRendering:n.fn(async()=>{}),previewAt:n.fn(),clearPreview:n.fn(),stop:n.fn(),getParticleCount:n.fn(()=>0)}}async function p(){await Promise.resolve(),await Promise.resolve()}n.mock("../gameplay/actors/builtin/index.js",()=>({builtInActors:{},default:{}})),n.mock("../gameplay/actors/index.js",()=>({CharacterAnimationComponent:class{},CharacterMovementComponent:class{}})),n.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),n.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),n.mock("../effects/vfx/vfx-service.js",()=>({VfxService:class{}})),n.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,s)=>(s in e||(e[s]=("string"!=typeof s||!s.startsWith("is"))&&n.fn()),e[s]),set:(e,s,a)=>(e[s]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),s("sequence VFX tracks",()=>{t("preloads unique VFX assets when preparing a sequence",async()=>{const s=new l,t=n.fn(async e=>{});s.setWorld({scene:new e.Scene,preloadVisualEffects:t}),s.setActorFactory({}),s.setAssetLoader({getAsset:n.fn(async e=>({id:e,vfx:{emitters:[]}}))});const i=c(),p=o();p.clips.push(r("vfx-asset",0,1),r("vfx-asset",1,1),r("other-vfx-asset",2,1)),i.tracks.push(p),s.load(i),await s.prepare(),a(t).toHaveBeenCalledTimes(1),a([...t.mock.calls[0][0]]).toHaveLength(2)}),t("creates and retains gameplay VFX actors while preparing a sequence",async()=>{const s=i(),t=n.fn(async()=>s),d=new l;d.setWorld({scene:new e.Scene,preloadVisualEffects:n.fn(async()=>{})}),d.setActorFactory({}),d.setAssetLoader({getAsset:n.fn(async e=>({id:e,vfx:{emitters:[]}}))}),d.setVfxService({createFromAssetId:t});const f=c(),v=o();v.clips.push(r("vfx-asset",0,1)),f.tracks.push(v),d.load(f),await d.prepare(),a(t).toHaveBeenCalledTimes(1),a(s.prepareRendering).toHaveBeenCalledTimes(1),a(s.play).not.toHaveBeenCalled(),d.play(),await p(),a(t).toHaveBeenCalledTimes(1)}),t("restarts an already-active VFX clip when seeking backwards within the clip",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=2;const f=o(),v=r("vfx-asset",0,1.5);f.clips.push(v),d.tracks.push(f),t.load(d),t.play(),await p(),t.seek(1),s.restart.mockClear(),s.play.mockClear(),t.seek(.25),a(s.restart).toHaveBeenCalledTimes(1),a(s.play).toHaveBeenCalledTimes(1)}),t("restarts active VFX clips when a looping sequence wraps to the start",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=1,d.loop=!0;const f=o(),v=r("vfx-asset",0,1);f.clips.push(v),d.tracks.push(f),t.load(d),t.play(),await p(),s.restart.mockClear(),s.play.mockClear(),t.update(1.1),a(s.restart).toHaveBeenCalledTimes(1),a(s.play).toHaveBeenCalledTimes(1)}),t("does not replay a VFX while its sequence clip remains active",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=1;const f=o();f.clips.push(r("vfx-asset",0,.8)),d.tracks.push(f),t.load(d),t.play(),await p(),a(s.play).toHaveBeenCalledTimes(1),t.update(.3),t.update(.3),a(s.play).toHaveBeenCalledTimes(1)}),t("clears VFX particles when seeking backwards before the clip starts",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const d=c();d.duration=2;const f=o(),v=r("vfx-asset",.5,1);f.clips.push(v),d.tracks.push(f),t.load(d),t.play(),t.seek(.75),await p(),s.restart.mockClear(),s.stop.mockClear(),s.pause.mockClear(),t.seek(.25),a(s.restart).toHaveBeenCalledTimes(1),a(s.stop).toHaveBeenCalled(),a(s.pause).toHaveBeenCalledTimes(1)}),t("rebuilds VFX at the playhead when an editor preview seeks while paused",async()=>{const s=i(),t=n.fn(async()=>s),d=new l;d.setWorld({scene:new e.Scene}),d.setVfxService({createFromAssetId:t}),d.externalTimeControl=!0,d.inEditor=!0;const f=c();f.duration=2;const v=o();v.clips.push(r("vfx-asset",.5,1)),f.tracks.push(v),d.load(f),d.seek(.75),await p(),a(t).toHaveBeenCalledWith("vfx-asset",a.any(e.Scene),{backend:"cpu"}),a(s.previewAt).toHaveBeenCalledWith(.25,1,"finish",.25,a.any(Number)),a(s.play).not.toHaveBeenCalled()}),t("keeps gameplay sequence VFX on the configured world backend",async()=>{const s=i(),t=n.fn(async()=>s),d=new l;d.setWorld({scene:new e.Scene}),d.setVfxService({createFromAssetId:t});const f=c();f.duration=1;const v=o();v.clips.push(r("vfx-asset",0,1)),f.tracks.push(v),d.load(f),d.play(),await p(),a(t).toHaveBeenCalledWith("vfx-asset",a.any(e.Scene),void 0)}),t("clears an editor VFX preview when seeking before its clip",async()=>{const s=i(),t=new l;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)}),t.externalTimeControl=!0,t.inEditor=!0;const d=c();d.duration=2;const f=o();f.clips.push(r("vfx-asset",.5,1)),d.tracks.push(f),t.load(d),t.seek(.75),await p(),s.clearPreview.mockClear(),t.seek(.25),a(s.clearPreview).toHaveBeenCalledTimes(1)})});/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as o,vi as n}from"vitest";import*as r from"three";import{NodeShaderMaterial as s}from"three-shader-graph";import{RenderingView as i}from"../rendering.js";import{createVfxRenderPreparation as a}from"../effects/vfx/vfx-render-preparation.js";import{StretchedSprite as c}from"../effects/vfx/stretched-sprite.js";import{materializeVfx as l}from"../effects/vfx/vfx-materializer.js";n.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&n.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),e("VFX rendering preload",()=>{o("includes delayed and nested collision outputs without emitting any particles",async()=>{const e=(e,t=[])=>({id:e,name:e,position:[0,0,0],rotation:[0,0,0],rate:{type:"once",count:0,delay:100},initializers:[],behaviours:[],children:t,spawnEvent:"collision",output:{type:"sprite",texture:e,space:"world",color:"#ffffff",materialSource:"default"}}),o={type:"vfx",id:"test",vfx:{emitters:[e("root",[e("child",[e("grandchild")])])]}},s={getCamera:()=>new r.PerspectiveCamera},i=await l(o,new r.Object3D,{getTexture:n.fn(async()=>new r.Texture)},s,{},{},{},{});t(i.renderBodies).toHaveLength(3),t(i.system.getCount()).toBe(0);const c=a(i.renderBodies,s);t(c.scene.children).toHaveLength(3),t(i.system.getCount()).toBe(0),c.dispose(),i.dispose(),i.system.destroy()}),o("prepares mesh, sprite and stretched output variants without disposing source resources",()=>{const e=new s({});e.transparent=!0;const o=new r.PlaneGeometry,i=new r.Mesh(o,e),l=new c(new r.PlaneGeometry,new s({})),p=new r.Sprite(new r.SpriteMaterial),d=n.spyOn(o,"dispose"),m=n.spyOn(e,"dispose"),f=a([i,l,p],{getCamera:()=>new r.PerspectiveCamera}),u=[];f.scene.traverse(e=>{(e instanceof r.Mesh||e instanceof r.Sprite)&&u.push(e)}),t(u).toHaveLength(3);const v=u.find(e=>e instanceof r.InstancedMesh);t(v.count).toBe(1),t(v.material).toBe(e),t(v.geometry.getAttribute("particleData")).toBeDefined(),t(v.geometry).not.toBe(o),f.dispose(),t(d).not.toHaveBeenCalled(),t(m).not.toHaveBeenCalled(),t(f.scene.children).toHaveLength(0)}),o("waits for every concurrent compilation and recovers after a failed request",async()=>{let e;const o=new Promise(t=>{e=t}),s=n.fn().mockImplementationOnce(()=>o).mockRejectedValueOnce(new Error("compile failed")).mockResolvedValue(void 0),a=Object.create(i.prototype);a.compileTail=Promise.resolve(),a.compileSceneAsync=s;const c=[new r.Scene,new r.Scene,new r.Scene],l=c.map(e=>a.compileAsync(e)),p=Promise.allSettled(l);await Promise.resolve(),await Promise.resolve(),t(s).toHaveBeenCalledTimes(1),e();const d=await p;t(s.mock.calls.map(e=>e[0])).toEqual(c),t(d.map(e=>e.status)).toEqual(["fulfilled","rejected","fulfilled"]),t(a.compileInProgress).toBe(!1)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function defineSharedStruct(e,t){const r=new SharedStructType(Array.from(Object.keys(e)),t);return(...t)=>{const n=new r;let c=0;for(const[r,o]of Object.entries(e))n[r]=t.length>c?t[c]:o,c++;return n}}/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hology/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.266",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"terse": "node scripts/terse.js copy"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
|
+
"./scene/animation-binary": "./dist/scene/animation-binary.js",
|
|
18
19
|
".": "./dist/index.js",
|
|
19
20
|
"./rendering": "./dist/rendering.js",
|
|
20
21
|
"./rendering/node-material-webgpu-resolver": "./dist/rendering/node-material-webgpu-resolver.js",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"types": "./dist/index.d.ts",
|
|
69
70
|
"typesVersions": {
|
|
70
71
|
"*": {
|
|
72
|
+
"scene/animation-binary": ["./dist/scene/animation-binary.d.ts"],
|
|
71
73
|
"rendering": [
|
|
72
74
|
"./dist/rendering.d.ts"
|
|
73
75
|
],
|
|
@@ -244,4 +246,4 @@
|
|
|
244
246
|
"vitest": "^4.1.9",
|
|
245
247
|
"@hology/webgpu-renderer": "^0.1.12"
|
|
246
248
|
}
|
|
247
|
-
}
|
|
249
|
+
}
|