@womp/spark-agent 5.6.1 → 5.7.1

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 (43) hide show
  1. package/README.md +424 -421
  2. package/dist/agent-run.d.ts +1 -1
  3. package/dist/agent.d.ts +2 -0
  4. package/dist/agent.js +3 -1
  5. package/dist/capabilities/geometry.js +14 -14
  6. package/dist/capabilities/materials.js +14 -14
  7. package/dist/capabilities/openscad-instructions.js +77 -77
  8. package/dist/capabilities/operations.js +28 -28
  9. package/dist/capabilities/scene.js +81 -81
  10. package/dist/capabilities/wompify.js +10 -10
  11. package/dist/client.d.ts +7 -0
  12. package/dist/client.js +14 -0
  13. package/dist/geometry/schemas/backend-schema.json +3393 -3393
  14. package/dist/geometry/schemas/flowir-dialect.json +1023 -1023
  15. package/dist/geometry.js +6 -6
  16. package/dist/goals.d.ts +43 -0
  17. package/dist/goals.js +37 -0
  18. package/dist/materials/recipes.js +266 -266
  19. package/dist/materials/reference.js +94 -94
  20. package/dist/materials/structure.js +10 -10
  21. package/dist/materials/surface-recipes.js +6 -6
  22. package/dist/prompt.js +74 -74
  23. package/dist/server/executor.d.ts +1 -1
  24. package/dist/server/executor.js +2 -0
  25. package/dist/server/express.js +18 -0
  26. package/dist/server/fixtures/materials/matte-blue.json +14 -14
  27. package/dist/server/fixtures/materials/polished-copper.json +14 -14
  28. package/dist/server/goal-evaluator.d.ts +29 -0
  29. package/dist/server/goal-evaluator.js +55 -0
  30. package/dist/server/index.d.ts +4 -0
  31. package/dist/server/index.js +4 -0
  32. package/dist/server/service.d.ts +17 -0
  33. package/dist/server/service.js +204 -5
  34. package/dist/server/sqlite-store.d.ts +2 -0
  35. package/dist/server/sqlite-store.js +45 -38
  36. package/dist/server/store.d.ts +2 -0
  37. package/dist/server/store.js +12 -3
  38. package/dist/server/types.d.ts +6 -0
  39. package/dist/ui.d.ts +7 -3
  40. package/dist/ui.js +3 -1
  41. package/dist/wompify.d.ts +11 -0
  42. package/dist/wompify.js +11 -2
  43. package/package.json +140 -140
@@ -1,284 +1,284 @@
1
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;
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
6
  };`;
7
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}
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
13
  });`;
14
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
- ]
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
40
  };`;
41
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
- ]
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
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
- ]
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
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'}));
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
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
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}
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
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;
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
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;
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
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');
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
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);
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
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)];
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
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.
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
284
  `;