@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
@@ -41,6 +41,12 @@
41
41
 
42
42
  import { createHash } from 'node:crypto';
43
43
  import * as THREE from 'three';
44
+ import {
45
+ collectAtlasProvenance,
46
+ readAtlasEncodedTexture,
47
+ readValidatedAtlasDelivery,
48
+ type AtlasReferenceV1,
49
+ } from './atlas-provenance';
44
50
  import type { EncodedTextureData, KilnTextureMetadata, TextureUsage } from './textures';
45
51
 
46
52
  export interface BakedTextureBindingV1 {
@@ -94,6 +100,8 @@ export interface BakedTextureProvenanceV1 {
94
100
  * regenerate the exact bytes. Absent for textures loaded from a file.
95
101
  */
96
102
  procedural?: ProceduralRecipeRecord;
103
+ /** Compact atlas source identity; the export scene table owns its recipe/manifest. */
104
+ atlas?: AtlasReferenceV1;
97
105
  }
98
106
 
99
107
  /** The `userData.kilnProcedural` stamp `proceduralTexture()` leaves behind. */
@@ -228,6 +236,7 @@ export async function bakeSceneTextures(
228
236
  root: THREE.Object3D,
229
237
  warnings: string[],
230
238
  ): Promise<BakedTextureProvenanceV1[]> {
239
+ const atlas = collectAtlasProvenance(root);
231
240
  // Collect first, encode second: traversal is sync, encoding is not, and doing
232
241
  // them together would interleave awaits with a mutating walk.
233
242
  const pending = new Map<
@@ -251,7 +260,8 @@ export async function bakeSceneTextures(
251
260
  for (const [slot, usage] of SLOT_USAGE) {
252
261
  const texture = bag[slot];
253
262
  if (!isTexture(texture)) continue;
254
- if ((texture.userData as Record<string, unknown>)['encoded']) continue;
263
+ if ((texture.userData as Record<string, unknown>)['encoded'] && !atlas.texture(texture))
264
+ continue;
255
265
  const existing = pending.get(texture) ?? { texture, bindings: [] };
256
266
  existing.bindings.push({
257
267
  node: node.name || '(unnamed node)',
@@ -267,19 +277,84 @@ export async function bakeSceneTextures(
267
277
  const baked: BakedTextureProvenanceV1[] = [];
268
278
  for (const entry of pending.values()) {
269
279
  const name = entry.texture.name || '(unnamed texture)';
270
- const raw = readPixels(entry.texture);
280
+ const atlasRef = atlas.texture(entry.texture);
281
+ const atlasRecord = atlasRef && atlas.table!.atlases.find((a) => a.id === atlasRef.id)!;
282
+ const encoded = atlasRef ? readAtlasEncodedTexture(entry.texture) : undefined;
283
+ if (
284
+ encoded &&
285
+ (encoded.mime !== 'image/png' ||
286
+ !(encoded.bytes instanceof Uint8Array) ||
287
+ encoded.bytes.length > 16_777_216)
288
+ )
289
+ throw new TypeError('Atlas baked encoding must be bounded PNG bytes.');
290
+ const raw =
291
+ encoded && atlasRecord
292
+ ? {
293
+ width: atlasRecord.spec.width,
294
+ height: atlasRecord.spec.height,
295
+ channels: 4 as const,
296
+ data: new Uint8Array(0),
297
+ }
298
+ : readPixels(entry.texture);
271
299
  const firstBinding = entry.bindings[0]!;
272
300
  const bindingSummary = `material ${JSON.stringify(firstBinding.material)} (${firstBinding.slot})${entry.bindings.length > 1 ? ` and ${entry.bindings.length - 1} other binding(s)` : ''}`;
273
301
  if (typeof raw === 'string') {
302
+ if (atlasRef)
303
+ throw new TypeError(
304
+ `Atlas has stale compiled provenance: ${raw}; rebuild with textureAtlas() or remove atlas metadata for a manual texture.`,
305
+ );
274
306
  warnings.push(
275
307
  `Texture ${JSON.stringify(name)} on ${bindingSummary} was dropped from the GLB: ${raw}.`,
276
308
  );
277
309
  continue;
278
310
  }
279
311
 
312
+ if (atlasRecord) {
313
+ const stale = () =>
314
+ new TypeError(
315
+ 'Atlas has stale compiled provenance: rebuild with textureAtlas() after editing pixels, or remove atlas metadata for a manual texture.',
316
+ );
317
+ if (encoded) {
318
+ const delivery = readValidatedAtlasDelivery(entry.texture);
319
+ if (delivery) {
320
+ if (
321
+ delivery.id !== atlasRef!.id ||
322
+ delivery.finalMime !== encoded.mime ||
323
+ delivery.finalImageSha256 !==
324
+ `sha256:${createHash('sha256').update(encoded.bytes).digest('hex')}`
325
+ )
326
+ throw stale();
327
+ } else {
328
+ const sharp = (await import('sharp')).default;
329
+ const image = sharp(encoded.bytes, {
330
+ limitInputPixels: atlasRecord.spec.width * atlasRecord.spec.height,
331
+ });
332
+ const metadata = await image.metadata();
333
+ if (
334
+ metadata.width !== atlasRecord.spec.width ||
335
+ metadata.height !== atlasRecord.spec.height
336
+ )
337
+ throw stale();
338
+ const decoded = await image.ensureAlpha().raw().toBuffer();
339
+ if (
340
+ `sha256:${createHash('sha256').update(decoded).digest('hex')}` !==
341
+ atlasRecord.manifest.decodedPixelHash
342
+ )
343
+ throw stale();
344
+ }
345
+ } else if (
346
+ raw.width !== atlasRecord.spec.width ||
347
+ raw.height !== atlasRecord.spec.height ||
348
+ raw.channels !== 4 ||
349
+ `sha256:${createHash('sha256').update(raw.data).digest('hex')}` !==
350
+ atlasRecord.manifest.decodedPixelHash
351
+ )
352
+ throw stale();
353
+ }
354
+
280
355
  let bytes: Uint8Array;
281
356
  try {
282
- bytes = await encodePngFromRaw(raw);
357
+ bytes = encoded?.bytes ?? (await encodePngFromRaw(raw));
283
358
  } catch (err) {
284
359
  warnings.push(
285
360
  `Texture ${JSON.stringify(name)} on ${bindingSummary} was dropped from the GLB: PNG encoding failed (${err instanceof Error ? err.message : String(err)}).`,
@@ -302,18 +377,20 @@ export async function bakeSceneTextures(
302
377
  // metallic-roughness map occupies both Three.js slots, where choosing the
303
378
  // first (`roughnessMap`) loses the authored combined-channel meaning.
304
379
  const primaryUsage =
380
+ atlasRecord?.spec.usage ??
305
381
  recipe?.usage ??
306
382
  (entry.bindings.some(({ slot }) => slot === 'roughnessMap') &&
307
383
  entry.bindings.some(({ slot }) => slot === 'metalnessMap')
308
384
  ? 'metallicRoughness'
309
385
  : (entry.bindings[0]?.usage ?? 'albedo'));
310
- (entry.texture.userData as Record<string, unknown>)['kilnTexture'] = {
311
- usage: primaryUsage,
312
- colorSpace,
313
- width: raw.width,
314
- height: raw.height,
315
- hasAlpha: hasTranslucentPixel(raw),
316
- } satisfies KilnTextureMetadata;
386
+ if (!encoded)
387
+ (entry.texture.userData as Record<string, unknown>)['kilnTexture'] = {
388
+ usage: primaryUsage,
389
+ colorSpace,
390
+ width: raw.width,
391
+ height: raw.height,
392
+ hasAlpha: hasTranslucentPixel(raw),
393
+ } satisfies KilnTextureMetadata;
317
394
 
318
395
  const sha1 = createHash('sha1').update(bytes).digest('hex');
319
396
  const imageSha256 = `sha256:${createHash('sha256').update(bytes).digest('hex')}` as const;
@@ -334,6 +411,7 @@ export async function bakeSceneTextures(
334
411
  imageSha256,
335
412
  sha1,
336
413
  ...(recipe ? { procedural: recipe } : {}),
414
+ ...(atlasRef ? { atlas: atlasRef } : {}),
337
415
  });
338
416
  }
339
417
 
@@ -8,6 +8,24 @@
8
8
  */
9
9
  import type * as THREE from 'three';
10
10
  import type { TextureUsage } from './textures';
11
+ import type { PinnedMaterialMapPortV1 } from './pinned-material-map';
12
+
13
+ export interface PreparedPortableTextureV3 {
14
+ descriptor: {
15
+ resourceId: string;
16
+ usage: TextureUsage;
17
+ allowedSlots: readonly string[];
18
+ colorSpace: 'srgb' | 'linear';
19
+ width: number;
20
+ height: number;
21
+ encodedByteLength: number;
22
+ imageSha256: `sha256:${string}`;
23
+ };
24
+ /** Caller-owned verified PNG bytes, prepared without pixel decoding. */
25
+ bytes: Uint8Array;
26
+ /** Verified provenance only; decoding supplies actual hasAlpha metadata. */
27
+ userData: Record<string, unknown>;
28
+ }
11
29
 
12
30
  import {
13
31
  type ApprovedTextureResourceCache,
@@ -21,11 +39,17 @@ import type {
21
39
  } from './material-recipes';
22
40
 
23
41
  export interface TextureResolver {
42
+ /** Host-only, verified immutable material closure for atlas compilation. */
43
+ pinnedMaterialMaps?: PinnedMaterialMapPortV1;
24
44
  /** Host-only metadata used to reject invalid portable slot bindings before loading. */
25
45
  describeApprovedTexture?(
26
46
  resourceId: unknown,
27
47
  ): { usage: TextureUsage; allowedSlots: readonly string[] } | undefined;
28
48
  loadApprovedTexture(resourceId: unknown): Promise<THREE.DataTexture>;
49
+ preparePortableTexture?(
50
+ resourceId: unknown,
51
+ context?: { signal?: AbortSignal },
52
+ ): Promise<PreparedPortableTextureV3>;
29
53
  materialRecipe(id: unknown, overrides?: unknown): Promise<THREE.MeshStandardMaterial>;
30
54
  }
31
55
 
@@ -47,6 +71,37 @@ export function createTextureResolver(
47
71
  }
48
72
  return (await cache.load(resourceId as ApprovedTextureResourceId)).texture;
49
73
  },
74
+ async preparePortableTexture(
75
+ resourceId: unknown,
76
+ context?: { signal?: AbortSignal },
77
+ ): Promise<PreparedPortableTextureV3> {
78
+ context?.signal?.throwIfAborted();
79
+ if (typeof resourceId !== 'string')
80
+ throw new RangeError('Unsupported approved texture resource ID.');
81
+ const resolved = await cache.resolveAsync(resourceId as ApprovedTextureResourceId);
82
+ context?.signal?.throwIfAborted();
83
+ // resolveAsync already verifies closed ID, length, hash and bounded PNG IHDR.
84
+ const header = new DataView(
85
+ resolved.bytes.buffer,
86
+ resolved.bytes.byteOffset,
87
+ resolved.bytes.byteLength,
88
+ );
89
+ return {
90
+ descriptor: {
91
+ resourceId,
92
+ usage: resolved.provenance.usage,
93
+ allowedSlots: [...resolved.descriptor.allowedSlots],
94
+ colorSpace: resolved.provenance.colorSpace,
95
+ width: header.getUint32(16),
96
+ height: header.getUint32(20),
97
+ encodedByteLength: resolved.bytes.length,
98
+ imageSha256: `sha256:${resolved.provenance.contentHash}`,
99
+ },
100
+ // resolveAsync already returns a caller-owned copy, never cached bytes.
101
+ bytes: resolved.bytes,
102
+ userData: { kilnTexture: { approvedResource: structuredClone(resolved.provenance) } },
103
+ };
104
+ },
50
105
  async materialRecipe(id: unknown, overrides?: unknown): Promise<THREE.MeshStandardMaterial> {
51
106
  if (typeof id !== 'string') throw new TypeError('materialRecipe id must be a string');
52
107
  return applyTrustedMaterialRecipe(
@@ -3,14 +3,14 @@ import {
3
3
  assertMaterialJson,
4
4
  materialLibraryHashSchema,
5
5
  materialLibraryIdSchema,
6
- materialLibraryPayloadSchema,
6
+ materialLibraryPayloadAnySchema,
7
7
  materialLibraryPortableSpec,
8
8
  materialSourceSchema,
9
- proceduralMaterialDraftSchema,
10
9
  type MaterialLibrary,
11
- type MaterialManifestV1,
10
+ type MaterialManifest,
12
11
  } from '../material-library';
13
12
  import { createMaterialRecordV1, decodeMaterialLibraryPayload } from '../material-library-node';
13
+ import { createProceduralMaterialRecord } from '../material-authoring-node';
14
14
  import { createMaterialPresetDraft, listMaterialPresets } from '../material-presets';
15
15
  import { nestedRecordDescription, requireActionFields } from './actions';
16
16
  import type { KilnToolDef } from './registry';
@@ -89,18 +89,18 @@ export const materialToolInput = z.strictObject({
89
89
  .describe("get: the immutable revision; omitted, the material's only revision."),
90
90
  draft: record(
91
91
  nestedRecordDescription(
92
- 'create-procedural: materialId, name, tileable, sources, maps with layered procedural specs; optional tags, physicalSizeMeters, parameters.',
92
+ 'create-procedural: legacy layered draft, or schemaVersion 2 with explicit samplers and RGBA/atlas recipes. Exact atlas sources must exist in this library.',
93
93
  'shape:material-draft',
94
94
  ),
95
95
  ),
96
96
  payload: record(
97
97
  nestedRecordDescription(
98
- 'import: schemaVersion 1 and complete normalized records with embedded PNG bytes.',
98
+ 'import: schemaVersion 1 or 2, complete normalized records and their exact dependencies with embedded PNG bytes.',
99
99
  'shape:material-import',
100
100
  ),
101
101
  ),
102
102
  });
103
- const materialResult = (material: MaterialManifestV1) => ({
103
+ const materialResult = (material: MaterialManifest) => ({
104
104
  ok: true,
105
105
  material,
106
106
  portableSpec: materialLibraryPortableSpec(material),
@@ -157,7 +157,7 @@ export function createKilnMaterialDef(
157
157
  return {
158
158
  name: 'kiln_material',
159
159
  description:
160
- 'Manage optional immutable material resources in this workspace. presets discovers shipped architecture, wood, metal, fabric and ground recipes; create-preset bakes one with an explicit seed, creator and license; list returns compact material/revision summaries; get returns full provenance, hashes, map conventions, physical repeat scale and a code-ready portable material spec; create-procedural bakes bounded editable layer recipes including optional height-derived normals; import accepts complete normalized records with embedded PNG bytes. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before authored evaluation resolves the resources; no project is required.',
160
+ 'Manage optional immutable material resources in this workspace. presets lists shipped recipes; create-preset uses explicit seed, creator and license; list returns revision summaries; get returns provenance, hashes, sampling and a portable material spec. create-procedural accepts legacy layer drafts or schemaVersion 2 drafts with explicit samplers and bounded RGBA or named atlas recipes. Atlas resource sources require exact saved revisions; import requires complete normalized records and dependencies with embedded PNG bytes. Read shape:material-draft or shape:material-import through kiln_discover for full fields. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before evaluation; no project or mesh is required.',
161
161
  inputSchema: materialToolInput,
162
162
  actionContract: MATERIAL_CONTRACT,
163
163
  annotations: {
@@ -225,15 +225,19 @@ export function createKilnMaterialDef(
225
225
  }
226
226
  }
227
227
  case 'create-procedural': {
228
- const record = await createMaterialRecordV1(
229
- proceduralMaterialDraftSchema.parse(input.draft),
228
+ const { record, dependencies } = await createProceduralMaterialRecord(
229
+ input.draft,
230
+ library,
231
+ );
232
+ const [material] = await library.import(
233
+ [record],
234
+ dependencies.length ? { materials: dependencies } : undefined,
230
235
  );
231
- const [material] = await library.import([record]);
232
236
  return materialResult(material!);
233
237
  }
234
238
  case 'import': {
235
239
  const records = await decodeMaterialLibraryPayload(
236
- materialLibraryPayloadSchema.parse(input.payload),
240
+ materialLibraryPayloadAnySchema.parse(input.payload),
237
241
  );
238
242
  const materials = await library.import(records);
239
243
  return {
@@ -492,26 +492,16 @@ async function renderDerivativeCell(
492
492
  ): Promise<import('../views').DerivativeCellRenderResult> {
493
493
  let derivativeRoot = input.root as THREE.Object3D;
494
494
  if (input.backfaceCull === false) {
495
- derivativeRoot = derivativeRoot.clone(true);
496
- derivativeRoot.traverse((node) => {
497
- const mesh = node as THREE.Mesh;
498
- if (!mesh.isMesh) return;
499
- const prepare = (material: THREE.Material) => {
500
- const copy = material.clone();
501
- copy.side = THREE.DoubleSide;
502
- return copy;
503
- };
504
- mesh.material = Array.isArray(mesh.material)
505
- ? mesh.material.map(prepare)
506
- : prepare(mesh.material);
507
- });
495
+ const { cloneReviewScene } = await import('../views/scene-copy');
496
+ derivativeRoot = cloneReviewScene(derivativeRoot, { omitHidden: true, doubleSided: true });
508
497
  }
509
498
  // `withCameraVisibility` and `isolateSubtree` express isolation by clearing
510
499
  // `mesh.visible`. The CPU rasterizer culls on that flag (see views/raster.ts),
511
500
  // but glTF carries no per-mesh visibility, so serializing unfiltered ships the
512
501
  // hidden geometry to the GPU service and makes isolate a silent no-op on exactly
513
502
  // the material-faithful path it is most useful on. Prune on a copy: the caller
514
- // restores `.visible` afterwards and must not observe a mutated scene.
503
+ // restores `.visible` afterwards and must not observe a mutated scene. Required
504
+ // joint ancestors survive as transform-only nodes in the derivative.
515
505
  const { withoutHiddenMeshes } = await import('../views/camera');
516
506
  derivativeRoot = withoutHiddenMeshes(derivativeRoot);
517
507
  const rendered = await renderSceneToGLB(derivativeRoot, {
@@ -2807,7 +2797,7 @@ export async function buildProgramAssetDraft(
2807
2797
  const dependencies = [
2808
2798
  ...(rendered.materialResourceProvenance ?? []),
2809
2799
  ...(rendered.materialLibraryDependencies ?? []).map((manifest) => ({
2810
- kind: 'kiln.material.v1',
2800
+ kind: `kiln.material.v${manifest.schemaVersion}`,
2811
2801
  delivery: 'runtime',
2812
2802
  manifest,
2813
2803
  })),
@@ -216,7 +216,7 @@ export function createKilnReviewDef(context: {
216
216
  const dependencies = [
217
217
  ...(evaluation.materialResourceProvenance ?? []),
218
218
  ...(evaluation.materialLibraryDependencies ?? []).map((manifest) => ({
219
- kind: 'kiln.material.v1',
219
+ kind: `kiln.material.v${manifest.schemaVersion}`,
220
220
  delivery: 'runtime',
221
221
  manifest,
222
222
  })),
package/src/uv-data.ts ADDED
@@ -0,0 +1,238 @@
1
+ /** Internal safety and correspondence boundary for the xatlasjs adapter. */
2
+ import * as THREE from 'three';
3
+ import { GEOMETRY_ALLOCATION_LIMITS as limits } from './geometry-budget';
4
+
5
+ type Attribute = THREE.BufferAttribute | THREE.InterleavedBufferAttribute;
6
+
7
+ export interface AtlasMesh {
8
+ vertexCount: number;
9
+ vertex: { coords1?: Float32Array | number[] };
10
+ index?: Uint16Array | Uint32Array | number[];
11
+ oldIndexes: Uint16Array | Uint32Array | number[];
12
+ }
13
+
14
+ /** Reject truncated Uint16 output and reordered faces before copying protected data. */
15
+ export function validateAtlasRemap(mesh: AtlasMesh, inputIndex: Uint32Array, sourceCount: number) {
16
+ const count = mesh.vertexCount;
17
+ if (!Number.isSafeInteger(count) || count < 1 || count > limits.unwrapVertices)
18
+ throw new RangeError(
19
+ `autoUnwrap: xatlas output exceeds the supported 1..${limits.unwrapVertices} vertices. Split the mesh before unwrapping.`,
20
+ );
21
+ if (mesh.oldIndexes.length !== count)
22
+ throw new Error('autoUnwrap: xatlas returned an incomplete source-vertex map.');
23
+ for (const oldIndex of mesh.oldIndexes)
24
+ if (!Number.isSafeInteger(oldIndex) || oldIndex < 0 || oldIndex >= sourceCount)
25
+ throw new Error('autoUnwrap: xatlas returned an invalid source-vertex map.');
26
+ const index = mesh.index;
27
+ if (!index || index.length !== inputIndex.length)
28
+ throw new Error(
29
+ 'autoUnwrap: xatlas changed the triangle count; face correspondence is unsupported.',
30
+ );
31
+ for (let i = 0; i < index.length; i++) {
32
+ const vertex = index[i]!;
33
+ if (
34
+ !Number.isSafeInteger(vertex) ||
35
+ vertex < 0 ||
36
+ vertex >= count ||
37
+ mesh.oldIndexes[vertex] !== inputIndex[i]
38
+ )
39
+ throw new Error(
40
+ 'autoUnwrap: xatlas changed triangle correspondence; refusing to misassign vertex data or material groups.',
41
+ );
42
+ }
43
+ const uv = mesh.vertex.coords1;
44
+ if (!uv || uv.length !== count * 2 || !uv.every((v) => Number.isFinite(v) && v >= 0 && v <= 1))
45
+ throw new Error(
46
+ 'autoUnwrap: xatlas produced no valid UV coordinates (degenerate or zero-area triangles?).',
47
+ );
48
+ return { index, uv, oldIndexes: mesh.oldIndexes };
49
+ }
50
+
51
+ function interleaved(attribute: Attribute): attribute is THREE.InterleavedBufferAttribute {
52
+ return (attribute as THREE.InterleavedBufferAttribute).isInterleavedBufferAttribute === true;
53
+ }
54
+
55
+ /** Copy raw storage, including normalized integers and half floats, without welding. */
56
+ export function remapUnwrapAttribute(
57
+ attribute: Attribute,
58
+ oldIndexes: ArrayLike<number>,
59
+ ): THREE.BufferAttribute {
60
+ const ArrayType = attribute.array.constructor as new (length: number) => THREE.TypedArray;
61
+ const array = new ArrayType(oldIndexes.length * attribute.itemSize);
62
+ const stride = interleaved(attribute) ? attribute.data.stride : attribute.itemSize;
63
+ const offset = interleaved(attribute) ? attribute.offset : 0;
64
+ for (let i = 0; i < oldIndexes.length; i++) {
65
+ for (let c = 0; c < attribute.itemSize; c++)
66
+ array[i * attribute.itemSize + c] = attribute.array[oldIndexes[i]! * stride + offset + c]!;
67
+ }
68
+ const output = (attribute as Attribute & { isFloat16BufferAttribute?: boolean })
69
+ .isFloat16BufferAttribute
70
+ ? new THREE.Float16BufferAttribute(
71
+ array as Uint16Array,
72
+ attribute.itemSize,
73
+ attribute.normalized,
74
+ )
75
+ : new THREE.BufferAttribute(array, attribute.itemSize, attribute.normalized);
76
+ output.name = attribute.name;
77
+ output.setUsage(interleaved(attribute) ? attribute.data.usage : attribute.usage);
78
+ if (!interleaved(attribute)) output.gpuType = attribute.gpuType;
79
+ return output;
80
+ }
81
+
82
+ function validateAttribute(name: string, attribute: Attribute, count: number): void {
83
+ if (
84
+ (attribute as THREE.InstancedBufferAttribute).isInstancedBufferAttribute ||
85
+ (interleaved(attribute) &&
86
+ (attribute.data as THREE.InterleavedBuffer & { isInstancedInterleavedBuffer?: boolean })
87
+ .isInstancedInterleavedBuffer)
88
+ )
89
+ throw new TypeError(
90
+ `autoUnwrap: ${name} is per-instance data; unwrap the base geometry before instancing.`,
91
+ );
92
+ if (
93
+ attribute.count !== count ||
94
+ !Number.isSafeInteger(attribute.itemSize) ||
95
+ attribute.itemSize < 1 ||
96
+ attribute.itemSize > 4
97
+ )
98
+ throw new TypeError(
99
+ `autoUnwrap: ${name} must be a matching per-vertex attribute with 1..4 components.`,
100
+ );
101
+ if (
102
+ interleaved(attribute) &&
103
+ (!Number.isSafeInteger(attribute.offset) ||
104
+ attribute.offset < 0 ||
105
+ attribute.offset + attribute.itemSize > attribute.data.stride)
106
+ )
107
+ throw new TypeError(`autoUnwrap: ${name} has an invalid interleaved offset or stride.`);
108
+ }
109
+
110
+ /** Decode xyz through getters: .array alone ignores stride, normalization and half floats. */
111
+ function xyz(attribute: Attribute, name: string): Float32Array {
112
+ if (attribute.itemSize !== 3)
113
+ throw new TypeError(`autoUnwrap: ${name} requires three components.`);
114
+ const output = new Float32Array(attribute.count * 3);
115
+ for (let i = 0; i < attribute.count; i++) {
116
+ output[i * 3] = attribute.getX(i);
117
+ output[i * 3 + 1] = attribute.getY(i);
118
+ output[i * 3 + 2] = attribute.getZ(i);
119
+ }
120
+ if (!output.every(Number.isFinite))
121
+ throw new TypeError(`autoUnwrap: ${name} must contain finite Float32-representable values.`);
122
+ return output;
123
+ }
124
+
125
+ export function prepareUnwrap(
126
+ geometry: THREE.BufferGeometry,
127
+ opts: { resolution?: number; padding?: number; useNormals?: boolean },
128
+ ) {
129
+ const resolution = opts.resolution ?? 1024;
130
+ const padding = opts.padding ?? 2;
131
+ if (!Number.isSafeInteger(resolution) || resolution < 1 || resolution > limits.unwrapResolution)
132
+ throw new RangeError(
133
+ `autoUnwrap: resolution must be an integer from 1 to ${limits.unwrapResolution}.`,
134
+ );
135
+ if (!Number.isSafeInteger(padding) || padding < 0 || padding > resolution)
136
+ throw new RangeError('autoUnwrap: padding must be an integer from 0 to resolution.');
137
+ if (opts.useNormals !== undefined && typeof opts.useNormals !== 'boolean')
138
+ throw new TypeError('autoUnwrap: useNormals must be a boolean.');
139
+ if ((geometry as THREE.InstancedBufferGeometry).isInstancedBufferGeometry)
140
+ throw new TypeError('autoUnwrap: unwrap the base geometry before instancing.');
141
+ const position = geometry.getAttribute('position');
142
+ if (position?.itemSize !== 3)
143
+ throw new TypeError('autoUnwrap: geometry requires a three-component position attribute.');
144
+ const count = position.count;
145
+ const corners = geometry.index?.count ?? count;
146
+ if (count === 0 || corners === 0)
147
+ throw new Error(
148
+ 'autoUnwrap: geometry has no triangles. Check the operation that produced it (for example an empty CSG result).',
149
+ );
150
+ if (!Number.isSafeInteger(count) || count < 1 || count > limits.unwrapVertices)
151
+ throw new RangeError(
152
+ `autoUnwrap: input exceeds ${limits.unwrapVertices} vertices supported by xatlasjs. Split the mesh before unwrapping.`,
153
+ );
154
+ if (!Number.isSafeInteger(corners) || corners % 3 !== 0)
155
+ throw new TypeError(
156
+ 'autoUnwrap: geometry has an index count that is not a whole number of triangles.',
157
+ );
158
+ if (corners / 3 > limits.unwrapTriangles)
159
+ throw new RangeError(`autoUnwrap: triangle budget exceeds ${limits.unwrapTriangles}.`);
160
+ const attributes = Object.entries(geometry.attributes);
161
+ for (const [name, attribute] of attributes) validateAttribute(name, attribute, count);
162
+ const normal = geometry.getAttribute('normal');
163
+ if (normal && normal.itemSize !== 3)
164
+ throw new TypeError('autoUnwrap: normal requires three components.');
165
+ const useNormals = !!opts.useNormals && !!normal;
166
+ for (const [name, targets] of Object.entries(geometry.morphAttributes)) {
167
+ if (!['position', 'normal', 'color'].includes(name))
168
+ throw new TypeError(
169
+ `autoUnwrap: morph ${name} is unsupported; UV0 and tangents change during unwrapping.`,
170
+ );
171
+ if (name === 'normal' && targets.length && !useNormals)
172
+ throw new TypeError(
173
+ 'autoUnwrap: normal morphs require base normals and useNormals: true to preserve their reference frame.',
174
+ );
175
+ for (const attribute of targets) {
176
+ validateAttribute(`morph ${name}`, attribute, count);
177
+ const base = geometry.getAttribute(name);
178
+ if (!base || attribute.itemSize !== base.itemSize)
179
+ throw new TypeError(`autoUnwrap: morph ${name} requires a matching base attribute.`);
180
+ attributes.push([`morph ${name}`, attribute]);
181
+ }
182
+ }
183
+ // Bound owned output buffers before reading attributes or entering native code.
184
+ const perVertexBytes = attributes.reduce(
185
+ (sum, [, attr]) => sum + attr.itemSize * attr.array.BYTES_PER_ELEMENT,
186
+ 20,
187
+ );
188
+ if (
189
+ perVertexBytes * Math.min(count + corners, limits.unwrapVertices) >
190
+ limits.unwrapAttributeBytes
191
+ )
192
+ throw new RangeError(
193
+ `autoUnwrap: attribute budget exceeds ${limits.unwrapAttributeBytes} bytes.`,
194
+ );
195
+ for (const [name, attribute] of attributes) {
196
+ const getters = [attribute.getX, attribute.getY, attribute.getZ, attribute.getW];
197
+ for (let i = 0; i < count; i++) {
198
+ for (let c = 0; c < attribute.itemSize; c++) {
199
+ const value = getters[c]!.call(attribute, i);
200
+ if (!Number.isFinite(value))
201
+ throw new TypeError(`autoUnwrap: ${name} contains non-finite values.`);
202
+ }
203
+ }
204
+ }
205
+ if (geometry.index && (geometry.index.itemSize !== 1 || geometry.index.normalized))
206
+ throw new TypeError('autoUnwrap: index must be a scalar, non-normalized integer attribute.');
207
+ const index = new Uint32Array(corners);
208
+ for (let i = 0; i < corners; i++) {
209
+ const value = geometry.index ? geometry.index.getX(i) : i;
210
+ if (!Number.isSafeInteger(value) || value < 0 || value >= count)
211
+ throw new RangeError('autoUnwrap: index references an invalid source vertex.');
212
+ index[i] = value;
213
+ }
214
+ for (const { start, count: length, materialIndex = 0 } of geometry.groups) {
215
+ if (
216
+ !Number.isSafeInteger(start) ||
217
+ !Number.isSafeInteger(length) ||
218
+ start < 0 ||
219
+ length < 0 ||
220
+ start % 3 ||
221
+ length % 3 ||
222
+ start + length > corners ||
223
+ !Number.isSafeInteger(materialIndex) ||
224
+ materialIndex < 0
225
+ )
226
+ throw new TypeError(
227
+ 'autoUnwrap: groups must reference complete source triangles and nonnegative material indices.',
228
+ );
229
+ }
230
+ return {
231
+ index,
232
+ positions: xyz(position, 'position'),
233
+ normals: useNormals ? xyz(normal, 'normal') : null,
234
+ useNormals,
235
+ resolution,
236
+ padding,
237
+ };
238
+ }
package/src/uv-shapes.ts CHANGED
@@ -47,6 +47,9 @@ export function remapUV(
47
47
  values[i * 2 + 1] = nextV;
48
48
  }
49
49
  const cloned = geo.clone();
50
+ // BufferGeometry.clone() shares userData; owned UV results must not write
51
+ // warnings or later atlas bindings back into their source geometry.
52
+ cloned.userData = structuredClone(geo.userData);
50
53
  cloned.setAttribute('uv', new THREE.Float32BufferAttribute(values, 2));
51
54
  if ((scale[0] !== 1 || scale[1] !== 1) && cloned.hasAttribute('tangent')) {
52
55
  cloned.deleteAttribute('tangent');