@instruktlabs/kiln 1.2.1 → 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 (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +86 -1
  4. package/README.md +18 -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/install.md +7 -6
  18. package/docs/material-library.md +9 -5
  19. package/docs/migration.md +16 -0
  20. package/docs/procedural-surfaces-v3.md +276 -0
  21. package/docs/projects-and-live-review.md +1 -1
  22. package/docs/rendering.md +41 -0
  23. package/docs/runtime.md +3 -3
  24. package/docs/sdk.md +55 -2
  25. package/docs/tools-operations.md +2 -2
  26. package/docs/tools.md +3 -3
  27. package/lib/asset-materials-node.d.ts +3 -3
  28. package/lib/asset-materials-node.js +29 -33
  29. package/lib/assets-node.d.ts +1 -1
  30. package/lib/assets-node.js +11 -4
  31. package/lib/assets.d.ts +4 -2
  32. package/lib/assets.js +61 -16
  33. package/lib/atlas-provenance.d.ts +86 -0
  34. package/lib/atlas-provenance.js +448 -0
  35. package/lib/character.d.ts +11 -2
  36. package/lib/character.js +90 -2
  37. package/lib/community-exporter.d.ts +3 -2
  38. package/lib/community-exporter.js +139 -3
  39. package/lib/discovery/helper-contracts.js +161 -7
  40. package/lib/discovery/helper-specs.js +49 -4
  41. package/lib/discovery/shapes.js +20 -7
  42. package/lib/engine-identity.d.ts +1 -1
  43. package/lib/engine-identity.js +1 -1
  44. package/lib/evaluator/authoring-diagnostic.d.ts +1 -1
  45. package/lib/evaluator/authoring-diagnostic.js +12 -0
  46. package/lib/evaluator/protocol.d.ts +2 -2
  47. package/lib/evaluator/protocol.js +16 -6
  48. package/lib/evaluator/subprocess.js +2 -2
  49. package/lib/exporter-node.js +9 -1
  50. package/lib/geometry-budget.d.ts +7 -0
  51. package/lib/geometry-budget.js +8 -0
  52. package/lib/gltf-texture-binding.d.ts +6 -0
  53. package/lib/gltf-texture-binding.js +54 -0
  54. package/lib/material-authoring-node.d.ts +6 -0
  55. package/lib/material-authoring-node.js +49 -0
  56. package/lib/material-authoring-schema.d.ts +871 -0
  57. package/lib/material-authoring-schema.js +310 -0
  58. package/lib/material-export.d.ts +5 -0
  59. package/lib/material-export.js +21 -0
  60. package/lib/material-library-node.d.ts +34 -8
  61. package/lib/material-library-node.js +691 -11
  62. package/lib/material-library.d.ts +3704 -4
  63. package/lib/material-library.js +272 -0
  64. package/lib/mcp-core.d.ts +1 -1
  65. package/lib/pinned-material-map.d.ts +33 -0
  66. package/lib/pinned-material-map.js +1 -0
  67. package/lib/portable-material-runtime.d.ts +5 -0
  68. package/lib/portable-material-runtime.js +160 -0
  69. package/lib/portable-sampler-runtime.d.ts +4 -0
  70. package/lib/portable-sampler-runtime.js +26 -0
  71. package/lib/portable-sampler.d.ts +11 -0
  72. package/lib/portable-sampler.js +42 -0
  73. package/lib/primitives.d.ts +15 -3
  74. package/lib/primitives.js +24 -4
  75. package/lib/procedural-material-v2.d.ts +2 -0
  76. package/lib/procedural-material-v2.js +2 -1
  77. package/lib/procedural-material-v3.d.ts +72 -0
  78. package/lib/procedural-material-v3.js +162 -0
  79. package/lib/procedural-texture-v2-compiler.d.ts +31 -0
  80. package/lib/procedural-texture-v2-compiler.js +172 -0
  81. package/lib/procedural-texture-v3-contract.d.ts +125 -0
  82. package/lib/procedural-texture-v3-contract.js +333 -0
  83. package/lib/procedural-texture-v3.d.ts +12 -0
  84. package/lib/procedural-texture-v3.js +165 -0
  85. package/lib/procedural-texture.d.ts +9 -31
  86. package/lib/procedural-texture.js +43 -170
  87. package/lib/project-bundle-node.d.ts +1 -1
  88. package/lib/project-bundle-node.js +70 -13
  89. package/lib/project-bundle.d.ts +91 -3
  90. package/lib/project-bundle.js +48 -7
  91. package/lib/qa/atlas.d.ts +21 -0
  92. package/lib/qa/atlas.js +474 -0
  93. package/lib/qa/index.d.ts +1 -0
  94. package/lib/qa/index.js +1 -0
  95. package/lib/qa/material.js +2 -0
  96. package/lib/qa/skin-integrity.d.ts +18 -0
  97. package/lib/qa/skin-integrity.js +221 -0
  98. package/lib/render-service-host.js +4 -7
  99. package/lib/render.d.ts +3 -3
  100. package/lib/render.js +108 -16
  101. package/lib/skin-binding.d.ts +20 -0
  102. package/lib/skin-binding.js +144 -0
  103. package/lib/texture-atlas-contract.d.ts +97 -0
  104. package/lib/texture-atlas-contract.js +414 -0
  105. package/lib/texture-atlas-pixels.d.ts +73 -0
  106. package/lib/texture-atlas-pixels.js +290 -0
  107. package/lib/texture-atlas.d.ts +10 -0
  108. package/lib/texture-atlas.js +143 -0
  109. package/lib/texture-bake.d.ts +3 -0
  110. package/lib/texture-bake.js +66 -11
  111. package/lib/texture-resolver.d.ts +22 -0
  112. package/lib/texture-resolver.js +24 -0
  113. package/lib/tools/materials.js +8 -7
  114. package/lib/tools/registry.js +5 -16
  115. package/lib/tools/review.js +1 -1
  116. package/lib/uv-data.d.ts +32 -0
  117. package/lib/uv-data.js +172 -0
  118. package/lib/uv-shapes.js +3 -0
  119. package/lib/uv.d.ts +9 -3
  120. package/lib/uv.js +53 -82
  121. package/lib/views/camera.d.ts +3 -3
  122. package/lib/views/camera.js +32 -28
  123. package/lib/views/face-policy.d.ts +9 -0
  124. package/lib/views/face-policy.js +16 -0
  125. package/lib/views/glb.js +218 -24
  126. package/lib/views/pose.d.ts +7 -1
  127. package/lib/views/pose.js +63 -12
  128. package/lib/views/raster.d.ts +6 -2
  129. package/lib/views/raster.js +49 -37
  130. package/lib/views/scene-copy.d.ts +6 -0
  131. package/lib/views/scene-copy.js +73 -0
  132. package/lib/views/scene-raster.js +2 -1
  133. package/lib/views/triangle-coverage.d.ts +8 -0
  134. package/lib/views/triangle-coverage.js +58 -0
  135. package/lib/workspace-node.js +7 -3
  136. package/lib/workspace.d.ts +2 -2
  137. package/package.json +3 -1
  138. package/plugin.json +1 -1
  139. package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
  140. package/plugins/kiln-engine/README.md +3 -3
  141. package/plugins/kiln-engine/package-provenance.json +6 -6
  142. package/plugins/kiln-engine/plugin.json +1 -1
  143. package/plugins/kiln-engine/runtime.json +1 -1
  144. package/skills/kiln-author-asset/SKILL.md +1 -1
  145. package/skills/kiln-author-asset/references/camera-cli.md +13 -0
  146. package/skills/kiln-author-asset/references/geometry-recipes.md +134 -0
  147. package/skills/kiln-author-asset/references/projects-and-materials.md +32 -2
  148. package/skills/kiln-compose-scene/references/runtime-scenes.md +19 -0
  149. package/skills/kiln-qa-asset/references/integration-checks.md +37 -2
  150. package/src/asset-materials-node.ts +38 -45
  151. package/src/assets-node.ts +22 -5
  152. package/src/assets.ts +61 -20
  153. package/src/atlas-provenance.ts +522 -0
  154. package/src/character.ts +107 -2
  155. package/src/community-exporter.ts +143 -3
  156. package/src/discovery/helper-contracts.ts +188 -7
  157. package/src/discovery/helper-specs.ts +69 -4
  158. package/src/discovery/shapes.ts +25 -9
  159. package/src/engine-identity.ts +1 -1
  160. package/src/evaluator/authoring-diagnostic.ts +19 -1
  161. package/src/evaluator/protocol.ts +19 -9
  162. package/src/evaluator/subprocess.ts +2 -2
  163. package/src/exporter-node.ts +12 -1
  164. package/src/generated/mcp-manifest.json +3 -3
  165. package/src/geometry-budget.ts +8 -0
  166. package/src/gltf-texture-binding.ts +60 -0
  167. package/src/material-authoring-node.ts +68 -0
  168. package/src/material-authoring-schema.ts +343 -0
  169. package/src/material-export.ts +25 -0
  170. package/src/material-library-node.ts +840 -16
  171. package/src/material-library.ts +348 -5
  172. package/src/pinned-material-map.ts +43 -0
  173. package/src/portable-material-runtime.ts +200 -0
  174. package/src/portable-sampler-runtime.ts +40 -0
  175. package/src/portable-sampler.ts +54 -0
  176. package/src/primitives.ts +98 -4
  177. package/src/procedural-material-v2.ts +2 -1
  178. package/src/procedural-material-v3.ts +224 -0
  179. package/src/procedural-texture-v2-compiler.ts +234 -0
  180. package/src/procedural-texture-v3-contract.ts +437 -0
  181. package/src/procedural-texture-v3.ts +220 -0
  182. package/src/procedural-texture.ts +61 -229
  183. package/src/project-bundle-node.ts +106 -14
  184. package/src/project-bundle.ts +61 -18
  185. package/src/project-cli.ts +29 -17
  186. package/src/qa/atlas.ts +597 -0
  187. package/src/qa/index.ts +1 -0
  188. package/src/qa/material.ts +2 -0
  189. package/src/qa/skin-integrity.ts +291 -0
  190. package/src/render-service-host.ts +4 -6
  191. package/src/render.ts +133 -15
  192. package/src/skin-binding.ts +225 -0
  193. package/src/texture-atlas-contract.ts +558 -0
  194. package/src/texture-atlas-pixels.ts +456 -0
  195. package/src/texture-atlas.ts +185 -0
  196. package/src/texture-bake.ts +88 -10
  197. package/src/texture-resolver.ts +55 -0
  198. package/src/tools/materials.ts +15 -11
  199. package/src/tools/registry.ts +5 -15
  200. package/src/tools/review.ts +1 -1
  201. package/src/uv-data.ts +238 -0
  202. package/src/uv-shapes.ts +3 -0
  203. package/src/uv.ts +73 -128
  204. package/src/viewer/app.ts +37 -0
  205. package/src/viewer/live-panel.ts +57 -1
  206. package/src/viewer/material-preview-panel.ts +140 -0
  207. package/src/viewer/material-preview.ts +138 -0
  208. package/src/viewer/materials-panel.ts +34 -5
  209. package/src/viewer/navigation.ts +2 -0
  210. package/src/views/camera.ts +37 -40
  211. package/src/views/face-policy.ts +20 -0
  212. package/src/views/glb.ts +294 -17
  213. package/src/views/pose.ts +71 -13
  214. package/src/views/raster.ts +73 -46
  215. package/src/views/scene-copy.ts +86 -0
  216. package/src/views/scene-raster.ts +2 -1
  217. package/src/views/triangle-coverage.ts +78 -0
  218. package/src/workspace-node.ts +11 -3
  219. package/src/workspace.ts +2 -2
@@ -0,0 +1,54 @@
1
+ /** Shared portable sampler contract; no renderer, loader or native dependencies. */
2
+ import { ProceduralTextureError } from './procedural-material-v2';
3
+
4
+ export const PORTABLE_MIN_FILTERS_V1 = [
5
+ 'nearest',
6
+ 'linear',
7
+ 'nearest-mipmap-nearest',
8
+ 'linear-mipmap-nearest',
9
+ 'nearest-mipmap-linear',
10
+ 'linear-mipmap-linear',
11
+ ] as const;
12
+ export const PORTABLE_MAG_FILTERS_V1 = ['nearest', 'linear'] as const;
13
+ export const PORTABLE_WRAPS_V1 = ['repeat', 'clamp-to-edge', 'mirrored-repeat'] as const;
14
+ export interface PortableSamplerV1 {
15
+ minFilter: (typeof PORTABLE_MIN_FILTERS_V1)[number];
16
+ magFilter: (typeof PORTABLE_MAG_FILTERS_V1)[number];
17
+ wrapS: (typeof PORTABLE_WRAPS_V1)[number];
18
+ wrapT: (typeof PORTABLE_WRAPS_V1)[number];
19
+ }
20
+ export type CanonicalPortableSamplerV1 = PortableSamplerV1;
21
+ export function canonicalizePortableSamplerV1(input: unknown): CanonicalPortableSamplerV1 {
22
+ if (!input || typeof input !== 'object' || Array.isArray(input))
23
+ throw new ProceduralTextureError('portableSampler requires a complete JSON object.');
24
+ const prototype = Object.getPrototypeOf(input);
25
+ if (prototype !== Object.prototype && prototype !== null)
26
+ throw new ProceduralTextureError('portableSampler must be plain JSON data.');
27
+ for (const key of Reflect.ownKeys(input)) {
28
+ const descriptor = Object.getOwnPropertyDescriptor(input, key)!;
29
+ if (typeof key !== 'string' || !descriptor.enumerable || !('value' in descriptor))
30
+ throw new ProceduralTextureError(
31
+ 'portableSampler requires enumerable JSON data fields, never accessors.',
32
+ );
33
+ }
34
+ const value = input as Record<string, unknown>;
35
+ const allowed = {
36
+ minFilter: PORTABLE_MIN_FILTERS_V1,
37
+ magFilter: PORTABLE_MAG_FILTERS_V1,
38
+ wrapS: PORTABLE_WRAPS_V1,
39
+ wrapT: PORTABLE_WRAPS_V1,
40
+ };
41
+ if (Object.keys(value).some((key) => !Object.hasOwn(allowed, key)))
42
+ throw new ProceduralTextureError('portableSampler contains an unsupported field.');
43
+ for (const [key, options] of Object.entries(allowed))
44
+ if (!(options as readonly unknown[]).includes(value[key]))
45
+ throw new ProceduralTextureError(
46
+ `portableSampler.${key} requires one of ${options.join(', ')}.`,
47
+ );
48
+ return {
49
+ minFilter: value['minFilter'],
50
+ magFilter: value['magFilter'],
51
+ wrapS: value['wrapS'],
52
+ wrapT: value['wrapT'],
53
+ } as CanonicalPortableSamplerV1;
54
+ }
package/src/primitives.ts CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  GEOMETRY_ALLOCATION_LIMITS,
20
20
  } from './geometry-budget';
