@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
@@ -4,6 +4,21 @@ import {
4
4
  canonicalizeProceduralTextureSpecV2,
5
5
  type CanonicalPortableMaterialSpecV2,
6
6
  } from './procedural-material-v2';
7
+ import {
8
+ canonicalizeProceduralTextureSpecV3,
9
+ PROCEDURAL_TEXTURE_V3_COMPILER_ID,
10
+ } from './procedural-texture-v3-contract';
11
+ import { canonicalizePortableSamplerV1 } from './portable-sampler';
12
+ import type { CanonicalPortableMaterialSpecV3 } from './procedural-material-v3';
13
+ import {
14
+ canonicalizeTextureAtlasSpecV1,
15
+ validateTextureAtlasManifestV1,
16
+ TEXTURE_ATLAS_COMPILER_ID,
17
+ type CanonicalTextureAtlasSpecV1,
18
+ } from './texture-atlas-contract';
19
+ import type { TextureAtlasManifestV1 } from './texture-atlas-pixels';
20
+ import type { MaterialMapPinV1 } from './pinned-material-map';
21
+ import { makeMaterialAuthoringSchemas } from './material-authoring-schema';
7
22
 
8
23
  export const MATERIAL_LIBRARY_LIMITS = Object.freeze({
9
24
  maxMapBytes: 8 * 1024 * 1024,
@@ -301,9 +316,14 @@ export interface MaterialLibraryPayloadV1 {
301
316
  records: { manifest: MaterialManifestV1; files: Record<string, string> }[];
302
317
  }
303
318
  export interface MaterialLibrary {
304
- list(): Promise<MaterialManifestV1[]>;
305
- read(materialId: string, revisionId: string): Promise<MaterialRecordV1>;
306
- import(records: MaterialRecordV1[]): Promise<MaterialManifestV1[]>;
319
+ list(): Promise<MaterialManifest[]>;
320
+ read(materialId: string, revisionId: string): Promise<MaterialRecord>;
321
+ readManifest?(materialId: string, revisionId: string): Promise<MaterialManifest>;
322
+ readRecordFiles?(materialId: string, revisionId: string): Promise<MaterialRecord>;
323
+ import(
324
+ records: MaterialRecord[],
325
+ options?: { materials?: readonly MaterialRecord[] },
326
+ ): Promise<MaterialManifest[]>;
307
327
  }
308
328
  export const materialLibraryPayloadSchema = z
309
329
  .object({
@@ -455,7 +475,7 @@ export function validateMaterialRecordShape(record: MaterialRecordV1): MaterialM
455
475
  return manifest;
456
476
  }
457
477
  export function materialLibraryResourceId(
458
- manifest: MaterialManifestV1,
478
+ manifest: MaterialManifest,
459
479
  slot: MaterialLibrarySlot,
460
480
  ): string {
461
481
  materialLibraryHashSchema.parse(manifest.revisionId);
@@ -464,7 +484,35 @@ export function materialLibraryResourceId(
464
484
  }
465
485
  export function materialLibraryPortableSpec(
466
486
  manifest: MaterialManifestV1,
467
- ): CanonicalPortableMaterialSpecV2 {
487
+ ): CanonicalPortableMaterialSpecV2;
488
+ export function materialLibraryPortableSpec(
489
+ manifest: MaterialManifestV2,
490
+ ): CanonicalPortableMaterialSpecV3;
491
+ export function materialLibraryPortableSpec(
492
+ manifest: MaterialManifest,
493
+ ): CanonicalPortableMaterialSpecV2 | CanonicalPortableMaterialSpecV3;
494
+ export function materialLibraryPortableSpec(
495
+ manifest: MaterialManifest,
496
+ ): CanonicalPortableMaterialSpecV2 | CanonicalPortableMaterialSpecV3 {
497
+ if (manifest.schemaVersion === 2) {
498
+ const modern = validateMaterialManifestAny(manifest) as MaterialManifestV2;
499
+ return {
500
+ schemaVersion: 3,
501
+ model: 'pbrMetallicRoughness',
502
+ name: modern.name,
503
+ ...modern.parameters,
504
+ textures: Object.fromEntries(
505
+ modern.maps.map((map) => [
506
+ map.slot,
507
+ {
508
+ kind: 'resource' as const,
509
+ resourceId: materialLibraryResourceId(modern, map.slot),
510
+ sampler: { ...map.sampler },
511
+ },
512
+ ]),
513
+ ),
514
+ };
515
+ }
468
516
  manifest = validateMaterialManifest(manifest);
469
517
  return {
470
518
  schemaVersion: 2,
@@ -479,3 +527,298 @@ export function materialLibraryPortableSpec(
479
527
  ),
480
528
  };
481
529
  }
530
+
531
+ /** New versions are explicit; every exported V1 schema/type above stays V1. */
532
+ const authoringSchemas = makeMaterialAuthoringSchemas(materialProceduralSpecSchema);
533
+ const samplerSchema = authoringSchemas.portableSamplerSchema.refine((value) => {
534
+ try {
535
+ return (
536
+ canonicalMaterialJson(value) === canonicalMaterialJson(canonicalizePortableSamplerV1(value))
537
+ );
538
+ } catch {
539
+ return false;
540
+ }
541
+ });
542
+ const v3SpecSchema = authoringSchemas.canonicalProceduralTextureSpecV3Schema.refine((value) => {
543
+ try {
544
+ return (
545
+ canonicalMaterialJson(value) ===
546
+ canonicalMaterialJson(canonicalizeProceduralTextureSpecV3(value))
547
+ );
548
+ } catch {
549
+ return false;
550
+ }
551
+ });
552
+ const atlasSpecSchema: z.ZodType<CanonicalTextureAtlasSpecV1> =
553
+ authoringSchemas.canonicalTextureAtlasSpecV1Schema.refine((value) => {
554
+ try {
555
+ return (
556
+ canonicalMaterialJson(value) ===
557
+ canonicalMaterialJson(canonicalizeTextureAtlasSpecV1(value))
558
+ );
559
+ } catch {
560
+ return false;
561
+ }
562
+ });
563
+ const atlasManifestSchema: z.ZodType<TextureAtlasManifestV1> =
564
+ authoringSchemas.textureAtlasManifestV1Schema;
565
+ export const materialMapPinSchema = z
566
+ .object({
567
+ resourceId: materialLibraryIdSchema,
568
+ revisionId: materialLibraryHashSchema,
569
+ sha256: materialLibraryHashSchema,
570
+ })
571
+ .strict()
572
+ .refine((pin) => pin.sha256 === pin.revisionId, 'Material pin hash must match revision');
573
+ const materialRecipeV2Schema = z.discriminatedUnion('kind', [
574
+ z
575
+ .object({
576
+ kind: z.literal('procedural'),
577
+ compilerId: z.enum(['kiln.procedural-texture.v2', PROCEDURAL_TEXTURE_V3_COMPILER_ID]),
578
+ spec: z.union([materialProceduralSpecSchema, v3SpecSchema]),
579
+ recipeHash: materialLibraryHashSchema,
580
+ pixelHash: materialLibraryHashSchema,
581
+ encoder: z.object({ name: z.literal('sharp'), version: text }).strict(),
582
+ derive: materialNormalDerivationSchema.optional(),
583
+ })
584
+ .strict(),
585
+ z
586
+ .object({
587
+ kind: z.literal('atlas'),
588
+ compilerId: z.literal(TEXTURE_ATLAS_COMPILER_ID),
589
+ spec: atlasSpecSchema,
590
+ manifest: atlasManifestSchema,
591
+ encoder: z.object({ name: z.literal('sharp'), version: text }).strict(),
592
+ })
593
+ .strict()
594
+ .superRefine((recipe, context) => {
595
+ try {
596
+ validateTextureAtlasManifestV1(recipe.manifest, recipe.spec);
597
+ } catch {
598
+ context.addIssue({ code: 'custom', message: 'Invalid stored atlas manifest' });
599
+ }
600
+ }),
601
+ ]);
602
+ export const materialManifestV2Schema = materialManifestSchema
603
+ .omit({ schemaVersion: true, maps: true })
604
+ .extend({
605
+ schemaVersion: z.literal(2),
606
+ dependencies: z.array(materialMapPinSchema).max(16),
607
+ maps: z
608
+ .array(
609
+ materialManifestSchema.shape.maps.element
610
+ .omit({ procedural: true })
611
+ .extend({
612
+ sampler: samplerSchema,
613
+ pixelHash: materialLibraryHashSchema,
614
+ recipe: materialRecipeV2Schema.optional(),
615
+ })
616
+ .strict(),
617
+ )
618
+ .min(1)
619
+ .max(5),
620
+ })
621
+ .strict();
622
+ export type MaterialManifestV2 = z.infer<typeof materialManifestV2Schema>;
623
+ export type MaterialMapV2 = MaterialManifestV2['maps'][number];
624
+ export interface MaterialRecordV2 {
625
+ manifest: MaterialManifestV2;
626
+ files: Record<string, Uint8Array>;
627
+ }
628
+ export type MaterialManifest = MaterialManifestV1 | MaterialManifestV2;
629
+ export type MaterialRecord = MaterialRecordV1 | MaterialRecordV2;
630
+ export interface MaterialLibraryPayloadV2 {
631
+ schemaVersion: 2;
632
+ records: { manifest: MaterialManifest; files: Record<string, string> }[];
633
+ }
634
+ export type MaterialLibraryPayload = MaterialLibraryPayloadV1 | MaterialLibraryPayloadV2;
635
+ export const materialManifestAnySchema = z.discriminatedUnion('schemaVersion', [
636
+ materialManifestSchema,
637
+ materialManifestV2Schema,
638
+ ]);
639
+ export const materialLibraryPayloadV2Schema = materialLibraryPayloadSchema
640
+ .extend({
641
+ schemaVersion: z.literal(2),
642
+ records: z
643
+ .array(
644
+ materialLibraryPayloadSchema.shape.records.element
645
+ .extend({ manifest: materialManifestAnySchema })
646
+ .strict(),
647
+ )
648
+ .max(MATERIAL_LIBRARY_LIMITS.maxPayloadRecords),
649
+ })
650
+ .strict();
651
+ export const materialLibraryPayloadAnySchema = z.discriminatedUnion('schemaVersion', [
652
+ materialLibraryPayloadSchema,
653
+ materialLibraryPayloadV2Schema,
654
+ ]);
655
+ export function materialRecordDependencyPins(manifest: MaterialManifest): MaterialMapPinV1[] {
656
+ return manifest.schemaVersion === 1 ? [] : manifest.dependencies.map((pin) => ({ ...pin }));
657
+ }
658
+ /** Opt-in JSON-only standalone creation; external bytes remain host-only. */
659
+ export const proceduralMaterialDraftV2Schema = proceduralMaterialDraftSchema
660
+ .extend({
661
+ schemaVersion: z.literal(2),
662
+ maps: z
663
+ .array(
664
+ proceduralMaterialDraftSchema.shape.maps.element
665
+ .omit({ procedural: true })
666
+ .extend({
667
+ sampler: samplerSchema,
668
+ procedural: z
669
+ .union([
670
+ materialProceduralSpecSchema,
671
+ authoringSchemas.proceduralTextureSpecV3Schema.refine((value) => {
672
+ try {
673
+ canonicalizeProceduralTextureSpecV3(value);
674
+ return true;
675
+ } catch {
676
+ return false;
677
+ }
678
+ }),
679
+ ])
680
+ .optional(),
681
+ atlas: authoringSchemas.textureAtlasSpecV1Schema
682
+ .refine((value) => {
683
+ try {
684
+ canonicalizeTextureAtlasSpecV1(value);
685
+ return true;
686
+ } catch {
687
+ return false;
688
+ }
689
+ })
690
+ .optional(),
691
+ })
692
+ .strict()
693
+ .refine(
694
+ (map) => (map.procedural !== undefined) !== (map.atlas !== undefined),
695
+ 'Supply exactly one procedural or atlas recipe',
696
+ ),
697
+ )
698
+ .min(1)
699
+ .max(5),
700
+ })
701
+ .strict();
702
+ export type ProceduralMaterialDraftV2 = z.infer<typeof proceduralMaterialDraftV2Schema>;
703
+ export const proceduralMaterialDraftAnySchema = z.union([
704
+ proceduralMaterialDraftSchema,
705
+ proceduralMaterialDraftV2Schema,
706
+ ]);
707
+ export function validateMaterialManifestAny(value: unknown): MaterialManifest {
708
+ assertMaterialJson(value);
709
+ if (
710
+ new TextEncoder().encode(JSON.stringify(value)).length >
711
+ MATERIAL_LIBRARY_LIMITS.maxManifestBytes
712
+ )
713
+ throw Error('Material manifest exceeds size limit');
714
+ if (!value || typeof value !== 'object' || Array.isArray(value))
715
+ throw Error('Invalid material manifest');
716
+ if ((value as Record<string, unknown>)['schemaVersion'] === 1)
717
+ return validateMaterialManifest(value);
718
+ const manifest = materialManifestV2Schema.parse(value);
719
+ if (
720
+ new Set(manifest.sources.map((s) => s.id)).size !== manifest.sources.length ||
721
+ new Set(manifest.maps.map((m) => m.slot)).size !== manifest.maps.length
722
+ )
723
+ throw Error('Duplicate material source or slot');
724
+ let bytes = 0,
725
+ pixels = 0;
726
+ const pins = new Map<string, MaterialMapPinV1>();
727
+ for (const map of manifest.maps) {
728
+ const source = manifest.sources.find((s) => s.id === map.sourceId);
729
+ if (
730
+ !source ||
731
+ map.file !== `${map.slot}.png` ||
732
+ map.usage !== MATERIAL_LIBRARY_USAGE[map.slot] ||
733
+ map.colorSpace !==
734
+ (map.slot === 'baseColor' || map.slot === 'emissive' ? 'srgb' : 'linear') ||
735
+ (map.slot === 'normal') !== (map.normalConvention === 'opengl') ||
736
+ (map.slot === 'metallicRoughness') !==
737
+ (map.channelPacking === 'r-occlusion-g-roughness-b-metallic')
738
+ )
739
+ throw Error('Material map source/usage/convention mismatch');
740
+ const recipe = map.recipe;
741
+ if (recipe) {
742
+ if (source.kind !== 'procedural' || map.originalFile)
743
+ throw Error('Procedural/atlas map source convention mismatch');
744
+ if (recipe.kind === 'procedural') {
745
+ const spec = recipe.spec;
746
+ if (spec.schemaVersion === 3) {
747
+ if (
748
+ recipe.compilerId !== PROCEDURAL_TEXTURE_V3_COMPILER_ID ||
749
+ recipe.derive ||
750
+ spec.width !== map.width ||
751
+ spec.height !== map.height
752
+ )
753
+ throw Error('V3 compiler/dimensions/derivation mismatch');
754
+ } else if (
755
+ recipe.compilerId !== 'kiln.procedural-texture.v2' ||
756
+ spec.size !== map.width ||
757
+ map.width !== map.height ||
758
+ canonicalMaterialJson(spec) !==
759
+ canonicalMaterialJson(canonicalizeProceduralTextureSpecV2(spec))
760
+ )
761
+ throw Error('V2 compiler/canonical recipe dimensions mismatch');
762
+ if (
763
+ spec.usage !== map.usage ||
764
+ recipe.pixelHash !== map.pixelHash ||
765
+ (recipe.derive && map.slot !== 'normal')
766
+ )
767
+ throw Error('Procedural recipe pixel/usage mismatch');
768
+ } else {
769
+ validateTextureAtlasManifestV1(recipe.manifest, recipe.spec);
770
+ if (
771
+ recipe.spec.width !== map.width ||
772
+ recipe.spec.height !== map.height ||
773
+ recipe.spec.usage !== map.usage ||
774
+ canonicalMaterialJson(recipe.spec.sampler) !== canonicalMaterialJson(map.sampler) ||
775
+ recipe.manifest.decodedPixelHash !== map.pixelHash
776
+ )
777
+ throw Error('Atlas recipe map mismatch');
778
+ for (const region of recipe.spec.regions)
779
+ if (region.source.kind === 'resource') {
780
+ const pin = region.source.pin;
781
+ pins.set(`${pin.resourceId}/${pin.revisionId}`, pin);
782
+ }
783
+ }
784
+ } else if (
785
+ source.kind !== 'external' ||
786
+ !source.originalFiles.some((f) => f.name === map.originalFile)
787
+ )
788
+ throw Error('External material source original file missing');
789
+ bytes += map.bytes;
790
+ pixels += map.width * map.height;
791
+ }
792
+ const actual = manifest.dependencies.map((p) => `${p.resourceId}/${p.revisionId}`);
793
+ if (
794
+ new Set(actual).size !== actual.length ||
795
+ actual.length !== pins.size ||
796
+ actual.some((k) => !pins.has(k))
797
+ )
798
+ throw Error('Material dependency pin inventory mismatch');
799
+ if (
800
+ bytes > MATERIAL_LIBRARY_LIMITS.maxRecordBytes ||
801
+ pixels > MATERIAL_LIBRARY_LIMITS.maxRecordPixels
802
+ )
803
+ throw Error('Material record exceeds byte/pixel budget');
804
+ return structuredClone(manifest);
805
+ }
806
+ export function validateMaterialRecordShapeAny(record: MaterialRecord): MaterialManifest {
807
+ assertMaterialJson(record, true);
808
+ if (record.manifest.schemaVersion === 1)
809
+ return validateMaterialRecordShape(record as MaterialRecordV1);
810
+ if (Object.keys(record).some((k) => k !== 'manifest' && k !== 'files'))
811
+ throw Error('Invalid material record');
812
+ const manifest = validateMaterialManifestAny(record.manifest);
813
+ if (!record.files || Object.keys(record.files).length !== manifest.maps.length)
814
+ throw Error('Material file inventory mismatch');
815
+ for (const map of manifest.maps) {
816
+ const bytes = record.files[map.file];
817
+ if (!(bytes instanceof Uint8Array) || bytes.length !== map.bytes)
818
+ throw Error('Material file inventory mismatch');
819
+ const size = materialPngDimensions(bytes);
820
+ if (size.width !== map.width || size.height !== map.height)
821
+ throw Error('Material PNG dimensions mismatch');
822
+ }
823
+ return manifest;
824
+ }
@@ -0,0 +1,43 @@
1
+ /** Host-only atlas map capability. Authored code receives neither this port nor bytes. */
2
+ import type { TextureUsage } from './texture-contract';
3
+ export type MaterialMapSlotV1 =
4
+ | 'baseColor'
5
+ | 'normal'
6
+ | 'metallicRoughness'
7
+ | 'emissive'
8
+ | 'occlusion';
9
+ export type MaterialHashV1 = `sha256:${string}`;
10
+ export interface MaterialMapPinV1 {
11
+ resourceId: string;
12
+ revisionId: string;
13
+ sha256: string;
14
+ }
15
+ export interface VerifiedPinnedMaterialMapDescriptorV1 {
16
+ materialId: string;
17
+ revisionId: MaterialHashV1;
18
+ resourceId: string;
19
+ slot: MaterialMapSlotV1;
20
+ usage: TextureUsage;
21
+ colorSpace: 'srgb' | 'linear';
22
+ width: number;
23
+ height: number;
24
+ encodedByteLength: number;
25
+ imageSha256: MaterialHashV1;
26
+ decodedPixelHash?: MaterialHashV1;
27
+ }
28
+ export interface OwnedMaterialMapPixelsV1 {
29
+ descriptor: VerifiedPinnedMaterialMapDescriptorV1;
30
+ pixels: Uint8Array;
31
+ decodedPixelHash: MaterialHashV1;
32
+ }
33
+ export interface PinnedMaterialMapPortV1 {
34
+ describePinnedMaterialMap(
35
+ mapResourceId: unknown,
36
+ pin: MaterialMapPinV1,
37
+ ): VerifiedPinnedMaterialMapDescriptorV1 | undefined;
38
+ resolvePinnedMaterialMapPixels(
39
+ mapResourceId: unknown,
40
+ pin: MaterialMapPinV1,
41
+ context?: { signal?: AbortSignal },
42
+ ): Promise<OwnedMaterialMapPixelsV1>;
43
+ }
@@ -12,6 +12,16 @@ import {
12
12
  } from './procedural-material-v2';
13
13
  import { proceduralTexture } from './procedural-texture';
14
14
  import { pbrMaterial, type PbrMaterialOptions, type TextureUsage } from './textures';
15
+ import { loadTexture } from './textures';
16
+ import {
17
+ admitPortableMaterialSpecV3,
18
+ PORTABLE_MATERIAL_V3_LIMITS,
19
+ PORTABLE_MATERIAL_SLOT_USAGE,
20
+ PortableMaterialV3Error,
21
+ type CanonicalPortableMaterialSpecV3,
22
+ } from './procedural-material-v3';
23
+ import { applyPortableSamplerV1 } from './portable-sampler-runtime';
24
+ import type { PreparedPortableTextureV3 } from './texture-resolver';
15
25
 
16
26
  const SLOT_USAGE = {
17
27
  baseColor: 'albedo',
@@ -107,3 +117,193 @@ export async function compilePortableMaterialSpecV2(
107
117
  spec satisfies CanonicalPortableMaterialSpecV2;
108
118
  return material;
109
119
  }
120
+
121
+ /** New V3 path owns every mutable texture/source/pixel/encoded-byte binding. */
122
+ export async function compilePortableMaterialSpecV3(
123
+ input: unknown,
124
+ options: { resolver?: TextureResolver; signal?: AbortSignal } = {},
125
+ ): Promise<THREE.MeshStandardMaterial> {
126
+ const owned = new Map<PortableSlot, THREE.Texture>();
127
+ try {
128
+ const admission = admitPortableMaterialSpecV3(input),
129
+ spec = admission.spec;
130
+ const resolver = options.resolver ?? DEFAULT_TEXTURE_RESOLVER;
131
+ const entries = Object.entries(spec.textures) as Array<
132
+ [PortableSlot, NonNullable<CanonicalPortableMaterialSpecV3['textures'][PortableSlot]>]
133
+ >;
134
+ // All slots are checked before even bounded encoded-byte preparation.
135
+ for (const [slot, ref] of entries)
136
+ if (ref.kind === 'resource') {
137
+ const descriptor = resolver.describeApprovedTexture?.(ref.resourceId);
138
+ if (
139
+ !descriptor?.allowedSlots.includes(slot) ||
140
+ descriptor.usage !== PORTABLE_MATERIAL_SLOT_USAGE[slot]
141
+ )
142
+ throw new ProceduralTextureError(
143
+ `${slot} requires an injected resource approved for its exact slot and usage.`,
144
+ );
145
+ if (!resolver.preparePortableTexture)
146
+ throw new ProceduralTextureError(
147
+ 'V3 needs a verified-byte preparation resolver; keep V2 for a legacy resolver.',
148
+ );
149
+ }
150
+ const prepared = new Map<PortableSlot, PreparedPortableTextureV3>();
151
+ let encoded = 0,
152
+ texels = admission.texels,
153
+ resourcePixels = 0;
154
+ for (const [slot, ref] of entries)
155
+ if (ref.kind === 'resource') {
156
+ options.signal?.throwIfAborted();
157
+ const raw = await resolver.preparePortableTexture!(ref.resourceId, {
158
+ signal: options.signal,
159
+ });
160
+ const d = structuredClone(raw.descriptor);
161
+ if (
162
+ !(raw.bytes instanceof Uint8Array) ||
163
+ raw.bytes.length < 24 ||
164
+ raw.bytes.length > 8 * 1024 * 1024 ||
165
+ d.encodedByteLength !== raw.bytes.length ||
166
+ d.resourceId !== ref.resourceId ||
167
+ d.usage !== PORTABLE_MATERIAL_SLOT_USAGE[slot] ||
168
+ !d.allowedSlots.includes(slot) ||
169
+ d.colorSpace !== (slot === 'baseColor' || slot === 'emissive' ? 'srgb' : 'linear') ||
170
+ !Number.isInteger(d.width) ||
171
+ !Number.isInteger(d.height) ||
172
+ d.width < 1 ||
173
+ d.height < 1 ||
174
+ d.width > 4096 ||
175
+ d.height > 4096 ||
176
+ !/^sha256:[a-f0-9]{64}$/.test(d.imageSha256)
177
+ )
178
+ throw new ProceduralTextureError('Prepared texture descriptor/bytes are invalid.');
179
+ const pixels = d.width * d.height;
180
+ texels += pixels;
181
+ resourcePixels += pixels;
182
+ encoded += raw.bytes.length;
183
+ // Decoded RGBA write/copy plus encoded snapshot/hash/decode passes are
184
+ // charged beside the sole procedural admission owner's work estimate.
185
+ const resourceWork = resourcePixels * 12 + encoded * 8;
186
+ if (admission.estimatedWorkUnits + resourceWork > PORTABLE_MATERIAL_V3_LIMITS.maxWork)
187
+ throw new ProceduralTextureError(
188
+ 'Aggregate material work budget exceeded before compilation or decoding.',
189
+ );
190
+ if (
191
+ texels > PORTABLE_MATERIAL_V3_LIMITS.maxTexels ||
192
+ encoded > PORTABLE_MATERIAL_V3_LIMITS.maxEncodedBytes
193
+ )
194
+ throw new ProceduralTextureError(
195
+ 'Aggregate resource texel/encoded-byte budget exceeded before decoding.',
196
+ );
197
+ const snapshot = {
198
+ descriptor: structuredClone(d),
199
+ bytes: raw.bytes.slice(),
200
+ userData: structuredClone(raw.userData),
201
+ };
202
+ const header = new DataView(
203
+ snapshot.bytes.buffer,
204
+ snapshot.bytes.byteOffset,
205
+ snapshot.bytes.byteLength,
206
+ );
207
+ if (
208
+ ![137, 80, 78, 71, 13, 10, 26, 10].every((n, i) => snapshot.bytes[i] === n) ||
209
+ String.fromCharCode(...snapshot.bytes.subarray(12, 16)) !== 'IHDR' ||
210
+ header.getUint32(16) !== d.width ||
211
+ header.getUint32(20) !== d.height
212
+ )
213
+ throw new ProceduralTextureError('Prepared texture PNG/header dimensions mismatch.');
214
+ const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', snapshot.bytes));
215
+ if (
216
+ `sha256:${Array.from(hash, (x) => x.toString(16).padStart(2, '0')).join('')}` !==
217
+ d.imageSha256
218
+ )
219
+ throw new ProceduralTextureError('Prepared texture encoded hash mismatch.');
220
+ prepared.set(slot, snapshot);
221
+ }
222
+ // Includes held outputs, decoder/copy allowances, encoded snapshots and generated mips.
223
+ const transient =
224
+ admission.conservativePixelAllocationBytes +
225
+ resourcePixels * 12 +
226
+ encoded * 3 +
227
+ Math.ceil((texels * 4) / 3);
228
+ if (transient > PORTABLE_MATERIAL_V3_LIMITS.maxTransientBytes)
229
+ throw new ProceduralTextureError(
230
+ 'Aggregate material allocation budget exceeded before decoding.',
231
+ );
232
+ for (const [slot, ref] of entries) {
233
+ options.signal?.throwIfAborted();
234
+ let texture: THREE.DataTexture;
235
+ if (ref.kind === 'procedural') texture = proceduralTexture(ref.spec);
236
+ else {
237
+ const source = prepared.get(slot)!;
238
+ texture = await loadTexture(source.bytes.slice(), {
239
+ usage: source.descriptor.usage,
240
+ name: ref.resourceId,
241
+ });
242
+ owned.set(slot, texture);
243
+ if (
244
+ texture.image.width !== source.descriptor.width ||
245
+ texture.image.height !== source.descriptor.height ||
246
+ !texture.image.data ||
247
+ texture.image.data.length !== source.descriptor.width * source.descriptor.height * 4
248
+ )
249
+ throw new ProceduralTextureError('Prepared texture decoded dimensions mismatch.');
250
+ const actual = texture.userData['kilnTexture'];
251
+ texture.userData = { ...structuredClone(source.userData), ...texture.userData };
252
+ texture.userData['kilnTexture'] = {
253
+ ...(source.userData['kilnTexture'] as Record<string, unknown>),
254
+ ...actual,
255
+ };
256
+ }
257
+ owned.set(slot, texture);
258
+ texture.flipY = false;
259
+ texture.premultiplyAlpha = false;
260
+ applyPortableSamplerV1(texture, ref.sampler);
261
+ texture.userData['kilnPortableBinding'] = {
262
+ schemaVersion: 3,
263
+ slot,
264
+ sampler: { ...ref.sampler },
265
+ };
266
+ }
267
+ options.signal?.throwIfAborted();
268
+ const material = pbrMaterial({
269
+ roughness: spec.roughness,
270
+ metalness: spec.metalness,
271
+ emissiveIntensity: spec.emissiveIntensity,
272
+ alphaMode: spec.alphaMode,
273
+ ...(spec.alphaMode === 'mask' ? { alphaCutoff: spec.alphaCutoff } : {}),
274
+ doubleSided: spec.doubleSided,
275
+ ...(owned.has('baseColor')
276
+ ? { albedo: owned.get('baseColor') }
277
+ : spec.baseColor !== undefined
278
+ ? { albedo: spec.baseColor }
279
+ : {}),
280
+ ...(owned.has('normal') ? { normal: owned.get('normal') } : {}),
281
+ ...(owned.has('metallicRoughness')
282
+ ? { metallicRoughness: owned.get('metallicRoughness') }
283
+ : {}),
284
+ ...(owned.has('emissive')
285
+ ? { emissive: owned.get('emissive') }
286
+ : spec.emissive !== undefined
287
+ ? { emissive: spec.emissive }
288
+ : {}),
289
+ ...(owned.has('occlusion') ? { aoMap: owned.get('occlusion') } : {}),
290
+ });
291
+ if (spec.name) material.name = spec.name;
292
+ if (owned.has('baseColor') && spec.baseColor !== undefined)
293
+ material.color.setHex(spec.baseColor);
294
+ if (owned.has('emissive') && spec.emissive !== undefined)
295
+ material.emissive.setHex(spec.emissive);
296
+ material.userData['kilnPortableMaterial'] = spec;
297
+ return material;
298
+ } catch (error) {
299
+ for (const texture of owned.values()) texture.dispose();
300
+ if (error instanceof PortableMaterialV3Error) throw error;
301
+ const wrapped = new PortableMaterialV3Error(
302
+ error instanceof ProceduralTextureError
303
+ ? error.message
304
+ : 'Texture preparation or compilation failed; verify injected bytes, metadata and cancellation state.',
305
+ );
306
+ wrapped.cause = error;
307
+ throw wrapped;
308
+ }
309
+ }
@@ -0,0 +1,40 @@
1
+ /** Apply only after the caller has established independent texture ownership. */
2
+ import {
3
+ ClampToEdgeWrapping,
4
+ LinearFilter,
5
+ LinearMipmapLinearFilter,
6
+ LinearMipmapNearestFilter,
7
+ MirroredRepeatWrapping,
8
+ NearestFilter,
9
+ NearestMipmapLinearFilter,
10
+ NearestMipmapNearestFilter,
11
+ RepeatWrapping,
12
+ type Texture,
13
+ } from 'three';
14
+ import { canonicalizePortableSamplerV1, type CanonicalPortableSamplerV1 } from './portable-sampler';
15
+ const min = {
16
+ nearest: NearestFilter,
17
+ linear: LinearFilter,
18
+ 'nearest-mipmap-nearest': NearestMipmapNearestFilter,
19
+ 'linear-mipmap-nearest': LinearMipmapNearestFilter,
20
+ 'nearest-mipmap-linear': NearestMipmapLinearFilter,
21
+ 'linear-mipmap-linear': LinearMipmapLinearFilter,
22
+ } as const;
23
+ const wrap = {
24
+ repeat: RepeatWrapping,
25
+ 'clamp-to-edge': ClampToEdgeWrapping,
26
+ 'mirrored-repeat': MirroredRepeatWrapping,
27
+ } as const;
28
+ export function applyPortableSamplerV1<T extends Texture>(
29
+ ownedTexture: T,
30
+ sampler: CanonicalPortableSamplerV1,
31
+ ): T {
32
+ const value = canonicalizePortableSamplerV1(sampler);
33
+ ownedTexture.minFilter = min[value.minFilter];
34
+ ownedTexture.magFilter = value.magFilter === 'nearest' ? NearestFilter : LinearFilter;
35
+ ownedTexture.wrapS = wrap[value.wrapS];
36
+ ownedTexture.wrapT = wrap[value.wrapT];
37
+ ownedTexture.generateMipmaps = value.minFilter.includes('mipmap');
38
+ ownedTexture.needsUpdate = true;
39
+ return ownedTexture;
40
+ }