@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +86 -1
- package/README.md +18 -0
- package/dist/agent-run.mjs +6392 -1477
- package/dist/build.json +23 -23
- package/dist/cli.mjs +8453 -3407
- package/dist/evaluator-worker.mjs +16608 -12387
- package/dist/mcp-engine.mjs +6746 -1725
- package/dist/mcp-server.mjs +4 -4
- package/dist/viewer/app.js +203 -199
- package/dist/viewer/chat.html +209 -205
- package/dist/viewer/style.css +10 -0
- package/docs/atlas-provenance.md +23 -0
- package/docs/community-exporter.md +27 -0
- package/docs/export-profiles.md +20 -0
- package/docs/install.md +7 -6
- package/docs/material-library.md +9 -5
- package/docs/migration.md +16 -0
- package/docs/procedural-surfaces-v3.md +276 -0
- package/docs/projects-and-live-review.md +1 -1
- package/docs/rendering.md +41 -0
- package/docs/runtime.md +3 -3
- package/docs/sdk.md +55 -2
- package/docs/tools-operations.md +2 -2
- package/docs/tools.md +3 -3
- package/lib/asset-materials-node.d.ts +3 -3
- package/lib/asset-materials-node.js +29 -33
- package/lib/assets-node.d.ts +1 -1
- package/lib/assets-node.js +11 -4
- package/lib/assets.d.ts +4 -2
- package/lib/assets.js +61 -16
- package/lib/atlas-provenance.d.ts +86 -0
- package/lib/atlas-provenance.js +448 -0
- package/lib/character.d.ts +11 -2
- package/lib/character.js +90 -2
- package/lib/community-exporter.d.ts +3 -2
- package/lib/community-exporter.js +139 -3
- package/lib/discovery/helper-contracts.js +161 -7
- package/lib/discovery/helper-specs.js +49 -4
- package/lib/discovery/shapes.js +20 -7
- package/lib/engine-identity.d.ts +1 -1
- package/lib/engine-identity.js +1 -1
- package/lib/evaluator/authoring-diagnostic.d.ts +1 -1
- package/lib/evaluator/authoring-diagnostic.js +12 -0
- package/lib/evaluator/protocol.d.ts +2 -2
- package/lib/evaluator/protocol.js +16 -6
- package/lib/evaluator/subprocess.js +2 -2
- package/lib/exporter-node.js +9 -1
- package/lib/geometry-budget.d.ts +7 -0
- package/lib/geometry-budget.js +8 -0
- package/lib/gltf-texture-binding.d.ts +6 -0
- package/lib/gltf-texture-binding.js +54 -0
- package/lib/material-authoring-node.d.ts +6 -0
- package/lib/material-authoring-node.js +49 -0
- package/lib/material-authoring-schema.d.ts +871 -0
- package/lib/material-authoring-schema.js +310 -0
- package/lib/material-export.d.ts +5 -0
- package/lib/material-export.js +21 -0
- package/lib/material-library-node.d.ts +34 -8
- package/lib/material-library-node.js +691 -11
- package/lib/material-library.d.ts +3704 -4
- package/lib/material-library.js +272 -0
- package/lib/mcp-core.d.ts +1 -1
- package/lib/pinned-material-map.d.ts +33 -0
- package/lib/pinned-material-map.js +1 -0
- package/lib/portable-material-runtime.d.ts +5 -0
- package/lib/portable-material-runtime.js +160 -0
- package/lib/portable-sampler-runtime.d.ts +4 -0
- package/lib/portable-sampler-runtime.js +26 -0
- package/lib/portable-sampler.d.ts +11 -0
- package/lib/portable-sampler.js +42 -0
- package/lib/primitives.d.ts +15 -3
- package/lib/primitives.js +24 -4
- package/lib/procedural-material-v2.d.ts +2 -0
- package/lib/procedural-material-v2.js +2 -1
- package/lib/procedural-material-v3.d.ts +72 -0
- package/lib/procedural-material-v3.js +162 -0
- package/lib/procedural-texture-v2-compiler.d.ts +31 -0
- package/lib/procedural-texture-v2-compiler.js +172 -0
- package/lib/procedural-texture-v3-contract.d.ts +125 -0
- package/lib/procedural-texture-v3-contract.js +333 -0
- package/lib/procedural-texture-v3.d.ts +12 -0
- package/lib/procedural-texture-v3.js +165 -0
- package/lib/procedural-texture.d.ts +9 -31
- package/lib/procedural-texture.js +43 -170
- package/lib/project-bundle-node.d.ts +1 -1
- package/lib/project-bundle-node.js +70 -13
- package/lib/project-bundle.d.ts +91 -3
- package/lib/project-bundle.js +48 -7
- package/lib/qa/atlas.d.ts +21 -0
- package/lib/qa/atlas.js +474 -0
- package/lib/qa/index.d.ts +1 -0
- package/lib/qa/index.js +1 -0
- package/lib/qa/material.js +2 -0
- package/lib/qa/skin-integrity.d.ts +18 -0
- package/lib/qa/skin-integrity.js +221 -0
- package/lib/render-service-host.js +4 -7
- package/lib/render.d.ts +3 -3
- package/lib/render.js +108 -16
- package/lib/skin-binding.d.ts +20 -0
- package/lib/skin-binding.js +144 -0
- package/lib/texture-atlas-contract.d.ts +97 -0
- package/lib/texture-atlas-contract.js +414 -0
- package/lib/texture-atlas-pixels.d.ts +73 -0
- package/lib/texture-atlas-pixels.js +290 -0
- package/lib/texture-atlas.d.ts +10 -0
- package/lib/texture-atlas.js +143 -0
- package/lib/texture-bake.d.ts +3 -0
- package/lib/texture-bake.js +66 -11
- package/lib/texture-resolver.d.ts +22 -0
- package/lib/texture-resolver.js +24 -0
- package/lib/tools/materials.js +8 -7
- package/lib/tools/registry.js +5 -16
- package/lib/tools/review.js +1 -1
- package/lib/uv-data.d.ts +32 -0
- package/lib/uv-data.js +172 -0
- package/lib/uv-shapes.js +3 -0
- package/lib/uv.d.ts +9 -3
- package/lib/uv.js +53 -82
- package/lib/views/camera.d.ts +3 -3
- package/lib/views/camera.js +32 -28
- package/lib/views/face-policy.d.ts +9 -0
- package/lib/views/face-policy.js +16 -0
- package/lib/views/glb.js +218 -24
- package/lib/views/pose.d.ts +7 -1
- package/lib/views/pose.js +63 -12
- package/lib/views/raster.d.ts +6 -2
- package/lib/views/raster.js +49 -37
- package/lib/views/scene-copy.d.ts +6 -0
- package/lib/views/scene-copy.js +73 -0
- package/lib/views/scene-raster.js +2 -1
- package/lib/views/triangle-coverage.d.ts +8 -0
- package/lib/views/triangle-coverage.js +58 -0
- package/lib/workspace-node.js +7 -3
- package/lib/workspace.d.ts +2 -2
- package/package.json +3 -1
- package/plugin.json +1 -1
- package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
- package/plugins/kiln-engine/README.md +3 -3
- package/plugins/kiln-engine/package-provenance.json +6 -6
- package/plugins/kiln-engine/plugin.json +1 -1
- package/plugins/kiln-engine/runtime.json +1 -1
- package/skills/kiln-author-asset/SKILL.md +1 -1
- package/skills/kiln-author-asset/references/camera-cli.md +13 -0
- package/skills/kiln-author-asset/references/geometry-recipes.md +134 -0
- package/skills/kiln-author-asset/references/projects-and-materials.md +32 -2
- package/skills/kiln-compose-scene/references/runtime-scenes.md +19 -0
- package/skills/kiln-qa-asset/references/integration-checks.md +37 -2
- package/src/asset-materials-node.ts +38 -45
- package/src/assets-node.ts +22 -5
- package/src/assets.ts +61 -20
- package/src/atlas-provenance.ts +522 -0
- package/src/character.ts +107 -2
- package/src/community-exporter.ts +143 -3
- package/src/discovery/helper-contracts.ts +188 -7
- package/src/discovery/helper-specs.ts +69 -4
- package/src/discovery/shapes.ts +25 -9
- package/src/engine-identity.ts +1 -1
- package/src/evaluator/authoring-diagnostic.ts +19 -1
- package/src/evaluator/protocol.ts +19 -9
- package/src/evaluator/subprocess.ts +2 -2
- package/src/exporter-node.ts +12 -1
- package/src/generated/mcp-manifest.json +3 -3
- package/src/geometry-budget.ts +8 -0
- package/src/gltf-texture-binding.ts +60 -0
- package/src/material-authoring-node.ts +68 -0
- package/src/material-authoring-schema.ts +343 -0
- package/src/material-export.ts +25 -0
- package/src/material-library-node.ts +840 -16
- package/src/material-library.ts +348 -5
- package/src/pinned-material-map.ts +43 -0
- package/src/portable-material-runtime.ts +200 -0
- package/src/portable-sampler-runtime.ts +40 -0
- package/src/portable-sampler.ts +54 -0
- package/src/primitives.ts +98 -4
- package/src/procedural-material-v2.ts +2 -1
- package/src/procedural-material-v3.ts +224 -0
- package/src/procedural-texture-v2-compiler.ts +234 -0
- package/src/procedural-texture-v3-contract.ts +437 -0
- package/src/procedural-texture-v3.ts +220 -0
- package/src/procedural-texture.ts +61 -229
- package/src/project-bundle-node.ts +106 -14
- package/src/project-bundle.ts +61 -18
- package/src/project-cli.ts +29 -17
- package/src/qa/atlas.ts +597 -0
- package/src/qa/index.ts +1 -0
- package/src/qa/material.ts +2 -0
- package/src/qa/skin-integrity.ts +291 -0
- package/src/render-service-host.ts +4 -6
- package/src/render.ts +133 -15
- package/src/skin-binding.ts +225 -0
- package/src/texture-atlas-contract.ts +558 -0
- package/src/texture-atlas-pixels.ts +456 -0
- package/src/texture-atlas.ts +185 -0
- package/src/texture-bake.ts +88 -10
- package/src/texture-resolver.ts +55 -0
- package/src/tools/materials.ts +15 -11
- package/src/tools/registry.ts +5 -15
- package/src/tools/review.ts +1 -1
- package/src/uv-data.ts +238 -0
- package/src/uv-shapes.ts +3 -0
- package/src/uv.ts +73 -128
- package/src/viewer/app.ts +37 -0
- package/src/viewer/live-panel.ts +57 -1
- package/src/viewer/material-preview-panel.ts +140 -0
- package/src/viewer/material-preview.ts +138 -0
- package/src/viewer/materials-panel.ts +34 -5
- package/src/viewer/navigation.ts +2 -0
- package/src/views/camera.ts +37 -40
- package/src/views/face-policy.ts +20 -0
- package/src/views/glb.ts +294 -17
- package/src/views/pose.ts +71 -13
- package/src/views/raster.ts +73 -46
- package/src/views/scene-copy.ts +86 -0
- package/src/views/scene-raster.ts +2 -1
- package/src/views/triangle-coverage.ts +78 -0
- package/src/workspace-node.ts +11 -3
- 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
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
268
|
-
export
|
|
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 {
|
|
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 {
|
|
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<
|
|
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
|
-
|
|
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
|
|
91
|
+
let manifest: MaterialManifest;
|
|
69
92
|
try {
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
215
|
-
|
|
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));
|
package/src/project-bundle.ts
CHANGED
|
@@ -14,8 +14,10 @@ import { MAX_PROJECT_REVISIONS, projectRevisionSchema, type ProjectRevision } fr
|
|
|
14
14
|
import {
|
|
15
15
|
canonicalMaterialJson,
|
|
16
16
|
materialManifestSchema,
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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:
|
|
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:
|
|
121
|
-
const manifest =
|
|
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:
|
|
148
|
-
const
|
|
149
|
-
|
|
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
|
|
232
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
371
|
-
|
|
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
|
|
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');
|