@hology/core 0.0.246 → 0.0.247
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gameplay/actors/builtin/components/index.d.ts +2 -0
- package/dist/gameplay/actors/builtin/components/index.js +1 -1
- package/dist/gameplay/actors/builtin/components/spline-component.d.ts +65 -0
- package/dist/gameplay/actors/builtin/components/spline-component.js +4 -0
- package/dist/gameplay/actors/builtin/index.d.ts +6 -0
- package/dist/gameplay/actors/builtin/index.js +1 -1
- package/dist/gameplay/actors/builtin/navmesh-actor.js +1 -1
- package/dist/gameplay/actors/builtin/spline-actor.d.ts +6 -0
- package/dist/gameplay/actors/builtin/spline-actor.js +4 -0
- package/dist/gameplay/actors/builtin/water-spline-volume-actor.d.ts +13 -0
- package/dist/gameplay/actors/builtin/water-spline-volume-actor.js +4 -0
- package/dist/gameplay/actors/builtin/water-volume-actor.d.ts +8 -0
- package/dist/gameplay/actors/builtin/water-volume-actor.js +4 -0
- package/dist/gameplay/actors/index.d.ts +4 -0
- package/dist/gameplay/actors/index.js +1 -1
- package/dist/gameplay/ai/build-tile.d.ts +3 -1
- package/dist/gameplay/ai/build-tile.js +1 -1
- package/dist/gameplay/ai/dynamic-tiled-navmesh.d.ts +4 -21
- package/dist/gameplay/ai/dynamic-tiled-navmesh.js +1 -1
- package/dist/gameplay/ai/dynamic-tiled-navmesh.worker.d.ts +10 -1
- package/dist/gameplay/ai/dynamic-tiled-navmesh.worker.js +1 -1
- package/dist/gameplay/ai/index.d.ts +1 -0
- package/dist/gameplay/ai/index.js +1 -1
- package/dist/gameplay/ai/navigation-areas.d.ts +22 -0
- package/dist/gameplay/ai/navigation-areas.js +4 -0
- package/dist/gameplay/ai/navigation.d.ts +28 -4
- package/dist/gameplay/ai/navigation.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/shader/graph/compiler.js +1 -1
- package/dist/shader/graph/model.d.ts +1 -1
- package/dist/shader/graph/registry.js +1 -1
- package/dist/test/build-tile.test.js +1 -1
- package/dist/test/navmesh-worker-pool.test.js +1 -1
- package/dist/test/node-material-webgpu-resolver.test.js +1 -1
- package/dist/test/shader-graph.test.js +1 -1
- package/dist/test/spline-component.test.d.ts +2 -0
- package/dist/test/spline-component.test.js +4 -0
- package/dist/test/water-spline-volume-actor.test.d.ts +2 -0
- package/dist/test/water-spline-volume-actor.test.js +4 -0
- package/dist/test/worker-task-executor.test.d.ts +2 -0
- package/dist/test/worker-task-executor.test.js +4 -0
- package/dist/worker/index.d.ts +11 -2
- package/dist/worker/index.js +1 -1
- package/dist/worker/task-executor.d.ts +22 -0
- package/dist/worker/task-executor.js +4 -0
- package/dist/worker/task-worker.d.ts +3 -0
- package/dist/worker/task-worker.js +4 -0
- package/dist/worker/task.d.ts +39 -0
- package/dist/worker/task.js +4 -0
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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,4 @@
|
|
|
1
|
+
import{describe as n,expect as t,it as e,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";function p(n=!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}],n),!1),t}o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const n=new Proxy({},{get:(n,t)=>(t in n||(n[t]=o.fn()),n[t]),set:(n,t,e)=>(n[t]=e,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>n})}),n("SplineComponent",()=>{e("samples cubic position and tangent",()=>{const n=p();t(n.samplePosition(.5).x).toBeCloseTo(5),t(n.sampleTangent(.5).toArray()).toEqual([1,0,0])}),e("builds and uses an arc-length approximation",()=>{const n=p();t(n.length).toBeCloseTo(10),t(n.samplePositionAtDistance(2.5).x).toBeCloseTo(2.5,2)}),e("finds the closest point and distance along the spline",()=>{const n=p().closestPoint(new s(4,3,0));t(n.position.x).toBeCloseTo(4,2),t(n.distance).toBeCloseTo(3,2),t(n.distanceAlongSpline).toBeCloseTo(4,2)}),e("round trips compact point data",()=>{const n=p(!0),e=new i;e.setData(n.splineData,!1),t(e.closed).toBe(!0),t(e.points[1].inTangent.x).toBeCloseTo(-10/3),t(n.splineData).not.toContain("\n")}),e("inserts a point without changing the Bezier shape",()=>{const n=p(),e=Array.from({length:9},(t,e)=>n.samplePosition(e/8).clone());t(n.insertPointOnSegment(0)).toBe(1),t(n.points).toHaveLength(3),e.forEach((e,o)=>{t(n.samplePosition(o/8).distanceTo(e)).toBeLessThan(1e-5)})})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as n,vi as o}from"vitest";import{Box3 as i,Vector3 as a}from"three";import{NavigationArea as s}from"../gameplay/ai/navigation-areas.js";import{WaterSplineVolume as r}from"../gameplay/actors/builtin/water-spline-volume-actor.js";import{packSplineData as l}from"../gameplay/actors/builtin/components/spline-component.js";function m(e,t){e.spline.setData(l([{position:new a,inTangent:new a,outTangent:new a(t/3,0,0)},{position:new a(t,0,0),inTangent:new a(-t/3,0,0),outTangent:new a}],!1),!1)}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})}),e("WaterSplineVolume",()=>{n("creates water navigation prisms centered below the spline surface",()=>{const e=new r;e.width=4,e.depth=3,m(e,10);const n=e.getNavigationAreaModifiers();t(n).toHaveLength(20),t(n.every(e=>e.area===s.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 r;e.width=2,e.depth=1,m(e,2);const n=Object.values(e).find(e=>"WaterSplineVolumeEditorComponent"===e?.constructor?.name);n.onInit();const o=[...n.visual.children],a=(new i).setFromObject(n.visual);t(a.max.x).toBeCloseTo(2),m(e,6),e.spline.changed.next(e.spline),a.setFromObject(n.visual),t(a.max.x).toBeCloseTo(6),t(a.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 r;e.width=10,e.depth=1,e.spline.setData(l([{position:new a(-1,0,0),inTangent:new a,outTangent:new a(1/3,0,0)},{position:new a(0,0,0),inTangent:new a(-1/3,0,0),outTangent:new a(0,0,1/3)},{position:new a(0,0,1),inTangent:new a(0,0,-1/3),outTangent:new a}],!1),!1);const n=Object.values(e).find(e=>"WaterSplineVolumeEditorComponent"===e?.constructor?.name);n.onInit();const o=(new i).setFromObject(n.visual);t(o.min.x).toBeGreaterThanOrEqual(-1-e.width*u/2),t(o.max.x).toBeLessThanOrEqual(e.width*u/2),t(o.min.z).toBeGreaterThanOrEqual(-e.width*u/2),t(o.max.z).toBeLessThanOrEqual(1+e.width*u/2)})});const u=1.5;/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as 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
|
+
*/
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
package/dist/worker/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
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,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
|
+
*/
|