@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
@@ -0,0 +1,144 @@
1
+ import * as THREE from 'three';
2
+ import { AuthoringDiagnosticError } from "./evaluator/authoring-diagnostic.js";
3
+ import { stampSemanticMetadataV1, } from "./contracts/index.js";
4
+ import { GEOMETRY_ALLOCATION_LIMITS as limits, assertFiniteTriple } from "./geometry-budget.js";
5
+ function topRoot(node) {
6
+ while (node.parent)
7
+ node = node.parent;
8
+ return node;
9
+ }
10
+ function assertMatrix(matrix, label) {
11
+ if (!matrix.elements.every((value) => Number.isFinite(value) && Number.isFinite(Math.fround(value))) ||
12
+ !Number.isFinite(matrix.determinant()) ||
13
+ matrix.determinant() === 0 ||
14
+ !matrix
15
+ .clone()
16
+ .invert()
17
+ .elements.every((value) => Number.isFinite(value) && Number.isFinite(Math.fround(value))))
18
+ throw new RangeError(`createSkinnedPart ${label} must have finite invertible bind transforms.`);
19
+ }
20
+ /** Clone unchanged topology, attach explicit four-lane influences and bind at the current rest pose. */
21
+ export function createSkinnedPart(name, geometry, material, options) {
22
+ try {
23
+ return buildSkinnedPart(name, geometry, material, options);
24
+ }
25
+ catch (error) {
26
+ if (error instanceof TypeError || error instanceof RangeError)
27
+ throw new AuthoringDiagnosticError('SKIN_BINDING_ARGUMENT', error.message);
28
+ throw error;
29
+ }
30
+ }
31
+ function buildSkinnedPart(name, geometry, material, options) {
32
+ if (typeof name !== 'string' ||
33
+ !name.trim() ||
34
+ THREE.PropertyBinding.sanitizeNodeName(name) !== name)
35
+ throw new TypeError('createSkinnedPart name must be nonempty and free of animation binding punctuation.');
36
+ if (!geometry?.isBufferGeometry)
37
+ throw new TypeError('createSkinnedPart requires BufferGeometry.');
38
+ const materials = Array.isArray(material) ? material : [material];
39
+ if (!materials.length || materials.some((value) => !value?.isMaterial))
40
+ throw new TypeError('createSkinnedPart requires a material or nonempty material array.');
41
+ if (!options?.parent?.isObject3D)
42
+ throw new TypeError('createSkinnedPart requires an existing parent in the bone hierarchy.');
43
+ const { bones, indices, weights, parent } = options;
44
+ if (!Array.isArray(bones) || bones.length < 1 || bones.length > limits.skinBones)
45
+ throw new RangeError(`createSkinnedPart bone limit is 1..${limits.skinBones}.`);
46
+ if (options.normalizeWeights !== undefined && typeof options.normalizeWeights !== 'boolean')
47
+ throw new TypeError('createSkinnedPart normalizeWeights must be boolean.');
48
+ const top = topRoot(parent), names = new Set(), members = new Set();
49
+ for (const bone of bones) {
50
+ if (!bone?.isBone || topRoot(bone) !== top || members.has(bone))
51
+ throw new TypeError('createSkinnedPart bones must be distinct Bones in the parent hierarchy.');
52
+ if (!bone.name ||
53
+ THREE.PropertyBinding.sanitizeNodeName(bone.name) !== bone.name ||
54
+ names.has(bone.name))
55
+ throw new TypeError('createSkinnedPart bones need unique animation-safe names.');
56
+ members.add(bone);
57
+ names.add(bone.name);
58
+ }
59
+ // Collision with any other node makes name-based animation ambiguous.
60
+ top.traverse((node) => {
61
+ if (names.has(node.name) && !members.has(node))
62
+ throw new TypeError(`createSkinnedPart ambiguous bone name: ${node.name}.`);
63
+ });
64
+ const position = geometry.getAttribute('position'), count = position?.count;
65
+ if (position?.itemSize !== 3 ||
66
+ !Number.isSafeInteger(count) ||
67
+ count < 1 ||
68
+ count > limits.skinVertices)
69
+ throw new RangeError(`createSkinnedPart position vertex limit is 1..${limits.skinVertices} xyz vertices.`);
70
+ if (geometry.hasAttribute('skinIndex') || geometry.hasAttribute('skinWeight'))
71
+ throw new TypeError('createSkinnedPart geometry already contains binding attributes; provide unbound geometry.');
72
+ let bytes = count * 4 * (2 + 4);
73
+ const attributes = [
74
+ geometry.getIndex(),
75
+ ...Object.values(geometry.attributes),
76
+ ...Object.values(geometry.morphAttributes).flat(),
77
+ ];
78
+ for (const attribute of attributes) {
79
+ if (!attribute)
80
+ continue;
81
+ bytes += attribute.array.byteLength;
82
+ if (!Number.isSafeInteger(bytes) || bytes > limits.skinAttributeBytes)
83
+ throw new RangeError(`createSkinnedPart attribute allocation limit is ${limits.skinAttributeBytes} bytes.`);
84
+ }
85
+ if (!indices || !weights || indices.length !== count * 4 || weights.length !== count * 4)
86
+ throw new RangeError('createSkinnedPart requires exactly four indices and weights per vertex.');
87
+ // First pass validates all input before cloning or allocating output attributes.
88
+ for (let vertex = 0; vertex < count; vertex++) {
89
+ let sum = 0;
90
+ for (let lane = 0; lane < 4; lane++) {
91
+ const i = vertex * 4 + lane, index = indices[i], weight = weights[i];
92
+ if (!Number.isInteger(index) || index < 0 || index >= bones.length)
93
+ throw new RangeError(`createSkinnedPart vertex ${vertex} lane ${lane} has an invalid bone index.`);
94
+ if (!Number.isFinite(weight) || weight < 0)
95
+ throw new RangeError(`createSkinnedPart vertex ${vertex} lane ${lane} needs a finite nonnegative weight.`);
96
+ if (!options.normalizeWeights && weight > 1)
97
+ throw new RangeError(`createSkinnedPart vertex ${vertex} weights must be <=1 and sum to one; request normalizeWeights explicitly.`);
98
+ sum += weight;
99
+ }
100
+ if (!Number.isFinite(sum) ||
101
+ sum <= 0 ||
102
+ (!options.normalizeWeights && Math.abs(sum - 1) > 1e-5))
103
+ throw new RangeError(`createSkinnedPart vertex ${vertex} has an invalid weight sum; require one or explicitly normalize a positive finite sum.`);
104
+ }
105
+ for (const key of ['position', 'rotation', 'scale'])
106
+ if (options[key] !== undefined)
107
+ assertFiniteTriple(`createSkinnedPart ${key}`, options[key]);
108
+ const local = new THREE.Matrix4().compose(new THREE.Vector3(...(options.position ?? [0, 0, 0])), new THREE.Quaternion().setFromEuler(new THREE.Euler(...(options.rotation ?? [0, 0, 0]).map(THREE.MathUtils.degToRad))), new THREE.Vector3(...(options.scale ?? [1, 1, 1])));
109
+ top.updateWorldMatrix(true, true);
110
+ const bind = new THREE.Matrix4().multiplyMatrices(parent.matrixWorld, local);
111
+ assertMatrix(bind, 'mesh');
112
+ for (const bone of bones) {
113
+ assertMatrix(bone.matrixWorld, `bone ${bone.name}`);
114
+ }
115
+ const skinIndices = new Uint16Array(indices.length), skinWeights = new Float32Array(weights.length);
116
+ for (let vertex = 0; vertex < count; vertex++) {
117
+ const start = vertex * 4;
118
+ const sum = options.normalizeWeights
119
+ ? weights[start] + weights[start + 1] + weights[start + 2] + weights[start + 3]
120
+ : 1;
121
+ for (let lane = 0; lane < 4; lane++) {
122
+ skinIndices[start + lane] = indices[start + lane];
123
+ skinWeights[start + lane] = weights[start + lane] / sum;
124
+ }
125
+ }
126
+ const owned = geometry.clone();
127
+ owned.setAttribute('skinIndex', new THREE.BufferAttribute(skinIndices, 4));
128
+ owned.setAttribute('skinWeight', new THREE.BufferAttribute(skinWeights, 4));
129
+ const mesh = new THREE.SkinnedMesh(owned, Array.isArray(material) ? [...material] : material);
130
+ mesh.name = `Mesh_${name}`;
131
+ if (options.position)
132
+ mesh.position.fromArray(options.position);
133
+ if (options.rotation)
134
+ mesh.rotation.set(...options.rotation.map(THREE.MathUtils.degToRad));
135
+ if (options.scale)
136
+ mesh.scale.fromArray(options.scale);
137
+ if (options.semantic)
138
+ stampSemanticMetadataV1(mesh, options.semantic);
139
+ const skeleton = new THREE.Skeleton([...bones], bones.map((bone) => bone.matrixWorld.clone().invert()));
140
+ mesh.bind(skeleton, bind);
141
+ parent.add(mesh);
142
+ mesh.updateWorldMatrix(true, false);
143
+ return mesh;
144
+ }
@@ -0,0 +1,97 @@
1
+ /** Browser-safe named pixel regions. Admission never loads or allocates image pixels. */
2
+ import { type CanonicalPortableSamplerV1, type PortableSamplerV1 } from "./portable-sampler.js";
3
+ import { type CanonicalProceduralTextureSpecV3, type ProceduralTextureSpecV3 } from "./procedural-texture-v3-contract.js";
4
+ import type { MaterialMapPinV1 } from "./pinned-material-map.js";
5
+ import type { TextureAtlasManifestV1 } from "./texture-atlas-pixels.js";
6
+ import { type TextureUsage } from "./texture-contract.js";
7
+ import { AuthoringDiagnosticError } from "./evaluator/authoring-diagnostic.js";
8
+ export declare const TEXTURE_ATLAS_COMPILER_ID: 'kiln.texture-atlas.v1.1';
9
+ export declare const TEXTURE_ATLAS_LIMITS: Readonly<{
10
+ edge: 1024;
11
+ outputTexels: 1048576;
12
+ regions: 64;
13
+ sources: 16;
14
+ sourceTexels: 4194304;
15
+ encodedSourceBytes: 8388608;
16
+ encodedBytes: 16777216;
17
+ jsonBytes: 262144;
18
+ work: 64000000;
19
+ allocatedBytes: 67108864;
20
+ uvVertices: 262144;
21
+ uvAttributeBytes: 134217728;
22
+ }>;
23
+ export interface AtlasRectangleV1 {
24
+ x: number;
25
+ y: number;
26
+ width: number;
27
+ height: number;
28
+ }
29
+ export type AtlasEdgeModeV1 = 'copy-edge' | 'mask-rgb-dilate';
30
+ export interface AtlasResourceExpectationV1 {
31
+ width?: number;
32
+ height?: number;
33
+ imageSha256?: `sha256:${string}`;
34
+ decodedPixelHash?: `sha256:${string}`;
35
+ }
36
+ export type TextureAtlasSourceV1 = {
37
+ kind: 'procedural';
38
+ spec: ProceduralTextureSpecV3;
39
+ } | {
40
+ kind: 'resource';
41
+ resourceId: string;
42
+ pin: MaterialMapPinV1;
43
+ expected?: AtlasResourceExpectationV1;
44
+ };
45
+ export interface TextureAtlasRegionV1 {
46
+ name: string;
47
+ content: AtlasRectangleV1;
48
+ gutter?: number;
49
+ edgeMode?: AtlasEdgeModeV1;
50
+ source: TextureAtlasSourceV1;
51
+ crop?: AtlasRectangleV1;
52
+ }
53
+ export interface TextureAtlasSpecV1 {
54
+ schemaVersion: 1;
55
+ width: number;
56
+ height: number;
57
+ usage: TextureUsage;
58
+ sampler?: PortableSamplerV1;
59
+ regions: TextureAtlasRegionV1[];
60
+ }
61
+ export type CanonicalTextureAtlasSourceV1 = {
62
+ kind: 'procedural';
63
+ spec: CanonicalProceduralTextureSpecV3;
64
+ } | Extract<TextureAtlasSourceV1, {
65
+ kind: 'resource';
66
+ }>;
67
+ export interface CanonicalTextureAtlasRegionV1 {
68
+ name: string;
69
+ content: AtlasRectangleV1;
70
+ gutter: number;
71
+ edgeMode: AtlasEdgeModeV1;
72
+ source: CanonicalTextureAtlasSourceV1;
73
+ crop?: AtlasRectangleV1;
74
+ }
75
+ export interface CanonicalTextureAtlasSpecV1 {
76
+ schemaVersion: 1;
77
+ width: number;
78
+ height: number;
79
+ usage: TextureUsage;
80
+ sampler: CanonicalPortableSamplerV1;
81
+ regions: CanonicalTextureAtlasRegionV1[];
82
+ }
83
+ export declare class TextureAtlasError extends AuthoringDiagnosticError {
84
+ constructor(message: string);
85
+ }
86
+ export declare function atlasFailure(message: string): never;
87
+ export declare function atlasAllocationV1(region: Pick<CanonicalTextureAtlasRegionV1, 'content' | 'gutter'>): AtlasRectangleV1;
88
+ export declare function assertAtlasCropV1(crop: AtlasRectangleV1, content: AtlasRectangleV1, width: number, height: number): void;
89
+ export declare function atlasSourceIdentityV1(source: CanonicalTextureAtlasSourceV1): string;
90
+ export declare function canonicalizeTextureAtlasSpecV1(input: unknown): CanonicalTextureAtlasSpecV1;
91
+ export declare function canonicalTextureAtlasJsonV1(input: unknown): string;
92
+ export declare function hashTextureAtlasSpecV1(input: unknown): `sha256:${string}`;
93
+ /** Internal identity helper for bounded atlas metadata; stable across object property order. */
94
+ export declare function canonicalAtlasMetadataJsonV1(input: unknown): string;
95
+ export declare function atlasLayoutHashV1(spec: CanonicalTextureAtlasSpecV1): `sha256:${string}`;
96
+ /** Pure persisted-manifest reader. Actual PNG/RGBA and recompilation belong to the host. */
97
+ export declare function validateTextureAtlasManifestV1(input: unknown, providedSpec: CanonicalTextureAtlasSpecV1): TextureAtlasManifestV1;
@@ -0,0 +1,414 @@
1
+ /** Browser-safe named pixel regions. Admission never loads or allocates image pixels. */
2
+ import { canonicalizePortableSamplerV1, } from "./portable-sampler.js";
3
+ import { admitProceduralTextureSpecV3, hashProceduralTextureSpecV3, } from "./procedural-texture-v3-contract.js";
4
+ import { sha256Hex } from "./procedural-material-v2.js";
5
+ import { TEXTURE_USAGES } from "./texture-contract.js";
6
+ import { AuthoringDiagnosticError } from "./evaluator/authoring-diagnostic.js";
7
+ export const TEXTURE_ATLAS_COMPILER_ID = 'kiln.texture-atlas.v1.1';
8
+ export const TEXTURE_ATLAS_LIMITS = Object.freeze({
9
+ edge: 1024,
10
+ outputTexels: 1_048_576,
11
+ regions: 64,
12
+ sources: 16,
13
+ sourceTexels: 4_194_304,
14
+ encodedSourceBytes: 8_388_608,
15
+ encodedBytes: 16_777_216,
16
+ jsonBytes: 262_144,
17
+ work: 64_000_000,
18
+ allocatedBytes: 67_108_864,
19
+ uvVertices: 262_144,
20
+ uvAttributeBytes: 134_217_728,
21
+ });
22
+ export class TextureAtlasError extends AuthoringDiagnosticError {
23
+ constructor(message) {
24
+ super('TEXTURE_ATLAS_CONTRACT', `textureAtlas: ${message}`);
25
+ this.name = 'TextureAtlasError';
26
+ }
27
+ }
28
+ export function atlasFailure(message) {
29
+ throw new TextureAtlasError(message);
30
+ }
31
+ function preflight(input) {
32
+ let nodes = 0;
33
+ let bytes = 0;
34
+ const encoder = new TextEncoder();
35
+ const addBytes = (count) => {
36
+ bytes += count;
37
+ if (bytes > TEXTURE_ATLAS_LIMITS.jsonBytes)
38
+ atlasFailure('JSON byte budget exceeded.');
39
+ };
40
+ const visit = (value, depth) => {
41
+ if (++nodes > 65_536 || depth > 16)
42
+ atlasFailure('JSON node/depth budget exceeded.');
43
+ if (value === null || typeof value === 'boolean') {
44
+ addBytes(value === false ? 5 : 4);
45
+ return;
46
+ }
47
+ if (typeof value === 'number') {
48
+ if (!Number.isFinite(value))
49
+ atlasFailure('JSON numbers must be finite.');
50
+ addBytes(JSON.stringify(value).length);
51
+ return;
52
+ }
53
+ if (typeof value === 'string') {
54
+ if (value.length > 4096)
55
+ atlasFailure('JSON string budget exceeded.');
56
+ addBytes(encoder.encode(JSON.stringify(value)).length);
57
+ return;
58
+ }
59
+ if (typeof value !== 'object')
60
+ atlasFailure('plain JSON data is required.');
61
+ const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
62
+ if (array ? proto !== Array.prototype : proto !== null && proto !== Object.prototype)
63
+ atlasFailure('plain JSON prototypes are required.');
64
+ const length = array ? Object.getOwnPropertyDescriptor(value, 'length')?.value : undefined;
65
+ if (array && (!Number.isSafeInteger(length) || length > 1024))
66
+ atlasFailure('JSON array budget exceeded.');
67
+ const keys = Reflect.ownKeys(value);
68
+ if (keys.length > 1024 || (array && keys.length !== length + 1))
69
+ atlasFailure('dense bounded JSON fields are required.');
70
+ addBytes(2 + Math.max(0, keys.length - (array ? 1 : 0) - 1));
71
+ for (const key of keys) {
72
+ if (array && key === 'length')
73
+ continue;
74
+ if (typeof key !== 'string' || ['__proto__', 'prototype', 'constructor'].includes(key))
75
+ atlasFailure('forbidden JSON field.');
76
+ if (key.length > 4096)
77
+ atlasFailure('JSON field name budget exceeded.');
78
+ if (!array)
79
+ addBytes(encoder.encode(JSON.stringify(key)).length + 1);
80
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
81
+ if (!descriptor.enumerable || !('value' in descriptor))
82
+ atlasFailure('JSON accessors/hidden fields are unsupported.');
83
+ if (array && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= length))
84
+ atlasFailure('invalid JSON array key.');
85
+ visit(descriptor.value, depth + 1);
86
+ }
87
+ };
88
+ visit(input, 0);
89
+ }
90
+ function record(value, fields, label) {
91
+ if (!value || typeof value !== 'object' || Array.isArray(value))
92
+ atlasFailure(`${label} must be a JSON object.`);
93
+ const r = value;
94
+ if (Object.keys(r).some((k) => !fields.includes(k)))
95
+ atlasFailure(`${label} has an unknown field.`);
96
+ return r;
97
+ }
98
+ function integer(value, min, max, label) {
99
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min || value > max)
100
+ atlasFailure(`${label} must be an integer ${min}..${max}.`);
101
+ return value === 0 ? 0 : value;
102
+ }
103
+ function hash(value, label) {
104
+ if (typeof value !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(value))
105
+ atlasFailure(`${label} must be an exact SHA-256 hash.`);
106
+ return value;
107
+ }
108
+ function rect(value, label) {
109
+ const r = record(value, ['x', 'y', 'width', 'height'], label);
110
+ return {
111
+ x: integer(r['x'], 0, 4096, `${label}.x`),
112
+ y: integer(r['y'], 0, 4096, `${label}.y`),
113
+ width: integer(r['width'], 1, 4096, `${label}.width`),
114
+ height: integer(r['height'], 1, 4096, `${label}.height`),
115
+ };
116
+ }
117
+ export function atlasAllocationV1(region) {
118
+ const { content: c, gutter: g } = region;
119
+ return { x: c.x - g, y: c.y - g, width: c.width + 2 * g, height: c.height + 2 * g };
120
+ }
121
+ export function assertAtlasCropV1(crop, content, width, height) {
122
+ if (crop.x + crop.width > width || crop.y + crop.height > height)
123
+ atlasFailure('source crop is out of bounds.');
124
+ if (crop.width !== content.width || crop.height !== content.height)
125
+ atlasFailure('source crop/content dimensions must match; resampling is unsupported.');
126
+ }
127
+ export function atlasSourceIdentityV1(source) {
128
+ return source.kind === 'procedural'
129
+ ? `procedural:${hashProceduralTextureSpecV3(source.spec)}`
130
+ : `resource:${source.pin.resourceId}:${source.pin.revisionId}:${source.resourceId}`;
131
+ }
132
+ export function canonicalizeTextureAtlasSpecV1(input) {
133
+ preflight(input);
134
+ const r = record(input, ['schemaVersion', 'width', 'height', 'usage', 'sampler', 'regions'], 'atlas');
135
+ if (r['schemaVersion'] !== 1)
136
+ atlasFailure('schemaVersion must be 1.');
137
+ const width = integer(r['width'], 4, TEXTURE_ATLAS_LIMITS.edge, 'width'), height = integer(r['height'], 4, TEXTURE_ATLAS_LIMITS.edge, 'height');
138
+ if (!TEXTURE_USAGES.includes(r['usage']))
139
+ atlasFailure('usage must be a texture usage.');
140
+ const usage = r['usage'];
141
+ if (!Array.isArray(r['regions']) ||
142
+ !r['regions'].length ||
143
+ r['regions'].length > TEXTURE_ATLAS_LIMITS.regions)
144
+ atlasFailure('regions must contain 1..64 entries.');
145
+ const regions = r['regions']
146
+ .map((value, i) => {
147
+ const q = record(value, ['name', 'content', 'gutter', 'edgeMode', 'source', 'crop'], `regions[${i}]`);
148
+ if (typeof q['name'] !== 'string' || !/^[a-z][a-z0-9_-]{0,79}$/.test(q['name']))
149
+ atlasFailure('region name must be a stable lowercase identifier.');
150
+ const content = rect(q['content'], 'content'), gutter = q['gutter'] === undefined ? 2 : integer(q['gutter'], 0, 32, 'gutter');
151
+ const edgeMode = q['edgeMode'] === undefined ? 'copy-edge' : q['edgeMode'];
152
+ if (!['copy-edge', 'mask-rgb-dilate'].includes(edgeMode))
153
+ atlasFailure('unsupported edgeMode.');
154
+ if (edgeMode === 'mask-rgb-dilate' && usage !== 'albedo' && usage !== 'emissive')
155
+ atlasFailure('mask RGB dilation requires color usage.');
156
+ const s = record(q['source'], ['kind', 'spec', 'resourceId', 'pin', 'expected'], 'source');
157
+ let source;
158
+ let crop = q['crop'] === undefined ? undefined : rect(q['crop'], 'crop');
159
+ if (s['kind'] === 'procedural') {
160
+ record(s, ['kind', 'spec'], 'procedural source');
161
+ const admission = admitProceduralTextureSpecV3(s['spec']);
162
+ if (admission.spec.usage !== usage)
163
+ atlasFailure('source usage does not match atlas usage.');
164
+ source = { kind: 'procedural', spec: admission.spec };
165
+ crop ??= { x: 0, y: 0, width: admission.spec.width, height: admission.spec.height };
166
+ assertAtlasCropV1(crop, content, admission.spec.width, admission.spec.height);
167
+ }
168
+ else if (s['kind'] === 'resource') {
169
+ record(s, ['kind', 'resourceId', 'pin', 'expected'], 'resource source');
170
+ if (typeof s['resourceId'] !== 'string' ||
171
+ !/^[a-zA-Z0-9][a-zA-Z0-9_.:/-]{0,199}$/.test(s['resourceId']))
172
+ atlasFailure('source resourceId must be an exact returned map ID.');
173
+ const p = record(s['pin'], ['resourceId', 'revisionId', 'sha256'], 'source pin');
174
+ if (typeof p['resourceId'] !== 'string' || !/^[a-z][a-z0-9_-]{0,79}$/.test(p['resourceId']))
175
+ atlasFailure('pin resourceId must be a material ID.');
176
+ const revisionId = hash(p['revisionId'], 'pin revisionId'), sha256 = hash(p['sha256'], 'pin sha256');
177
+ if (revisionId !== sha256)
178
+ atlasFailure('source pin revision/hash mismatch.');
179
+ let expected;
180
+ if (s['expected'] !== undefined) {
181
+ const e = record(s['expected'], ['width', 'height', 'imageSha256', 'decodedPixelHash'], 'source expected');
182
+ expected = {
183
+ ...(e['width'] === undefined
184
+ ? {}
185
+ : { width: integer(e['width'], 1, 4096, 'expected.width') }),
186
+ ...(e['height'] === undefined
187
+ ? {}
188
+ : { height: integer(e['height'], 1, 4096, 'expected.height') }),
189
+ ...(e['imageSha256'] === undefined
190
+ ? {}
191
+ : { imageSha256: hash(e['imageSha256'], 'expected.imageSha256') }),
192
+ ...(e['decodedPixelHash'] === undefined
193
+ ? {}
194
+ : { decodedPixelHash: hash(e['decodedPixelHash'], 'expected.decodedPixelHash') }),
195
+ };
196
+ }
197
+ source = {
198
+ kind: 'resource',
199
+ resourceId: s['resourceId'],
200
+ pin: { resourceId: p['resourceId'], revisionId, sha256 },
201
+ ...(expected ? { expected } : {}),
202
+ };
203
+ if (crop && (crop.width !== content.width || crop.height !== content.height))
204
+ atlasFailure('source crop/content dimensions must match; resampling is unsupported.');
205
+ }
206
+ else
207
+ atlasFailure('source kind must be procedural or pinned resource.');
208
+ const result = {
209
+ name: q['name'],
210
+ content,
211
+ gutter,
212
+ edgeMode: edgeMode,
213
+ source,
214
+ ...(crop ? { crop } : {}),
215
+ };
216
+ const allocation = atlasAllocationV1(result);
217
+ if (allocation.x < 0 ||
218
+ allocation.y < 0 ||
219
+ allocation.x + allocation.width > width ||
220
+ allocation.y + allocation.height > height)
221
+ atlasFailure('padded allocation is out of bounds.');
222
+ return result;
223
+ })
224
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
225
+ if (new Set(regions.map((v) => v.name)).size !== regions.length)
226
+ atlasFailure('duplicate region name.');
227
+ for (let i = 0; i < regions.length; i++)
228
+ for (let j = i + 1; j < regions.length; j++) {
229
+ const a = atlasAllocationV1(regions[i]), b = atlasAllocationV1(regions[j]);
230
+ if (a.x < b.x + b.width &&
231
+ b.x < a.x + a.width &&
232
+ a.y < b.y + b.height &&
233
+ b.y < a.y + a.height)
234
+ atlasFailure(`padded allocations overlap: ${regions[i].name}, ${regions[j].name}.`);
235
+ }
236
+ if (new Set(regions.map((v) => atlasSourceIdentityV1(v.source))).size > TEXTURE_ATLAS_LIMITS.sources)
237
+ atlasFailure('distinct source budget exceeds 16.');
238
+ let sampler;
239
+ try {
240
+ sampler = canonicalizePortableSamplerV1(r['sampler'] === undefined
241
+ ? {
242
+ minFilter: 'linear-mipmap-linear',
243
+ magFilter: 'linear',
244
+ wrapS: 'clamp-to-edge',
245
+ wrapT: 'clamp-to-edge',
246
+ }
247
+ : r['sampler']);
248
+ }
249
+ catch (e) {
250
+ atlasFailure(`sampler: ${e.message}`);
251
+ }
252
+ const spec = {
253
+ schemaVersion: 1,
254
+ width,
255
+ height,
256
+ usage,
257
+ sampler,
258
+ regions,
259
+ };
260
+ if (new TextEncoder().encode(JSON.stringify(spec)).length > TEXTURE_ATLAS_LIMITS.jsonBytes)
261
+ atlasFailure('canonical JSON byte budget exceeded.');
262
+ return spec;
263
+ }
264
+ export function canonicalTextureAtlasJsonV1(input) {
265
+ return JSON.stringify(canonicalizeTextureAtlasSpecV1(input));
266
+ }
267
+ export function hashTextureAtlasSpecV1(input) {
268
+ return `sha256:${sha256Hex(`${TEXTURE_ATLAS_COMPILER_ID}\n${canonicalTextureAtlasJsonV1(input)}`)}`;
269
+ }
270
+ /** Internal identity helper for bounded atlas metadata; stable across object property order. */
271
+ export function canonicalAtlasMetadataJsonV1(input) {
272
+ preflight(input);
273
+ const sort = (value) => Array.isArray(value)
274
+ ? value.map(sort)
275
+ : value !== null && typeof value === 'object'
276
+ ? Object.fromEntries(Object.entries(value)
277
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
278
+ .map(([k, v]) => [k, sort(v)]))
279
+ : value;
280
+ return JSON.stringify(sort(input));
281
+ }
282
+ export function atlasLayoutHashV1(spec) {
283
+ return `sha256:${sha256Hex(`kiln.atlas-layout.v1\n${canonicalAtlasMetadataJsonV1({ width: spec.width, height: spec.height, origin: 'top-left', regions: spec.regions.map((r) => ({ name: r.name, content: r.content, allocation: atlasAllocationV1(r) })) })}`)}`;
284
+ }
285
+ /** Pure persisted-manifest reader. Actual PNG/RGBA and recompilation belong to the host. */
286
+ export function validateTextureAtlasManifestV1(input, providedSpec) {
287
+ preflight(input);
288
+ const spec = canonicalizeTextureAtlasSpecV1(providedSpec);
289
+ const r = record(input, [
290
+ 'version',
291
+ 'compilerId',
292
+ 'width',
293
+ 'height',
294
+ 'usage',
295
+ 'origin',
296
+ 'sampler',
297
+ 'specHash',
298
+ 'layoutHash',
299
+ 'decodedPixelHash',
300
+ 'atlasPixelHash',
301
+ 'sources',
302
+ 'regions',
303
+ 'manifestHash',
304
+ ], 'manifest');
305
+ if (r['version'] !== 'kiln.atlas-manifest.v1' ||
306
+ r['compilerId'] !== TEXTURE_ATLAS_COMPILER_ID ||
307
+ r['width'] !== spec.width ||
308
+ r['height'] !== spec.height ||
309
+ r['usage'] !== spec.usage ||
310
+ r['origin'] !== 'top-left')
311
+ atlasFailure('manifest version/compiler/dimensions/usage/origin mismatch.');
312
+ if (canonicalAtlasMetadataJsonV1(r['sampler']) !== canonicalAtlasMetadataJsonV1(spec.sampler) ||
313
+ r['specHash'] !== hashTextureAtlasSpecV1(spec) ||
314
+ r['layoutHash'] !== atlasLayoutHashV1(spec))
315
+ atlasFailure('manifest sampler/spec/layout identity mismatch.');
316
+ hash(r['decodedPixelHash'], 'manifest decodedPixelHash');
317
+ hash(r['atlasPixelHash'], 'manifest atlasPixelHash');
318
+ const expectedSources = new Map(spec.regions.map((region) => [atlasSourceIdentityV1(region.source), region.source]));
319
+ if (!Array.isArray(r['sources']) ||
320
+ r['sources'].length !== expectedSources.size ||
321
+ !Array.isArray(r['regions']) ||
322
+ r['regions'].length !== spec.regions.length)
323
+ atlasFailure('manifest source/region inventory mismatch.');
324
+ const dimensions = new Map();
325
+ for (const sourceValue of r['sources']) {
326
+ const s = record(sourceValue, [
327
+ 'id',
328
+ 'kind',
329
+ 'width',
330
+ 'height',
331
+ 'decodedPixelHash',
332
+ 'recipeHash',
333
+ 'compilerId',
334
+ 'descriptor',
335
+ ], 'manifest source');
336
+ if (typeof s['id'] !== 'string' || dimensions.has(s['id']))
337
+ atlasFailure('duplicate/invalid manifest source identity.');
338
+ const source = expectedSources.get(s['id']);
339
+ if (!source || s['kind'] !== source.kind)
340
+ atlasFailure('manifest source identity mismatch.');
341
+ const width = integer(s['width'], 1, 4096, 'manifest source width'), height = integer(s['height'], 1, 4096, 'manifest source height');
342
+ hash(s['decodedPixelHash'], 'manifest source decodedPixelHash');
343
+ if (source.kind === 'procedural') {
344
+ record(s, ['id', 'kind', 'width', 'height', 'decodedPixelHash', 'recipeHash', 'compilerId'], 'manifest procedural source');
345
+ if (width !== source.spec.width ||
346
+ height !== source.spec.height ||
347
+ s['recipeHash'] !== hashProceduralTextureSpecV3(source.spec) ||
348
+ s['compilerId'] !== 'kiln.procedural-rgba.v3.1')
349
+ atlasFailure('manifest procedural source identity mismatch.');
350
+ }
351
+ else {
352
+ record(s, ['id', 'kind', 'width', 'height', 'decodedPixelHash', 'descriptor'], 'manifest resource source');
353
+ const d = record(s['descriptor'], [
354
+ 'materialId',
355
+ 'revisionId',
356
+ 'resourceId',
357
+ 'slot',
358
+ 'usage',
359
+ 'colorSpace',
360
+ 'width',
361
+ 'height',
362
+ 'encodedByteLength',
363
+ 'imageSha256',
364
+ 'decodedPixelHash',
365
+ ], 'manifest source descriptor');
366
+ const slots = {
367
+ baseColor: 'albedo',
368
+ normal: 'normal',
369
+ metallicRoughness: 'metallicRoughness',
370
+ emissive: 'emissive',
371
+ occlusion: 'occlusion',
372
+ };
373
+ if (d['materialId'] !== source.pin.resourceId ||
374
+ d['revisionId'] !== source.pin.revisionId ||
375
+ d['resourceId'] !== source.resourceId ||
376
+ d['width'] !== width ||
377
+ d['height'] !== height ||
378
+ d['usage'] !== spec.usage ||
379
+ typeof d['slot'] !== 'string' ||
380
+ slots[d['slot']] !== spec.usage ||
381
+ d['colorSpace'] !==
382
+ (spec.usage === 'albedo' || spec.usage === 'emissive' ? 'srgb' : 'linear'))
383
+ atlasFailure('manifest pinned resource descriptor mismatch.');
384
+ hash(d['imageSha256'], 'manifest source imageSha256');
385
+ integer(d['encodedByteLength'], 1, TEXTURE_ATLAS_LIMITS.encodedSourceBytes, 'manifest encodedByteLength');
386
+ if (d['decodedPixelHash'] !== undefined && d['decodedPixelHash'] !== s['decodedPixelHash'])
387
+ atlasFailure('manifest source decodedPixelHash mismatch.');
388
+ for (const key of ['width', 'height', 'imageSha256', 'decodedPixelHash']) {
389
+ const expected = source.expected?.[key];
390
+ if (expected !== undefined && expected !== (key === 'decodedPixelHash' ? s[key] : d[key]))
391
+ atlasFailure('manifest source expectation mismatch.');
392
+ }
393
+ }
394
+ dimensions.set(s['id'], { width, height });
395
+ }
396
+ for (let i = 0; i < spec.regions.length; i++) {
397
+ const region = spec.regions[i], v = record(r['regions'][i], ['name', 'content', 'allocation', 'crop', 'sourceId'], 'manifest region');
398
+ const sourceId = atlasSourceIdentityV1(region.source), size = dimensions.get(sourceId);
399
+ const crop = region.crop ?? { x: 0, y: 0, width: size.width, height: size.height };
400
+ assertAtlasCropV1(crop, region.content, size.width, size.height);
401
+ if (v['name'] !== region.name ||
402
+ v['sourceId'] !== sourceId ||
403
+ canonicalAtlasMetadataJsonV1(v['content']) !== canonicalAtlasMetadataJsonV1(region.content) ||
404
+ canonicalAtlasMetadataJsonV1(v['allocation']) !==
405
+ canonicalAtlasMetadataJsonV1(atlasAllocationV1(region)) ||
406
+ canonicalAtlasMetadataJsonV1(v['crop']) !== canonicalAtlasMetadataJsonV1(crop))
407
+ atlasFailure('manifest named region/crop/allocation identity mismatch.');
408
+ }
409
+ const { manifestHash, ...payload } = r;
410
+ if (manifestHash !==
411
+ `sha256:${sha256Hex(`kiln.atlas-manifest.v1\n${canonicalAtlasMetadataJsonV1(payload)}`)}`)
412
+ atlasFailure('manifest hash mismatch.');
413
+ return structuredClone(input);
414
+ }