@instruktlabs/kiln 1.2.2 → 1.2.3

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 (214) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +74 -0
  4. package/README.md +7 -0
  5. package/dist/agent-run.mjs +6392 -1477
  6. package/dist/build.json +23 -23
  7. package/dist/cli.mjs +8453 -3407
  8. package/dist/evaluator-worker.mjs +16608 -12387
  9. package/dist/mcp-engine.mjs +6746 -1725
  10. package/dist/mcp-server.mjs +4 -4
  11. package/dist/viewer/app.js +203 -199
  12. package/dist/viewer/chat.html +209 -205
  13. package/dist/viewer/style.css +10 -0
  14. package/docs/atlas-provenance.md +23 -0
  15. package/docs/community-exporter.md +27 -0
  16. package/docs/export-profiles.md +20 -0
  17. package/docs/material-library.md +9 -5
  18. package/docs/procedural-surfaces-v3.md +276 -0
  19. package/docs/projects-and-live-review.md +1 -1
  20. package/docs/rendering.md +41 -0
  21. package/docs/sdk.md +53 -0
  22. package/docs/tools-operations.md +2 -2
  23. package/docs/tools.md +3 -3
  24. package/lib/asset-materials-node.d.ts +3 -3
  25. package/lib/asset-materials-node.js +29 -33
  26. package/lib/assets-node.d.ts +1 -1
  27. package/lib/assets-node.js +11 -4
  28. package/lib/assets.d.ts +4 -2
  29. package/lib/assets.js +61 -16
  30. package/lib/atlas-provenance.d.ts +86 -0
  31. package/lib/atlas-provenance.js +448 -0
  32. package/lib/character.d.ts +11 -2
  33. package/lib/character.js +90 -2
  34. package/lib/community-exporter.d.ts +3 -2
  35. package/lib/community-exporter.js +139 -3
  36. package/lib/discovery/helper-contracts.js +161 -7
  37. package/lib/discovery/helper-specs.js +49 -4
  38. package/lib/discovery/shapes.js +20 -7
  39. package/lib/engine-identity.d.ts +1 -1
  40. package/lib/engine-identity.js +1 -1
  41. package/lib/evaluator/authoring-diagnostic.d.ts +1 -1
  42. package/lib/evaluator/authoring-diagnostic.js +12 -0
  43. package/lib/evaluator/protocol.d.ts +2 -2
  44. package/lib/evaluator/protocol.js +16 -6
  45. package/lib/evaluator/subprocess.js +2 -2
  46. package/lib/exporter-node.js +9 -1
  47. package/lib/geometry-budget.d.ts +7 -0
  48. package/lib/geometry-budget.js +8 -0
  49. package/lib/gltf-texture-binding.d.ts +6 -0
  50. package/lib/gltf-texture-binding.js +54 -0
  51. package/lib/material-authoring-node.d.ts +6 -0
  52. package/lib/material-authoring-node.js +49 -0
  53. package/lib/material-authoring-schema.d.ts +871 -0
  54. package/lib/material-authoring-schema.js +310 -0
  55. package/lib/material-export.d.ts +5 -0
  56. package/lib/material-export.js +21 -0
  57. package/lib/material-library-node.d.ts +34 -8
  58. package/lib/material-library-node.js +691 -11
  59. package/lib/material-library.d.ts +3704 -4
  60. package/lib/material-library.js +272 -0
  61. package/lib/mcp-core.d.ts +1 -1
  62. package/lib/pinned-material-map.d.ts +33 -0
  63. package/lib/pinned-material-map.js +1 -0
  64. package/lib/portable-material-runtime.d.ts +5 -0
  65. package/lib/portable-material-runtime.js +160 -0
  66. package/lib/portable-sampler-runtime.d.ts +4 -0
  67. package/lib/portable-sampler-runtime.js +26 -0
  68. package/lib/portable-sampler.d.ts +11 -0
  69. package/lib/portable-sampler.js +42 -0
  70. package/lib/primitives.d.ts +15 -3
  71. package/lib/primitives.js +24 -4
  72. package/lib/procedural-material-v2.d.ts +2 -0
  73. package/lib/procedural-material-v2.js +2 -1
  74. package/lib/procedural-material-v3.d.ts +72 -0
  75. package/lib/procedural-material-v3.js +162 -0
  76. package/lib/procedural-texture-v2-compiler.d.ts +31 -0
  77. package/lib/procedural-texture-v2-compiler.js +172 -0
  78. package/lib/procedural-texture-v3-contract.d.ts +125 -0
  79. package/lib/procedural-texture-v3-contract.js +333 -0
  80. package/lib/procedural-texture-v3.d.ts +12 -0
  81. package/lib/procedural-texture-v3.js +165 -0
  82. package/lib/procedural-texture.d.ts +9 -31
  83. package/lib/procedural-texture.js +43 -170
  84. package/lib/project-bundle-node.d.ts +1 -1
  85. package/lib/project-bundle-node.js +70 -13
  86. package/lib/project-bundle.d.ts +91 -3
  87. package/lib/project-bundle.js +48 -7
  88. package/lib/qa/atlas.d.ts +21 -0
  89. package/lib/qa/atlas.js +474 -0
  90. package/lib/qa/index.d.ts +1 -0
  91. package/lib/qa/index.js +1 -0
  92. package/lib/qa/material.js +2 -0
  93. package/lib/qa/skin-integrity.d.ts +18 -0
  94. package/lib/qa/skin-integrity.js +221 -0
  95. package/lib/render-service-host.js +4 -7
  96. package/lib/render.d.ts +3 -3
  97. package/lib/render.js +108 -16
  98. package/lib/skin-binding.d.ts +20 -0
  99. package/lib/skin-binding.js +144 -0
  100. package/lib/texture-atlas-contract.d.ts +97 -0
  101. package/lib/texture-atlas-contract.js +414 -0
  102. package/lib/texture-atlas-pixels.d.ts +73 -0
  103. package/lib/texture-atlas-pixels.js +290 -0
  104. package/lib/texture-atlas.d.ts +10 -0
  105. package/lib/texture-atlas.js +143 -0
  106. package/lib/texture-bake.d.ts +3 -0
  107. package/lib/texture-bake.js +66 -11
  108. package/lib/texture-resolver.d.ts +22 -0
  109. package/lib/texture-resolver.js +24 -0
  110. package/lib/tools/materials.js +8 -7
  111. package/lib/tools/registry.js +5 -16
  112. package/lib/tools/review.js +1 -1
  113. package/lib/uv-data.d.ts +32 -0
  114. package/lib/uv-data.js +172 -0
  115. package/lib/uv-shapes.js +3 -0
  116. package/lib/uv.d.ts +9 -3
  117. package/lib/uv.js +53 -82
  118. package/lib/views/camera.d.ts +3 -3
  119. package/lib/views/camera.js +32 -28
  120. package/lib/views/face-policy.d.ts +9 -0
  121. package/lib/views/face-policy.js +16 -0
  122. package/lib/views/glb.js +218 -24
  123. package/lib/views/pose.d.ts +7 -1
  124. package/lib/views/pose.js +63 -12
  125. package/lib/views/raster.d.ts +6 -2
  126. package/lib/views/raster.js +49 -37
  127. package/lib/views/scene-copy.d.ts +6 -0
  128. package/lib/views/scene-copy.js +73 -0
  129. package/lib/views/scene-raster.js +2 -1
  130. package/lib/views/triangle-coverage.d.ts +8 -0
  131. package/lib/views/triangle-coverage.js +58 -0
  132. package/lib/workspace-node.js +7 -3
  133. package/lib/workspace.d.ts +2 -2
  134. package/package.json +3 -1
  135. package/plugin.json +1 -1
  136. package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
  137. package/plugins/kiln-engine/README.md +3 -3
  138. package/plugins/kiln-engine/package-provenance.json +6 -6
  139. package/plugins/kiln-engine/plugin.json +1 -1
  140. package/plugins/kiln-engine/runtime.json +1 -1
  141. package/skills/kiln-author-asset/references/geometry-recipes.md +124 -0
  142. package/skills/kiln-author-asset/references/projects-and-materials.md +32 -2
  143. package/skills/kiln-compose-scene/references/runtime-scenes.md +19 -0
  144. package/skills/kiln-qa-asset/references/integration-checks.md +37 -2
  145. package/src/asset-materials-node.ts +38 -45
  146. package/src/assets-node.ts +22 -5
  147. package/src/assets.ts +61 -20
  148. package/src/atlas-provenance.ts +522 -0
  149. package/src/character.ts +107 -2
  150. package/src/community-exporter.ts +143 -3
  151. package/src/discovery/helper-contracts.ts +188 -7
  152. package/src/discovery/helper-specs.ts +69 -4
  153. package/src/discovery/shapes.ts +25 -9
  154. package/src/engine-identity.ts +1 -1
  155. package/src/evaluator/authoring-diagnostic.ts +19 -1
  156. package/src/evaluator/protocol.ts +19 -9
  157. package/src/evaluator/subprocess.ts +2 -2
  158. package/src/exporter-node.ts +12 -1
  159. package/src/generated/mcp-manifest.json +3 -3
  160. package/src/geometry-budget.ts +8 -0
  161. package/src/gltf-texture-binding.ts +60 -0
  162. package/src/material-authoring-node.ts +68 -0
  163. package/src/material-authoring-schema.ts +343 -0
  164. package/src/material-export.ts +25 -0
  165. package/src/material-library-node.ts +840 -16
  166. package/src/material-library.ts +348 -5
  167. package/src/pinned-material-map.ts +43 -0
  168. package/src/portable-material-runtime.ts +200 -0
  169. package/src/portable-sampler-runtime.ts +40 -0
  170. package/src/portable-sampler.ts +54 -0
  171. package/src/primitives.ts +98 -4
  172. package/src/procedural-material-v2.ts +2 -1
  173. package/src/procedural-material-v3.ts +224 -0
  174. package/src/procedural-texture-v2-compiler.ts +234 -0
  175. package/src/procedural-texture-v3-contract.ts +437 -0
  176. package/src/procedural-texture-v3.ts +220 -0
  177. package/src/procedural-texture.ts +61 -229
  178. package/src/project-bundle-node.ts +106 -14
  179. package/src/project-bundle.ts +61 -18
  180. package/src/project-cli.ts +29 -17
  181. package/src/qa/atlas.ts +597 -0
  182. package/src/qa/index.ts +1 -0
  183. package/src/qa/material.ts +2 -0
  184. package/src/qa/skin-integrity.ts +291 -0
  185. package/src/render-service-host.ts +4 -6
  186. package/src/render.ts +133 -15
  187. package/src/skin-binding.ts +225 -0
  188. package/src/texture-atlas-contract.ts +558 -0
  189. package/src/texture-atlas-pixels.ts +456 -0
  190. package/src/texture-atlas.ts +185 -0
  191. package/src/texture-bake.ts +88 -10
  192. package/src/texture-resolver.ts +55 -0
  193. package/src/tools/materials.ts +15 -11
  194. package/src/tools/registry.ts +5 -15
  195. package/src/tools/review.ts +1 -1
  196. package/src/uv-data.ts +238 -0
  197. package/src/uv-shapes.ts +3 -0
  198. package/src/uv.ts +73 -128
  199. package/src/viewer/app.ts +37 -0
  200. package/src/viewer/live-panel.ts +57 -1
  201. package/src/viewer/material-preview-panel.ts +140 -0
  202. package/src/viewer/material-preview.ts +138 -0
  203. package/src/viewer/materials-panel.ts +34 -5
  204. package/src/viewer/navigation.ts +2 -0
  205. package/src/views/camera.ts +37 -40
  206. package/src/views/face-policy.ts +20 -0
  207. package/src/views/glb.ts +294 -17
  208. package/src/views/pose.ts +71 -13
  209. package/src/views/raster.ts +73 -46
  210. package/src/views/scene-copy.ts +86 -0
  211. package/src/views/scene-raster.ts +2 -1
  212. package/src/views/triangle-coverage.ts +78 -0
  213. package/src/workspace-node.ts +11 -3
  214. package/src/workspace.ts +2 -2
