@womp/spark-agent 5.2.2 → 5.3.0

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 (58) hide show
  1. package/README.md +430 -423
  2. package/dist/agent-run.d.ts +1 -1
  3. package/dist/agent-tools.js +30 -12
  4. package/dist/capabilities/geometry.js +14 -14
  5. package/dist/capabilities/materials.d.ts +45 -0
  6. package/dist/capabilities/materials.js +89 -0
  7. package/dist/capabilities/openscad-instructions.js +77 -77
  8. package/dist/capabilities/operations.js +28 -28
  9. package/dist/capabilities/scene.d.ts +21 -0
  10. package/dist/capabilities/scene.js +82 -82
  11. package/dist/capabilities.d.ts +1 -0
  12. package/dist/capabilities.js +1 -0
  13. package/dist/error-diagnostics.d.ts +24 -0
  14. package/dist/error-diagnostics.js +53 -0
  15. package/dist/generated/materials-declarations.d.ts +1 -0
  16. package/dist/generated/materials-declarations.js +3 -0
  17. package/dist/geometry/schemas/backend-schema.json +3393 -3393
  18. package/dist/geometry/schemas/flowir-dialect.json +1023 -1023
  19. package/dist/geometry.js +6 -6
  20. package/dist/kakapo.d.ts +1 -0
  21. package/dist/kakapo.js +1 -0
  22. package/dist/materials/composition.d.ts +64 -0
  23. package/dist/materials/composition.js +121 -0
  24. package/dist/materials/graph.d.ts +79 -0
  25. package/dist/materials/graph.js +335 -0
  26. package/dist/materials/inspection.d.ts +40 -0
  27. package/dist/materials/inspection.js +54 -0
  28. package/dist/materials/recipes.d.ts +12 -0
  29. package/dist/materials/recipes.js +284 -0
  30. package/dist/materials/reference.d.ts +2 -0
  31. package/dist/materials/reference.js +97 -0
  32. package/dist/materials/shaping.d.ts +294 -0
  33. package/dist/materials/shaping.js +184 -0
  34. package/dist/materials/structure.d.ts +2 -0
  35. package/dist/materials/structure.js +12 -0
  36. package/dist/materials/surface-recipes.d.ts +78 -0
  37. package/dist/materials/surface-recipes.js +193 -0
  38. package/dist/materials/types.d.ts +222 -0
  39. package/dist/materials/types.js +1 -0
  40. package/dist/models.d.ts +7 -0
  41. package/dist/models.js +8 -0
  42. package/dist/pricing.js +5 -0
  43. package/dist/prompt.d.ts +1 -1
  44. package/dist/prompt.js +72 -72
  45. package/dist/scene-operations.d.ts +3 -2
  46. package/dist/scene-operations.js +19 -11
  47. package/dist/sdk-geometry-host.js +3 -1
  48. package/dist/sdk-materials-host.d.ts +9 -0
  49. package/dist/sdk-materials-host.js +360 -0
  50. package/dist/sdk-openscad-host.js +3 -1
  51. package/dist/sdk-scene-host.d.ts +6 -1
  52. package/dist/sdk-scene-host.js +19 -3
  53. package/dist/server/fixtures/materials/matte-blue.json +14 -14
  54. package/dist/server/fixtures/materials/polished-copper.json +14 -14
  55. package/dist/server/sqlite-store.js +37 -37
  56. package/dist/server/standalone.js +6 -1
  57. package/dist/ui.d.ts +50 -6
  58. package/package.json +139 -139
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { screenshotOptions } from "../capabilities/scene.js";
3
+ export const materialInspectionSchema = z.object({ views: z.array(screenshotOptions).min(1), timeoutMs: z.number().int().positive().max(300000).optional() }).strict();
4
+ export async function inspectMaterial(host, captures, options, context) {
5
+ const action = async () => {
6
+ const frames = [];
7
+ const deadline = performance.now() + (options.timeoutMs ?? 60000);
8
+ const settled = (status) => status.remaining === 0 && status.pendingResources !== null && Object.values(status.pendingResources).every(names => names.length === 0) && !status.errors?.length;
9
+ const observe = async () => { context.signal.throwIfAborted(); const status = await host.status(context); context.signal.throwIfAborted(); return status; };
10
+ const wait = async (initial) => {
11
+ let status = initial;
12
+ while (!settled(status) && status.remaining !== null && status.pendingResources !== null && !status.errors?.length && performance.now() < deadline) {
13
+ await new Promise((resolve, reject) => {
14
+ const abort = () => { clearTimeout(timer); reject(context.signal.reason); };
15
+ const timer = setTimeout(() => { context.signal.removeEventListener("abort", abort); resolve(); }, Math.min(200, Math.max(0, deadline - performance.now())));
16
+ context.signal.addEventListener("abort", abort, { once: true });
17
+ if (context.signal.aborted) {
18
+ context.signal.removeEventListener("abort", abort);
19
+ abort();
20
+ }
21
+ });
22
+ status = await observe();
23
+ }
24
+ return status;
25
+ };
26
+ const result = (status) => ({ frames, status, queueSettled: settled(status), timedOut: !settled(status) && performance.now() >= deadline });
27
+ context.signal.throwIfAborted();
28
+ // A first frame registers shaders even when the queue was initially empty.
29
+ await captures.screenshot(options.views[0], context);
30
+ let status = await wait(await observe());
31
+ if (!settled(status))
32
+ return result(status);
33
+ for (const view of options.views) {
34
+ context.signal.throwIfAborted();
35
+ let frame = await captures.screenshot(view, context);
36
+ const next = await observe();
37
+ // Another angle can expose previously hidden shaders; settle and recapture it.
38
+ if (!settled(next)) {
39
+ status = await wait(next);
40
+ if (!settled(status))
41
+ return result(status);
42
+ frame = await captures.screenshot(view, context);
43
+ status = await observe();
44
+ if (!settled(status))
45
+ return result(status);
46
+ }
47
+ else
48
+ status = next;
49
+ frames.push(frame);
50
+ }
51
+ return result(status);
52
+ };
53
+ return context.waitForService ? context.waitForService("Inspecting material views", action) : action();
54
+ }
@@ -0,0 +1,12 @@
1
+ export declare const MATERIALS_FIELD_COMPOSITION_SNIPPET = "const graph = await womp.materials.compose({\n fields,\n color:{low:colorLow,high:colorHigh,weights:colorWeights},\n roughness:{low:roughnessLow,high:roughnessHigh,weights:roughnessWeights},\n relief:{mode:'height',scale:reliefScale,weights:reliefWeights}\n});";
2
+ export declare const MATERIALS_PROJECTION_RECIPE = "const catalogue = await womp.materials.catalog({\n operations:['InputPosition','InputNormal','Mapping','Projection','PredefinedTexture','OutputMaterial'],\n includeTextures:true, refreshTextures:true\n});\nif (!catalogue.textures?.includes(textureName)) throw new Error('Texture is not listed: ' + textureName);\nconst enumValue = (op, slot, label) => {\n const value = catalogue.nodes[op]?.inputs[slot]?.values?.indexOf(label) ?? -1;\n if (value < 0) throw new Error(op + ' input ' + slot + ' has no option: ' + label);\n return value;\n};\nconst rotation = {x:0,y:90,z:0};\nconst graph = {\n nodes:[\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'InputNormal'}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:rotation,3:1}}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:rotation,3:1}}),\n womp.materials.node({op:'Projection',inputs:{0:enumValue('Projection',0,projectionName),3:4,4:{x:0.13,y:0.21,z:0.07},5:0}}),\n womp.materials.node({op:'PredefinedTexture',texture:textureName,inputs:{3:0,4:enumValue('PredefinedTexture',4,'sRGB (Color)'),5:0,6:0}}),\n womp.materials.node({op:'OutputMaterial'})\n ],\n connections:[\n {input_node:0,input_slot:0,output_node:2,output_slot:0,flags:0},\n {input_node:1,input_slot:0,output_node:3,output_slot:0,flags:0},\n {input_node:2,input_slot:0,output_node:4,output_slot:1,flags:0},\n {input_node:3,input_slot:0,output_node:4,output_slot:2,flags:0},\n {input_node:4,input_slot:0,output_node:5,output_slot:0,flags:0},\n {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0}\n ]\n};";
3
+ export declare const MATERIALS_NORMAL_MAP_RECIPE = "const catalogue = await womp.materials.catalog({\n operations:['Constant','Passthrough','OutputMaterial','InputPosition','InputNormal','Projection','Mapping','PredefinedTexture','OutputSurfaceNormal'],\n includeTextures:true, refreshTextures:true\n});\nif (!catalogue.textures?.includes(textureName)) throw new Error('Texture is not listed: ' + textureName);\nconst enumValue = (op, slot, label) => {\n const value = catalogue.nodes[op]?.inputs[slot]?.values?.indexOf(label) ?? -1;\n if (value < 0) throw new Error(op + ' input ' + slot + ' has no option: ' + label);\n return value;\n};\nconst graph = {\n nodes:[\n womp.materials.node({op:'Constant',inputs:{2:0.5}}),\n womp.materials.node({op:'Passthrough',inputs:{0:0.3}}),\n womp.materials.node({op:'OutputMaterial'}),\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'InputNormal'}),\n womp.materials.node({op:'Projection',inputs:{0:enumValue('Projection',0,'Equirectangular'),3:1,4:0,5:0}}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:0,3:1}}),\n womp.materials.node({op:'PredefinedTexture',texture:textureName,inputs:{3:0,4:enumValue('PredefinedTexture',4,'Normal'),5:0,6:0}}),\n womp.materials.node({op:'OutputSurfaceNormal'})\n ],\n connections:[\n {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:3,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0},\n {input_node:3,input_slot:0,output_node:5,output_slot:1,flags:0},\n {input_node:4,input_slot:0,output_node:5,output_slot:2,flags:0},\n {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0},\n {input_node:6,input_slot:0,output_node:7,output_slot:0,flags:0},\n {input_node:7,input_slot:0,output_node:8,output_slot:0,flags:0}\n ]\n};";
4
+ export declare const MATERIALS_HEIGHT_MAP_RECIPE = "const catalogue = await womp.materials.catalog({\n operations:['Constant','Passthrough','OutputMaterial','InputPosition','InputNormal','Projection','Mapping','PredefinedTexture','OutputHeight'],\n includeTextures:true, refreshTextures:true\n});\nif (!catalogue.textures?.includes(textureName)) throw new Error('Texture is not listed: ' + textureName);\nif (!Number.isFinite(heightScale) || heightScale <= 0) throw new Error('Choose a finite positive heightScale');\nconst enumValue = (op, slot, label) => {\n const value = catalogue.nodes[op]?.inputs[slot]?.values?.indexOf(label) ?? -1;\n if (value < 0) throw new Error(op + ' input ' + slot + ' has no option: ' + label);\n return value;\n};\nconst graph = {\n nodes:[\n womp.materials.node({op:'Constant',inputs:{2:0.5}}),\n womp.materials.node({op:'Passthrough',inputs:{0:0.3}}),\n womp.materials.node({op:'OutputMaterial'}),\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'InputNormal'}),\n womp.materials.node({op:'Projection',inputs:{0:enumValue('Projection',0,'Equirectangular'),3:1,4:0,5:0}}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:0,3:1}}),\n womp.materials.node({op:'PredefinedTexture',texture:textureName,inputs:{3:0,4:enumValue('PredefinedTexture',4,'No transform'),5:0,6:0}}),\n womp.materials.node({op:'OutputHeight',inputs:{2:heightScale}})\n ],\n connections:[\n {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:3,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0},\n {input_node:3,input_slot:0,output_node:5,output_slot:1,flags:0},\n {input_node:4,input_slot:0,output_node:5,output_slot:2,flags:0},\n {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0},\n {input_node:6,input_slot:0,output_node:7,output_slot:0,flags:0},\n {input_node:7,input_slot:0,output_node:8,output_slot:0,flags:0}\n ]\n};";
5
+ export declare const MATERIALS_GENERATOR_RECIPE = "const settings = {\n Noise1D:{inputs:{1:5,2:1,3:1,4:0}, ramp:[0.2,0.8,0.025,0.92]},\n Noise3D:{inputs:{1:5,2:1,3:1,4:0}},\n NoiseFBM:{inputs:{1:4,2:2,3:1,4:0}, ramp:[0.25,0.75,0.018,0.95]},\n NoiseVoronoi:{inputs:{1:4}},\n WavesTexture:{inputs:{1:{x:10,y:0,z:0},2:1,3:0}, ramp:[0,1]},\n CheckerTexture:{inputs:{1:3,2:1}, ramp:[0,1]},\n MagicTexture:{inputs:{1:5,2:2,3:1}}\n};\nif (!Object.hasOwn(settings,generatorName)) throw new Error('No recipe for: ' + generatorName);\nconst preset = settings[generatorName];\nconst catalogue = await womp.materials.catalog({operations:['InputPosition','Mapping',generatorName,...(preset.ramp ? ['ColorRamp'] : []),'OutputMaterial']});\nif (!catalogue.nodes[generatorName]) throw new Error('Generator is not listed: ' + generatorName);\nconst enumValue = (op, slot, label) => {\n const value = catalogue.nodes[op]?.inputs[slot]?.values?.indexOf(label) ?? -1;\n if (value < 0) throw new Error(op + ' input ' + slot + ' has no option: ' + label);\n return value;\n};\nif (generatorName === 'NoiseVoronoi') {\n preset.inputs[2] = enumValue('NoiseVoronoi',2,'Color');\n preset.inputs[3] = enumValue('NoiseVoronoi',3,'Euclidean');\n}\nconst gray = value => ({x:value,y:value,z:value});\nconst nodes = [\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:{x:generatorName === 'WavesTexture' ? 45 : 0,y:90,z:0},3:1}}),\n womp.materials.node({op:generatorName,inputs:preset.inputs})\n];\nif (preset.ramp) {\n const [start,end,dark,light] = preset.ramp;\n nodes.push(womp.materials.node({op:'ColorRamp',inputs:{3:enumValue('ColorRamp',3,'Linear')},ramp:[\n {position:start,color:dark === undefined ? {x:0.018,y:0.04,z:0.085} : gray(dark)},\n {position:end,color:light === undefined ? {x:0.94,y:0.78,z:0.36} : gray(light)}\n ]}));\n}\nnodes.push(womp.materials.node({op:'OutputMaterial'}));\nconst graph = {nodes,connections:nodes.slice(1).map((_,i) => ({input_node:i,input_slot:0,output_node:i+1,output_slot:0,flags:0}))};";
6
+ export declare const MATERIALS_COMBINED_NORMAL_RECIPE = "const extra = await womp.materials.catalog({operations:['NoiseFBM','Bump','SplitValues','CombineValues','Math']});\nObject.assign(catalogue.nodes,extra.nodes);\ngraph.nodes.pop();\ngraph.connections.pop();\ngraph.nodes.push(\n womp.materials.node({op:'NoiseFBM',inputs:{1:4,2:3,3:1,4:0}}),\n womp.materials.node({op:'Bump',inputs:{1:0.08}}),\n womp.materials.node({op:'SplitValues'}),\n womp.materials.node({op:'SplitValues'}),\n womp.materials.node({op:'Math',inputs:{0:enumValue('Math',0,'Add'),3:0}}),\n womp.materials.node({op:'Math',inputs:{0:enumValue('Math',0,'Mulitiply'),3:0}}),\n womp.materials.node({op:'SplitValues'}),\n womp.materials.node({op:'CombineValues'}),\n womp.materials.node({op:'Math',inputs:{0:enumValue('Math',0,'Normalize'),3:0}}),\n womp.materials.node({op:'OutputSurfaceNormal'})\n);\ngraph.connections.push(\n {input_node:3,input_slot:0,output_node:8,output_slot:0,flags:0},\n {input_node:8,input_slot:0,output_node:9,output_slot:0,flags:0},\n {input_node:7,input_slot:0,output_node:10,output_slot:0,flags:0},\n {input_node:9,input_slot:0,output_node:11,output_slot:0,flags:0},\n {input_node:7,input_slot:0,output_node:12,output_slot:1,flags:0},\n {input_node:9,input_slot:0,output_node:12,output_slot:2,flags:0},\n {input_node:10,input_slot:2,output_node:13,output_slot:1,flags:0},\n {input_node:11,input_slot:2,output_node:13,output_slot:2,flags:0},\n {input_node:12,input_slot:0,output_node:14,output_slot:0,flags:0},\n {input_node:14,input_slot:0,output_node:15,output_slot:0,flags:0},\n {input_node:14,input_slot:1,output_node:15,output_slot:1,flags:0},\n {input_node:13,input_slot:0,output_node:15,output_slot:2,flags:0},\n {input_node:15,input_slot:0,output_node:16,output_slot:1,flags:0},\n {input_node:16,input_slot:0,output_node:17,output_slot:0,flags:0}\n);";
7
+ export declare const MATERIALS_SCALAR_RGB_RECIPE = "await womp.materials.catalog({operations:['GetComponent','CombineValues']});\nconst colorEdge = graph.connections.find(edge => graph.nodes[edge.output_node].op === 'OutputMaterial' && edge.output_slot === 0);\nif (!colorEdge) throw new Error('Scalar-to-RGB requires an existing primary-color connection');\nconst component = graph.nodes.length;\ngraph.nodes.push(\n womp.materials.node({op:'GetComponent',inputs:{1:0}}),\n womp.materials.node({op:'CombineValues'})\n);\ngraph.connections.push(\n {input_node:colorEdge.input_node,input_slot:colorEdge.input_slot,output_node:component,output_slot:0,flags:0},\n ...[0,1,2].map(slot => ({input_node:component,input_slot:0,output_node:component+1,output_slot:slot,flags:0}))\n);\ncolorEdge.input_node = component+1;\ncolorEdge.input_slot = 0;";
8
+ export declare const MATERIALS_HSL_RECIPE = "await womp.materials.catalog({operations:['HSL']});\nconst hslEdge = graph.connections.find(edge => graph.nodes[edge.output_node].op === 'OutputMaterial' && edge.output_slot === 0);\nif (!hslEdge) throw new Error('HSL adjustment requires an existing primary-color connection');\nconst hsl = graph.nodes.length;\ngraph.nodes.push(womp.materials.node({op:'HSL',inputs:{1:0.33,2:0.8,3:0}}));\ngraph.connections.push({input_node:hslEdge.input_node,input_slot:hslEdge.input_slot,output_node:hsl,output_slot:0,flags:0});\nhslEdge.input_node = hsl;\nhslEdge.input_slot = 0;";
9
+ export declare const MATERIALS_COORDINATE_SPACE_RECIPE = "if (!['InputPosition','InputWorldPosition'].includes(positionOp)) throw new Error('Choose InputPosition or InputWorldPosition');\nconst coordinateCatalogue = await womp.materials.catalog({operations:[positionOp]});\nif (!coordinateCatalogue.nodes[positionOp]) throw new Error('Position source is not listed: ' + positionOp);\nconst positionNodes = graph.nodes.filter(node => ['InputPosition','InputWorldPosition'].includes(node.op));\nif (positionNodes.length !== 1) throw new Error('Choose a specific position source in this graph before changing space');\nif (positionNodes[0].op !== positionOp && graph.nodes.some(node => node.op === 'Projection')) throw new Error('Projection position and normal spaces must be changed together; build that mapping explicitly');\npositionNodes[0].op = positionOp;";
10
+ export declare const MATERIALS_CLONE_COLOR_RECIPE = "const sourceMaterial = await womp.materials.read(materialId);\nconst sourceConstant = sourceMaterial.material.layers.flatMap(layer => layer?.nodes ?? []).find(node => node.op === 'Constant' && node.reference_id === referenceId);\nif (!sourceConstant) throw new Error('No Constant with reference ' + referenceId + ' in material ' + materialId);\nconst cloned = await womp.materials.write({cloneFrom:materialId,objectIds:[objectId]});\nconst clonedReference = cloned.referenceIdChanges[String(referenceId)] ?? referenceId;\nprint({clonedMaterialId:cloned.materialId,referenceId:clonedReference});\nconst cloneState = await womp.materials.read(cloned.materialId);\nconst cloneConstant = cloneState.material.layers.flatMap(layer => layer?.nodes ?? []).find(node => node.op === 'Constant' && node.reference_id === clonedReference);\nif (!cloneConstant) throw new Error('Cloned Constant changed; inspect material ' + cloned.materialId + ' before editing');\nconst override = cloneState.material.data.constants?.[String(clonedReference)];\nconst zero = {x:0,y:0,z:0};\nconst preservedValue = override === undefined ? (cloneConstant.in1 ?? zero) : (override.value ?? zero);\nprint(await womp.materials.write({materialId:cloned.materialId,constants:{[clonedReference]:{value:preservedValue,color:newColor}}}));";
11
+ export declare const MATERIALS_RESET_CONSTANT_RECIPE = "const reset = await womp.materials.write({materialId,constants:{[referenceId]:null}});\nprint(reset);\nconst afterReset = await womp.materials.read(materialId);\nconst resetReference = reset.referenceIdChanges[String(referenceId)];\nprint({materialId,referenceId:resetReference,material:afterReset.material});";
12
+ export declare const MATERIALS_RECIPES = "\nThe graph examples construct one shader layer with womp.materials; the clone and reset examples perform writes. recipe() resolves its own live selectors; use these known starting patterns without requesting their descriptors. For custom graphs, discover only unfamiliar operations and required texture resource names. The graph snippets leave a variable named graph for assignment.\n\nReusable surface graphs\nconst settings = {\n coordinateSpace:'object', featureSize:1,\n colorA:{x:0.45,y:0.35,z:0.2}, colorB:{x:0.02,y:0.02,z:0.02},\n roughnessA:0.45, roughnessB:0.45, relief:'height', reliefScale:0.0003\n};\nconst graph = await womp.materials.recipe({pattern:'grid',...settings});\n// This single probe includes 1-unit major cells and 0.1-unit minor cells.\nconst isolated = await womp.materials.write({cloneFrom:materialId,layers:[graph],objectIds});\n// Inspect complementary views together before replacing the probe on this isolated material.\n// Choose noise for irregular grain/pores, grain for elongated structure, woven for crossed threads.\n// Set colors, finish and feature size from the requested surface, rather than retaining probe values.\nconst surface = await womp.materials.recipe({pattern, ...surfaceSettings});\nprint(await womp.materials.write({materialId:isolated.materialId,layers:[surface],objectIds}));\n\nfeatureSize is the repeat scale in object coordinates, or physical mm with coordinateSpace:'world'. Colors in settings are linear RGB; convert display palettes before using them. An axis vector gives deliberate anisotropy; grain stretches along the largest component. Object coordinates belong to the carrier and can differ across primitives and meshes. Check the probe against actual geometry dimensions and transforms before translating its spacing into physical dimensions. World coordinates give consistent physical spacing across differently scaled carriers, but remain anchored to the scene. These graphs are editable starting structures: add the target's distinctive pores, loops, knots or irregular threads when required. Strongly stretched noise alone can look smeared; separate directional structure from broad color variation and fine roughness/relief.\nPattern-specific calibration observations (native strength, object coordinates, detail 4, height and bump tested independently):\n- Dual-scale grid, featureSize 1: height 0.0003\u20130.001; start at 0.0003.\n- Noise, featureSize 0.04: 0.01\u20130.03 produced visible fine relief.\n- Crossed periodic threads, featureSize 0.04: 0.003\u20130.03 retained rounded thread shading.\n- Noise, featureSize 0.08: 0.01\u20130.03 produced low-amplitude grain.\n- Fine directional grain, featureSize {x:0.015,y:0.015,z:2}: 0.0003\u20130.003 produced faint directional relief; 0.01 and above produced stronger ridges.\nThese observations on curved and flat fixtures under fixed lighting are starting points, not quality targets or upper limits. Use independently weighted structural and micro height fields. Choose visible relief by its actual gradient, scale and requested finish; universal bump multipliers can destroy gloss. Compare close and grazing views without changing the scene. Height and bump strengths are not generally interchangeable.\n\nCompose generic scalar fields\nBuild broad variation, defining structure and fine detail with field(), using independent scales and contrast. Custom generators, domain warping, periodic profiles or masks can supply the same {graph,source:{node,slot}} contract. Retrieve unfamiliar descriptors together. compose() remaps imported node indices and wires weighted channels; it does not prescribe a material family.\nSet fields to terminal-free fields and colorWeights, roughnessWeights and reliefWeights to arrays with one weight per field. Omitted or all-zero channels inherit the incoming material. Normalized color/roughness fields use nonnegative weights summing to at most 1 and valid linear colorLow/colorHigh and roughnessLow/roughnessHigh endpoints. Relief weights may be signed; choose reliefScale from the actual gradient. Combined scalar height is converted to normals once. Keep coordinates compatible and preserve fine detail's own scale and contrast. Named Constant handles from write() permit parameter-only refinement without rebuilding these graphs.\nconst graph = await womp.materials.compose({\n fields,\n color:{low:colorLow,high:colorHigh,weights:colorWeights},\n roughness:{low:roughnessLow,high:roughnessHigh,weights:roughnessWeights},\n relief:{mode:'height',scale:reliefScale,weights:reliefWeights}\n});\n\nTexture projection\nSet textureName to a listed RGB color resource and projectionName to a listed Projection mode. Position and normal rotations stay paired. Choose Planar, Cubic, Cylindrical, Equirectangular, Triplanar or a normalized mode for the carrier, then inspect its seams and transitions at the intended size.\nconst catalogue = await womp.materials.catalog({\n operations:['InputPosition','InputNormal','Mapping','Projection','PredefinedTexture','OutputMaterial'],\n includeTextures:true, refreshTextures:true\n});\nif (!catalogue.textures?.includes(textureName)) throw new Error('Texture is not listed: ' + textureName);\nconst enumValue = (op, slot, label) => {\n const value = catalogue.nodes[op]?.inputs[slot]?.values?.indexOf(label) ?? -1;\n if (value < 0) throw new Error(op + ' input ' + slot + ' has no option: ' + label);\n return value;\n};\nconst rotation = {x:0,y:90,z:0};\nconst graph = {\n nodes:[\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'InputNormal'}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:rotation,3:1}}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:rotation,3:1}}),\n womp.materials.node({op:'Projection',inputs:{0:enumValue('Projection',0,projectionName),3:4,4:{x:0.13,y:0.21,z:0.07},5:0}}),\n womp.materials.node({op:'PredefinedTexture',texture:textureName,inputs:{3:0,4:enumValue('PredefinedTexture',4,'sRGB (Color)'),5:0,6:0}}),\n womp.materials.node({op:'OutputMaterial'})\n ],\n connections:[\n {input_node:0,input_slot:0,output_node:2,output_slot:0,flags:0},\n {input_node:1,input_slot:0,output_node:3,output_slot:0,flags:0},\n {input_node:2,input_slot:0,output_node:4,output_slot:1,flags:0},\n {input_node:3,input_slot:0,output_node:4,output_slot:2,flags:0},\n {input_node:4,input_slot:0,output_node:5,output_slot:0,flags:0},\n {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0}\n ]\n};\n\nTangent normal texture\nSet textureName to a known normal-map resource. The graph uses Normal decoding and Equirectangular projection; adapt projection and map convention to the carrier. It changes shading normals, not geometry.\nconst catalogue = await womp.materials.catalog({\n operations:['Constant','Passthrough','OutputMaterial','InputPosition','InputNormal','Projection','Mapping','PredefinedTexture','OutputSurfaceNormal'],\n includeTextures:true, refreshTextures:true\n});\nif (!catalogue.textures?.includes(textureName)) throw new Error('Texture is not listed: ' + textureName);\nconst enumValue = (op, slot, label) => {\n const value = catalogue.nodes[op]?.inputs[slot]?.values?.indexOf(label) ?? -1;\n if (value < 0) throw new Error(op + ' input ' + slot + ' has no option: ' + label);\n return value;\n};\nconst graph = {\n nodes:[\n womp.materials.node({op:'Constant',inputs:{2:0.5}}),\n womp.materials.node({op:'Passthrough',inputs:{0:0.3}}),\n womp.materials.node({op:'OutputMaterial'}),\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'InputNormal'}),\n womp.materials.node({op:'Projection',inputs:{0:enumValue('Projection',0,'Equirectangular'),3:1,4:0,5:0}}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:0,3:1}}),\n womp.materials.node({op:'PredefinedTexture',texture:textureName,inputs:{3:0,4:enumValue('PredefinedTexture',4,'Normal'),5:0,6:0}}),\n womp.materials.node({op:'OutputSurfaceNormal'})\n ],\n connections:[\n {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:3,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0},\n {input_node:3,input_slot:0,output_node:5,output_slot:1,flags:0},\n {input_node:4,input_slot:0,output_node:5,output_slot:2,flags:0},\n {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0},\n {input_node:6,input_slot:0,output_node:7,output_slot:0,flags:0},\n {input_node:7,input_slot:0,output_node:8,output_slot:0,flags:0}\n ]\n};\n\nHeight texture\nSet textureName to a known scalar resource and heightScale to a finite positive value. This uses No transform decoding and OutputHeight Scale; it is not numerically equivalent to tangent-normal strength.\nconst catalogue = await womp.materials.catalog({\n operations:['Constant','Passthrough','OutputMaterial','InputPosition','InputNormal','Projection','Mapping','PredefinedTexture','OutputHeight'],\n includeTextures:true, refreshTextures:true\n});\nif (!catalogue.textures?.includes(textureName)) throw new Error('Texture is not listed: ' + textureName);\nif (!Number.isFinite(heightScale) || heightScale <= 0) throw new Error('Choose a finite positive heightScale');\nconst enumValue = (op, slot, label) => {\n const value = catalogue.nodes[op]?.inputs[slot]?.values?.indexOf(label) ?? -1;\n if (value < 0) throw new Error(op + ' input ' + slot + ' has no option: ' + label);\n return value;\n};\nconst graph = {\n nodes:[\n womp.materials.node({op:'Constant',inputs:{2:0.5}}),\n womp.materials.node({op:'Passthrough',inputs:{0:0.3}}),\n womp.materials.node({op:'OutputMaterial'}),\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'InputNormal'}),\n womp.materials.node({op:'Projection',inputs:{0:enumValue('Projection',0,'Equirectangular'),3:1,4:0,5:0}}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:0,3:1}}),\n womp.materials.node({op:'PredefinedTexture',texture:textureName,inputs:{3:0,4:enumValue('PredefinedTexture',4,'No transform'),5:0,6:0}}),\n womp.materials.node({op:'OutputHeight',inputs:{2:heightScale}})\n ],\n connections:[\n {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:3,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0},\n {input_node:3,input_slot:0,output_node:5,output_slot:1,flags:0},\n {input_node:4,input_slot:0,output_node:5,output_slot:2,flags:0},\n {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0},\n {input_node:6,input_slot:0,output_node:7,output_slot:0,flags:0},\n {input_node:7,input_slot:0,output_node:8,output_slot:0,flags:0}\n ]\n};\n\nCombine normal texture and procedural bump\nAppend this immediately after the tangent-normal graph above, before writing. Its node indices depend on that graph. The whiteout composition is approximate; inspect the combined result and each component under the same light.\nconst extra = await womp.materials.catalog({operations:['NoiseFBM','Bump','SplitValues','CombineValues','Math']});\nObject.assign(catalogue.nodes,extra.nodes);\ngraph.nodes.pop();\ngraph.connections.pop();\ngraph.nodes.push(\n womp.materials.node({op:'NoiseFBM',inputs:{1:4,2:3,3:1,4:0}}),\n womp.materials.node({op:'Bump',inputs:{1:0.08}}),\n womp.materials.node({op:'SplitValues'}),\n womp.materials.node({op:'SplitValues'}),\n womp.materials.node({op:'Math',inputs:{0:enumValue('Math',0,'Add'),3:0}}),\n womp.materials.node({op:'Math',inputs:{0:enumValue('Math',0,'Mulitiply'),3:0}}),\n womp.materials.node({op:'SplitValues'}),\n womp.materials.node({op:'CombineValues'}),\n womp.materials.node({op:'Math',inputs:{0:enumValue('Math',0,'Normalize'),3:0}}),\n womp.materials.node({op:'OutputSurfaceNormal'})\n);\ngraph.connections.push(\n {input_node:3,input_slot:0,output_node:8,output_slot:0,flags:0},\n {input_node:8,input_slot:0,output_node:9,output_slot:0,flags:0},\n {input_node:7,input_slot:0,output_node:10,output_slot:0,flags:0},\n {input_node:9,input_slot:0,output_node:11,output_slot:0,flags:0},\n {input_node:7,input_slot:0,output_node:12,output_slot:1,flags:0},\n {input_node:9,input_slot:0,output_node:12,output_slot:2,flags:0},\n {input_node:10,input_slot:2,output_node:13,output_slot:1,flags:0},\n {input_node:11,input_slot:2,output_node:13,output_slot:2,flags:0},\n {input_node:12,input_slot:0,output_node:14,output_slot:0,flags:0},\n {input_node:14,input_slot:0,output_node:15,output_slot:0,flags:0},\n {input_node:14,input_slot:1,output_node:15,output_slot:1,flags:0},\n {input_node:13,input_slot:0,output_node:15,output_slot:2,flags:0},\n {input_node:15,input_slot:0,output_node:16,output_slot:1,flags:0},\n {input_node:16,input_slot:0,output_node:17,output_slot:0,flags:0}\n);\n\nScalar texture to neutral RGB\nAppend this to a graph with an existing primary-color connection only when the sampled texture is known to be single-channel. It copies X/R to all three color channels.\nawait womp.materials.catalog({operations:['GetComponent','CombineValues']});\nconst colorEdge = graph.connections.find(edge => graph.nodes[edge.output_node].op === 'OutputMaterial' && edge.output_slot === 0);\nif (!colorEdge) throw new Error('Scalar-to-RGB requires an existing primary-color connection');\nconst component = graph.nodes.length;\ngraph.nodes.push(\n womp.materials.node({op:'GetComponent',inputs:{1:0}}),\n womp.materials.node({op:'CombineValues'})\n);\ngraph.connections.push(\n {input_node:colorEdge.input_node,input_slot:colorEdge.input_slot,output_node:component,output_slot:0,flags:0},\n ...[0,1,2].map(slot => ({input_node:component,input_slot:0,output_node:component+1,output_slot:slot,flags:0}))\n);\ncolorEdge.input_node = component+1;\ncolorEdge.input_slot = 0;\n\nHSL adjustment\nAppend this to a graph with an existing primary-color connection. Lightness 0 leaves that component unchanged.\nawait womp.materials.catalog({operations:['HSL']});\nconst hslEdge = graph.connections.find(edge => graph.nodes[edge.output_node].op === 'OutputMaterial' && edge.output_slot === 0);\nif (!hslEdge) throw new Error('HSL adjustment requires an existing primary-color connection');\nconst hsl = graph.nodes.length;\ngraph.nodes.push(womp.materials.node({op:'HSL',inputs:{1:0.33,2:0.8,3:0}}));\ngraph.connections.push({input_node:hslEdge.input_node,input_slot:hslEdge.input_slot,output_node:hsl,output_slot:0,flags:0});\nhslEdge.input_node = hsl;\nhslEdge.input_slot = 0;\n\nLocal or world coordinates\nSet positionOp to InputPosition or InputWorldPosition. This changes one position source in a simple mapping graph; a Projection graph also needs a matching normal-space change.\nif (!['InputPosition','InputWorldPosition'].includes(positionOp)) throw new Error('Choose InputPosition or InputWorldPosition');\nconst coordinateCatalogue = await womp.materials.catalog({operations:[positionOp]});\nif (!coordinateCatalogue.nodes[positionOp]) throw new Error('Position source is not listed: ' + positionOp);\nconst positionNodes = graph.nodes.filter(node => ['InputPosition','InputWorldPosition'].includes(node.op));\nif (positionNodes.length !== 1) throw new Error('Choose a specific position source in this graph before changing space');\nif (positionNodes[0].op !== positionOp && graph.nodes.some(node => node.op === 'Projection')) throw new Error('Projection position and normal spaces must be changed together; build that mapping explicitly');\npositionNodes[0].op = positionOp;\n\nGenerators\nSet generatorName to Noise1D, Noise3D, NoiseFBM, NoiseVoronoi, WavesTexture, CheckerTexture or MagicTexture. The settings are starting examples, not a complete operation catalogue; adjust one control at a time.\nconst settings = {\n Noise1D:{inputs:{1:5,2:1,3:1,4:0}, ramp:[0.2,0.8,0.025,0.92]},\n Noise3D:{inputs:{1:5,2:1,3:1,4:0}},\n NoiseFBM:{inputs:{1:4,2:2,3:1,4:0}, ramp:[0.25,0.75,0.018,0.95]},\n NoiseVoronoi:{inputs:{1:4}},\n WavesTexture:{inputs:{1:{x:10,y:0,z:0},2:1,3:0}, ramp:[0,1]},\n CheckerTexture:{inputs:{1:3,2:1}, ramp:[0,1]},\n MagicTexture:{inputs:{1:5,2:2,3:1}}\n};\nif (!Object.hasOwn(settings,generatorName)) throw new Error('No recipe for: ' + generatorName);\nconst preset = settings[generatorName];\nconst catalogue = await womp.materials.catalog({operations:['InputPosition','Mapping',generatorName,...(preset.ramp ? ['ColorRamp'] : []),'OutputMaterial']});\nif (!catalogue.nodes[generatorName]) throw new Error('Generator is not listed: ' + generatorName);\nconst enumValue = (op, slot, label) => {\n const value = catalogue.nodes[op]?.inputs[slot]?.values?.indexOf(label) ?? -1;\n if (value < 0) throw new Error(op + ' input ' + slot + ' has no option: ' + label);\n return value;\n};\nif (generatorName === 'NoiseVoronoi') {\n preset.inputs[2] = enumValue('NoiseVoronoi',2,'Color');\n preset.inputs[3] = enumValue('NoiseVoronoi',3,'Euclidean');\n}\nconst gray = value => ({x:value,y:value,z:value});\nconst nodes = [\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:{x:generatorName === 'WavesTexture' ? 45 : 0,y:90,z:0},3:1}}),\n womp.materials.node({op:generatorName,inputs:preset.inputs})\n];\nif (preset.ramp) {\n const [start,end,dark,light] = preset.ramp;\n nodes.push(womp.materials.node({op:'ColorRamp',inputs:{3:enumValue('ColorRamp',3,'Linear')},ramp:[\n {position:start,color:dark === undefined ? {x:0.018,y:0.04,z:0.085} : gray(dark)},\n {position:end,color:light === undefined ? {x:0.94,y:0.78,z:0.36} : gray(light)}\n ]}));\n}\nnodes.push(womp.materials.node({op:'OutputMaterial'}));\nconst graph = {nodes,connections:nodes.slice(1).map((_,i) => ({input_node:i,input_slot:0,output_node:i+1,output_slot:0,flags:0}))};\n\nAssign the selected graph\nprint(await womp.materials.write({layers:[graph],objectIds:[objectId]}));\n\nClone a material and edit one Constant\nSet materialId, objectId and referenceId from a current read, with newColor as a 0..1 RGB vector. The workflow retains the source material and the Constant's numeric value. Inspect the printed clone ID before any retry.\nconst sourceMaterial = await womp.materials.read(materialId);\nconst sourceConstant = sourceMaterial.material.layers.flatMap(layer => layer?.nodes ?? []).find(node => node.op === 'Constant' && node.reference_id === referenceId);\nif (!sourceConstant) throw new Error('No Constant with reference ' + referenceId + ' in material ' + materialId);\nconst cloned = await womp.materials.write({cloneFrom:materialId,objectIds:[objectId]});\nconst clonedReference = cloned.referenceIdChanges[String(referenceId)] ?? referenceId;\nprint({clonedMaterialId:cloned.materialId,referenceId:clonedReference});\nconst cloneState = await womp.materials.read(cloned.materialId);\nconst cloneConstant = cloneState.material.layers.flatMap(layer => layer?.nodes ?? []).find(node => node.op === 'Constant' && node.reference_id === clonedReference);\nif (!cloneConstant) throw new Error('Cloned Constant changed; inspect material ' + cloned.materialId + ' before editing');\nconst override = cloneState.material.data.constants?.[String(clonedReference)];\nconst zero = {x:0,y:0,z:0};\nconst preservedValue = override === undefined ? (cloneConstant.in1 ?? zero) : (override.value ?? zero);\nprint(await womp.materials.write({materialId:cloned.materialId,constants:{[clonedReference]:{value:preservedValue,color:newColor}}}));\n\nReset a Constant override\nSet materialId and referenceId to the current IDs. The reset returns a new reference ID; use a fresh read before further edits.\nconst reset = await womp.materials.write({materialId,constants:{[referenceId]:null}});\nprint(reset);\nconst afterReset = await womp.materials.read(materialId);\nconst resetReference = reset.referenceIdChanges[String(referenceId)];\nprint({materialId,referenceId:resetReference,material:afterReset.material});\n\nA successful write is a scene commit, not evidence that shader compilation or texture loading completed. Check status() and an inspected image separately; status() observations can be null when a query fails.\n";
@@ -0,0 +1,284 @@
1
+ import { MATERIAL_RECIPE_RELIEF_GUIDANCE } from "./surface-recipes.js";
2
+ const ENUM_RECIPE = `const enumValue = (op, slot, label) => {
3
+ const value = catalogue.nodes[op]?.inputs[slot]?.values?.indexOf(label) ?? -1;
4
+ if (value < 0) throw new Error(op + ' input ' + slot + ' has no option: ' + label);
5
+ return value;
6
+ };`;
7
+ // Compose terminal-free helpers or custom scalar graphs through the shared API.
8
+ export const MATERIALS_FIELD_COMPOSITION_SNIPPET = `const graph = await womp.materials.compose({
9
+ fields,
10
+ color:{low:colorLow,high:colorHigh,weights:colorWeights},
11
+ roughness:{low:roughnessLow,high:roughnessHigh,weights:roughnessWeights},
12
+ relief:{mode:'height',scale:reliefScale,weights:reliefWeights}
13
+ });`;
14
+ // Rotate position and normal mapping together.
15
+ export const MATERIALS_PROJECTION_RECIPE = `const catalogue = await womp.materials.catalog({
16
+ operations:['InputPosition','InputNormal','Mapping','Projection','PredefinedTexture','OutputMaterial'],
17
+ includeTextures:true, refreshTextures:true
18
+ });
19
+ if (!catalogue.textures?.includes(textureName)) throw new Error('Texture is not listed: ' + textureName);
20
+ ${ENUM_RECIPE}
21
+ const rotation = {x:0,y:90,z:0};
22
+ const graph = {
23
+ nodes:[
24
+ womp.materials.node({op:'InputPosition'}),
25
+ womp.materials.node({op:'InputNormal'}),
26
+ womp.materials.node({op:'Mapping',inputs:{1:0,2:rotation,3:1}}),
27
+ womp.materials.node({op:'Mapping',inputs:{1:0,2:rotation,3:1}}),
28
+ womp.materials.node({op:'Projection',inputs:{0:enumValue('Projection',0,projectionName),3:4,4:{x:0.13,y:0.21,z:0.07},5:0}}),
29
+ womp.materials.node({op:'PredefinedTexture',texture:textureName,inputs:{3:0,4:enumValue('PredefinedTexture',4,'sRGB (Color)'),5:0,6:0}}),
30
+ womp.materials.node({op:'OutputMaterial'})
31
+ ],
32
+ connections:[
33
+ {input_node:0,input_slot:0,output_node:2,output_slot:0,flags:0},
34
+ {input_node:1,input_slot:0,output_node:3,output_slot:0,flags:0},
35
+ {input_node:2,input_slot:0,output_node:4,output_slot:1,flags:0},
36
+ {input_node:3,input_slot:0,output_node:4,output_slot:2,flags:0},
37
+ {input_node:4,input_slot:0,output_node:5,output_slot:0,flags:0},
38
+ {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0}
39
+ ]
40
+ };`;
41
+ // Decode a texture as a tangent-space normal map.
42
+ export const MATERIALS_NORMAL_MAP_RECIPE = `const catalogue = await womp.materials.catalog({
43
+ operations:['Constant','Passthrough','OutputMaterial','InputPosition','InputNormal','Projection','Mapping','PredefinedTexture','OutputSurfaceNormal'],
44
+ includeTextures:true, refreshTextures:true
45
+ });
46
+ if (!catalogue.textures?.includes(textureName)) throw new Error('Texture is not listed: ' + textureName);
47
+ ${ENUM_RECIPE}
48
+ const graph = {
49
+ nodes:[
50
+ womp.materials.node({op:'Constant',inputs:{2:0.5}}),
51
+ womp.materials.node({op:'Passthrough',inputs:{0:0.3}}),
52
+ womp.materials.node({op:'OutputMaterial'}),
53
+ womp.materials.node({op:'InputPosition'}),
54
+ womp.materials.node({op:'InputNormal'}),
55
+ womp.materials.node({op:'Projection',inputs:{0:enumValue('Projection',0,'Equirectangular'),3:1,4:0,5:0}}),
56
+ womp.materials.node({op:'Mapping',inputs:{1:0,2:0,3:1}}),
57
+ womp.materials.node({op:'PredefinedTexture',texture:textureName,inputs:{3:0,4:enumValue('PredefinedTexture',4,'Normal'),5:0,6:0}}),
58
+ womp.materials.node({op:'OutputSurfaceNormal'})
59
+ ],
60
+ connections:[
61
+ {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},
62
+ {input_node:1,input_slot:0,output_node:2,output_slot:3,flags:0},
63
+ {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0},
64
+ {input_node:3,input_slot:0,output_node:5,output_slot:1,flags:0},
65
+ {input_node:4,input_slot:0,output_node:5,output_slot:2,flags:0},
66
+ {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0},
67
+ {input_node:6,input_slot:0,output_node:7,output_slot:0,flags:0},
68
+ {input_node:7,input_slot:0,output_node:8,output_slot:0,flags:0}
69
+ ]
70
+ };`;
71
+ export const MATERIALS_HEIGHT_MAP_RECIPE = `const catalogue = await womp.materials.catalog({
72
+ operations:['Constant','Passthrough','OutputMaterial','InputPosition','InputNormal','Projection','Mapping','PredefinedTexture','OutputHeight'],
73
+ includeTextures:true, refreshTextures:true
74
+ });
75
+ if (!catalogue.textures?.includes(textureName)) throw new Error('Texture is not listed: ' + textureName);
76
+ if (!Number.isFinite(heightScale) || heightScale <= 0) throw new Error('Choose a finite positive heightScale');
77
+ ${ENUM_RECIPE}
78
+ const graph = {
79
+ nodes:[
80
+ womp.materials.node({op:'Constant',inputs:{2:0.5}}),
81
+ womp.materials.node({op:'Passthrough',inputs:{0:0.3}}),
82
+ womp.materials.node({op:'OutputMaterial'}),
83
+ womp.materials.node({op:'InputPosition'}),
84
+ womp.materials.node({op:'InputNormal'}),
85
+ womp.materials.node({op:'Projection',inputs:{0:enumValue('Projection',0,'Equirectangular'),3:1,4:0,5:0}}),
86
+ womp.materials.node({op:'Mapping',inputs:{1:0,2:0,3:1}}),
87
+ womp.materials.node({op:'PredefinedTexture',texture:textureName,inputs:{3:0,4:enumValue('PredefinedTexture',4,'No transform'),5:0,6:0}}),
88
+ womp.materials.node({op:'OutputHeight',inputs:{2:heightScale}})
89
+ ],
90
+ connections:[
91
+ {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},
92
+ {input_node:1,input_slot:0,output_node:2,output_slot:3,flags:0},
93
+ {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0},
94
+ {input_node:3,input_slot:0,output_node:5,output_slot:1,flags:0},
95
+ {input_node:4,input_slot:0,output_node:5,output_slot:2,flags:0},
96
+ {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0},
97
+ {input_node:6,input_slot:0,output_node:7,output_slot:0,flags:0},
98
+ {input_node:7,input_slot:0,output_node:8,output_slot:0,flags:0}
99
+ ]
100
+ };`;
101
+ export const MATERIALS_GENERATOR_RECIPE = `const settings = {
102
+ Noise1D:{inputs:{1:5,2:1,3:1,4:0}, ramp:[0.2,0.8,0.025,0.92]},
103
+ Noise3D:{inputs:{1:5,2:1,3:1,4:0}},
104
+ NoiseFBM:{inputs:{1:4,2:2,3:1,4:0}, ramp:[0.25,0.75,0.018,0.95]},
105
+ NoiseVoronoi:{inputs:{1:4}},
106
+ WavesTexture:{inputs:{1:{x:10,y:0,z:0},2:1,3:0}, ramp:[0,1]},
107
+ CheckerTexture:{inputs:{1:3,2:1}, ramp:[0,1]},
108
+ MagicTexture:{inputs:{1:5,2:2,3:1}}
109
+ };
110
+ if (!Object.hasOwn(settings,generatorName)) throw new Error('No recipe for: ' + generatorName);
111
+ const preset = settings[generatorName];
112
+ const catalogue = await womp.materials.catalog({operations:['InputPosition','Mapping',generatorName,...(preset.ramp ? ['ColorRamp'] : []),'OutputMaterial']});
113
+ if (!catalogue.nodes[generatorName]) throw new Error('Generator is not listed: ' + generatorName);
114
+ ${ENUM_RECIPE}
115
+ if (generatorName === 'NoiseVoronoi') {
116
+ preset.inputs[2] = enumValue('NoiseVoronoi',2,'Color');
117
+ preset.inputs[3] = enumValue('NoiseVoronoi',3,'Euclidean');
118
+ }
119
+ const gray = value => ({x:value,y:value,z:value});
120
+ const nodes = [
121
+ womp.materials.node({op:'InputPosition'}),
122
+ womp.materials.node({op:'Mapping',inputs:{1:0,2:{x:generatorName === 'WavesTexture' ? 45 : 0,y:90,z:0},3:1}}),
123
+ womp.materials.node({op:generatorName,inputs:preset.inputs})
124
+ ];
125
+ if (preset.ramp) {
126
+ const [start,end,dark,light] = preset.ramp;
127
+ nodes.push(womp.materials.node({op:'ColorRamp',inputs:{3:enumValue('ColorRamp',3,'Linear')},ramp:[
128
+ {position:start,color:dark === undefined ? {x:0.018,y:0.04,z:0.085} : gray(dark)},
129
+ {position:end,color:light === undefined ? {x:0.94,y:0.78,z:0.36} : gray(light)}
130
+ ]}));
131
+ }
132
+ nodes.push(womp.materials.node({op:'OutputMaterial'}));
133
+ const graph = {nodes,connections:nodes.slice(1).map((_,i) => ({input_node:i,input_slot:0,output_node:i+1,output_slot:0,flags:0}))};`;
134
+ // Append to MATERIALS_NORMAL_MAP_RECIPE before writing.
135
+ export const MATERIALS_COMBINED_NORMAL_RECIPE = `const extra = await womp.materials.catalog({operations:['NoiseFBM','Bump','SplitValues','CombineValues','Math']});
136
+ Object.assign(catalogue.nodes,extra.nodes);
137
+ graph.nodes.pop();
138
+ graph.connections.pop();
139
+ graph.nodes.push(
140
+ womp.materials.node({op:'NoiseFBM',inputs:{1:4,2:3,3:1,4:0}}),
141
+ womp.materials.node({op:'Bump',inputs:{1:0.08}}),
142
+ womp.materials.node({op:'SplitValues'}),
143
+ womp.materials.node({op:'SplitValues'}),
144
+ womp.materials.node({op:'Math',inputs:{0:enumValue('Math',0,'Add'),3:0}}),
145
+ womp.materials.node({op:'Math',inputs:{0:enumValue('Math',0,'Mulitiply'),3:0}}),
146
+ womp.materials.node({op:'SplitValues'}),
147
+ womp.materials.node({op:'CombineValues'}),
148
+ womp.materials.node({op:'Math',inputs:{0:enumValue('Math',0,'Normalize'),3:0}}),
149
+ womp.materials.node({op:'OutputSurfaceNormal'})
150
+ );
151
+ graph.connections.push(
152
+ {input_node:3,input_slot:0,output_node:8,output_slot:0,flags:0},
153
+ {input_node:8,input_slot:0,output_node:9,output_slot:0,flags:0},
154
+ {input_node:7,input_slot:0,output_node:10,output_slot:0,flags:0},
155
+ {input_node:9,input_slot:0,output_node:11,output_slot:0,flags:0},
156
+ {input_node:7,input_slot:0,output_node:12,output_slot:1,flags:0},
157
+ {input_node:9,input_slot:0,output_node:12,output_slot:2,flags:0},
158
+ {input_node:10,input_slot:2,output_node:13,output_slot:1,flags:0},
159
+ {input_node:11,input_slot:2,output_node:13,output_slot:2,flags:0},
160
+ {input_node:12,input_slot:0,output_node:14,output_slot:0,flags:0},
161
+ {input_node:14,input_slot:0,output_node:15,output_slot:0,flags:0},
162
+ {input_node:14,input_slot:1,output_node:15,output_slot:1,flags:0},
163
+ {input_node:13,input_slot:0,output_node:15,output_slot:2,flags:0},
164
+ {input_node:15,input_slot:0,output_node:16,output_slot:1,flags:0},
165
+ {input_node:16,input_slot:0,output_node:17,output_slot:0,flags:0}
166
+ );`;
167
+ export const MATERIALS_SCALAR_RGB_RECIPE = `await womp.materials.catalog({operations:['GetComponent','CombineValues']});
168
+ const colorEdge = graph.connections.find(edge => graph.nodes[edge.output_node].op === 'OutputMaterial' && edge.output_slot === 0);
169
+ if (!colorEdge) throw new Error('Scalar-to-RGB requires an existing primary-color connection');
170
+ const component = graph.nodes.length;
171
+ graph.nodes.push(
172
+ womp.materials.node({op:'GetComponent',inputs:{1:0}}),
173
+ womp.materials.node({op:'CombineValues'})
174
+ );
175
+ graph.connections.push(
176
+ {input_node:colorEdge.input_node,input_slot:colorEdge.input_slot,output_node:component,output_slot:0,flags:0},
177
+ ...[0,1,2].map(slot => ({input_node:component,input_slot:0,output_node:component+1,output_slot:slot,flags:0}))
178
+ );
179
+ colorEdge.input_node = component+1;
180
+ colorEdge.input_slot = 0;`;
181
+ export const MATERIALS_HSL_RECIPE = `await womp.materials.catalog({operations:['HSL']});
182
+ const hslEdge = graph.connections.find(edge => graph.nodes[edge.output_node].op === 'OutputMaterial' && edge.output_slot === 0);
183
+ if (!hslEdge) throw new Error('HSL adjustment requires an existing primary-color connection');
184
+ const hsl = graph.nodes.length;
185
+ graph.nodes.push(womp.materials.node({op:'HSL',inputs:{1:0.33,2:0.8,3:0}}));
186
+ graph.connections.push({input_node:hslEdge.input_node,input_slot:hslEdge.input_slot,output_node:hsl,output_slot:0,flags:0});
187
+ hslEdge.input_node = hsl;
188
+ hslEdge.input_slot = 0;`;
189
+ export const MATERIALS_COORDINATE_SPACE_RECIPE = `if (!['InputPosition','InputWorldPosition'].includes(positionOp)) throw new Error('Choose InputPosition or InputWorldPosition');
190
+ const coordinateCatalogue = await womp.materials.catalog({operations:[positionOp]});
191
+ if (!coordinateCatalogue.nodes[positionOp]) throw new Error('Position source is not listed: ' + positionOp);
192
+ const positionNodes = graph.nodes.filter(node => ['InputPosition','InputWorldPosition'].includes(node.op));
193
+ if (positionNodes.length !== 1) throw new Error('Choose a specific position source in this graph before changing space');
194
+ if (positionNodes[0].op !== positionOp && graph.nodes.some(node => node.op === 'Projection')) throw new Error('Projection position and normal spaces must be changed together; build that mapping explicitly');
195
+ positionNodes[0].op = positionOp;`;
196
+ export const MATERIALS_CLONE_COLOR_RECIPE = `const sourceMaterial = await womp.materials.read(materialId);
197
+ const sourceConstant = sourceMaterial.material.layers.flatMap(layer => layer?.nodes ?? []).find(node => node.op === 'Constant' && node.reference_id === referenceId);
198
+ if (!sourceConstant) throw new Error('No Constant with reference ' + referenceId + ' in material ' + materialId);
199
+ const cloned = await womp.materials.write({cloneFrom:materialId,objectIds:[objectId]});
200
+ const clonedReference = cloned.referenceIdChanges[String(referenceId)] ?? referenceId;
201
+ print({clonedMaterialId:cloned.materialId,referenceId:clonedReference});
202
+ const cloneState = await womp.materials.read(cloned.materialId);
203
+ const cloneConstant = cloneState.material.layers.flatMap(layer => layer?.nodes ?? []).find(node => node.op === 'Constant' && node.reference_id === clonedReference);
204
+ if (!cloneConstant) throw new Error('Cloned Constant changed; inspect material ' + cloned.materialId + ' before editing');
205
+ const override = cloneState.material.data.constants?.[String(clonedReference)];
206
+ const zero = {x:0,y:0,z:0};
207
+ const preservedValue = override === undefined ? (cloneConstant.in1 ?? zero) : (override.value ?? zero);
208
+ print(await womp.materials.write({materialId:cloned.materialId,constants:{[clonedReference]:{value:preservedValue,color:newColor}}}));`;
209
+ export const MATERIALS_RESET_CONSTANT_RECIPE = `const reset = await womp.materials.write({materialId,constants:{[referenceId]:null}});
210
+ print(reset);
211
+ const afterReset = await womp.materials.read(materialId);
212
+ const resetReference = reset.referenceIdChanges[String(referenceId)];
213
+ print({materialId,referenceId:resetReference,material:afterReset.material});`;
214
+ export const MATERIALS_RECIPES = `
215
+ The graph examples construct one shader layer with womp.materials; the clone and reset examples perform writes. recipe() resolves its own live selectors; use these known starting patterns without requesting their descriptors. For custom graphs, discover only unfamiliar operations and required texture resource names. The graph snippets leave a variable named graph for assignment.
216
+
217
+ Reusable surface graphs
218
+ const settings = {
219
+ coordinateSpace:'object', featureSize:1,
220
+ colorA:{x:0.45,y:0.35,z:0.2}, colorB:{x:0.02,y:0.02,z:0.02},
221
+ roughnessA:0.45, roughnessB:0.45, relief:'height', reliefScale:0.0003
222
+ };
223
+ const graph = await womp.materials.recipe({pattern:'grid',...settings});
224
+ // This single probe includes 1-unit major cells and 0.1-unit minor cells.
225
+ const isolated = await womp.materials.write({cloneFrom:materialId,layers:[graph],objectIds});
226
+ // Inspect complementary views together before replacing the probe on this isolated material.
227
+ // Choose noise for irregular grain/pores, grain for elongated structure, woven for crossed threads.
228
+ // Set colors, finish and feature size from the requested surface, rather than retaining probe values.
229
+ const surface = await womp.materials.recipe({pattern, ...surfaceSettings});
230
+ print(await womp.materials.write({materialId:isolated.materialId,layers:[surface],objectIds}));
231
+
232
+ featureSize is the repeat scale in object coordinates, or physical mm with coordinateSpace:'world'. Colors in settings are linear RGB; convert display palettes before using them. An axis vector gives deliberate anisotropy; grain stretches along the largest component. Object coordinates belong to the carrier and can differ across primitives and meshes. Check the probe against actual geometry dimensions and transforms before translating its spacing into physical dimensions. World coordinates give consistent physical spacing across differently scaled carriers, but remain anchored to the scene. These graphs are editable starting structures: add the target's distinctive pores, loops, knots or irregular threads when required. Strongly stretched noise alone can look smeared; separate directional structure from broad color variation and fine roughness/relief.
233
+ ${MATERIAL_RECIPE_RELIEF_GUIDANCE}
234
+
235
+ Compose generic scalar fields
236
+ Build broad variation, defining structure and fine detail with field(), using independent scales and contrast. Custom generators, domain warping, periodic profiles or masks can supply the same {graph,source:{node,slot}} contract. Retrieve unfamiliar descriptors together. compose() remaps imported node indices and wires weighted channels; it does not prescribe a material family.
237
+ Set fields to terminal-free fields and colorWeights, roughnessWeights and reliefWeights to arrays with one weight per field. Omitted or all-zero channels inherit the incoming material. Normalized color/roughness fields use nonnegative weights summing to at most 1 and valid linear colorLow/colorHigh and roughnessLow/roughnessHigh endpoints. Relief weights may be signed; choose reliefScale from the actual gradient. Combined scalar height is converted to normals once. Keep coordinates compatible and preserve fine detail's own scale and contrast. Named Constant handles from write() permit parameter-only refinement without rebuilding these graphs.
238
+ ${MATERIALS_FIELD_COMPOSITION_SNIPPET}
239
+
240
+ Texture projection
241
+ Set textureName to a listed RGB color resource and projectionName to a listed Projection mode. Position and normal rotations stay paired. Choose Planar, Cubic, Cylindrical, Equirectangular, Triplanar or a normalized mode for the carrier, then inspect its seams and transitions at the intended size.
242
+ ${MATERIALS_PROJECTION_RECIPE}
243
+
244
+ Tangent normal texture
245
+ Set textureName to a known normal-map resource. The graph uses Normal decoding and Equirectangular projection; adapt projection and map convention to the carrier. It changes shading normals, not geometry.
246
+ ${MATERIALS_NORMAL_MAP_RECIPE}
247
+
248
+ Height texture
249
+ Set textureName to a known scalar resource and heightScale to a finite positive value. This uses No transform decoding and OutputHeight Scale; it is not numerically equivalent to tangent-normal strength.
250
+ ${MATERIALS_HEIGHT_MAP_RECIPE}
251
+
252
+ Combine normal texture and procedural bump
253
+ Append this immediately after the tangent-normal graph above, before writing. Its node indices depend on that graph. The whiteout composition is approximate; inspect the combined result and each component under the same light.
254
+ ${MATERIALS_COMBINED_NORMAL_RECIPE}
255
+
256
+ Scalar texture to neutral RGB
257
+ Append this to a graph with an existing primary-color connection only when the sampled texture is known to be single-channel. It copies X/R to all three color channels.
258
+ ${MATERIALS_SCALAR_RGB_RECIPE}
259
+
260
+ HSL adjustment
261
+ Append this to a graph with an existing primary-color connection. Lightness 0 leaves that component unchanged.
262
+ ${MATERIALS_HSL_RECIPE}
263
+
264
+ Local or world coordinates
265
+ Set positionOp to InputPosition or InputWorldPosition. This changes one position source in a simple mapping graph; a Projection graph also needs a matching normal-space change.
266
+ ${MATERIALS_COORDINATE_SPACE_RECIPE}
267
+
268
+ Generators
269
+ Set generatorName to Noise1D, Noise3D, NoiseFBM, NoiseVoronoi, WavesTexture, CheckerTexture or MagicTexture. The settings are starting examples, not a complete operation catalogue; adjust one control at a time.
270
+ ${MATERIALS_GENERATOR_RECIPE}
271
+
272
+ Assign the selected graph
273
+ print(await womp.materials.write({layers:[graph],objectIds:[objectId]}));
274
+
275
+ Clone a material and edit one Constant
276
+ Set materialId, objectId and referenceId from a current read, with newColor as a 0..1 RGB vector. The workflow retains the source material and the Constant's numeric value. Inspect the printed clone ID before any retry.
277
+ ${MATERIALS_CLONE_COLOR_RECIPE}
278
+
279
+ Reset a Constant override
280
+ Set materialId and referenceId to the current IDs. The reset returns a new reference ID; use a fresh read before further edits.
281
+ ${MATERIALS_RESET_CONSTANT_RECIPE}
282
+
283
+ A successful write is a scene commit, not evidence that shader compilation or texture loading completed. Check status() and an inspected image separately; status() observations can be null when a query fails.
284
+ `;
@@ -0,0 +1,2 @@
1
+ export declare const MATERIALS_NOISE_BUMP_RECIPE = "const graph = {\n nodes: [\n womp.materials.node({op:'Constant',inputs:{2:0.5}}),\n womp.materials.node({op:'Passthrough',inputs:{0:0.3}}),\n womp.materials.node({op:'OutputMaterial'}),\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:0,3:1}}),\n womp.materials.node({op:'NoiseFBM',inputs:{1:4,2:3,3:1,4:0}}),\n womp.materials.node({op:'Bump',inputs:{1:0.03}}),\n womp.materials.node({op:'OutputSurfaceNormal'})\n ],\n connections: [\n {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:3,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0},\n {input_node:3,input_slot:0,output_node:4,output_slot:0,flags:0},\n {input_node:4,input_slot:0,output_node:5,output_slot:0,flags:0},\n {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0},\n {input_node:6,input_slot:0,output_node:7,output_slot:0,flags:0}\n ]\n};";
2
+ export declare const MATERIALS_REFERENCE = "\nProcedural shaders are material graphs, separate from Geometry Nodes and Workbench Flow. recipe() builds the standard grid, noise, grain and woven graphs with live selectors internally; their controls are already defined. Use catalog() for unfamiliar custom operations or includeTextures:true for resource names. get_node_types is a different graph system.\n\nStorage and sharing\n- Material IDs and shader IDs occupy separate positive 16-bit namespaces; material 0 remains the scene default. There are eight ordered shader slots, where null/0 is empty. write always pads trailing slots to eight to clear the native layer cache.\n- write without materialId creates a material. cloneFrom copies base properties, layers and metadata. Existing materialId edits affect every user of that material. Graph edits copy shared shader storage automatically.\n- Graph nodes have op, optional name/location/editor_flags/reference_id, in0..in19 vectors, and optional external_data bytes. Read preserves native data. Keep unrelated metadata; node() constructs new nodes with scalar broadcasting and payload encoding.\n- Connections use zero-based ARRAY INDICES: input_node/source node, input_slot/source OUTPUT; output_node/destination node, output_slot/destination INPUT; flags:0 for new connections; native differential flags:2 are also supported. An input has one producer. Graphs must be acyclic, output nodes terminal, and enum/integer/Boolean selectors literal. Numeric Float/Float3/Color ports can interconnect; native math uses float3 and scalar consumers use X.\n- Unconnected OutputMaterial inputs inherit the incoming material, including the previous layer. Literal values on unconnected OutputMaterial inputs do not set properties. Connect Constant or Passthrough instead.\n- OutputMaterial inputs: 0 primary color, 1 secondary color, 2 subsurface, 3 metalness, 4 roughness, 5 transmittance, 6 translucency, 7 IOR, 8 iridescence, 9 emission, 10 absorption, 11 sheen, 12 specular tint, 13 translucency weight, 14 material weight, 15 dispersion, 16 displacement. Verify descriptors on the current engine.\n- InputMaterial and the exactly spelled InputUnderlayingMaterial read the incoming layer material, not independently preserved base snapshots. OutputHeight changes shading normals. Material displacement has separate export semantics; do not claim it changed mesh geometry.\n\nConstants and resetting\n- Constant input 0 chooses Pi/Half Pi/Double Pi/Exponent; input 1 is value vector, 2 color, 3 UI exposure enum, 4 scope. Outputs 0 built-in constant, 1 value, 2 color.\n- Give a new adjustable Constant a unique positive safe-integer reference_id, or let write allocate it and read it back. Keep reference IDs stable while editing. Independent constants across layers must have distinct references.\n- constants overrides are keyed by reference ID and contain BOTH value:{x,y,z} and color:{x,y,z}. Native omission zeros the missing counterpart. Use read to preserve it.\n- Reset with constants:{[referenceId]:null}. Kakapo retains deleted override keys in its in-memory cache; Spark gives the reset node a fresh reference and removes the stored old override. Replacing layers on an existing material also gives unoverridden Constants fresh references, so reusing an old graph cannot revive cached overrides. Use the returned referenceIdChanges mapping for subsequent edits. Other materials retain their references and overrides. Reset follows graph defaults and survives reopening.\n\nUnits, colors and mapping\n- Graph vectors use renderer coordinates. One native length unit is 25 mm; Spark scene editing flips X/Z into its public basis, so a public mm position maps to {-z/(25*u),y/(25*u),-x/(25*u)}, where u is the scene unit_multiplier (default 1). Do not scale every numeric shader input: colors, frequencies, normals and normalized UVs have different meanings.\n- Mapping computes rotate(value, rotationDegrees) * scale + translation. Set scale explicitly to {x:1,y:1,z:1}; omitted literals are zero. Projection mode comes from the live enum descriptor.\n- InputPosition uses the carrier's local coordinates, including its procedural transform; it is not universally a normalized box or a physical distance. InputWorldPosition uses renderer world coordinates. Pair local position with InputNormal, or world position with InputWorldNormal, for projection. Compensate non-uniform geometry scale in mapping, without changing the geometry. A 1-unit and 0.1-unit object-space grid together reveals actual spacing and stretching in one inspection.\n- Connected procedural colors are linear RGB in 0..1: Constant colors, ramp stops and recipe colorA/colorB. scene materialSetColor uses display RGB 0..255; to reproduce its base color in a procedural graph use (channel/255)**2.2 per channel. This renderer uses gamma 2.2 as its sRGB approximation; exact sRGB decoding is c/12.92 for c<=0.04045, otherwise ((c+0.055)/1.055)**2.4. Use exact decoding for known standard sRGB data, or gamma 2.2 when matching the renderer's base colors and texture transform. Never decode already-linear values, roughness, metalness, height or normals.\n- Native InputMaterial in layer 0 applies pow(color,1/2.2) to incoming primary/secondary/subsurface color, while later slots return linear color. Passing that layer-0 color back to OutputMaterial brightens it. If reading and rewriting incoming color in slot 0, raise it to 2.2 before linear calculations. Direct connected literals need no slot-specific correction. Leading empty slots still determine the shader's slot. PredefinedTexture's sRGB (Color) transform decodes RGB with pow(c,2.2); do not decode its output twice.\n- NoiseFBM detail is 1..16. Coordinate scale changes spatial frequency; noise scale is amplitude. Random is sampled during shading, not a stable per-object seed.\n- WavesTexture is radial in its supplied coordinates. For parallel grain or warp/weft, use the recipe graphs or component-wise periodic functions rather than assuming WavesTexture produces straight stripes.\n\nResource payloads\n- node({op:'PredefinedTexture',texture:catalogueName,...}) encodes the exact UTF-8 filename plus NUL. The GPU texture slot and dimensions are renderer-owned; never author them. write resolves the current display index from the filename. Texture also requires a discovered resource name; uploading files is host-owned.\n- Refresh requests run reload_textures before get_procedural_nodes; the latter returns a JSON string whose PredefinedTexture.inputs[2].values is authoritative. A refreshed name list does not prove GPU upload success.\n- node({op:'ColorRamp',ramp:[{position:0,color:{x:0,y:0,z:0}},{position:1,color:{x:1,y:1,z:1}}],inputs:{3:0}}) encodes 16 little-endian bytes per stop: position,r,g,b. Use at least two strictly increasing positions in 0..1. Modes come from input 3's enum.\n- node({op:'OptionSelector',options:[{name:'Low',value:0.2},{name:'High',value:0.8}],inputs:{1:0}}) encodes 36 bytes per option: float32 value and 32-byte NUL-terminated name. Maximum 32 options, names at most 31 UTF-8 bytes. Select an integer in [0,count). Dynamic selector connections are refused because this engine's upper clamp can read out of bounds.\n- Hidden/CPU-only payload ports cannot be connected. Unknown operation/payload, invalid selectors, cycles, disabled edges and duplicate output nodes are rejected before commit. Native payload padding is normalized without changing its meaning. HSL's output is vector-valued despite a Float descriptor; native numeric connections preserve it.\n\nRecipe: constant colored material with roughness\nconst graph = {\n nodes: [\n womp.materials.node({op:'Constant',name:'Color',reference_id:101,inputs:{2:{x:0.65,y:0.12,z:0.03},3:1}}),\n womp.materials.node({op:'Passthrough',name:'Roughness',inputs:{0:0.35}}),\n womp.materials.node({op:'OutputMaterial'})\n ],\n connections:[\n {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0}\n ]\n};\nprint(await womp.materials.write({layers:[graph],objectIds:[objectId]}));\n\nFor a noise gradient, connect InputPosition to NoiseFBM's coordinate input, NoiseFBM's scalar output to ColorRamp input 0, and the ramp to OutputMaterial input 0. Set frequency, amplitude and detail deliberately. For mapped textures, connect position through Mapping/Projection to PredefinedTexture's coordinate input, then connect its color to the material output. Resolve selectors for custom graphs from the current catalogue; reuse descriptors already read.\n\nProjection and texture controls\n- Resolve projection selectors by their current descriptor names. Match Planar to flat faces, Cubic to box faces, Cylindrical to axial sides and Equirectangular to spheres; inspect caps, seams, poles and transitions where applicable. Center normalized projections on the intended carrier and check the result after resizing.\n- Rotate position and normal inputs consistently, and set Mapping scale explicitly to 1. Compare projection changes under the same light and camera with an asymmetric texture that reveals axis swaps and stretching.\n- Choose PredefinedTexture's transform by the resource's known purpose: sRGB (Color), No transform for scalar data, or Normal for an encoded normal map. False for Disable Filtering or Do not repeat leaves filtering or repetition enabled. Check those settings at the intended image scale.\n- A single-channel texture may sample as (R,0,0). For neutral RGB, connect X through GetComponent to all three CombineValues inputs. A catalogue name does not establish channels, color space or normal convention. Alpha requires an explicit connection to the intended material input.\n- HSL hue, saturation and lightness inputs are additive offsets, not multipliers; use 0 for unchanged channels. Its color input/output is linear RGB. Perceptual palette edits can be performed in a known display-color encoding and decoded back to linear RGB before writing.\n\nRecipe: one-shader noise bump\nDiscover InputPosition, Mapping, NoiseFBM, Bump and OutputSurfaceNormal before using this example. It sets gray color, roughness 0.3 and metalness 0.3. Allocate fresh Constant references through write rather than copying IDs from another material.\nconst graph = {\n nodes: [\n womp.materials.node({op:'Constant',inputs:{2:0.5}}),\n womp.materials.node({op:'Passthrough',inputs:{0:0.3}}),\n womp.materials.node({op:'OutputMaterial'}),\n womp.materials.node({op:'InputPosition'}),\n womp.materials.node({op:'Mapping',inputs:{1:0,2:0,3:1}}),\n womp.materials.node({op:'NoiseFBM',inputs:{1:4,2:3,3:1,4:0}}),\n womp.materials.node({op:'Bump',inputs:{1:0.03}}),\n womp.materials.node({op:'OutputSurfaceNormal'})\n ],\n connections: [\n {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:3,flags:0},\n {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0},\n {input_node:3,input_slot:0,output_node:4,output_slot:0,flags:0},\n {input_node:4,input_slot:0,output_node:5,output_slot:0,flags:0},\n {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0},\n {input_node:6,input_slot:0,output_node:7,output_slot:0,flags:0}\n ]\n};\nreturn await womp.materials.write({layers:[graph],objectIds:[objectId]});\n\nBump and normal verification\n- Bump Scale changes shading-normal strength; coordinate scale changes feature frequency. Compare positive and negative Bump Scale under the same light when the relief direction matters. Neither changes the silhouette.\n- Strength depends on the height gradient, including feature frequency, contrast and noise detail. Native Bump estimates height differences over local-position offsets and returns a normalized tangent-space normal; its Scale is not a 0..1 blend strength or a physical depth in mm. The offsets depend on scene/object scale and numerical precision, not pixel footprint. More octaves do not guarantee resolved detail. Independently weight defining profiles and micro-noise before Bump; strengthen washed-out structure while preserving coherent highlights and the requested finish. Use calibration only with its stated space and feature size.\n- Feed a Normal-decoded texture into tangent-space OutputSurfaceNormal. Use a known map convention, and inspect flat and green-flipped controls if handedness is uncertain.\n- OutputHeight Scale is separate from tangent-normal-map strength. The current native OutputHeight shader does not use its Coarseness input; texture sampling has a separate control. Match the desired appearance visually rather than treating height and normal strengths as equivalent.\n- Combine normals deliberately and normalize the result. Whiteout composition, normalize(vec3(nTexture.x+nBump.x,nTexture.y+nBump.y,nTexture.z*nBump.z)), is approximate. Compare it with each input alone; these operations change shading normals, not geometry.\n\nVerification\n- When numeric, status().remaining counts compiler tasks globally; null means that query failed. Zero is not a per-graph success flag: old valid shaders can keep rendering during compilation or failure.\n- After writing, inspect a scene screenshot, compare intended channels at a useful scale, and reopen the saved scene when persistence is part of acceptance. Separate committed, persisted, compiler queue idle, and visually verified results.\n- The current SDK does not expose per-shader compiler diagnostics. Retain write receipts and any available runtime error; do not infer compilation success from an idle queue.\n- InputSceneInfo IDs and Random values are runtime evaluation data, not persistent authored seeds. Use Constants when reproducible variation is required.\n";
@@ -0,0 +1,97 @@
1
+ // A one-layer graph with procedural bump feeding the surface normal.
2
+ export const MATERIALS_NOISE_BUMP_RECIPE = `const graph = {
3
+ nodes: [
4
+ womp.materials.node({op:'Constant',inputs:{2:0.5}}),
5
+ womp.materials.node({op:'Passthrough',inputs:{0:0.3}}),
6
+ womp.materials.node({op:'OutputMaterial'}),
7
+ womp.materials.node({op:'InputPosition'}),
8
+ womp.materials.node({op:'Mapping',inputs:{1:0,2:0,3:1}}),
9
+ womp.materials.node({op:'NoiseFBM',inputs:{1:4,2:3,3:1,4:0}}),
10
+ womp.materials.node({op:'Bump',inputs:{1:0.03}}),
11
+ womp.materials.node({op:'OutputSurfaceNormal'})
12
+ ],
13
+ connections: [
14
+ {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},
15
+ {input_node:1,input_slot:0,output_node:2,output_slot:3,flags:0},
16
+ {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0},
17
+ {input_node:3,input_slot:0,output_node:4,output_slot:0,flags:0},
18
+ {input_node:4,input_slot:0,output_node:5,output_slot:0,flags:0},
19
+ {input_node:5,input_slot:0,output_node:6,output_slot:0,flags:0},
20
+ {input_node:6,input_slot:0,output_node:7,output_slot:0,flags:0}
21
+ ]
22
+ };`;
23
+ export const MATERIALS_REFERENCE = `
24
+ Procedural shaders are material graphs, separate from Geometry Nodes and Workbench Flow. recipe() builds the standard grid, noise, grain and woven graphs with live selectors internally; their controls are already defined. Use catalog() for unfamiliar custom operations or includeTextures:true for resource names. get_node_types is a different graph system.
25
+
26
+ Storage and sharing
27
+ - Material IDs and shader IDs occupy separate positive 16-bit namespaces; material 0 remains the scene default. There are eight ordered shader slots, where null/0 is empty. write always pads trailing slots to eight to clear the native layer cache.
28
+ - write without materialId creates a material. cloneFrom copies base properties, layers and metadata. Existing materialId edits affect every user of that material. Graph edits copy shared shader storage automatically.
29
+ - Graph nodes have op, optional name/location/editor_flags/reference_id, in0..in19 vectors, and optional external_data bytes. Read preserves native data. Keep unrelated metadata; node() constructs new nodes with scalar broadcasting and payload encoding.
30
+ - Connections use zero-based ARRAY INDICES: input_node/source node, input_slot/source OUTPUT; output_node/destination node, output_slot/destination INPUT; flags:0 for new connections; native differential flags:2 are also supported. An input has one producer. Graphs must be acyclic, output nodes terminal, and enum/integer/Boolean selectors literal. Numeric Float/Float3/Color ports can interconnect; native math uses float3 and scalar consumers use X.
31
+ - Unconnected OutputMaterial inputs inherit the incoming material, including the previous layer. Literal values on unconnected OutputMaterial inputs do not set properties. Connect Constant or Passthrough instead.
32
+ - OutputMaterial inputs: 0 primary color, 1 secondary color, 2 subsurface, 3 metalness, 4 roughness, 5 transmittance, 6 translucency, 7 IOR, 8 iridescence, 9 emission, 10 absorption, 11 sheen, 12 specular tint, 13 translucency weight, 14 material weight, 15 dispersion, 16 displacement. Verify descriptors on the current engine.
33
+ - InputMaterial and the exactly spelled InputUnderlayingMaterial read the incoming layer material, not independently preserved base snapshots. OutputHeight changes shading normals. Material displacement has separate export semantics; do not claim it changed mesh geometry.
34
+
35
+ Constants and resetting
36
+ - Constant input 0 chooses Pi/Half Pi/Double Pi/Exponent; input 1 is value vector, 2 color, 3 UI exposure enum, 4 scope. Outputs 0 built-in constant, 1 value, 2 color.
37
+ - Give a new adjustable Constant a unique positive safe-integer reference_id, or let write allocate it and read it back. Keep reference IDs stable while editing. Independent constants across layers must have distinct references.
38
+ - constants overrides are keyed by reference ID and contain BOTH value:{x,y,z} and color:{x,y,z}. Native omission zeros the missing counterpart. Use read to preserve it.
39
+ - Reset with constants:{[referenceId]:null}. Kakapo retains deleted override keys in its in-memory cache; Spark gives the reset node a fresh reference and removes the stored old override. Replacing layers on an existing material also gives unoverridden Constants fresh references, so reusing an old graph cannot revive cached overrides. Use the returned referenceIdChanges mapping for subsequent edits. Other materials retain their references and overrides. Reset follows graph defaults and survives reopening.
40
+
41
+ Units, colors and mapping
42
+ - Graph vectors use renderer coordinates. One native length unit is 25 mm; Spark scene editing flips X/Z into its public basis, so a public mm position maps to {-z/(25*u),y/(25*u),-x/(25*u)}, where u is the scene unit_multiplier (default 1). Do not scale every numeric shader input: colors, frequencies, normals and normalized UVs have different meanings.
43
+ - Mapping computes rotate(value, rotationDegrees) * scale + translation. Set scale explicitly to {x:1,y:1,z:1}; omitted literals are zero. Projection mode comes from the live enum descriptor.
44
+ - InputPosition uses the carrier's local coordinates, including its procedural transform; it is not universally a normalized box or a physical distance. InputWorldPosition uses renderer world coordinates. Pair local position with InputNormal, or world position with InputWorldNormal, for projection. Compensate non-uniform geometry scale in mapping, without changing the geometry. A 1-unit and 0.1-unit object-space grid together reveals actual spacing and stretching in one inspection.
45
+ - Connected procedural colors are linear RGB in 0..1: Constant colors, ramp stops and recipe colorA/colorB. scene materialSetColor uses display RGB 0..255; to reproduce its base color in a procedural graph use (channel/255)**2.2 per channel. This renderer uses gamma 2.2 as its sRGB approximation; exact sRGB decoding is c/12.92 for c<=0.04045, otherwise ((c+0.055)/1.055)**2.4. Use exact decoding for known standard sRGB data, or gamma 2.2 when matching the renderer's base colors and texture transform. Never decode already-linear values, roughness, metalness, height or normals.
46
+ - Native InputMaterial in layer 0 applies pow(color,1/2.2) to incoming primary/secondary/subsurface color, while later slots return linear color. Passing that layer-0 color back to OutputMaterial brightens it. If reading and rewriting incoming color in slot 0, raise it to 2.2 before linear calculations. Direct connected literals need no slot-specific correction. Leading empty slots still determine the shader's slot. PredefinedTexture's sRGB (Color) transform decodes RGB with pow(c,2.2); do not decode its output twice.
47
+ - NoiseFBM detail is 1..16. Coordinate scale changes spatial frequency; noise scale is amplitude. Random is sampled during shading, not a stable per-object seed.
48
+ - WavesTexture is radial in its supplied coordinates. For parallel grain or warp/weft, use the recipe graphs or component-wise periodic functions rather than assuming WavesTexture produces straight stripes.
49
+
50
+ Resource payloads
51
+ - node({op:'PredefinedTexture',texture:catalogueName,...}) encodes the exact UTF-8 filename plus NUL. The GPU texture slot and dimensions are renderer-owned; never author them. write resolves the current display index from the filename. Texture also requires a discovered resource name; uploading files is host-owned.
52
+ - Refresh requests run reload_textures before get_procedural_nodes; the latter returns a JSON string whose PredefinedTexture.inputs[2].values is authoritative. A refreshed name list does not prove GPU upload success.
53
+ - node({op:'ColorRamp',ramp:[{position:0,color:{x:0,y:0,z:0}},{position:1,color:{x:1,y:1,z:1}}],inputs:{3:0}}) encodes 16 little-endian bytes per stop: position,r,g,b. Use at least two strictly increasing positions in 0..1. Modes come from input 3's enum.
54
+ - node({op:'OptionSelector',options:[{name:'Low',value:0.2},{name:'High',value:0.8}],inputs:{1:0}}) encodes 36 bytes per option: float32 value and 32-byte NUL-terminated name. Maximum 32 options, names at most 31 UTF-8 bytes. Select an integer in [0,count). Dynamic selector connections are refused because this engine's upper clamp can read out of bounds.
55
+ - Hidden/CPU-only payload ports cannot be connected. Unknown operation/payload, invalid selectors, cycles, disabled edges and duplicate output nodes are rejected before commit. Native payload padding is normalized without changing its meaning. HSL's output is vector-valued despite a Float descriptor; native numeric connections preserve it.
56
+
57
+ Recipe: constant colored material with roughness
58
+ const graph = {
59
+ nodes: [
60
+ womp.materials.node({op:'Constant',name:'Color',reference_id:101,inputs:{2:{x:0.65,y:0.12,z:0.03},3:1}}),
61
+ womp.materials.node({op:'Passthrough',name:'Roughness',inputs:{0:0.35}}),
62
+ womp.materials.node({op:'OutputMaterial'})
63
+ ],
64
+ connections:[
65
+ {input_node:0,input_slot:2,output_node:2,output_slot:0,flags:0},
66
+ {input_node:1,input_slot:0,output_node:2,output_slot:4,flags:0}
67
+ ]
68
+ };
69
+ print(await womp.materials.write({layers:[graph],objectIds:[objectId]}));
70
+
71
+ For a noise gradient, connect InputPosition to NoiseFBM's coordinate input, NoiseFBM's scalar output to ColorRamp input 0, and the ramp to OutputMaterial input 0. Set frequency, amplitude and detail deliberately. For mapped textures, connect position through Mapping/Projection to PredefinedTexture's coordinate input, then connect its color to the material output. Resolve selectors for custom graphs from the current catalogue; reuse descriptors already read.
72
+
73
+ Projection and texture controls
74
+ - Resolve projection selectors by their current descriptor names. Match Planar to flat faces, Cubic to box faces, Cylindrical to axial sides and Equirectangular to spheres; inspect caps, seams, poles and transitions where applicable. Center normalized projections on the intended carrier and check the result after resizing.
75
+ - Rotate position and normal inputs consistently, and set Mapping scale explicitly to 1. Compare projection changes under the same light and camera with an asymmetric texture that reveals axis swaps and stretching.
76
+ - Choose PredefinedTexture's transform by the resource's known purpose: sRGB (Color), No transform for scalar data, or Normal for an encoded normal map. False for Disable Filtering or Do not repeat leaves filtering or repetition enabled. Check those settings at the intended image scale.
77
+ - A single-channel texture may sample as (R,0,0). For neutral RGB, connect X through GetComponent to all three CombineValues inputs. A catalogue name does not establish channels, color space or normal convention. Alpha requires an explicit connection to the intended material input.
78
+ - HSL hue, saturation and lightness inputs are additive offsets, not multipliers; use 0 for unchanged channels. Its color input/output is linear RGB. Perceptual palette edits can be performed in a known display-color encoding and decoded back to linear RGB before writing.
79
+
80
+ Recipe: one-shader noise bump
81
+ Discover InputPosition, Mapping, NoiseFBM, Bump and OutputSurfaceNormal before using this example. It sets gray color, roughness 0.3 and metalness 0.3. Allocate fresh Constant references through write rather than copying IDs from another material.
82
+ ${MATERIALS_NOISE_BUMP_RECIPE}
83
+ return await womp.materials.write({layers:[graph],objectIds:[objectId]});
84
+
85
+ Bump and normal verification
86
+ - Bump Scale changes shading-normal strength; coordinate scale changes feature frequency. Compare positive and negative Bump Scale under the same light when the relief direction matters. Neither changes the silhouette.
87
+ - Strength depends on the height gradient, including feature frequency, contrast and noise detail. Native Bump estimates height differences over local-position offsets and returns a normalized tangent-space normal; its Scale is not a 0..1 blend strength or a physical depth in mm. The offsets depend on scene/object scale and numerical precision, not pixel footprint. More octaves do not guarantee resolved detail. Independently weight defining profiles and micro-noise before Bump; strengthen washed-out structure while preserving coherent highlights and the requested finish. Use calibration only with its stated space and feature size.
88
+ - Feed a Normal-decoded texture into tangent-space OutputSurfaceNormal. Use a known map convention, and inspect flat and green-flipped controls if handedness is uncertain.
89
+ - OutputHeight Scale is separate from tangent-normal-map strength. The current native OutputHeight shader does not use its Coarseness input; texture sampling has a separate control. Match the desired appearance visually rather than treating height and normal strengths as equivalent.
90
+ - Combine normals deliberately and normalize the result. Whiteout composition, normalize(vec3(nTexture.x+nBump.x,nTexture.y+nBump.y,nTexture.z*nBump.z)), is approximate. Compare it with each input alone; these operations change shading normals, not geometry.
91
+
92
+ Verification
93
+ - When numeric, status().remaining counts compiler tasks globally; null means that query failed. Zero is not a per-graph success flag: old valid shaders can keep rendering during compilation or failure.
94
+ - After writing, inspect a scene screenshot, compare intended channels at a useful scale, and reopen the saved scene when persistence is part of acceptance. Separate committed, persisted, compiler queue idle, and visually verified results.
95
+ - The current SDK does not expose per-shader compiler diagnostics. Retain write receipts and any available runtime error; do not infer compilation success from an idle queue.
96
+ - InputSceneInfo IDs and Random values are runtime evaluation data, not persistent authored seeds. Use Constants when reproducible variation is required.
97
+ `;