@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
@@ -38,15 +38,12 @@
38
38
  */
39
39
 
40
40
  import * as THREE from 'three';
41
+ import { compileProceduralTextureSpecV2 } from './procedural-texture-v2-compiler';
42
+ import { compileProceduralTextureSpecV3 } from './procedural-texture-v3';
43
+ import type { ProceduralTextureSpecV3 } from './procedural-texture-v3';
41
44
  import {
42
- type CanonicalProceduralLayerV2,
43
- type CanonicalProceduralTextureSpecV2,
44
- type ProceduralBlend,
45
- type ProceduralTextureSpec,
45
+ type ProceduralTextureSpec as LegacyProceduralTextureSpec,
46
46
  ProceduralTextureError,
47
- canonicalProceduralTextureJsonV2,
48
- canonicalizeProceduralTextureSpecV2,
49
- hashProceduralTextureSpecV2,
50
47
  } from './procedural-material-v2';
51
48
 
52
49
  export {
@@ -76,237 +73,72 @@ export type {
76
73
  PortableTextureRefV2,
77
74
  ProceduralBlend,
78
75
  ProceduralLayer,
79
- ProceduralTextureSpec,
80
76
  ProceduralTextureSpecV1,
81
77
  ProceduralTextureSpecV2,
82
78
  } from './procedural-material-v2';
83
79
 
84
- // -----------------------------------------------------------------------------
85
- // Deterministic value noise
86
- // -----------------------------------------------------------------------------
87
-
88
- /**
89
- * Integer hash of a lattice coordinate. Not a cryptographic hash — it only has
90
- * to decorrelate neighbouring integers and return the same value forever.
91
- */
92
- function hash2(x: number, y: number, seed: number): number {
93
- let h = (Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 1274126177)) | 0;
94
- h = h ^ (h >>> 13);
95
- h = Math.imul(h, 1274126177) | 0;
96
- h = h ^ (h >>> 16);
97
- return (h >>> 0) / 4294967296;
98
- }
99
-
100
- const smoothstep = (t: number): number => t * t * (3 - 2 * t);
101
-
102
- /**
103
- * Value noise on a lattice that wraps at `period`, so the result tiles.
104
- * `period` is in lattice cells, and both axes use the same one.
105
- */
106
- function valueNoise(u: number, v: number, period: number, seed: number): number {
107
- const x = u * period;
108
- const y = v * period;
109
- const xi = Math.floor(x);
110
- const yi = Math.floor(y);
111
- const xf = x - xi;
112
- const yf = y - yi;
113
- // Wrapping the lattice index (not the sample point) is what makes it tile:
114
- // the cell to the right of the last one is the first one again.
115
- const wrap = (n: number): number => ((n % period) + period) % period;
116
- const x0 = wrap(xi);
117
- const y0 = wrap(yi);
118
- const x1 = wrap(xi + 1);
119
- const y1 = wrap(yi + 1);
120
- const sx = smoothstep(xf);
121
- const sy = smoothstep(yf);
122
- const n00 = hash2(x0, y0, seed);
123
- const n10 = hash2(x1, y0, seed);
124
- const n01 = hash2(x0, y1, seed);
125
- const n11 = hash2(x1, y1, seed);
126
- const top = n00 + sx * (n10 - n00);
127
- const bottom = n01 + sx * (n11 - n01);
128
- return top + sy * (bottom - top);
129
- }
130
-
131
- function fbm(u: number, v: number, period: number, octaves: number, seed: number): number {
132
- let sum = 0;
133
- let amplitude = 1;
134
- let total = 0;
135
- let p = period;
136
- for (let o = 0; o < octaves; o++) {
137
- sum += valueNoise(u, v, p, seed + o * 101) * amplitude;
138
- total += amplitude;
139
- amplitude *= 0.5;
140
- // Doubling the period keeps every octave tileable at the base period.
141
- p *= 2;
142
- }
143
- return total > 0 ? sum / total : 0;
144
- }
145
-
146
- // -----------------------------------------------------------------------------
147
- // Layer evaluation
148
- // -----------------------------------------------------------------------------
149
-
150
- const rgbOf = (color: number): [number, number, number] => [
151
- (color >> 16) & 0xff,
152
- (color >> 8) & 0xff,
153
- color & 0xff,
154
- ];
155
-
156
- const lerp = (a: number, b: number, t: number): number => a + (b - a) * t;
157
-
158
- /** Rotate normalized coords about the center so angled patterns stay centered. */
159
- function project(u: number, v: number, angleDeg: number): number {
160
- const a = (angleDeg * Math.PI) / 180;
161
- return (u - 0.5) * Math.cos(a) + (v - 0.5) * Math.sin(a) + 0.5;
162
- }
163
-
164
- /** Evaluate one layer at a normalized coordinate, returning 0-255 RGB. */
165
- function evalLayer(
166
- layer: CanonicalProceduralLayerV2,
167
- u: number,
168
- v: number,
169
- ): [number, number, number] {
170
- switch (layer.op) {
171
- case 'solid':
172
- return rgbOf(layer.color);
80
+ export * from './procedural-texture-v3';
81
+ export type ProceduralTextureSpec = LegacyProceduralTextureSpec | ProceduralTextureSpecV3;
173
82
 
174
- case 'checker': {
175
- const cx = Math.floor(u * layer.squares);
176
- const cy = Math.floor(v * layer.squares);
177
- return rgbOf((cx + cy) % 2 === 0 ? layer.colorA : layer.colorB);
178
- }
179
-
180
- case 'stripes': {
181
- const t = project(u, v, layer.angleDeg);
182
- return rgbOf(Math.floor(t * layer.count) % 2 === 0 ? layer.colorA : layer.colorB);
183
- }
184
-
185
- case 'gradient': {
186
- const t = Math.min(1, Math.max(0, project(u, v, layer.angleDeg)));
187
- const from = rgbOf(layer.from);
188
- const to = rgbOf(layer.to);
189
- return [lerp(from[0], to[0], t), lerp(from[1], to[1], t), lerp(from[2], to[2], t)];
190
- }
191
-
192
- case 'bricks': {
193
- const row = Math.floor(v * layer.rows);
194
- // Offset alternate rows, wrapping into [0,1) so the pattern still tiles.
195
- const shifted = (u + (row % 2 === 0 ? 0 : layer.stagger / layer.cols) + 1) % 1;
196
- const inCol = (shifted * layer.cols) % 1;
197
- const inRow = (v * layer.rows) % 1;
198
- const isMortar =
199
- inCol < layer.mortarWidth ||
200
- inCol > 1 - layer.mortarWidth ||
201
- inRow < layer.mortarWidth ||
202
- inRow > 1 - layer.mortarWidth;
203
- return rgbOf(isMortar ? layer.mortar : layer.brick);
204
- }
205
-
206
- case 'noise': {
207
- const n = fbm(u, v, layer.scale, layer.octaves, layer.seed);
208
- const a = rgbOf(layer.colorA);
209
- const b = rgbOf(layer.colorB);
210
- return [lerp(a[0], b[0], n), lerp(a[1], b[1], n), lerp(a[2], b[2], n)];
211
- }
212
- }
213
- }
214
-
215
- function blendChannel(mode: ProceduralBlend, base: number, top: number): number {
216
- const b = base / 255;
217
- const t = top / 255;
218
- let out: number;
219
- switch (mode) {
220
- case 'multiply':
221
- out = b * t;
222
- break;
223
- case 'screen':
224
- out = 1 - (1 - b) * (1 - t);
225
- break;
226
- case 'overlay':
227
- out = b < 0.5 ? 2 * b * t : 1 - 2 * (1 - b) * (1 - t);
228
- break;
229
- default:
230
- out = t;
231
- }
232
- return out * 255;
233
- }
234
-
235
- // -----------------------------------------------------------------------------
236
- // Public API
237
- // -----------------------------------------------------------------------------
238
-
239
- /**
240
- * Build a tiling texture from a bounded, deterministic layer stack.
241
- *
242
- * The result is a plain RGBA `DataTexture`; `renderSceneToGLB` bakes it to PNG
243
- * and embeds it, so nothing else has to be done to ship it.
244
- *
245
- * @example
246
- * const bark = proceduralTexture({
247
- * schemaVersion: 2,
248
- * size: 256,
249
- * usage: 'albedo',
250
- * name: 'Bark',
251
- * layers: [
252
- * { op: 'solid', color: 0x5a4632 },
253
- * { op: 'noise', colorA: 0x3d2f21, colorB: 0x7a6248, scale: 6, octaves: 4, blend: 'overlay' },
254
- * { op: 'stripes', colorA: 0x000000, colorB: 0xffffff, count: 24, angleDeg: 90,
255
- * blend: 'multiply', opacity: 0.15 },
256
- * ],
257
- * });
258
- * const trunk = createPart('Trunk', cylinderGeo(0.3, 0.3, 3), pbrMaterial({ albedo: bark }), { parent: root });
259
- */
260
- export interface CompiledProceduralTextureV2 {
261
- spec: CanonicalProceduralTextureSpecV2;
262
- pixels: Uint8Array;
263
- canonicalJson: string;
264
- recipeHash: `sha256:${string}`;
83
+ /** Dispatch never invokes an input accessor; absent/undefined retains V1 compatibility. */
84
+ export function proceduralTextureSchemaVersion(input: unknown): 1 | 2 | 3 {
85
+ if (!input || typeof input !== 'object' || Array.isArray(input))
86
+ throw new ProceduralTextureError(
87
+ 'proceduralTexture requires a plain JSON object.',
88
+ 'PROCEDURAL_TEXTURE_V3_CONTRACT',
89
+ );
90
+ const prototype = Object.getPrototypeOf(input);
91
+ if (prototype !== Object.prototype && prototype !== null)
92
+ throw new ProceduralTextureError(
93
+ 'proceduralTexture requires a plain JSON object.',
94
+ 'PROCEDURAL_TEXTURE_V3_CONTRACT',
95
+ );
96
+ const descriptor = Object.getOwnPropertyDescriptor(input, 'schemaVersion');
97
+ if (descriptor && (!descriptor.enumerable || !('value' in descriptor)))
98
+ throw new ProceduralTextureError(
99
+ 'proceduralTexture.schemaVersion must be a JSON data field.',
100
+ 'PROCEDURAL_TEXTURE_V3_CONTRACT',
101
+ );
102
+ const version: unknown = descriptor?.value;
103
+ if (version === undefined || version === 1) return 1;
104
+ if (version === 2 || version === 3) return version;
105
+ throw new ProceduralTextureError(
106
+ 'proceduralTexture.schemaVersion must be 1, 2 or 3.',
107
+ 'PROCEDURAL_TEXTURE_V3_CONTRACT',
108
+ );
265
109
  }
266
110
 
267
- /** Validate, canonicalize, and compile one texture without constructing Three.js state. */
268
- export function compileProceduralTextureSpecV2(input: unknown): CompiledProceduralTextureV2 {
269
- const spec = canonicalizeProceduralTextureSpecV2(input);
270
- const { size, layers } = spec;
271
- const data = new Uint8Array(size * size * 4);
272
- for (let y = 0; y < size; y++) {
273
- // Sample at texel centers so a pattern with N divisions lands on exact
274
- // boundaries instead of drifting half a texel.
275
- const v = (y + 0.5) / size;
276
- for (let x = 0; x < size; x++) {
277
- const u = (x + 0.5) / size;
278
- let r = 0;
279
- let g = 0;
280
- let b = 0;
281
- for (const [i, layer] of layers.entries()) {
282
- const [lr, lg, lb] = evalLayer(layer, u, v);
283
- if (i === 0) {
284
- r = lr;
285
- g = lg;
286
- b = lb;
287
- continue;
288
- }
289
- r = lerp(r, blendChannel(layer.blend, r, lr), layer.opacity);
290
- g = lerp(g, blendChannel(layer.blend, g, lg), layer.opacity);
291
- b = lerp(b, blendChannel(layer.blend, b, lb), layer.opacity);
292
- }
293
- const o = (y * size + x) * 4;
294
- data[o] = Math.max(0, Math.min(255, Math.round(r)));
295
- data[o + 1] = Math.max(0, Math.min(255, Math.round(g)));
296
- data[o + 2] = Math.max(0, Math.min(255, Math.round(b)));
297
- data[o + 3] = 255;
298
- }
299
- }
300
-
301
- return {
302
- spec,
303
- pixels: data,
304
- canonicalJson: canonicalProceduralTextureJsonV2(spec),
305
- recipeHash: hashProceduralTextureSpecV2(spec),
306
- };
307
- }
111
+ export { compileProceduralTextureSpecV2 } from './procedural-texture-v2-compiler';
112
+ export type { CompiledProceduralTextureV2 } from './procedural-texture-v2-compiler';
308
113
 
309
114
  export function proceduralTexture(spec: ProceduralTextureSpec): THREE.DataTexture {
115
+ if (proceduralTextureSchemaVersion(spec) === 3) {
116
+ const compiled = compileProceduralTextureSpecV3(spec);
117
+ const { width, height, usage, name } = compiled.spec;
118
+ const tex = new THREE.DataTexture(
119
+ compiled.pixels,
120
+ width,
121
+ height,
122
+ THREE.RGBAFormat,
123
+ THREE.UnsignedByteType,
124
+ );
125
+ if (name) tex.name = name;
126
+ tex.colorSpace =
127
+ usage === 'albedo' || usage === 'emissive' ? THREE.SRGBColorSpace : THREE.NoColorSpace;
128
+ tex.wrapS = THREE.RepeatWrapping;
129
+ tex.wrapT = THREE.RepeatWrapping;
130
+ tex.minFilter = THREE.LinearMipmapLinearFilter;
131
+ tex.magFilter = THREE.LinearFilter;
132
+ tex.generateMipmaps = true;
133
+ tex.needsUpdate = true;
134
+ tex.userData['kilnProcedural'] = {
135
+ ...compiled.spec,
136
+ canonicalJson: compiled.canonicalJson,
137
+ recipeHash: compiled.recipeHash,
138
+ compilerId: compiled.compilerId,
139
+ };
140
+ return tex;
141
+ }
310
142
  const compiled = compileProceduralTextureSpecV2(spec);
311
143
  const { size, usage, name, layers } = compiled.spec;
312
144
 
@@ -12,9 +12,19 @@ import {
12
12
  ProjectNotFoundError,
13
13
  type ProjectRevision,
14
14
  } from './projects';
15
- import { canonicalMaterialJson, type MaterialRecordV1 } from './material-library';
15
+ import {
16
+ canonicalMaterialJson,
17
+ MATERIAL_LIBRARY_LIMITS,
18
+ type MaterialManifest,
19
+ type MaterialRecord,
20
+ } from './material-library';
16
21
  import { dependencyManifests, resolveSavedAssetMaterials } from './asset-materials-node';
17
- import { createMaterialLibraryPayload, verifyMaterialRecordV1 } from './material-library-node';
22
+ import {
23
+ createMaterialLibraryPayload,
24
+ verifyMaterialRecord,
25
+ resolveMaterialRecordClosure,
26
+ validateMaterialManifestIdentityAny,
27
+ } from './material-library-node';
18
28
  import {
19
29
  PROJECT_BUNDLE_LIMITS,
20
30
  decodeProjectBundle,
@@ -26,7 +36,7 @@ import {
26
36
  async function assetMaterials(
27
37
  workspace: FileWorkspace,
28
38
  asset: AssetRecord,
29
- ): Promise<MaterialRecordV1[]> {
39
+ ): Promise<MaterialRecord[]> {
30
40
  return resolveSavedAssetMaterials(asset, workspace.materials);
31
41
  }
32
42
  export async function resolveAssetMaterialPayload(workspace: FileWorkspace, asset: AssetRecord) {
@@ -60,27 +70,101 @@ export async function exportWorkspaceProjectBundle(
60
70
  ),
61
71
  });
62
72
  }
63
- const materials = new Map<string, MaterialRecordV1>();
73
+ let materials: MaterialRecord[] = [];
64
74
  if (profile === 'editable') {
75
+ const roots = new Map<string, MaterialManifest>();
76
+ const assetRoots = new Set<string>();
77
+ const embedded = new Map<
78
+ string,
79
+ { manifest: MaterialManifest; files: Record<string, string> }
80
+ >();
81
+ const addRoot = (manifest: MaterialManifest) => {
82
+ const key = `${manifest.materialId}/${manifest.revisionId}`;
83
+ const previous = roots.get(key);
84
+ if (previous && canonicalMaterialJson(previous) !== canonicalMaterialJson(manifest))
85
+ throw new Error('Conflicting project material identity');
86
+ roots.set(key, manifest);
87
+ };
65
88
  for (const dependency of project.materialDependencies) {
66
89
  if (dependency.sha256 !== dependency.revisionId)
67
90
  throw new Error(`Material lock hash mismatch: ${dependency.resourceId}`);
68
- let record: MaterialRecordV1;
91
+ let manifest: MaterialManifest;
69
92
  try {
70
- record = await workspace.materials.read(dependency.resourceId, dependency.revisionId);
93
+ manifest = await workspace.materials.readManifest(
94
+ dependency.resourceId,
95
+ dependency.revisionId,
96
+ );
71
97
  } catch (error) {
72
98
  throw new Error(
73
99
  `Locked project material unavailable: ${dependency.resourceId} at ${dependency.revisionId}`,
74
100
  { cause: error },
75
101
  );
76
102
  }
77
- materials.set(`${record.manifest.materialId}/${record.manifest.revisionId}`, record);
103
+ addRoot(manifest);
104
+ }
105
+ for (const asset of assets) {
106
+ for (const manifest of dependencyManifests(asset.record)) {
107
+ addRoot(manifest);
108
+ assetRoots.add(`${manifest.materialId}/${manifest.revisionId}`);
109
+ }
110
+ for (const wire of asset.record.materialResources?.records ?? []) {
111
+ const manifest = validateMaterialManifestIdentityAny(wire.manifest);
112
+ const key = `${manifest.materialId}/${manifest.revisionId}`;
113
+ const previous = embedded.get(key);
114
+ if (
115
+ previous &&
116
+ (canonicalMaterialJson(previous.manifest) !== canonicalMaterialJson(manifest) ||
117
+ canonicalMaterialJson(previous.files) !== canonicalMaterialJson(wire.files))
118
+ )
119
+ throw new Error('Conflicting embedded material identity');
120
+ embedded.set(key, { manifest, files: wire.files });
121
+ }
78
122
  }
79
- for (const asset of assets)
80
- for (const record of await assetMaterials(workspace, asset.record))
81
- materials.set(`${record.manifest.materialId}/${record.manifest.revisionId}`, record);
123
+ const lockedRead = async <T>(
124
+ id: string,
125
+ revision: string,
126
+ read: () => Promise<T>,
127
+ ): Promise<T> => {
128
+ try {
129
+ return await read();
130
+ } catch (error) {
131
+ throw new Error(
132
+ `Locked ${assetRoots.has(`${id}/${revision}`) ? 'asset' : 'project'} material unavailable: ${id} at ${revision}`,
133
+ { cause: error },
134
+ );
135
+ }
136
+ };
137
+ materials = await resolveMaterialRecordClosure(
138
+ [...roots.values()],
139
+ {
140
+ readManifest: async (id, revision) =>
141
+ embedded.get(`${id}/${revision}`)?.manifest ??
142
+ lockedRead(id, revision, () => workspace.materials.readManifest(id, revision)),
143
+ read: async (id, revision) => {
144
+ const wire = embedded.get(`${id}/${revision}`);
145
+ if (!wire)
146
+ return lockedRead(id, revision, () =>
147
+ workspace.materials.readRecordFiles(id, revision),
148
+ );
149
+ return {
150
+ manifest: wire.manifest,
151
+ files: Object.fromEntries(
152
+ Object.entries(wire.files).map(([name, value]) => [
153
+ name,
154
+ new Uint8Array(Buffer.from(value, 'base64')),
155
+ ]),
156
+ ),
157
+ } as MaterialRecord;
158
+ },
159
+ },
160
+ {
161
+ maxRecords: 1024,
162
+ maxBytes: PROJECT_BUNDLE_LIMITS.maxBytes,
163
+ maxPixels: 1024 * MATERIAL_LIBRARY_LIMITS.maxRecordPixels,
164
+ },
165
+ );
82
166
  }
83
- return encodeProjectBundle({ project, assets, materials: [...materials.values()] }, profile);
167
+ return encodeProjectBundle({ project, assets, materials }, profile);
84
168
  }
85
169
 
86
170
  async function retainArchive(workspace: FileWorkspace, bytes: Uint8Array, digest: string) {
@@ -153,7 +237,8 @@ export async function importWorkspaceProjectBundle(
153
237
  record,
154
238
  ]),
155
239
  );
