@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,73 @@
1
+ import { type ProceduralTextureAdmissionV3 } from "./procedural-texture-v3-contract.js";
2
+ import type { PinnedMaterialMapPortV1, VerifiedPinnedMaterialMapDescriptorV1 } from "./pinned-material-map.js";
3
+ import { TEXTURE_ATLAS_COMPILER_ID, type AtlasRectangleV1, type CanonicalTextureAtlasSourceV1, type CanonicalTextureAtlasSpecV1 } from "./texture-atlas-contract.js";
4
+ type Hash = `sha256:${string}`;
5
+ export interface TextureAtlasCompileOptionsV1 {
6
+ resources?: PinnedMaterialMapPortV1;
7
+ signal?: AbortSignal;
8
+ }
9
+ export interface TextureAtlasBudgetV1 {
10
+ sourceTexels: number;
11
+ encodedBytes: number;
12
+ workUnits: number;
13
+ conservativePixelAllocationBytes: number;
14
+ dilationScratchBytes: number;
15
+ }
16
+ interface PreparedSource {
17
+ id: string;
18
+ source: CanonicalTextureAtlasSourceV1;
19
+ width: number;
20
+ height: number;
21
+ descriptor?: VerifiedPinnedMaterialMapDescriptorV1;
22
+ admission?: ProceduralTextureAdmissionV3;
23
+ }
24
+ export interface TextureAtlasAdmissionV1 {
25
+ spec: CanonicalTextureAtlasSpecV1;
26
+ sources: PreparedSource[];
27
+ crops: Map<string, AtlasRectangleV1>;
28
+ budget: TextureAtlasBudgetV1;
29
+ }
30
+ export interface TextureAtlasSourceManifestV1 {
31
+ id: string;
32
+ kind: 'procedural' | 'resource';
33
+ width: number;
34
+ height: number;
35
+ decodedPixelHash: Hash;
36
+ recipeHash?: Hash;
37
+ compilerId?: string;
38
+ descriptor?: VerifiedPinnedMaterialMapDescriptorV1;
39
+ }
40
+ export interface TextureAtlasRegionManifestV1 {
41
+ name: string;
42
+ content: AtlasRectangleV1;
43
+ allocation: AtlasRectangleV1;
44
+ crop: AtlasRectangleV1;
45
+ sourceId: string;
46
+ }
47
+ export interface TextureAtlasManifestV1 {
48
+ version: 'kiln.atlas-manifest.v1';
49
+ compilerId: typeof TEXTURE_ATLAS_COMPILER_ID;
50
+ width: number;
51
+ height: number;
52
+ usage: CanonicalTextureAtlasSpecV1['usage'];
53
+ origin: 'top-left';
54
+ sampler: CanonicalTextureAtlasSpecV1['sampler'];
55
+ specHash: Hash;
56
+ layoutHash: Hash;
57
+ decodedPixelHash: Hash;
58
+ atlasPixelHash: Hash;
59
+ sources: TextureAtlasSourceManifestV1[];
60
+ regions: TextureAtlasRegionManifestV1[];
61
+ manifestHash: Hash;
62
+ }
63
+ export interface CompiledTextureAtlasV1 {
64
+ spec: CanonicalTextureAtlasSpecV1;
65
+ canonicalJson: string;
66
+ pixels: Uint8Array;
67
+ manifest: TextureAtlasManifestV1;
68
+ budget: TextureAtlasBudgetV1;
69
+ }
70
+ /** Admit all sources and aggregate work/buffer allocations before any source pixel load. */
71
+ export declare function admitTextureAtlasSpecV1(input: unknown, options?: TextureAtlasCompileOptionsV1): TextureAtlasAdmissionV1;
72
+ export declare function compileTextureAtlasSpecV1(input: unknown, options?: TextureAtlasCompileOptionsV1): Promise<CompiledTextureAtlasV1>;
73
+ export {};
@@ -0,0 +1,290 @@
1
+ /** Deterministic fixed-scale composition. Resource loading is a trusted host capability. */
2
+ import { compileProceduralTextureSpecV3 } from "./procedural-texture-v3.js";
3
+ import { admitProceduralTextureSpecV3, hashProceduralTextureSpecV3, PROCEDURAL_TEXTURE_V3_COMPILER_ID, } from "./procedural-texture-v3-contract.js";
4
+ import { sha256Hex } from "./procedural-material-v2.js";
5
+ import { atlasAllocationV1, atlasFailure, atlasLayoutHashV1, atlasSourceIdentityV1, assertAtlasCropV1, canonicalizeTextureAtlasSpecV1, canonicalAtlasMetadataJsonV1, hashTextureAtlasSpecV1, TEXTURE_ATLAS_COMPILER_ID, TEXTURE_ATLAS_LIMITS, } from "./texture-atlas-contract.js";
6
+ const isHash = (value) => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
7
+ const boundedInteger = (value, max) => Number.isSafeInteger(value) && value >= 1 && value <= max;
8
+ function checkDescriptor(d, source, usage) {
9
+ const slots = {
10
+ baseColor: 'albedo',
11
+ normal: 'normal',
12
+ metallicRoughness: 'metallicRoughness',
13
+ emissive: 'emissive',
14
+ occlusion: 'occlusion',
15
+ };
16
+ if (d.materialId !== source.pin.resourceId ||
17
+ d.revisionId !== source.pin.revisionId ||
18
+ d.resourceId !== source.resourceId ||
19
+ !isHash(d.revisionId) ||
20
+ !isHash(d.imageSha256))
21
+ atlasFailure('pinned source descriptor identity/hash mismatch.');
22
+ if (d.usage !== usage ||
23
+ slots[d.slot] !== usage ||
24
+ d.colorSpace !== (usage === 'albedo' || usage === 'emissive' ? 'srgb' : 'linear'))
25
+ atlasFailure('pinned source usage/slot/colorSpace mismatch.');
26
+ if (!boundedInteger(d.width, 4096) ||
27
+ !boundedInteger(d.height, 4096) ||
28
+ !boundedInteger(d.encodedByteLength, TEXTURE_ATLAS_LIMITS.encodedSourceBytes) ||
29
+ (d.decodedPixelHash !== undefined && !isHash(d.decodedPixelHash)))
30
+ atlasFailure('pinned source dimensions/byte/hash budget is invalid.');
31
+ for (const key of ['width', 'height', 'imageSha256', 'decodedPixelHash']) {
32
+ const expected = source.expected?.[key];
33
+ if (expected !== undefined && d[key] !== undefined && expected !== d[key])
34
+ atlasFailure(`source expected ${key} does not match verified descriptor.`);
35
+ }
36
+ }
37
+ /** Admit all sources and aggregate work/buffer allocations before any source pixel load. */
38
+ export function admitTextureAtlasSpecV1(input, options = {}) {
39
+ const spec = canonicalizeTextureAtlasSpecV1(input), unique = new Map(), crops = new Map();
40
+ const outputTexels = spec.width * spec.height;
41
+ // Include output and two output-sized hash snapshots/domain concatenation buffers.
42
+ const budget = {
43
+ sourceTexels: 0,
44
+ encodedBytes: 0,
45
+ workUnits: outputTexels * 13,
46
+ conservativePixelAllocationBytes: outputTexels * 12,
47
+ dilationScratchBytes: 0,
48
+ };
49
+ for (const region of spec.regions) {
50
+ const id = atlasSourceIdentityV1(region.source);
51
+ let prepared = unique.get(id);
52
+ if (!prepared) {
53
+ if (region.source.kind === 'procedural') {
54
+ const admission = admitProceduralTextureSpecV3(region.source.spec);
55
+ prepared = {
56
+ id,
57
+ source: region.source,
58
+ width: admission.spec.width,
59
+ height: admission.spec.height,
60
+ admission,
61
+ };
62
+ budget.workUnits += admission.estimatedWorkUnits;
63
+ budget.conservativePixelAllocationBytes += admission.conservativePixelAllocationBytes;
64
+ }
65
+ else {
66
+ const described = options.resources?.describePinnedMaterialMap(region.source.resourceId, region.source.pin);
67
+ if (!described)
68
+ atlasFailure(`source ${region.name} requires a verified pinned library map. Get its exact map ID with kiln_material get and supply materialDependencies or project pins; approved resources are unsupported for atlas sources.`);
69
+ checkDescriptor(described, region.source, spec.usage);
70
+ // Snapshot verified metadata before an async loader can mutate its own object.
71
+ const descriptor = structuredClone(described);
72
+ prepared = {
73
+ id,
74
+ source: region.source,
75
+ width: descriptor.width,
76
+ height: descriptor.height,
77
+ descriptor,
78
+ };
79
+ budget.encodedBytes += descriptor.encodedByteLength;
80
+ // Returned owned pixels + our immutable snapshot + decoder/copy allowance + encoded input.
81
+ budget.conservativePixelAllocationBytes +=
82
+ descriptor.width * descriptor.height * 12 + descriptor.encodedByteLength;
83
+ }
84
+ const texels = prepared.width * prepared.height;
85
+ budget.sourceTexels += texels;
86
+ budget.workUnits += texels * 8;
87
+ budget.conservativePixelAllocationBytes += texels * 4; // Raw-pixel hash snapshot allowance.
88
+ unique.set(id, prepared);
89
+ }
90
+ if (region.source.kind === 'resource')
91
+ checkDescriptor(prepared.descriptor, region.source, spec.usage);
92
+ const crop = region.crop ?? { x: 0, y: 0, width: prepared.width, height: prepared.height };
93
+ assertAtlasCropV1(crop, region.content, prepared.width, prepared.height);
94
+ crops.set(region.name, crop);
95
+ const allocation = atlasAllocationV1(region), texels = allocation.width * allocation.height;
96
+ budget.workUnits += texels * (region.edgeMode === 'mask-rgb-dilate' ? 16 : 2);
97
+ if (region.edgeMode === 'mask-rgb-dilate')
98
+ budget.dilationScratchBytes = Math.max(budget.dilationScratchBytes, texels * 8);
99
+ }
100
+ budget.workUnits += (spec.regions.length * (spec.regions.length - 1)) / 2;
101
+ budget.conservativePixelAllocationBytes += budget.dilationScratchBytes;
102
+ if (budget.sourceTexels > TEXTURE_ATLAS_LIMITS.sourceTexels)
103
+ atlasFailure('full decoded source texel budget exceeded; a small crop does not reduce source decode cost.');
104
+ if (budget.encodedBytes > TEXTURE_ATLAS_LIMITS.encodedBytes)
105
+ atlasFailure('aggregate encoded source byte budget exceeded.');
106
+ if (budget.workUnits > TEXTURE_ATLAS_LIMITS.work)
107
+ atlasFailure(`aggregate work budget ${budget.workUnits} exceeds ${TEXTURE_ATLAS_LIMITS.work}.`);
108
+ if (budget.conservativePixelAllocationBytes > TEXTURE_ATLAS_LIMITS.allocatedBytes)
109
+ atlasFailure(`aggregate pixel allocation budget ${budget.conservativePixelAllocationBytes} exceeds ${TEXTURE_ATLAS_LIMITS.allocatedBytes}.`);
110
+ return {
111
+ spec,
112
+ sources: [...unique.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
113
+ crops,
114
+ budget,
115
+ };
116
+ }
117
+ async function pixelDigest(bytes) {
118
+ // Compiler/host snapshots are ArrayBuffer-backed, never shared mutable cache state.
119
+ const result = await crypto.subtle.digest('SHA-256', bytes);
120
+ return `sha256:${[...new Uint8Array(result)].map((v) => v.toString(16).padStart(2, '0')).join('')}`;
121
+ }
122
+ function cancelled(signal) {
123
+ if (signal?.aborted)
124
+ atlasFailure('compilation cancelled.');
125
+ }
126
+ /** Copy exact crop and fill only this region's disjoint allocation. Scratch is reused. */
127
+ function composeRegion(output, atlasWidth, region, crop, source, sourceWidth, distances, donors) {
128
+ const allocation = atlasAllocationV1(region), w = allocation.width, h = allocation.height, g = region.gutter;
129
+ const sourcePixel = (cx, cy) => ((crop.y + cy) * sourceWidth + crop.x + cx) * 4;
130
+ const destination = (x, y) => ((allocation.y + y) * atlasWidth + allocation.x + x) * 4;
131
+ if (region.edgeMode === 'copy-edge') {
132
+ for (let y = 0; y < h; y++)
133
+ for (let x = 0; x < w; x++) {
134
+ const s = sourcePixel(Math.max(0, Math.min(crop.width - 1, x - g)), Math.max(0, Math.min(crop.height - 1, y - g))), d = destination(x, y);
135
+ for (let c = 0; c < 4; c++)
136
+ output[d + c] = source[s + c];
137
+ }
138
+ return;
139
+ }
140
+ const length = w * h, infinity = 0x3fffffff;
141
+ distances.fill(infinity, 0, length);
142
+ donors.fill(infinity, 0, length);
143
+ for (let cy = 0; cy < crop.height; cy++)
144
+ for (let cx = 0; cx < crop.width; cx++) {
145
+ const s = sourcePixel(cx, cy), d = destination(cx + g, cy + g), index = (cy + g) * w + cx + g;
146
+ for (let c = 0; c < 4; c++)
147
+ output[d + c] = source[s + c];
148
+ if (source[s + 3] > 0) {
149
+ distances[index] = 0;
150
+ donors[index] = cy * crop.width + cx;
151
+ }
152
+ }
153
+ const consider = (i, neighbor) => {
154
+ const distance = distances[neighbor] + 1;
155
+ if (distance < distances[i] ||
156
+ (distance === distances[i] && donors[neighbor] < donors[i])) {
157
+ distances[i] = distance;
158
+ donors[i] = donors[neighbor];
159
+ }
160
+ };
161
+ for (let y = 0; y < h; y++)
162
+ for (let x = 0; x < w; x++) {
163
+ const i = y * w + x;
164
+ if (x)
165
+ consider(i, i - 1);
166
+ if (y)
167
+ consider(i, i - w);
168
+ }
169
+ for (let y = h - 1; y >= 0; y--)
170
+ for (let x = w - 1; x >= 0; x--) {
171
+ const i = y * w + x;
172
+ if (x + 1 < w)
173
+ consider(i, i + 1);
174
+ if (y + 1 < h)
175
+ consider(i, i + w);
176
+ }
177
+ for (let y = 0; y < h; y++)
178
+ for (let x = 0; x < w; x++) {
179
+ const d = destination(x, y);
180
+ if (output[d + 3] > 0)
181
+ continue;
182
+ const donor = donors[y * w + x];
183
+ if (donor === infinity) {
184
+ output[d] = 0;
185
+ output[d + 1] = 0;
186
+ output[d + 2] = 0;
187
+ }
188
+ else {
189
+ const s = sourcePixel(donor % crop.width, Math.floor(donor / crop.width));
190
+ for (let c = 0; c < 3; c++)
191
+ output[d + c] = source[s + c];
192
+ }
193
+ }
194
+ }
195
+ export async function compileTextureAtlasSpecV1(input, options = {}) {
196
+ const admission = admitTextureAtlasSpecV1(input, options), { spec, sources, crops, budget } = admission;
197
+ cancelled(options.signal);
198
+ const sourcePixels = new Map(), sourceManifests = [];
199
+ // Sequential loads avoid multiplying decoder/snapshot peaks; each exact source compiles once.
200
+ for (const prepared of sources) {
201
+ cancelled(options.signal);
202
+ let pixels, manifest;
203
+ if (prepared.source.kind === 'procedural') {
204
+ pixels = compileProceduralTextureSpecV3(prepared.source.spec).pixels;
205
+ manifest = {
206
+ id: prepared.id,
207
+ kind: 'procedural',
208
+ width: prepared.width,
209
+ height: prepared.height,
210
+ decodedPixelHash: await pixelDigest(pixels),
211
+ recipeHash: hashProceduralTextureSpecV3(prepared.source.spec),
212
+ compilerId: PROCEDURAL_TEXTURE_V3_COMPILER_ID,
213
+ };
214
+ }
215
+ else {
216
+ const resolved = await options.resources.resolvePinnedMaterialMapPixels(prepared.source.resourceId, prepared.source.pin, { signal: options.signal });
217
+ checkDescriptor(resolved.descriptor, prepared.source, spec.usage);
218
+ if (canonicalAtlasMetadataJsonV1(resolved.descriptor) !==
219
+ canonicalAtlasMetadataJsonV1(prepared.descriptor))
220
+ atlasFailure('resolved source descriptor changed after admission.');
221
+ if (!(resolved.pixels instanceof Uint8Array) ||
222
+ resolved.pixels.length !== prepared.width * prepared.height * 4)
223
+ atlasFailure('decoded source RGBA dimensions/length mismatch.');
224
+ pixels = Uint8Array.from(resolved.pixels);
225
+ const decodedPixelHash = await pixelDigest(pixels);
226
+ if (decodedPixelHash !== resolved.decodedPixelHash ||
227
+ (prepared.descriptor.decodedPixelHash !== undefined &&
228
+ decodedPixelHash !== prepared.descriptor.decodedPixelHash))
229
+ atlasFailure('decoded source pixel hash mismatch.');
230
+ manifest = {
231
+ id: prepared.id,
232
+ kind: 'resource',
233
+ width: prepared.width,
234
+ height: prepared.height,
235
+ decodedPixelHash,
236
+ descriptor: prepared.descriptor,
237
+ };
238
+ }
239
+ sourcePixels.set(prepared.id, pixels);
240
+ sourceManifests.push(manifest);
241
+ }
242
+ // Compare every reuse's expectations, including an old external source without a descriptor hash.
243
+ for (const region of spec.regions)
244
+ if (region.source.kind === 'resource' &&
245
+ region.source.expected?.decodedPixelHash !== undefined) {
246
+ const actual = sourceManifests.find((v) => v.id === atlasSourceIdentityV1(region.source)).decodedPixelHash;
247
+ if (actual !== region.source.expected.decodedPixelHash)
248
+ atlasFailure('source expected decodedPixelHash mismatch.');
249
+ }
250
+ cancelled(options.signal);
251
+ const pixels = new Uint8Array(spec.width * spec.height * 4), scratchLength = budget.dilationScratchBytes / 8;
252
+ const distances = new Int32Array(scratchLength), donors = new Int32Array(scratchLength);
253
+ for (const region of spec.regions)
254
+ composeRegion(pixels, spec.width, region, crops.get(region.name), sourcePixels.get(atlasSourceIdentityV1(region.source)), sources.find((v) => v.id === atlasSourceIdentityV1(region.source)).width, distances, donors);
255
+ const decodedPixelHash = await pixelDigest(pixels), header = new TextEncoder().encode(`kiln.atlas-pixels.v1\n${spec.width},${spec.height},${spec.usage}\n`);
256
+ const tagged = new Uint8Array(header.length + pixels.length);
257
+ tagged.set(header);
258
+ tagged.set(pixels, header.length);
259
+ const atlasPixelHash = await pixelDigest(tagged);
260
+ const payload = {
261
+ version: 'kiln.atlas-manifest.v1',
262
+ compilerId: TEXTURE_ATLAS_COMPILER_ID,
263
+ width: spec.width,
264
+ height: spec.height,
265
+ usage: spec.usage,
266
+ origin: 'top-left',
267
+ sampler: spec.sampler,
268
+ specHash: hashTextureAtlasSpecV1(spec),
269
+ layoutHash: atlasLayoutHashV1(spec),
270
+ decodedPixelHash,
271
+ atlasPixelHash,
272
+ sources: sourceManifests,
273
+ regions: spec.regions.map((r) => ({
274
+ name: r.name,
275
+ content: r.content,
276
+ allocation: atlasAllocationV1(r),
277
+ crop: crops.get(r.name),
278
+ sourceId: atlasSourceIdentityV1(r.source),
279
+ })),
280
+ };
281
+ const manifestHash = `sha256:${sha256Hex(`kiln.atlas-manifest.v1\n${canonicalAtlasMetadataJsonV1(payload)}`)}`;
282
+ cancelled(options.signal);
283
+ return {
284
+ spec,
285
+ canonicalJson: JSON.stringify(spec),
286
+ pixels,
287
+ manifest: { ...payload, manifestHash },
288
+ budget,
289
+ };
290
+ }
@@ -0,0 +1,10 @@
1
+ /** Three state and affine UV wrapper; host resources never enter authored arguments. */
2
+ import * as THREE from 'three';
3
+ import { type TextureAtlasCompileOptionsV1, type TextureAtlasManifestV1 } from "./texture-atlas-pixels.js";
4
+ /** SDK/host API. Sandbox integration closes over options and exposes exactly one argument. */
5
+ export declare function textureAtlas(input: unknown, options?: TextureAtlasCompileOptionsV1): Promise<THREE.DataTexture>;
6
+ export interface AtlasUVOptionsV1 {
7
+ inputOrigin: 'top-left' | 'bottom-left';
8
+ mode: 'content-boundary' | 'texel-center';
9
+ }
10
+ export declare function remapUVToAtlas(geometry: THREE.BufferGeometry, atlas: THREE.Texture | TextureAtlasManifestV1, name: string, options: AtlasUVOptionsV1): THREE.BufferGeometry;
@@ -0,0 +1,143 @@
1
+ /** Three state and affine UV wrapper; host resources never enter authored arguments. */
2
+ import * as THREE from 'three';
3
+ import { applyPortableSamplerV1 } from "./portable-sampler-runtime.js";
4
+ import { compileTextureAtlasSpecV1, } from "./texture-atlas-pixels.js";
5
+ import { atlasFailure, canonicalAtlasMetadataJsonV1, TEXTURE_ATLAS_COMPILER_ID, TEXTURE_ATLAS_LIMITS, } from "./texture-atlas-contract.js";
6
+ import { sha256Hex } from "./procedural-material-v2.js";
7
+ import { remapUV } from "./uv-shapes.js";
8
+ /** SDK/host API. Sandbox integration closes over options and exposes exactly one argument. */
9
+ export async function textureAtlas(input, options = {}) {
10
+ const compiled = await compileTextureAtlasSpecV1(input, options);
11
+ const texture = new THREE.DataTexture(compiled.pixels, compiled.spec.width, compiled.spec.height, THREE.RGBAFormat, THREE.UnsignedByteType);
12
+ texture.name = 'Atlas';
13
+ texture.flipY = false;
14
+ texture.colorSpace =
15
+ compiled.spec.usage === 'albedo' || compiled.spec.usage === 'emissive'
16
+ ? THREE.SRGBColorSpace
17
+ : THREE.NoColorSpace;
18
+ applyPortableSamplerV1(texture, compiled.spec.sampler);
19
+ texture.userData.kilnAtlas = { spec: compiled.spec, manifest: compiled.manifest };
20
+ texture.userData.kilnTexture = {
21
+ usage: compiled.spec.usage,
22
+ colorSpace: compiled.spec.usage === 'albedo' || compiled.spec.usage === 'emissive' ? 'srgb' : 'linear',
23
+ width: compiled.spec.width,
24
+ height: compiled.spec.height,
25
+ hasAlpha: compiled.pixels.some((v, i) => i % 4 === 3 && v < 255),
26
+ };
27
+ return texture;
28
+ }
29
+ function manifestOf(atlas) {
30
+ const value = atlas?.isTexture
31
+ ? atlas.userData.kilnAtlas?.manifest
32
+ : atlas;
33
+ if (!value || typeof value !== 'object')
34
+ atlasFailure('an atlas texture or compiled manifest is required.');
35
+ canonicalAtlasMetadataJsonV1(value);
36
+ const manifest = value;
37
+ if (manifest.version !== 'kiln.atlas-manifest.v1' ||
38
+ manifest.compilerId !== TEXTURE_ATLAS_COMPILER_ID ||
39
+ manifest.origin !== 'top-left' ||
40
+ !Array.isArray(manifest.regions) ||
41
+ !manifest.regions.length ||
42
+ manifest.regions.length > 64 ||
43
+ !Number.isSafeInteger(manifest.width) ||
44
+ manifest.width < 4 ||
45
+ manifest.width > 1024 ||
46
+ !Number.isSafeInteger(manifest.height) ||
47
+ manifest.height < 4 ||
48
+ manifest.height > 1024)
49
+ atlasFailure('invalid atlas manifest dimensions/version.');
50
+ const { manifestHash, ...payload } = manifest;
51
+ if (`sha256:${sha256Hex(`kiln.atlas-manifest.v1\n${canonicalAtlasMetadataJsonV1(payload)}`)}` !==
52
+ manifestHash)
53
+ atlasFailure('atlas manifest identity mismatch.');
54
+ return manifest;
55
+ }
56
+ export function remapUVToAtlas(geometry, atlas, name, options) {
57
+ const manifest = manifestOf(atlas);
58
+ canonicalAtlasMetadataJsonV1(options);
59
+ if (!options ||
60
+ typeof options !== 'object' ||
61
+ Array.isArray(options) ||
62
+ Object.keys(options).some((k) => !['inputOrigin', 'mode'].includes(k)) ||
63
+ !['top-left', 'bottom-left'].includes(options.inputOrigin) ||
64
+ !['content-boundary', 'texel-center'].includes(options.mode))
65
+ atlasFailure('explicit inputOrigin and content-boundary/texel-center mode are required.');
66
+ const region = manifest.regions.find((r) => r.name === name);
67
+ if (!region)
68
+ atlasFailure(`unknown named region ${String(name)}.`);
69
+ const { x, y, width: w, height: h } = region.content;
70
+ if (![x, y, w, h].every(Number.isSafeInteger) ||
71
+ x < 0 ||
72
+ y < 0 ||
73
+ w < 1 ||
74
+ h < 1 ||
75
+ x + w > manifest.width ||
76
+ y + h > manifest.height)
77
+ atlasFailure('invalid region content rectangle.');
78
+ const uv = geometry?.getAttribute?.('uv'), position = geometry?.getAttribute?.('position');
79
+ if (!position ||
80
+ !uv ||
81
+ uv.itemSize !== 2 ||
82
+ uv.count !== position.count ||
83
+ uv.count < 1 ||
84
+ uv.count > TEXTURE_ATLAS_LIMITS.uvVertices ||
85
+ uv.isInstancedBufferAttribute ||
86
+ uv.data?.isInstancedInterleavedBuffer)
87
+ atlasFailure('matching per-vertex UV0 and positions are required; per-instance UVs are unsupported.');
88
+ let bytes = uv.count * 8;
89
+ const interleavedBuffers = new Set();
90
+ const count = (attribute) => {
91
+ if (attribute.isInterleavedBufferAttribute) {
92
+ // Three's geometry-copy data cache deduplicates interleaved backing buffers.
93
+ const buffer = attribute.array.buffer;
94
+ if (!interleavedBuffers.has(buffer)) {
95
+ interleavedBuffers.add(buffer);
96
+ bytes += buffer.byteLength;
97
+ }
98
+ }
99
+ else {
100
+ // Ordinary attributes/morphs/index clone independently even with shared input arrays.
101
+ // Typed convenience subclasses may allocate in both constructor and copy.
102
+ bytes +=
103
+ attribute.array.byteLength *
104
+ (attribute.constructor === THREE.BufferAttribute ||
105
+ attribute.constructor === THREE.InstancedBufferAttribute
106
+ ? 1
107
+ : 2);
108
+ }
109
+ };
110
+ for (const attr of Object.values(geometry.attributes))
111
+ count(attr);
112
+ for (const attrs of Object.values(geometry.morphAttributes))
113
+ for (const attr of attrs)
114
+ count(attr);
115
+ if (geometry.index)
116
+ count(geometry.index);
117
+ if (bytes > TEXTURE_ATLAS_LIMITS.uvAttributeBytes)
118
+ atlasFailure('owned UV clone attribute allocation budget exceeded.');
119
+ for (let i = 0; i < uv.count; i++)
120
+ if (![uv.getX(i), uv.getY(i)].every((v) => Number.isFinite(v) && v >= 0 && v <= 1))
121
+ atlasFailure('UV0 must be finite in the unit domain [0,1]; region tiling is unsupported.');
122
+ const center = options.mode === 'texel-center', su = (w - (center ? 1 : 0)) / manifest.width, sv = (h - (center ? 1 : 0)) / manifest.height;
123
+ const bottom = options.inputOrigin === 'bottom-left';
124
+ const offset = [
125
+ (x + (center ? 0.5 : 0)) / manifest.width,
126
+ (y + (bottom ? h - (center ? 0.5 : 0) : center ? 0.5 : 0)) / manifest.height,
127
+ ];
128
+ const scale = [su, bottom ? -sv : sv];
129
+ const output = remapUV(geometry, { scale, offset });
130
+ output.userData.kilnAtlasBinding = {
131
+ manifestHash: manifest.manifestHash,
132
+ specHash: manifest.specHash,
133
+ layoutHash: manifest.layoutHash,
134
+ region: name,
135
+ content: structuredClone(region.content),
136
+ allocation: structuredClone(region.allocation),
137
+ inputOrigin: options.inputOrigin,
138
+ mode: options.mode,
139
+ scale,
140
+ offset,
141
+ };
142
+ return output;
143
+ }
@@ -39,6 +39,7 @@
39
39
  * blocks, with a message that now names the procedural fix.
40
40
  */
41
41
  import * as THREE from 'three';
42
+ import { type AtlasReferenceV1 } from "./atlas-provenance.js";
42
43
  import type { TextureUsage } from "./textures.js";
43
44
  export interface BakedTextureBindingV1 {
44
45
  node: string;
@@ -90,6 +91,8 @@ export interface BakedTextureProvenanceV1 {
90
91
  * regenerate the exact bytes. Absent for textures loaded from a file.
91
92
  */
92
93
  procedural?: ProceduralRecipeRecord;
94
+ /** Compact atlas source identity; the export scene table owns its recipe/manifest. */
95
+ atlas?: AtlasReferenceV1;
93
96
  }
94
97
  /** The `userData.kilnProcedural` stamp `proceduralTexture()` leaves behind. */
95
98
  interface ProceduralRecipeRecord {
@@ -40,6 +40,7 @@
40
40
  */
41
41
  import { createHash } from 'node:crypto';
42
42
  import * as THREE from 'three';
43
+ import { collectAtlasProvenance, readAtlasEncodedTexture, readValidatedAtlasDelivery, } from "./atlas-provenance.js";
43
44
  /**
44
45
  * Material slots the GLB bridge can serialize, with the glTF usage each one
45
46
  * implies. Kept in the same order the bridge reads them so a warning list and a
@@ -142,6 +143,7 @@ function isTexture(value) {
142
143
  * @returns Provenance for each baked texture, sorted by name/slot.
143
144
  */
144
145
  export async function bakeSceneTextures(root, warnings) {
146
+ const atlas = collectAtlasProvenance(root);
145
147
  // Collect first, encode second: traversal is sync, encoding is not, and doing
146
148
  // them together would interleave awaits with a mutating walk.
147
149
  const pending = new Map();
@@ -154,7 +156,7 @@ export async function bakeSceneTextures(root, warnings) {
154
156
  const texture = bag[slot];
155
157
  if (!isTexture(texture))
156
158
  continue;
157
- if (texture.userData['encoded'])
159
+ if (texture.userData['encoded'] && !atlas.texture(texture))
158
160
  continue;
159
161
  const existing = pending.get(texture) ?? { texture, bindings: [] };
160
162
  existing.bindings.push({
@@ -170,16 +172,66 @@ export async function bakeSceneTextures(root, warnings) {
170
172
  const baked = [];
171
173
  for (const entry of pending.values()) {
172
174
  const name = entry.texture.name || '(unnamed texture)';
173
- const raw = readPixels(entry.texture);
175
+ const atlasRef = atlas.texture(entry.texture);
176
+ const atlasRecord = atlasRef && atlas.table.atlases.find((a) => a.id === atlasRef.id);
177
+ const encoded = atlasRef ? readAtlasEncodedTexture(entry.texture) : undefined;
178
+ if (encoded &&
179
+ (encoded.mime !== 'image/png' ||
180
+ !(encoded.bytes instanceof Uint8Array) ||
181
+ encoded.bytes.length > 16_777_216))
182
+ throw new TypeError('Atlas baked encoding must be bounded PNG bytes.');
183
+ const raw = encoded && atlasRecord
184
+ ? {
185
+ width: atlasRecord.spec.width,
186
+ height: atlasRecord.spec.height,
187
+ channels: 4,
188
+ data: new Uint8Array(0),
189
+ }
190
+ : readPixels(entry.texture);
174
191
  const firstBinding = entry.bindings[0];
175
192
  const bindingSummary = `material ${JSON.stringify(firstBinding.material)} (${firstBinding.slot})${entry.bindings.length > 1 ? ` and ${entry.bindings.length - 1} other binding(s)` : ''}`;
176
193
  if (typeof raw === 'string') {
194
+ if (atlasRef)
195
+ throw new TypeError(`Atlas has stale compiled provenance: ${raw}; rebuild with textureAtlas() or remove atlas metadata for a manual texture.`);
177
196
  warnings.push(`Texture ${JSON.stringify(name)} on ${bindingSummary} was dropped from the GLB: ${raw}.`);
178
197
  continue;
179
198
  }
199
+ if (atlasRecord) {
200
+ const stale = () => new TypeError('Atlas has stale compiled provenance: rebuild with textureAtlas() after editing pixels, or remove atlas metadata for a manual texture.');
201
+ if (encoded) {
202
+ const delivery = readValidatedAtlasDelivery(entry.texture);
203
+ if (delivery) {
204
+ if (delivery.id !== atlasRef.id ||
205
+ delivery.finalMime !== encoded.mime ||
206
+ delivery.finalImageSha256 !==
207
+ `sha256:${createHash('sha256').update(encoded.bytes).digest('hex')}`)
208
+ throw stale();
209
+ }
210
+ else {
211
+ const sharp = (await import('sharp')).default;
212
+ const image = sharp(encoded.bytes, {
213
+ limitInputPixels: atlasRecord.spec.width * atlasRecord.spec.height,
214
+ });
215
+ const metadata = await image.metadata();
216
+ if (metadata.width !== atlasRecord.spec.width ||
217
+ metadata.height !== atlasRecord.spec.height)
218
+ throw stale();
219
+ const decoded = await image.ensureAlpha().raw().toBuffer();
220
+ if (`sha256:${createHash('sha256').update(decoded).digest('hex')}` !==
221
+ atlasRecord.manifest.decodedPixelHash)
222
+ throw stale();
223
+ }
224
+ }
225
+ else if (raw.width !== atlasRecord.spec.width ||
226
+ raw.height !== atlasRecord.spec.height ||
227
+ raw.channels !== 4 ||
228
+ `sha256:${createHash('sha256').update(raw.data).digest('hex')}` !==
229
+ atlasRecord.manifest.decodedPixelHash)
230
+ throw stale();
231
+ }
180
232
  let bytes;
181
233
  try {
182
- bytes = await encodePngFromRaw(raw);
234
+ bytes = encoded?.bytes ?? (await encodePngFromRaw(raw));
183
235
  }
184
236
  catch (err) {
185
237
  warnings.push(`Texture ${JSON.stringify(name)} on ${bindingSummary} was dropped from the GLB: PNG encoding failed (${err instanceof Error ? err.message : String(err)}).`);
@@ -197,18 +249,20 @@ export async function bakeSceneTextures(root, warnings) {
197
249
  // Prefer the authored portable usage over incidental slot order. A packed
198
250
  // metallic-roughness map occupies both Three.js slots, where choosing the
199
251
  // first (`roughnessMap`) loses the authored combined-channel meaning.
200
- const primaryUsage = recipe?.usage ??
252
+ const primaryUsage = atlasRecord?.spec.usage ??
253
+ recipe?.usage ??
201
254
  (entry.bindings.some(({ slot }) => slot === 'roughnessMap') &&
202
255
  entry.bindings.some(({ slot }) => slot === 'metalnessMap')
203
256
  ? 'metallicRoughness'
204
257
  : (entry.bindings[0]?.usage ?? 'albedo'));
205
- entry.texture.userData['kilnTexture'] = {
206
- usage: primaryUsage,
207
- colorSpace,
208
- width: raw.width,
209
- height: raw.height,
210
- hasAlpha: hasTranslucentPixel(raw),
211
- };
258
+ if (!encoded)
259
+ entry.texture.userData['kilnTexture'] = {
260
+ usage: primaryUsage,
261
+ colorSpace,
262
+ width: raw.width,
263
+ height: raw.height,
264
+ hasAlpha: hasTranslucentPixel(raw),
265
+ };
212
266
  const sha1 = createHash('sha1').update(bytes).digest('hex');
213
267
  const imageSha256 = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
214
268
  baked.push({
@@ -228,6 +282,7 @@ export async function bakeSceneTextures(root, warnings) {
228
282
  imageSha256,
229
283
  sha1,
230
284
  ...(recipe ? { procedural: recipe } : {}),
285
+ ...(atlasRef ? { atlas: atlasRef } : {}),
231
286
  });
232
287
  }
233
288
  return baked.sort((a, b) => `${a.node}:${a.material}:${a.texture}:${a.slot}`.localeCompare(`${b.node}:${b.material}:${b.texture}:${b.slot}`));