@hology/core 0.0.270 → 0.0.271

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.
Files changed (54) hide show
  1. package/dist/effects/sequence/sequence-player.js +1 -1
  2. package/dist/effects/vfx/gpu-vfx-world-service.d.ts +2 -0
  3. package/dist/effects/vfx/gpu-vfx-world-service.js +1 -1
  4. package/dist/effects/vfx/vfx-actor.js +1 -1
  5. package/dist/effects/vfx/vfx-render-preparation.d.ts +3 -0
  6. package/dist/effects/vfx/vfx-render-preparation.js +1 -1
  7. package/dist/gameplay/actors/builtin/components/spline-component.d.ts +7 -39
  8. package/dist/gameplay/actors/builtin/components/spline-component.js +1 -1
  9. package/dist/gameplay/actors/builtin/navmesh-actor.d.ts +16 -2
  10. package/dist/gameplay/actors/builtin/navmesh-actor.js +1 -1
  11. package/dist/gameplay/actors/builtin/water-spline-volume-actor.d.ts +2 -2
  12. package/dist/gameplay/actors/builtin/water-spline-volume-actor.js +1 -1
  13. package/dist/gameplay/ai/build-tile.js +1 -1
  14. package/dist/gameplay/ai/dynamic-tiled-navmesh.js +1 -1
  15. package/dist/gameplay/ai/dynamic-tiled-navmesh.worker.js +1 -1
  16. package/dist/gameplay/ai/navigation.d.ts +2 -0
  17. package/dist/gameplay/ai/navigation.js +1 -1
  18. package/dist/gameplay/services/physics/physics-system.d.ts +1 -1
  19. package/dist/gameplay/services/physics/physics-system.js +1 -1
  20. package/dist/gameplay/services/render.js +1 -1
  21. package/dist/navigation/bake.d.ts +68 -0
  22. package/dist/navigation/bake.js +4 -0
  23. package/dist/navigation/cache.d.ts +11 -0
  24. package/dist/navigation/cache.js +4 -0
  25. package/dist/navigation/spline-data.d.ts +64 -0
  26. package/dist/navigation/spline-data.js +4 -0
  27. package/dist/navigation/water-spline-geometry.d.ts +23 -0
  28. package/dist/navigation/water-spline-geometry.js +4 -0
  29. package/dist/scene/asset-resource-loader.d.ts +2 -2
  30. package/dist/scene/asset-resource-loader.js +1 -1
  31. package/dist/scene/bootstrap.js +1 -1
  32. package/dist/scene/collision/authored-colliders.d.ts +4 -0
  33. package/dist/scene/collision/authored-colliders.js +4 -0
  34. package/dist/scene/collision/rapier-shape.d.ts +7 -0
  35. package/dist/scene/collision/rapier-shape.js +4 -0
  36. package/dist/scene/materializer.d.ts +3 -0
  37. package/dist/scene/materializer.js +1 -1
  38. package/dist/scene/objects/shapes.js +1 -1
  39. package/dist/scene/runtime-backend-service.d.ts +1 -0
  40. package/dist/scene/runtime-bundled-backend-service.d.ts +1 -0
  41. package/dist/scene/runtime-bundled-backend-service.js +1 -1
  42. package/dist/test/authored-collision-rescale.test.js +1 -1
  43. package/dist/test/gpu-vfx-preload.test.d.ts +2 -0
  44. package/dist/test/gpu-vfx-preload.test.js +4 -0
  45. package/dist/test/materializer-prefetch.test.js +1 -1
  46. package/dist/test/navigation-bake-backend.test.d.ts +2 -0
  47. package/dist/test/navigation-bake-backend.test.js +4 -0
  48. package/dist/test/navigation-bake.test.d.ts +2 -0
  49. package/dist/test/navigation-bake.test.js +4 -0
  50. package/dist/test/sequence-vfx.test.js +1 -1
  51. package/dist/test/shader-graph.test.js +1 -1
  52. package/dist/test/vfx-render-preparation.test.js +1 -1
  53. package/package.json +6 -2
  54. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