21
21
  import { buildWallPanels } from './wall-panels';
22
- import { createJointChain } from './character';
22
+ import { createBoneChain, createJointChain, createSkinnedPart } from './character';
23
23
  import { describeAssembly, replicateAssembly } from './assembly';
24
24
  import { defineLod } from './lod';
25
25
  export { defineLod } from './lod';
@@ -57,8 +57,12 @@ import * as profile from './profile';
57
57
  import * as solids from './solids';
58
58
  import * as proceduralTextures from './procedural-texture';
59
59
  import * as textures from './textures';
60
+ import { textureAtlas, remapUVToAtlas } from './texture-atlas';
60
61
  import type { TextureResolver } from './texture-resolver';
61
- import { compilePortableMaterialSpecV2 } from './portable-material-runtime';
62
+ import {
63
+ compilePortableMaterialSpecV2,
64
+ compilePortableMaterialSpecV3,
65
+ } from './portable-material-runtime';
62
66
  import { createVehicleFrame, createWheelAssembly, createWheelGeometrySet } from './vehicle';
63
67
  import {
64
68
  stampSemanticMetadataV1,
@@ -72,6 +76,36 @@ export { projectUV } from './uv-project';
72
76
  export type { ProjectUVOptions } from './uv-project';
73
77
  export { remapUV } from './uv-shapes';
74
78
  export type { RemapUVOptions } from './uv-shapes';
79
+ export { textureAtlas, remapUVToAtlas } from './texture-atlas';
80
+ export type { AtlasUVOptionsV1 } from './texture-atlas';
81
+ export {
82
+ TextureAtlasError,
83
+ TEXTURE_ATLAS_COMPILER_ID,
84
+ TEXTURE_ATLAS_LIMITS,
85
+ canonicalizeTextureAtlasSpecV1,
86
+ canonicalTextureAtlasJsonV1,
87
+ hashTextureAtlasSpecV1,
88
+ } from './texture-atlas-contract';
89
+ export type {
90
+ AtlasRectangleV1,
91
+ AtlasEdgeModeV1,
92
+ AtlasResourceExpectationV1,
93
+ TextureAtlasSourceV1,
94
+ TextureAtlasRegionV1,
95
+ TextureAtlasSpecV1,
96
+ CanonicalTextureAtlasSourceV1,
97
+ CanonicalTextureAtlasRegionV1,
98
+ CanonicalTextureAtlasSpecV1,
99
+ } from './texture-atlas-contract';
100
+ export { compileTextureAtlasSpecV1 } from './texture-atlas-pixels';
101
+ export type {
102
+ TextureAtlasCompileOptionsV1,
103
+ TextureAtlasBudgetV1,
104
+ TextureAtlasSourceManifestV1,
105
+ TextureAtlasRegionManifestV1,
106
+ TextureAtlasManifestV1,
107
+ CompiledTextureAtlasV1,
108
+ } from './texture-atlas-pixels';
75
109
 
76
110
  export {
77
111
  createGableEndPanel,
@@ -86,8 +120,10 @@ export type {
86
120
  RoofFaceFrame,
87
121
  RoofSurfaceLayoutOptions,
88
122
  } from './architecture';
89
- export { createJointChain } from './character';
123
+ export { createBoneChain, createJointChain, createSkinnedPart } from './character';
90
124
  export type {
125
+ BoneChainResultV1,
126
+ CreateSkinnedPartOptions,
91
127
  CreateJointChainOptionsV1,
92
128
  JointChainResultV1,
93
129
  JointChainSegmentV1,
@@ -100,7 +136,50 @@ export {
100
136
  export { materialRecipe } from './material-recipe-runtime';
101
137
  export type { ApplyMaterialRecipeOptions } from './material-recipe-runtime';
102
138
  export type { MaterialRecipeId, MaterialRecipeOverridesV1 } from './material-recipes';
103
- export { compilePortableMaterialSpecV2 } from './portable-material-runtime';
139
+ export {
140
+ compilePortableMaterialSpecV2,
141
+ compilePortableMaterialSpecV3,
142
+ } from './portable-material-runtime';
143
+ export {
144
+ canonicalizePortableMaterialSpecV3,
145
+ PortableMaterialV3Error,
146
+ PORTABLE_MATERIAL_V3_LIMITS,
147
+ } from './procedural-material-v3';
148
+ export type {
149
+ PortableMaterialSpecV3,
150
+ CanonicalPortableMaterialSpecV3,
151
+ PortableTextureRefV3,
152
+ CanonicalPortableTextureRefV3,
153
+ } from './procedural-material-v3';
154
+ export { canonicalizePortableSamplerV1 } from './portable-sampler';
155
+ export type { PortableSamplerV1, CanonicalPortableSamplerV1 } from './portable-sampler';
156
+ export {
157
+ compileProceduralTextureSpecV3,
158
+ canonicalizeProceduralTextureSpecV3,
159
+ canonicalProceduralTextureJsonV3,
160
+ hashProceduralTextureSpecV3,
161
+ PROCEDURAL_TEXTURE_V3_COMPILER_ID,
162
+ PROCEDURAL_TEXTURE_SPEC_VERSION_V3,
163
+ MAX_PROCEDURAL_V3_EDGE,
164
+ MAX_PROCEDURAL_V3_TEXELS,
165
+ MAX_PROCEDURAL_V3_OPERATIONS,
166
+ MAX_PROCEDURAL_V3_VERTICES,
167
+ MAX_PROCEDURAL_V3_STAMPS,
168
+ MAX_PROCEDURAL_V3_PALETTE,
169
+ MAX_PROCEDURAL_V3_WORK,
170
+ MAX_PROCEDURAL_V3_TRANSIENT_BYTES,
171
+ } from './procedural-texture-v3';
172
+ export type {
173
+ CompiledProceduralTextureV3,
174
+ RgbaV3,
175
+ PixelPointV3,
176
+ PixelRectangleV3,
177
+ ProceduralShapeV3,
178
+ ProceduralOperationV3,
179
+ CanonicalProceduralOperationV3,
180
+ ProceduralTextureSpecV3,
181
+ CanonicalProceduralTextureSpecV3,
182
+ } from './procedural-texture-v3';
104
183
  export {
105
184
  MAX_PORTABLE_MATERIAL_TEXELS,
106
185
  MAX_PORTABLE_MATERIAL_TEXTURES,
@@ -2173,6 +2252,8 @@ export function buildSandboxGlobals(
2173
2252
  replicateAssembly: wrap('replicateAssembly', replicateAssembly),
2174
2253
  createPivot: wrap('createPivot', createPivot),
2175
2254
  createJointChain: wrap('createJointChain', createJointChain),
2255
+ createBoneChain: wrap('createBoneChain', createBoneChain),
2256
+ createSkinnedPart: wrap('createSkinnedPart', createSkinnedPart),
2176
2257
  createVehicleFrame: wrap('createVehicleFrame', createVehicleFrame),
2177
2258
  createWheelGeometrySet: wrap('createWheelGeometrySet', createWheelGeometrySet),
2178
2259
  createWheelAssembly: wrap('createWheelAssembly', createWheelAssembly),
@@ -2223,6 +2304,13 @@ export function buildSandboxGlobals(
2223
2304
  );
2224
2305
  return compilePortableMaterialSpecV2(args[0], { resolver: options.textureResolver });
2225
2306
  }),
2307
+ compilePortableMaterialSpecV3: wrap('compilePortableMaterialSpecV3', (...args: unknown[]) => {
2308
+ if (args.length !== 1)
2309
+ return Promise.reject(
2310
+ new TypeError('compilePortableMaterialSpecV3 accepts exactly one material spec'),
2311
+ );
2312
+ return compilePortableMaterialSpecV3(args[0], { resolver: options.textureResolver });
2313
+ }),
2226
2314
  basicMaterial: wrap('basicMaterial', basicMaterial),
2227
2315
  glassMaterial: wrap('glassMaterial', glassMaterial),
2228
2316
  lambertMaterial: wrap('lambertMaterial', lambertMaterial),
@@ -2275,6 +2363,7 @@ export function buildSandboxGlobals(
2275
2363
  // Shape-aware unwraps (sync — preserve built-in directional UVs)
2276
2364
  projectUV: wrap('projectUV', projectUV),
2277
2365
  remapUV: wrap('remapUV', uvShapes.remapUV),
2366
+ remapUVToAtlas: wrap('remapUVToAtlas', remapUVToAtlas),
2278
2367
  // Parametric primitives
2279
2368
  gearGeo: wrapGeo('gearGeo', gears.gearGeo),
2280
2369
  bladeGeo: wrapGeo('bladeGeo', gears.bladeGeo),
@@ -2283,6 +2372,11 @@ export function buildSandboxGlobals(
2283
2372
  loadApprovedTexture: wrap('loadApprovedTexture', loadApprovedTexture),
2284
2373
  // Procedural textures (sync — no I/O, so no await)
2285
2374
  proceduralTexture: wrap('proceduralTexture', proceduralTextures.proceduralTexture),
2375
+ textureAtlas: wrap('textureAtlas', (...args: unknown[]) => {
2376
+ if (args.length !== 1)
2377
+ return Promise.reject(new TypeError('textureAtlas accepts exactly one atlas spec'));
2378
+ return textureAtlas(args[0], { resources: options.textureResolver?.pinnedMaterialMaps });
2379
+ }),
2286
2380
  normalMapFromHeight: wrap('normalMapFromHeight', proceduralTextures.normalMapFromHeight),
2287
2381
  pbrMaterial: wrap('pbrMaterial', textures.pbrMaterial),
2288
2382
  foliageMaterial: wrap('foliageMaterial', textures.foliageMaterial),
@@ -519,7 +519,8 @@ const SHA256_K = new Uint32Array([
519
519
  const rotateRight = (value: number, bits: number): number =>
520
520
  (value >>> bits) | (value << (32 - bits));
521
521
 
522
- function sha256Hex(text: string): string {
522
+ /** Browser-safe SHA-256 shared by versioned recipe contracts. */
523
+ export function sha256Hex(text: string): string {
523
524
  const source = new TextEncoder().encode(text);
524
525
  const bitLength = source.length * 8;
525
526
  const paddedLength = Math.ceil((source.length + 9) / 64) * 64;
@@ -0,0 +1,224 @@
1
+ /** Strict opt-in PBR material data. Pixel/byte loading is outside this module. */
2
+ import {
3
+ canonicalizePortableMaterialSpecV2,
4
+ ProceduralTextureError,
5
+ } from './procedural-material-v2';
6
+ import {
7
+ admitProceduralTextureSpecV3,
8
+ type CanonicalProceduralTextureSpecV3,
9
+ type ProceduralTextureSpecV3,
10
+ } from './procedural-texture-v3-contract';
11
+ import {
12
+ canonicalizePortableSamplerV1,
13
+ type CanonicalPortableSamplerV1,
14
+ type PortableSamplerV1,
15
+ } from './portable-sampler';
16
+ import type { MaterialMapSlotV1 } from './pinned-material-map';
17
+
18
+ export const PORTABLE_MATERIAL_V3_LIMITS = Object.freeze({
19
+ maxBindings: 5,
20
+ maxTexels: 4 * 1024 * 1024,
21
+ maxEncodedBytes: 16 * 1024 * 1024,
22
+ maxWork: 64_000_000,
23
+ maxTransientBytes: 64 * 1024 * 1024,
24
+ maxJsonBytes: 1024 * 1024,
25
+ });
26
+ export const PORTABLE_MATERIAL_SLOT_USAGE = {
27
+ baseColor: 'albedo',
28
+ normal: 'normal',
29
+ metallicRoughness: 'metallicRoughness',
30
+ emissive: 'emissive',
31
+ occlusion: 'occlusion',
32
+ } as const;
33
+ export type PortableTextureRefV3 =
34
+ | { kind: 'procedural'; spec: ProceduralTextureSpecV3; sampler: PortableSamplerV1 }
35
+ | { kind: 'resource'; resourceId: string; sampler: PortableSamplerV1 };
36
+ export type CanonicalPortableTextureRefV3 =
37
+ | {
38
+ kind: 'procedural';
39
+ spec: CanonicalProceduralTextureSpecV3;
40
+ sampler: CanonicalPortableSamplerV1;
41
+ }
42
+ | { kind: 'resource'; resourceId: string; sampler: CanonicalPortableSamplerV1 };
43
+ export interface PortableMaterialSpecV3 {
44
+ schemaVersion: 3;
45
+ model: 'pbrMetallicRoughness';
46
+ name?: string;
47
+ baseColor?: number;
48
+ roughness?: number;
49
+ metalness?: number;
50
+ emissive?: number;
51
+ emissiveIntensity?: number;
52
+ alphaMode?: 'opaque' | 'mask' | 'blend';
53
+ alphaCutoff?: number;
54
+ doubleSided?: boolean;
55
+ textures?: Partial<Record<MaterialMapSlotV1, PortableTextureRefV3>>;
56
+ }
57
+ export interface CanonicalPortableMaterialSpecV3
58
+ extends Omit<
59
+ PortableMaterialSpecV3,
60
+ | 'textures'
61
+ | 'roughness'
62
+ | 'metalness'
63
+ | 'emissiveIntensity'
64
+ | 'alphaMode'
65
+ | 'alphaCutoff'
66
+ | 'doubleSided'
67
+ > {
68
+ roughness: number;
69
+ metalness: number;
70
+ emissiveIntensity: number;
71
+ alphaMode: 'opaque' | 'mask' | 'blend';
72
+ alphaCutoff: number;
73
+ doubleSided: boolean;
74
+ textures: Partial<Record<MaterialMapSlotV1, CanonicalPortableTextureRefV3>>;
75
+ }
76
+ export class PortableMaterialV3Error extends ProceduralTextureError {
77
+ constructor(message: string) {
78
+ super(`portableMaterial V3: ${message}`, 'PORTABLE_MATERIAL_V3_CONTRACT');
79
+ this.name = 'PortableMaterialV3Error';
80
+ }
81
+ }
82
+ function fail(message: string): never {
83
+ throw new PortableMaterialV3Error(message);
84
+ }
85
+ /** Bound untrusted data before reading any accessor or making canonical copies. */
86
+ function assertJson(input: unknown): void {
87
+ let nodes = 0;
88
+ const visit = (value: unknown, depth: number): void => {
89
+ if (++nodes > 65536 || depth > 16) fail('JSON size/depth budget exceeded.');
90
+ if (value === null || typeof value === 'boolean') return;
91
+ if (typeof value === 'string') {
92
+ if (value.length > PORTABLE_MATERIAL_V3_LIMITS.maxJsonBytes) fail('string budget exceeded.');
93
+ return;
94
+ }
95
+ if (typeof value === 'number' && Number.isFinite(value)) return;
96
+ if (!value || typeof value !== 'object') fail('only plain JSON data is supported.');
97
+ const array = Array.isArray(value),
98
+ proto = Object.getPrototypeOf(value);
99
+ if (array ? proto !== Array.prototype : proto !== Object.prototype && proto !== null)
100
+ fail('only plain JSON prototypes are supported.');
101
+ const descriptors = Object.getOwnPropertyDescriptors(value),
102
+ keys = Reflect.ownKeys(value);
103
+ if (keys.length > 1025) fail('field/array budget exceeded.');
104
+ const length = array ? (descriptors['length']?.value as number) : undefined;
105
+ if (array && (length! > 1024 || keys.length !== length! + 1))
106
+ fail('arrays must be bounded dense JSON data.');
107
+ for (const key of keys) {
108
+ if (array && key === 'length') continue;
109
+ if (typeof key !== 'string' || ['__proto__', 'prototype', 'constructor'].includes(key))
110
+ fail('forbidden JSON field.');
111
+ const d = descriptors[key]!;
112
+ if (!d.enumerable || !('value' in d)) fail('JSON accessors/hidden fields are unsupported.');
113
+ if (array && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= length!))
114
+ fail('invalid array key.');
115
+ visit(d.value, depth + 1);
116
+ }
117
+ };
118
+ visit(input, 0);
119
+ if (
120
+ new TextEncoder().encode(JSON.stringify(input)).length >
121
+ PORTABLE_MATERIAL_V3_LIMITS.maxJsonBytes
122
+ )
123
+ fail('JSON byte budget exceeded.');
124
+ }
125
+ function record(value: unknown, keys: readonly string[]): Record<string, unknown> {
126
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('expected a JSON object.');
127
+ const result = value as Record<string, unknown>;
128
+ if (Object.keys(result).some((key) => !keys.includes(key)))
129
+ fail('unknown field in material or texture reference.');
130
+ return result;
131
+ }
132
+ export interface PortableMaterialAdmissionV3 {
133
+ spec: CanonicalPortableMaterialSpecV3;
134
+ texels: number;
135
+ estimatedWorkUnits: number;
136
+ conservativePixelAllocationBytes: number;
137
+ }
138
+ function admitMaterial(input: unknown): PortableMaterialAdmissionV3 {
139
+ assertJson(input);
140
+ const scalarKeys = [
141
+ 'baseColor',
142
+ 'roughness',
143
+ 'metalness',
144
+ 'emissive',
145
+ 'emissiveIntensity',
146
+ 'alphaMode',
147
+ 'alphaCutoff',
148
+ 'doubleSided',
149
+ ];
150
+ const r = record(input, ['schemaVersion', 'model', 'name', 'textures', ...scalarKeys]);
151
+ if (r['schemaVersion'] !== 3 || r['model'] !== 'pbrMetallicRoughness')
152
+ fail('requires schemaVersion 3 and model pbrMetallicRoughness.');
153
+ const name = r['name'];
154
+ if (name !== undefined && (typeof name !== 'string' || name.length < 1 || name.length > 200))
155
+ fail('name requires 1..200 characters.');
156
+ const common = canonicalizePortableMaterialSpecV2({
157
+ schemaVersion: 2,
158
+ model: 'pbrMetallicRoughness',
159
+ ...Object.fromEntries(scalarKeys.filter((k) => r[k] !== undefined).map((k) => [k, r[k]])),
160
+ });
161
+ const refs = record(r['textures'] ?? {}, Object.keys(PORTABLE_MATERIAL_SLOT_USAGE));
162
+ const textures: CanonicalPortableMaterialSpecV3['textures'] = {};
163
+ let texels = 0,
164
+ work = 0,
165
+ bytes = 0;
166
+ for (const [key, value] of Object.entries(refs)) {
167
+ const slot = key as MaterialMapSlotV1;
168
+ const ref = record(value, ['kind', 'spec', 'resourceId', 'sampler']);
169
+ const sampler = canonicalizePortableSamplerV1(ref['sampler']);
170
+ if (ref['kind'] === 'procedural') {
171
+ if (ref['resourceId'] !== undefined)
172
+ fail('procedural references cannot carry a resource ID.');
173
+ const admitted = admitProceduralTextureSpecV3(ref['spec']);
174
+ if (admitted.spec.usage !== PORTABLE_MATERIAL_SLOT_USAGE[slot])
175
+ fail(`${slot} requires ${PORTABLE_MATERIAL_SLOT_USAGE[slot]} data.`);
176
+ texels += admitted.spec.width * admitted.spec.height;
177
+ work += admitted.estimatedWorkUnits;
178
+ bytes += admitted.conservativePixelAllocationBytes;
179
+ textures[slot] = { kind: 'procedural', spec: admitted.spec, sampler };
180
+ } else if (ref['kind'] === 'resource') {
181
+ if (
182
+ ref['spec'] !== undefined ||
183
+ typeof ref['resourceId'] !== 'string' ||
184
+ ref['resourceId'].length > 128 ||
185
+ !/^kiln\.[a-z0-9][a-z0-9._-]+$/.test(ref['resourceId'])
186
+ )
187
+ fail('resource requires only a bounded closed kiln.* ID and sampler.');
188
+ textures[slot] = { kind: 'resource', resourceId: ref['resourceId'], sampler };
189
+ } else fail('texture kind must be procedural or resource.');
190
+ }
191
+ if (
192
+ texels > PORTABLE_MATERIAL_V3_LIMITS.maxTexels ||
193
+ work > PORTABLE_MATERIAL_V3_LIMITS.maxWork ||
194
+ bytes > PORTABLE_MATERIAL_V3_LIMITS.maxTransientBytes
195
+ )
196
+ fail('aggregate texel/work/allocation budget exceeded.');
197
+ const { schemaVersion: _, textures: __, ...parameters } = common;
198
+ return {
199
+ spec: {
200
+ schemaVersion: 3,
201
+ ...parameters,
202
+ ...(name !== undefined ? { name: name as string } : {}),
203
+ textures,
204
+ },
205
+ texels,
206
+ estimatedWorkUnits: work,
207
+ conservativePixelAllocationBytes: bytes,
208
+ };
209
+ }
210
+ export function admitPortableMaterialSpecV3(input: unknown): PortableMaterialAdmissionV3 {
211
+ try {
212
+ return admitMaterial(input);
213
+ } catch (error) {
214
+ if (error instanceof PortableMaterialV3Error) throw error;
215
+ throw new PortableMaterialV3Error(
216
+ error instanceof ProceduralTextureError ? error.message : 'Invalid bounded material data.',
217
+ );
218
+ }
219
+ }
220
+ export function canonicalizePortableMaterialSpecV3(
221
+ input: unknown,
222
+ ): CanonicalPortableMaterialSpecV3 {
223
+ return admitPortableMaterialSpecV3(input).spec;
224
+ }
@@ -0,0 +1,234 @@
1
+ /** Legacy V1/V2 pixel compiler. Extraction preserves arithmetic and recipe identity. */
2
+ import {
3
+ type CanonicalProceduralLayerV2,
4
+ type CanonicalProceduralTextureSpecV2,
5
+ type ProceduralBlend,
6
+ canonicalProceduralTextureJsonV2,
7
+ canonicalizeProceduralTextureSpecV2,
8
+ hashProceduralTextureSpecV2,
9
+ } from './procedural-material-v2';
10
+
11
+ // -----------------------------------------------------------------------------
12
+ // Deterministic value noise
13
+ // -----------------------------------------------------------------------------
14
+
15
+ /**
16
+ * Integer hash of a lattice coordinate. Not a cryptographic hash — it only has
17
+ * to decorrelate neighbouring integers and return the same value forever.
18
+ */
19
+ function hash2(x: number, y: number, seed: number): number {
20
+ let h = (Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 1274126177)) | 0;
21
+ h = h ^ (h >>> 13);
22
+ h = Math.imul(h, 1274126177) | 0;
23
+ h = h ^ (h >>> 16);
24
+ return (h >>> 0) / 4294967296;
25
+ }
26
+
27
+ const smoothstep = (t: number): number => t * t * (3 - 2 * t);
28
+
29
+ /**
30
+ * Value noise on a lattice that wraps at `period`, so the result tiles.
31
+ * `period` is in lattice cells, and both axes use the same one.
32
+ */
33
+ function valueNoise(u: number, v: number, period: number, seed: number): number {
34
+ const x = u * period;
35
+ const y = v * period;
36
+ const xi = Math.floor(x);
37
+ const yi = Math.floor(y);
38
+ const xf = x - xi;
39
+ const yf = y - yi;
40
+ // Wrapping the lattice index (not the sample point) is what makes it tile:
41
+ // the cell to the right of the last one is the first one again.
42
+ const wrap = (n: number): number => ((n % period) + period) % period;
43
+ const x0 = wrap(xi);
44
+ const y0 = wrap(yi);
45
+ const x1 = wrap(xi + 1);
46
+ const y1 = wrap(yi + 1);
47
+ const sx = smoothstep(xf);
48
+ const sy = smoothstep(yf);
49
+ const n00 = hash2(x0, y0, seed);
50
+ const n10 = hash2(x1, y0, seed);
51
+ const n01 = hash2(x0, y1, seed);
52
+ const n11 = hash2(x1, y1, seed);
53
+ const top = n00 + sx * (n10 - n00);
54
+ const bottom = n01 + sx * (n11 - n01);
55
+ return top + sy * (bottom - top);
56
+ }
57
+
58
+ function fbm(u: number, v: number, period: number, octaves: number, seed: number): number {
59
+ let sum = 0;
60
+ let amplitude = 1;
61
+ let total = 0;
62
+ let p = period;
63
+ for (let o = 0; o < octaves; o++) {
64
+ sum += valueNoise(u, v, p, seed + o * 101) * amplitude;
65
+ total += amplitude;
66
+ amplitude *= 0.5;
67
+ // Doubling the period keeps every octave tileable at the base period.
68
+ p *= 2;
69
+ }
70
+ return total > 0 ? sum / total : 0;
71
+ }
72
+
73
+ // -----------------------------------------------------------------------------
74
+ // Layer evaluation
75
+ // -----------------------------------------------------------------------------
76
+
77
+ const rgbOf = (color: number): [number, number, number] => [
78
+ (color >> 16) & 0xff,
79
+ (color >> 8) & 0xff,
80
+ color & 0xff,
81
+ ];
82
+
83
+ const lerp = (a: number, b: number, t: number): number => a + (b - a) * t;
84
+
85
+ /** Rotate normalized coords about the center so angled patterns stay centered. */
86
+ function project(u: number, v: number, angleDeg: number): number {
87
+ const a = (angleDeg * Math.PI) / 180;
88
+ return (u - 0.5) * Math.cos(a) + (v - 0.5) * Math.sin(a) + 0.5;
89
+ }
90
+
91
+ /** Evaluate one layer at a normalized coordinate, returning 0-255 RGB. */
92
+ function evalLayer(
93
+ layer: CanonicalProceduralLayerV2,
94
+ u: number,
95
+ v: number,
96
+ ): [number, number, number] {
97
+ switch (layer.op) {
98
+ case 'solid':
99
+ return rgbOf(layer.color);
100
+
101
+ case 'checker': {
102
+ const cx = Math.floor(u * layer.squares);
103
+ const cy = Math.floor(v * layer.squares);
104
+ return rgbOf((cx + cy) % 2 === 0 ? layer.colorA : layer.colorB);
105
+ }
106
+
107
+ case 'stripes': {
108
+ const t = project(u, v, layer.angleDeg);
109
+ return rgbOf(Math.floor(t * layer.count) % 2 === 0 ? layer.colorA : layer.colorB);
110
+ }
111
+
112
+ case 'gradient': {
113
+ const t = Math.min(1, Math.max(0, project(u, v, layer.angleDeg)));
114
+ const from = rgbOf(layer.from);
115
+ const to = rgbOf(layer.to);
116
+ return [lerp(from[0], to[0], t), lerp(from[1], to[1], t), lerp(from[2], to[2], t)];
117
+ }
118
+
119
+ case 'bricks': {
120
+ const row = Math.floor(v * layer.rows);
121
+ // Offset alternate rows, wrapping into [0,1) so the pattern still tiles.
122
+ const shifted = (u + (row % 2 === 0 ? 0 : layer.stagger / layer.cols) + 1) % 1;
123
+ const inCol = (shifted * layer.cols) % 1;
124
+ const inRow = (v * layer.rows) % 1;
125
+ const isMortar =
126
+ inCol < layer.mortarWidth ||
127
+ inCol > 1 - layer.mortarWidth ||
128
+ inRow < layer.mortarWidth ||
129
+ inRow > 1 - layer.mortarWidth;
130
+ return rgbOf(isMortar ? layer.mortar : layer.brick);
131
+ }
132
+
133
+ case 'noise': {
134
+ const n = fbm(u, v, layer.scale, layer.octaves, layer.seed);
135
+ const a = rgbOf(layer.colorA);
136
+ const b = rgbOf(layer.colorB);
137
+ return [lerp(a[0], b[0], n), lerp(a[1], b[1], n), lerp(a[2], b[2], n)];
138
+ }
139
+ }
140
+ }
141
+
142
+ function blendChannel(mode: ProceduralBlend, base: number, top: number): number {
143
+ const b = base / 255;
144
+ const t = top / 255;
145
+ let out: number;
146
+ switch (mode) {
147
+ case 'multiply':
148
+ out = b * t;
149
+ break;
150
+ case 'screen':
151
+ out = 1 - (1 - b) * (1 - t);
152
+ break;
153
+ case 'overlay':
154
+ out = b < 0.5 ? 2 * b * t : 1 - 2 * (1 - b) * (1 - t);
155
+ break;
156
+ default:
157
+ out = t;
158
+ }
159
+ return out * 255;
160
+ }
161
+
162
+ // -----------------------------------------------------------------------------
163
+ // Public API
164
+ // -----------------------------------------------------------------------------
165
+
166
+ /**
167
+ * Build a tiling texture from a bounded, deterministic layer stack.
168
+ *
169
+ * The result is a plain RGBA `DataTexture`; `renderSceneToGLB` bakes it to PNG
170
+ * and embeds it, so nothing else has to be done to ship it.
171
+ *
172
+ * @example
173
+ * const bark = proceduralTexture({
174
+ * schemaVersion: 2,
175
+ * size: 256,
176
+ * usage: 'albedo',
177
+ * name: 'Bark',
178
+ * layers: [
179
+ * { op: 'solid', color: 0x5a4632 },
180
+ * { op: 'noise', colorA: 0x3d2f21, colorB: 0x7a6248, scale: 6, octaves: 4, blend: 'overlay' },
181
+ * { op: 'stripes', colorA: 0x000000, colorB: 0xffffff, count: 24, angleDeg: 90,
182
+ * blend: 'multiply', opacity: 0.15 },
183
+ * ],
184
+ * });
185
+ * const trunk = createPart('Trunk', cylinderGeo(0.3, 0.3, 3), pbrMaterial({ albedo: bark }), { parent: root });
186
+ */
187
+ export interface CompiledProceduralTextureV2 {
188
+ spec: CanonicalProceduralTextureSpecV2;
189
+ pixels: Uint8Array;
190
+ canonicalJson: string;
191
+ recipeHash: `sha256:${string}`;
192
+ }
193
+
194
+ /** Validate, canonicalize, and compile one texture without constructing Three.js state. */
195
+ export function compileProceduralTextureSpecV2(input: unknown): CompiledProceduralTextureV2 {
196
+ const spec = canonicalizeProceduralTextureSpecV2(input);
197
+ const { size, layers } = spec;
198
+ const data = new Uint8Array(size * size * 4);
199
+ for (let y = 0; y < size; y++) {
200
+ // Sample at texel centers so a pattern with N divisions lands on exact
201
+ // boundaries instead of drifting half a texel.
202
+ const v = (y + 0.5) / size;
203
+ for (let x = 0; x < size; x++) {
204
+ const u = (x + 0.5) / size;
205
+ let r = 0;
206
+ let g = 0;
207
+ let b = 0;
208
+ for (const [i, layer] of layers.entries()) {
209
+ const [lr, lg, lb] = evalLayer(layer, u, v);
210
+ if (i === 0) {
211
+ r = lr;
212
+ g = lg;
213
+ b = lb;
214
+ continue;
215
+ }
216
+ r = lerp(r, blendChannel(layer.blend, r, lr), layer.opacity);
217
+ g = lerp(g, blendChannel(layer.blend, g, lg), layer.opacity);
218
+ b = lerp(b, blendChannel(layer.blend, b, lb), layer.opacity);
219
+ }
220
+ const o = (y * size + x) * 4;
221
+ data[o] = Math.max(0, Math.min(255, Math.round(r)));
222
+ data[o + 1] = Math.max(0, Math.min(255, Math.round(g)));
223
+ data[o + 2] = Math.max(0, Math.min(255, Math.round(b)));
224
+ data[o + 3] = 255;
225
+ }
226
+ }
227
+
228
+ return {
229
+ spec,
230
+ pixels: data,
231
+ canonicalJson: canonicalProceduralTextureJsonV2(spec),
232
+ recipeHash: hashProceduralTextureSpecV2(spec),
233
+ };
234
+ }