@@ -1,8 +1,11 @@
1
1
  import * as THREE from 'three';
2
2
  import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
3
+ import { GLB_BUFFER } from '@gltf-transform/core';
3
4
  import { createGltfIO } from "./gltf-io.js";
4
5
  import { openShellExtras } from "./open-shell.js";
5
6
  import { rigExtrasForExport } from "./rig-export.js";
7
+ import { assertPortableMaterialSides } from "./material-export.js";
8
+ import { collectAtlasProvenance, refreshAtlasDocumentProvenance, } from "./atlas-provenance.js";
6
9
  import { KILN_SEMANTIC_EXTRAS_KEY, validateSemanticMetadataV1, cloneSemanticMetadataV1, createSemanticMetadataV1, } from "./contracts/index.js";
7
10
  export function resolveGltfExporter(value = typeof process !== 'undefined'
8
11
  ? process.env['KILN_GLTF_EXPORTER']
@@ -39,9 +42,13 @@ function cleanClone(source) {
39
42
  shadow.userData = {};
40
43
  return shadow.clone();
41
44
  }
42
- export async function communitySceneDocument(root, clips, platform = browserPlatform, authorExtras) {
45
+ export async function communitySceneDocument(root, clips, platform = browserPlatform, authorExtras, atlas = collectAtlasProvenance(root)) {
46
+ assertPortableMaterialSides(root);
43
47
  const materials = new Map();
48
+ const originalMaterials = new Map();
49
+ const originalGeometries = new Map();
44
50
  const textures = new Map();
51
+ const directImages = new Map();
45
52
  const nodes = new Map();
46
53
  const encoded = [];
47
54
  function texture(source) {
@@ -55,6 +62,11 @@ export async function communitySceneDocument(root, clips, platform = browserPlat
55
62
  textures.set(source, copy);
56
63
  const bytes = source.userData['encoded'];
57
64
  if (bytes?.bytes) {
65
+ // Canvas stores premultiplied color and can discard hidden cutout RGB or
66
+ // round partial-alpha channels. Keep already-encoded direct images when
67
+ // the exporter does not need to flip or derive a new texture from them.
68
+ if (!copy.flipY && (bytes.mime === 'image/png' || bytes.mime === 'image/jpeg'))
69
+ directImages.set(copy, { bytes: bytes.bytes, mime: bytes.mime });
58
70
  encoded.push((async () => {
59
71
  copy.image = await platform.decode(bytes.bytes);
60
72
  Object.defineProperty(copy, 'isDataTexture', { value: true });
@@ -83,6 +95,7 @@ export async function communitySceneDocument(root, clips, platform = browserPlat
83
95
  copy[key] = texture(value);
84
96
  }
85
97
  materials.set(source, copy);
98
+ originalMaterials.set(copy, source);
86
99
  return copy;
87
100
  }
88
101
  function node(source) {
@@ -115,6 +128,7 @@ export async function communitySceneDocument(root, clips, platform = browserPlat
115
128
  map: sprite.material.map ? texture(sprite.material.map) : null,
116
129
  });
117
130
  Object.assign(spriteMaterial.userData, authorExtras?.material(sprite.material));
131
+ originalMaterials.set(spriteMaterial, sprite.material);
118
132
  copy = new THREE.Mesh(geometry, spriteMaterial);
119
133
  THREE.Object3D.prototype.copy.call(copy, shadow, false);
120
134
  }
@@ -143,6 +157,7 @@ export async function communitySceneDocument(root, clips, platform = browserPlat
143
157
  const original = source;
144
158
  const mesh = copy;
145
159
  mesh.geometry = cleanClone(original.geometry);
160
+ originalGeometries.set(mesh, original.geometry);
146
161
  mesh.material = Array.isArray(original.material)
147
162
  ? original.material.map(material)
148
163
  : material(original.material);
@@ -169,6 +184,47 @@ export async function communitySceneDocument(root, clips, platform = browserPlat
169
184
  }
170
185
  await Promise.all(encoded);
171
186
  await platform.prepare(textures.size > 0);
187
+ // Upstream channel-packing and normal-basis conversions use drawImage, which
188
+ // does not accept a DataTexture's {data,width,height} image in either host.
189
+ // Adapt only those detached inputs; direct color textures keep their bytes.
190
+ const canvasInputs = new Map();
191
+ const canvasInput = (map) => {
192
+ const raw = map?.image;
193
+ if (!map || !raw?.data)
194
+ return map;
195
+ const found = canvasInputs.get(map);
196
+ if (found)
197
+ return found;
198
+ const { data, width, height } = raw;
199
+ const canvas = new OffscreenCanvas(width, height);
200
+ const context = canvas.getContext('2d');
201
+ if (!context)
202
+ throw new Error('A 2D canvas is required to prepare export texture channels.');
203
+ context.putImageData(new ImageData(new Uint8ClampedArray(data), width, height), 0, 0);
204
+ const adapted = new THREE.Texture().copy(map);
205
+ adapted.source = new THREE.TextureSource(canvas);
206
+ canvasInputs.set(map, adapted);
207
+ const direct = directImages.get(map);
208
+ if (direct)
209
+ directImages.set(adapted, direct);
210
+ return adapted;
211
+ };
212
+ for (const material of materials.values()) {
213
+ // putImageData ignores the canvas transform used by GLTFExporter for flipY.
214
+ // A canvas image makes that explicit orientation request reach drawImage.
215
+ for (const [key, value] of Object.entries(material)) {
216
+ if (value instanceof THREE.Texture && value.flipY && value.image?.data)
217
+ material[key] = canvasInput(value);
218
+ }
219
+ const standard = material;
220
+ if (!standard.isMeshStandardMaterial)
221
+ continue;
222
+ if (standard.metalnessMap !== standard.roughnessMap) {
223
+ standard.metalnessMap = canvasInput(standard.metalnessMap);
224
+ standard.roughnessMap = canvasInput(standard.roughnessMap);
225
+ }
226
+ standard.normalMap = canvasInput(standard.normalMap);
227
+ }
172
228
  const exportClips = clips.map((clip) => {
173
229
  const cloned = cleanClone(clip);
174
230
  // UUID-targeted clips must point at the export copy, not the live scene.
@@ -191,7 +247,48 @@ export async function communitySceneDocument(root, clips, platform = browserPlat
191
247
  });
192
248
  return cloned;
193
249
  });
194
- const bytes = await new GLTFExporter().parseAsync(copy, {
250
+ const preservedImages = new Map();
251
+ const exporter = new GLTFExporter().register(() => ({
252
+ writeMesh(mesh, definition) {
253
+ const geometry = originalGeometries.get(mesh);
254
+ const binding = geometry && atlas.binding(geometry);
255
+ if (binding)
256
+ for (const primitive of definition.primitives)
257
+ primitive.extras = { ...primitive.extras, kilnAtlasBinding: binding };
258
+ },
259
+ async writeMaterialAsync(material, definition) {
260
+ const source = originalMaterials.get(material);
261
+ if (!source)
262
+ return;
263
+ const bag = source;
264
+ const pbr = definition.pbrMetallicRoughness;
265
+ for (const [slot, info] of [
266
+ ['map', pbr?.baseColorTexture],
267
+ ['normalMap', definition.normalTexture],
268
+ ['metalnessMap', pbr?.metallicRoughnessTexture],
269
+ ['roughnessMap', pbr?.metallicRoughnessTexture],
270
+ [
271
+ 'emissiveMap',
272
+ definition.emissiveTexture,
273
+ ],
274
+ ['aoMap', definition.occlusionTexture],
275
+ ]) {
276
+ const texture = bag[slot], ref = texture && atlas.texture(texture);
277
+ if (ref && info) {
278
+ const prior = info.extras?.['kilnAtlasReference'];
279
+ if (prior && prior.id !== ref.id)
280
+ throw new TypeError('Atlas channel packing requires one exact source identity.');
281
+ info.extras = { ...info.extras, kilnAtlasReference: ref };
282
+ }
283
+ }
284
+ },
285
+ writeTexture(map, definition) {
286
+ const direct = directImages.get(map);
287
+ if (direct && typeof definition['source'] === 'number')
288
+ preservedImages.set(definition['source'], direct);
289
+ },
290
+ }));
291
+ const bytes = await exporter.parseAsync(copy, {
195
292
  binary: true,
196
293
  trs: true,
197
294
  onlyVisible: false,
@@ -199,6 +296,39 @@ export async function communitySceneDocument(root, clips, platform = browserPlat
199
296
  });
200
297
  const io = createGltfIO();
201
298
  const json = await io.binaryToJSON(new Uint8Array(bytes));
299
+ for (const [index, original] of preservedImages) {
300
+ const image = json.json.images?.[index];
301
+ if (!image)
302
+ throw new Error('The glTF converter lost a direct texture image.');
303
+ const uri = `kiln-direct-image-${index}`;
304
+ image.uri = uri;
305
+ image.mimeType = original.mime;
306
+ delete image.bufferView;
307
+ json.resources[uri] = new Uint8Array(original.bytes);
308
+ }
309
+ // GLTFExporter includes four-byte buffer alignment in generated image views.
310
+ // Keep the BIN allocation aligned but end a PNG's view at its IEND chunk.
311
+ // Direct authored bytes above remain untouched; this is not an input decoder.
312
+ for (const image of json.json.images ?? []) {
313
+ if (image.mimeType !== 'image/png' || image.bufferView === undefined)
314
+ continue;
315
+ const view = json.json.bufferViews[image.bufferView];
316
+ const buffer = json.resources[json.json.buffers?.[view.buffer]?.uri ?? GLB_BUFFER];
317
+ const start = view.byteOffset ?? 0;
318
+ const png = buffer.subarray(start, start + view.byteLength);
319
+ const data = new DataView(png.buffer, png.byteOffset, png.byteLength);
320
+ for (let offset = 8; offset + 12 <= png.length;) {
321
+ const length = data.getUint32(offset), end = offset + length + 12;
322
+ if (end > png.length)
323
+ break;
324
+ if (data.getUint32(offset + 4) === 0x49454e44 && length === 0) {
325
+ if (png.length - end <= 3 && png.subarray(end).every((byte) => byte === 0))
326
+ view.byteLength = end;
327
+ break;
328
+ }
329
+ offset = end;
330
+ }
331
+ }
202
332
  // Three names texture/sampler bindings; glTF Transform models textures by
203
333
  // image. Carry the first named binding onto an unnamed shared image so the
204
334
  // conversion retains a useful authored resource name without duplicating it.
@@ -209,7 +339,13 @@ export async function communitySceneDocument(root, clips, platform = browserPlat
209
339
  if (image && !image.name)
210
340
  image.name = binding.name;
211
341
  }
212
- return io.readJSON(json);
342
+ const document = await io.readJSON(json);
343
+ if (atlas.table) {
344
+ const scene = document.getRoot().getDefaultScene() ?? document.getRoot().listScenes()[0];
345
+ scene.setExtras({ ...scene.getExtras(), kilnAtlasProvenance: atlas.table });
346
+ await refreshAtlasDocumentProvenance(document);
347
+ }
348
+ return document;
213
349
  }
214
350
  /** Encode the effective UV matrix in glTF's T*R*S order, including manual
215
351
  * matrices and Three's center. A sheared UV basis needs baking, not TRS. */
@@ -951,6 +951,64 @@ define('createJointChain', {
951
951
  aliases: ['joint chain', 'articulated chain'],
952
952
  intents: ['create a body-plan-neutral articulated chain'],
953
953
  });
954
+ define('createBoneChain', {
955
+ ...nodeFacts,
956
+ axes: 'Explicit local forward/bend axes and rest transforms; body-plan neutral.',
957
+ origin: 'Offsets are local to the preceding bone; first offset is under the supplied parent.',
958
+ parameters: [
959
+ '1..1024 descriptors; unique roles and sanitized node names across the parent hierarchy. The first parentRole is inferred from a declared parent joint; an explicit value must agree.',
960
+ ],
961
+ preservation: [
962
+ 'Creates real Bones with the same versioned semantic descriptors as rigid createJointChain; leaves existing nodes untouched.',
963
+ ],
964
+ semantics: ['Preserves declared roles, local rest frames, axes, contacts and end-effectors.'],
965
+ cost: 'Linear in descriptors and existing parent hierarchy; validates before adding the chain.',
966
+ }, {
967
+ kind: 'assembly',
968
+ references: ['src/character.ts'],
969
+ tags: ['rig', 'bones', 'skinning'],
970
+ aliases: ['bone chain', 'semantic bones'],
971
+ intents: ['create bones for a skinned custom body plan'],
972
+ related: [
973
+ { name: 'createJointChain', relation: 'alternative' },
974
+ { name: 'createSkinnedPart', relation: 'companion' },
975
+ ],
976
+ limitations: [
977
+ 'Does not bind geometry, create animations, choose a body plan or generate anatomical weights.',
978
+ ],
979
+ });
980
+ define('createSkinnedPart', {
981
+ ...nodeFacts,
982
+ ownership: 'Owns cloned geometry, influence arrays and inverse bind matrices. Materials and caller-owned bones are shared; source geometry stays unchanged.',
983
+ coordinates: 'Geometry is mesh-local; supplied mesh transforms are local to the required parent. All bones must share its existing hierarchy and be in the intended rest pose.',
984
+ axes: 'No forced character forward axis. Local rotation options are Euler XYZ degrees, matching createPart.',
985
+ origin: 'The supplied geometry origin is unchanged.',
986
+ topology: [
987
+ 'Creates a skinned mesh on cloned unchanged topology; no automatic solid repair or watertightness claim.',
988
+ ],
989
+ parameters: [
990
+ '1..1024 distinct named Bones; 1..262144 xyz vertices; 128 MiB total cloned attribute storage plus new influences.',
991
+ 'Exactly four valid indices and finite nonnegative weights per vertex. Default requires each weight <=1 and sum within 1e-5 of one; normalizeWeights:true explicitly normalizes positive finite sums. Zero sums never receive invented weights.',
992
+ 'Existing skin attributes, singular/nonfinite bind transforms and ambiguous bone names are rejected.',
993
+ ],
994
+ preservation: [
995
+ 'Unchanged topology retains UV/color/morph attributes and material groups on an owned clone. Skeleton order matches the supplied bone array.',
996
+ ],
997
+ semantics: ['Optional semantic metadata is stamped on the returned Mesh_name.'],
998
+ cost: 'O(vertices + attributes + bones + parent hierarchy); no automatic weighting or anatomical fit computation.',
999
+ }, {
1000
+ kind: 'assembly',
1001
+ references: ['src/skin-binding.ts'],
1002
+ tags: ['rig', 'skinning', 'weights', 'binding'],
1003
+ aliases: ['skin binding', 'weighted mesh'],
1004
+ intents: ['bind explicit vertex influences without mutating shared geometry'],
1005
+ related: [{ name: 'createBoneChain', relation: 'prerequisite' }],
1006
+ limitations: [
1007
+ 'Requires the explicit Three exporter for skin delivery; legacy defaults remain unchanged.',
1008
+ 'Four influences are this helper contract, not a new global restriction on direct Three.js authoring.',
1009
+ 'Bind only after rest transforms are assembled. Reparenting or editing rest geometry afterward needs deliberate rebinding; runtime posed bounds remain the consumer responsibility.',
1010
+ ],
1011
+ });
954
1012
  define('createVehicleFrame', {
955
1013
  ...nodeFacts,
956
1014
  axes: '+X forward, +Y up, +Z right.',
@@ -1151,6 +1209,33 @@ define('compilePortableMaterialSpecV2', {
1151
1209
  aliases: ['portable material specification', 'project material library'],
1152
1210
  intents: ['compile a strict portable material definition'],
1153
1211
  });
1212
+ define('compilePortableMaterialSpecV3', {
1213
+ ...materialFacts,
1214
+ execution: 'async',
1215
+ ownership: 'Creates an owned material and owned texture, Source, pixel and encoded-byte bindings; never mutates a cached resource texture.',
1216
+ parameters: [
1217
+ 'Strict schemaVersion 3 pbrMetallicRoughness specification, with existing scalar PBR fields and exact slot usage checks.',
1218
+ 'Each texture ref is a typed procedural V3 spec or approved resource ID plus a complete sampler: minFilter, magFilter, wrapS and wrapT.',
1219
+ 'Procedural dimensions are independent width/height; colors are straight RGBA bytes. The material selects opaque, mask or blend behavior explicitly.',
1220
+ 'Source accepts exactly one specification. Host resources are injected after verification; authored source cannot provide bytes, paths, URLs or a resolver.',
1221
+ ],
1222
+ preservation: [
1223
+ 'Existing V1/V2 recipe algorithms, hashes and material defaults are unchanged.',
1224
+ 'Sampler and alpha state export through standard glTF material and sampler fields; runtime glow, lighting and postprocessing remain consumer choices.',
1225
+ ],
1226
+ }, {
1227
+ ...materialMetadata,
1228
+ references: [
1229
+ 'src/procedural-material-v3.ts',
1230
+ 'src/portable-material-runtime.ts',
1231
+ 'src/portable-sampler.ts',
1232
+ ],
1233
+ aliases: ['portable RGBA material', 'explicit texture sampling'],
1234
+ intents: ['compile cutout or pixel-textured portable materials'],
1235
+ limitations: [
1236
+ 'Height-map derivation is not part of the V3 binding contract. V2 remains available for existing derive recipes.',
1237
+ ],
1238
+ });
1154
1239
  const animationFacts = {
1155
1240
  units: 'Keyframe times and durations are seconds; transform values are absolute, not additive offsets.',
1156
1241
  axes: 'Target-local XYZ axes.',
@@ -1653,13 +1738,16 @@ define('autoUnwrap', {
1653
1738
  execution: 'async',
1654
1739
  axes: 'Chart orientation is selected by xatlas, not a directional material frame.',
1655
1740
  parameters: [
1656
- 'Nonempty complete triangle geometry; resolution is a packing limit, not guaranteed final dimensions.',
1657
- 'padding defaults 2; useNormals defaults false.',
1741
+ 'Nonempty complete triangle geometry; at most 65,536 source and output vertices, 1,000,000 triangles and a 128 MiB attribute allocation bound. Split larger meshes.',
1742
+ 'resolution is an integer 1..4096 target, not guaranteed final dimensions; read userData.atlas.width/height. padding is an integer 0..resolution, default 2.',
1743
+ 'useNormals defaults false (recompute); true uses and preserves source normals. Normal morphs require true and a base normal attribute.',
1658
1744
  ],
1659
1745
  topology: ['Reindexes/splits vertices for charts; does not repair malformed source topology.'],
1660
1746
  preservation: [
1661
- 'Returns new positions/index/UVs and atlas metadata; normals depend on input/options.',
1662
- 'Arbitrary custom attributes, groups, materials and original UV orientation are not preserved by this reconstruction.',
1747
+ 'Returns owned buffers; source remains unchanged. Seam mapping retains per-vertex attributes with 1..4 components, including colors, skin weights/indices, extra UVs and normalized/interleaved storage values.',
1748
+ 'Preserves position/normal/color morph targets, relative-mode flag, triangle groups, draw range, name and JSON userData. Normal policy stays opt-in; zero normals are repaired.',
1749
+ 'Replaces UV0 and atlas metadata; drops stale tangents. Recompute tangents after final UVs. Mesh materials, skeleton and animation belong to the caller, not the geometry helper.',
1750
+ 'Rejects instanced data, unsupported morph semantics, multiple atlas pages, invalid correspondence and adapter index overflow instead of silently discarding data.',
1663
1751
  ],
1664
1752
  cost: 'WASM charting/packing depends on mesh complexity and resolution; no general time bound.',
1665
1753
  }, {
@@ -1705,16 +1793,22 @@ define('proceduralTexture', {
1705
1793
  ...textureFacts,
1706
1794
  ownership: 'Creates a new DataTexture with generated bytes and portable metadata.',
1707
1795
  parameters: [
1708
- 'Strict schemaVersion 2; size power-of-two 4..1024; at most 8 layers.',
1796
+ 'V1/V2 preserve opaque patterns: size power-of-two 4..1024 and at most 8 layers. Explicit schemaVersion 3 uses width/height integers 4..1024, byte RGBA, at most 64 ordered operations and 64 polygon vertices.',
1797
+ 'V3 paint/mask use integer pixel-edge shapes with binary center coverage and fixed integer source-over rounding. A mask clears outside its shape; invert keeps outside. Zero-alpha pixels are transparent black.',
1798
+ 'V3 color-only palette colors are 1..32 RGB integers, optional fixed Bayer4 dither. Color-only stamps use bounded Xorshift32 seed/area placement; periodic wrapping is explicit and at most 256 expanded copies. Legacy-v2 layers delegate unchanged square V2 patterns with matching usage.',
1709
1799
  'Pattern counts 1..256 and noise octaves 1..6; seeded patterns are deterministic.',
1710
1800
  'For stripes and gradient, angleDeg rotates the direction of color variation in UV space about the image center. At 0 degrees colors vary across U: stripe bands run along V. At 90 degrees colors vary across V: bands run along U. This does not rotate the mesh UVs.',
1711
1801
  'Stripes count is the number of alternating bands across one unit of the projected coordinate, not the number of colorA/colorB pairs. Gradient runs from its from color to its to color along that coordinate and clamps outside 0..1.',
1712
1802
  'Unknown keys, callbacks, paths, URLs and shader source are rejected.',
1713
1803
  ],
1714
1804
  preservation: ['Produces repeat-wrapped encoded texture content; color space follows usage.'],
1715
- cost: 'Bounded pixel/layer budgets; generation scales with pixels × layers × pattern work.',
1805
+ cost: 'V2 retains pixel/layer budgets. V3 preflights at most 1,048,576 texels, 64million work units including polygon edges, palette comparisons and periodic copies, and 64MiB transient bytes.',
1716
1806
  }, {
1717
- references: ['src/procedural-texture.ts'],
1807
+ references: [
1808
+ 'src/procedural-texture.ts',
1809
+ 'src/procedural-texture-v3-contract.ts',
1810
+ 'docs/procedural-surfaces-v3.md',
1811
+ ],
1718
1812
  tags: ['texture', 'procedural', 'deterministic'],
1719
1813
  aliases: ['generated texture', 'noise texture', 'layered pattern'],
1720
1814
  intents: ['generate a repeatable texture without external image files'],
@@ -1819,6 +1913,66 @@ define('remapUV', {
1819
1913
  { name: 'copyGeometry', relation: 'companion' },
1820
1914
  ],
1821
1915
  });
1916
+ define('textureAtlas', {
1917
+ ...textureFacts,
1918
+ execution: 'async',
1919
+ units: 'Integer image pixel edges; half-open content rectangles and gutters.',
1920
+ axes: 'Image +X right and +Y down, top-left origin. Texture flipY=false.',
1921
+ ownership: 'Creates an owned texture and immutable-by-copy source snapshots; host map capability is never exposed to authored code.',
1922
+ parameters: [
1923
+ 'schemaVersion1, width/height 4..1024, explicit usage and 1..64 named regions from at most16 distinct sources.',
1924
+ 'Each region has name, content rectangle and source; gutter defaults2 and edgeMode defaults copy-edge. Source crops and content dimensions must match without resampling.',
1925
+ 'Source is {kind:"procedural",spec:V3} or {kind:"resource",resourceId:exactMapId,pin:{resourceId:materialId,revisionId,sha256},expected?}. Pins are host-selected immutable library revisions; no arbitrary acquisition I/O.',
1926
+ 'Optional sampler specifies all four portable fields; default linear-mipmap-linear/linear/clamp-to-edge on both axes.',
1927
+ ],
1928
+ preservation: [
1929
+ 'Copies straight RGBA; mask-rgb-dilate changes only transparent RGB, leaving coverage unchanged. Sources and padded allocations stay separate.',
1930
+ ],
1931
+ semantics: [
1932
+ 'Named atlas metadata records compiler, recipe, layout, source, raw-pixel and domain-separated atlas hashes. It does not establish project membership.',
1933
+ ],
1934
+ cost: 'Bounded full-source decode, compilation, pixel work and scratch memory admitted together before source loads.',
1935
+ }, {
1936
+ references: [
1937
+ 'src/texture-atlas-contract.ts',
1938
+ 'src/texture-atlas-pixels.ts',
1939
+ 'src/texture-atlas.ts',
1940
+ ],
1941
+ tags: ['texture', 'atlas', 'rgba', 'ownership'],
1942
+ aliases: ['named texture sheet', 'foliage atlas'],
1943
+ intents: ['compose reusable named pixel regions without a mesh'],
1944
+ limitations: [
1945
+ 'No automatic packing, rotation or resampling. Gutters reduce filtering bleed but cannot isolate every downsampled mip level.',
1946
+ ],
1947
+ related: [{ name: 'remapUVToAtlas', relation: 'companion' }],
1948
+ });
1949
+ define('remapUVToAtlas', {
1950
+ ...ownedGeometry,
1951
+ units: 'Input unit UVs [0,1]; target rectangles are atlas pixels.',
1952
+ axes: 'Explicit top-left or bottom-left input origin; target follows top-left image coordinates.',
1953
+ parameters: [
1954
+ 'Requires a compiled atlas texture or manifest and exact region name.',
1955
+ 'inputOrigin and content-boundary/texel-center mode are both required; finite per-vertex UV0 only.',
1956
+ ],
1957
+ topology: ['Preserves vertices, indices, groups, draw range and morph correspondence.'],
1958
+ preservation: [
1959
+ 'Owns output attributes and metadata; UV0 is replaced. Nonidentity scale drops stale tangents with a warning.',
1960
+ ],
1961
+ semantics: [
1962
+ 'Records the named region and affine mapping without changing asset category, project membership or materials.',
1963
+ ],
1964
+ cost: 'Linear UV mapping plus a bounded clone of geometry and morph buffers.',
1965
+ }, {
1966
+ references: ['src/texture-atlas.ts', 'src/uv-shapes.ts'],
1967
+ tags: ['uv', 'atlas', 'ownership'],
1968
+ aliases: ['named region UV mapping'],
1969
+ intents: ['bind existing UVs to a named atlas region'],
1970
+ limitations: ['Does not unwrap, project, resample or tile within a region.'],
1971
+ related: [
1972
+ { name: 'textureAtlas', relation: 'prerequisite' },
1973
+ { name: 'projectUV', relation: 'companion' },
1974
+ ],
1975
+ });
1822
1976
  define('materialBudgetAdvisory', {
1823
1977
  units: 'Counts distinct material object references.',
1824
1978
  axes: 'Not applicable.',
@@ -56,6 +56,24 @@ const PRIMITIVES = [
56
56
  example: "const leg = createJointChain('LegL', [{ role: 'hip.left', offset: [0, 1, -0.2], side: 'left' }, { role: 'knee.left', offset: [0, -0.5, 0], side: 'left' }, { role: 'ankle.left', offset: [0, -0.5, 0], side: 'left', endEffector: true, contact: true }], { parent: root });",
57
57
  promptNotes: 'Use only the resolved body-plan graph. Offsets are local to the previous joint; contact end effectors must land at world Y=0.',
58
58
  },
59
+ {
60
+ name: 'createBoneChain',
61
+ signature: 'createBoneChain(name, segments: JointChainSegmentV1[], opts?: { parent?, parentRole? })',
62
+ returns: '{ root: Bone, end: Bone, nodes: Bone[], byRole, descriptors }',
63
+ category: 'structure',
64
+ description: 'Creates an opt-in semantic Bone chain with the same local rest descriptors as createJointChain, for explicitly skinned meshes.',
65
+ example: "const bones = createBoneChain('Arm', [{ role: 'arm.base', offset: [0, 0, 0] }, { role: 'arm.tip', offset: [0, 1, 0] }], { parent: root });",
66
+ promptNotes: 'The bones must be in their assembled rest pose before binding. Use unique roles and names across the rig. createJointChain still returns rigid pivots.',
67
+ },
68
+ {
69
+ name: 'createSkinnedPart',
70
+ signature: 'createSkinnedPart(name, geometry, material, opts: { parent, bones: Bone[], indices: ArrayLike<number>, weights: ArrayLike<number>, normalizeWeights?, position?, rotation?, scale?, semantic? })',
71
+ returns: 'THREE.SkinnedMesh',
72
+ category: 'structure',
73
+ description: 'Clones unchanged geometry and binds explicit four-lane vertex influences to an already assembled rest-pose bone hierarchy.',
74
+ example: "const skin = createSkinnedPart('Arm', geometry, material, { parent: root, bones: chain.nodes, indices, weights });",
75
+ promptNotes: 'Supply exactly four bone indices and weights per vertex; zero-weight lanes still need valid indices. Weights sum to one unless normalizeWeights:true is explicit; zero sums are rejected. Mesh transforms use local meters and Euler degrees. Three exporter is required for portable skin delivery; no automatic converter switch.',
76
+ },
59
77
  {
60
78
  name: 'createVehicleFrame',
61
79
  signature: 'createVehicleFrame(name, opts?: { chassis?, axles?, seats?, contacts?, steering?, propulsion?, parent? })',
@@ -445,6 +463,15 @@ const PRIMITIVES = [
445
463
  example: "const steel = await compilePortableMaterialSpecV2({ schemaVersion: 2, model: 'pbrMetallicRoughness', roughness: 0.45, metalness: 0.85, textures: { metallicRoughness: { kind: 'procedural', spec: { schemaVersion: 2, usage: 'metallicRoughness', size: 64, layers: [{ op: 'solid', color: 0x0080cc }] } } } });",
446
464
  promptNotes: 'Use textures: { baseColor: { kind: "resource", resourceId: "kiln.texture.rough-concrete-albedo.v1" } } for an approved resource, or { kind: "procedural", spec: { schemaVersion: 2, ... } }; bare ID strings are invalid. Use metallicRoughness as one packed G=roughness/B=metalness map. Every procedural ref usage must match its slot; resource refs must be approved for that exact slot. baseColor and emissive are numeric color integers from 0x000000 to 0xffffff, not CSS or materialRecipe hex strings. roughness, metalness and alphaCutoff are finite 0..1; emissiveIntensity is finite 0..64 (unlike materialRecipe, whose intensity is 0..1).',
447
465
  },
466
+ {
467
+ name: 'compilePortableMaterialSpecV3',
468
+ signature: "await compilePortableMaterialSpecV3({ schemaVersion: 3, model: 'pbrMetallicRoughness', name?, baseColor?, roughness?, metalness?, emissive?, emissiveIntensity?, alphaMode?, alphaCutoff?, doubleSided?, textures?: { baseColor?, normal?, metallicRoughness?, emissive?, occlusion? } })",
469
+ returns: 'Promise<THREE.MeshStandardMaterial>',
470
+ category: 'material',
471
+ description: 'Compiles opt-in portable PBR materials with bounded rectangular RGBA procedural textures and explicit portable samplers, preserving existing V2 behavior.',
472
+ example: "const leaf = await compilePortableMaterialSpecV3({ schemaVersion: 3, model: 'pbrMetallicRoughness', alphaMode: 'mask', doubleSided: true, textures: { baseColor: { kind: 'procedural', spec: { schemaVersion: 3, width: 16, height: 32, usage: 'albedo', background: [0, 0, 0, 0], operations: [{ op: 'paint', shape: { kind: 'ellipse', x: 2, y: 1, width: 12, height: 30 }, rgba: [60, 90, 40, 255] }] }, sampler: { minFilter: 'nearest', magFilter: 'nearest', wrapS: 'clamp-to-edge', wrapT: 'clamp-to-edge' } } } });",
473
+ promptNotes: 'Every texture ref needs all four sampler fields. minFilter accepts nearest, linear, nearest-mipmap-nearest, linear-mipmap-nearest, nearest-mipmap-linear or linear-mipmap-linear; magFilter accepts nearest or linear; wrapS/wrapT accept clamp-to-edge, repeat or mirrored-repeat. Use typed procedural V3 specs or approved resource refs. Slot usages must match. RGBA alpha alone does not select mask/blend: set material alphaMode explicitly. Each binding owns texture state and bytes; no resolver, path, URL, raw pixels or shader arguments. V2 recipes and defaults remain separate.',
474
+ },
448
475
  {
449
476
  name: 'basicMaterial',
450
477
  signature: 'basicMaterial(color, opts?: { transparent, opacity })',
@@ -711,7 +738,7 @@ const PRIMITIVES = [
711
738
  signature: 'await autoUnwrap(geometry: BufferGeometry, opts?: { resolution?: 1024, padding?: 2, useNormals?: false })',
712
739
  returns: 'Promise<THREE.BufferGeometry>',
713
740
  category: 'uv',
714
- description: 'xatlas-based UV atlas for ANY geometry (CSG output, subdivided, deformed). Output is a packed atlas with arbitrary per-chart rotation — use for non-tileable baked textures. For directional tileable textures on box/cylinder/plane primitives, prefer the shape-aware unwraps below.',
741
+ description: 'xatlas-based UV atlas for bounded triangle geometry, including CSG output. Remaps vertex data and morphs across seams; preserves groups. Replaces UV0 and removes stale tangents. Maximum 65,536 input and output vertices; split larger meshes. Packed charts rotate arbitrarily: use for baked textures; prefer projectUV or original primitive UVs for directional tiling. See the preservation contract before unwrapping skinned or morphed geometry.',
715
742
  example: 'const unwrapped = await autoUnwrap(someCsgResult, { resolution: 1024 });\nconst mesh = new THREE.Mesh(unwrapped, bakedPbr);',
716
743
  },
717
744
  // ---------------------------------------------------------------------------
@@ -728,12 +755,30 @@ const PRIMITIVES = [
728
755
  },
729
756
  {
730
757
  name: 'proceduralTexture',
731
- signature: 'proceduralTexture({ schemaVersion: 2, size?: 4..1024 pow2, usage?, name?, layers: ProceduralLayer[] })',
758
+ signature: 'proceduralTexture({ schemaVersion: 2, size?: 4..1024 pow2, usage?, name?, layers: ProceduralLayer[] } | { schemaVersion: 3, width: 4..1024, height: 4..1024, usage?, name?, background?: RGBA, operations: OperationV3[], palette? })',
732
759
  returns: 'THREE.DataTexture (tiling, sRGB or linear per usage)',
733
760
  category: 'textures',
734
- description: 'Builds a repeat-wrapped texture from a bounded layer stack, with no image file needed. Layers composite bottom-first; the same spec produces the same bytes. Seeded noise is tileable, while other patterns can have visible repeat seams. Baked to PNG and embedded in the GLB automatically.',
761
+ description: 'Builds a repeat-wrapped deterministic texture without an image file. V2 retains opaque pattern layers. Explicit V3 adds rectangular RGBA, pixel shapes/masks, color palettes and seeded stamps. Repeat wrapping alone does not prove seamless pixels. Baked to PNG and embedded in the GLB automatically.',
735
762
  example: "const bark = proceduralTexture({ schemaVersion: 2, size: 256, usage: 'albedo', name: 'Bark', layers: [{ op: 'solid', color: 0x5a4632 }, { op: 'noise', colorA: 0x3d2f21, colorB: 0x7a6248, scale: 6, octaves: 4, blend: 'overlay' }] });",
736
- promptNotes: "Layer fields by op — solid: { color }; checker: { colorA, colorB, squares? }; stripes: { colorA, colorB, count?, angleDeg? }; gradient: { from, to, angleDeg? }; bricks: { brick, mortar, rows?, cols?, mortarWidth?: 0..1 (fraction of a brick cell, default 0.06), stagger?: 0..1 (row offset fraction, default 0.5) }; noise: { colorA, colorB, scale?, octaves?, seed? }. Every layer also accepts blend?: 'normal'|'multiply'|'screen'|'overlay' and opacity?: 0..1. Pattern counts are integers 1..256; noise octaves are 1..6. Sync — no await. Unknown/prototype keys, callbacks, paths, URLs, and shader source are rejected. Max 8 layers.",
763
+ promptNotes: "V2 layer fields by op: solid: { color }; checker: { colorA, colorB, squares? }; stripes: { colorA, colorB, count?, angleDeg? }; gradient: { from, to, angleDeg? }; bricks: { brick, mortar, rows?, cols?, mortarWidth?: 0..1 (fraction of a brick cell, default 0.06), stagger?: 0..1 (row offset fraction, default 0.5) }; noise: { colorA, colorB, scale?, octaves?, seed? }. V2 blend normal|multiply|screen|overlay; opacity 0..1; max 8 layers, counts 1..256, octaves 1..6. V3 operations: paint {shape,rgba,opacity?}; mask {shape,alpha?,invert?}; stamps {shape,rgba,count,seed,area,periodic?,opacity?}; legacy-v2 {spec,alpha?,opacity?} delegates an explicit V2 recipe with matching square dimensions/usage. RGBA is four bytes; V3 opacity/alpha are bytes0..255 default255. Shape kinds: rectangle/ellipse {x,y,width,height}; polygon {points:[[x,y],...]}; line {from:[x,y],to:[x,y],width}. Coordinates are integer pixel edges; centers define binary coverage, round line caps. Mask clears outside; invert keeps outside instead. area {x,y,width,height} sets seeded stamp translation offsets; periodic:true wraps copies explicitly. V3 background defaults transparent black, max64 ops,64 polygon vertices,256 expanded stamps (periodic counts9),32 palette colors,64million estimated work including edges. palette {colors:[RGB integers],dither?:'none'|'bayer4'} and stamps are albedo/emissive only. Zero alpha normalizes RGB to0. Sampler defaults remain repeat/linear/mips; alphaMode belongs to pbrMaterial. Sync; strict JSON rejects unknown/prototype fields and accessors. See docs/procedural-surfaces-v3.md for exact rounding and bounds.",
764
+ },
765
+ {
766
+ name: 'textureAtlas',
767
+ signature: 'await textureAtlas({ schemaVersion: 1, width, height, usage, sampler?, regions: [{ name, content: { x, y, width, height }, gutter?, edgeMode?, source, crop? }] })',
768
+ returns: 'Promise<THREE.DataTexture>',
769
+ category: 'textures',
770
+ description: 'Composes named, disjoint pixel regions from bounded procedural V3 recipes or exact pinned library maps without creating a mesh.',
771
+ example: "const atlas = await textureAtlas({ schemaVersion: 1, width: 8, height: 8, usage: 'albedo', regions: [{ name: 'patch', content: { x: 2, y: 2, width: 4, height: 4 }, source: { kind: 'procedural', spec: { schemaVersion: 3, width: 4, height: 4, background: [70, 90, 40, 255], operations: [] } } }] });",
772
+ promptNotes: 'Coordinates are integer pixel edges from top left. Width/height 4..1024. Content excludes gutter (default2); padded allocations must not overlap. copy-edge (default) extrudes RGBA; mask-rgb-dilate preserves positive alpha and extends nearest donor RGB into zero-alpha pixels without growing alpha, for albedo/emissive only. No packing, rotation or resampling. An optional crop must exactly match content dimensions. Resource sources require {kind:"resource",resourceId:exactMapId,pin:{resourceId:materialId,revisionId,sha256}} and a host-supplied exact dependency; paths, URLs, latest and approved non-library resources are unsupported. Default sampler is linear-mipmap-linear/linear/clamp-to-edge on both axes; explicit samplers require all four fields. Gutters do not guarantee isolation at every mip level. Use remapUVToAtlas with an explicit input origin and mode; inspect at intended scene distance.',
773
+ },
774
+ {
775
+ name: 'remapUVToAtlas',
776
+ signature: "remapUVToAtlas(geometry, atlasTextureOrManifest, regionName, { inputOrigin: 'top-left' | 'bottom-left', mode: 'content-boundary' | 'texel-center' })",
777
+ returns: 'THREE.BufferGeometry',
778
+ category: 'uv',
779
+ description: 'Maps existing unit UV0 into a named atlas content rectangle on an owned geometry clone, preserving topology and rich attributes.',
780
+ example: "const card = remapUVToAtlas(planeGeo(1, 1), atlas, 'patch', { inputOrigin: 'bottom-left', mode: 'texel-center' });",
781
+ promptNotes: 'UV0 must be finite in [0,1], with two components per vertex. Content-boundary maps endpoints to content edges; texel-center maps endpoints to the first/last content texel centers. Explicitly identify the input UV origin; atlas image coordinates use top left and flipY=false. Geometry, normals, colors, skin weights, morph buffers, groups and source metadata are preserved independently. UV scale changes invalidate tangents; regenerate when needed. This helper does not project, unwrap, tile within a region or change material sampling.',
737
782
  },
738
783
  {
739
784
  name: 'normalMapFromHeight',
@@ -8,11 +8,12 @@
8
8
  * the shape a model reads is the shape the server enforces.
9
9
  */
10
10
  import { z } from 'zod';
11
- import { materialLibraryPayloadSchema, proceduralMaterialDraftSchema } from "../material-library.js";
11
+ import { materialLibraryPayloadAnySchema, proceduralMaterialDraftAnySchema, } from "../material-library.js";
12
12
  import { projectDraftSchema, projectPatchSchema } from "../projects.js";
13
13
  import { cameraShotInput, captureShapeInput } from "../tools/capture-input.js";
14
14
  const jsonSchema = (schema) => {
15
- const { $schema: _dialect, ...rest } = z.toJSONSchema(schema, { io: 'input' });
15
+ // Local JSON Schema refs preserve complete fields without repeating nested recipes.
16
+ const { $schema: _dialect, ...rest } = z.toJSONSchema(schema, { io: 'input', reused: 'ref' });
16
17
  return rest;
17
18
  };
18
19
  const shape = (name, tool, field, summary, schema, extra = {}) => ({
@@ -42,12 +43,24 @@ export const discoveryShapes = [
42
43
  intents: ['update a project brief, design, inventory or material pins'],
43
44
  references: ['docs/projects-and-live-review.md'],
44
45
  }),
45
- shape('material-draft', 'kiln_material', 'draft', 'The record kiln_material create-procedural takes: materialId, name, tileable, sources with license, and maps whose layered procedural specs (solid, checker, stripes, gradient, bricks, noise) bake into PNG maps; optional tags, physicalSizeMeters and parameters.', proceduralMaterialDraftSchema, {
46
- aliases: ['procedural material', 'material recipe draft', 'texture layers'],
47
- intents: ['bake a custom tileable material from layers'],
48
- references: ['docs/projects-and-live-review.md'],
46
+ shape('material-draft', 'kiln_material', 'draft', 'The record kiln_material create-procedural takes: a legacy layered draft or schemaVersion 2 with explicit map samplers and V3 RGBA or named atlas recipes. Each includes materialId, name, tileable, sources with license, and maps. Atlas resource sources pin exact saved revisions.', proceduralMaterialDraftAnySchema, {
47
+ aliases: [
48
+ 'procedural material',
49
+ 'material recipe draft',
50
+ 'texture layers',
51
+ 'saved atlas',
52
+ 'RGBA texture',
53
+ ],
54
+ intents: [
55
+ 'bake a custom tileable material from layers',
56
+ 'save a standalone texture or named atlas',
57
+ ],
58
+ limitations: [
59
+ 'Semantic recipe, canonical-form and aggregate budget checks apply beyond the field shapes. Exact pinned sources must already exist in the injected library. No file paths or source fetching.',
60
+ ],
61
+ references: ['docs/projects-and-live-review.md', 'docs/procedural-surfaces-v3.md'],
49
62
  }),
50
- shape('material-import', 'kiln_material', 'payload', 'The record kiln_material import takes: schemaVersion 1 and complete normalized material records, each a manifest plus base64 PNG files keyed by map name.', materialLibraryPayloadSchema, {
63
+ shape('material-import', 'kiln_material', 'payload', 'The record kiln_material import takes: schemaVersion 1 for legacy-only data or 2 for mixed/new records, each a manifest plus base64 PNG files keyed by map name. Supply complete exact dependency closures for atlas records.', materialLibraryPayloadAnySchema, {
51
64
  aliases: ['material import payload', 'material records'],
52
65
  intents: ['import a material library payload'],
53
66
  references: ['docs/projects-and-live-review.md'],
@@ -26,7 +26,7 @@
26
26
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
27
27
  * or with any plugin manifest, which is what keeps the literal honest.
28
28
  */
29
- export declare const ENGINE_VERSION = "1.2.2";
29
+ export declare const ENGINE_VERSION = "1.2.3";
30
30
  /**
31
31
  * Where this engine was loaded from, as a `file://` URL of the package root.
32
32
  *
@@ -26,7 +26,7 @@
26
26
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
27
27
  * or with any plugin manifest, which is what keeps the literal honest.
28
28
  */
29
- export const ENGINE_VERSION = '1.2.2';
29
+ export const ENGINE_VERSION = '1.2.3';
30
30
  /**
31
31
  * Where this engine was loaded from, as a `file://` URL of the package root.
32
32
  *
@@ -1,5 +1,5 @@
1
1
  /** Closed, engine-owned repair hints. Never serialize exception messages or stacks. */
2
- export type AuthoringDiagnostic = 'TUBE_RADIUS' | 'TAPER_CONE_AXIS' | 'SOLID_FLOAT32_COLLAPSE' | 'MESH_DATA_NONFINITE' | 'MATERIAL_ALPHA_MODE' | 'MATERIAL_COLOR_ARGUMENT' | 'MATERIAL_PACKED_CHANNELS' | 'MATERIAL_RECIPE_TEXTURE_BINDING' | 'PORTABLE_TEXTURE_REFERENCE' | 'PORTABLE_COLOR_ARGUMENT' | 'PART_NAME_ARGUMENT' | 'PART_GEOMETRY_ARGUMENT' | 'PART_MATERIAL_ARGUMENT' | 'REMOVED_HELPER' | 'UNBOUND_VARIABLE' | 'GEAR_RADII_ORDER' | 'ROUNDED_BOX_RADIUS' | 'PROCEDURAL_TEXTURE_UNKNOWN_KEY' | 'PROCEDURAL_TEXTURE_BLEND' | 'MATERIAL_FRACTION_RANGE' | 'PARAMETRIC_PERIODIC_ENDPOINT' | 'PROFILE_HOLES_UNSUPPORTED' | 'PROFILE_BEVEL_COLLAPSE' | 'PROFILE_CORRESPONDENCE_COLLAPSE' | 'UNINITIALIZED_BINDING' | 'BUILD_RESULT' | 'MATERIAL_RECIPE_OVERRIDE' | 'PROGRAM_TYPE_ERROR' | 'PROGRAM_RANGE_ERROR' | 'LOD_SET' | 'OPEN_SHELL';
2
+ export type AuthoringDiagnostic = 'TUBE_RADIUS' | 'TAPER_CONE_AXIS' | 'SOLID_FLOAT32_COLLAPSE' | 'MESH_DATA_NONFINITE' | 'MATERIAL_ALPHA_MODE' | 'MATERIAL_COLOR_ARGUMENT' | 'MATERIAL_PACKED_CHANNELS' | 'MATERIAL_RECIPE_TEXTURE_BINDING' | 'PORTABLE_TEXTURE_REFERENCE' | 'PORTABLE_COLOR_ARGUMENT' | 'PART_NAME_ARGUMENT' | 'PART_GEOMETRY_ARGUMENT' | 'PART_MATERIAL_ARGUMENT' | 'REMOVED_HELPER' | 'UNBOUND_VARIABLE' | 'GEAR_RADII_ORDER' | 'ROUNDED_BOX_RADIUS' | 'PROCEDURAL_TEXTURE_UNKNOWN_KEY' | 'PROCEDURAL_TEXTURE_BLEND' | 'PROCEDURAL_TEXTURE_V3_CONTRACT' | 'PORTABLE_MATERIAL_V3_CONTRACT' | 'TEXTURE_ATLAS_CONTRACT' | 'MATERIAL_FRACTION_RANGE' | 'PARAMETRIC_PERIODIC_ENDPOINT' | 'PROFILE_HOLES_UNSUPPORTED' | 'PROFILE_BEVEL_COLLAPSE' | 'PROFILE_CORRESPONDENCE_COLLAPSE' | 'UNINITIALIZED_BINDING' | 'BUILD_RESULT' | 'MATERIAL_RECIPE_OVERRIDE' | 'PROGRAM_TYPE_ERROR' | 'PROGRAM_RANGE_ERROR' | 'LOD_SET' | 'OPEN_SHELL' | 'BONE_CHAIN_ARGUMENT' | 'SKIN_BINDING_ARGUMENT' | 'EXPORT_MATERIAL_SIDE_UNSUPPORTED';
3
3
  export declare const UNBOUND_VARIABLE_ADVICE = "Check variable spelling and scope: generated code used an undeclared variable. Read the current source and check declarations before retrying. If it was meant to be a Kiln helper, call kiln_discover to confirm the exact name and signature; the sandbox exposes only those globals.";
4
4
  export declare const GEAR_RADII_ORDER_ADVICE = "gearGeo requires boreRadius < rootRadius < tipRadius; specify rootRadius when changing tipRadius. Omitted radii keep their absolute defaults.";
5
5
  export declare const ROUNDED_BOX_RADIUS_ADVICE = "roundedBoxGeo: radius must be less than half the smallest dimension. Reduce radius or increase the smallest dimension; equality is invalid.";
@@ -27,6 +27,18 @@ export const OPEN_SHELL_ADVICE = 'markOpenShell(part, reason) marks a part as in
27
27
  */
28
28
  export const EXECUTION_REJECTED_ADVICE = 'No message, stack or identifier crosses the sandbox: a thrown Error, a source-policy denial and any other failure are each reported only as this sentence. Run kiln_validate on the same source for the failing line and code; to read values, render the program and call kiln_inspect (listParts, measurements) instead of throwing. A TypeError, RangeError, unbound variable or build() mistake is named when it is the cause.';
29
29
  export function authoringDiagnosticAdvice(diagnostic) {
30
+ if (diagnostic === 'PORTABLE_MATERIAL_V3_CONTRACT')
31
+ return 'compilePortableMaterialSpecV3 requires schemaVersion 3, model pbrMetallicRoughness, typed V3 procedural or injected resource references, and complete minFilter/magFilter/wrapS/wrapT samplers. Match texture usage to its slot and split work exceeding the material budget. Resource bindings need a host with verified-byte admission. V2 remains available unchanged. Shader code, paths, raw textures and V3 height derivation are unsupported. Read kiln_discover ids ["compilePortableMaterialSpecV3"] for the exact contract.';
32
+ if (diagnostic === 'TEXTURE_ATLAS_CONTRACT')
33
+ return 'textureAtlas requires bounded named integer content rectangles with nonoverlapping gutter allocations, matching source crops and usage, and explicit portable sampling. Sources are V3 recipes or exact host-injected library map pins. Split atlases exceeding aggregate work or allocation budgets. remapUVToAtlas requires finite unit UV0, a named content region, inputOrigin and mapping mode; it preserves topology and does not unwrap or tile a region. Read kiln_discover ids ["textureAtlas", "remapUVToAtlas"] for the exact contracts.';
34
+ if (diagnostic === 'PROCEDURAL_TEXTURE_V3_CONTRACT')
35
+ return 'proceduralTexture schemaVersion 3 requires bounded JSON: width/height 4..1024, byte RGBA, integer pixel shapes and at most 64 operations. Palette/dither and seeded stamps support only albedo/emissive. Work includes polygon edges and periodic copies. Read kiln_discover ids ["proceduralTexture"] for the V3 contract; keep schemaVersion 2 for existing opaque patterns.';
36
+ if (diagnostic === 'BONE_CHAIN_ARGUMENT')
37
+ return 'createBoneChain requires 1..1024 valid local joint descriptors with unique roles and animation-safe names across the parent hierarchy. Use normalized perpendicular forward/bend axes and finite rest transforms; an explicit parentRole must match the declared parent. Read the retained source and kiln_discover ids ["createBoneChain"] for the exact contract.';
38
+ if (diagnostic === 'SKIN_BINDING_ARGUMENT')
39
+ return 'createSkinnedPart requires four valid bone indices and finite nonnegative weights per vertex, summing to one unless normalizeWeights:true is explicit. Zero sums are rejected. Assemble distinct named Bones and the parent in one rest-pose hierarchy before binding. Use unbound geometry within the documented budgets and finite invertible transforms. Read the retained source and kiln_discover ids ["createSkinnedPart"] for the exact contract; the Three exporter is required for skin delivery.';
40
+ if (diagnostic === 'EXPORT_MATERIAL_SIDE_UNSUPPORTED')
41
+ return 'EXPORT_MATERIAL_SIDE_UNSUPPORTED: A used material has BackSide, which glTF cannot encode directly. Author inward-facing triangles with FrontSide, or choose DoubleSide when both faces are intended. Neither adds missing geometry. Read the retained program with kiln_source query BackSide to locate the material; source names and exception text do not cross the evaluator boundary.';
30
42
  if (diagnostic === 'UNINITIALIZED_BINDING')
31
43
  return UNINITIALIZED_BINDING_ADVICE;
32
44
  if (diagnostic === 'LOD_SET')