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