- import{BoxCollisionShape as e,CapsuleCollisionShape as t,ConeCollisionShape as r,ConvexPolyhedronCollisionShape as s,CylinderCollisionShape as n,PlaneCollisionShape as i,SphereCollisionShape as o,TrimeshCollisionShape as a}from"../collision/collision-shape.js";import{BoxGeometry as m,PlaneGeometry as h,Vector3 as l,CylinderGeometry as c,SphereGeometry as g,Euler as p,TorusGeometry as u,ConeGeometry as d,CircleGeometry as w,CapsuleGeometry as y}from"three";import{rampGeometry as f}from"./ramp-geometry.js";import{RoundedBoxGeometry as S}from"three/examples/jsm/geometries/RoundedBoxGeometry.js";import{TextGeometry as x}from"three/examples/jsm/geometries/TextGeometry.js";import{Font as j}from"three/examples/jsm/loaders/FontLoader.js";import*as b from"three/examples/fonts/helvetiker_regular.typeface.json";import{createSpiralStairsGeometry as L,createStairsGeometry as z}from"./stairs-geometry.js";import*as B from"three/examples/jsm/utils/BufferGeometryUtils.js";function E(e){return{type:"number",default:e}}function G(e){return{type:"number",default:e,float:!0}}function k(e){return{type:"boolean",default:e}}function v(e,t){const r={};for(let s in t)null==e[s]?r[s]=t[s].default:r[s]=e[s];return r}const T={box:{geometry:()=>new m(1,1,1),collision:()=>new e(new l(1,1,1))},ramp:{geometry:()=>f(),collision:()=>new s(f())},plane:new class{constructor(){this.parameters={widthSegments:E(1),heightSegments:E(1)}}geometry(e={}){const t=v(e,this.parameters);return new h(1,1,t.widthSegments,t.heightSegments)}collision(e={}){return new i(1,1)}},cylinder:new class{constructor(){this.parameters={radialSegments:E(12),heightSegments:E(1),openEnded:k(!1)}}geometry(e={}){const t=v(e,this.parameters);return new c(.5,.5,1,t.radialSegments,t.heightSegments,t.openEnded)}collision(e={}){const t=v(e,this.parameters);return new n(.5,.5,1,t.radialSegments,new p(0,1,0))}},sphere:new class{constructor(){this.parameters={withSegments:E(12),heightSegments:E(12)}}geometry(e={}){const t=v(e,this.parameters);return new g(.5,t.withSegments,t.heightSegments)}collision(){return new o(.5)}},torus:new class{constructor(){this.parameters={radius:G(1),tube:G(.3),radialSegments:E(16),tubularSegments:E(50)}}geometry(e={}){const t=v(e,this.parameters);return new u(t.radius,t.tube,t.radialSegments,t.tubularSegments)}collision(e={}){return new a(this.geometry(e))}},cone:new class{constructor(){this.parameters={radialSegments:E(12),heightSegments:E(1),openEnded:k(!1)}}geometry(e={}){const t=v(e,this.parameters);return new d(.5,1,t.radialSegments,t.heightSegments)}collision(e={}){return new r(1,.5)}},circle:new class{constructor(){this.parameters={segments:E(32)}}geometry(e={}){const t=v(e,this.parameters);return new w(.5,t.segments)}collision(e={}){return new a(this.geometry(e))}},capsule:new class{constructor(){this.parameters={radius:G(.5),length:G(1),capSegments:E(8),radialSegments:E(32)}}geometry(e={}){const t=v(e,this.parameters);return new y(t.radius,t.length,t.capSegments,t.radialSegments)}collision(e={}){const r=v(e,this.parameters);return new t(r.length,r.radius)}},roundedBox:new class{constructor(){this.parameters={width:G(1),height:G(1),depth:G(1),segments:E(4),radius:G(.1)}}geometry(e={}){const t=v(e,this.parameters),r=new S(t.width,t.height,t.depth,t.segments,t.radius);return B.mergeVertices(r)}collision(t={}){const r=v(t,this.parameters);return new e(new l(r.width,r.height,r.depth))}},text:new class{constructor(){var e;this.parameters={text:(e="Text",{type:"string",default:e}),size:G(1),depth:G(.2)}}geometry(e={}){const t=v(e,this.parameters);return new x(t.text,{font:new j(b),size:t.size,height:t.depth})}collision(e={}){return v(e,this.parameters).text.trim().length,null}},stairs:new class{constructor(){this.parameters={floating:k(!1),width:G(1),height:G(.25),depth:G(.25),steps:E(4)}}geometry(e={}){const t=v(e,this.parameters);return z(t.floating,t.steps,t.depth,t.height,t.width)}collision(e={}){return new a(this.geometry(e))}},stairsSpiral:new class{constructor(){this.parameters={floating:k(!1),width:G(1),height:G(.25),steps:E(4),degrees:E(90),radius:G(.2),flipped:k(!1)}}geometry(e={}){const t=v(e,this.parameters);return L(t.floating,t.steps,t.height,t.width,t.degrees,t.radius,t.flipped)}collision(e={}){return new a(this.geometry(e))}}};export const ShapeLibrary=T;export const ShapeLibraryKeys=Object.keys(ShapeLibrary);/*
1
+ import{BoxCollisionShape as e,CapsuleCollisionShape as t,ConeCollisionShape as r,ConvexPolyhedronCollisionShape as s,CylinderCollisionShape as n,PlaneCollisionShape as i,SphereCollisionShape as o,TrimeshCollisionShape as a}from"../collision/collision-shape.js";import{BoxGeometry as m,PlaneGeometry as h,Vector3 as l,CylinderGeometry as c,SphereGeometry as g,Euler as p,TorusGeometry as u,ConeGeometry as d,CircleGeometry as w,CapsuleGeometry as y}from"three";import{rampGeometry as f}from"./ramp-geometry.js";import{RoundedBoxGeometry as S}from"three/examples/jsm/geometries/RoundedBoxGeometry.js";import{TextGeometry as x}from"three/examples/jsm/geometries/TextGeometry.js";import{Font as j}from"three/examples/jsm/loaders/FontLoader.js";import b from"three/examples/fonts/helvetiker_regular.typeface.json"with{type:"json"};import{createSpiralStairsGeometry as L,createStairsGeometry as z}from"./stairs-geometry.js";import*as B from"three/examples/jsm/utils/BufferGeometryUtils.js";function E(e){return{type:"number",default:e}}function G(e){return{type:"number",default:e,float:!0}}function k(e){return{type:"boolean",default:e}}function v(e,t){const r={};for(let s in t)null==e[s]?r[s]=t[s].default:r[s]=e[s];return r}const T={box:{geometry:()=>new m(1,1,1),collision:()=>new e(new l(1,1,1))},ramp:{geometry:()=>f(),collision:()=>new s(f())},plane:new class{constructor(){this.parameters={widthSegments:E(1),heightSegments:E(1)}}geometry(e={}){const t=v(e,this.parameters);return new h(1,1,t.widthSegments,t.heightSegments)}collision(e={}){return new i(1,1)}},cylinder:new class{constructor(){this.parameters={radialSegments:E(12),heightSegments:E(1),openEnded:k(!1)}}geometry(e={}){const t=v(e,this.parameters);return new c(.5,.5,1,t.radialSegments,t.heightSegments,t.openEnded)}collision(e={}){const t=v(e,this.parameters);return new n(.5,.5,1,t.radialSegments,new p(0,1,0))}},sphere:new class{constructor(){this.parameters={withSegments:E(12),heightSegments:E(12)}}geometry(e={}){const t=v(e,this.parameters);return new g(.5,t.withSegments,t.heightSegments)}collision(){return new o(.5)}},torus:new class{constructor(){this.parameters={radius:G(1),tube:G(.3),radialSegments:E(16),tubularSegments:E(50)}}geometry(e={}){const t=v(e,this.parameters);return new u(t.radius,t.tube,t.radialSegments,t.tubularSegments)}collision(e={}){return new a(this.geometry(e))}},cone:new class{constructor(){this.parameters={radialSegments:E(12),heightSegments:E(1),openEnded:k(!1)}}geometry(e={}){const t=v(e,this.parameters);return new d(.5,1,t.radialSegments,t.heightSegments)}collision(e={}){return new r(1,.5)}},circle:new class{constructor(){this.parameters={segments:E(32)}}geometry(e={}){const t=v(e,this.parameters);return new w(.5,t.segments)}collision(e={}){return new a(this.geometry(e))}},capsule:new class{constructor(){this.parameters={radius:G(.5),length:G(1),capSegments:E(8),radialSegments:E(32)}}geometry(e={}){const t=v(e,this.parameters);return new y(t.radius,t.length,t.capSegments,t.radialSegments)}collision(e={}){const r=v(e,this.parameters);return new t(r.length,r.radius)}},roundedBox:new class{constructor(){this.parameters={width:G(1),height:G(1),depth:G(1),segments:E(4),radius:G(.1)}}geometry(e={}){const t=v(e,this.parameters),r=new S(t.width,t.height,t.depth,t.segments,t.radius);return B.mergeVertices(r)}collision(t={}){const r=v(t,this.parameters);return new e(new l(r.width,r.height,r.depth))}},text:new class{constructor(){var e;this.parameters={text:(e="Text",{type:"string",default:e}),size:G(1),depth:G(.2)}}geometry(e={}){const t=v(e,this.parameters);return new x(t.text,{font:new j(b),size:t.size,height:t.depth})}collision(e={}){return v(e,this.parameters).text.trim().length,null}},stairs:new class{constructor(){this.parameters={floating:k(!1),width:G(1),height:G(.25),depth:G(.25),steps:E(4)}}geometry(e={}){const t=v(e,this.parameters);return z(t.floating,t.steps,t.depth,t.height,t.width)}collision(e={}){return new a(this.geometry(e))}},stairsSpiral:new class{constructor(){this.parameters={floating:k(!1),width:G(1),height:G(.25),steps:E(4),degrees:E(90),radius:G(.2),flipped:k(!1)}}geometry(e={}){const t=v(e,this.parameters);return L(t.floating,t.steps,t.height,t.width,t.degrees,t.radius,t.flipped)}collision(e={}){return new a(this.geometry(e))}}};export const ShapeLibrary=T;export const ShapeLibraryKeys=Object.keys(ShapeLibrary);/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,6 +1,7 @@
1
1
  import type { SceneObject } from './materializer.js';
2
2
  import type { Asset, SceneData } from './model.js';
3
3
  export interface RuntimeBackendService {
4
+ getSceneNavigationBakes?(sceneId: string): Promise<import('../navigation/bake.js').NavigationBakes>;
4
5
  getAssets(): Promise<Asset[]>;
5
6
  getAsset(id: string): Promise<Asset>;
6
7
  getScenes(): Promise<SceneData[]>;
@@ -3,6 +3,7 @@ import type { Asset, SceneData } from './model.js';
3
3
  import type { RuntimeBackendService } from './runtime-backend-service.js';
4
4
  export declare class RuntimeBundledBackendService implements RuntimeBackendService {
5
5
  private readonly dataBundleUrl;
6
+ getSceneNavigationBakes(sceneId: string): Promise<import('../navigation/bake.js').NavigationBakes>;
6
7
  private assets;
7
8
  private scenes;
8
9
  private sceneBlobs;
@@ -1,4 +1,4 @@
1
- import{decodeStorageValue as e,parseStorageJson as s}from"./storage/packed-json.js";export class RuntimeBundledBackendService{constructor(e=new URL("../hology_data.json",import.meta.url)){this.dataBundleUrl=e,this.assets=new Map,this.scenes=new Map,this.sceneBlobs=new Map}getAssets(){return Promise.resolve(Array.from(this.assets.values()))}getAsset(e){return Promise.resolve(this.assets.get(e))}getScenes(){return Promise.resolve(Array.from(this.scenes.values()))}getScene(e){return Promise.resolve(this.scenes.get(e))}async getSceneData(e){const t=this.sceneBlobs.get(e);return"string"==typeof t.data?s(t.data):t.data}static tryCreate(){}async preloadData(){const s=await(await fetch(this.dataBundleUrl)).json()??{};for(const t of s.asset??[]){const s=e(t);this.assets.set(s.id,s)}for(const e of s.scene??[])this.scenes.set(e.id,e),this.scenes.set(e.name,e);for(const t of s["scene-blob"]??[]){const s=e(t);this.sceneBlobs.set(s.id,s)}}}/*
1
+ import{decodeStorageValue as e,parseStorageJson as s}from"./storage/packed-json.js";export class RuntimeBundledBackendService{async getSceneNavigationBakes(e){const s=this.sceneBlobs.get(e)?.navigationBakes??{},t=String(this.dataBundleUrl);return Object.fromEntries(Object.entries(s).map(([e,s])=>{const n=this.dataBundleUrl instanceof URL||/^[a-z]+:/i.test(t)||globalThis.location?.href?new URL(s.file,new URL(t,globalThis.location?.href)).href:t.slice(0,t.lastIndexOf("/")+1)+s.file;return[e,{...s,file:n}]}))}constructor(e=new URL("../hology_data.json",import.meta.url)){this.dataBundleUrl=e,this.assets=new Map,this.scenes=new Map,this.sceneBlobs=new Map}getAssets(){return Promise.resolve(Array.from(this.assets.values()))}getAsset(e){return Promise.resolve(this.assets.get(e))}getScenes(){return Promise.resolve(Array.from(this.scenes.values()))}getScene(e){return Promise.resolve(this.scenes.get(e))}async getSceneData(e){const t=this.sceneBlobs.get(e);return"string"==typeof t.data?s(t.data):t.data}static tryCreate(){}async preloadData(){const s=await(await fetch(this.dataBundleUrl)).json()??{};for(const t of s.asset??[]){const s=e(t);this.assets.set(s.id,s)}for(const e of s.scene??[])this.scenes.set(e.id,e),this.scenes.set(e.name,e);for(const t of s["scene-blob"]??[]){const s=e(t);this.sceneBlobs.set(s.id,s)}}}/*
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{expect as e,test as o}from"vitest";import{Vector3 as s}from"three";import{createAuthoredCollisionShapes as t}from"../scene/asset-resource-loader.js";import{BoxCollisionShape as i,CapsuleCollisionShape as n,CylinderCollisionShape as l,SphereCollisionShape as r}from"../scene/collision/collision-shape.js";function a(o,s,t=1e-5){e(o.distanceTo(s)).toBeLessThan(t)}o("authored colliders are converted back to asset-local units when the asset is rescaled",()=>{const[o,d,c,p]=t([{id:"box",name:"Box Collider",type:"box",position:[.5,-1,2],size:[1,2,3]},{id:"sphere",name:"Sphere Collider",type:"sphere",position:[1,2,3],radius:.25},{id:"capsule",name:"Capsule Collider",type:"capsule",position:[-2,.5,4],radius:.2,length:1.5},{id:"cylinder",name:"Cylinder Collider",type:"cylinder",position:[3,-4,5],radius:.4,height:2.5}],.01);e(o).toBeInstanceOf(i),a(o.dimensions,new s(100,200,300)),a(o.offset,new s(50,-100,200)),e(d).toBeInstanceOf(r),e(d.radius).toBeCloseTo(25),a(d.offset,new s(100,200,300)),e(c).toBeInstanceOf(n),e(c.radius).toBeCloseTo(20),e(c.length).toBeCloseTo(150),a(c.offset,new s(-200,50,400)),e(p).toBeInstanceOf(l),e(p.radiusTop).toBeCloseTo(40),e(p.radiusBottom).toBeCloseTo(40),e(p.height).toBeCloseTo(250),a(p.offset,new s(300,-400,500))}),o("authored colliders keep their values when no asset rescale is applied",()=>{const[o]=t([{id:"box",name:"Box Collider",type:"box",position:[1,2,3],size:[4,5,6]}]);e(o).toBeInstanceOf(i),a(o.dimensions,new s(4,5,6)),a(o.offset,new s(1,2,3))});/*
1
+ import{expect as e,test as o}from"vitest";import{Vector3 as s}from"three";import{createAuthoredCollisionShapes as t}from"../scene/collision/authored-colliders.js";import{BoxCollisionShape as i,CapsuleCollisionShape as n,CylinderCollisionShape as l,SphereCollisionShape as r}from"../scene/collision/collision-shape.js";function a(o,s,t=1e-5){e(o.distanceTo(s)).toBeLessThan(t)}o("authored colliders are converted back to asset-local units when the asset is rescaled",()=>{const[o,d,c,p]=t([{id:"box",name:"Box Collider",type:"box",position:[.5,-1,2],size:[1,2,3]},{id:"sphere",name:"Sphere Collider",type:"sphere",position:[1,2,3],radius:.25},{id:"capsule",name:"Capsule Collider",type:"capsule",position:[-2,.5,4],radius:.2,length:1.5},{id:"cylinder",name:"Cylinder Collider",type:"cylinder",position:[3,-4,5],radius:.4,height:2.5}],.01);e(o).toBeInstanceOf(i),a(o.dimensions,new s(100,200,300)),a(o.offset,new s(50,-100,200)),e(d).toBeInstanceOf(r),e(d.radius).toBeCloseTo(25),a(d.offset,new s(100,200,300)),e(c).toBeInstanceOf(n),e(c.radius).toBeCloseTo(20),e(c.length).toBeCloseTo(150),a(c.offset,new s(-200,50,400)),e(p).toBeInstanceOf(l),e(p.radiusTop).toBeCloseTo(40),e(p.radiusBottom).toBeCloseTo(40),e(p.height).toBeCloseTo(250),a(p.offset,new s(300,-400,500))}),o("authored colliders keep their values when no asset rescale is applied",()=>{const[o]=t([{id:"box",name:"Box Collider",type:"box",position:[1,2,3],size:[4,5,6]}]);e(o).toBeInstanceOf(i),a(o.dimensions,new s(4,5,6)),a(o.offset,new s(1,2,3))});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gpu-vfx-preload.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as r,it as t,vi as n}from"vitest";n.hoisted(()=>{const e=new Proxy({},{get:(e,r)=>e[r]??(e[r]=n.fn())});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),n.mock("../rendering/webgpu-runtime-bridge.js",()=>({ensureWebGpuRuntimeBindings:n.fn(async()=>{})}));import{GpuVfxWorldService as s}from"../effects/vfx/gpu-vfx-world-service.js";function o(){let e;return{promise:new Promise(r=>{e=r}),resolve:e}}function a(){const e=[],r=Object.assign(Object.create(s.prototype),{view:{requestedBackend:"webgpu"},cpuFallbackReasons:new Map,entriesByAssetId:new Map,pendingEntryBuilds:new Set,rebuildTail:Promise.resolve(!0),pendingRebuild:null,currentEpoch:null,createEntry:n.fn(async r=>{await r.loaded;const t={asset:r,references:0,reservedInstances:1};return e.push(t),t}),rebuildWorld:n.fn(async()=>(r.currentEpoch={reservations:new Map(e.map(e=>[e,e.reservedInstances]))},!0))});return{world:r,entries:e}}async function i(){await new Promise(e=>setTimeout(e,0))}e("GPU VFX preload generations",()=>{t("waits for concurrent templates and builds one world with every actor reservation",async()=>{const{world:e,entries:t}=a(),n=o(),s={id:"first",vfx:{inputs:[],emitters:[]}},l={id:"second",vfx:{inputs:[],emitters:[]},loaded:n.promise};let c=!1;const d=Promise.all([e.prepare(s),e.prepare(s),e.prepare(l)]).then(e=>(c=!0,e));await i(),r(e.createEntry).toHaveBeenCalledTimes(2),r(e.rebuildWorld).not.toHaveBeenCalled(),r(c).toBe(!1),n.resolve();const p=await d;r(p.every(e=>null!=e)).toBe(!0),r(e.rebuildWorld).toHaveBeenCalledTimes(1);for(const n of t)r(e.currentEpoch.reservations.get(n)).toBeGreaterThanOrEqual(n.references)}),t("builds another generation for a template arriving during GPU creation",async()=>{const{world:e,entries:t}=a(),n=o();e.rebuildWorld.mockImplementationOnce(async()=>{const r=new Map(t.map(e=>[e,e.reservedInstances]));return await n.promise,e.currentEpoch={reservations:r},!0});const s=e.prepare({id:"first",vfx:{inputs:[],emitters:[]}});await i(),r(e.rebuildWorld).toHaveBeenCalledTimes(1);const l=e.prepare({id:"second",vfx:{inputs:[],emitters:[]}});await i(),r(e.rebuildWorld).toHaveBeenCalledTimes(1),n.resolve(),await Promise.all([s,l]),r(e.rebuildWorld).toHaveBeenCalledTimes(2),r(e.currentEpoch.reservations.size).toBe(2)})});/*
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 a,vi as s}from"vitest";import{Scene as i,Texture as r}from"three";import{Subject as n}from"rxjs";s.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&s.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),s.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),s.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),s.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),s.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:s.fn()}));import{SceneMaterializer as d}from"../scene/materializer.js";import{DetailTier as p,SerializedParamType as o}from"../scene/model.js";function m(e,t){const a=s.fn(async(e,t)=>({scene:{},animations:[]})),p=s.fn(async e=>new r),o=s.fn(async e=>({texture:new r,layerIndex:0})),m=s.fn(),l={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},f={onCreate:new n,onDelete:new n,onUpdate:new n,getAsset:s.fn(async e=>t.get(e)??null),getAssets:s.fn(async()=>Array.from(t.values()))};return{materializer:new d(new i,l,f,{getMesh:a,getTexture:p,getTextureArray:o},{renderer:{initTexture:m},initTexture:m,onLoop:s.fn()},[],[],{create:async()=>null,initActor:async()=>{}}),getMesh:a,getTexture:p,getTextureArray:o,initTexture:m}}function l(e){return{name:e.id,...e}}function f(e,t){return{id:e,name:e,type:"mesh",fileKey:`${e}.glb`,fileFormat:"glb",mesh:t}}function c(e,t){return{id:e,name:e,type:"prefab",prefab:{objects:t}}}function u(e,t){return{id:e,name:e,type:"material",material:{type:"shader",side:0,params:{},shaderParams:{map:{type:o.Texture,value:t}}}}}function h(e){return{id:e,name:e,type:"texture",fileKey:`${e}.png`,fileFormat:"png"}}function y(e){return{color:"#ffffff",materialId:e}}e("SceneMaterializer asset prefetch",()=>{a("prefetches unique materialized asset meshes without creating discarded instances",async()=>{const e=f("mesh-a"),a=f("mesh-b"),s=f("mesh-c"),i=f("mesh-disabled"),r=f("mesh-high",{detailTier:p.high}),n=c("prefab-a",[l({id:"prefab-mesh-c",type:"asset_mesh",assetId:s.id}),l({id:"prefab-duplicate-mesh-a",type:"asset_mesh",assetId:e.id}),l({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),d=new Map([e,a,s,i,r,n].map(e=>[e.id,e])),o=[l({id:"root",type:"group",children:[l({id:"mesh-a-1",type:"asset_mesh",assetId:e.id}),l({id:"mesh-a-2",type:"asset_mesh",assetId:e.id}),l({id:"disabled-group",type:"group",enabled:!1,children:[l({id:"disabled-mesh",type:"asset_mesh",assetId:i.id})]}),l({id:"prefab-instance",type:"prefab",assetId:n.id}),l({id:"high-detail-mesh",type:"asset_mesh",assetId:r.id})]}),l({id:"mesh-b",type:"asset_mesh",assetId:a.id})],{materializer:u,getMesh:h}=m(o,d);u.detailTier=p.low,await u.preInit(),await u.prefetchAssets(),t(h).toHaveBeenCalledTimes(3),t(new Set(h.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(h.mock.calls.every(([,e])=>!0===e?.mergeGeomtries)).toBe(!0),t(h.mock.calls.every(([,e])=>!1===e?.clone)).toBe(!0)}),a("initializes textures from nested groups and prefab assets",async()=>{const e=h("texture-root"),a=h("texture-group"),s=h("texture-prefab"),i=h("texture-skipped"),r=u("material-root",e.id),n=u("material-group",a.id),d=u("material-prefab",s.id),p=u("material-skipped",i.id),o=c("prefab-a",[l({id:"prefab-group",type:"group",children:[l({id:"prefab-shape",type:"shape_mesh",materialAssignments:[y(d.id)]})]}),l({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),f=new Map([e,a,s,i,r,n,d,p,o].map(e=>[e.id,e])),g=[l({id:"root-group",type:"group",children:[l({id:"nested-mesh",type:"asset_mesh",materialAssignments:[y(n.id)]}),l({id:"disabled-group",type:"group",enabled:!1,children:[l({id:"skipped-mesh",type:"asset_mesh",materialAssignments:[y(p.id)]})]}),l({id:"prefab-instance",type:"prefab",assetId:o.id})]}),l({id:"root-shape",type:"shape_mesh",materialAssignments:[y(r.id)]})],{materializer:b,getTexture:x,initTexture:w}=m(g,f);await b.preInit(),await b.initTextures(),t(x).toHaveBeenCalledTimes(3),t(new Set(x.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(w).toHaveBeenCalledTimes(3)})});/*
1
+ import{describe as e,expect as t,it as a,vi as s}from"vitest";import{Scene as r,Texture as i}from"three";import{Subject as n}from"rxjs";s.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&s.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),s.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),s.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),s.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),s.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:s.fn()}));import{SceneMaterializer as d}from"../scene/materializer.js";import{DetailTier as o,SerializedParamType as l}from"../scene/model.js";function p(e,t){const a=s.fn(async(e,t)=>({scene:{},animations:[]})),o=s.fn(async e=>new i),l=s.fn(async e=>({texture:new i,layerIndex:0})),p=s.fn(),c={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},m={onCreate:new n,onDelete:new n,onUpdate:new n,getAsset:s.fn(async e=>t.get(e)??null),getAssets:s.fn(async()=>Array.from(t.values()))};return{materializer:new d(new r,c,m,{getMesh:a,getTexture:o,getTextureArray:l},{renderer:{initTexture:p},initTexture:p,onLoop:s.fn()},[],[],{create:async()=>null,initActor:async()=>{}}),getMesh:a,getTexture:o,getTextureArray:l,initTexture:p}}function c(e){return{name:e.id,...e}}function m(e,t){return{id:e,name:e,type:"mesh",fileKey:`${e}.glb`,fileFormat:"glb",mesh:t}}function f(e,t){return{id:e,name:e,type:"prefab",prefab:{objects:t}}}function u(e,t){return{id:e,name:e,type:"material",material:{type:"shader",side:0,params:{},shaderParams:{map:{type:l.Texture,value:t}}}}}function y(e){return{id:e,name:e,type:"texture",fileKey:`${e}.png`,fileFormat:"png"}}function h(e){return{color:"#ffffff",materialId:e}}e("SceneMaterializer asset prefetch",()=>{a("shares VFX warmup across characters and starts independent effects concurrently",async()=>{const{materializer:e}=p([],new Map);let a;e.inEditor=!1;const i=new Promise(e=>{a=e}),d=[0,1].map(()=>({object:new r,prepareRendering:s.fn(()=>i),onEndPlay:s.fn(),disposed:new n})),o=d.map((e,t)=>({asset:{id:`vfx-${t}`,vfx:{emitters:[]}},create:s.fn(async()=>e)})),l=e.preloadVisualEffects(o),c=e.preloadVisualEffects([o[0]]);await Promise.resolve(),t(o.every(e=>1===e.create.mock.calls.length)).toBe(!0),t(d.every(e=>1===e.prepareRendering.mock.calls.length)).toBe(!0),t(d.every(e=>0===e.onEndPlay.mock.calls.length)).toBe(!0),a(),await Promise.all([l,c]),await e.preloadVisualEffects(o),t(o.every(e=>1===e.create.mock.calls.length)).toBe(!0),t(d.every(e=>1===e.onEndPlay.mock.calls.length)).toBe(!0)}),a("retries a failed VFX preload",async()=>{const{materializer:e}=p([],new Map);e.inEditor=!1;const a={asset:{id:"vfx",vfx:{emitters:[]}},create:s.fn().mockRejectedValueOnce(new Error("load failed")).mockResolvedValue({object:new r,prepareRendering:s.fn(async()=>{}),onEndPlay:s.fn(),disposed:new n})};await t(e.preloadVisualEffects([a])).rejects.toThrow("load failed"),await e.preloadVisualEffects([a]),t(a.create).toHaveBeenCalledTimes(2)}),a("prefetches unique materialized asset meshes without creating discarded instances",async()=>{const e=m("mesh-a"),a=m("mesh-b"),s=m("mesh-c"),r=m("mesh-disabled"),i=m("mesh-high",{detailTier:o.high}),n=f("prefab-a",[c({id:"prefab-mesh-c",type:"asset_mesh",assetId:s.id}),c({id:"prefab-duplicate-mesh-a",type:"asset_mesh",assetId:e.id}),c({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),d=new Map([e,a,s,r,i,n].map(e=>[e.id,e])),l=[c({id:"root",type:"group",children:[c({id:"mesh-a-1",type:"asset_mesh",assetId:e.id}),c({id:"mesh-a-2",type:"asset_mesh",assetId:e.id}),c({id:"disabled-group",type:"group",enabled:!1,children:[c({id:"disabled-mesh",type:"asset_mesh",assetId:r.id})]}),c({id:"prefab-instance",type:"prefab",assetId:n.id}),c({id:"high-detail-mesh",type:"asset_mesh",assetId:i.id})]}),c({id:"mesh-b",type:"asset_mesh",assetId:a.id})],{materializer:u,getMesh:y}=p(l,d);u.detailTier=o.low,await u.preInit(),await u.prefetchAssets(),t(y).toHaveBeenCalledTimes(3),t(new Set(y.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(y.mock.calls.every(([,e])=>!0===e?.mergeGeomtries)).toBe(!0),t(y.mock.calls.every(([,e])=>!1===e?.clone)).toBe(!0)}),a("initializes textures from nested groups and prefab assets",async()=>{const e=y("texture-root"),a=y("texture-group"),s=y("texture-prefab"),r=y("texture-skipped"),i=u("material-root",e.id),n=u("material-group",a.id),d=u("material-prefab",s.id),o=u("material-skipped",r.id),l=f("prefab-a",[c({id:"prefab-group",type:"group",children:[c({id:"prefab-shape",type:"shape_mesh",materialAssignments:[h(d.id)]})]}),c({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),m=new Map([e,a,s,r,i,n,d,o,l].map(e=>[e.id,e])),g=[c({id:"root-group",type:"group",children:[c({id:"nested-mesh",type:"asset_mesh",materialAssignments:[h(n.id)]}),c({id:"disabled-group",type:"group",enabled:!1,children:[c({id:"skipped-mesh",type:"asset_mesh",materialAssignments:[h(o.id)]})]}),c({id:"prefab-instance",type:"prefab",assetId:l.id})]}),c({id:"root-shape",type:"shape_mesh",materialAssignments:[h(i.id)]})],{materializer:b,getTexture:w,initTexture:x}=p(g,m);await b.preInit(),await b.initTextures(),t(w).toHaveBeenCalledTimes(3),t(new Set(w.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(x).toHaveBeenCalledTimes(3)})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=navigation-bake-backend.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{afterEach as e,expect as a,it as n,vi as s}from"vitest";import{RuntimeBundledBackendService as t}from"../scene/runtime-bundled-backend-service.js";e(()=>s.unstubAllGlobals()),n("resolves baked references beside the bundle for browser and native URLs",async()=>{s.stubGlobal("fetch",s.fn(async()=>({json:async()=>({"scene-blob":[{id:"scene",data:[],navigationBakes:{"prefab/nav":{file:"navigation/mesh.bin",settings:"settings"}}}]})})));for(const[e,n]of[["https://example.com/game/hology_data.json","https://example.com/game/navigation/mesh.bin"],["data/hology_data.json","data/navigation/mesh.bin"]]){const s=new t(e);await s.preloadData(),a((await s.getSceneNavigationBakes("scene"))["prefab/nav"].file).toBe(n)}});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=navigation-bake.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as t,it as a}from"vitest";import{Box3 as o,Matrix4 as s,Vector3 as i}from"three";import{importNavMesh as n,NavMeshQuery as r}from"recast-navigation";import{bakeNavigation as p,collectNavigationInputs as c,landscapeNavigationGeometry as d,navigationEntries as l,navigationVolumes as y}from"../navigation/bake.js";const h=e=>({id:"object",name:"Object",type:"group",...e});e("headless navigation baking",()=>{a("bakes, serializes and imports walkable collision geometry crossing the volume boundary",async()=>{const e=[h({id:"floor",type:"shape_mesh",shape:"box",position:[0,-.5,0],scale:[100,1,100]}),h({id:"nav",type:"actor",actor:{type:"NavMesh"},scale:[40,10,40]})],a=l(e,[]),o=y(a)[0],s=await c({objects:e,assets:[],loadCollisionShapes:async()=>[]},a,o.bounds),i=await p(o,s),d=n(i).navMesh,m=new r(d);try{const e=m.computePath({x:-10,y:0,z:0},{x:10,y:0,z:0});t(e.success).toBe(!0),t(e.path.at(-1)?.x).toBeCloseTo(10)}finally{m.destroy(),d.destroy()}}),a("uses full-density distant terrain, transformed bounds and authored holes",()=>{const e=h({shape:"landscape",landscape:{options:{sectionSize:10,density:2,sections:{x:8,y:1}},heightMaps:[{x:7,y:0,points:[{i:4,y:3}]}],holes:[{m:"7,0",i:0,w:[1]}]}}),a=(new s).makeTranslation(1e3,0,0),n=new o(new i(1028,-10,-11),new i(1034,10,1)),r=d(e,a,n);t(r).toHaveLength(1),t(r[0].positions[13]).toBe(3),t(r[0].positions[0]).toBe(1025),t(r[0].indices).not.toContain(0),t(r[0].indices.length).toBe(21)}),a("expands nested prefab volumes with inherited transforms and overrides",()=>{const e={id:"prefab",name:"Prefab",type:"prefab",prefab:{mainActorId:"nav",objects:[h({id:"nav",type:"actor",actor:{type:"NavMesh"},position:[2,0,0],scale:[10,5,10]})]}},a=[h({id:"instance",type:"prefab",assetId:e.id,position:[100,0,0],prefab:{params:{cellSize:{type:0,value:.5}}}})],o=y(l(a,[e]))[0];t(o.id).toBe("instance/nav"),t(o.bounds.min.x).toBe(97),t(o.config.cs).toBe(.5),t(()=>l([h({type:"prefab",assetId:"missing"})],[])).toThrow("Missing navigation prefab")}),a("waits for in-flight worker jobs before releasing a failed bake",async()=>{const e=y(l([h({type:"actor",actor:{type:"NavMesh"},scale:[100,10,100]})],[]))[0];let a=0;await t(p(e,{geometry:[]},async()=>{if(0===a++)throw new Error("worker failed");await new Promise(e=>setTimeout(e,10)),a++})).rejects.toThrow("worker failed"),t(a).toBe(7)})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
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(),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)})});/*
1
+ import*as e from"three";import{describe as s,expect as a,it as t,vi as n}from"vitest";import{createAudioClipInstance as r,createAudioTrack as c,createSequenceData as o,createVfxClipInstance as i,createVfxTrack as l}from"../effects/sequence/sequence-data.js";import{SequencePlayer as p}from"../effects/sequence/sequence-player.js";function d(){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 f(){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("keeps preparation pending until ability audio decoding finishes",async()=>{let e;const s=new Promise(s=>{e=s}),t=n.fn(()=>s),i=new p;i.setAssetLoader({getAudioByAssetId:t});const l=o(),d=c();d.clips.push(r("swing",0,1)),l.tracks.push(d),i.load(l);let v=!1;const u=i.prepare().then(()=>{v=!0});await f(),a(v).toBe(!1),a(t).toHaveBeenCalledTimes(1),e({}),await u,a(v).toBe(!0)}),t("preloads unique VFX assets when preparing a sequence",async()=>{const s=new p,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 r=o(),c=l();c.clips.push(i("vfx-asset",0,1),i("vfx-asset",1,1),i("other-vfx-asset",2,1)),r.tracks.push(c),s.load(r),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=d(),t=n.fn(async()=>s),r=n.fn(async()=>{}),c=new p;c.setWorld({scene:new e.Scene,preloadVisualEffects:r}),c.setActorFactory({}),c.setAssetLoader({getAsset:n.fn(async e=>({id:e,vfx:{emitters:[]}}))}),c.setVfxService({createFromAssetId:t});const v=o(),u=l();u.clips.push(i("vfx-asset",0,1)),v.tracks.push(u),c.load(v),await c.prepare(),a(t).toHaveBeenCalledTimes(1),a(r).not.toHaveBeenCalled(),a(s.prepareRendering).toHaveBeenCalledTimes(1),a(s.play).not.toHaveBeenCalled(),c.play(),await f(),a(t).toHaveBeenCalledTimes(1)}),t("prepares each retained effect as soon as it loads and waits for its rendering resources",async()=>{const s=d(),t=d();let r,c;s.prepareRendering.mockReturnValue(new Promise(e=>{c=e}));const v=new Promise(e=>{r=e}),u=new p;u.setWorld({scene:new e.Scene}),u.setAssetLoader({}),u.setVfxService({createFromAssetId:n.fn(e=>"first"===e?Promise.resolve(s):v)});const m=o(),w=l();w.clips.push(i("first",0,1),i("second",1,1)),m.tracks.push(w),u.load(m);let y=!1;const h=u.prepare().then(()=>{y=!0});await f(),a(s.prepareRendering).toHaveBeenCalledTimes(1),a(t.prepareRendering).not.toHaveBeenCalled(),a(y).toBe(!1),r(t),await f(),a(t.prepareRendering).toHaveBeenCalledTimes(1),a(y).toBe(!1),c(),await h,a(y).toBe(!0)}),t("restarts an already-active VFX clip when seeking backwards within the clip",async()=>{const s=d(),t=new p;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const r=o();r.duration=2;const c=l(),v=i("vfx-asset",0,1.5);c.clips.push(v),r.tracks.push(c),t.load(r),t.play(),await f(),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=d(),t=new p;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const r=o();r.duration=1,r.loop=!0;const c=l(),v=i("vfx-asset",0,1);c.clips.push(v),r.tracks.push(c),t.load(r),t.play(),await f(),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=d(),t=new p;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const r=o();r.duration=1;const c=l();c.clips.push(i("vfx-asset",0,.8)),r.tracks.push(c),t.load(r),t.play(),await f(),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=d(),t=new p;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)});const r=o();r.duration=2;const c=l(),v=i("vfx-asset",.5,1);c.clips.push(v),r.tracks.push(c),t.load(r),t.play(),t.seek(.75),await f(),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=d(),t=n.fn(async()=>s),r=new p;r.setWorld({scene:new e.Scene}),r.setVfxService({createFromAssetId:t}),r.externalTimeControl=!0,r.inEditor=!0;const c=o();c.duration=2;const v=l();v.clips.push(i("vfx-asset",.5,1)),c.tracks.push(v),r.load(c),r.seek(.75),await f(),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=d(),t=n.fn(async()=>s),r=new p;r.setWorld({scene:new e.Scene}),r.setVfxService({createFromAssetId:t});const c=o();c.duration=1;const v=l();v.clips.push(i("vfx-asset",0,1)),c.tracks.push(v),r.load(c),r.play(),await f(),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=d(),t=new p;t.setWorld({scene:new e.Scene}),t.setVfxService({createFromAssetId:n.fn(async()=>s)}),t.externalTimeControl=!0,t.inEditor=!0;const r=o();r.duration=2;const c=l();c.clips.push(i("vfx-asset",.5,1)),r.tracks.push(c),t.load(r),t.seek(.75),await f(),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
  */
