@hology/core 0.0.246 → 0.0.248

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 (87) hide show
  1. package/dist/gameplay/actors/actor.js +1 -1
  2. package/dist/gameplay/actors/builtin/components/index.d.ts +2 -0
  3. package/dist/gameplay/actors/builtin/components/index.js +1 -1
  4. package/dist/gameplay/actors/builtin/components/spline-component.d.ts +65 -0
  5. package/dist/gameplay/actors/builtin/components/spline-component.js +4 -0
  6. package/dist/gameplay/actors/builtin/components/water-post-process-settings.d.ts +24 -0
  7. package/dist/gameplay/actors/builtin/components/water-post-process-settings.js +4 -0
  8. package/dist/gameplay/actors/builtin/index.d.ts +6 -0
  9. package/dist/gameplay/actors/builtin/index.js +1 -1
  10. package/dist/gameplay/actors/builtin/navmesh-actor.js +1 -1
  11. package/dist/gameplay/actors/builtin/post-process-volume-actor.d.ts +1 -2
  12. package/dist/gameplay/actors/builtin/spline-actor.d.ts +6 -0
  13. package/dist/gameplay/actors/builtin/spline-actor.js +4 -0
  14. package/dist/gameplay/actors/builtin/water-spline-volume-actor.d.ts +23 -0
  15. package/dist/gameplay/actors/builtin/water-spline-volume-actor.js +4 -0
  16. package/dist/gameplay/actors/builtin/water-volume-actor.d.ts +19 -0
  17. package/dist/gameplay/actors/builtin/water-volume-actor.js +4 -0
  18. package/dist/gameplay/actors/camera/camera-component.js +1 -1
  19. package/dist/gameplay/actors/camera/third-person-camera-component.js +1 -1
  20. package/dist/gameplay/actors/component.d.ts +0 -1
  21. package/dist/gameplay/actors/component.js +1 -1
  22. package/dist/gameplay/actors/index.d.ts +4 -0
  23. package/dist/gameplay/actors/index.js +1 -1
  24. package/dist/gameplay/actors/internal/component-init.d.ts +1 -1
  25. package/dist/gameplay/actors/internal/component-init.js +1 -1
  26. package/dist/gameplay/ai/build-tile.d.ts +3 -1
  27. package/dist/gameplay/ai/build-tile.js +1 -1
  28. package/dist/gameplay/ai/dynamic-tiled-navmesh.d.ts +4 -21
  29. package/dist/gameplay/ai/dynamic-tiled-navmesh.js +1 -1
  30. package/dist/gameplay/ai/dynamic-tiled-navmesh.worker.d.ts +10 -1
  31. package/dist/gameplay/ai/dynamic-tiled-navmesh.worker.js +1 -1
  32. package/dist/gameplay/ai/index.d.ts +1 -0
  33. package/dist/gameplay/ai/index.js +1 -1
  34. package/dist/gameplay/ai/navigation-areas.d.ts +22 -0
  35. package/dist/gameplay/ai/navigation-areas.js +4 -0
  36. package/dist/gameplay/ai/navigation.d.ts +28 -4
  37. package/dist/gameplay/ai/navigation.js +1 -1
  38. package/dist/gameplay/inject.d.ts +3 -0
  39. package/dist/gameplay/inject.js +1 -1
  40. package/dist/gameplay/services/actor-update-registry.d.ts +13 -0
  41. package/dist/gameplay/services/actor-update-registry.js +4 -0
  42. package/dist/gameplay/services/render.d.ts +1 -0
  43. package/dist/gameplay/services/render.js +1 -1
  44. package/dist/gameplay/services/world.js +1 -1
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.js +1 -1
  47. package/dist/rendering/webgpu-experimental-backend.js +1 -1
  48. package/dist/rendering.d.ts +8 -0
  49. package/dist/rendering.js +1 -1
  50. package/dist/scene/materializer.js +1 -1
  51. package/dist/shader/graph/compiler.js +1 -1
  52. package/dist/shader/graph/model.d.ts +1 -1
  53. package/dist/shader/graph/registry.js +1 -1
  54. package/dist/shader/parameter.d.ts +3 -0
  55. package/dist/shader/post-process-shader.js +1 -1
  56. package/dist/shader-nodes/depth.d.ts +3 -1
  57. package/dist/shader-nodes/depth.js +1 -1
  58. package/dist/shader-nodes/index.d.ts +1 -0
  59. package/dist/shader-nodes/index.js +1 -1
  60. package/dist/shader-nodes/water-volume.d.ts +9 -0
  61. package/dist/shader-nodes/water-volume.js +4 -0
  62. package/dist/shader-nodes/water.d.ts +12 -0
  63. package/dist/shader-nodes/water.js +4 -0
  64. package/dist/test/build-tile.test.js +1 -1
  65. package/dist/test/materializer-dynamic-transform.test.js +1 -1
  66. package/dist/test/navmesh-worker-pool.test.js +1 -1
  67. package/dist/test/node-material-webgpu-resolver.test.js +1 -1
  68. package/dist/test/post-process-effect.test.js +1 -1
  69. package/dist/test/shader-graph.test.js +1 -1
  70. package/dist/test/spline-component.test.d.ts +2 -0
  71. package/dist/test/spline-component.test.js +4 -0
  72. package/dist/test/view-controller-updates.test.d.ts +2 -0
  73. package/dist/test/view-controller-updates.test.js +4 -0
  74. package/dist/test/water-spline-volume-actor.test.d.ts +2 -0
  75. package/dist/test/water-spline-volume-actor.test.js +4 -0
  76. package/dist/test/worker-task-executor.test.d.ts +2 -0
  77. package/dist/test/worker-task-executor.test.js +4 -0
  78. package/dist/worker/index.d.ts +11 -2
  79. package/dist/worker/index.js +1 -1
  80. package/dist/worker/task-executor.d.ts +22 -0
  81. package/dist/worker/task-executor.js +4 -0
  82. package/dist/worker/task-worker.d.ts +3 -0
  83. package/dist/worker/task-worker.js +4 -0
  84. package/dist/worker/task.d.ts +39 -0
  85. package/dist/worker/task.js +4 -0
  86. package/package.json +3 -3
  87. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
