@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
@@ -98,6 +98,32 @@ const PRIMITIVES: HelperSpec[] = [
98
98
  promptNotes:
99
99
  'Use only the resolved body-plan graph. Offsets are local to the previous joint; contact end effectors must land at world Y=0.',
100
100
  },
101
+ {
102
+ name: 'createBoneChain',
103
+ signature:
104
+ 'createBoneChain(name, segments: JointChainSegmentV1[], opts?: { parent?, parentRole? })',
105
+ returns: '{ root: Bone, end: Bone, nodes: Bone[], byRole, descriptors }',
106
+ category: 'structure',
107
+ description:
108
+ 'Creates an opt-in semantic Bone chain with the same local rest descriptors as createJointChain, for explicitly skinned meshes.',
109
+ example:
110
+ "const bones = createBoneChain('Arm', [{ role: 'arm.base', offset: [0, 0, 0] }, { role: 'arm.tip', offset: [0, 1, 0] }], { parent: root });",
111
+ promptNotes:
112
+ 'The bones must be in their assembled rest pose before binding. Use unique roles and names across the rig. createJointChain still returns rigid pivots.',
113
+ },
114
+ {
115
+ name: 'createSkinnedPart',
116
+ signature:
117
+ 'createSkinnedPart(name, geometry, material, opts: { parent, bones: Bone[], indices: ArrayLike<number>, weights: ArrayLike<number>, normalizeWeights?, position?, rotation?, scale?, semantic? })',
118
+ returns: 'THREE.SkinnedMesh',
119
+ category: 'structure',
120
+ description:
121
+ 'Clones unchanged geometry and binds explicit four-lane vertex influences to an already assembled rest-pose bone hierarchy.',
122
+ example:
123
+ "const skin = createSkinnedPart('Arm', geometry, material, { parent: root, bones: chain.nodes, indices, weights });",
124
+ promptNotes:
125
+ '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.',
126
+ },
101
127
  {
102
128
  name: 'createVehicleFrame',
103
129
  signature:
@@ -596,6 +622,19 @@ const PRIMITIVES: HelperSpec[] = [
596
622
  promptNotes:
597
623
  '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).',
598
624
  },
625
+ {
626
+ name: 'compilePortableMaterialSpecV3',
627
+ signature:
628
+ "await compilePortableMaterialSpecV3({ schemaVersion: 3, model: 'pbrMetallicRoughness', name?, baseColor?, roughness?, metalness?, emissive?, emissiveIntensity?, alphaMode?, alphaCutoff?, doubleSided?, textures?: { baseColor?, normal?, metallicRoughness?, emissive?, occlusion? } })",
629
+ returns: 'Promise<THREE.MeshStandardMaterial>',
630
+ category: 'material',
631
+ description:
632
+ 'Compiles opt-in portable PBR materials with bounded rectangular RGBA procedural textures and explicit portable samplers, preserving existing V2 behavior.',
633
+ example:
634
+ "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' } } } });",
635
+ promptNotes:
636
+ '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.',
637
+ },
599
638
  {
600
639
  name: 'basicMaterial',
601
640
  signature: 'basicMaterial(color, opts?: { transparent, opacity })',
@@ -937,7 +976,7 @@ const PRIMITIVES: HelperSpec[] = [
937
976
  returns: 'Promise<THREE.BufferGeometry>',
938
977
  category: 'uv',
939
978
  description:
940
- '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.',
979
+ '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.',
941
980
  example:
942
981
  'const unwrapped = await autoUnwrap(someCsgResult, { resolution: 1024 });\nconst mesh = new THREE.Mesh(unwrapped, bakedPbr);',
943
982
  },
@@ -959,15 +998,41 @@ const PRIMITIVES: HelperSpec[] = [
959
998
  {
960
999
  name: 'proceduralTexture',
961
1000
  signature:
962
- 'proceduralTexture({ schemaVersion: 2, size?: 4..1024 pow2, usage?, name?, layers: ProceduralLayer[] })',
1001
+ '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? })',
963
1002
  returns: 'THREE.DataTexture (tiling, sRGB or linear per usage)',
964
1003
  category: 'textures',
965
1004
  description:
966
- '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.',
1005
+ '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.',
967
1006
  example:
968
1007
  "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' }] });",
969
1008
  promptNotes:
970
- "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.",
1009
+ "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.",
1010
+ },
1011
+ {
1012
+ name: 'textureAtlas',
1013
+ signature:
1014
+ 'await textureAtlas({ schemaVersion: 1, width, height, usage, sampler?, regions: [{ name, content: { x, y, width, height }, gutter?, edgeMode?, source, crop? }] })',
1015
+ returns: 'Promise<THREE.DataTexture>',
1016
+ category: 'textures',
1017
+ description:
1018
+ 'Composes named, disjoint pixel regions from bounded procedural V3 recipes or exact pinned library maps without creating a mesh.',
1019
+ example:
1020
+ "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: [] } } }] });",
1021
+ promptNotes:
1022
+ '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.',
1023
+ },
1024
+ {
1025
+ name: 'remapUVToAtlas',
1026
+ signature:
1027
+ "remapUVToAtlas(geometry, atlasTextureOrManifest, regionName, { inputOrigin: 'top-left' | 'bottom-left', mode: 'content-boundary' | 'texel-center' })",
1028
+ returns: 'THREE.BufferGeometry',
1029
+ category: 'uv',
1030
+ description:
1031
+ 'Maps existing unit UV0 into a named atlas content rectangle on an owned geometry clone, preserving topology and rich attributes.',
1032
+ example:
1033
+ "const card = remapUVToAtlas(planeGeo(1, 1), atlas, 'patch', { inputOrigin: 'bottom-left', mode: 'texel-center' });",
1034
+ promptNotes:
1035
+ '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.',
971
1036
  },
972
1037
  {
973
1038
  name: 'normalMapFromHeight',
@@ -8,7 +8,10 @@
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';
11
+ import {
12
+ materialLibraryPayloadAnySchema,
13
+ proceduralMaterialDraftAnySchema,
14
+ } from '../material-library';
12
15
  import { projectDraftSchema, projectPatchSchema } from '../projects';
13
16
  import { cameraShotInput, captureShapeInput } from '../tools/capture-input';
14
17
  import type { DiscoveryEntry } from './catalog-schema';
@@ -16,7 +19,8 @@ import type { DiscoveryEntry } from './catalog-schema';
16
19
  type ShapeEntry = Extract<DiscoveryEntry, { kind: 'shape' }>;
17
20
 
18
21
  const jsonSchema = (schema: z.ZodType): Record<string, unknown> => {
19
- const { $schema: _dialect, ...rest } = z.toJSONSchema(schema, { io: 'input' });
22
+ // Local JSON Schema refs preserve complete fields without repeating nested recipes.
23
+ const { $schema: _dialect, ...rest } = z.toJSONSchema(schema, { io: 'input', reused: 'ref' });
20
24
  return rest;
21
25
  };
22
26
 
@@ -73,20 +77,32 @@ export const discoveryShapes: ShapeEntry[] = [
73
77
  'material-draft',
74
78
  'kiln_material',
75
79
  'draft',
76
- '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.',
77
- proceduralMaterialDraftSchema,
80
+ '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.',
81
+ proceduralMaterialDraftAnySchema,
78
82
  {
79
- aliases: ['procedural material', 'material recipe draft', 'texture layers'],
80
- intents: ['bake a custom tileable material from layers'],
81
- references: ['docs/projects-and-live-review.md'],
83
+ aliases: [
84
+ 'procedural material',
85
+ 'material recipe draft',
86
+ 'texture layers',
87
+ 'saved atlas',
88
+ 'RGBA texture',
89
+ ],
90
+ intents: [
91
+ 'bake a custom tileable material from layers',
92
+ 'save a standalone texture or named atlas',
93
+ ],
94
+ limitations: [
95
+ '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.',
96
+ ],
97
+ references: ['docs/projects-and-live-review.md', 'docs/procedural-surfaces-v3.md'],
82
98
  },
83
99
  ),
84
100
  shape(
85
101
  'material-import',
86
102
  'kiln_material',
87
103
  'payload',
88
- 'The record kiln_material import takes: schemaVersion 1 and complete normalized material records, each a manifest plus base64 PNG files keyed by map name.',
89
- materialLibraryPayloadSchema,
104
+ '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.',
105
+ materialLibraryPayloadAnySchema,
90
106
  {
91
107
  aliases: ['material import payload', 'material records'],
92
108
  intents: ['import a material library payload'],
@@ -27,7 +27,7 @@
27
27
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
28
28
  * or with any plugin manifest, which is what keeps the literal honest.
29
29
  */
30
- export const ENGINE_VERSION = '1.2.2';
30
+ export const ENGINE_VERSION = '1.2.3';
31
31
 
32
32
  /**
33
33
  * Where this engine was loaded from, as a `file://` URL of the package root.
@@ -19,6 +19,9 @@ export type AuthoringDiagnostic =
19
19
  | 'ROUNDED_BOX_RADIUS'
20
20
  | 'PROCEDURAL_TEXTURE_UNKNOWN_KEY'
21
21
  | 'PROCEDURAL_TEXTURE_BLEND'
22
+ | 'PROCEDURAL_TEXTURE_V3_CONTRACT'
23
+ | 'PORTABLE_MATERIAL_V3_CONTRACT'
24
+ | 'TEXTURE_ATLAS_CONTRACT'
22
25
  | 'MATERIAL_FRACTION_RANGE'
23
26
  | 'PARAMETRIC_PERIODIC_ENDPOINT'
24
27
  | 'PROFILE_HOLES_UNSUPPORTED'
@@ -30,7 +33,10 @@ export type AuthoringDiagnostic =
30
33
  | 'PROGRAM_TYPE_ERROR'
31
34
  | 'PROGRAM_RANGE_ERROR'
32
35
  | 'LOD_SET'
33
- | 'OPEN_SHELL';
36
+ | 'OPEN_SHELL'
37
+ | 'BONE_CHAIN_ARGUMENT'
38
+ | 'SKIN_BINDING_ARGUMENT'
39
+ | 'EXPORT_MATERIAL_SIDE_UNSUPPORTED';
34
40
  // Names no identifier on purpose. The identifier is only available from the
35
41
  // sandboxed exception message, and this module's contract is that no captured
36
42
  // identifier, path, message or stack crosses that boundary. Pointing at the
@@ -76,6 +82,18 @@ export const OPEN_SHELL_ADVICE =
76
82
  export const EXECUTION_REJECTED_ADVICE =
77
83
  '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.';
78
84
  export function authoringDiagnosticAdvice(diagnostic: AuthoringDiagnostic | undefined): string {
85
+ if (diagnostic === 'PORTABLE_MATERIAL_V3_CONTRACT')
86
+ 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.';
87
+ if (diagnostic === 'TEXTURE_ATLAS_CONTRACT')
88
+ 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.';
89
+ if (diagnostic === 'PROCEDURAL_TEXTURE_V3_CONTRACT')
90
+ 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.';
91
+ if (diagnostic === 'BONE_CHAIN_ARGUMENT')
92
+ 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.';
93
+ if (diagnostic === 'SKIN_BINDING_ARGUMENT')
94
+ 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.';
95
+ if (diagnostic === 'EXPORT_MATERIAL_SIDE_UNSUPPORTED')
96
+ 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.';
79
97
  if (diagnostic === 'UNINITIALIZED_BINDING') return UNINITIALIZED_BINDING_ADVICE;
80
98
  if (diagnostic === 'LOD_SET') return LOD_SET_ADVICE;
81
99
  if (diagnostic === 'OPEN_SHELL') return OPEN_SHELL_ADVICE;
@@ -17,10 +17,10 @@ import { validateRequirementsQaReport } from '../qa/requirements-report';
17
17
  import type { RenderGlbOptions, RenderResult } from '../render';
18
18
  import type { KhronosGltfValidationReport } from '../qa/gltf';
19
19
  import {
20
- validateMaterialLibraryPayload,
21
- validateMaterialManifestIdentity,
20
+ validateMaterialLibraryPayloadAny,
21
+ validateMaterialManifestIdentityAny,
22
22
  } from '../material-library-node';
23
- import type { MaterialLibraryPayloadV1 } from '../material-library';
23
+ import { canonicalMaterialJson, type MaterialLibraryPayload } from '../material-library';
24
24
  import { rebuildOptionsSchema } from '../rebuild-options';
25
25
 
26
26
  export const EVALUATOR_REQUEST_VERSION = 'kiln.evaluator.request.v2' as const;
@@ -287,7 +287,7 @@ function parseOptions(
287
287
  }
288
288
  const options: Omit<RenderGlbOptions, 'textureResolver' | 'diagnosticConsole'> = {};
289
289
  if (value.materialResources !== undefined)
290
- options.materialResources = validateMaterialLibraryPayload(value.materialResources);
290
+ options.materialResources = validateMaterialLibraryPayloadAny(value.materialResources);
291
291
  if (value.gltfExporter !== undefined) {
292
292
  if (value.gltfExporter !== 'legacy' && value.gltfExporter !== 'three') fail('request');
293
293
  options.gltfExporter = value.gltfExporter;
@@ -437,6 +437,9 @@ export function decodeEvaluatorResultV2(
437
437
  value.error.diagnostic !== 'ROUNDED_BOX_RADIUS' &&
438
438
  value.error.diagnostic !== 'PROCEDURAL_TEXTURE_UNKNOWN_KEY' &&
439
439
  value.error.diagnostic !== 'PROCEDURAL_TEXTURE_BLEND' &&
440
+ value.error.diagnostic !== 'PROCEDURAL_TEXTURE_V3_CONTRACT' &&
441
+ value.error.diagnostic !== 'PORTABLE_MATERIAL_V3_CONTRACT' &&
442
+ value.error.diagnostic !== 'TEXTURE_ATLAS_CONTRACT' &&
440
443
  value.error.diagnostic !== 'MATERIAL_FRACTION_RANGE' &&
441
444
  value.error.diagnostic !== 'MATERIAL_RECIPE_TEXTURE_BINDING' &&
442
445
  value.error.diagnostic !== 'PORTABLE_TEXTURE_REFERENCE' &&
@@ -462,7 +465,10 @@ export function decodeEvaluatorResultV2(
462
465
  value.error.diagnostic !== 'PROGRAM_TYPE_ERROR' &&
463
466
  value.error.diagnostic !== 'PROGRAM_RANGE_ERROR' &&
464
467
  value.error.diagnostic !== 'LOD_SET' &&
465
- value.error.diagnostic !== 'OPEN_SHELL'))
468
+ value.error.diagnostic !== 'OPEN_SHELL' &&
469
+ value.error.diagnostic !== 'EXPORT_MATERIAL_SIDE_UNSUPPORTED' &&
470
+ value.error.diagnostic !== 'BONE_CHAIN_ARGUMENT' &&
471
+ value.error.diagnostic !== 'SKIN_BINDING_ARGUMENT'))
466
472
  )
467
473
  return fail('result');
468
474
  if (value.error.qa !== undefined) {
@@ -524,7 +530,7 @@ export function decodeEvaluatorResultV2(
524
530
  fail('result');
525
531
  try {
526
532
  for (const manifest of value.render.materialLibraryDependencies)
527
- validateMaterialManifestIdentity(manifest);
533
+ validateMaterialManifestIdentityAny(manifest);
528
534
  } catch {
529
535
  return fail('result');
530
536
  }
@@ -657,13 +663,17 @@ export function createEvaluatorPortV2(
657
663
  /** All transports bind decoded acceptance evidence to the same current-host snapshot. */
658
664
  export function assertEvaluatorResultMaterialResources(
659
665
  result: EvaluatorResultV2,
660
- expected?: MaterialLibraryPayloadV1,
666
+ expected?: MaterialLibraryPayload,
661
667
  ): void {
662
668
  if (!result.ok) return;
663
669
  const actual = result.render.materialLibraryDependencies ?? [];
664
- const wanted = expected?.records.map((record) => record.manifest.revisionId).sort() ?? [];
670
+ const wanted = expected?.records.map((record) => record.manifest) ?? [];
671
+ const identity = (manifest: (typeof actual)[number]) =>
672
+ `${manifest.materialId}/${manifest.revisionId}`;
665
673
  if (
666
- JSON.stringify(actual.map((manifest) => manifest.revisionId).sort()) !== JSON.stringify(wanted)
674
+ new Set(actual.map(identity)).size !== actual.length ||
675
+ JSON.stringify(actual.map(canonicalMaterialJson).sort()) !==
676
+ JSON.stringify(wanted.map(canonicalMaterialJson).sort())
667
677
  )
668
678
  throw new EvaluatorPortError('PROTOCOL_ERROR');
669
679
  }
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
8
8
  import type { RenderGlbOptions, RenderResult } from '../render';
9
9
  import { AssetQaBlockedError } from '../qa/run';
10
10
  import { assertNoLegacyRuntimePolicy, resolveRequirementsContext } from '../requirements-context';
11
- import { validateMaterialLibraryPayload } from '../material-library-node';
11
+ import { validateMaterialLibraryPayloadAny } from '../material-library-node';
12
12
  import {
13
13
  decodeEvaluatorResultV2,
14
14
  assertEvaluatorResultRequirements,
@@ -117,7 +117,7 @@ export async function renderGLBViaProcessLaunch(
117
117
  96 * 1024 * 1024,
118
118
  );
119
119
  if (options.materialResources !== undefined)
120
- validateMaterialLibraryPayload(options.materialResources);
120
+ validateMaterialLibraryPayloadAny(options.materialResources);
121
121
  const request: EvaluatorRequestV2 = {
122
122
  version: EVALUATOR_REQUEST_VERSION,
123
123
  requestId: 'render-1',
@@ -19,8 +19,19 @@ export async function prepareNodeExport(hasTextures: boolean): Promise<void> {
19
19
  const host = globalThis as unknown as Record<string, unknown>;
20
20
  if (!host['FileReader']) host['FileReader'] = ExportFileReader;
21
21
  if (!hasTextures || host['OffscreenCanvas'] || host['document']) return;
22
- const { Canvas, ImageData } = await import('@napi-rs/canvas');
22
+ const { Canvas, ImageData } = await import('@napi-rs/canvas').catch((cause: unknown) => {
23
+ throw new Error(
24
+ 'Headless textured export requires @napi-rs/canvas. Reinstall Kiln with optional dependencies enabled, then verify the native binding in the same Node runtime and platform used by the Kiln host.',
25
+ { cause },
26
+ );
27
+ });
23
28
  class ExportCanvas extends Canvas {
29
+ constructor(width: number, height: number) {
30
+ super(width, height);
31
+ // Native Canvas.data() is a method, not a DataTexture image byte array.
32
+ // Shadow it on the instance: the native constructor supplies its own prototype.
33
+ Object.defineProperty(this, 'data', { value: undefined });
34
+ }
24
35
  override async convertToBlob(
25
36
  options: { type?: string; mime?: string; quality?: number } = {},
26
37
  ): Promise<Blob> {
@@ -1289,7 +1289,7 @@
1289
1289
  },
1290
1290
  {
1291
1291
  "name": "kiln_material",
1292
- "description": "Manage optional immutable material resources in this workspace. presets discovers shipped architecture, wood, metal, fabric and ground recipes; create-preset bakes one with an explicit seed, creator and license; list returns compact material/revision summaries; get returns full provenance, hashes, map conventions, physical repeat scale and a code-ready portable material spec; create-procedural bakes bounded editable layer recipes including optional height-derived normals; import accepts complete normalized records with embedded PNG bytes. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before authored evaluation resolves the resources; no project is required.",
1292
+ "description": "Manage optional immutable material resources in this workspace. presets lists shipped recipes; create-preset uses explicit seed, creator and license; list returns revision summaries; get returns provenance, hashes, sampling and a portable material spec. create-procedural accepts legacy layer drafts or schemaVersion 2 drafts with explicit samplers and bounded RGBA or named atlas recipes. Atlas resource sources require exact saved revisions; import requires complete normalized records and dependencies with embedded PNG bytes. Read shape:material-draft or shape:material-import through kiln_discover for full fields. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before evaluation; no project or mesh is required.",
1293
1293
  "inputSchema": {
1294
1294
  "type": "object",
1295
1295
  "properties": {
@@ -1386,7 +1386,7 @@
1386
1386
  "pattern": "^sha256:[a-f0-9]{64}$"
1387
1387
  },
1388
1388
  "draft": {
1389
- "description": "create-procedural: materialId, name, tileable, sources, maps with layered procedural specs; optional tags, physicalSizeMeters, parameters. Shape: kiln_discover({ ids: ['shape:material-draft'] }).",
1389
+ "description": "create-procedural: legacy layered draft, or schemaVersion 2 with explicit samplers and RGBA/atlas recipes. Exact atlas sources must exist in this library. Shape: kiln_discover({ ids: ['shape:material-draft'] }).",
1390
1390
  "type": "object",
1391
1391
  "propertyNames": {
1392
1392
  "type": "string"
@@ -1394,7 +1394,7 @@
1394
1394
  "additionalProperties": {}
1395
1395
  },
1396
1396
  "payload": {
1397
- "description": "import: schemaVersion 1 and complete normalized records with embedded PNG bytes. Shape: kiln_discover({ ids: ['shape:material-import'] }).",
1397
+ "description": "import: schemaVersion 1 or 2, complete normalized records and their exact dependencies with embedded PNG bytes. Shape: kiln_discover({ ids: ['shape:material-import'] }).",
1398
1398
  "type": "object",
1399
1399
  "propertyNames": {
1400
1400
  "type": "string"
@@ -7,6 +7,14 @@ export const GEOMETRY_ALLOCATION_LIMITS = Object.freeze({
7
7
  repeatedMeshes: 10_000,
8
8
  primitiveSegments: 4096,
9
9
  primitiveGridVertices: 262_144,
10
+ // xatlasjs 0.2 returns Uint16 indices and source-vertex correspondence.
11
+ unwrapVertices: 65_536,
12
+ unwrapTriangles: 1_000_000,
13
+ unwrapAttributeBytes: 128 * 1024 * 1024,
14
+ unwrapResolution: 4096,
15
+ skinBones: 1024,
16
+ skinVertices: 262_144,
17
+ skinAttributeBytes: 128 * 1024 * 1024,
10
18
  });
11
19
  export function assertDimension(name: string, value: number, allowZero = false): void {
12
20
  if (
@@ -0,0 +1,60 @@
1
+ import * as THREE from 'three';
2
+ import type { TextureInfo } from '@gltf-transform/core';
3
+ import type { Transform } from '@gltf-transform/extensions';
4
+
5
+ const MIN: Record<number, THREE.MinificationTextureFilter> = {
6
+ 9728: THREE.NearestFilter,
7
+ 9729: THREE.LinearFilter,
8
+ 9984: THREE.NearestMipmapNearestFilter,
9
+ 9985: THREE.LinearMipmapNearestFilter,
10
+ 9986: THREE.NearestMipmapLinearFilter,
11
+ 9987: THREE.LinearMipmapLinearFilter,
12
+ };
13
+ const WRAP: Record<number, THREE.Wrapping> = {
14
+ 33071: THREE.ClampToEdgeWrapping,
15
+ 33648: THREE.MirroredRepeatWrapping,
16
+ 10497: THREE.RepeatWrapping,
17
+ };
18
+
19
+ /** glTF image orientation and sampling belong to every texture use, not just atlases. */
20
+ export function restoreGltfTextureBinding(texture: THREE.Texture, info: TextureInfo): void {
21
+ texture.minFilter = MIN[info.getMinFilter() ?? 9729]!;
22
+ texture.magFilter = info.getMagFilter() === 9728 ? THREE.NearestFilter : THREE.LinearFilter;
23
+ texture.wrapS = WRAP[info.getWrapS()]!;
24
+ texture.wrapT = WRAP[info.getWrapT()]!;
25
+ texture.generateMipmaps = [9984, 9985, 9986, 9987].includes(info.getMinFilter() ?? 9729);
26
+ texture.flipY = false;
27
+ const transform = info.getExtension<Transform>('KHR_texture_transform');
28
+ texture.channel = transform?.getTexCoord() ?? info.getTexCoord();
29
+ if (transform) {
30
+ texture.offset.fromArray(transform.getOffset());
31
+ texture.repeat.fromArray(transform.getScale());
32
+ texture.rotation = transform.getRotation();
33
+ // glTF composes T*R*S. Three's automatic UV matrix composes nonuniform
34
+ // repeat and rotation in a different order; retain the actual glTF basis.
35
+ const c = Math.cos(texture.rotation),
36
+ s = Math.sin(texture.rotation);
37
+ const [sx, sy] = transform.getScale(),
38
+ [x, y] = transform.getOffset();
39
+ texture.matrix.set(sx * c, sy * s, x, -sx * s, sy * c, y, 0, 0, 1);
40
+ texture.matrixAutoUpdate = false;
41
+ }
42
+ }
43
+
44
+ /** Two uses of one image may differ in sampler, UV channel, transform or atlas provenance. */
45
+ export function gltfTextureBindingKey(info: TextureInfo | null | undefined): string {
46
+ if (!info) return '';
47
+ const transform = info.getExtension<Transform>('KHR_texture_transform');
48
+ return JSON.stringify([
49
+ info.getMinFilter(),
50
+ info.getMagFilter(),
51
+ info.getWrapS(),
52
+ info.getWrapT(),
53
+ info.getTexCoord(),
54
+ transform?.getTexCoord(),
55
+ transform?.getOffset(),
56
+ transform?.getScale(),
57
+ transform?.getRotation(),
58
+ info.getExtras()['kilnAtlasReference'],
59
+ ]);
60
+ }
@@ -0,0 +1,68 @@
1
+ /** Shared CLI/MCP JSON authoring boundary. Library pins never imply project membership. */
2
+ import {
3
+ MATERIAL_LIBRARY_LIMITS,
4
+ assertMaterialJson,
5
+ proceduralMaterialDraftSchema,
6
+ proceduralMaterialDraftV2Schema,
7
+ type MaterialLibrary,
8
+ type MaterialManifest,
9
+ type MaterialRecord,
10
+ } from './material-library';
11
+ import {
12
+ createMaterialRecordV1,
13
+ createMaterialRecordV2,
14
+ resolveMaterialRecordClosure,
15
+ } from './material-library-node';
16
+ import { canonicalizeTextureAtlasSpecV1 } from './texture-atlas-contract';
17
+ import type { MaterialMapPinV1 } from './pinned-material-map';
18
+
19
+ export async function createProceduralMaterialRecord(
20
+ value: unknown,
21
+ library: MaterialLibrary,
22
+ ): Promise<{ record: MaterialRecord; dependencies: MaterialRecord[] }> {
23
+ assertMaterialJson(value);
24
+ if (
25
+ new TextEncoder().encode(JSON.stringify(value)).length >
26
+ MATERIAL_LIBRARY_LIMITS.maxManifestBytes
27
+ )
28
+ throw new Error('Material draft exceeds size limit');
29
+ if (!value || typeof value !== 'object' || Array.isArray(value))
30
+ throw new Error('Material draft must be a JSON object');
31
+ if (!Object.hasOwn(value, 'schemaVersion'))
32
+ return {
33
+ record: await createMaterialRecordV1(proceduralMaterialDraftSchema.parse(value)),
34
+ dependencies: [],
35
+ };
36
+
37
+ const draft = proceduralMaterialDraftV2Schema.parse(value);
38
+ const pins = new Map<string, MaterialMapPinV1>();
39
+ for (const map of draft.maps) {
40
+ if (!map.atlas) continue;
41
+ for (const region of canonicalizeTextureAtlasSpecV1(map.atlas).regions) {
42
+ if (region.source.kind !== 'resource') continue;
43
+ const pin = region.source.pin;
44
+ pins.set(`${pin.resourceId}@${pin.revisionId}`, pin);
45
+ }
46
+ }
47
+ if (pins.size > MATERIAL_LIBRARY_LIMITS.maxPayloadRecords)
48
+ throw new Error('Material source closure count limit exceeded');
49
+ const roots: MaterialManifest[] = [];
50
+ if (pins.size && !library.readManifest)
51
+ throw new Error('Pinned atlas creation requires a material library with readManifest support.');
52
+ for (const pin of pins.values()) {
53
+ const manifest = await library.readManifest!(pin.resourceId, pin.revisionId);
54
+ if (manifest.materialId !== pin.resourceId || manifest.revisionId !== pin.revisionId)
55
+ throw new Error('Material source pin identity mismatch');
56
+ roots.push(manifest);
57
+ }
58
+ const dependencies = roots.length
59
+ ? await resolveMaterialRecordClosure(roots, {
60
+ readManifest: (id, revision) => library.readManifest!(id, revision),
61
+ read: (id, revision) =>
62
+ library.readRecordFiles
63
+ ? library.readRecordFiles(id, revision)
64
+ : library.read(id, revision),
65
+ })
66
+ : [];
67
+ return { record: await createMaterialRecordV2(draft, { materials: dependencies }), dependencies };
68
+ }