@@ -1,4 +1,4 @@
1
- import{expect as e,test as t,vi as o}from"vitest";import{DoubleSide as a,FrontSide as r,Texture as n}from"three";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),o.mock("three/examples/jsm/Addons.js",async e=>({...await e(),ConvexHull:class{},SkeletonUtils:{clone:e=>e},RectAreaLightHelper:class{}})),o.mock("three-shader-graph",async e=>{const t=await e();return{...t,rgb:(e,o,a)=>null==o&&null==a&&!0===e?.isColor&&"function"==typeof e.getHexString?t.rgb("#"+e.getHexString()):t.rgb(e,o,a)}}),o.mock("../rendering.js",()=>({RenderingView:class{constructor(){this._id="test",this.csm={setupMaterial:o.fn()}}getEnvTexture(e){return e}},setRenderingPaused:o.fn()}));import{materialFromAsset as s,prepareCustomParamsFromShaderGraph as d,prepareShaderGraphParameters as u}from"../scene/materializer.js";import{SerializedParamType as i}from"../scene/model.js";import{AssetsProvider as p}from"../scene/assets-provider.js";import{AssetResourceLoader as l}from"../scene/asset-resource-loader.js";import{buildShaderGraphMaterial as c,buildShaderGraphPostProcessMaterial as m,compileShaderGraph as v,compileShaderGraphPreview as f,createDefaultPostProcessShaderGraphDocument as h,createDefaultShaderGraphDocument as g,createDefaultVfxShaderGraphDocument as y,shaderGraphPostProcessWeightUniformName as I,shaderGraphParametersToPropertyParameters as b}from"../shader/graph/index.js";import{NodeShaderMaterial as x,textureSampler2d as k,UniformFloatNode as B,UniformSampler2dArraySlice as T,vec2 as w}from"../shader-nodes/index.js";import{StandardShader as C}from"../shader/builtin/standard-shader.js";import{sceneMapUniformName as M}from"../shader-nodes/scene-sample.js";function j(e,t,o){return{version:1,target:"surface",parameters:[],nodes:e,edges:t,outputs:o}}function S(e,t){return{id:e,name:e,type:"material",material:{type:"shaderGraph",side:r,params:{},shaderParams:{},shaderGraph:t}}}t("graph validation rejects missing nodes, invalid ports, and incompatible edges",()=>{const t=g();t.outputs.color={input:{nodeId:"missing",port:"value"}},e(()=>v(t)).toThrow(/Missing shader graph node/);const o=g();o.outputs.color={input:{nodeId:"base-color",port:"missing"}},e(()=>v(o)).toThrow(/does not have output/);const a=j([{id:"a",kind:"constant.vec2",params:{value:[1,1]}},{id:"b",kind:"constant.vec3",params:{value:[1,1,1]}},{id:"add",kind:"math.add"}],[{from:{nodeId:"a",port:"value"},to:{nodeId:"add",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"add",port:"b"}}],{color:{input:{nodeId:"add",port:"value"}}});e(()=>v(a)).toThrow(/Cannot add vec2 and vec3/)}),t("scene lighting nodes expose renderer ambient and directional light values",()=>{const t=j([{id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"}],[],{color:{input:{nodeId:"ambient",port:"color"}}}),o=v(t);e(o.nodes.get("ambient")?.outputs.color.type).toBe("rgb"),e(o.nodes.get("directional")?.outputs.color.type).toBe("rgb"),e(o.nodes.get("directional")?.outputs.direction.type).toBe("vec3");const a=c(t);e(a.fragmentShader).toContain("ambientLightColor"),t.outputs.color={input:{nodeId:"directional",port:"color"}};const r=c(t);e(r.fragmentShader).toContain("directionalLights"),e(r.fragmentShader).toContain("NUM_DIR_LIGHTS"),e(r.fragmentShader).toMatch(/max\s*\(\s*float\s*\(\s*NUM_DIR_LIGHTS\s*\)/)}),t("named reroutes pass values through declarations to multiple usages",()=>{const t=j([{id:"source",kind:"constant.float",params:{value:.75}},{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Mask"}},{id:"usage-a",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}},{id:"usage-b",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}},{id:"add",kind:"math.add"}],[{from:{nodeId:"source",port:"value"},to:{nodeId:"declaration",port:"value"}},{from:{nodeId:"usage-a",port:"value"},to:{nodeId:"add",port:"a"}},{from:{nodeId:"usage-b",port:"value"},to:{nodeId:"add",port:"b"}}],{color:{input:{nodeId:"add",port:"value"}}}),o=v(t),a=o.nodes.get("declaration")?.outputs.value;e(a?.type).toBe("float"),e(o.nodes.get("usage-a")?.outputs.value).toBe(a),e(o.nodes.get("usage-b")?.outputs.value).toBe(a),e(o.output.color).toBeTruthy()}),t("named reroutes preserve dynamic vector types",()=>{const t=j([{id:"source",kind:"constant.vec3",params:{value:[1,2,3]}},{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Direction"}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}}],[{from:{nodeId:"source",port:"value"},to:{nodeId:"declaration",port:"value"}}],{color:{input:{nodeId:"usage",port:"value"}}}),o=v(t);e(o.nodes.get("usage")?.outputs.value.type).toBe("vec3")}),t("named reroute validation rejects missing and invalid declarations",()=>{const t=j([{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"missing"}}],[],{});e(()=>v(t)).toThrow(/declaration "missing" does not exist/);const o=j([{id:"not-declaration",kind:"constant.float",params:{value:1}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"not-declaration"}}],[],{});e(()=>v(o)).toThrow(/not a named reroute declaration/)}),t("named reroute cycles report a compile error",()=>{const t=j([{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Cycle"}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}}],[{from:{nodeId:"usage",port:"value"},to:{nodeId:"declaration",port:"value"}}],{color:{input:{nodeId:"usage",port:"value"}}});e(()=>v(t)).toThrow(/contains a cycle/)}),t("surface graph compiles to NodeShaderOutput",()=>{const t=v(g("surface"));e(t.output.color).toBeTruthy(),e(t.output.transparent).toBe(!1)}),t("inline literal inputs compile without separate constant nodes",()=>{const t=j([{id:"uv",kind:"builtin.uv"},{id:"scale-uv",kind:"math.multiply",inputs:{b:{value:2,valueType:"float"}}},{id:"rotate",kind:"uv.rotate",inputs:{angle:{value:.5,valueType:"float"},center:{value:[.25,.75],valueType:"vec2"}}},{id:"translate",kind:"transform.translate",inputs:{offset:{value:[0,1,0],valueType:"vec3"}}}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"scale-uv",port:"a"}},{from:{nodeId:"scale-uv",port:"value"},to:{nodeId:"rotate",port:"uv"}}],{color:{input:{nodeId:"rotate",port:"value"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.nodes.get("scale-uv")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("rotate")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("translate")?.outputs.value.type).toBe("mat4"),e(o.output.color).toBeTruthy(),e(o.output.transform).toBeTruthy()}),t("uv attribute node supports selecting non-default uv channels",()=>{const t=j([{id:"uv",kind:"builtin.uv",params:{channel:2}}],[],{color:{input:{nodeId:"uv",port:"value"}}}),o=v(t);e(o.nodes.get("uv")?.outputs.value.type).toBe("vec2");const a=c(t);e(a.fragmentShader).toContain("uv2")}),t.each(["surface","decal"])("world normal node compiles for %s graphs",t=>{const o=j([{id:"world-normal",kind:"builtin.worldNormal"}],[],{color:{input:{nodeId:"world-normal",port:"value"}}});o.target=t;const a=v(o);e(a.nodes.get("world-normal")?.outputs.value.type).toBe("vec3"),e(c(o).fragmentShader).toBeTruthy()}),t("sprite graph compiles with sprite-compatible material output",()=>{const t=g("sprite"),o=v(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!0)}),t("default VFX shader graphs compile mapped targets and build non-sprite materials",()=>{for(const t of["sprite","surface","decal","trail"]){const o=y(t);if(e(v(o).output.color).toBeTruthy(),"sprite"===t)continue;const a=c(o,{trailBillboard:"trail"===t});e(a.isMaterial).toBe(!0)}}),t("graph material options apply to supported material targets",()=>{const t=g("surface");t.materialOptions={side:"double",transparent:!0};const o=c(t);e(o.side).toBe(a),e(o.transparent).toBe(!0);const r=g("decal");r.materialOptions={transparent:!0};const n=c(r);e(n.transparent).toBe(!0)}),t("standard graph compiles extended material outputs and vertex transform",()=>{const t=j([{id:"metalness",kind:"constant.float",params:{value:.8}},{id:"ao",kind:"constant.float",params:{value:.5}},{id:"sheen",kind:"constant.vec3",params:{value:[.1,.2,.3]}},{id:"offset",kind:"constant.vec3",params:{value:[0,1,0]}},{id:"translate",kind:"transform.translate"}],[{from:{nodeId:"offset",port:"value"},to:{nodeId:"translate",port:"offset"}}],{metalness:{input:{nodeId:"metalness",port:"value"}},ambientOcclusion:{input:{nodeId:"ao",port:"value"}},sheenColor:{input:{nodeId:"sheen",port:"value"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.output.metalness).toBeTruthy(),e(o.output.ambientOcclusion).toBeTruthy(),e(o.output.sheenColor).toBeTruthy(),e(o.output.transform).toBeTruthy();const a=c(t);e(a.outputMetalness).toBeTruthy(),e(a.outputAmbientOcclusion).toBeTruthy(),e(a.outputTransform).toBeTruthy()}),t("math multiply composes transform matrices",()=>{const t=j([{id:"translate",kind:"transform.translate",inputs:{offset:{value:[0,1,0],valueType:"vec3"}}},{id:"scale",kind:"transform.scale",inputs:{scale:{value:[2,2,2],valueType:"vec3"}}},{id:"compose",kind:"math.multiply"}],[{from:{nodeId:"translate",port:"value"},to:{nodeId:"compose",port:"a"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"compose",port:"b"}}],{transform:{input:{nodeId:"compose",port:"value"}}}),o=v(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("mat4"),e(o.output.transform).toBeTruthy(),e(c(t).outputTransform).toBeTruthy()}),t("matrix input nodes compile as mat4 uniforms",()=>{for(const t of["builtin.viewMatrix","builtin.projectionMatrix","builtin.modelViewMatrix"]){const o=j([{id:"matrix",kind:t}],[],{transform:{input:{nodeId:"matrix",port:"value"}}}),a=v(o);e(a.nodes.get("matrix")?.outputs.value.type).toBe("mat4"),e(a.output.transform).toBeTruthy()}}),t("compose vec4 node compiles from float components",()=>{const t=j([{id:"compose",kind:"compose.vec4",inputs:{x:{value:.1},y:{value:.2},z:{value:.3},w:{value:1}}}],[],{color:{input:{nodeId:"compose",port:"value"}}}),o=v(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("vec4"),e(o.output.color).toBeTruthy()}),t("atan2 node compiles from y and x inputs",()=>{const t=j([{id:"atan2",kind:"math.atan2",inputs:{y:{value:1},x:{value:0}}}],[],{color:{input:{nodeId:"atan2",port:"value"}}}),o=v(t);e(o.nodes.get("atan2")?.outputs.value.type).toBe("float"),e(o.output.color).toBeTruthy()}),t("built-in attribute nodes compile for fragment and transform outputs",()=>{const t=j([{id:"color",kind:"builtin.color"},{id:"normal",kind:"builtin.normal"},{id:"position",kind:"builtin.position"},{id:"worldPosition",kind:"builtin.worldPosition"},{id:"objectPosition",kind:"builtin.objectPosition"},{id:"translate",kind:"transform.translate"}],[{from:{nodeId:"objectPosition",port:"value"},to:{nodeId:"translate",port:"offset"}}],{color:{input:{nodeId:"color",port:"value"}},normal:{input:{nodeId:"normal",port:"value"}},roughness:{input:{nodeId:"position",port:"x"}},metalness:{input:{nodeId:"worldPosition",port:"x"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.nodes.get("color")?.outputs.value.type).toBe("rgba"),e(o.nodes.get("normal")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("position")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("worldPosition")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("objectPosition")?.outputs.value.type).toBe("vec3"),e(o.output.color).toBeTruthy(),e(o.output.normal).toBeTruthy(),e(o.output.transform).toBeTruthy(),e(c(t).outputTransform).toBeTruthy()}),t("painted layer mix node compiles color layers and builds a material",()=>{const t=j([{id:"base",kind:"constant.rgba",params:{value:"#224422",alpha:1}},{id:"grass",kind:"constant.rgba",params:{value:"#55aa33",alpha:1}},{id:"stone",kind:"constant.rgba",params:{value:"#777777",alpha:1}},{id:"mix",kind:"layer.mixPainted",params:{mode:"soft",enableNoise:!0,softness:.25,noiseScale:.2,noiseAmount:.4}}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"grass",port:"value"},to:{nodeId:"mix",port:"layer1"}},{from:{nodeId:"stone",port:"value"},to:{nodeId:"mix",port:"layer3"}}],{color:{input:{nodeId:"mix",port:"value"}}}),o=v(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(c(t).isMaterial).toBe(!0)}),t("painted layer mix node compiles sparse float layers in hard and soft modes",()=>{for(const t of["hard","soft"]){const o=j([{id:"base",kind:"constant.float",params:{value:.8}},{id:"mud",kind:"constant.float",params:{value:1}},{id:"wet",kind:"constant.float",params:{value:.25}},{id:"mix",kind:"layer.mixPainted",params:{mode:t,enableNoise:!1,softness:.3,noiseScale:.1,noiseAmount:.5}}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"mud",port:"value"},to:{nodeId:"mix",port:"layer2"}},{from:{nodeId:"wet",port:"value"},to:{nodeId:"mix",port:"layer8"}}],{roughness:{input:{nodeId:"mix",port:"value"}}}),a=v(o);e(a.nodes.get("mix")?.outputs.value.type).toBe("float"),e(a.output.roughness).toBeTruthy()}}),t("painted layer mix node rejects incompatible layer types",()=>{const t=j([{id:"base",kind:"constant.vec2",params:{value:[0,1]}},{id:"layer",kind:"constant.vec3",params:{value:[1,0,0]}},{id:"mix",kind:"layer.mixPainted"}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"layer",port:"value"},to:{nodeId:"mix",port:"layer1"}}],{color:{input:{nodeId:"mix",port:"value"}}});e(()=>v(t)).toThrow(/Cannot mix vec2 and vec3/)}),t("additional math, uv, shape, noise, and effect nodes compile",()=>{const t=j([{id:"uv",kind:"builtin.uv"},{id:"angle",kind:"constant.float",params:{value:.5}},{id:"scale",kind:"constant.float",params:{value:4}},{id:"half",kind:"constant.float",params:{value:.5}},{id:"axis",kind:"constant.vec3",params:{value:[0,1,0]}},{id:"rotatedUv",kind:"uv.rotate"},{id:"twirl",kind:"uv.twirl"},{id:"bulge",kind:"uv.bulge"},{id:"radial",kind:"uv.radial"},{id:"flipbook",kind:"uv.flipbook"},{id:"shape",kind:"shape.roundedRectangle"},{id:"noise",kind:"noise.simplex"},{id:"voronoi",kind:"noise.voronoi2d"},{id:"fresnel",kind:"effect.fresnel"},{id:"sin",kind:"math.sin"},{id:"pow",kind:"math.pow"},{id:"mix",kind:"math.mix"},{id:"rotateAxis",kind:"transform.rotateAxis"}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"rotatedUv",port:"uv"}},{from:{nodeId:"angle",port:"value"},to:{nodeId:"rotatedUv",port:"angle"}},{from:{nodeId:"rotatedUv",port:"value"},to:{nodeId:"twirl",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"twirl",port:"strength"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"bulge",port:"uv"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"bulge",port:"power"}},{from:{nodeId:"bulge",port:"value"},to:{nodeId:"radial",port:"uv"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"flipbook",port:"uv"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"shape",port:"uv"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"width"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"height"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"radius"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"noise",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"noise",port:"scale"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"voronoi",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"voronoi",port:"scale"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"fresnel",port:"power"}},{from:{nodeId:"radial",port:"angle"},to:{nodeId:"sin",port:"value"}},{from:{nodeId:"noise",port:"value"},to:{nodeId:"pow",port:"a"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"pow",port:"b"}},{from:{nodeId:"shape",port:"value"},to:{nodeId:"mix",port:"a"}},{from:{nodeId:"voronoi",port:"value"},to:{nodeId:"mix",port:"b"}},{from:{nodeId:"fresnel",port:"value"},to:{nodeId:"mix",port:"t"}},{from:{nodeId:"axis",port:"value"},to:{nodeId:"rotateAxis",port:"axis"}},{from:{nodeId:"angle",port:"value"},to:{nodeId:"rotateAxis",port:"angle"}}],{color:{input:{nodeId:"mix",port:"value"}},opacity:{input:{nodeId:"sin",port:"value"}},anisotropyDirection:{input:{nodeId:"flipbook",port:"value"}},transform:{input:{nodeId:"rotateAxis",port:"value"}}}),o=v(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("float"),e(o.nodes.get("twirl")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("bulge")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("radial")?.outputs.coords.type).toBe("vec2"),e(o.nodes.get("flipbook")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("rotateAxis")?.outputs.value.type).toBe("mat4"),e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.transform).toBeTruthy()}),t("slider input node outputs a clamped float value",()=>{const t=j([{id:"slider",kind:"input.slider",params:{value:1.25}}],[],{roughness:{input:{nodeId:"slider",port:"value"}}}),o=v(t);e(o.nodes.get("slider")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy()}),t("comparison and boolean graph nodes compile to boolean outputs",()=>{const t=j([{id:"a",kind:"constant.float",params:{value:.75}},{id:"b",kind:"constant.float",params:{value:.5}},{id:"greater",kind:"compare.greaterThan"},{id:"lessEqual",kind:"compare.lessThanOrEqual"},{id:"not",kind:"boolean.not"},{id:"and",kind:"boolean.and"},{id:"or",kind:"boolean.or"}],[{from:{nodeId:"a",port:"value"},to:{nodeId:"greater",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"greater",port:"b"}},{from:{nodeId:"a",port:"value"},to:{nodeId:"lessEqual",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"lessEqual",port:"b"}},{from:{nodeId:"lessEqual",port:"value"},to:{nodeId:"not",port:"value"}},{from:{nodeId:"greater",port:"value"},to:{nodeId:"and",port:"a"}},{from:{nodeId:"not",port:"value"},to:{nodeId:"and",port:"b"}},{from:{nodeId:"and",port:"value"},to:{nodeId:"or",port:"a"}},{from:{nodeId:"lessEqual",port:"value"},to:{nodeId:"or",port:"b"}}],{discard:{input:{nodeId:"or",port:"value"}}}),o=v(t);e(o.nodes.get("greater")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("lessEqual")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("not")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("and")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("or")?.outputs.value.type).toBe("boolean"),e(o.output.discard).toBeTruthy()}),t("split component node exposes vector and color channels as separate outputs",()=>{const t=j([{id:"vec",kind:"constant.vec3",params:{value:[.2,.4,.6]}},{id:"color",kind:"constant.rgba",params:{value:"#336699",alpha:.75}},{id:"splitVec",kind:"decompose.split"},{id:"splitColor",kind:"decompose.split"},{id:"sum",kind:"math.add"}],[{from:{nodeId:"vec",port:"value"},to:{nodeId:"splitVec",port:"value"}},{from:{nodeId:"color",port:"value"},to:{nodeId:"splitColor",port:"value"}},{from:{nodeId:"splitVec",port:"z"},to:{nodeId:"sum",port:"a"}},{from:{nodeId:"splitColor",port:"a"},to:{nodeId:"sum",port:"b"}}],{roughness:{input:{nodeId:"sum",port:"value"}}}),o=v(t);e(Object.keys(o.nodes.get("splitVec")?.outputs??{})).toEqual(["x","y","z"]),e(Object.keys(o.nodes.get("splitColor")?.outputs??{})).toEqual(["r","g","b","a","x","y","z","w"]),e(o.nodes.get("splitVec")?.outputs.z.type).toBe("float"),e(o.nodes.get("splitColor")?.outputs.a.type).toBe("float"),e(o.output.roughness).toBeTruthy()}),t("color gradient node compiles editable stops to a sampled color",()=>{const t=j([{id:"threshold",kind:"constant.float",params:{value:.75}},{id:"gradient",kind:"color.gradient",params:{stops:[{position:0,color:"#000000",alpha:1},{position:.5,color:"#ff0000",alpha:1},{position:1,color:"#ffffff",alpha:1}]}}],[{from:{nodeId:"threshold",port:"value"},to:{nodeId:"gradient",port:"t"}}],{color:{input:{nodeId:"gradient",port:"value"}}}),o=v(t);e(o.nodes.get("gradient")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(f(t,"gradient").color).toBeTruthy()}),t("curve node compiles editable curve to a sampled float",()=>{const t=j([{id:"t",kind:"constant.float",params:{value:.75}},{id:"curve",kind:"utility.curve",params:{curve:"InOutQuad"}}],[{from:{nodeId:"t",port:"value"},to:{nodeId:"curve",port:"t"}}],{roughness:{input:{nodeId:"curve",port:"value"}}}),o=v(t);e(o.nodes.get("curve")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy(),e(f(t,"curve").color).toBeTruthy()}),t("graph parameters convert to property params and custom defaults",()=>{const t=g();t.parameters.push({name:"opacity",type:"float",group:"Surface",defaultValue:.5});const o=b(t);e(o.map(e=>e.name)).toEqual(["baseColor","opacity"]),e(o.every(e=>!0===e.options.optional)).toBe(!0),e(o.map(e=>e.options.group??null)).toEqual([null,"Surface"]);const a=d(t);e(a.baseColor).toMatchObject({type:i.RgbNode,value:"#ffffff",override:!1}),e(a.opacity).toMatchObject({type:i.FloatNode,value:.5,override:!1})}),t("existing shader graph param values keep overriding unless explicitly disabled",()=>{const t=g(),o=d(t,{baseColor:{type:i.RgbNode,value:"#ff0000"}});e(o.baseColor).toMatchObject({type:i.RgbNode,value:"#ff0000",override:!0});const a=d(t,{baseColor:{type:i.RgbNode,value:"#00ff00",override:!1}});e(a.baseColor).toMatchObject({type:i.RgbNode,value:"#ffffff",override:!1})}),t("texture graph parameter defaults serialize as sampler parameters",()=>{const t=j([],[],{});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=d(t);e(o.map).toMatchObject({type:i.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph params keep graph-declared serialized types over previous string params",()=>{const t=g();t.parameters.push({name:"map",type:"texture",defaultValue:null});const o=d(t,{baseColor:{type:i.String,value:"#ff0000"},map:{type:i.String,value:"texture-asset-id"}});e(o.baseColor).toMatchObject({type:i.RgbNode,value:"#ff0000"}),e(o.map).toMatchObject({type:i.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph texture params resolve asset ids before compile",async()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=await u({},t,new D([{id:"texture-asset-id",name:"Map",type:"texture",fileKey:"map.png",fileFormat:"png"}]),new A);e(o.map).toBeTruthy(),e(typeof o.map).not.toBe("string"),e(v(t,{parameters:o}).output.color).toBeTruthy()}),t("shader graph texture array params expose named slice uniforms",async()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-array-layer"});const o=await u({},t,new D([{id:"texture-array-layer",name:"Map Layer",type:"texture",fileKey:"map.png",fileFormat:"png",texture:{textureArrayFileKey:"textures/packed.ktx2",textureArrayLayer:5}}]),new O(new n,5)),a=c(t,{parameters:o});e(a.uniforms.pu_map_i?.value).toBe(5),e(a.fragmentShader).toContain("uniform float pu_map_i")}),t("standard shader height maps support texture array slices",()=>{const t=new C;t.heightMap=new T("heightMap",new n,new B("heightMap_i",3,void 0,!1)),t.map=k(new n),t.heightScale=.05;const o=t.build();e(o.fragmentShader).toContain("uniform sampler2DArray heightMap"),e(o.fragmentShader).toContain("uniform float heightMap_i"),e(o.fragmentShader).toContain("texture(heightMap, vec3(uv + vCurrOffset, heightMapLayer)).r")}),t("standard shader supports particle opacity for instanced VFX materials",()=>{const t=(new C).build();e(t.fragmentShader).toContain("IS_PARTICLE"),e(t.fragmentShader).toContain("particleData")}),t("material graph asset reference and local embedded graph both build materials",async()=>{const t={id:"graph-asset",name:"Reusable Graph",type:"shaderGraph",shaderGraph:g()},o=S("referenced",{source:"asset",assetId:t.id}),a=S("local",{source:"local",graph:g()}),r=new D([t,o,a]),n=new l;e((await s(o,null,r,n,[],!1)).isMaterial).toBe(!0),e((await s(a,null,r,n,[],!1)).isMaterial).toBe(!0)}),t("selected-node preview compiles scalar, vector, color, rgba, and boolean outputs",()=>{const t=[{id:"float",kind:"constant.float",params:{value:.25}},{id:"slider",kind:"input.slider",params:{value:.5}},{id:"vec2",kind:"constant.vec2",params:{value:[1,0]}},{id:"rgb",kind:"constant.vec3",params:{value:[1,.5,0]}},{id:"rgba",kind:"constant.rgba",params:{value:"#ff8800",alpha:.75}},{id:"boolean",kind:"constant.boolean",params:{value:!0}}];for(const o of t){const t=j([o],[],{color:{input:{nodeId:o.id,port:"value"}}});e(f(t,o.id).color).toBeTruthy()}}),t("post-process shader graph builds a fullscreen-safe material",()=>{const t=h(),o=m(t);e(o).toBeInstanceOf(x),e(o.depthWrite).toBe(!1),e(o.depthTest).toBe(!1),e(o.toneMapped).toBe(!1),e(o.transparent).toBe(!1),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[M]).toBeTruthy(),e(o.uniforms[I]).toBeTruthy()}),t("empty post-process shader graph defaults to identity scene color",()=>{const t=h();t.nodes=[],t.edges=[],t.outputs={};const o=v(t),a=m(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!1),e(a.uniforms[M]).toBeTruthy(),e(a.uniforms[I]).toBeTruthy()}),t("post-process color and opacity outputs expose blend weight uniform",()=>{const t=j([{id:"color",kind:"constant.rgba",params:{value:"#ff0044",alpha:1}},{id:"opacity",kind:"constant.float",params:{value:.35}}],[],{color:{input:{nodeId:"color",port:"value"}},opacity:{input:{nodeId:"opacity",port:"value"}},roughness:{value:.8}});t.target="postProcess";const o=v(t),a=m(t);e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.roughness).toBeUndefined(),e(a.uniforms[I]).toBeTruthy()}),t("selected-node preview builds through post-process fullscreen path",()=>{const t=j([{id:"float",kind:"constant.float",params:{value:.25}}],[],{color:{input:{nodeId:"float",port:"value"}}});t.target="postProcess";const o=m(t,{previewNodeId:"float"});e(o).toBeInstanceOf(x),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[M]).toBeTruthy(),e(o.uniforms[I]).toBeTruthy()}),t("texture parameter nodes expose sampled value and channel outputs",()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"albedo"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"albedo",type:"texture",defaultValue:null});const o=v(t,{parameters:{albedo:new n}}),a=o.nodes.get("texture")?.outputs??{};e(Object.keys(a)).toEqual(["value","rgb","r","g","b","a","sampler"]),e(a.value.type).toBe("rgba"),e(a.rgb.type).toBe("rgb"),e(a.r.type).toBe("float"),e(a.g.type).toBe("float"),e(a.b.type).toBe("float"),e(a.a.type).toBe("float"),e(a.sampler.type).toBe("sampler2d"),e(o.output.color).toBeTruthy()}),t("texture parameter nodes accept resolved sampler values",()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=v(t,{parameters:{map:k(new n)}});e(o.output.color).toBeTruthy()}),t("missing texture parameter values warn and use fallback sampler",()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"missingTexture"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"missingTexture",type:"texture",defaultValue:null});const a=o.spyOn(console,"warn").mockImplementation(()=>{});try{const o=v(t),r=o.nodes.get("texture")?.outputs??{};e(r.sampler.type).toBe("sampler2d"),e(o.output.color).toBeTruthy(),e(a).toHaveBeenCalledWith(e.stringContaining("missingTexture"))}finally{a.mockRestore()}}),t("scene depth graph nodes expose sampled and fragment linear eye depth",()=>{const t=j([{id:"uv",kind:"builtin.screenUv"},{id:"sceneDepth",kind:"scene.sampleDepth"},{id:"fragmentDepth",kind:"scene.fragmentDepth"},{id:"depthFade",kind:"math.subtract"}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"sceneDepth",port:"uv"}},{from:{nodeId:"sceneDepth",port:"linearEyeDepth"},to:{nodeId:"depthFade",port:"a"}},{from:{nodeId:"fragmentDepth",port:"linearEyeDepth"},to:{nodeId:"depthFade",port:"b"}}],{opacity:{input:{nodeId:"depthFade",port:"value"}}}),o=v(t);e(o.nodes.get("sceneDepth")?.outputs.depth.type).toBe("float"),e(o.nodes.get("sceneDepth")?.outputs.linearEyeDepth.type).toBe("float"),e(o.nodes.get("fragmentDepth")?.outputs.linearEyeDepth.type).toBe("float"),e(o.output.opacity).toBeTruthy()}),t("screen UV builtin accepts a fullscreen compiler override",()=>{const t=j([{id:"uv",kind:"builtin.screenUv"}],[],{}),o=w(.25,.75),a=v(t,{screenUV:o});e(a.nodes.get("uv")?.outputs.value.value).toBe(o)}),t("unpack normal node compiles with various input combinations",()=>{const t=j([{id:"sample",kind:"constant.vec3",params:{value:[.5,.5,1]}},{id:"scale",kind:"constant.float",params:{value:1}},{id:"normal",kind:"builtin.normal"},{id:"toNormal",kind:"color.unpackNormal"}],[{from:{nodeId:"sample",port:"value"},to:{nodeId:"toNormal",port:"color"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"toNormal",port:"scale"}},{from:{nodeId:"normal",port:"value"},to:{nodeId:"toNormal",port:"normal"}}],{normal:{input:{nodeId:"toNormal",port:"value"}}}),o=v(t);e(o.nodes.get("toNormal")?.outputs.value.type).toBe("vec3"),e(o.output.normal).toBeTruthy()});class D extends p{constructor(e){super(),this.onCreate=null,this.onDelete=null,this.onUpdate=null,this.assets=new Map(e.map(e=>[e.id,e]))}async getAsset(e){return this.assets.get(e)}async getAssets(){return Array.from(this.assets.values())}}class A extends l{async getTexture(){return new n}async getTextureArray(){return{texture:null,layerIndex:null}}}class O extends A{constructor(e,t){super(),this.textureArray=e,this.layerIndex=t}async getTextureArray(){return{texture:this.textureArray,layerIndex:this.layerIndex}}}/*
1
+ import{expect as e,test as t,vi as o}from"vitest";import{DoubleSide as a,FrontSide as r,Texture as n}from"three";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),o.mock("three/examples/jsm/Addons.js",async e=>({...await e(),ConvexHull:class{},SkeletonUtils:{clone:e=>e},RectAreaLightHelper:class{}})),o.mock("three-shader-graph",async e=>{const t=await e();return{...t,rgb:(e,o,a)=>null==o&&null==a&&!0===e?.isColor&&"function"==typeof e.getHexString?t.rgb("#"+e.getHexString()):t.rgb(e,o,a)}}),o.mock("../rendering.js",()=>({RenderingView:class{constructor(){this._id="test",this.csm={setupMaterial:o.fn()}}getEnvTexture(e){return e}},setRenderingPaused:o.fn()}));import{materialFromAsset as s,prepareCustomParamsFromShaderGraph as d,prepareShaderGraphParameters as u}from"../scene/materializer.js";import{SerializedParamType as i}from"../scene/model.js";import{AssetsProvider as l}from"../scene/assets-provider.js";import{AssetResourceLoader as p}from"../scene/asset-resource-loader.js";import{buildShaderGraphMaterial as c,buildShaderGraphPostProcessMaterial as m,compileShaderGraph as v,compileShaderGraphPreview as f,createDefaultPostProcessShaderGraphDocument as h,createDefaultShaderGraphDocument as g,createDefaultVfxShaderGraphDocument as y,shaderGraphPostProcessWeightUniformName as I,shaderGraphParametersToPropertyParameters as b}from"../shader/graph/index.js";import{NodeShaderMaterial as x,textureSampler2d as k,UniformFloatNode as B,UniformSampler2dArraySlice as T,vec2 as w}from"../shader-nodes/index.js";import{StandardShader as C}from"../shader/builtin/standard-shader.js";import{sceneMapUniformName as M}from"../shader-nodes/scene-sample.js";function j(e,t,o){return{version:1,target:"surface",parameters:[],nodes:e,edges:t,outputs:o}}function S(e,t){return{id:e,name:e,type:"material",material:{type:"shaderGraph",side:r,params:{},shaderParams:{},shaderGraph:t}}}t("graph validation rejects missing nodes, invalid ports, and incompatible edges",()=>{const t=g();t.outputs.color={input:{nodeId:"missing",port:"value"}},e(()=>v(t)).toThrow(/Missing shader graph node/);const o=g();o.outputs.color={input:{nodeId:"base-color",port:"missing"}},e(()=>v(o)).toThrow(/does not have output/);const a=j([{id:"a",kind:"constant.vec2",params:{value:[1,1]}},{id:"b",kind:"constant.vec3",params:{value:[1,1,1]}},{id:"add",kind:"math.add"}],[{from:{nodeId:"a",port:"value"},to:{nodeId:"add",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"add",port:"b"}}],{color:{input:{nodeId:"add",port:"value"}}});e(()=>v(a)).toThrow(/Cannot add vec2 and vec3/)}),t("scene lighting nodes expose renderer ambient and directional light values",()=>{const t=j([{id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"}],[],{color:{input:{nodeId:"ambient",port:"color"}}}),o=v(t);e(o.nodes.get("ambient")?.outputs.color.type).toBe("rgb"),e(o.nodes.get("directional")?.outputs.color.type).toBe("rgb"),e(o.nodes.get("directional")?.outputs.direction.type).toBe("vec3");const a=c(t);e(a.fragmentShader).toContain("ambientLightColor"),t.outputs.color={input:{nodeId:"directional",port:"color"}};const r=c(t);e(r.fragmentShader).toContain("directionalLights"),e(r.fragmentShader).toContain("NUM_DIR_LIGHTS"),e(r.fragmentShader).toMatch(/max\s*\(\s*float\s*\(\s*NUM_DIR_LIGHTS\s*\)/)}),t("named reroutes pass values through declarations to multiple usages",()=>{const t=j([{id:"source",kind:"constant.float",params:{value:.75}},{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Mask"}},{id:"usage-a",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}},{id:"usage-b",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}},{id:"add",kind:"math.add"}],[{from:{nodeId:"source",port:"value"},to:{nodeId:"declaration",port:"value"}},{from:{nodeId:"usage-a",port:"value"},to:{nodeId:"add",port:"a"}},{from:{nodeId:"usage-b",port:"value"},to:{nodeId:"add",port:"b"}}],{color:{input:{nodeId:"add",port:"value"}}}),o=v(t),a=o.nodes.get("declaration")?.outputs.value;e(a?.type).toBe("float"),e(o.nodes.get("usage-a")?.outputs.value).toBe(a),e(o.nodes.get("usage-b")?.outputs.value).toBe(a),e(o.output.color).toBeTruthy()}),t("named reroutes preserve dynamic vector types",()=>{const t=j([{id:"source",kind:"constant.vec3",params:{value:[1,2,3]}},{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Direction"}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}}],[{from:{nodeId:"source",port:"value"},to:{nodeId:"declaration",port:"value"}}],{color:{input:{nodeId:"usage",port:"value"}}}),o=v(t);e(o.nodes.get("usage")?.outputs.value.type).toBe("vec3")}),t("named reroute validation rejects missing and invalid declarations",()=>{const t=j([{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"missing"}}],[],{});e(()=>v(t)).toThrow(/declaration "missing" does not exist/);const o=j([{id:"not-declaration",kind:"constant.float",params:{value:1}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"not-declaration"}}],[],{});e(()=>v(o)).toThrow(/not a named reroute declaration/)}),t("named reroute cycles report a compile error",()=>{const t=j([{id:"declaration",kind:"utility.namedRerouteDeclaration",params:{name:"Cycle"}},{id:"usage",kind:"utility.namedRerouteUsage",params:{declarationId:"declaration"}}],[{from:{nodeId:"usage",port:"value"},to:{nodeId:"declaration",port:"value"}}],{color:{input:{nodeId:"usage",port:"value"}}});e(()=>v(t)).toThrow(/contains a cycle/)}),t("surface graph compiles to NodeShaderOutput",()=>{const t=v(g("surface"));e(t.output.color).toBeTruthy(),e(t.output.transparent).toBe(!1)}),t("inline literal inputs compile without separate constant nodes",()=>{const t=j([{id:"uv",kind:"builtin.uv"},{id:"scale-uv",kind:"math.multiply",inputs:{b:{value:2,valueType:"float"}}},{id:"rotate",kind:"uv.rotate",inputs:{angle:{value:.5,valueType:"float"},center:{value:[.25,.75],valueType:"vec2"}}},{id:"translate",kind:"transform.translate",inputs:{offset:{value:[0,1,0],valueType:"vec3"}}}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"scale-uv",port:"a"}},{from:{nodeId:"scale-uv",port:"value"},to:{nodeId:"rotate",port:"uv"}}],{color:{input:{nodeId:"rotate",port:"value"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.nodes.get("scale-uv")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("rotate")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("translate")?.outputs.value.type).toBe("mat4"),e(o.output.color).toBeTruthy(),e(o.output.transform).toBeTruthy()}),t("uv attribute node supports selecting non-default uv channels",()=>{const t=j([{id:"uv",kind:"builtin.uv",params:{channel:2}}],[],{color:{input:{nodeId:"uv",port:"value"}}}),o=v(t);e(o.nodes.get("uv")?.outputs.value.type).toBe("vec2");const a=c(t);e(a.fragmentShader).toContain("uv2")}),t.each(["surface","decal"])("world normal node compiles for %s graphs",t=>{const o=j([{id:"world-normal",kind:"builtin.worldNormal"}],[],{color:{input:{nodeId:"world-normal",port:"value"}}});o.target=t;const a=v(o);e(a.nodes.get("world-normal")?.outputs.value.type).toBe("vec3"),e(c(o).fragmentShader).toBeTruthy()}),t("sprite graph compiles with sprite-compatible material output",()=>{const t=g("sprite"),o=v(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!0)}),t("default VFX shader graphs compile mapped targets and build non-sprite materials",()=>{for(const t of["sprite","surface","decal","trail"]){const o=y(t);if(e(v(o).output.color).toBeTruthy(),"sprite"===t)continue;const a=c(o,{trailBillboard:"trail"===t});e(a.isMaterial).toBe(!0)}}),t("graph material options apply to supported material targets",()=>{const t=g("surface");t.materialOptions={side:"double",transparent:!0};const o=c(t);e(o.side).toBe(a),e(o.transparent).toBe(!0);const r=g("decal");r.materialOptions={transparent:!0};const n=c(r);e(n.transparent).toBe(!0)}),t("standard graph compiles extended material outputs and vertex transform",()=>{const t=j([{id:"metalness",kind:"constant.float",params:{value:.8}},{id:"ao",kind:"constant.float",params:{value:.5}},{id:"sheen",kind:"constant.vec3",params:{value:[.1,.2,.3]}},{id:"offset",kind:"constant.vec3",params:{value:[0,1,0]}},{id:"translate",kind:"transform.translate"}],[{from:{nodeId:"offset",port:"value"},to:{nodeId:"translate",port:"offset"}}],{metalness:{input:{nodeId:"metalness",port:"value"}},ambientOcclusion:{input:{nodeId:"ao",port:"value"}},sheenColor:{input:{nodeId:"sheen",port:"value"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.output.metalness).toBeTruthy(),e(o.output.ambientOcclusion).toBeTruthy(),e(o.output.sheenColor).toBeTruthy(),e(o.output.transform).toBeTruthy();const a=c(t);e(a.outputMetalness).toBeTruthy(),e(a.outputAmbientOcclusion).toBeTruthy(),e(a.outputTransform).toBeTruthy()}),t("math multiply composes transform matrices",()=>{const t=j([{id:"translate",kind:"transform.translate",inputs:{offset:{value:[0,1,0],valueType:"vec3"}}},{id:"scale",kind:"transform.scale",inputs:{scale:{value:[2,2,2],valueType:"vec3"}}},{id:"compose",kind:"math.multiply"}],[{from:{nodeId:"translate",port:"value"},to:{nodeId:"compose",port:"a"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"compose",port:"b"}}],{transform:{input:{nodeId:"compose",port:"value"}}}),o=v(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("mat4"),e(o.output.transform).toBeTruthy(),e(c(t).outputTransform).toBeTruthy()}),t("matrix input nodes compile as mat4 uniforms",()=>{for(const t of["builtin.viewMatrix","builtin.projectionMatrix","builtin.modelViewMatrix"]){const o=j([{id:"matrix",kind:t}],[],{transform:{input:{nodeId:"matrix",port:"value"}}}),a=v(o);e(a.nodes.get("matrix")?.outputs.value.type).toBe("mat4"),e(a.output.transform).toBeTruthy()}}),t("compose vec4 node compiles from float components",()=>{const t=j([{id:"compose",kind:"compose.vec4",inputs:{x:{value:.1},y:{value:.2},z:{value:.3},w:{value:1}}}],[],{color:{input:{nodeId:"compose",port:"value"}}}),o=v(t);e(o.nodes.get("compose")?.outputs.value.type).toBe("vec4"),e(o.output.color).toBeTruthy()}),t("atan2 node compiles from y and x inputs",()=>{const t=j([{id:"atan2",kind:"math.atan2",inputs:{y:{value:1},x:{value:0}}}],[],{color:{input:{nodeId:"atan2",port:"value"}}}),o=v(t);e(o.nodes.get("atan2")?.outputs.value.type).toBe("float"),e(o.output.color).toBeTruthy()}),t("built-in attribute nodes compile for fragment and transform outputs",()=>{const t=j([{id:"color",kind:"builtin.color"},{id:"normal",kind:"builtin.normal"},{id:"position",kind:"builtin.position"},{id:"worldPosition",kind:"builtin.worldPosition"},{id:"objectPosition",kind:"builtin.objectPosition"},{id:"translate",kind:"transform.translate"}],[{from:{nodeId:"objectPosition",port:"value"},to:{nodeId:"translate",port:"offset"}}],{color:{input:{nodeId:"color",port:"value"}},normal:{input:{nodeId:"normal",port:"value"}},roughness:{input:{nodeId:"position",port:"x"}},metalness:{input:{nodeId:"worldPosition",port:"x"}},transform:{input:{nodeId:"translate",port:"value"}}}),o=v(t);e(o.nodes.get("color")?.outputs.value.type).toBe("rgba"),e(o.nodes.get("normal")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("position")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("worldPosition")?.outputs.value.type).toBe("vec3"),e(o.nodes.get("objectPosition")?.outputs.value.type).toBe("vec3"),e(o.output.color).toBeTruthy(),e(o.output.normal).toBeTruthy(),e(o.output.transform).toBeTruthy(),e(c(t).outputTransform).toBeTruthy()}),t("painted layer mix node compiles color layers and builds a material",()=>{const t=j([{id:"base",kind:"constant.rgba",params:{value:"#224422",alpha:1}},{id:"grass",kind:"constant.rgba",params:{value:"#55aa33",alpha:1}},{id:"stone",kind:"constant.rgba",params:{value:"#777777",alpha:1}},{id:"mix",kind:"layer.mixPainted",params:{mode:"soft",enableNoise:!0,softness:.25,noiseScale:.2,noiseAmount:.4}}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"grass",port:"value"},to:{nodeId:"mix",port:"layer1"}},{from:{nodeId:"stone",port:"value"},to:{nodeId:"mix",port:"layer3"}}],{color:{input:{nodeId:"mix",port:"value"}}}),o=v(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(c(t).isMaterial).toBe(!0)}),t("painted layer mix node compiles sparse float layers in hard and soft modes",()=>{for(const t of["hard","soft"]){const o=j([{id:"base",kind:"constant.float",params:{value:.8}},{id:"mud",kind:"constant.float",params:{value:1}},{id:"wet",kind:"constant.float",params:{value:.25}},{id:"mix",kind:"layer.mixPainted",params:{mode:t,enableNoise:!1,softness:.3,noiseScale:.1,noiseAmount:.5}}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"mud",port:"value"},to:{nodeId:"mix",port:"layer2"}},{from:{nodeId:"wet",port:"value"},to:{nodeId:"mix",port:"layer8"}}],{roughness:{input:{nodeId:"mix",port:"value"}}}),a=v(o);e(a.nodes.get("mix")?.outputs.value.type).toBe("float"),e(a.output.roughness).toBeTruthy()}}),t("painted layer mix node rejects incompatible layer types",()=>{const t=j([{id:"base",kind:"constant.vec2",params:{value:[0,1]}},{id:"layer",kind:"constant.vec3",params:{value:[1,0,0]}},{id:"mix",kind:"layer.mixPainted"}],[{from:{nodeId:"base",port:"value"},to:{nodeId:"mix",port:"base"}},{from:{nodeId:"layer",port:"value"},to:{nodeId:"mix",port:"layer1"}}],{color:{input:{nodeId:"mix",port:"value"}}});e(()=>v(t)).toThrow(/Cannot mix vec2 and vec3/)}),t("additional math, uv, shape, noise, and effect nodes compile",()=>{const t=j([{id:"uv",kind:"builtin.uv"},{id:"angle",kind:"constant.float",params:{value:.5}},{id:"scale",kind:"constant.float",params:{value:4}},{id:"half",kind:"constant.float",params:{value:.5}},{id:"axis",kind:"constant.vec3",params:{value:[0,1,0]}},{id:"rotatedUv",kind:"uv.rotate"},{id:"twirl",kind:"uv.twirl"},{id:"bulge",kind:"uv.bulge"},{id:"radial",kind:"uv.radial"},{id:"flipbook",kind:"uv.flipbook"},{id:"shape",kind:"shape.roundedRectangle"},{id:"noise",kind:"noise.simplex"},{id:"voronoi",kind:"noise.voronoi2d"},{id:"fresnel",kind:"effect.fresnel"},{id:"sin",kind:"math.sin"},{id:"pow",kind:"math.pow"},{id:"mix",kind:"math.mix"},{id:"rotateAxis",kind:"transform.rotateAxis"}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"rotatedUv",port:"uv"}},{from:{nodeId:"angle",port:"value"},to:{nodeId:"rotatedUv",port:"angle"}},{from:{nodeId:"rotatedUv",port:"value"},to:{nodeId:"twirl",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"twirl",port:"strength"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"bulge",port:"uv"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"bulge",port:"power"}},{from:{nodeId:"bulge",port:"value"},to:{nodeId:"radial",port:"uv"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"flipbook",port:"uv"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"shape",port:"uv"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"width"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"height"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"shape",port:"radius"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"noise",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"noise",port:"scale"}},{from:{nodeId:"twirl",port:"value"},to:{nodeId:"voronoi",port:"uv"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"voronoi",port:"scale"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"fresnel",port:"power"}},{from:{nodeId:"radial",port:"angle"},to:{nodeId:"sin",port:"value"}},{from:{nodeId:"noise",port:"value"},to:{nodeId:"pow",port:"a"}},{from:{nodeId:"half",port:"value"},to:{nodeId:"pow",port:"b"}},{from:{nodeId:"shape",port:"value"},to:{nodeId:"mix",port:"a"}},{from:{nodeId:"voronoi",port:"value"},to:{nodeId:"mix",port:"b"}},{from:{nodeId:"fresnel",port:"value"},to:{nodeId:"mix",port:"t"}},{from:{nodeId:"axis",port:"value"},to:{nodeId:"rotateAxis",port:"axis"}},{from:{nodeId:"angle",port:"value"},to:{nodeId:"rotateAxis",port:"angle"}}],{color:{input:{nodeId:"mix",port:"value"}},opacity:{input:{nodeId:"sin",port:"value"}},anisotropyDirection:{input:{nodeId:"flipbook",port:"value"}},transform:{input:{nodeId:"rotateAxis",port:"value"}}}),o=v(t);e(o.nodes.get("mix")?.outputs.value.type).toBe("float"),e(o.nodes.get("twirl")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("bulge")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("radial")?.outputs.coords.type).toBe("vec2"),e(o.nodes.get("flipbook")?.outputs.value.type).toBe("vec2"),e(o.nodes.get("rotateAxis")?.outputs.value.type).toBe("mat4"),e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.transform).toBeTruthy()}),t("slider input node outputs a clamped float value",()=>{const t=j([{id:"slider",kind:"input.slider",params:{value:1.25}}],[],{roughness:{input:{nodeId:"slider",port:"value"}}}),o=v(t);e(o.nodes.get("slider")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy()}),t("comparison and boolean graph nodes compile to boolean outputs",()=>{const t=j([{id:"a",kind:"constant.float",params:{value:.75}},{id:"b",kind:"constant.float",params:{value:.5}},{id:"greater",kind:"compare.greaterThan"},{id:"lessEqual",kind:"compare.lessThanOrEqual"},{id:"not",kind:"boolean.not"},{id:"and",kind:"boolean.and"},{id:"or",kind:"boolean.or"}],[{from:{nodeId:"a",port:"value"},to:{nodeId:"greater",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"greater",port:"b"}},{from:{nodeId:"a",port:"value"},to:{nodeId:"lessEqual",port:"a"}},{from:{nodeId:"b",port:"value"},to:{nodeId:"lessEqual",port:"b"}},{from:{nodeId:"lessEqual",port:"value"},to:{nodeId:"not",port:"value"}},{from:{nodeId:"greater",port:"value"},to:{nodeId:"and",port:"a"}},{from:{nodeId:"not",port:"value"},to:{nodeId:"and",port:"b"}},{from:{nodeId:"and",port:"value"},to:{nodeId:"or",port:"a"}},{from:{nodeId:"lessEqual",port:"value"},to:{nodeId:"or",port:"b"}}],{discard:{input:{nodeId:"or",port:"value"}}}),o=v(t);e(o.nodes.get("greater")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("lessEqual")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("not")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("and")?.outputs.value.type).toBe("boolean"),e(o.nodes.get("or")?.outputs.value.type).toBe("boolean"),e(o.output.discard).toBeTruthy()}),t("split component node exposes vector and color channels as separate outputs",()=>{const t=j([{id:"vec",kind:"constant.vec3",params:{value:[.2,.4,.6]}},{id:"color",kind:"constant.rgba",params:{value:"#336699",alpha:.75}},{id:"splitVec",kind:"decompose.split"},{id:"splitColor",kind:"decompose.split"},{id:"sum",kind:"math.add"}],[{from:{nodeId:"vec",port:"value"},to:{nodeId:"splitVec",port:"value"}},{from:{nodeId:"color",port:"value"},to:{nodeId:"splitColor",port:"value"}},{from:{nodeId:"splitVec",port:"z"},to:{nodeId:"sum",port:"a"}},{from:{nodeId:"splitColor",port:"a"},to:{nodeId:"sum",port:"b"}}],{roughness:{input:{nodeId:"sum",port:"value"}}}),o=v(t);e(Object.keys(o.nodes.get("splitVec")?.outputs??{})).toEqual(["x","y","z"]),e(Object.keys(o.nodes.get("splitColor")?.outputs??{})).toEqual(["r","g","b","a","x","y","z","w"]),e(o.nodes.get("splitVec")?.outputs.z.type).toBe("float"),e(o.nodes.get("splitColor")?.outputs.a.type).toBe("float"),e(o.output.roughness).toBeTruthy()}),t("color gradient node compiles editable stops to a sampled color",()=>{const t=j([{id:"threshold",kind:"constant.float",params:{value:.75}},{id:"gradient",kind:"color.gradient",params:{stops:[{position:0,color:"#000000",alpha:1},{position:.5,color:"#ff0000",alpha:1},{position:1,color:"#ffffff",alpha:1}]}}],[{from:{nodeId:"threshold",port:"value"},to:{nodeId:"gradient",port:"t"}}],{color:{input:{nodeId:"gradient",port:"value"}}}),o=v(t);e(o.nodes.get("gradient")?.outputs.value.type).toBe("rgba"),e(o.output.color).toBeTruthy(),e(f(t,"gradient").color).toBeTruthy()}),t("curve node compiles editable curve to a sampled float",()=>{const t=j([{id:"t",kind:"constant.float",params:{value:.75}},{id:"curve",kind:"utility.curve",params:{curve:"InOutQuad"}}],[{from:{nodeId:"t",port:"value"},to:{nodeId:"curve",port:"t"}}],{roughness:{input:{nodeId:"curve",port:"value"}}}),o=v(t);e(o.nodes.get("curve")?.outputs.value.type).toBe("float"),e(o.output.roughness).toBeTruthy(),e(f(t,"curve").color).toBeTruthy()}),t("graph parameters convert to property params and custom defaults",()=>{const t=g();t.parameters.push({name:"opacity",type:"float",group:"Surface",defaultValue:.5});const o=b(t);e(o.map(e=>e.name)).toEqual(["baseColor","opacity"]),e(o.every(e=>!0===e.options.optional)).toBe(!0),e(o.map(e=>e.options.group??null)).toEqual([null,"Surface"]);const a=d(t);e(a.baseColor).toMatchObject({type:i.RgbNode,value:"#ffffff",override:!1}),e(a.opacity).toMatchObject({type:i.FloatNode,value:.5,override:!1})}),t("existing shader graph param values keep overriding unless explicitly disabled",()=>{const t=g(),o=d(t,{baseColor:{type:i.RgbNode,value:"#ff0000"}});e(o.baseColor).toMatchObject({type:i.RgbNode,value:"#ff0000",override:!0});const a=d(t,{baseColor:{type:i.RgbNode,value:"#00ff00",override:!1}});e(a.baseColor).toMatchObject({type:i.RgbNode,value:"#ffffff",override:!1})}),t("texture graph parameter defaults serialize as sampler parameters",()=>{const t=j([],[],{});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=d(t);e(o.map).toMatchObject({type:i.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph params keep graph-declared serialized types over previous string params",()=>{const t=g();t.parameters.push({name:"map",type:"texture",defaultValue:null});const o=d(t,{baseColor:{type:i.String,value:"#ff0000"},map:{type:i.String,value:"texture-asset-id"}});e(o.baseColor).toMatchObject({type:i.RgbNode,value:"#ff0000"}),e(o.map).toMatchObject({type:i.Sampler2DNode,value:"texture-asset-id"})}),t("shader graph texture params resolve asset ids before compile",async()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=await u({},t,new D([{id:"texture-asset-id",name:"Map",type:"texture",fileKey:"map.png",fileFormat:"png"}]),new O);e(o.map).toBeTruthy(),e(typeof o.map).not.toBe("string"),e(v(t,{parameters:o}).output.color).toBeTruthy()}),t("shader graph requests all texture parameters before waiting for decoding",async()=>{const t=g();let a;t.parameters=["colorMap","normalMap","controlMap"].map(e=>({name:e,type:"texture",defaultValue:e}));const r=new Promise(e=>{a=e}),s=o.fn(async()=>(await r,new n)),d=u({},t,{getAsset:async e=>({id:e,type:"texture"})},{getTexture:s});await Promise.resolve(),await Promise.resolve(),e(s).toHaveBeenCalledTimes(3),a();const i=await d;e(Object.keys(i)).toEqual(["colorMap","normalMap","controlMap"]),e(Object.values(i).every(e=>null!=e)).toBe(!0)}),t("shader graph texture array params expose named slice uniforms",async()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-array-layer"});const o=await u({},t,new D([{id:"texture-array-layer",name:"Map Layer",type:"texture",fileKey:"map.png",fileFormat:"png",texture:{textureArrayFileKey:"textures/packed.ktx2",textureArrayLayer:5}}]),new P(new n,5)),a=c(t,{parameters:o});e(a.uniforms.pu_map_i?.value).toBe(5),e(a.fragmentShader).toContain("uniform float pu_map_i")}),t("standard shader height maps support texture array slices",()=>{const t=new C;t.heightMap=new T("heightMap",new n,new B("heightMap_i",3,void 0,!1)),t.map=k(new n),t.heightScale=.05;const o=t.build();e(o.fragmentShader).toContain("uniform sampler2DArray heightMap"),e(o.fragmentShader).toContain("uniform float heightMap_i"),e(o.fragmentShader).toContain("texture(heightMap, vec3(uv + vCurrOffset, heightMapLayer)).r")}),t("standard shader supports particle opacity for instanced VFX materials",()=>{const t=(new C).build();e(t.fragmentShader).toContain("IS_PARTICLE"),e(t.fragmentShader).toContain("particleData")}),t("material graph asset reference and local embedded graph both build materials",async()=>{const t={id:"graph-asset",name:"Reusable Graph",type:"shaderGraph",shaderGraph:g()},o=S("referenced",{source:"asset",assetId:t.id}),a=S("local",{source:"local",graph:g()}),r=new D([t,o,a]),n=new p;e((await s(o,null,r,n,[],!1)).isMaterial).toBe(!0),e((await s(a,null,r,n,[],!1)).isMaterial).toBe(!0)}),t("selected-node preview compiles scalar, vector, color, rgba, and boolean outputs",()=>{const t=[{id:"float",kind:"constant.float",params:{value:.25}},{id:"slider",kind:"input.slider",params:{value:.5}},{id:"vec2",kind:"constant.vec2",params:{value:[1,0]}},{id:"rgb",kind:"constant.vec3",params:{value:[1,.5,0]}},{id:"rgba",kind:"constant.rgba",params:{value:"#ff8800",alpha:.75}},{id:"boolean",kind:"constant.boolean",params:{value:!0}}];for(const o of t){const t=j([o],[],{color:{input:{nodeId:o.id,port:"value"}}});e(f(t,o.id).color).toBeTruthy()}}),t("post-process shader graph builds a fullscreen-safe material",()=>{const t=h(),o=m(t);e(o).toBeInstanceOf(x),e(o.depthWrite).toBe(!1),e(o.depthTest).toBe(!1),e(o.toneMapped).toBe(!1),e(o.transparent).toBe(!1),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[M]).toBeTruthy(),e(o.uniforms[I]).toBeTruthy()}),t("empty post-process shader graph defaults to identity scene color",()=>{const t=h();t.nodes=[],t.edges=[],t.outputs={};const o=v(t),a=m(t);e(o.output.color).toBeTruthy(),e(o.output.transparent).toBe(!1),e(a.uniforms[M]).toBeTruthy(),e(a.uniforms[I]).toBeTruthy()}),t("post-process color and opacity outputs expose blend weight uniform",()=>{const t=j([{id:"color",kind:"constant.rgba",params:{value:"#ff0044",alpha:1}},{id:"opacity",kind:"constant.float",params:{value:.35}}],[],{color:{input:{nodeId:"color",port:"value"}},opacity:{input:{nodeId:"opacity",port:"value"}},roughness:{value:.8}});t.target="postProcess";const o=v(t),a=m(t);e(o.output.color).toBeTruthy(),e(o.output.opacity).toBeTruthy(),e(o.output.roughness).toBeUndefined(),e(a.uniforms[I]).toBeTruthy()}),t("selected-node preview builds through post-process fullscreen path",()=>{const t=j([{id:"float",kind:"constant.float",params:{value:.25}}],[],{color:{input:{nodeId:"float",port:"value"}}});t.target="postProcess";const o=m(t,{previewNodeId:"float"});e(o).toBeInstanceOf(x),e(o.userData.isPostProcessShaderGraph).toBe(!0),e(o.uniforms[M]).toBeTruthy(),e(o.uniforms[I]).toBeTruthy()}),t("texture parameter nodes expose sampled value and channel outputs",()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"albedo"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"albedo",type:"texture",defaultValue:null});const o=v(t,{parameters:{albedo:new n}}),a=o.nodes.get("texture")?.outputs??{};e(Object.keys(a)).toEqual(["value","rgb","r","g","b","a","sampler"]),e(a.value.type).toBe("rgba"),e(a.rgb.type).toBe("rgb"),e(a.r.type).toBe("float"),e(a.g.type).toBe("float"),e(a.b.type).toBe("float"),e(a.a.type).toBe("float"),e(a.sampler.type).toBe("sampler2d"),e(o.output.color).toBeTruthy()}),t("texture parameter nodes accept resolved sampler values",()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"map"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"map",type:"texture",defaultValue:"texture-asset-id"});const o=v(t,{parameters:{map:k(new n)}});e(o.output.color).toBeTruthy()}),t("missing texture parameter values warn and use fallback sampler",()=>{const t=j([{id:"texture",kind:"input.parameter",params:{parameter:"missingTexture"}}],[],{color:{input:{nodeId:"texture",port:"value"}}});t.parameters.push({name:"missingTexture",type:"texture",defaultValue:null});const a=o.spyOn(console,"warn").mockImplementation(()=>{});try{const o=v(t),r=o.nodes.get("texture")?.outputs??{};e(r.sampler.type).toBe("sampler2d"),e(o.output.color).toBeTruthy(),e(a).toHaveBeenCalledWith(e.stringContaining("missingTexture"))}finally{a.mockRestore()}}),t("scene depth graph nodes expose sampled and fragment linear eye depth",()=>{const t=j([{id:"uv",kind:"builtin.screenUv"},{id:"sceneDepth",kind:"scene.sampleDepth"},{id:"fragmentDepth",kind:"scene.fragmentDepth"},{id:"depthFade",kind:"math.subtract"}],[{from:{nodeId:"uv",port:"value"},to:{nodeId:"sceneDepth",port:"uv"}},{from:{nodeId:"sceneDepth",port:"linearEyeDepth"},to:{nodeId:"depthFade",port:"a"}},{from:{nodeId:"fragmentDepth",port:"linearEyeDepth"},to:{nodeId:"depthFade",port:"b"}}],{opacity:{input:{nodeId:"depthFade",port:"value"}}}),o=v(t);e(o.nodes.get("sceneDepth")?.outputs.depth.type).toBe("float"),e(o.nodes.get("sceneDepth")?.outputs.linearEyeDepth.type).toBe("float"),e(o.nodes.get("fragmentDepth")?.outputs.linearEyeDepth.type).toBe("float"),e(o.output.opacity).toBeTruthy()}),t("screen UV builtin accepts a fullscreen compiler override",()=>{const t=j([{id:"uv",kind:"builtin.screenUv"}],[],{}),o=w(.25,.75),a=v(t,{screenUV:o});e(a.nodes.get("uv")?.outputs.value.value).toBe(o)}),t("unpack normal node compiles with various input combinations",()=>{const t=j([{id:"sample",kind:"constant.vec3",params:{value:[.5,.5,1]}},{id:"scale",kind:"constant.float",params:{value:1}},{id:"normal",kind:"builtin.normal"},{id:"toNormal",kind:"color.unpackNormal"}],[{from:{nodeId:"sample",port:"value"},to:{nodeId:"toNormal",port:"color"}},{from:{nodeId:"scale",port:"value"},to:{nodeId:"toNormal",port:"scale"}},{from:{nodeId:"normal",port:"value"},to:{nodeId:"toNormal",port:"normal"}}],{normal:{input:{nodeId:"toNormal",port:"value"}}}),o=v(t);e(o.nodes.get("toNormal")?.outputs.value.type).toBe("vec3"),e(o.output.normal).toBeTruthy()});class D extends l{constructor(e){super(),this.onCreate=null,this.onDelete=null,this.onUpdate=null,this.assets=new Map(e.map(e=>[e.id,e]))}async getAsset(e){return this.assets.get(e)}async getAssets(){return Array.from(this.assets.values())}}class O extends p{async getTexture(){return new n}async getTextureArray(){return{texture:null,layerIndex:null}}}class P extends O{constructor(e,t){super(),this.textureArray=e,this.layerIndex=t}async getTextureArray(){return{texture:this.textureArray,layerIndex:this.layerIndex}}}/*
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{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)})});/*
1
+ import{describe as e,expect as t,it as n,vi as o}from"vitest";import*as a from"three";import{NodeShaderMaterial as s}from"three-shader-graph";import{RenderingView as r}from"../rendering.js";import{createVfxRenderPreparation as i,prepareVfxRendering as c}from"../effects/vfx/vfx-render-preparation.js";import{StretchedSprite as l}from"../effects/vfx/stretched-sprite.js";import{materializeVfx as d}from"../effects/vfx/vfx-materializer.js";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&o.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),e("VFX rendering preload",()=>{n("batches concurrent effects and keeps their probe resources alive until compilation finishes",async()=>{let e;const n=new Promise(t=>{e=t}),r=o.fn(e=>n),i={compileAsync:r,activeBackend:"webgpu"},l={getCamera:()=>new a.PerspectiveCamera},d=new s({}),m=new a.PlaneGeometry,p=[0,1].map(()=>c([new a.Mesh(m,d)],l,i));await new Promise(e=>setTimeout(e,5)),t(r).toHaveBeenCalledTimes(1);const f=r.mock.calls[0][0];t(f.children).toHaveLength(2);let v=0;f.traverse(e=>{e instanceof a.Mesh&&v++}),t(v).toBe(2);const u=f.children[0].children[0],w=o.spyOn(u.geometry,"dispose"),y=o.spyOn(m,"dispose"),h=o.spyOn(d,"dispose");t(w).not.toHaveBeenCalled(),e(),await Promise.all(p),t(w).toHaveBeenCalledOnce(),t(y).not.toHaveBeenCalled(),t(h).not.toHaveBeenCalled(),t(f.children).toHaveLength(0)}),n("rejects every effect in a failed batch and allows subsequent preparation",async()=>{const e=o.fn().mockRejectedValueOnce(new Error("compile failed")).mockResolvedValue(void 0),n=o.fn(),s={compileAsync:e,initTextures:n,activeBackend:"webgl"},r={getCamera:()=>new a.PerspectiveCamera},i=await Promise.allSettled([0,1].map(()=>c([],r,s)));t(i.map(e=>e.status)).toEqual(["rejected","rejected"]),t(e).toHaveBeenCalledTimes(1),t(n).not.toHaveBeenCalled(),await c([],r,s),t(e).toHaveBeenCalledTimes(2),t(n).toHaveBeenCalledOnce()}),n("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"}}),n={type:"vfx",id:"test",vfx:{emitters:[e("root",[e("child",[e("grandchild")])])]}},s={getCamera:()=>new a.PerspectiveCamera},r=await d(n,new a.Object3D,{getTexture:o.fn(async()=>new a.Texture)},s,{},{},{},{});t(r.renderBodies).toHaveLength(3),t(r.system.getCount()).toBe(0);const c=i(r.renderBodies,s);t(c.scene.children).toHaveLength(3),t(r.system.getCount()).toBe(0),c.dispose(),r.dispose(),r.system.destroy()}),n("prepares mesh, sprite and stretched output variants without disposing source resources",()=>{const e=new s({});e.transparent=!0;const n=new a.PlaneGeometry,r=new a.Mesh(n,e),c=new l(new a.PlaneGeometry,new s({})),d=new a.Sprite(new a.SpriteMaterial),m=o.spyOn(n,"dispose"),p=o.spyOn(e,"dispose"),f=i([r,c,d],{getCamera:()=>new a.PerspectiveCamera}),v=[];f.scene.traverse(e=>{(e instanceof a.Mesh||e instanceof a.Sprite)&&v.push(e)}),t(v).toHaveLength(3);const u=v.find(e=>e instanceof a.InstancedMesh);t(u.count).toBe(1),t(u.material).toBe(e),t(u.geometry.getAttribute("particleData")).toBeDefined(),t(u.geometry).not.toBe(n),f.dispose(),t(m).not.toHaveBeenCalled(),t(p).not.toHaveBeenCalled(),t(f.scene.children).toHaveLength(0)}),n("waits for every concurrent compilation and recovers after a failed request",async()=>{let e;const n=new Promise(t=>{e=t}),s=o.fn().mockImplementationOnce(()=>n).mockRejectedValueOnce(new Error("compile failed")).mockResolvedValue(void 0),i=Object.create(r.prototype);i.compileTail=Promise.resolve(),i.compileSceneAsync=s;const c=[new a.Scene,new a.Scene,new a.Scene],l=c.map(e=>i.compileAsync(e)),d=Promise.allSettled(l);await Promise.resolve(),await Promise.resolve(),t(s).toHaveBeenCalledTimes(1),e();const m=await d;t(s.mock.calls.map(e=>e[0])).toEqual(c),t(m.map(e=>e.status)).toEqual(["fulfilled","rejected","fulfilled"]),t(i.compileInProgress).toBe(!1)})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hology/core",
3
- "version": "0.0.270",
3
+ "version": "0.0.271",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -64,7 +64,8 @@
64
64
  "./effects/vfx/vfx-runtime": "./dist/effects/vfx/vfx-runtime.js",
65
65
  "./effects/vfx": "./dist/effects/vfx/index.js",
66
66
  "./config/project-config": "./dist/config/project-config.js",
67
- "./worker": "./dist/worker/index.js"
67
+ "./worker": "./dist/worker/index.js",
68
+ "./navigation/bake": "./dist/navigation/bake.js"
68
69
  },
69
70
  "types": "./dist/index.d.ts",
70
71
  "typesVersions": {
@@ -203,6 +204,9 @@
203
204
  ],
204
205
  "worker": [
205
206
  "./dist/worker/index.d.ts"
207
+ ],
208
+ "navigation/bake": [
209
+ "./dist/navigation/bake.d.ts"
206
210
  ]
207
211
  }
208
212
  },