156
- for (const record of decoded.materials) await verifyMaterialRecordV1(record);
240
+ for (const record of decoded.materials)
241
+ await verifyMaterialRecord(record, { materials: decoded.materials });
157
242
  for (const asset of decoded.assets) {
158
243
  await verifyAssetRecord(asset.record);
159
244
  for (const manifest of dependencyManifests(asset.record)) {
@@ -211,8 +296,15 @@ export async function importWorkspaceProjectBundle(
211
296
  // Nothing above performs writes. Verified immutable resources can safely survive a
212
297
  // later disk failure or project-ID conflict; imports never overwrite other revisions.
213
298
  await retainArchive(workspace, snapshot, digest);
214
- for (let i = 0; i < decoded.materials.length; i += 100)
215
- await workspace.materials.import(decoded.materials.slice(i, i + 100));
299
+ const newMaterialContext = decoded.materials.some(
300
+ (record) => record.manifest.schemaVersion === 2,
301
+ );
302
+ for (let i = 0; i < decoded.materials.length; i += 100) {
303
+ const batch = decoded.materials.slice(i, i + 100);
304
+ if (newMaterialContext)
305
+ await workspace.materials.import(batch, { materials: decoded.materials });
306
+ else await workspace.materials.import(batch);
307
+ }
216
308
  const records = [...destinationRecords.values()];
217
309
  for (let i = 0; i < records.length; i += 100)
218
310
  await library.import(options.collectionId, records.slice(i, i + 100));
@@ -14,8 +14,10 @@ import { MAX_PROJECT_REVISIONS, projectRevisionSchema, type ProjectRevision } fr
14
14
  import {
15
15
  canonicalMaterialJson,
16
16
  materialManifestSchema,
17
- validateMaterialRecordShape,
18
- type MaterialRecordV1,
17
+ materialManifestAnySchema,
18
+ materialRecordDependencyPins,
19
+ validateMaterialRecordShapeAny,
20
+ type MaterialRecord,
19
21
  } from './material-library';
20
22
 
21
23
  export const PROJECT_BUNDLE_LIMITS = Object.freeze({
@@ -69,7 +71,16 @@ export const projectBundleManifestSchema = z
69
71
  ),
70
72
  })
71
73
  .strict();
72
- export type ProjectBundleManifest = z.infer<typeof projectBundleManifestSchema>;
74
+ export const projectBundleManifestV2Schema = projectBundleManifestSchema.extend({
75
+ version: z.literal('kiln.project-bundle.v2'),
76
+ });
77
+ export const projectBundleManifestAnySchema = z.union([
78
+ projectBundleManifestSchema,
79
+ projectBundleManifestV2Schema,
80
+ ]);
81
+ export type ProjectBundleManifestV1 = z.infer<typeof projectBundleManifestSchema>;
82
+ export type ProjectBundleManifestV2 = z.infer<typeof projectBundleManifestV2Schema>;
83
+ export type ProjectBundleManifest = ProjectBundleManifestV1 | ProjectBundleManifestV2;
73
84
  export interface ProjectBundleAsset {
74
85
  inventoryId: string;
75
86
  collectionId: string;
@@ -77,7 +88,7 @@ export interface ProjectBundleAsset {
77
88
  }
78
89
  export interface ProjectBundleInput {
79
90
  project: ProjectRevision;
80
- materials: MaterialRecordV1[];
91
+ materials: MaterialRecord[];
81
92
  assets: ProjectBundleAsset[];
82
93
  }
83
94
  export interface DecodedProjectBundle extends ProjectBundleInput {
@@ -117,8 +128,8 @@ async function verifyAsset(record: AssetRecord) {
117
128
  if ((await projectBundleHash(file)) !== record.manifest.files[name]!.sha256)
118
129
  throw new Error('Asset file hash mismatch');
119
130
  }
120
- async function verifyMaterial(record: MaterialRecordV1) {
121
- const manifest = validateMaterialRecordShape(record);
131
+ async function verifyMaterial(record: MaterialRecord) {
132
+ const manifest = validateMaterialRecordShapeAny(record);
122
133
  const { revisionId, ...payload } = manifest;
123
134
  if ((await projectBundleHash(encoder.encode(canonicalMaterialJson(payload)))) !== revisionId)
124
135
  throw new Error('Material revision hash mismatch');
@@ -144,10 +155,9 @@ function assertAssetSelection(project: ProjectRevision, entries: ProjectBundleMa
144
155
  throw new Error('Bundle inventory asset binding mismatch');
145
156
  }
146
157
  }
147
- function assertMaterialClosure(project: ProjectRevision, records: MaterialRecordV1[]) {
148
- const identities = records.map(
149
- (record) => `${record.manifest.materialId}/${record.manifest.revisionId}`,
150
- );
158
+ function assertMaterialClosure(project: ProjectRevision, records: MaterialRecord[]) {
159
+ const manifests = records.map((record) => validateMaterialRecordShapeAny(record));
160
+ const identities = manifests.map((manifest) => `${manifest.materialId}/${manifest.revisionId}`);
151
161
  if (new Set(identities).size !== identities.length)
152
162
  throw new Error('Duplicate material revision');
153
163
  for (const dependency of project.materialDependencies)
@@ -156,6 +166,33 @@ function assertMaterialClosure(project: ProjectRevision, records: MaterialRecord
156
166
  !identities.includes(`${dependency.resourceId}/${dependency.revisionId}`)
157
167
  )
158
168
  throw new Error(`Missing locked material: ${dependency.resourceId}`);
169
+ const byKey = new Map(
170
+ manifests.map((manifest) => [`${manifest.materialId}/${manifest.revisionId}`, manifest]),
171
+ );
172
+ const visited = new Set<string>();
173
+ for (const key of identities) {
174
+ const active = new Set<string>();
175
+ const stack: Array<{ key: string; exit: boolean }> = [{ key, exit: false }];
176
+ while (stack.length) {
177
+ const frame = stack.pop()!;
178
+ if (frame.exit) {
179
+ active.delete(frame.key);
180
+ visited.add(frame.key);
181
+ continue;
182
+ }
183
+ if (active.has(frame.key)) throw new Error('Cyclic project material closure');
184
+ if (visited.has(frame.key)) continue;
185
+ const manifest = byKey.get(frame.key);
186
+ if (!manifest) throw new Error('Missing transitive project material');
187
+ active.add(frame.key);
188
+ stack.push({ key: frame.key, exit: true });
189
+ for (const pin of materialRecordDependencyPins(manifest)) {
190
+ if (pin.sha256 !== pin.revisionId)
191
+ throw new Error('Transitive material lock hash mismatch');
192
+ stack.push({ key: `${pin.resourceId}/${pin.revisionId}`, exit: false });
193
+ }
194
+ }
195
+ }
159
196
  }
160
197
 
161
198
  /** Editable includes normalized rebuild resources; runtime intentionally excludes source/recipes. */
@@ -228,8 +265,12 @@ export async function encodeProjectBundle(
228
265
  for (const [name, bytes] of Object.entries(record.files)) add(prefix + name, bytes);
229
266
  }
230
267
  }
231
- const manifest = projectBundleManifestSchema.parse({
232
- version: 'kiln.project-bundle.v1',
268
+ const version =
269
+ profile === 'editable' && input.materials.some((record) => record.manifest.schemaVersion === 2)
270
+ ? 'kiln.project-bundle.v2'
271
+ : 'kiln.project-bundle.v1';
272
+ const manifest = projectBundleManifestAnySchema.parse({
273
+ version,
233
274
  profile,
234
275
  editable: profile === 'editable',
235
276
  resourceClosure:
@@ -294,7 +335,7 @@ export async function decodeProjectBundle(bytes: Uint8Array): Promise<DecodedPro
294
335
  }
295
336
  return value;
296
337
  };
297
- const manifest = projectBundleManifestSchema.parse(
338
+ const manifest = projectBundleManifestAnySchema.parse(
298
339
  json('bundle.json', PROJECT_BUNDLE_LIMITS.maxManifestBytes),
299
340
  );
300
341
  if (
@@ -314,7 +355,7 @@ export async function decodeProjectBundle(bytes: Uint8Array): Promise<DecodedPro
314
355
  assertAssetSelection(project, manifest.assets);
315
356
  const used = new Set(['bundle.json', 'project.json']);
316
357
  const assets: ProjectBundleAsset[] = [];
317
- const materials: MaterialRecordV1[] = [];
358
+ const materials: MaterialRecord[] = [];
318
359
  const runtimeAssets: DecodedProjectBundle['runtimeAssets'] = [];
319
360
  for (const identity of manifest.assets) {
320
361
  const prefix = assetPrefix(identity);
@@ -367,16 +408,18 @@ export async function decodeProjectBundle(bytes: Uint8Array): Promise<DecodedPro
367
408
  throw new Error('Runtime profile contains editable material records');
368
409
  for (const identity of manifest.materials) {
369
410
  const prefix = materialPrefix(identity);
370
- const materialManifest = materialManifestSchema.parse(
371
- json(`${prefix}manifest.json`, 256 * 1024),
372
- );
411
+ const materialManifest = (
412
+ manifest.version === 'kiln.project-bundle.v1'
413
+ ? materialManifestSchema
414
+ : materialManifestAnySchema
415
+ ).parse(json(`${prefix}manifest.json`, 256 * 1024));
373
416
  used.add(`${prefix}manifest.json`);
374
417
  if (
375
418
  materialManifest.materialId !== identity.materialId ||
376
419
  materialManifest.revisionId !== identity.revisionId
377
420
  )
378
421
  throw new Error('Bundle material identity mismatch');
379
- const record: MaterialRecordV1 = { manifest: materialManifest, files: {} };
422
+ const record = { manifest: materialManifest, files: {} } as MaterialRecord;
380
423
  for (const map of materialManifest.maps) {
381
424
  const bytes = files[prefix + map.file];
382
425
  if (!bytes) throw new Error('Missing material map');