@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,456 @@
1
+ /** Deterministic fixed-scale composition. Resource loading is a trusted host capability. */
2
+ import { compileProceduralTextureSpecV3 } from './procedural-texture-v3';
3
+ import {
4
+ admitProceduralTextureSpecV3,
5
+ hashProceduralTextureSpecV3,
6
+ PROCEDURAL_TEXTURE_V3_COMPILER_ID,
7
+ type ProceduralTextureAdmissionV3,
8
+ } from './procedural-texture-v3-contract';
9
+ import { sha256Hex } from './procedural-material-v2';
10
+ import type {
11
+ PinnedMaterialMapPortV1,
12
+ VerifiedPinnedMaterialMapDescriptorV1,
13
+ } from './pinned-material-map';
14
+ import {
15
+ atlasAllocationV1,
16
+ atlasFailure,
17
+ atlasLayoutHashV1,
18
+ atlasSourceIdentityV1,
19
+ assertAtlasCropV1,
20
+ canonicalizeTextureAtlasSpecV1,
21
+ canonicalAtlasMetadataJsonV1,
22
+ hashTextureAtlasSpecV1,
23
+ TEXTURE_ATLAS_COMPILER_ID,
24
+ TEXTURE_ATLAS_LIMITS,
25
+ type AtlasRectangleV1,
26
+ type CanonicalTextureAtlasRegionV1,
27
+ type CanonicalTextureAtlasSourceV1,
28
+ type CanonicalTextureAtlasSpecV1,
29
+ } from './texture-atlas-contract';
30
+
31
+ type Hash = `sha256:${string}`;
32
+ export interface TextureAtlasCompileOptionsV1 {
33
+ resources?: PinnedMaterialMapPortV1;
34
+ signal?: AbortSignal;
35
+ }
36
+ export interface TextureAtlasBudgetV1 {
37
+ sourceTexels: number;
38
+ encodedBytes: number;
39
+ workUnits: number;
40
+ conservativePixelAllocationBytes: number;
41
+ dilationScratchBytes: number;
42
+ }
43
+ interface PreparedSource {
44
+ id: string;
45
+ source: CanonicalTextureAtlasSourceV1;
46
+ width: number;
47
+ height: number;
48
+ descriptor?: VerifiedPinnedMaterialMapDescriptorV1;
49
+ admission?: ProceduralTextureAdmissionV3;
50
+ }
51
+ export interface TextureAtlasAdmissionV1 {
52
+ spec: CanonicalTextureAtlasSpecV1;
53
+ sources: PreparedSource[];
54
+ crops: Map<string, AtlasRectangleV1>;
55
+ budget: TextureAtlasBudgetV1;
56
+ }
57
+ export interface TextureAtlasSourceManifestV1 {
58
+ id: string;
59
+ kind: 'procedural' | 'resource';
60
+ width: number;
61
+ height: number;
62
+ decodedPixelHash: Hash;
63
+ recipeHash?: Hash;
64
+ compilerId?: string;
65
+ descriptor?: VerifiedPinnedMaterialMapDescriptorV1;
66
+ }
67
+ export interface TextureAtlasRegionManifestV1 {
68
+ name: string;
69
+ content: AtlasRectangleV1;
70
+ allocation: AtlasRectangleV1;
71
+ crop: AtlasRectangleV1;
72
+ sourceId: string;
73
+ }
74
+ export interface TextureAtlasManifestV1 {
75
+ version: 'kiln.atlas-manifest.v1';
76
+ compilerId: typeof TEXTURE_ATLAS_COMPILER_ID;
77
+ width: number;
78
+ height: number;
79
+ usage: CanonicalTextureAtlasSpecV1['usage'];
80
+ origin: 'top-left';
81
+ sampler: CanonicalTextureAtlasSpecV1['sampler'];
82
+ specHash: Hash;
83
+ layoutHash: Hash;
84
+ decodedPixelHash: Hash;
85
+ atlasPixelHash: Hash;
86
+ sources: TextureAtlasSourceManifestV1[];
87
+ regions: TextureAtlasRegionManifestV1[];
88
+ manifestHash: Hash;
89
+ }
90
+ export interface CompiledTextureAtlasV1 {
91
+ spec: CanonicalTextureAtlasSpecV1;
92
+ canonicalJson: string;
93
+ pixels: Uint8Array;
94
+ manifest: TextureAtlasManifestV1;
95
+ budget: TextureAtlasBudgetV1;
96
+ }
97
+
98
+ const isHash = (value: unknown): value is Hash =>
99
+ typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
100
+ const boundedInteger = (value: number, max: number) =>
101
+ Number.isSafeInteger(value) && value >= 1 && value <= max;
102
+ function checkDescriptor(
103
+ d: VerifiedPinnedMaterialMapDescriptorV1,
104
+ source: Extract<CanonicalTextureAtlasSourceV1, { kind: 'resource' }>,
105
+ usage: CanonicalTextureAtlasSpecV1['usage'],
106
+ ): void {
107
+ const slots = {
108
+ baseColor: 'albedo',
109
+ normal: 'normal',
110
+ metallicRoughness: 'metallicRoughness',
111
+ emissive: 'emissive',
112
+ occlusion: 'occlusion',
113
+ };
114
+ if (
115
+ d.materialId !== source.pin.resourceId ||
116
+ d.revisionId !== source.pin.revisionId ||
117
+ d.resourceId !== source.resourceId ||
118
+ !isHash(d.revisionId) ||
119
+ !isHash(d.imageSha256)
120
+ )
121
+ atlasFailure('pinned source descriptor identity/hash mismatch.');
122
+ if (
123
+ d.usage !== usage ||
124
+ slots[d.slot] !== usage ||
125
+ d.colorSpace !== (usage === 'albedo' || usage === 'emissive' ? 'srgb' : 'linear')
126
+ )
127
+ atlasFailure('pinned source usage/slot/colorSpace mismatch.');
128
+ if (
129
+ !boundedInteger(d.width, 4096) ||
130
+ !boundedInteger(d.height, 4096) ||
131
+ !boundedInteger(d.encodedByteLength, TEXTURE_ATLAS_LIMITS.encodedSourceBytes) ||
132
+ (d.decodedPixelHash !== undefined && !isHash(d.decodedPixelHash))
133
+ )
134
+ atlasFailure('pinned source dimensions/byte/hash budget is invalid.');
135
+ for (const key of ['width', 'height', 'imageSha256', 'decodedPixelHash'] as const) {
136
+ const expected = source.expected?.[key];
137
+ if (expected !== undefined && d[key] !== undefined && expected !== d[key])
138
+ atlasFailure(`source expected ${key} does not match verified descriptor.`);
139
+ }
140
+ }
141
+
142
+ /** Admit all sources and aggregate work/buffer allocations before any source pixel load. */
143
+ export function admitTextureAtlasSpecV1(
144
+ input: unknown,
145
+ options: TextureAtlasCompileOptionsV1 = {},
146
+ ): TextureAtlasAdmissionV1 {
147
+ const spec = canonicalizeTextureAtlasSpecV1(input),
148
+ unique = new Map<string, PreparedSource>(),
149
+ crops = new Map<string, AtlasRectangleV1>();
150
+ const outputTexels = spec.width * spec.height;
151
+ // Include output and two output-sized hash snapshots/domain concatenation buffers.
152
+ const budget: TextureAtlasBudgetV1 = {
153
+ sourceTexels: 0,
154
+ encodedBytes: 0,
155
+ workUnits: outputTexels * 13,
156
+ conservativePixelAllocationBytes: outputTexels * 12,
157
+ dilationScratchBytes: 0,
158
+ };
159
+ for (const region of spec.regions) {
160
+ const id = atlasSourceIdentityV1(region.source);
161
+ let prepared = unique.get(id);
162
+ if (!prepared) {
163
+ if (region.source.kind === 'procedural') {
164
+ const admission = admitProceduralTextureSpecV3(region.source.spec);
165
+ prepared = {
166
+ id,
167
+ source: region.source,
168
+ width: admission.spec.width,
169
+ height: admission.spec.height,
170
+ admission,
171
+ };
172
+ budget.workUnits += admission.estimatedWorkUnits;
173
+ budget.conservativePixelAllocationBytes += admission.conservativePixelAllocationBytes;
174
+ } else {
175
+ const described = options.resources?.describePinnedMaterialMap(
176
+ region.source.resourceId,
177
+ region.source.pin,
178
+ );
179
+ if (!described)
180
+ atlasFailure(
181
+ `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.`,
182
+ );
183
+ checkDescriptor(described, region.source, spec.usage);
184
+ // Snapshot verified metadata before an async loader can mutate its own object.
185
+ const descriptor = structuredClone(described);
186
+ prepared = {
187
+ id,
188
+ source: region.source,
189
+ width: descriptor.width,
190
+ height: descriptor.height,
191
+ descriptor,
192
+ };
193
+ budget.encodedBytes += descriptor.encodedByteLength;
194
+ // Returned owned pixels + our immutable snapshot + decoder/copy allowance + encoded input.
195
+ budget.conservativePixelAllocationBytes +=
196
+ descriptor.width * descriptor.height * 12 + descriptor.encodedByteLength;
197
+ }
198
+ const texels = prepared.width * prepared.height;
199
+ budget.sourceTexels += texels;
200
+ budget.workUnits += texels * 8;
201
+ budget.conservativePixelAllocationBytes += texels * 4; // Raw-pixel hash snapshot allowance.
202
+ unique.set(id, prepared);
203
+ }
204
+ if (region.source.kind === 'resource')
205
+ checkDescriptor(prepared.descriptor!, region.source, spec.usage);
206
+ const crop = region.crop ?? { x: 0, y: 0, width: prepared.width, height: prepared.height };
207
+ assertAtlasCropV1(crop, region.content, prepared.width, prepared.height);
208
+ crops.set(region.name, crop);
209
+ const allocation = atlasAllocationV1(region),
210
+ texels = allocation.width * allocation.height;
211
+ budget.workUnits += texels * (region.edgeMode === 'mask-rgb-dilate' ? 16 : 2);
212
+ if (region.edgeMode === 'mask-rgb-dilate')
213
+ budget.dilationScratchBytes = Math.max(budget.dilationScratchBytes, texels * 8);
214
+ }
215
+ budget.workUnits += (spec.regions.length * (spec.regions.length - 1)) / 2;
216
+ budget.conservativePixelAllocationBytes += budget.dilationScratchBytes;
217
+ if (budget.sourceTexels > TEXTURE_ATLAS_LIMITS.sourceTexels)
218
+ atlasFailure(
219
+ 'full decoded source texel budget exceeded; a small crop does not reduce source decode cost.',
220
+ );
221
+ if (budget.encodedBytes > TEXTURE_ATLAS_LIMITS.encodedBytes)
222
+ atlasFailure('aggregate encoded source byte budget exceeded.');
223
+ if (budget.workUnits > TEXTURE_ATLAS_LIMITS.work)
224
+ atlasFailure(`aggregate work budget ${budget.workUnits} exceeds ${TEXTURE_ATLAS_LIMITS.work}.`);
225
+ if (budget.conservativePixelAllocationBytes > TEXTURE_ATLAS_LIMITS.allocatedBytes)
226
+ atlasFailure(
227
+ `aggregate pixel allocation budget ${budget.conservativePixelAllocationBytes} exceeds ${TEXTURE_ATLAS_LIMITS.allocatedBytes}.`,
228
+ );
229
+ return {
230
+ spec,
231
+ sources: [...unique.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
232
+ crops,
233
+ budget,
234
+ };
235
+ }
236
+ async function pixelDigest(bytes: Uint8Array): Promise<Hash> {
237
+ // Compiler/host snapshots are ArrayBuffer-backed, never shared mutable cache state.
238
+ const result = await crypto.subtle.digest('SHA-256', bytes as Uint8Array<ArrayBuffer>);
239
+ return `sha256:${[...new Uint8Array(result)].map((v) => v.toString(16).padStart(2, '0')).join('')}`;
240
+ }
241
+ function cancelled(signal?: AbortSignal): void {
242
+ if (signal?.aborted) atlasFailure('compilation cancelled.');
243
+ }
244
+
245
+ /** Copy exact crop and fill only this region's disjoint allocation. Scratch is reused. */
246
+ function composeRegion(
247
+ output: Uint8Array,
248
+ atlasWidth: number,
249
+ region: CanonicalTextureAtlasRegionV1,
250
+ crop: AtlasRectangleV1,
251
+ source: Uint8Array,
252
+ sourceWidth: number,
253
+ distances: Int32Array,
254
+ donors: Int32Array,
255
+ ): void {
256
+ const allocation = atlasAllocationV1(region),
257
+ w = allocation.width,
258
+ h = allocation.height,
259
+ g = region.gutter;
260
+ const sourcePixel = (cx: number, cy: number) => ((crop.y + cy) * sourceWidth + crop.x + cx) * 4;
261
+ const destination = (x: number, y: number) =>
262
+ ((allocation.y + y) * atlasWidth + allocation.x + x) * 4;
263
+ if (region.edgeMode === 'copy-edge') {
264
+ for (let y = 0; y < h; y++)
265
+ for (let x = 0; x < w; x++) {
266
+ const s = sourcePixel(
267
+ Math.max(0, Math.min(crop.width - 1, x - g)),
268
+ Math.max(0, Math.min(crop.height - 1, y - g)),
269
+ ),
270
+ d = destination(x, y);
271
+ for (let c = 0; c < 4; c++) output[d + c] = source[s + c]!;
272
+ }
273
+ return;
274
+ }
275
+ const length = w * h,
276
+ infinity = 0x3fffffff;
277
+ distances.fill(infinity, 0, length);
278
+ donors.fill(infinity, 0, length);
279
+ for (let cy = 0; cy < crop.height; cy++)
280
+ for (let cx = 0; cx < crop.width; cx++) {
281
+ const s = sourcePixel(cx, cy),
282
+ d = destination(cx + g, cy + g),
283
+ index = (cy + g) * w + cx + g;
284
+ for (let c = 0; c < 4; c++) output[d + c] = source[s + c]!;
285
+ if (source[s + 3]! > 0) {
286
+ distances[index] = 0;
287
+ donors[index] = cy * crop.width + cx;
288
+ }
289
+ }
290
+ const consider = (i: number, neighbor: number) => {
291
+ const distance = distances[neighbor]! + 1;
292
+ if (
293
+ distance < distances[i]! ||
294
+ (distance === distances[i]! && donors[neighbor]! < donors[i]!)
295
+ ) {
296
+ distances[i] = distance;
297
+ donors[i] = donors[neighbor]!;
298
+ }
299
+ };
300
+ for (let y = 0; y < h; y++)
301
+ for (let x = 0; x < w; x++) {
302
+ const i = y * w + x;
303
+ if (x) consider(i, i - 1);
304
+ if (y) consider(i, i - w);
305
+ }
306
+ for (let y = h - 1; y >= 0; y--)
307
+ for (let x = w - 1; x >= 0; x--) {
308
+ const i = y * w + x;
309
+ if (x + 1 < w) consider(i, i + 1);
310
+ if (y + 1 < h) consider(i, i + w);
311
+ }
312
+ for (let y = 0; y < h; y++)
313
+ for (let x = 0; x < w; x++) {
314
+ const d = destination(x, y);
315
+ if (output[d + 3]! > 0) continue;
316
+ const donor = donors[y * w + x]!;
317
+ if (donor === infinity) {
318
+ output[d] = 0;
319
+ output[d + 1] = 0;
320
+ output[d + 2] = 0;
321
+ } else {
322
+ const s = sourcePixel(donor % crop.width, Math.floor(donor / crop.width));
323
+ for (let c = 0; c < 3; c++) output[d + c] = source[s + c]!;
324
+ }
325
+ }
326
+ }
327
+
328
+ export async function compileTextureAtlasSpecV1(
329
+ input: unknown,
330
+ options: TextureAtlasCompileOptionsV1 = {},
331
+ ): Promise<CompiledTextureAtlasV1> {
332
+ const admission = admitTextureAtlasSpecV1(input, options),
333
+ { spec, sources, crops, budget } = admission;
334
+ cancelled(options.signal);
335
+ const sourcePixels = new Map<string, Uint8Array>(),
336
+ sourceManifests: TextureAtlasSourceManifestV1[] = [];
337
+ // Sequential loads avoid multiplying decoder/snapshot peaks; each exact source compiles once.
338
+ for (const prepared of sources) {
339
+ cancelled(options.signal);
340
+ let pixels: Uint8Array, manifest: TextureAtlasSourceManifestV1;
341
+ if (prepared.source.kind === 'procedural') {
342
+ pixels = compileProceduralTextureSpecV3(prepared.source.spec).pixels;
343
+ manifest = {
344
+ id: prepared.id,
345
+ kind: 'procedural',
346
+ width: prepared.width,
347
+ height: prepared.height,
348
+ decodedPixelHash: await pixelDigest(pixels),
349
+ recipeHash: hashProceduralTextureSpecV3(prepared.source.spec),
350
+ compilerId: PROCEDURAL_TEXTURE_V3_COMPILER_ID,
351
+ };
352
+ } else {
353
+ const resolved = await options.resources!.resolvePinnedMaterialMapPixels(
354
+ prepared.source.resourceId,
355
+ prepared.source.pin,
356
+ { signal: options.signal },
357
+ );
358
+ checkDescriptor(resolved.descriptor, prepared.source, spec.usage);
359
+ if (
360
+ canonicalAtlasMetadataJsonV1(resolved.descriptor) !==
361
+ canonicalAtlasMetadataJsonV1(prepared.descriptor)
362
+ )
363
+ atlasFailure('resolved source descriptor changed after admission.');
364
+ if (
365
+ !(resolved.pixels instanceof Uint8Array) ||
366
+ resolved.pixels.length !== prepared.width * prepared.height * 4
367
+ )
368
+ atlasFailure('decoded source RGBA dimensions/length mismatch.');
369
+ pixels = Uint8Array.from(resolved.pixels);
370
+ const decodedPixelHash = await pixelDigest(pixels);
371
+ if (
372
+ decodedPixelHash !== resolved.decodedPixelHash ||
373
+ (prepared.descriptor!.decodedPixelHash !== undefined &&
374
+ decodedPixelHash !== prepared.descriptor!.decodedPixelHash)
375
+ )
376
+ atlasFailure('decoded source pixel hash mismatch.');
377
+ manifest = {
378
+ id: prepared.id,
379
+ kind: 'resource',
380
+ width: prepared.width,
381
+ height: prepared.height,
382
+ decodedPixelHash,
383
+ descriptor: prepared.descriptor,
384
+ };
385
+ }
386
+ sourcePixels.set(prepared.id, pixels);
387
+ sourceManifests.push(manifest);
388
+ }
389
+ // Compare every reuse's expectations, including an old external source without a descriptor hash.
390
+ for (const region of spec.regions)
391
+ if (
392
+ region.source.kind === 'resource' &&
393
+ region.source.expected?.decodedPixelHash !== undefined
394
+ ) {
395
+ const actual = sourceManifests.find(
396
+ (v) => v.id === atlasSourceIdentityV1(region.source),
397
+ )!.decodedPixelHash;
398
+ if (actual !== region.source.expected.decodedPixelHash)
399
+ atlasFailure('source expected decodedPixelHash mismatch.');
400
+ }
401
+ cancelled(options.signal);
402
+ const pixels = new Uint8Array(spec.width * spec.height * 4),
403
+ scratchLength = budget.dilationScratchBytes / 8;
404
+ const distances = new Int32Array(scratchLength),
405
+ donors = new Int32Array(scratchLength);
406
+ for (const region of spec.regions)
407
+ composeRegion(
408
+ pixels,
409
+ spec.width,
410
+ region,
411
+ crops.get(region.name)!,
412
+ sourcePixels.get(atlasSourceIdentityV1(region.source))!,
413
+ sources.find((v) => v.id === atlasSourceIdentityV1(region.source))!.width,
414
+ distances,
415
+ donors,
416
+ );
417
+ const decodedPixelHash = await pixelDigest(pixels),
418
+ header = new TextEncoder().encode(
419
+ `kiln.atlas-pixels.v1\n${spec.width},${spec.height},${spec.usage}\n`,
420
+ );
421
+ const tagged = new Uint8Array(header.length + pixels.length);
422
+ tagged.set(header);
423
+ tagged.set(pixels, header.length);
424
+ const atlasPixelHash = await pixelDigest(tagged);
425
+ const payload = {
426
+ version: 'kiln.atlas-manifest.v1' as const,
427
+ compilerId: TEXTURE_ATLAS_COMPILER_ID,
428
+ width: spec.width,
429
+ height: spec.height,
430
+ usage: spec.usage,
431
+ origin: 'top-left' as const,
432
+ sampler: spec.sampler,
433
+ specHash: hashTextureAtlasSpecV1(spec),
434
+ layoutHash: atlasLayoutHashV1(spec),
435
+ decodedPixelHash,
436
+ atlasPixelHash,
437
+ sources: sourceManifests,
438
+ regions: spec.regions.map((r) => ({
439
+ name: r.name,
440
+ content: r.content,
441
+ allocation: atlasAllocationV1(r),
442
+ crop: crops.get(r.name)!,
443
+ sourceId: atlasSourceIdentityV1(r.source),
444
+ })),
445
+ };
446
+ const manifestHash =
447
+ `sha256:${sha256Hex(`kiln.atlas-manifest.v1\n${canonicalAtlasMetadataJsonV1(payload)}`)}` as Hash;
448
+ cancelled(options.signal);
449
+ return {
450
+ spec,
451
+ canonicalJson: JSON.stringify(spec),
452
+ pixels,
453
+ manifest: { ...payload, manifestHash },
454
+ budget,
455
+ };
456
+ }
@@ -0,0 +1,185 @@
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';
4
+ import {
5
+ compileTextureAtlasSpecV1,
6
+ type TextureAtlasCompileOptionsV1,
7
+ type TextureAtlasManifestV1,
8
+ } from './texture-atlas-pixels';
9
+ import {
10
+ atlasFailure,
11
+ canonicalAtlasMetadataJsonV1,
12
+ TEXTURE_ATLAS_COMPILER_ID,
13
+ TEXTURE_ATLAS_LIMITS,
14
+ } from './texture-atlas-contract';
15
+ import { sha256Hex } from './procedural-material-v2';
16
+ import { remapUV } from './uv-shapes';
17
+ /** SDK/host API. Sandbox integration closes over options and exposes exactly one argument. */
18
+ export async function textureAtlas(
19
+ input: unknown,
20
+ options: TextureAtlasCompileOptionsV1 = {},
21
+ ): Promise<THREE.DataTexture> {
22
+ const compiled = await compileTextureAtlasSpecV1(input, options);
23
+ const texture = new THREE.DataTexture(
24
+ compiled.pixels,
25
+ compiled.spec.width,
26
+ compiled.spec.height,
27
+ THREE.RGBAFormat,
28
+ THREE.UnsignedByteType,
29
+ );
30
+ texture.name = 'Atlas';
31
+ texture.flipY = false;
32
+ texture.colorSpace =
33
+ compiled.spec.usage === 'albedo' || compiled.spec.usage === 'emissive'
34
+ ? THREE.SRGBColorSpace
35
+ : THREE.NoColorSpace;
36
+ applyPortableSamplerV1(texture, compiled.spec.sampler);
37
+ texture.userData.kilnAtlas = { spec: compiled.spec, manifest: compiled.manifest };
38
+ texture.userData.kilnTexture = {
39
+ usage: compiled.spec.usage,
40
+ colorSpace:
41
+ compiled.spec.usage === 'albedo' || compiled.spec.usage === 'emissive' ? 'srgb' : 'linear',
42
+ width: compiled.spec.width,
43
+ height: compiled.spec.height,
44
+ hasAlpha: compiled.pixels.some((v, i) => i % 4 === 3 && v < 255),
45
+ };
46
+ return texture;
47
+ }
48
+ export interface AtlasUVOptionsV1 {
49
+ inputOrigin: 'top-left' | 'bottom-left';
50
+ mode: 'content-boundary' | 'texel-center';
51
+ }
52
+ function manifestOf(atlas: THREE.Texture | TextureAtlasManifestV1): TextureAtlasManifestV1 {
53
+ const value = (atlas as THREE.Texture)?.isTexture
54
+ ? (atlas as THREE.Texture).userData.kilnAtlas?.manifest
55
+ : atlas;
56
+ if (!value || typeof value !== 'object')
57
+ atlasFailure('an atlas texture or compiled manifest is required.');
58
+ canonicalAtlasMetadataJsonV1(value);
59
+ const manifest = value as TextureAtlasManifestV1;
60
+ if (
61
+ manifest.version !== 'kiln.atlas-manifest.v1' ||
62
+ manifest.compilerId !== TEXTURE_ATLAS_COMPILER_ID ||
63
+ manifest.origin !== 'top-left' ||
64
+ !Array.isArray(manifest.regions) ||
65
+ !manifest.regions.length ||
66
+ manifest.regions.length > 64 ||
67
+ !Number.isSafeInteger(manifest.width) ||
68
+ manifest.width < 4 ||
69
+ manifest.width > 1024 ||
70
+ !Number.isSafeInteger(manifest.height) ||
71
+ manifest.height < 4 ||
72
+ manifest.height > 1024
73
+ )
74
+ atlasFailure('invalid atlas manifest dimensions/version.');
75
+ const { manifestHash, ...payload } = manifest;
76
+ if (
77
+ `sha256:${sha256Hex(`kiln.atlas-manifest.v1\n${canonicalAtlasMetadataJsonV1(payload)}`)}` !==
78
+ manifestHash
79
+ )
80
+ atlasFailure('atlas manifest identity mismatch.');
81
+ return manifest;
82
+ }
83
+ export function remapUVToAtlas(
84
+ geometry: THREE.BufferGeometry,
85
+ atlas: THREE.Texture | TextureAtlasManifestV1,
86
+ name: string,
87
+ options: AtlasUVOptionsV1,
88
+ ): THREE.BufferGeometry {
89
+ const manifest = manifestOf(atlas);
90
+ canonicalAtlasMetadataJsonV1(options);
91
+ if (
92
+ !options ||
93
+ typeof options !== 'object' ||
94
+ Array.isArray(options) ||
95
+ Object.keys(options).some((k) => !['inputOrigin', 'mode'].includes(k)) ||
96
+ !['top-left', 'bottom-left'].includes(options.inputOrigin) ||
97
+ !['content-boundary', 'texel-center'].includes(options.mode)
98
+ )
99
+ atlasFailure('explicit inputOrigin and content-boundary/texel-center mode are required.');
100
+ const region = manifest.regions.find((r) => r.name === name);
101
+ if (!region) atlasFailure(`unknown named region ${String(name)}.`);
102
+ const { x, y, width: w, height: h } = region.content;
103
+ if (
104
+ ![x, y, w, h].every(Number.isSafeInteger) ||
105
+ x < 0 ||
106
+ y < 0 ||
107
+ w < 1 ||
108
+ h < 1 ||
109
+ x + w > manifest.width ||
110
+ y + h > manifest.height
111
+ )
112
+ atlasFailure('invalid region content rectangle.');
113
+ const uv = geometry?.getAttribute?.('uv'),
114
+ position = geometry?.getAttribute?.('position');
115
+ if (
116
+ !position ||
117
+ !uv ||
118
+ uv.itemSize !== 2 ||
119
+ uv.count !== position.count ||
120
+ uv.count < 1 ||
121
+ uv.count > TEXTURE_ATLAS_LIMITS.uvVertices ||
122
+ (uv as THREE.InstancedBufferAttribute).isInstancedBufferAttribute ||
123
+ (
124
+ (uv as THREE.InterleavedBufferAttribute).data as
125
+ | (THREE.InterleavedBuffer & { isInstancedInterleavedBuffer?: boolean })
126
+ | undefined
127
+ )?.isInstancedInterleavedBuffer
128
+ )
129
+ atlasFailure(
130
+ 'matching per-vertex UV0 and positions are required; per-instance UVs are unsupported.',
131
+ );
132
+ let bytes = uv.count * 8;
133
+ const interleavedBuffers = new Set<ArrayBufferLike>();
134
+ const count = (attribute: THREE.BufferAttribute | THREE.InterleavedBufferAttribute) => {
135
+ if ((attribute as THREE.InterleavedBufferAttribute).isInterleavedBufferAttribute) {
136
+ // Three's geometry-copy data cache deduplicates interleaved backing buffers.
137
+ const buffer = attribute.array.buffer;
138
+ if (!interleavedBuffers.has(buffer)) {
139
+ interleavedBuffers.add(buffer);
140
+ bytes += buffer.byteLength;
141
+ }
142
+ } else {
143
+ // Ordinary attributes/morphs/index clone independently even with shared input arrays.
144
+ // Typed convenience subclasses may allocate in both constructor and copy.
145
+ bytes +=
146
+ attribute.array.byteLength *
147
+ (attribute.constructor === THREE.BufferAttribute ||
148
+ attribute.constructor === THREE.InstancedBufferAttribute
149
+ ? 1
150
+ : 2);
151
+ }
152
+ };
153
+ for (const attr of Object.values(geometry.attributes)) count(attr);
154
+ for (const attrs of Object.values(geometry.morphAttributes))
155
+ for (const attr of attrs) count(attr);
156
+ if (geometry.index) count(geometry.index);
157
+ if (bytes > TEXTURE_ATLAS_LIMITS.uvAttributeBytes)
158
+ atlasFailure('owned UV clone attribute allocation budget exceeded.');
159
+ for (let i = 0; i < uv.count; i++)
160
+ if (![uv.getX(i), uv.getY(i)].every((v) => Number.isFinite(v) && v >= 0 && v <= 1))
161
+ atlasFailure('UV0 must be finite in the unit domain [0,1]; region tiling is unsupported.');
162
+ const center = options.mode === 'texel-center',
163
+ su = (w - (center ? 1 : 0)) / manifest.width,
164
+ sv = (h - (center ? 1 : 0)) / manifest.height;
165
+ const bottom = options.inputOrigin === 'bottom-left';
166
+ const offset: [number, number] = [
167
+ (x + (center ? 0.5 : 0)) / manifest.width,
168
+ (y + (bottom ? h - (center ? 0.5 : 0) : center ? 0.5 : 0)) / manifest.height,
169
+ ];
170
+ const scale: [number, number] = [su, bottom ? -sv : sv];
171
+ const output = remapUV(geometry, { scale, offset });
172
+ output.userData.kilnAtlasBinding = {
173
+ manifestHash: manifest.manifestHash,
174
+ specHash: manifest.specHash,
175
+ layoutHash: manifest.layoutHash,
176
+ region: name,
177
+ content: structuredClone(region.content),
178
+ allocation: structuredClone(region.allocation),
179
+ inputOrigin: options.inputOrigin,
180
+ mode: options.mode,
181
+ scale,
182
+ offset,
183
+ };
184
+ return output;
185
+ }