@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
package/src/render.ts CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  appendRequirementsFinalQa,
12
12
  } from './qa/requirements-run';
13
13
  import type { AssetRequirementsQaReportV2 } from './qa/requirements-report';
14
+ import { createRequirementsQaReport } from './qa/requirements-report';
15
+ import { inspectSkinIntegrity } from './qa/skin-integrity';
14
16
  import { AuthoringDiagnosticError, rethrowAuthoringError } from './evaluator/authoring-diagnostic';
15
17
  /**
16
18
  * Headless Kiln GLB Renderer
@@ -30,6 +32,14 @@ import { collectLodSets } from './lod';
30
32
  import { applyLodChains, summarizeLodChains } from './lod-export';
31
33
  import { applyNodeVisibility, drawnSceneBounds, summarizeHiddenNodes } from './node-visibility';
32
34
  import { communitySceneDocument, resolveGltfExporter } from './community-exporter';
35
+ import {
36
+ collectAtlasProvenance,
37
+ mergeAtlasDocumentProvenance,
38
+ refreshAtlasDocumentProvenance,
39
+ validateAtlasDocumentProvenance,
40
+ type AtlasProvenanceCollection,
41
+ } from './atlas-provenance';
42
+ import { assertPortableMaterialSides } from './material-export';
33
43
  import { isCubicSplineTrack } from './animation-spline';
34
44
  import {
35
45
  applyIndexPolicy,
@@ -47,7 +57,7 @@ import {
47
57
  type GeometryExportPolicy,
48
58
  } from './geometry-export';
49
59
  import { createHash } from 'node:crypto';
50
- import { Document, Logger } from '@gltf-transform/core';
60
+ import { Document, Logger, type TextureInfo } from '@gltf-transform/core';
51
61
  import { KHRMaterialsEmissiveStrength } from '@gltf-transform/extensions';
52
62
  import { dedup, instance, palette, weld, prune, mergeDocuments } from '@gltf-transform/functions';
53
63
 
@@ -98,7 +108,7 @@ import {
98
108
  type MaterialResourceProvenanceV1,
99
109
  } from './material-resources';
100
110
  import { DEFAULT_TEXTURE_RESOLVER, type TextureResolver } from './texture-resolver';
101
- import type { MaterialLibraryPayloadV1, MaterialManifestV1 } from './material-library';
111
+ import type { MaterialLibraryPayload, MaterialManifest } from './material-library';
102
112
  import { FULL_OPTIMIZATION_PIPELINE, type RebuildOptions } from './rebuild-options';
103
113
  import { rigidMerge, type RigidMergeSummary } from './rigid-merge';
104
114
  import { assertGeneratedSourceSafe } from './validation';
@@ -464,12 +474,55 @@ function bridgeEmissive(
464
474
  );
465
475
  }
466
476
 
477
+ const GLTF_MIN_FILTER = {
478
+ [THREE.NearestFilter]: 9728,
479
+ [THREE.LinearFilter]: 9729,
480
+ [THREE.NearestMipmapNearestFilter]: 9984,
481
+ [THREE.LinearMipmapNearestFilter]: 9985,
482
+ [THREE.NearestMipmapLinearFilter]: 9986,
483
+ [THREE.LinearMipmapLinearFilter]: 9987,
484
+ } as const;
485
+ const GLTF_MAG_FILTER = { [THREE.NearestFilter]: 9728, [THREE.LinearFilter]: 9729 } as const;
486
+ const GLTF_WRAP = {
487
+ [THREE.ClampToEdgeWrapping]: 33071,
488
+ [THREE.MirroredRepeatWrapping]: 33648,
489
+ [THREE.RepeatWrapping]: 10497,
490
+ } as const;
491
+
492
+ /** Samplers belong to each texture use, not the cached glTF image. */
493
+ function bridgeTextureSampler(
494
+ source: THREE.Texture,
495
+ target: TextureInfo,
496
+ atlas?: AtlasProvenanceCollection,
497
+ ): void {
498
+ const min = GLTF_MIN_FILTER[source.minFilter];
499
+ const mag = GLTF_MAG_FILTER[source.magFilter];
500
+ const s = GLTF_WRAP[source.wrapS];
501
+ const t = GLTF_WRAP[source.wrapT];
502
+ for (const [property, value] of [
503
+ ['minFilter', min],
504
+ ['magFilter', mag],
505
+ ['wrapS', s],
506
+ ['wrapT', t],
507
+ ] as const) {
508
+ if (typeof source[property] !== 'number' || value === undefined) {
509
+ throw new TypeError(
510
+ `${source.name || '(unnamed texture)'}: unsupported ${property} ${source[property]}`,
511
+ );
512
+ }
513
+ }
514
+ target.setMinFilter(min).setMagFilter(mag).setWrapS(s).setWrapT(t);
515
+ const ref = atlas?.texture(source);
516
+ if (ref) target.setExtras({ ...target.getExtras(), kilnAtlasReference: ref });
517
+ }
518
+
467
519
  function bridgeMaterial(
468
520
  doc: Document,
469
521
  threeMat: THREE.Material,
470
522
  cache: Map<THREE.Material, GtMaterial>,
471
523
  textureCache: Map<THREE.Texture, GtTexture>,
472
524
  authorExtras?: AuthorExtras,
525
+ atlas?: AtlasProvenanceCollection,
473
526
  ): GtMaterial {
474
527
  const cached = cache.get(threeMat);
475
528
  if (cached) return cached;
@@ -505,11 +558,17 @@ function bridgeMaterial(
505
558
  // PBR texture slots (Wave 3B)
506
559
  if (stdMat.map) {
507
560
  const t = bridgeTexture(doc, stdMat.map, textureCache);
508
- if (t) mat.setBaseColorTexture(t);
561
+ if (t) {
562
+ mat.setBaseColorTexture(t);
563
+ bridgeTextureSampler(stdMat.map, mat.getBaseColorTextureInfo()!, atlas);
564
+ }
509
565
  }
510
566
  if (stdMat.normalMap) {
511
567
  const t = bridgeTexture(doc, stdMat.normalMap, textureCache);
512
- if (t) mat.setNormalTexture(t);
568
+ if (t) {
569
+ mat.setNormalTexture(t);
570
+ bridgeTextureSampler(stdMat.normalMap, mat.getNormalTextureInfo()!, atlas);
571
+ }
513
572
  }
514
573
  // metallic + roughness live in one glTF texture (R=unused, G=rough, B=metal).
515
574
  // If the agent used separate Three.js maps we emit the roughness one as
@@ -522,15 +581,24 @@ function bridgeMaterial(
522
581
  const mrSource = stdMat.roughnessMap ?? stdMat.metalnessMap;
523
582
  if (mrSource) {
524
583
  const t = bridgeTexture(doc, mrSource, textureCache);
525
- if (t) mat.setMetallicRoughnessTexture(t);
584
+ if (t) {
585
+ mat.setMetallicRoughnessTexture(t);
586
+ bridgeTextureSampler(mrSource, mat.getMetallicRoughnessTextureInfo()!, atlas);
587
+ }
526
588
  }
527
589
  if (stdMat.emissiveMap) {
528
590
  const t = bridgeTexture(doc, stdMat.emissiveMap, textureCache);
529
- if (t) mat.setEmissiveTexture(t);
591
+ if (t) {
592
+ mat.setEmissiveTexture(t);
593
+ bridgeTextureSampler(stdMat.emissiveMap, mat.getEmissiveTextureInfo()!, atlas);
594
+ }
530
595
  }
531
596
  if (stdMat.aoMap) {
532
597
  const t = bridgeTexture(doc, stdMat.aoMap, textureCache);
533
- if (t) mat.setOcclusionTexture(t);
598
+ if (t) {
599
+ mat.setOcclusionTexture(t);
600
+ bridgeTextureSampler(stdMat.aoMap, mat.getOcclusionTextureInfo()!, atlas);
601
+ }
534
602
  mat.setOcclusionStrength(stdMat.aoMapIntensity ?? 1);
535
603
  }
536
604
  } else if (matFlags.isMeshLambertMaterial) {
@@ -580,7 +648,10 @@ function bridgeMaterial(
580
648
  if (threeMat.side === THREE.DoubleSide) mat.setDoubleSided(true);
581
649
  if (common.map && !mat.getBaseColorTexture()) {
582
650
  const texture = bridgeTexture(doc, common.map, textureCache);
583
- if (texture) mat.setBaseColorTexture(texture);
651
+ if (texture) {
652
+ mat.setBaseColorTexture(texture);
653
+ bridgeTextureSampler(common.map, mat.getBaseColorTextureInfo()!, atlas);
654
+ }
584
655
  }
585
656
 
586
657
  cache.set(threeMat, mat);
@@ -627,6 +698,7 @@ function bridgeGeometry(
627
698
  geometry: THREE.BufferGeometry,
628
699
  material: GtMaterial | GtMaterial[],
629
700
  meshName: string,
701
+ atlas?: AtlasProvenanceCollection,
630
702
  ): GtMesh {
631
703
  if (!geometry.getAttribute('normal')) {
632
704
  geometry.computeVertexNormals();
@@ -697,6 +769,8 @@ function bridgeGeometry(
697
769
  );
698
770
  }
699
771
 
772
+ const binding = atlas?.binding(geometry);
773
+ if (binding) prim.setExtras({ kilnAtlasBinding: binding });
700
774
  const mesh = doc.createMesh(meshName);
701
775
  if (!Array.isArray(material)) return mesh.addPrimitive(prim);
702
776
  const indexValues = indexAttr
@@ -715,6 +789,7 @@ function bridgeGeometry(
715
789
  .setType(TYPE_SCALAR)
716
790
  .setBuffer(buf),
717
791
  );
792
+ if (binding) part.setExtras({ kilnAtlasBinding: binding });
718
793
  mesh.addPrimitive(part);
719
794
  }
720
795
  prim.dispose();
@@ -730,6 +805,7 @@ function bridgeNode(
730
805
  meshCache: Map<string, GtMesh>,
731
806
  texCache: Map<THREE.Texture, GtTexture>,
732
807
  authorExtras?: AuthorExtras,
808
+ atlas?: AtlasProvenanceCollection,
733
809
  ): GtNode {
734
810
  const gtNode = doc.createNode(threeObj.name || undefined);
735
811
 
@@ -800,7 +876,7 @@ function bridgeNode(
800
876
  const threeMesh = threeObj as THREE.Mesh;
801
877
  const threeMats = Array.isArray(threeMesh.material) ? threeMesh.material : [threeMesh.material];
802
878
  const gtMats = threeMats.map((material) =>
803
- bridgeMaterial(doc, material, matCache, texCache, authorExtras),
879
+ bridgeMaterial(doc, material, matCache, texCache, authorExtras, atlas),
804
880
  );
805
881
  const gtMat = Array.isArray(threeMesh.material) ? gtMats : gtMats[0]!;
806
882
 
@@ -811,14 +887,14 @@ function bridgeNode(
811
887
  const cacheKey = `${threeMesh.geometry.uuid}__${threeMats.map((material) => material.uuid).join('_')}`;
812
888
  let gtMesh = meshCache.get(cacheKey);
813
889
  if (!gtMesh) {
814
- gtMesh = bridgeGeometry(doc, buf, threeMesh.geometry, gtMat, threeMesh.name || 'mesh');
890
+ gtMesh = bridgeGeometry(doc, buf, threeMesh.geometry, gtMat, threeMesh.name || 'mesh', atlas);
815
891
  meshCache.set(cacheKey, gtMesh);
816
892
  }
817
893
  gtNode.setMesh(gtMesh);
818
894
  } else if ((threeObj as THREE.Object3D & { isSprite?: boolean }).isSprite) {
819
895
  const sprite = threeObj as THREE.Sprite;
820
896
  const threeMat = sprite.material;
821
- const gtMat = bridgeMaterial(doc, threeMat, matCache, texCache, authorExtras);
897
+ const gtMat = bridgeMaterial(doc, threeMat, matCache, texCache, authorExtras, atlas);
822
898
  // Unit XY quad matches Three.js Sprite scale semantics. Non-default center
823
899
  // and material rotation are baked into the quad; camera-facing remains an
824
900
  // explicit semantic runtime behavior on the exported node.
@@ -833,7 +909,7 @@ function bridgeNode(
833
909
  geometry.translate(0.5 - centerX, 0.5 - centerY, 0);
834
910
  }
835
911
  if (rotation !== 0) geometry.rotateZ(rotation);
836
- gtMesh = bridgeGeometry(doc, buf, geometry, gtMat, sprite.name || 'sprite-quad');
912
+ gtMesh = bridgeGeometry(doc, buf, geometry, gtMat, sprite.name || 'sprite-quad', atlas);
837
913
  meshCache.set(cacheKey, gtMesh);
838
914
  }
839
915
  gtNode.setMesh(gtMesh);
@@ -853,6 +929,7 @@ function bridgeNode(
853
929
  meshCache,
854
930
  texCache,
855
931
  authorExtras,
932
+ atlas,
856
933
  );
857
934
  gtNode.addChild(childNode);
858
935
  }
@@ -1070,7 +1147,7 @@ export interface RenderResult {
1070
1147
  materialResourceProvenance?: MaterialResourceProvenanceV1[];
1071
1148
  /** Exact host-approved input dependencies, including licenses and procedural recipes.
1072
1149
  * These are declared build inputs, not proof that every supplied map was used. */
1073
- materialLibraryDependencies?: MaterialManifestV1[];
1150
+ materialLibraryDependencies?: MaterialManifest[];
1074
1151
  /** Textures baked into the returned GLB, including bounded procedural lineage. */
1075
1152
  bakedTextures?: BakedTextureProvenanceV1[];
1076
1153
  integrationManifest: IntegrationManifestV1;
@@ -1539,6 +1616,29 @@ export async function renderSceneToGLB(
1539
1616
  if (opts.geometryPolicy !== undefined && !['warn', 'strict'].includes(opts.geometryPolicy))
1540
1617
  throw new Error('geometryPolicy must be warn or strict');
1541
1618
  const exporter = resolveGltfExporter(opts.gltfExporter);
1619
+ // Mandatory source-data invariants precede any operation that dereferences bindings.
1620
+ // Reuse the structured QA transport without claiming that the full QA rules ran.
1621
+ // Like invalid geometry buffers, malformed skin data cannot be made safe by QA-warn
1622
+ // or derivative mode. No automatic weight normalization or rest-pose repair occurs.
1623
+ const skinIntegrity = inspectSkinIntegrity(root);
1624
+ if (skinIntegrity.findings.length) {
1625
+ throw new AssetQaBlockedError(
1626
+ createRequirementsQaReport(requirements, {
1627
+ findings: skinIntegrity.findings,
1628
+ executedRules: [],
1629
+ evaluatedDimensions: ['exportIntegrity'],
1630
+ metrics: {
1631
+ exportIntegrity: {
1632
+ skinMeshesChecked: skinIntegrity.meshesChecked,
1633
+ skinVerticesChecked: skinIntegrity.verticesChecked,
1634
+ skinInspectionTruncated: skinIntegrity.truncated,
1635
+ },
1636
+ },
1637
+ }),
1638
+ 'scene',
1639
+ );
1640
+ }
1641
+ assertPortableMaterialSides(root);
1542
1642
  // A set of LOD tiers needs its declared thresholds before anything is written. A
1543
1643
  // derivative re-serializes a review scene that has no declarations, so it is not judged.
1544
1644
  const lodSets = opts.derivative === true ? [] : collectLodSets(root);
@@ -1569,6 +1669,7 @@ export async function renderSceneToGLB(
1569
1669
  // that will actually be written. A procedural DataTexture used to reach the
1570
1670
  // bridge with no encoded bytes and be dropped without a word; now it is
1571
1671
  // either baked into the GLB or warned about by name.
1672
+ const atlas = collectAtlasProvenance(root);
1572
1673
  const bakedTextures = await bakeSceneTextures(root, warnings);
1573
1674
  // A normal-mapped mesh without tangents makes every runtime invent its own
1574
1675
  // tangent basis, so the same asset lights differently in different engines.
@@ -1599,6 +1700,7 @@ export async function renderSceneToGLB(
1599
1700
  nativeClips,
1600
1701
  (await import('./exporter-node')).nodeExportPlatform,
1601
1702
  authorExtras,
1703
+ atlas,
1602
1704
  )
1603
1705
  : new Document();
1604
1706
  // `asset.generator` defaults to the serializer's own version string, which put
@@ -1627,6 +1729,7 @@ export async function renderSceneToGLB(
1627
1729
  meshCache,
1628
1730
  texCache,
1629
1731
  authorExtras,
1732
+ atlas,
1630
1733
  );
1631
1734
  const gltfScene = doc.createScene(opts.sceneName ?? 'Scene').addChild(rootNode);
1632
1735
  doc.getRoot().setDefaultScene(gltfScene);
@@ -1647,6 +1750,10 @@ export async function renderSceneToGLB(
1647
1750
  scene.setExtras({ [REVIEW_CLIPS_EXTRAS_KEY]: review });
1648
1751
  }
1649
1752
  }
1753
+ if (atlas.table) {
1754
+ const scene = doc.getRoot().getDefaultScene()!;
1755
+ scene.setExtras({ ...scene.getExtras(), kilnAtlasProvenance: atlas.table });
1756
+ }
1650
1757
  // Visibility flags and LOD chains are written before any pass merges or prunes nodes, so
1651
1758
  // those passes see them and keep them; flags first, while every node is still in place.
1652
1759
  applyNodeVisibility(root, doc);
@@ -1734,6 +1841,8 @@ export async function renderSceneToGLB(
1734
1841
 
1735
1842
  const io = engineIO();
1736
1843
  ensureDefaultScene(doc);
1844
+ await refreshAtlasDocumentProvenance(doc);
1845
+ await validateAtlasDocumentProvenance(doc);
1737
1846
  const bytes = await io.writeBinary(doc);
1738
1847
  const artifactGlbSha256 = `sha256:${await sha256Hex(bytes)}` as const;
1739
1848
  const boundBakedTextures = bakedTextures.map((entry) => ({
@@ -1844,7 +1953,7 @@ export interface RenderGlbOptions {
1844
1953
  /** Trusted evaluator dependency; never derived from generated code. */
1845
1954
  textureResolver?: TextureResolver;
1846
1955
  /** Host-approved portable material maps, transported as data to evaluator workers. */
1847
- materialResources?: MaterialLibraryPayloadV1;
1956
+ materialResources?: MaterialLibraryPayload;
1848
1957
  /** In-process host diagnostic sink; not serializable to evaluator workers. */
1849
1958
  diagnosticConsole?: DiagnosticConsole;
1850
1959
  }
@@ -1874,7 +1983,7 @@ export async function renderGLBInProcess(
1874
1983
  : {}),
1875
1984
  instance: resolveInstance(opts.instance),
1876
1985
  };
1877
- let materialLibraryDependencies: MaterialManifestV1[] | undefined;
1986
+ let materialLibraryDependencies: MaterialManifest[] | undefined;
1878
1987
  if (opts.materialResources) {
1879
1988
  const { decodeMaterialLibraryPayload, createMaterialLibraryTextureResolver } = await import(
1880
1989
  './material-library-node'
@@ -2029,6 +2138,7 @@ export async function optimizeGlbBytes(
2029
2138
  try {
2030
2139
  const io = engineIO();
2031
2140
  const doc = await io.readBinary(bytes);
2141
+ await validateAtlasDocumentProvenance(doc);
2032
2142
  // GPU-instancing first (dedup already ran at bake; instance → palette is the
2033
2143
  // canonical order). Defaults OFF here — this is the web-tier re-bake seam, and
2034
2144
  // the caller opts in per-asset (Studio passes instance:'auto' + the record's
@@ -2053,6 +2163,8 @@ export async function optimizeGlbBytes(
2053
2163
  if (mode) summary = await consolidateMaterials(doc, mode);
2054
2164
  if (!summary && !instancing) return undefined;
2055
2165
  ensureDefaultScene(doc);
2166
+ await refreshAtlasDocumentProvenance(doc);
2167
+ await validateAtlasDocumentProvenance(doc);
2056
2168
  const outBytes = await io.writeBinary(doc);
2057
2169
  const gltfValidation = await assertFinalGlbValid(outBytes);
2058
2170
  let report: InstanceabilityReport | undefined;
@@ -2157,6 +2269,7 @@ export async function snapGlbToPalette(
2157
2269
  try {
2158
2270
  const io = engineIO();
2159
2271
  const doc = await io.readBinary(bytes);
2272
+ await validateAtlasDocumentProvenance(doc);
2160
2273
  const before = collectGlbMetrics(doc);
2161
2274
  const idx = buildSlotIndex(slots as readonly SnapSlot[]);
2162
2275
  let snapped = 0;
@@ -2209,6 +2322,8 @@ export async function snapGlbToPalette(
2209
2322
  drawsAfter: after.drawCalls,
2210
2323
  };
2211
2324
  ensureDefaultScene(doc);
2325
+ await refreshAtlasDocumentProvenance(doc);
2326
+ await validateAtlasDocumentProvenance(doc);
2212
2327
  const outBytes = await io.writeBinary(doc);
2213
2328
  const gltfValidation = await assertFinalGlbValid(outBytes);
2214
2329
  let report: InstanceabilityReport | undefined;
@@ -2322,6 +2437,7 @@ export async function composeSceneGLB(
2322
2437
  const part = parts[i]!;
2323
2438
  try {
2324
2439
  const src = await io.readBinary(part.bytes);
2440
+ await mergeAtlasDocumentProvenance(master, src);
2325
2441
  if (opts.keepAnimations && opts.animationNaming === 'instance') {
2326
2442
  for (const [clipIndex, animation] of src.getRoot().listAnimations().entries()) {
2327
2443
  animation.setName(
@@ -2393,6 +2509,8 @@ export async function composeSceneGLB(
2393
2509
 
2394
2510
  const metrics = collectGlbMetrics(master);
2395
2511
  ensureDefaultScene(master);
2512
+ await refreshAtlasDocumentProvenance(master);
2513
+ await validateAtlasDocumentProvenance(master);
2396
2514
  const bytes = await io.writeBinary(master);
2397
2515
  const gltfValidation = await assertFinalGlbValid(bytes, 'scene.glb');
2398
2516
  for (const issue of gltfValidation.issues.messages) {
@@ -0,0 +1,225 @@
1
+ import * as THREE from 'three';
2
+ import { AuthoringDiagnosticError } from './evaluator/authoring-diagnostic';
3
+ import {
4
+ stampSemanticMetadataV1,
5
+ type SemanticMetadataV1,
6
+ type SemanticMetadataV1Input,
7
+ } from './contracts';
8
+ import { GEOMETRY_ALLOCATION_LIMITS as limits, assertFiniteTriple } from './geometry-budget';
9
+
10
+ export interface CreateSkinnedPartOptions {
11
+ /** Already assembled common hierarchy containing every bone, in the rest pose. */
12
+ parent: THREE.Object3D;
13
+ /** Explicit, stable index order. Bones remain owned by the caller's hierarchy. */
14
+ bones: readonly THREE.Bone[];
15
+ /** Four indices and weights per geometry vertex, including zero-weight lanes. */
16
+ indices: ArrayLike<number>;
17
+ weights: ArrayLike<number>;
18
+ /** Default false: require weights to sum to one. Never invent weights for zero sums. */
19
+ normalizeWeights?: boolean;
20
+ position?: [number, number, number];
21
+ /** Local Euler angles in degrees, matching createPart. */
22
+ rotation?: [number, number, number];
23
+ scale?: [number, number, number];
24
+ semantic?: SemanticMetadataV1 | SemanticMetadataV1Input;
25
+ }
26
+
27
+ function topRoot(node: THREE.Object3D): THREE.Object3D {
28
+ while (node.parent) node = node.parent;
29
+ return node;
30
+ }
31
+
32
+ function assertMatrix(matrix: THREE.Matrix4, label: string): void {
33
+ if (
34
+ !matrix.elements.every(
35
+ (value) => Number.isFinite(value) && Number.isFinite(Math.fround(value)),
36
+ ) ||
37
+ !Number.isFinite(matrix.determinant()) ||
38
+ matrix.determinant() === 0 ||
39
+ !matrix
40
+ .clone()
41
+ .invert()
42
+ .elements.every((value) => Number.isFinite(value) && Number.isFinite(Math.fround(value)))
43
+ )
44
+ throw new RangeError(`createSkinnedPart ${label} must have finite invertible bind transforms.`);
45
+ }
46
+
47
+ /** Clone unchanged topology, attach explicit four-lane influences and bind at the current rest pose. */
48
+ export function createSkinnedPart(
49
+ name: string,
50
+ geometry: THREE.BufferGeometry,
51
+ material: THREE.Material | THREE.Material[],
52
+ options: CreateSkinnedPartOptions,
53
+ ): THREE.SkinnedMesh {
54
+ try {
55
+ return buildSkinnedPart(name, geometry, material, options);
56
+ } catch (error) {
57
+ if (error instanceof TypeError || error instanceof RangeError)
58
+ throw new AuthoringDiagnosticError('SKIN_BINDING_ARGUMENT', error.message);
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ function buildSkinnedPart(
64
+ name: string,
65
+ geometry: THREE.BufferGeometry,
66
+ material: THREE.Material | THREE.Material[],
67
+ options: CreateSkinnedPartOptions,
68
+ ): THREE.SkinnedMesh {
69
+ if (
70
+ typeof name !== 'string' ||
71
+ !name.trim() ||
72
+ THREE.PropertyBinding.sanitizeNodeName(name) !== name
73
+ )
74
+ throw new TypeError(
75
+ 'createSkinnedPart name must be nonempty and free of animation binding punctuation.',
76
+ );
77
+ if (!geometry?.isBufferGeometry)
78
+ throw new TypeError('createSkinnedPart requires BufferGeometry.');
79
+ const materials = Array.isArray(material) ? material : [material];
80
+ if (!materials.length || materials.some((value) => !value?.isMaterial))
81
+ throw new TypeError('createSkinnedPart requires a material or nonempty material array.');
82
+ if (!options?.parent?.isObject3D)
83
+ throw new TypeError('createSkinnedPart requires an existing parent in the bone hierarchy.');
84
+ const { bones, indices, weights, parent } = options;
85
+ if (!Array.isArray(bones) || bones.length < 1 || bones.length > limits.skinBones)
86
+ throw new RangeError(`createSkinnedPart bone limit is 1..${limits.skinBones}.`);
87
+ if (options.normalizeWeights !== undefined && typeof options.normalizeWeights !== 'boolean')
88
+ throw new TypeError('createSkinnedPart normalizeWeights must be boolean.');
89
+ const top = topRoot(parent),
90
+ names = new Set<string>(),
91
+ members = new Set<THREE.Bone>();
92
+ for (const bone of bones) {
93
+ if (!bone?.isBone || topRoot(bone) !== top || members.has(bone))
94
+ throw new TypeError(
95
+ 'createSkinnedPart bones must be distinct Bones in the parent hierarchy.',
96
+ );
97
+ if (
98
+ !bone.name ||
99
+ THREE.PropertyBinding.sanitizeNodeName(bone.name) !== bone.name ||
100
+ names.has(bone.name)
101
+ )
102
+ throw new TypeError('createSkinnedPart bones need unique animation-safe names.');
103
+ members.add(bone);
104
+ names.add(bone.name);
105
+ }
106
+ // Collision with any other node makes name-based animation ambiguous.
107
+ top.traverse((node) => {
108
+ if (names.has(node.name) && !members.has(node as THREE.Bone))
109
+ throw new TypeError(`createSkinnedPart ambiguous bone name: ${node.name}.`);
110
+ });
111
+ const position = geometry.getAttribute('position'),
112
+ count = position?.count;
113
+ if (
114
+ position?.itemSize !== 3 ||
115
+ !Number.isSafeInteger(count) ||
116
+ count! < 1 ||
117
+ count! > limits.skinVertices
118
+ )
119
+ throw new RangeError(
120
+ `createSkinnedPart position vertex limit is 1..${limits.skinVertices} xyz vertices.`,
121
+ );
122
+ if (geometry.hasAttribute('skinIndex') || geometry.hasAttribute('skinWeight'))
123
+ throw new TypeError(
124
+ 'createSkinnedPart geometry already contains binding attributes; provide unbound geometry.',
125
+ );
126
+ let bytes = count! * 4 * (2 + 4);
127
+ const attributes = [
128
+ geometry.getIndex(),
129
+ ...Object.values(geometry.attributes),
130
+ ...Object.values(geometry.morphAttributes).flat(),
131
+ ];
132
+ for (const attribute of attributes) {
133
+ if (!attribute) continue;
134
+ bytes += attribute.array.byteLength;
135
+ if (!Number.isSafeInteger(bytes) || bytes > limits.skinAttributeBytes)
136
+ throw new RangeError(
137
+ `createSkinnedPart attribute allocation limit is ${limits.skinAttributeBytes} bytes.`,
138
+ );
139
+ }
140
+ if (!indices || !weights || indices.length !== count! * 4 || weights.length !== count! * 4)
141
+ throw new RangeError('createSkinnedPart requires exactly four indices and weights per vertex.');
142
+ // First pass validates all input before cloning or allocating output attributes.
143
+ for (let vertex = 0; vertex < count!; vertex++) {
144
+ let sum = 0;
145
+ for (let lane = 0; lane < 4; lane++) {
146
+ const i = vertex * 4 + lane,
147
+ index = indices[i]!,
148
+ weight = weights[i]!;
149
+ if (!Number.isInteger(index) || index < 0 || index >= bones.length)
150
+ throw new RangeError(
151
+ `createSkinnedPart vertex ${vertex} lane ${lane} has an invalid bone index.`,
152
+ );
153
+ if (!Number.isFinite(weight) || weight < 0)
154
+ throw new RangeError(
155
+ `createSkinnedPart vertex ${vertex} lane ${lane} needs a finite nonnegative weight.`,
156
+ );
157
+ if (!options.normalizeWeights && weight > 1)
158
+ throw new RangeError(
159
+ `createSkinnedPart vertex ${vertex} weights must be <=1 and sum to one; request normalizeWeights explicitly.`,
160
+ );
161
+ sum += weight;
162
+ }
163
+ if (
164
+ !Number.isFinite(sum) ||
165
+ sum <= 0 ||
166
+ (!options.normalizeWeights && Math.abs(sum - 1) > 1e-5)
167
+ )
168
+ throw new RangeError(
169
+ `createSkinnedPart vertex ${vertex} has an invalid weight sum; require one or explicitly normalize a positive finite sum.`,
170
+ );
171
+ }
172
+ for (const key of ['position', 'rotation', 'scale'] as const)
173
+ if (options[key] !== undefined) assertFiniteTriple(`createSkinnedPart ${key}`, options[key]);
174
+ const local = new THREE.Matrix4().compose(
175
+ new THREE.Vector3(...(options.position ?? [0, 0, 0])),
176
+ new THREE.Quaternion().setFromEuler(
177
+ new THREE.Euler(
178
+ ...((options.rotation ?? [0, 0, 0]).map(THREE.MathUtils.degToRad) as [
179
+ number,
180
+ number,
181
+ number,
182
+ ]),
183
+ ),
184
+ ),
185
+ new THREE.Vector3(...(options.scale ?? [1, 1, 1])),
186
+ );
187
+ top.updateWorldMatrix(true, true);
188
+ const bind = new THREE.Matrix4().multiplyMatrices(parent.matrixWorld, local);
189
+ assertMatrix(bind, 'mesh');
190
+ for (const bone of bones) {
191
+ assertMatrix(bone.matrixWorld, `bone ${bone.name}`);
192
+ }
193
+ const skinIndices = new Uint16Array(indices.length),
194
+ skinWeights = new Float32Array(weights.length);
195
+ for (let vertex = 0; vertex < count!; vertex++) {
196
+ const start = vertex * 4;
197
+ const sum = options.normalizeWeights
198
+ ? weights[start]! + weights[start + 1]! + weights[start + 2]! + weights[start + 3]!
199
+ : 1;
200
+ for (let lane = 0; lane < 4; lane++) {
201
+ skinIndices[start + lane] = indices[start + lane]!;
202
+ skinWeights[start + lane] = weights[start + lane]! / sum;
203
+ }
204
+ }
205
+ const owned = geometry.clone();
206
+ owned.setAttribute('skinIndex', new THREE.BufferAttribute(skinIndices, 4));
207
+ owned.setAttribute('skinWeight', new THREE.BufferAttribute(skinWeights, 4));
208
+ const mesh = new THREE.SkinnedMesh(owned, Array.isArray(material) ? [...material] : material);
209
+ mesh.name = `Mesh_${name}`;
210
+ if (options.position) mesh.position.fromArray(options.position);
211
+ if (options.rotation)
212
+ mesh.rotation.set(
213
+ ...(options.rotation.map(THREE.MathUtils.degToRad) as [number, number, number]),
214
+ );
215
+ if (options.scale) mesh.scale.fromArray(options.scale);
216
+ if (options.semantic) stampSemanticMetadataV1(mesh, options.semantic);
217
+ const skeleton = new THREE.Skeleton(
218
+ [...bones],
219
+ bones.map((bone) => bone.matrixWorld.clone().invert()),
220
+ );
221
+ mesh.bind(skeleton, bind);
222
+ parent.add(mesh);
223
+ mesh.updateWorldMatrix(true, false);
224
+ return mesh;
225
+ }