- import{Subject as e}from"rxjs";import{Group as t,Object3D as n,Quaternion as s,Scene as o,Vector3 as a}from"three";import{describe as i,expect as r,it as c,vi as d}from"vitest";d.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&d.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),d.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),d.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),d.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),d.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:d.fn()}));import{PhysicsBodyType as f}from"../gameplay/services/physics/physics-system.js";import{SceneMaterializer as l}from"../scene/materializer.js";function p(e){return{name:e.id,...e}}i("SceneMaterializer dynamic transforms",()=>{c("preserves a nested dynamic object world transform when moving it to the scene root",async()=>{const i=new o,c=function(t){const n={getObjects:()=>[],onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},s={onCreate:new e,onDelete:new e,onUpdate:new e,getAsset:d.fn(async()=>null),getAssets:d.fn(async()=>[])};return new l(t,n,s,{},{renderer:{shadowMap:{needsUpdate:!1}}},[],[],{create:async()=>null,initActor:async()=>{}})}(i);c.inEditor=!1;const m=[.2,.7,-.1],y=[0,0,0],u=p({id:"parent",type:"group",position:[10,4,-7],rotation:m,scale:[2,3,4],children:[p({id:"dynamic-child",type:"shape_mesh",shape:"box",position:[3,-2,5],rotation:y,scale:[.5,1.5,2],physics:{type:f.dynamic}})]});await c.materialize(u);const g=i.getObjectByName("dynamic-child");r(g?.parent).toBe(i);const h=new t;h.position.set(10,4,-7),h.rotation.set(...m),h.scale.set(2,3,4);const w=new n;w.position.set(3,-2,5),w.rotation.set(...y),w.scale.set(.5,1.5,2),h.add(w);const v=w.getWorldPosition(new a),j=w.getWorldQuaternion(new s),b=w.getWorldScale(new a);r(g.position.distanceTo(v)).toBeLessThan(1e-6),r(1-Math.abs(g.quaternion.dot(j))).toBeLessThan(1e-6),r(g.scale.distanceTo(b)).toBeLessThan(1e-6)})});/*
1
+ import{Subject as e}from"rxjs";import{Group as t,Object3D as n,Quaternion as o,Scene as s,Vector3 as a}from"three";import{describe as i,expect as r,it as c,vi as d}from"vitest";d.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&d.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),d.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),d.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),d.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),d.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:d.fn()}));import{PhysicsBodyType as f}from"../gameplay/services/physics/physics-system.js";import{SceneMaterializer as l}from"../scene/materializer.js";function p(t){const n={onCreate:new e,onDelete:new e,onUpdate:new e,getAsset:d.fn(async()=>null),getAssets:d.fn(async()=>[])};return new l(t,{getObjects:()=>[],onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},n,{},{renderer:{shadowMap:{needsUpdate:!1}}},[],[],{create:async()=>null,initActor:async()=>{}})}function m(e){return{name:e.id,...e}}i("SceneMaterializer dynamic transforms",()=>{c("does not disable matrix updates on an actor root after an editor transform",()=>{const e=p(new s);e.inEditor=!0;const n=new t,o=n.updateMatrixWorld;e.beginEditorStaticSceneEdit([n]),e.endEditorStaticSceneEdit(),r(n.matrixAutoUpdate).toBe(!0),r(n.updateMatrixWorld).toBe(o)}),c("preserves a nested dynamic object world transform when moving it to the scene root",async()=>{const e=new s,i=p(e);i.inEditor=!1;const c=[.2,.7,-.1],d=[0,0,0],l=m({id:"parent",type:"group",position:[10,4,-7],rotation:c,scale:[2,3,4],children:[m({id:"dynamic-child",type:"shape_mesh",shape:"box",position:[3,-2,5],rotation:d,scale:[.5,1.5,2],physics:{type:f.dynamic}})]});await i.materialize(l);const u=e.getObjectByName("dynamic-child");r(u?.parent).toBe(e);const y=new t;y.position.set(10,4,-7),y.rotation.set(...c),y.scale.set(2,3,4);const g=new n;g.position.set(3,-2,5),g.rotation.set(...d),g.scale.set(.5,1.5,2),y.add(g);const h=g.getWorldPosition(new a),w=g.getWorldQuaternion(new o),x=g.getWorldScale(new a);r(u.position.distanceTo(h)).toBeLessThan(1e-6),r(1-Math.abs(u.quaternion.dot(w))).toBeLessThan(1e-6),r(u.scale.distanceTo(x)).toBeLessThan(1e-6)})});/*
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 s,vi as a}from"vitest";import*as n from"three";import{getTileBounds as o,NavMeshWorkerPool as l}from"../gameplay/ai/dynamic-tiled-navmesh.js";e("getTileBounds",()=>{s("creates adjacent fixed-size tiles relative to the navmesh minimum",()=>{const e=new n.Vector3(-100,-5,-200),s=new n.Vector3(300,25,400);t(o(e,s,2,3,10)).toEqual([[-80,-5,-170],[-70,25,-160]])})}),e("NavMeshWorkerPool",()=>{s("creates workers lazily and caps concurrent work",()=>{const e=[],s=new l(()=>{const t=new r;return e.push(t),t}),n={},o=a.fn();t(e).toHaveLength(0);for(let e=0;e<8;e++)s.schedule(n,{tileX:e},void 0,o,24);t(e).toHaveLength(4),t(e.flatMap(e=>e.messages)).toHaveLength(4),e[0].complete(0),t(e).toHaveLength(4),t(e.flatMap(e=>e.messages)).toHaveLength(5),t(o).toHaveBeenCalledTimes(1)}),s("does not deliver queued or active results after an owner is cancelled",()=>{const e=[],s=new l(()=>{const t=new r;return e.push(t),t}),n={},o=a.fn();s.schedule(n,{tileX:0},void 0,o,1),s.schedule(n,{tileX:1},void 0,o,1),s.cancel(n),e[0].complete(0),t(o).not.toHaveBeenCalled(),t(e[0].messages).toHaveLength(1)})});class r{constructor(){this.onmessage=null,this.onerror=null,this.messages=[],this.terminate=a.fn()}postMessage(e){this.messages.push(e)}complete(e){this.onmessage?.({data:{tileX:e,tileY:0,navMeshData:new Uint8Array}})}}/*
1
+ import{describe as e,expect as t,it as i}from"vitest";import*as m from"three";import{getTileBounds as a}from"../gameplay/ai/dynamic-tiled-navmesh.js";e("getTileBounds",()=>{i("creates adjacent fixed-size tiles relative to the navmesh minimum",()=>{const e=new m.Vector3(-100,-5,-200),i=new m.Vector3(300,25,400);t(a(e,i,2,3,10)).toEqual([[-80,-5,-170],[-70,25,-160]])})});/*
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}from"vitest";import{DataTexture as n,Material as a,MeshStandardMaterial as r,RGBAFormat as i,SpriteMaterial as s,Vector4 as l}from"three";import{batchingUniformVec4 as d,float as h,NodeShaderMaterial as u,rgb as m,rgba as c,standardMaterial as p,translateX as f,uniformFloat as S,uniformVec4 as v,uniformSampler2d as g,varyingAttributes as w,vec3 as x,vec4 as C}from"three-shader-graph";import{createNodeMaterialSignature as y,markMaterialUniformValuesNeedUpdate as B,NodeMaterialWebGpuResolver as M,setMaterialUniformValuesAutoUpdate as b,withThreeDefaultVertexAttributeValues as T}from"../rendering/node-material-webgpu-resolver.js";import{LandscapeCompositeShader as L}from"../shader/builtin/landscape-composite-shader.js";import{DecalUnlitShader as W}from"../shader/builtin/decal-unlit-shader.js";import{getSpritePosition as _}from"../shader/sprite-shader.js";import{depthNormal as G,depthWorldPosition as A,fragmentLinearEyeDepth as U,sampleSceneDepth as I,screenUV as V}from"../shader-nodes/depth.js";import{sampleSceneColor as k}from"../shader-nodes/scene-sample.js";import{applyDithering as j}from"../shader-nodes/dither.js";import{parallaxOcclusionMapping as D}from"../shader-nodes/pom.js";import{elapsedTimeUniformName as E}from"../shader-nodes/time.js";import{Trail as F}from"../effects/vfx/trail-renderer.js";import{particleUniforms as O}from"../shader-nodes/particle.js";import{buildShaderGraphMaterial as q,createDefaultShaderGraphDocument as P}from"../shader/graph/index.js";class z extends a{set uniformValuesNeedsUpdate(e){e&&this.uniformValuesAutoVersion++}constructor(e){super(),this.disposed=!1,this.uniformValuesAutoUpdate=!0,this.uniformValuesAutoVersion=0,this.vertexShader=e.vertexShader,this.fragmentShader=e.fragmentShader,this.depthVertexShader=e.depthVertexShader??null,this.depthFragmentShader=e.depthFragmentShader??null,this.pointShadowVertexShader=e.pointShadowVertexShader??null,this.pointShadowFragmentShader=e.pointShadowFragmentShader??null,this.uniforms=e.uniforms,this.sceneTextures=e.sceneTextures??{},this.graphTextures=e.graphTextures??{},this.vertexAttributes=e.vertexAttributes??[],this.transparent=e.transparent??!1,this.alphaTest=e.alphaTest??0,this.depthTest=e.depthTest??!0,this.depthWrite=e.depthWrite??!0,this.side=e.side??this.side}dispose(){this.disposed=!0,super.dispose()}}e("NodeMaterialWebGpuResolver",()=>{o("derives a textured, darkened albedo outline and updates its controls",()=>{const e=new n(new Uint8Array([128,96,64,255]),1,1,i);e.needsUpdate=!0;const o=g("outlineAlbedo",e).sample(w.uv),a=new u({color:p({color:o})});a.userData.outlineParameters={colorMode:"darkenedAlbedo",thickness:.012,darkening:.65,alpha:.8};const r=new M({WGSLShaderMaterial:z}),s=r.resolve(a,"outline");t(s).toBeInstanceOf(z),t(s.side).toBe(1),t(s.vertexShader).toContain("materialUniforms.outlineThickness"),t(s.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"skinIndex"}),t.objectContaining({name:"skinWeight"})])),t(s.vertexShader).toContain("rendererSkinPosition"),t(s.vertexShader).toContain("outline_local_position_"),t(s.vertexShader).toContain("outline_local_normal_"),t(s.vertexShader).toContain("sqrt("),t(s.fragmentShader).toContain("outlineAlbedoTexture"),t(s.fragmentShader).toContain("materialUniforms.outlineDarkening"),t(s.uniforms.outlineThickness.value).toBe(.012),t(s.uniforms.outlineDarkening.value).toBe(.65),t(s.uniforms.outlineAlpha.value).toBe(.8),a.userData.outlineParameters.darkening=2,a.userData.outlineParameters.thickness=.02;const l=r.resolve(a,"outline");t(l).toBe(s),t(l.uniforms.outlineThickness.value).toBe(.02),t(l.uniforms.outlineDarkening.value).toBe(1)}),o("declines an albedo outline when the source graph has no albedo output",()=>{const e=new u({color:c(16777215)}),o=new M({WGSLShaderMaterial:z});t(o.resolve(e,"outline")).toBeUndefined()}),o("compiles shader graph scene-light nodes against the renderer light ABI",()=>{const e=P("surface");e.shadingModel="unlit",e.nodes.push({id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"},{id:"combined",kind:"math.add"}),e.edges.push({from:{nodeId:"ambient",port:"color"},to:{nodeId:"combined",port:"a"}},{from:{nodeId:"directional",port:"color"},to:{nodeId:"combined",port:"b"}}),e.outputs.color={input:{nodeId:"combined",port:"value"}};const o=new M({WGSLShaderMaterial:z}).resolve(q(e));t(o?.fragmentShader).toContain("rendererAmbientLight()"),t(o?.fragmentShader).toContain("rendererDirectionalLightForLight"),t(o?.fragmentShader).toContain("rendererDirectionalLightCount()"),t(o?.fragmentShader).not.toContain("NUM_DIR_LIGHTS")}),o("compiles decal surface rejection without undefined WGSL expressions",()=>{const e=(new W).build(),o=new M({WGSLShaderMaterial:z}).resolve(e);t(o?.fragmentShader).not.toContain("undefined"),t(o?.fragmentShader).toContain("hology_scene_mapTexture")}),o("recompiles an opaque variant when receiveDecals changes",()=>{const e=new u({color:c(16777215)});e.userData.receiveDecals=!0;const o=new M({WGSLShaderMaterial:z},e=>[c(16777215,!1===e.userData.receiveDecals?0:2)]),n=o.resolve(e,"opaque");e.userData.receiveDecals=!1,e.needsUpdate=!0;const a=o.resolve(e,"opaque");t(a).not.toBe(n),t(n.disposed).toBe(!0),t(n.fragmentShader).not.toBe(a.fragmentShader)}),o("maps Hology elapsed time to the renderer frame clock",()=>{const e=S(E),o=new u({color:c(x(e)),transform:f(e)}),n=new M({WGSLShaderMaterial:z}).resolve(o);t(n?.vertexShader).toContain("rendererElapsedTime()"),t(n?.fragmentShader).toContain("rendererElapsedTime()"),t(n?.depthVertexShader).toContain("rendererElapsedTime()"),t(n?.pointShadowVertexShader).toContain("rendererElapsedTime()"),t(n?.uniforms).not.toHaveProperty(E)}),o("compiles the native sprite billboard position without a GLSL fallback",()=>{const e=new u({color:c(16777215),position:_(S("rotation",.25),S("screenSpaceSize",0),S("lockYAxis",1))}),o=new M({WGSLShaderMaterial:z}).resolve(e);t(o).toBeInstanceOf(z),t(o?.vertexShader).toContain("cos("),t(o?.vertexShader).toContain("sin("),t(o?.vertexShader).toContain("materialUniforms.lockYAxis"),t(o?.vertexShader).not.toContain("customFunction_")}),o("uses the per-instance rotation attribute for instanced sprites",()=>{const e=new u({color:c(16777215),position:_(S("rotation",0),void 0,void 0,!0)}),o=new M({WGSLShaderMaterial:z}).resolve(e);t(o?.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"rotation",format:"float32",stepMode:"instance"})])),t(o?.vertexShader).toContain("input.rotation"),t(o?.vertexShader).not.toContain("materialUniforms.rotation")}),o("uses per-instance particle opacity in WebGPU fragment shaders",()=>{const e=new u({color:c(m(16777215),O.opacity)}),o=new M({WGSLShaderMaterial:z}).resolve(e);t(o?.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"particleData",format:"float32x3",stepMode:"instance"})])),t(o?.fragmentShader).toContain(".x"),t(o?.fragmentShader).toContain("> 0.5")}),o("maps THREE.SpriteMaterial to the native sprite shader",()=>{const e=new n(new Uint8Array([255,255,255,255]),1,1,i),o=new s({map:e,color:8429760,opacity:.6,rotation:.25,sizeAttenuation:!1,alphaTest:.2,transparent:!0}),a=new M({WGSLShaderMaterial:z}),r=a.resolve(o);t(r).toBeInstanceOf(z),t(r?.vertexShader).toContain("materialUniforms.rotation"),t(r?.vertexShader).toContain("materialUniforms.screenSpaceSize"),t(r?.fragmentShader).toContain("textureSample("),t(r?.alphaTest).toBe(.2),t(r?.transparent).toBe(!0),t(r?.uniforms.rotation.value).toBe(.25),t(r?.uniforms.screenSpaceSize.value).toBe(1),o.rotation=.75;const l=a.resolve(o);t(l).toBe(r),t(l?.uniforms.rotation.value).toBe(.75)}),o("memoizes node-material signatures for repeated lookups",()=>{const e=["version","vertexShader","fragmentShader","alphaTest","alphaToCoverage","blending","colorWrite","depthFunc","depthTest","depthWrite","polygonOffset","polygonOffsetFactor","polygonOffsetUnits","premultipliedAlpha","side","stencilWrite","transparent"],o=new Map([["version",1],["vertexShader","#vertex"],["fragmentShader","#fragment"],["alphaTest",0],["alphaToCoverage",!1],["blending",1],["colorWrite",!0],["depthFunc",3],["depthTest",!0],["depthWrite",!0],["polygonOffset",!1],["polygonOffsetFactor",0],["polygonOffsetUnits",0],["premultipliedAlpha",!1],["side",0],["stencilWrite",!1],["transparent",!1]]);let n=0;const a=new Proxy({},{get:(t,a)=>"string"==typeof a&&e.includes(a)?(n+=1,o.get(a)):Reflect.get(t,a)});y(a),y(a),t(n).toBe(e.length)}),o("adapts a NodeShaderMaterial without replacing its scene material",()=>{const e=S("intensity",.75),o=new u({color:p({color:m(5210052).multiplyScalar(e),roughness:.42,metalness:.18}),transparent:!0,alphaTest:.25}),n=new M({WGSLShaderMaterial:z}),a=n.resolve(o);t(a).toBeInstanceOf(z),t(a).not.toBe(o),t(a?.transparent).toBe(!0),t(a?.alphaTest).toBe(.25),t(a?.fragmentShader).toContain("rendererPointLightForLight"),t(o.isShaderMaterial).toBe(!0),e.value=.25,t(n.resolve(o)).toBe(a),t(a?.uniforms.intensity?.value).toBe(.25)}),o("syncs direct NodeShaderMaterial uniform mutations to the resolved material",()=>{const e=v("trailTint",new l(0,0,0,0)),o=S("hology_particle_energy",1),n=new u({color:e.multiplyScalar(o)}),a=new M({WGSLShaderMaterial:z});n.uniforms.trailTint.value=new l(1,.25,.5,.75),n.uniforms.hology_particle_energy.value=.4;const r=a.resolve(n);t(r?.uniforms.trailTint.value).toEqual(new l(1,.25,.5,.75)),t(r?.uniforms.hology_particle_energy.value).toBe(.4),n.uniforms.trailTint.value.set(.2,.4,.6,.8),n.uniforms.hology_particle_energy.value=.1,t(a.resolve(n)).toBe(r),t(r?.uniforms.trailTint.value).toEqual(new l(.2,.4,.6,.8)),t(r?.uniforms.hology_particle_energy.value).toBe(.1)}),o("only syncs explicitly invalidated uniforms when auto update is disabled",()=>{const e=S("intensity",.75),o=new u({color:c(x(e))});o.uniformValuesAutoUpdate=!1;const n=new M({WGSLShaderMaterial:z}),a=n.resolve(o),r=a.uniformValuesAutoVersion;e.value=.25,t(n.resolve(o)).toBe(a),t(a?.uniforms.intensity.value).toBe(.75),t(a?.uniformValuesAutoVersion).toBe(r),o.uniformValuesNeedsUpdate=!0,t(n.resolve(o)).toBe(a),t(a?.uniforms.intensity.value).toBe(.25),t(a?.uniformValuesAutoVersion).toBe(r+1)}),o("supports explicit invalidation for mapped Three.js materials",()=>{const e=new r({roughness:.8});b(e,!1);const o=new M({WGSLShaderMaterial:z}),n=o.resolve(e);e.roughness=.2,t(o.resolve(e)).toBe(n),t(n?.uniforms.roughness.value).toBe(.8),B(e),t(o.resolve(e)).toBe(n),t(n?.uniforms.roughness.value).toBe(.2)}),o("binds uniforms referenced only by an MRT material output",()=>{const e=S("roughness",.7),o=new u({color:c(5601177),alphaTest:.1});o.outputRoughness=e;const n=new M({WGSLShaderMaterial:z},e=>[e.outputColor,C(x(0,0,1),e.outputRoughness)]).resolve(o);t(n?.fragmentShader).toContain("materialUniforms.roughness"),t(n?.fragmentShader).not.toContain("graphColor"),t(n?.fragmentShader).toMatch(/if \(.+ < 0\.1\) \{ discard; \}/),t(n?.uniforms.roughness?.value).toBe(.7)}),o("compiles a landscape composite built from node material semantic outputs",()=>{const e=new M({WGSLShaderMaterial:z}),o=new u({color:p({color:m(5147450),roughness:.82,metalness:0})}),n=new u({color:p({color:m(7432286),roughness:.6,metalness:.05})}),a=new L;a.layers=[o,n];const r=e.resolve(a.build());t(r).toBeInstanceOf(z),t(r?.fragmentShader).toContain("rendererDirectionalLightForLight"),t(r?.vertexShader).toContain("@location(6) material: vec4<f32>"),t(r?.vertexShader).toContain("@location(8) hole: f32")}),o("allocates the trail's authored vertex attributes",()=>{const e=new M({WGSLShaderMaterial:z}).resolve(F.createBaseMaterial());t(e).toBeInstanceOf(z),t(e?.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"nodeID",shaderLocation:6,format:"float32"}),t.objectContaining({name:"nodeCenter",shaderLocation:7,format:"float32x3"})]))}),o("uses Three's default value when a geometry has no uv1 attribute",()=>{const e=T([{name:"uv1",shaderLocation:6,format:"float32x2"},{name:"custom",shaderLocation:7,format:"float32"}]);t(e).toEqual(t.arrayContaining([t.objectContaining({name:"uv1",defaultValue:[0,0]}),t.objectContaining({name:"custom"})])),t(e[1]).not.toHaveProperty("defaultValue")}),o("compiles the foliage transform and named texture-uniform pattern",()=>{const e=new M({WGSLShaderMaterial:z}),o=new n(new Uint8Array([255,255,255,255]),1,1,i),a=new u({color:g("map").sample(w.uv),transform:f(S("windOffset",.25)),uniforms:{map:{value:o}},alphaTest:.5}),r=e.resolve(a);t(r).toBeInstanceOf(z),t(r?.vertexShader).toContain("rendererClipPosition(_ng_localPosition"),t(r?.depthVertexShader).toContain("materialUniforms.windOffset"),t(r?.depthFragmentShader).toContain("textureSample("),t(r?.depthFragmentShader).toContain("< 0.5) { discard;"),t(r?.pointShadowFragmentShader).toContain("distanceToLight")}),o("rebuilds cached materials when scene-local texture bindings change",()=>{const e=new M({WGSLShaderMaterial:z}),o=new n(new Uint8Array([255,255,255,255]),1,1,i),a=new n(new Uint8Array([32,64,128,255]),1,1,i),r=new u({color:g("map").sample(w.uv),uniforms:{map:{value:o}},alphaTest:.5}),s=e.resolve(r);r.uniforms.map.value=a;const l=e.resolve(r);t(l).not.toBe(s),t(s.disposed).toBe(!0),r.dispose();const d=e.resolve(r);t(d).not.toBe(l),t(l.disposed).toBe(!0)}),o("uses the renderer batch id without Three’s batchingIdTexture",()=>{const e=new M({WGSLShaderMaterial:z}),o=new n(new Uint8Array([255,96,24,255]),1,1,i),a=new u({color:c(d("vp0")),uniforms:{vp0:{value:o}}}),r=e.resolve(a);t(r).toBeInstanceOf(z),t(r?.vertexShader).toContain("rendererBatchingId(input.instanceIndex)")}),o("compiles depth nodes that use fragment-coordinate expressions",()=>{const e=new M({WGSLShaderMaterial:z}),o=new u({color:c(x(V.x,V.y,U))}),n=e.resolve(o);t(n).toBeInstanceOf(z),t(n?.fragmentShader).toContain("input.position.xy"),t(n?.fragmentShader).toContain("input.position.z"),t(n?.fragmentShader).toContain("rendererViewportSize()"),t(n?.fragmentShader).not.toContain("gl_FragCoord")}),o("maps scene color and depth nodes to renderer-owned textures",()=>{const e=new M({WGSLShaderMaterial:z}),o=new u({color:k(V).multiplyScalar(I(V))}),n=e.resolve(o);t(n.sceneTextures).toEqual({hology_scene_map:"sceneColor",hology_depth_map:"sceneDepth"}),t(n.graphTextures).toEqual({}),t(n.fragmentShader).toContain("hology_scene_mapTexture"),t(n.fragmentShader).toContain("hology_depth_mapTexture"),t(n.fragmentShader).toContain("vec4<f32>(textureSample(hology_depth_mapTexture")}),o("caches distinct material variants for opaque and late draw stages",()=>{const e=new M({WGSLShaderMaterial:z},(e,t)=>"opaque"===t?[c(x(1)),c(x(0))]:void 0),o=new u({color:c(x(1))}),n=e.resolve(o,"opaque"),a=e.resolve(o,"late");t(n).not.toBe(a),t(e.resolve(o,"opaque")).toBe(n),t(e.resolve(o,"late")).toBe(a),t(n?.fragmentShader).toContain("@location(1)"),t(a?.fragmentShader).not.toContain("@location(1)")}),o("reconstructs WebGPU depth positions with inverted NDC Y",()=>{const e=new M({WGSLShaderMaterial:z}),o=new u({color:c(A.add(G.multiplyScalar(0)))}),n=e.resolve(o);t(n.fragmentShader).toContain("vec2<f32>(2.0, -2.0)"),t(n.fragmentShader).toMatch(/-\(float_op_\d+\)/)}),o("emits dither and parallax nodes without GLSL custom functions",()=>{const e=new M({WGSLShaderMaterial:z}),o=new n(new Uint8Array([128,128,128,255]),1,1,i),a=D(w.uv,g("heightMap",o),h(.03)),r=new u({color:C(a.x,a.y,j(x(.2,.4,.8)).z,1)}),s=e.resolve(r);t(s).toBeInstanceOf(z),t(s?.fragmentShader).toContain("textureSampleLevel(heightMapTexture, heightMapSampler"),t(s?.fragmentShader).toMatch(/= -dpdy\(-pom_\d+ViewPosition\)/),t(s?.fragmentShader).toMatch(/= -dpdy\(pom_\d+Uv\)/),t(s?.fragmentShader).not.toContain("customFunction_"),t(s?.fragmentShader).not.toContain("gl_FragCoord")}),o("inherits, updates, and clears the WebGPU world environment",()=>{const e=new n(new Uint8Array([255,255,255,255]),1,1,i),o=new u({color:p({color:m(8421504),roughness:.5,metalness:.1})}),a=new M({WGSLShaderMaterial:z});a.setWorldEnvironment({texture:e,intensity:.75});const r=a.resolve(o);t(r.fragmentShader).toContain("envMapTexture"),t(r.uniforms.envMapIntensity?.value).toBe(.75),t(o.envMap).toBeUndefined(),a.setWorldEnvironment({texture:e,intensity:.25});const s=a.resolve(o);t(s).toBe(r),t(s.uniforms.envMapIntensity?.value).toBe(.25),a.setWorldEnvironment(null);const l=a.resolve(o);t(l).not.toBe(r),t(r.disposed).toBe(!0),t(l.fragmentShader).not.toContain("envMapTexture")}),o("keeps an authored material environment ahead of the world environment",()=>{const e=new n(new Uint8Array([255,255,255,255]),1,1,i),o=new n(new Uint8Array([64,96,128,255]),1,1,i),a=new u({color:p({color:m(8421504),roughness:.5,metalness:.1}),envMap:o}),r=new M({WGSLShaderMaterial:z});r.setWorldEnvironment({texture:e,intensity:.2}),r.resolve(a),t(a.uniforms.envMap.value).toBe(o),t(a.envMap).toBe(o)})});/*
1
+ import{describe as e,expect as t,it as o}from"vitest";import{DataTexture as n,Material as r,MeshStandardMaterial as a,RGBAFormat as i,SpriteMaterial as s,Vector4 as l}from"three";import{batchingUniformVec4 as d,float as h,NodeShaderMaterial as u,rgb as m,rgba as c,standardMaterial as p,translateX as f,uniformFloat as v,uniformVec4 as S,uniformSampler2d as g,varyingAttributes as w,vec3 as x,vec4 as C}from"three-shader-graph";import{createNodeMaterialSignature as y,markMaterialUniformValuesNeedUpdate as B,NodeMaterialWebGpuResolver as M,setMaterialUniformValuesAutoUpdate as b,withThreeDefaultVertexAttributeValues as L}from"../rendering/node-material-webgpu-resolver.js";import{LandscapeCompositeShader as T}from"../shader/builtin/landscape-composite-shader.js";import{DecalUnlitShader as W}from"../shader/builtin/decal-unlit-shader.js";import{getSpritePosition as _}from"../shader/sprite-shader.js";import{depthNormal as G,depthWorldPosition as A,fragmentLinearEyeDepth as U,sampleSceneDepth as I,screenUV as V}from"../shader-nodes/depth.js";import{sampleSceneColor as k}from"../shader-nodes/scene-sample.js";import{applyDithering as j}from"../shader-nodes/dither.js";import{parallaxOcclusionMapping as D}from"../shader-nodes/pom.js";import{elapsedTimeUniformName as E}from"../shader-nodes/time.js";import{Trail as F}from"../effects/vfx/trail-renderer.js";import{particleUniforms as O}from"../shader-nodes/particle.js";import{buildShaderGraphMaterial as q,createDefaultShaderGraphDocument as P}from"../shader/graph/index.js";class z extends r{set uniformValuesNeedsUpdate(e){e&&this.uniformValuesAutoVersion++}constructor(e){super(),this.disposed=!1,this.uniformValuesAutoUpdate=!0,this.uniformValuesAutoVersion=0,this.vertexShader=e.vertexShader,this.fragmentShader=e.fragmentShader,this.depthVertexShader=e.depthVertexShader??null,this.depthFragmentShader=e.depthFragmentShader??null,this.pointShadowVertexShader=e.pointShadowVertexShader??null,this.pointShadowFragmentShader=e.pointShadowFragmentShader??null,this.uniforms=e.uniforms,this.sceneTextures=e.sceneTextures??{},this.graphTextures=e.graphTextures??{},this.vertexAttributes=e.vertexAttributes??[],this.transparent=e.transparent??!1,this.alphaTest=e.alphaTest??0,this.depthTest=e.depthTest??!0,this.depthWrite=e.depthWrite??!0,this.side=e.side??this.side}dispose(){this.disposed=!0,super.dispose()}}e("NodeMaterialWebGpuResolver",()=>{o("derives a textured, darkened albedo outline and updates its controls",()=>{const e=new n(new Uint8Array([128,96,64,255]),1,1,i);e.needsUpdate=!0;const o=g("outlineAlbedo",e).sample(w.uv),r=new u({color:p({color:o})});r.userData.outlineParameters={colorMode:"darkenedAlbedo",thickness:.012,darkening:.65,alpha:.8};const a=new M({WGSLShaderMaterial:z}),s=a.resolve(r,"outline");t(s).toBeInstanceOf(z),t(s.side).toBe(1),t(s.vertexShader).toContain("materialUniforms.outlineThickness"),t(s.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"skinIndex"}),t.objectContaining({name:"skinWeight"})])),t(s.vertexShader).toContain("rendererSkinPosition"),t(s.vertexShader).toContain("outline_local_position_"),t(s.vertexShader).toContain("outline_local_normal_"),t(s.vertexShader).toContain("sqrt("),t(s.fragmentShader).toContain("outlineAlbedoTexture"),t(s.fragmentShader).toContain("materialUniforms.outlineDarkening"),t(s.uniforms.outlineThickness.value).toBe(.012),t(s.uniforms.outlineDarkening.value).toBe(.65),t(s.uniforms.outlineAlpha.value).toBe(.8),r.userData.outlineParameters.darkening=2,r.userData.outlineParameters.thickness=.02;const l=a.resolve(r,"outline");t(l).toBe(s),t(l.uniforms.outlineThickness.value).toBe(.02),t(l.uniforms.outlineDarkening.value).toBe(1)}),o("declines an albedo outline when the source graph has no albedo output",()=>{const e=new u({color:c(16777215)}),o=new M({WGSLShaderMaterial:z});t(o.resolve(e,"outline")).toBeUndefined()}),o("compiles shader graph scene-light nodes against the renderer light ABI",()=>{const e=P("surface");e.shadingModel="unlit",e.nodes.push({id:"ambient",kind:"scene.ambientLight"},{id:"directional",kind:"scene.directionalLight"},{id:"combined",kind:"math.add"}),e.edges.push({from:{nodeId:"ambient",port:"color"},to:{nodeId:"combined",port:"a"}},{from:{nodeId:"directional",port:"color"},to:{nodeId:"combined",port:"b"}}),e.outputs.color={input:{nodeId:"combined",port:"value"}};const o=new M({WGSLShaderMaterial:z}).resolve(q(e));t(o?.fragmentShader).toContain("rendererAmbientLight()"),t(o?.fragmentShader).toContain("rendererDirectionalLightForLight"),t(o?.fragmentShader).toContain("rendererDirectionalLightCount()"),t(o?.fragmentShader).not.toContain("NUM_DIR_LIGHTS")}),o("compiles the shader graph world normal without a WGSL matrix inverse",()=>{const e=P("surface");e.shadingModel="unlit",e.nodes.push({id:"world-normal",kind:"builtin.worldNormal"}),e.outputs.color={input:{nodeId:"world-normal",port:"value"}};const o=new M({WGSLShaderMaterial:z}).resolve(q(e));t(o?.vertexShader).toContain("transpose(rendererViewMatrix())"),t(o?.vertexShader).not.toContain("nodeInverseMat4")}),o("compiles decal surface rejection without undefined WGSL expressions",()=>{const e=(new W).build(),o=new M({WGSLShaderMaterial:z}).resolve(e);t(o?.fragmentShader).not.toContain("undefined"),t(o?.fragmentShader).toContain("hology_scene_mapTexture")}),o("recompiles an opaque variant when receiveDecals changes",()=>{const e=new u({color:c(16777215)});e.userData.receiveDecals=!0;const o=new M({WGSLShaderMaterial:z},e=>[c(16777215,!1===e.userData.receiveDecals?0:2)]),n=o.resolve(e,"opaque");e.userData.receiveDecals=!1,e.needsUpdate=!0;const r=o.resolve(e,"opaque");t(r).not.toBe(n),t(n.disposed).toBe(!0),t(n.fragmentShader).not.toBe(r.fragmentShader)}),o("maps Hology elapsed time to the renderer frame clock",()=>{const e=v(E),o=new u({color:c(x(e)),transform:f(e)}),n=new M({WGSLShaderMaterial:z}).resolve(o);t(n?.vertexShader).toContain("rendererElapsedTime()"),t(n?.fragmentShader).toContain("rendererElapsedTime()"),t(n?.depthVertexShader).toContain("rendererElapsedTime()"),t(n?.pointShadowVertexShader).toContain("rendererElapsedTime()"),t(n?.uniforms).not.toHaveProperty(E)}),o("compiles the native sprite billboard position without a GLSL fallback",()=>{const e=new u({color:c(16777215),position:_(v("rotation",.25),v("screenSpaceSize",0),v("lockYAxis",1))}),o=new M({WGSLShaderMaterial:z}).resolve(e);t(o).toBeInstanceOf(z),t(o?.vertexShader).toContain("cos("),t(o?.vertexShader).toContain("sin("),t(o?.vertexShader).toContain("materialUniforms.lockYAxis"),t(o?.vertexShader).not.toContain("customFunction_")}),o("uses the per-instance rotation attribute for instanced sprites",()=>{const e=new u({color:c(16777215),position:_(v("rotation",0),void 0,void 0,!0)}),o=new M({WGSLShaderMaterial:z}).resolve(e);t(o?.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"rotation",format:"float32",stepMode:"instance"})])),t(o?.vertexShader).toContain("input.rotation"),t(o?.vertexShader).not.toContain("materialUniforms.rotation")}),o("uses per-instance particle opacity in WebGPU fragment shaders",()=>{const e=new u({color:c(m(16777215),O.opacity)}),o=new M({WGSLShaderMaterial:z}).resolve(e);t(o?.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"particleData",format:"float32x3",stepMode:"instance"})])),t(o?.fragmentShader).toContain(".x"),t(o?.fragmentShader).toContain("> 0.5")}),o("maps THREE.SpriteMaterial to the native sprite shader",()=>{const e=new n(new Uint8Array([255,255,255,255]),1,1,i),o=new s({map:e,color:8429760,opacity:.6,rotation:.25,sizeAttenuation:!1,alphaTest:.2,transparent:!0}),r=new M({WGSLShaderMaterial:z}),a=r.resolve(o);t(a).toBeInstanceOf(z),t(a?.vertexShader).toContain("materialUniforms.rotation"),t(a?.vertexShader).toContain("materialUniforms.screenSpaceSize"),t(a?.fragmentShader).toContain("textureSample("),t(a?.alphaTest).toBe(.2),t(a?.transparent).toBe(!0),t(a?.uniforms.rotation.value).toBe(.25),t(a?.uniforms.screenSpaceSize.value).toBe(1),o.rotation=.75;const l=r.resolve(o);t(l).toBe(a),t(l?.uniforms.rotation.value).toBe(.75)}),o("memoizes node-material signatures for repeated lookups",()=>{const e=["version","vertexShader","fragmentShader","alphaTest","alphaToCoverage","blending","colorWrite","depthFunc","depthTest","depthWrite","polygonOffset","polygonOffsetFactor","polygonOffsetUnits","premultipliedAlpha","side","stencilWrite","transparent"],o=new Map([["version",1],["vertexShader","#vertex"],["fragmentShader","#fragment"],["alphaTest",0],["alphaToCoverage",!1],["blending",1],["colorWrite",!0],["depthFunc",3],["depthTest",!0],["depthWrite",!0],["polygonOffset",!1],["polygonOffsetFactor",0],["polygonOffsetUnits",0],["premultipliedAlpha",!1],["side",0],["stencilWrite",!1],["transparent",!1]]);let n=0;const r=new Proxy({},{get:(t,r)=>"string"==typeof r&&e.includes(r)?(n+=1,o.get(r)):Reflect.get(t,r)});y(r),y(r),t(n).toBe(e.length)}),o("adapts a NodeShaderMaterial without replacing its scene material",()=>{const e=v("intensity",.75),o=new u({color:p({color:m(5210052).multiplyScalar(e),roughness:.42,metalness:.18}),transparent:!0,alphaTest:.25}),n=new M({WGSLShaderMaterial:z}),r=n.resolve(o);t(r).toBeInstanceOf(z),t(r).not.toBe(o),t(r?.transparent).toBe(!0),t(r?.alphaTest).toBe(.25),t(r?.fragmentShader).toContain("rendererPointLightForLight"),t(o.isShaderMaterial).toBe(!0),e.value=.25,t(n.resolve(o)).toBe(r),t(r?.uniforms.intensity?.value).toBe(.25)}),o("syncs direct NodeShaderMaterial uniform mutations to the resolved material",()=>{const e=S("trailTint",new l(0,0,0,0)),o=v("hology_particle_energy",1),n=new u({color:e.multiplyScalar(o)}),r=new M({WGSLShaderMaterial:z});n.uniforms.trailTint.value=new l(1,.25,.5,.75),n.uniforms.hology_particle_energy.value=.4;const a=r.resolve(n);t(a?.uniforms.trailTint.value).toEqual(new l(1,.25,.5,.75)),t(a?.uniforms.hology_particle_energy.value).toBe(.4),n.uniforms.trailTint.value.set(.2,.4,.6,.8),n.uniforms.hology_particle_energy.value=.1,t(r.resolve(n)).toBe(a),t(a?.uniforms.trailTint.value).toEqual(new l(.2,.4,.6,.8)),t(a?.uniforms.hology_particle_energy.value).toBe(.1)}),o("only syncs explicitly invalidated uniforms when auto update is disabled",()=>{const e=v("intensity",.75),o=new u({color:c(x(e))});o.uniformValuesAutoUpdate=!1;const n=new M({WGSLShaderMaterial:z}),r=n.resolve(o),a=r.uniformValuesAutoVersion;e.value=.25,t(n.resolve(o)).toBe(r),t(r?.uniforms.intensity.value).toBe(.75),t(r?.uniformValuesAutoVersion).toBe(a),o.uniformValuesNeedsUpdate=!0,t(n.resolve(o)).toBe(r),t(r?.uniforms.intensity.value).toBe(.25),t(r?.uniformValuesAutoVersion).toBe(a+1)}),o("supports explicit invalidation for mapped Three.js materials",()=>{const e=new a({roughness:.8});b(e,!1);const o=new M({WGSLShaderMaterial:z}),n=o.resolve(e);e.roughness=.2,t(o.resolve(e)).toBe(n),t(n?.uniforms.roughness.value).toBe(.8),B(e),t(o.resolve(e)).toBe(n),t(n?.uniforms.roughness.value).toBe(.2)}),o("binds uniforms referenced only by an MRT material output",()=>{const e=v("roughness",.7),o=new u({color:c(5601177),alphaTest:.1});o.outputRoughness=e;const n=new M({WGSLShaderMaterial:z},e=>[e.outputColor,C(x(0,0,1),e.outputRoughness)]).resolve(o);t(n?.fragmentShader).toContain("materialUniforms.roughness"),t(n?.fragmentShader).not.toContain("graphColor"),t(n?.fragmentShader).toMatch(/if \(.+ < 0\.1\) \{ discard; \}/),t(n?.uniforms.roughness?.value).toBe(.7)}),o("compiles a landscape composite built from node material semantic outputs",()=>{const e=new M({WGSLShaderMaterial:z}),o=new u({color:p({color:m(5147450),roughness:.82,metalness:0})}),n=new u({color:p({color:m(7432286),roughness:.6,metalness:.05})}),r=new T;r.layers=[o,n];const a=e.resolve(r.build());t(a).toBeInstanceOf(z),t(a?.fragmentShader).toContain("rendererDirectionalLightForLight"),t(a?.vertexShader).toContain("@location(6) material: vec4<f32>"),t(a?.vertexShader).toContain("@location(8) hole: f32")}),o("allocates the trail's authored vertex attributes",()=>{const e=new M({WGSLShaderMaterial:z}).resolve(F.createBaseMaterial());t(e).toBeInstanceOf(z),t(e?.vertexAttributes).toEqual(t.arrayContaining([t.objectContaining({name:"nodeID",shaderLocation:6,format:"float32"}),t.objectContaining({name:"nodeCenter",shaderLocation:7,format:"float32x3"})]))}),o("uses Three's default value when a geometry has no uv1 attribute",()=>{const e=L([{name:"uv1",shaderLocation:6,format:"float32x2"},{name:"custom",shaderLocation:7,format:"float32"}]);t(e).toEqual(t.arrayContaining([t.objectContaining({name:"uv1",defaultValue:[0,0]}),t.objectContaining({name:"custom"})])),t(e[1]).not.toHaveProperty("defaultValue")}),o("compiles the foliage transform and named texture-uniform pattern",()=>{const e=new M({WGSLShaderMaterial:z}),o=new n(new Uint8Array([255,255,255,255]),1,1,i),r=new u({color:g("map").sample(w.uv),transform:f(v("windOffset",.25)),uniforms:{map:{value:o}},alphaTest:.5}),a=e.resolve(r);t(a).toBeInstanceOf(z),t(a?.vertexShader).toContain("rendererClipPosition(_ng_localPosition"),t(a?.depthVertexShader).toContain("materialUniforms.windOffset"),t(a?.depthFragmentShader).toContain("textureSample("),t(a?.depthFragmentShader).toContain("< 0.5) { discard;"),t(a?.pointShadowFragmentShader).toContain("distanceToLight")}),o("rebuilds cached materials when scene-local texture bindings change",()=>{const e=new M({WGSLShaderMaterial:z}),o=new n(new Uint8Array([255,255,255,255]),1,1,i),r=new n(new Uint8Array([32,64,128,255]),1,1,i),a=new u({color:g("map").sample(w.uv),uniforms:{map:{value:o}},alphaTest:.5}),s=e.resolve(a);a.uniforms.map.value=r;const l=e.resolve(a);t(l).not.toBe(s),t(s.disposed).toBe(!0),a.dispose();const d=e.resolve(a);t(d).not.toBe(l),t(l.disposed).toBe(!0)}),o("uses the renderer batch id without Three’s batchingIdTexture",()=>{const e=new M({WGSLShaderMaterial:z}),o=new n(new Uint8Array([255,96,24,255]),1,1,i),r=new u({color:c(d("vp0")),uniforms:{vp0:{value:o}}}),a=e.resolve(r);t(a).toBeInstanceOf(z),t(a?.vertexShader).toContain("rendererBatchingId(input.instanceIndex)")}),o("compiles depth nodes that use fragment-coordinate expressions",()=>{const e=new M({WGSLShaderMaterial:z}),o=new u({color:c(x(V.x,V.y,U))}),n=e.resolve(o);t(n).toBeInstanceOf(z),t(n?.fragmentShader).toContain("input.position.xy"),t(n?.fragmentShader).toContain("input.position.z"),t(n?.fragmentShader).toContain("rendererViewportSize()"),t(n?.fragmentShader).not.toContain("gl_FragCoord")}),o("maps scene color and depth nodes to renderer-owned textures",()=>{const e=new M({WGSLShaderMaterial:z}),o=new u({color:k(V).multiplyScalar(I(V))}),n=e.resolve(o);t(n.sceneTextures).toEqual({hology_scene_map:"sceneColor",hology_depth_map:"sceneDepth"}),t(n.graphTextures).toEqual({}),t(n.fragmentShader).toContain("hology_scene_mapTexture"),t(n.fragmentShader).toContain("hology_depth_mapTexture"),t(n.fragmentShader).toContain("vec4<f32>(textureSample(hology_depth_mapTexture")}),o("caches distinct material variants for opaque and late draw stages",()=>{const e=new M({WGSLShaderMaterial:z},(e,t)=>"opaque"===t?[c(x(1)),c(x(0))]:void 0),o=new u({color:c(x(1))}),n=e.resolve(o,"opaque"),r=e.resolve(o,"late");t(n).not.toBe(r),t(e.resolve(o,"opaque")).toBe(n),t(e.resolve(o,"late")).toBe(r),t(n?.fragmentShader).toContain("@location(1)"),t(r?.fragmentShader).not.toContain("@location(1)")}),o("reconstructs WebGPU depth positions with inverted NDC Y",()=>{const e=new M({WGSLShaderMaterial:z}),o=new u({color:c(A.add(G.multiplyScalar(0)))}),n=e.resolve(o);t(n.fragmentShader).toContain("vec2<f32>(2.0, -2.0)"),t(n.fragmentShader).toMatch(/-\(float_op_\d+\)/)}),o("emits dither and parallax nodes without GLSL custom functions",()=>{const e=new M({WGSLShaderMaterial:z}),o=new n(new Uint8Array([128,128,128,255]),1,1,i),r=D(w.uv,g("heightMap",o),h(.03)),a=new u({color:C(r.x,r.y,j(x(.2,.4,.8)).z,1)}),s=e.resolve(a);t(s).toBeInstanceOf(z),t(s?.fragmentShader).toContain("textureSampleLevel(heightMapTexture, heightMapSampler"),t(s?.fragmentShader).toMatch(/= -dpdy\(-pom_\d+ViewPosition\)/),t(s?.fragmentShader).toMatch(/= -dpdy\(pom_\d+Uv\)/),t(s?.fragmentShader).not.toContain("customFunction_"),t(s?.fragmentShader).not.toContain("gl_FragCoord")}),o("inherits, updates, and clears the WebGPU world environment",()=>{const e=new n(new Uint8Array([255,255,255,255]),1,1,i),o=new u({color:p({color:m(8421504),roughness:.5,metalness:.1})}),r=new M({WGSLShaderMaterial:z});r.setWorldEnvironment({texture:e,intensity:.75});const a=r.resolve(o);t(a.fragmentShader).toContain("envMapTexture"),t(a.uniforms.envMapIntensity?.value).toBe(.75),t(o.envMap).toBeUndefined(),r.setWorldEnvironment({texture:e,intensity:.25});const s=r.resolve(o);t(s).toBe(a),t(s.uniforms.envMapIntensity?.value).toBe(.25),r.setWorldEnvironment(null);const l=r.resolve(o);t(l).not.toBe(a),t(a.disposed).toBe(!0),t(l.fragmentShader).not.toContain("envMapTexture")}),o("keeps an authored material environment ahead of the world environment",()=>{const e=new n(new Uint8Array([255,255,255,255]),1,1,i),o=new n(new Uint8Array([64,96,128,255]),1,1,i),r=new u({color:p({color:m(8421504),roughness:.5,metalness:.1}),envMap:o}),a=new M({WGSLShaderMaterial:z});a.setWorldEnvironment({texture:e,intensity:.2}),a.resolve(r),t(r.uniforms.envMap.value).toBe(o),t(r.envMap).toBe(o)})});/*
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 r}from"vitest";import*as n from"three";import{compileNodeShaderMaterialForWebGpu as a,float as s,NodeShaderMaterial as i,uniformFloat as l}from"../shader-nodes/index.js";import{depthUniformName as u,farUniformName as m,linearEyeDepth as f,nearUniformName as d,resolutionUniformName as p,screenUV as c,sceneNormalUniformName as h}from"../shader-nodes/depth.js";import{aoMapUniformName as g}from"../shader-nodes/scene-sample.js";import{elapsedTimeUniformName as T,time as v}from"../shader-nodes/time.js";import{sampleSceneColor as b}from"../shader-nodes/scene-sample.js";import{NodeMaterialWebGpuResolver as w}from"../rendering/node-material-webgpu-resolver.js";import{DecalStandardShader as A}from"../shader/builtin/decal-standard-shader.js";import{applyPostProcessEffectUniformState as y,defaultPostProcessEffectStage as C,PostProcessEffectPass as B,PostProcessEffectRegistration as P,postProcessEffectStages as V}from"../rendering/post-process-effect.js";import{createPostProcessMaterial as x,PostProcessShader as S}from"../shader/post-process-shader.js";r.hoisted(()=>{if("undefined"!=typeof window){if("undefined"!=typeof HTMLCanvasElement){const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&r.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}class e{constructor(){this.currentTime=0,this.destination={},this.listener={positionX:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},positionY:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},positionZ:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},forwardX:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},forwardY:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},forwardZ:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},upX:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},upY:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},upZ:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()}}}createGain(){return{connect:r.fn(),disconnect:r.fn(),gain:{value:1,setValueAtTime:r.fn()}}}}Object.defineProperty(window,"AudioContext",{configurable:!0,value:e}),Object.defineProperty(window,"webkitAudioContext",{configurable:!0,value:e})}});class j extends S{output(){return s(1)}}e("post-process effect",()=>{o("exports stable stage names with a default late-stage slot",()=>{t(V).toEqual(["beforeFog","beforeDepthOfField","beforeColorAdjustment","beforeOutline","beforeAntiAliasing","beforeLut","beforeOutput"]),t(C).toBe("beforeOutput")}),o("builds post-process shaders with fullscreen-safe defaults",()=>{const e=(new j).build();t(e).toBeInstanceOf(i),t(e.depthWrite).toBe(!1),t(e.depthTest).toBe(!1),t(e.toneMapped).toBe(!1)}),o("updates shared renderer uniforms on post-process effect materials",()=>{const e=x(s(1));e.uniforms[p]={value:new n.Vector2},e.uniforms[u]={value:null},e.uniforms[h]={value:null},e.uniforms[d]={value:0},e.uniforms[m]={value:0},e.uniforms[T]={value:0},e.uniforms[g]={value:null};const o=new n.Texture,r=new n.Texture,a=new n.Texture,i=new n.Vector2(640,360);y(e,{aoEnabled:!0,aoTexture:a,cameraFar:500,cameraNear:.25,depthTexture:o,normalTexture:r,resolution:i,simulationTime:12.5}),t(e.uniforms[p].value.toArray()).toEqual([640,360]),t(e.uniforms[u].value).toBe(o),t(e.uniforms[h].value).toBe(r),t(e.uniforms[d].value).toBe(.25),t(e.uniforms[m].value).toBe(500),t(e.uniforms[T].value).toBe(12.5),t(e.uniforms[g].value).toBe(a),t(e.defines.USE_SSAO_MAP).toBe(""),y(e,{aoEnabled:!1,aoTexture:null,cameraFar:500,cameraNear:.25,depthTexture:o,normalTexture:r,resolution:i,simulationTime:13}),t(e.uniforms[g].value).toBeNull(),t(e.defines.USE_SSAO_MAP).toBeUndefined()}),o("compiles screen UV against the WebGPU fullscreen ABI",()=>{const e=x(b(c)),o=new w({}).compileFullscreen(e);t(o.fragmentShader).toContain("input.uv"),t(o.fragmentShader).not.toContain("rendererCamera.viewportAndTime"),t(o.fragmentShader).not.toContain("gl_FragCoord"),t(o.uniforms[p]).toBeUndefined();const r=a(e);t(r.fragmentShader).not.toContain("hologyFullscreenUv"),t(r.fragmentShader).toContain("input.position.xy"),t(r.fragmentShader).toContain("materialUniforms.hology_resolution"),t(r.fragmentShader).toContain("(input.position.xy / max(materialUniforms.hology_resolution, vec2<f32>(1.0, 1.0)))");const n=new w({}).compileFullscreen(x(v));t(n.fragmentShader).not.toContain("rendererElapsedTime"),t(n.uniforms[T]).toBeTruthy();const s=new w({}).compileFullscreen(x(f));t(s.fragmentShader).toContain("input.uv"),t(s.fragmentShader).toContain("textureSample(hology_depth_mapTexture, hology_depth_mapSampler,")}),o("uses runtime material uniform values in WebGPU fullscreen passes",()=>{const e="hology_post_process_weight",o=x(l(e,1)),r=new w({}).compileFullscreen(o);o.uniforms[e].value=.25,r.syncLiveUniforms(r.uniforms),t(r.uniforms[e].value).toBe(.25)}),o("keeps WebGPU decal varyings within the portable location limit",()=>{const e=(new A).build();t(e).toBeInstanceOf(i);const o=a(e),r=[...o.vertexShader.matchAll(/@location\((\d+)\)/g)].map(e=>Number(e[1]));t(Math.max(...r)).toBeLessThan(16),t(o.vertexShader).not.toContain("undefined"),t(o.fragmentShader).not.toContain("undefined")}),o("keeps registration state shared with both renderer adapters",()=>{const e=x(s(1)),o=new B(e),n={id:7,material:e,pass:o,enabled:!0,priority:0,stage:"beforeOutput"},a={refreshPostProcessEffectPassOrder:r.fn(),removePostProcessEffectEntry:r.fn()},i=new P(a,n);t(i.material).toBe(e),i.enabled=!1,t(n.enabled).toBe(!1),t(o.enabled).toBe(!1),i.priority=3,i.stage="beforeFog",t(n.priority).toBe(3),t(n.stage).toBe("beforeFog"),t(a.refreshPostProcessEffectPassOrder).toHaveBeenCalledTimes(3),i.dispose(),i.dispose(),t(a.removePostProcessEffectEntry).toHaveBeenCalledTimes(1),o.dispose()}),o("exposes post-process effect registration through ViewController",async()=>{const e=x(s(1)),o={material:e,enabled:!1,priority:4,stage:"beforeColorAdjustment",dispose:r.fn()},a=r.fn().mockReturnValue(o),{ViewController:i}=await import("../gameplay/services/render.js"),l=new i({camera:new n.PerspectiveCamera,running:!1,paused:!1,onLoop:r.fn(),setCamera(e){this.camera=e},stop:r.fn(),addPostProcessEffect:a,addPostProcessVolume:r.fn(),removePostProcessVolume:r.fn()}).addPostProcessEffect(e,{enabled:!1,priority:4,stage:"beforeColorAdjustment"});t(a).toHaveBeenCalledWith(e,{enabled:!1,priority:4,stage:"beforeColorAdjustment"}),t(l).toBe(o)})});/*
1
+ import{describe as e,expect as t,it as o,vi as r}from"vitest";import*as n from"three";import{compileNodeShaderMaterialForWebGpu as a,float as s,NodeShaderMaterial as i,uniformFloat as l}from"../shader-nodes/index.js";import{depthUniformName as m,farUniformName as u,linearEyeDepth as f,nearUniformName as d,resolutionUniformName as p,screenUV as c,sceneNormalUniformName as h}from"../shader-nodes/depth.js";import{aoMapUniformName as g}from"../shader-nodes/scene-sample.js";import{elapsedTimeUniformName as T,time as v}from"../shader-nodes/time.js";import{sampleSceneColor as b}from"../shader-nodes/scene-sample.js";import{waterVolumeMask as w,waterVolumeWorldToLocalUniformName as A}from"../shader-nodes/water-volume.js";import{NodeMaterialWebGpuResolver as C}from"../rendering/node-material-webgpu-resolver.js";import{DecalStandardShader as y}from"../shader/builtin/decal-standard-shader.js";import{applyPostProcessEffectUniformState as B,defaultPostProcessEffectStage as P,PostProcessEffectPass as V,PostProcessEffectRegistration as x,postProcessEffectStages as S}from"../rendering/post-process-effect.js";import{createPostProcessMaterial as j,PostProcessShader as E}from"../shader/post-process-shader.js";r.hoisted(()=>{if("undefined"!=typeof window){if("undefined"!=typeof HTMLCanvasElement){const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&r.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}class e{constructor(){this.currentTime=0,this.destination={},this.listener={positionX:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},positionY:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},positionZ:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},forwardX:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},forwardY:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},forwardZ:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},upX:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},upY:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()},upZ:{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()}}}createGain(){return{connect:r.fn(),disconnect:r.fn(),gain:{value:1,setValueAtTime:r.fn()}}}}Object.defineProperty(window,"AudioContext",{configurable:!0,value:e}),Object.defineProperty(window,"webkitAudioContext",{configurable:!0,value:e})}});class _ extends E{output(){return s(1)}}e("post-process effect",()=>{o("exports stable stage names with a default late-stage slot",()=>{t(S).toEqual(["beforeFog","beforeDepthOfField","beforeColorAdjustment","beforeOutline","beforeAntiAliasing","beforeLut","beforeOutput"]),t(P).toBe("beforeOutput")}),o("builds post-process shaders with fullscreen-safe defaults",()=>{const e=(new _).build();t(e).toBeInstanceOf(i),t(e.depthWrite).toBe(!1),t(e.depthTest).toBe(!1),t(e.toneMapped).toBe(!1)}),o("updates shared renderer uniforms on post-process effect materials",()=>{const e=j(s(1));e.uniforms[p]={value:new n.Vector2},e.uniforms[m]={value:null},e.uniforms[h]={value:null},e.uniforms[d]={value:0},e.uniforms[u]={value:0},e.uniforms[T]={value:0},e.uniforms[g]={value:null};const o=new n.Texture,r=new n.Texture,a=new n.Texture,i=new n.Vector2(640,360);B(e,{aoEnabled:!0,aoTexture:a,cameraFar:500,cameraNear:.25,depthTexture:o,normalTexture:r,resolution:i,simulationTime:12.5}),t(e.uniforms[p].value.toArray()).toEqual([640,360]),t(e.uniforms[m].value).toBe(o),t(e.uniforms[h].value).toBe(r),t(e.uniforms[d].value).toBe(.25),t(e.uniforms[u].value).toBe(500),t(e.uniforms[T].value).toBe(12.5),t(e.uniforms[g].value).toBe(a),t(e.defines.USE_SSAO_MAP).toBe(""),B(e,{aoEnabled:!1,aoTexture:null,cameraFar:500,cameraNear:.25,depthTexture:o,normalTexture:r,resolution:i,simulationTime:13}),t(e.uniforms[g].value).toBeNull(),t(e.defines.USE_SSAO_MAP).toBeUndefined()}),o("compiles screen UV against the WebGPU fullscreen ABI",()=>{const e=j(b(c)),o=new C({}).compileFullscreen(e);t(o.fragmentShader).toContain("input.uv"),t(o.fragmentShader).not.toContain("rendererCamera.viewportAndTime"),t(o.fragmentShader).not.toContain("gl_FragCoord"),t(o.uniforms[p]).toBeUndefined();const r=a(e);t(r.fragmentShader).not.toContain("hologyFullscreenUv"),t(r.fragmentShader).toContain("input.position.xy"),t(r.fragmentShader).toContain("materialUniforms.hology_resolution"),t(r.fragmentShader).toContain("(input.position.xy / max(materialUniforms.hology_resolution, vec2<f32>(1.0, 1.0)))");const n=new C({}).compileFullscreen(j(v));t(n.fragmentShader).not.toContain("rendererElapsedTime"),t(n.uniforms[T]).toBeTruthy();const s=new C({}).compileFullscreen(j(f));t(s.fragmentShader).toContain("input.uv"),t(s.fragmentShader).toContain("textureSample(hology_depth_mapTexture, hology_depth_mapSampler,")}),o("uses runtime material uniform values in WebGPU fullscreen passes",()=>{const e="hology_post_process_weight",o=j(l(e,1)),r=new C({}).compileFullscreen(o);o.uniforms[e].value=.25,r.syncLiveUniforms(r.uniforms),t(r.uniforms[e].value).toBe(.25)}),o("compiles the depth-reconstructed water volume mask for WebGPU fullscreen passes",()=>{const e=j(w),o=new C({}).compileFullscreen(e);t(o.fragmentShader).toContain("hology_depth_map"),t(o.uniforms[A]).toBeTruthy(),t(o.fragmentShader).not.toContain("rendererViewMatrix"),t(o.fragmentShader).not.toContain("rendererProjectionMatrix")}),o("keeps WebGPU decal varyings within the portable location limit",()=>{const e=(new y).build();t(e).toBeInstanceOf(i);const o=a(e),r=[...o.vertexShader.matchAll(/@location\((\d+)\)/g)].map(e=>Number(e[1]));t(Math.max(...r)).toBeLessThan(16),t(o.vertexShader).not.toContain("undefined"),t(o.fragmentShader).not.toContain("undefined")}),o("keeps registration state shared with both renderer adapters",()=>{const e=j(s(1)),o=new V(e),n={id:7,material:e,pass:o,enabled:!0,priority:0,stage:"beforeOutput"},a={refreshPostProcessEffectPassOrder:r.fn(),removePostProcessEffectEntry:r.fn()},i=new x(a,n);t(i.material).toBe(e),i.enabled=!1,t(n.enabled).toBe(!1),t(o.enabled).toBe(!1),i.priority=3,i.stage="beforeFog",t(n.priority).toBe(3),t(n.stage).toBe("beforeFog"),t(a.refreshPostProcessEffectPassOrder).toHaveBeenCalledTimes(3),i.dispose(),i.dispose(),t(a.removePostProcessEffectEntry).toHaveBeenCalledTimes(1),o.dispose()}),o("exposes post-process effect registration through ViewController",async()=>{const e=j(s(1)),o={material:e,enabled:!1,priority:4,stage:"beforeColorAdjustment",dispose:r.fn()},a=r.fn().mockReturnValue(o),{ViewController:i}=await import("../gameplay/services/render.js"),l=new i({camera:new n.PerspectiveCamera,running:!1,paused:!1,onLoop:r.fn(),setCamera(e){this.camera=e},stop:r.fn(),addPostProcessEffect:a,addPostProcessVolume:r.fn(),removePostProcessVolume:r.fn()}).addPostProcessEffect(e,{enabled:!1,priority:4,stage:"beforeColorAdjustment"});t(a).toHaveBeenCalledWith(e,{enabled:!1,priority:4,stage:"beforeColorAdjustment"}),t(l).toBe(o)})});/*
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("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 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}}}/*
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=spline-component.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as t,it as n,vi as o}from"vitest";import{Vector3 as s}from"three";import{SplineComponent as i,packSplineData as a}from"../gameplay/actors/builtin/components/spline-component.js";import{extractShaderParameters as r}from"../shader/parameter.js";function p(e=!1){const t=new i;return t.setData(a([{position:new s(0,0,0),inTangent:new s,outTangent:new s(10/3,0,0)},{position:new s(10,0,0),inTangent:new s(-10/3,0,0),outTangent:new s}],e),!1),t}o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=o.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),e("SplineComponent",()=>{n("samples cubic position and tangent",()=>{const e=p();t(e.samplePosition(.5).x).toBeCloseTo(5),t(e.sampleTangent(.5).toArray()).toEqual([1,0,0])}),n("builds and uses an arc-length approximation",()=>{const e=p();t(e.length).toBeCloseTo(10),t(e.samplePositionAtDistance(2.5).x).toBeCloseTo(2.5,2)}),n("finds the closest point and distance along the spline",()=>{const e=p().closestPoint(new s(4,3,0));t(e.position.x).toBeCloseTo(4,2),t(e.distance).toBeCloseTo(3,2),t(e.distanceAlongSpline).toBeCloseTo(4,2)}),n("round trips compact point data",()=>{const e=p(!0),n=new i;n.setData(e.splineData,!1),t(n.closed).toBe(!0),t(n.points[1].inTangent.x).toBeCloseTo(-10/3),t(e.splineData).not.toContain("\n")}),n("declares spline data as a string editor parameter",()=>{const e=r(i).find(e=>"splineData"===e.name);t(e?.type).toBe(String)}),n("inserts a point without changing the Bezier shape",()=>{const e=p(),n=Array.from({length:9},(t,n)=>e.samplePosition(n/8).clone());t(e.insertPointOnSegment(0)).toBe(1),t(e.points).toHaveLength(3),n.forEach((n,o)=>{t(e.samplePosition(o/8).distanceTo(n)).toBeLessThan(1e-5)})})});/*
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=view-controller-updates.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as t,it as o,vi as r}from"vitest";import*as n from"three";import{Subject as s}from"rxjs";import{ViewController as a}from"../gameplay/services/render.js";function i(){let e;const t={camera:new n.PerspectiveCamera,running:!1,paused:!1,simulationTimeScale:1,onLoop(t){e=t},stop:r.fn()};return{viewController:new a(t),stepFrame(t){e?.(t)}}}r.hoisted(()=>{if("undefined"!=typeof HTMLCanvasElement){HTMLCanvasElement.prototype.getContext=r.fn(()=>new Proxy({},{get:(e,t)=>(t in e||(e[t]=r.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)}));class e{constructor(){this.currentTime=0,this.destination={},this.listener=Object.fromEntries(["positionX","positionY","positionZ","forwardX","forwardY","forwardZ","upX","upY","upZ"].map(e=>[e,{linearRampToValueAtTime:r.fn(),setValueAtTime:r.fn()}]))}createGain(){return{connect:r.fn(),disconnect:r.fn(),gain:{value:1,setValueAtTime:r.fn()}}}}Object.defineProperty(window,"AudioContext",{configurable:!0,value:e}),Object.defineProperty(window,"webkitAudioContext",{configurable:!0,value:e})}}),e("ViewController actor updates",()=>{o("dispatches framework updates directly and preserves observable updates",()=>{const{viewController:e,stepFrame:o}=i(),r={disposed:new s},n=[];e.onUpdate().subscribe(()=>n.push("observable")),e.registerActorUpdate(r,()=>n.push("actor")),e.registerActorUpdate(r,()=>n.push("late"),!0),o(1/60),t(n).toEqual(["observable","actor","late"])}),o("stops actor-scoped observable and direct updates when disposed",()=>{const{viewController:e,stepFrame:o}=i(),n={disposed:new s},a=r.fn(),p=r.fn();e.onUpdate(n).subscribe(a),e.registerActorUpdate(n,p),o(1/60),n.disposed.next(!0),o(1/60),t(a).toHaveBeenCalledTimes(1),t(p).toHaveBeenCalledTimes(1)})});/*
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=water-spline-volume-actor.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as t,it as n,vi as o}from"vitest";import{Box3 as a,ShaderMaterial as i,Vector3 as s}from"three";import{NavigationArea as r}from"../gameplay/ai/navigation-areas.js";import{createWaterSplineSurfaceGeometry as l,WaterSplineVolume as c}from"../gameplay/actors/builtin/water-spline-volume-actor.js";import{packSplineData as u}from"../gameplay/actors/builtin/components/spline-component.js";function p(e,t){e.spline.setData(u([{position:new s,inTangent:new s,outTangent:new s(t/3,0,0)},{position:new s(t,0,0),inTangent:new s(-t/3,0,0),outTangent:new s}],!1),!1)}o.hoisted(()=>{if("undefined"!=typeof HTMLCanvasElement){const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=o.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),e("WaterSplineVolume",()=>{n("creates water navigation prisms centered below the spline surface",()=>{const e=new c;e.width=4,e.depth=3,p(e,10);const n=e.getNavigationAreaModifiers();t(n).toHaveLength(20),t(n.every(e=>e.area===r.Water)).toBe(!0),t(n[0].vertices[0]).toEqual([0,0,2]),t(n[0].vertices[1]).toEqual([0,0,-2]),t(n[0].minHeight).toBe(-3),t(n[0].maxHeight).toBe(0)}),n("rebuilds the editor visualization when the spline changes",()=>{const e=new c;e.width=2,e.depth=1,p(e,2);const n=Object.values(e).find(e=>"WaterSplineVolumeEditorComponent"===e?.constructor?.name);n.onInit();const o=[...n.visual.children],i=(new a).setFromObject(n.visual);t(i.max.x).toBeCloseTo(2),p(e,6),e.spline.changed.next(e.spline),i.setFromObject(n.visual),t(i.max.x).toBeCloseTo(6),t(i.min.y).toBeCloseTo(-1),t(n.visual.children).toEqual(o),t(o.every(e=>!1===e.frustumCulled)).toBe(!0)}),n("clamps offset joins at sharp turns",()=>{const e=new c;e.width=10,e.depth=1,e.spline.setData(u([{position:new s(-1,0,0),inTangent:new s,outTangent:new s(1/3,0,0)},{position:new s(0,0,0),inTangent:new s(-1/3,0,0),outTangent:new s(0,0,1/3)},{position:new s(0,0,1),inTangent:new s(0,0,-1/3),outTangent:new s}],!1),!1);const n=Object.values(e).find(e=>"WaterSplineVolumeEditorComponent"===e?.constructor?.name);n.onInit();const o=(new a).setFromObject(n.visual);t(o.min.x).toBeGreaterThanOrEqual(-1-e.width*g/2),t(o.max.x).toBeLessThanOrEqual(e.width*g/2),t(o.min.z).toBeGreaterThanOrEqual(-e.width*g/2),t(o.max.z).toBeLessThanOrEqual(1+e.width*g/2);const i=l(e.spline,e.width,.1,1),r=i.getAttribute("position"),p=i.getIndex();for(let e=0;e<p.count;e+=3){const n=p.getX(e),o=p.getX(e+1),a=p.getX(e+2),i=(r.getX(o)-r.getX(n))*(r.getZ(a)-r.getZ(n))-(r.getZ(o)-r.getZ(n))*(r.getX(a)-r.getX(n));t(i).toBeLessThanOrEqual(1e-6)}}),n("creates tessellated surface data for water shaders",()=>{const e=new c;p(e,10);const n=l(e.spline,4,2,2),o=n.getAttribute("position").count;t(o).toBeGreaterThan(0),t(n.getAttribute("uv").count).toBe(o),t(n.getAttribute("waterFlowDirection").count).toBe(o),t(Math.max(...Array.from({length:o},(e,t)=>n.getAttribute("waterDistanceFromBank").getX(t)))).toBeGreaterThan(0),t(Math.max(...Array.from({length:o},(e,t)=>n.getAttribute("waterDistanceAlongSpline").getX(t)))).toBeCloseTo(10)}),n("classifies points inside the authored spline prism",()=>{const e=new c;e.width=4,e.depth=3,p(e,10),t(e.containsWaterPoint(new s(5,-1,0))).toBe(!0),t(e.containsWaterPoint(new s(5,1,0))).toBe(!1),t(e.containsWaterPoint(new s(5,-4,0))).toBe(!1),t(e.containsWaterPoint(new s(5,-1,3))).toBe(!1)}),n("applies underwater graph changes without recreating the actor",()=>{const e=new c,n=new i;t(e.underwater.applySceneParameters({postProcessMaterial:n},new Set(["postProcessMaterial"]))).toBe(!0),t(e.underwater.postProcessMaterial).toBe(n)})});const g=1.5;/*
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=worker-task-executor.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as t,it as s,vi as a}from"vitest";import{WorkerTaskExecutor as o}from"../worker/task-executor.js";e("WorkerTaskExecutor",()=>{s("creates workers lazily, routes results by id, and caps concurrency",async()=>{const e=[],s=new o(()=>{const t=new r;return e.push(t),t},2),a=Array.from({length:5},(e,t)=>s.execute("double",t));t(e).toHaveLength(2),t(e.flatMap(e=>e.messages)).toHaveLength(2),e[0].complete(0),t(e.flatMap(e=>e.messages)).toHaveLength(3),e[1].complete(2),e[0].complete(4),e[1].complete(6),e[0].complete(8),await t(Promise.all(a)).resolves.toEqual([0,2,4,6,8])}),s("prioritizes high-priority tasks ahead of queued background work",()=>{const e=new r,s=new o(()=>e);s.execute("work","active"),s.execute("work","background",{priority:"background"}),s.execute("work","high",{priority:"high"}),e.complete("done"),t(e.messages[1]).toMatchObject({payload:"high"})}),s("rejects a cancelled task and sends cancellation to an active worker",async()=>{const e=new r,s=new o(()=>e),a=new AbortController,c=s.execute("work",{},{signal:a.signal});a.abort(),await t(c).rejects.toMatchObject({name:"AbortError"}),t(e.messages[1]).toMatchObject({kind:"cancel"})}),s("rejects the active task and replaces a failed worker",async()=>{const e=[],s=new o(()=>{const t=new r;return e.push(t),t}),a=s.execute("work",1),c=s.execute("work",2);e[0].fail("crashed"),await t(a).rejects.toThrow("crashed"),t(e).toHaveLength(2),e[1].complete(2),await t(c).resolves.toBe(2)})});class r{constructor(){this.onmessage=null,this.onerror=null,this.messages=[],this.terminate=a.fn()}postMessage(e){this.messages.push(e)}complete(e){const t=[...this.messages].reverse().find(e=>"task"===e.kind);this.onmessage?.({data:{kind:"result",id:t.id,payload:e}})}fail(e){this.onerror?.({message:e})}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,5 +1,14 @@
1
- export declare function registerWorker(builder: () => Worker): void;
1
+ import type { ExecuteTaskOptions } from './task-executor.js';
2
+ import type { TaskHandlers } from './task.js';
3
+ export type RegisterWorkerOptions = {
4
+ maxWorkers?: number;
5
+ };
6
+ export declare function registerWorker(builder: () => Worker, options?: RegisterWorkerOptions): void;
2
7
  export declare function hasWorkerBuilder(): boolean;
3
8
  export declare function createWorker(): Worker;
4
- export declare function initWorker(): void;
9
+ export declare function executeTask<TInput, TOutput>(type: string, input: TInput, options?: ExecuteTaskOptions<TOutput>): Promise<TOutput>;
10
+ export declare function initWorker(additionalHandlers?: TaskHandlers): void;
11
+ export type { ExecuteTaskOptions, TaskPriority } from './task-executor.js';
12
+ export type { TaskHandler, TaskHandlerResult, TaskHandlers } from './task.js';
13
+ export { TaskCancelledError, TaskExecutionError } from './task.js';
5
14
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,4 @@
1
- import{setupNavMeshWorker as r}from"../gameplay/ai/dynamic-tiled-navmesh.worker.js";let e;export function registerWorker(r){e=r}export function hasWorkerBuilder(){return null!=e}export function createWorker(){return e()}export function initWorker(){r()}/*
1
+ import{navMeshTaskHandlers as r}from"../gameplay/ai/dynamic-tiled-navmesh.worker.js";import{WorkerTaskExecutor as e}from"./task-executor.js";import{setupTaskWorker as o}from"./task-worker.js";let t,n;export function registerWorker(r,o={}){n?.terminate(),t=r,n=new e(r,o.maxWorkers)}export function hasWorkerBuilder(){return null!=t}export function createWorker(){if(null==t)throw new Error("No worker factory has been registered");return t()}export function executeTask(r,e,o={}){return null!=n?n.execute(r,e,o):o.signal?.aborted?Promise.reject(new DOMException("Task execution was cancelled","AbortError")):null!=o.fallback?Promise.resolve().then(o.fallback):Promise.reject(new Error(`No worker is registered and task "${r}" has no inline fallback`))}export function initWorker(e={}){o({...r,...e})}export{TaskCancelledError,TaskExecutionError}from"./task.js";/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,22 @@
1
+ export type TaskPriority = 'background' | 'normal' | 'high';
2
+ export type ExecuteTaskOptions<TOutput> = {
3
+ transfer?: Transferable[];
4
+ signal?: AbortSignal;
5
+ priority?: TaskPriority;
6
+ fallback?: () => TOutput | Promise<TOutput>;
7
+ };
8
+ export declare class WorkerTaskExecutor {
9
+ private readonly workerFactory;
10
+ private slots;
11
+ private queue;
12
+ private nextTaskId;
13
+ private readonly workerLimit;
14
+ constructor(workerFactory: () => Worker, maxWorkers?: number);
15
+ execute<TInput, TOutput>(type: string, payload: TInput, options?: ExecuteTaskOptions<TOutput>): Promise<TOutput>;
16
+ terminate(): void;
17
+ private cancel;
18
+ private drain;
19
+ private createSlot;
20
+ private finish;
21
+ }
22
+ //# sourceMappingURL=task-executor.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{TaskCancelledError as t,TaskExecutionError as s}from"./task.js";const e={background:0,normal:1,high:2};export class WorkerTaskExecutor{constructor(t,s=1){this.workerFactory=t,this.slots=[],this.queue=[],this.nextTaskId=1,this.workerLimit=Number.isFinite(s)?Math.max(1,Math.floor(s)):1}execute(s,i,r={}){return r.signal?.aborted?Promise.reject(new t):new Promise((t,n)=>{const o={id:this.nextTaskId++,type:s,payload:i,transfer:r.transfer,signal:r.signal,priority:e[r.priority??"normal"],resolve:t,reject:n};null!=o.signal&&(o.abort=()=>this.cancel(o),o.signal.addEventListener("abort",o.abort,{once:!0}));const a=this.queue.findIndex(t=>t.priority<o.priority);a<0?this.queue.push(o):this.queue.splice(a,0,o),this.drain()})}terminate(){const s=new t;for(const t of this.queue.splice(0))this.finish(t,()=>t.reject(s));for(const t of this.slots)null!=t.task&&this.finish(t.task,()=>t.task?.reject(s)),t.worker.terminate();this.slots=[]}cancel(s){const e=this.queue.indexOf(s);if(e>=0)return this.queue.splice(e,1),void this.finish(s,()=>s.reject(new t));const i=this.slots.find(t=>t.task===s);null!=i&&(i.worker.postMessage({kind:"cancel",id:s.id}),this.finish(s,()=>s.reject(new t)),i.task=void 0,this.drain())}drain(){for(;this.queue.length>0;){let s=this.slots.find(t=>null==t.task);if(null==s){if(this.slots.length>=this.workerLimit)return;s=this.createSlot(),this.slots.push(s)}const e=this.queue.shift();if(e.signal?.aborted){this.finish(e,()=>e.reject(new t));continue}s.task=e;const i={kind:"task",id:e.id,type:e.type,payload:e.payload};s.worker.postMessage(i,e.transfer)}}createSlot(){const t={worker:this.workerFactory()};return t.worker.onmessage=e=>{const i=t.task;null!=i&&e.data.id===i.id&&(t.task=void 0,this.finish(i,()=>{"result"===e.data.kind?i.resolve(e.data.payload):i.reject(new s(e.data.error))}),this.drain())},t.worker.onerror=s=>{const e=t.task;t.task=void 0,t.worker.terminate();const i=this.slots.indexOf(t);i>=0&&this.slots.splice(i,1),null!=e&&this.finish(e,()=>e.reject(new Error(s.message||"Worker task failed"))),this.drain()},t}finish(t,s){null!=t.abort&&t.signal?.removeEventListener("abort",t.abort),s()}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,3 @@
1
+ import type { TaskHandlers } from './task.js';
2
+ export declare function setupTaskWorker(handlers: TaskHandlers): void;
3
+ //# sourceMappingURL=task-worker.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{serializeTaskError as e}from"./task.js";export function setupTaskWorker(r){const s=new Map;self.onmessage=o=>{const t=o.data;if("cancel"===t.kind)return void s.get(t.id)?.abort();const n=r[t.type];if(null==n){const e={kind:"error",id:t.id,error:{name:"UnknownTaskError",message:`No worker task handler is registered for "${t.type}"`}};return void self.postMessage(e)}const a=new AbortController;s.set(t.id,a),Promise.resolve(n(t.payload,a.signal)).then(e=>{if(a.signal.aborted)return;const r={kind:"result",id:t.id,payload:e.payload};self.postMessage(r,e.transfer)}).catch(r=>{if(a.signal.aborted)return;const s={kind:"error",id:t.id,error:e(r)};self.postMessage(s)}).finally(()=>s.delete(t.id))}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,39 @@
1
+ export type WorkerTaskRequest = {
2
+ kind: 'task';
3
+ id: number;
4
+ type: string;
5
+ payload: unknown;
6
+ };
7
+ export type WorkerTaskCancel = {
8
+ kind: 'cancel';
9
+ id: number;
10
+ };
11
+ export type SerializedTaskError = {
12
+ name: string;
13
+ message: string;
14
+ stack?: string;
15
+ };
16
+ export type WorkerTaskResponse = {
17
+ kind: 'result';
18
+ id: number;
19
+ payload: unknown;
20
+ } | {
21
+ kind: 'error';
22
+ id: number;
23
+ error: SerializedTaskError;
24
+ };
25
+ export type WorkerTaskMessage = WorkerTaskRequest | WorkerTaskCancel;
26
+ export type TaskHandlerResult<T> = {
27
+ payload: T;
28
+ transfer?: Transferable[];
29
+ };
30
+ export type TaskHandler<TInput = unknown, TOutput = unknown> = (input: TInput, signal: AbortSignal) => TaskHandlerResult<TOutput> | Promise<TaskHandlerResult<TOutput>>;
31
+ export type TaskHandlers = Record<string, TaskHandler>;
32
+ export declare class TaskExecutionError extends Error {
33
+ constructor(error: SerializedTaskError);
34
+ }
35
+ export declare class TaskCancelledError extends Error {
36
+ constructor();
37
+ }
38
+ export declare function serializeTaskError(error: unknown): SerializedTaskError;
39
+ //# sourceMappingURL=task.d.ts.map
@@ -0,0 +1,4 @@
1
+ export class TaskExecutionError extends Error{constructor(r){super(r.message),this.name=r.name,this.stack=r.stack}}export class TaskCancelledError extends Error{constructor(){super("Task execution was cancelled"),this.name="AbortError"}}export function serializeTaskError(r){return r instanceof Error?{name:r.name,message:r.message,stack:r.stack}:{name:"Error",message:String(r)}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hology/core",
3
- "version": "0.0.246",
3
+ "version": "0.0.248",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -203,7 +203,7 @@
203
203
  "license": "SEE LICENSE IN LICENSE.md",
204
204
  "peerDependencies": {
205
205
  "three": "0.169.0",
206
- "@hology/webgpu-renderer": "^0.1.0"
206
+ "@hology/webgpu-renderer": "^0.1.12"
207
207
  },
208
208
  "peerDependenciesMeta": {
209
209
  "@hology/webgpu-renderer": {
@@ -239,6 +239,6 @@
239
239
  "terser": "^5.25.0",
240
240
  "typescript": "~5.8.2",
241
241
  "vitest": "^4.1.9",
242
- "@hology/webgpu-renderer": "^0.1.10"
242
+ "@hology/webgpu-renderer": "^0.1.12"
243
243
  }
244
244
  }