@instruktlabs/kiln 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +74 -0
- package/README.md +7 -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/material-library.md +9 -5
- 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/sdk.md +53 -0
- 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/references/geometry-recipes.md +124 -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/views/glb.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/** GLB-native input adapter for the deterministic CPU geometry-flat renderer. */
|
|
2
2
|
import { useCubicSpline } from '../animation-cubic';
|
|
3
|
+
import { gltfTextureBindingKey, restoreGltfTextureBinding } from '../gltf-texture-binding';
|
|
4
|
+
import { isCubicSplineTrack } from '../animation-spline';
|
|
3
5
|
import { createGltfIO } from '../gltf-io';
|
|
4
6
|
import { listLodChainNodes } from '../lod-export';
|
|
5
7
|
import { isHiddenGltfNode } from '../metrics';
|
|
6
8
|
import { registerReviewLodChains } from './lod';
|
|
9
|
+
import {
|
|
10
|
+
atlasDocumentTable,
|
|
11
|
+
expandAtlasBinding,
|
|
12
|
+
restoreAtlasTextureProvenance,
|
|
13
|
+
validateAtlasDocumentProvenance,
|
|
14
|
+
type AtlasProvenanceTableV1,
|
|
15
|
+
} from '../atlas-provenance';
|
|
7
16
|
|
|
8
17
|
import {
|
|
9
18
|
Primitive,
|
|
@@ -18,6 +27,9 @@ import {
|
|
|
18
27
|
Group,
|
|
19
28
|
Matrix4,
|
|
20
29
|
Mesh,
|
|
30
|
+
Bone,
|
|
31
|
+
SkinnedMesh,
|
|
32
|
+
Skeleton,
|
|
21
33
|
MeshStandardMaterial,
|
|
22
34
|
NoColorSpace,
|
|
23
35
|
SRGBColorSpace,
|
|
@@ -25,6 +37,8 @@ import {
|
|
|
25
37
|
type Object3D,
|
|
26
38
|
AnimationClip,
|
|
27
39
|
QuaternionKeyframeTrack,
|
|
40
|
+
NumberKeyframeTrack,
|
|
41
|
+
PropertyBinding,
|
|
28
42
|
VectorKeyframeTrack,
|
|
29
43
|
Quaternion,
|
|
30
44
|
Vector3,
|
|
@@ -188,11 +202,19 @@ function reviewClipsFromExtras(extras: unknown): AnimationClip[] | undefined {
|
|
|
188
202
|
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', 'Invalid animation review track.');
|
|
189
203
|
}
|
|
190
204
|
const property = track.name.slice(track.name.lastIndexOf('.') + 1);
|
|
191
|
-
const
|
|
205
|
+
const morph = /^morphTargetInfluences(?:\[[^\]]+\])?$/.test(property);
|
|
206
|
+
const Track = morph
|
|
207
|
+
? NumberKeyframeTrack
|
|
208
|
+
: property === 'quaternion'
|
|
209
|
+
? QuaternionKeyframeTrack
|
|
210
|
+
: VectorKeyframeTrack;
|
|
192
211
|
if (track.interpolation === 'CUBICSPLINE') {
|
|
212
|
+
const tuple = morph ? track.values.length / track.times.length : cubicTuple(property);
|
|
193
213
|
if (
|
|
194
214
|
track.times.length < 2 ||
|
|
195
|
-
track.values.length !== track.times.length *
|
|
215
|
+
track.values.length !== track.times.length * tuple ||
|
|
216
|
+
!Number.isInteger(tuple / 3) ||
|
|
217
|
+
tuple < 3
|
|
196
218
|
) {
|
|
197
219
|
throw new GlbGeometryFlatError(
|
|
198
220
|
'GLB_FLAT_PARSE_FAILED',
|
|
@@ -309,7 +331,9 @@ function flatMaterial(material: Material | null): FlatMaterial {
|
|
|
309
331
|
function preserveTexture(
|
|
310
332
|
source: GltfTexture | null,
|
|
311
333
|
usage: 'color' | 'data',
|
|
312
|
-
cache: Map<GltfTexture, Map<
|
|
334
|
+
cache: Map<GltfTexture, Map<string, Texture>>,
|
|
335
|
+
info?: import('@gltf-transform/core').TextureInfo | null,
|
|
336
|
+
atlas?: AtlasProvenanceTableV1,
|
|
313
337
|
): Texture | null {
|
|
314
338
|
if (!source) return null;
|
|
315
339
|
const encoded = source.getImage();
|
|
@@ -320,13 +344,18 @@ function preserveTexture(
|
|
|
320
344
|
byUsage = new Map();
|
|
321
345
|
cache.set(source, byUsage);
|
|
322
346
|
}
|
|
323
|
-
const
|
|
347
|
+
const reference = info?.getExtras()['kilnAtlasReference'];
|
|
348
|
+
const cacheKey = `${usage}:${gltfTextureBindingKey(info)}`;
|
|
349
|
+
const existing = byUsage.get(cacheKey);
|
|
324
350
|
if (existing) return existing;
|
|
325
351
|
const texture = new Texture();
|
|
326
352
|
texture.name = source.getName();
|
|
327
353
|
texture.colorSpace = usage === 'color' ? SRGBColorSpace : NoColorSpace;
|
|
328
354
|
texture.userData.encoded = { mime, bytes: Uint8Array.from(encoded) };
|
|
329
|
-
|
|
355
|
+
texture.flipY = false;
|
|
356
|
+
if (info) restoreGltfTextureBinding(texture, info);
|
|
357
|
+
if (reference && info && atlas) restoreAtlasTextureProvenance(texture, info, atlas);
|
|
358
|
+
byUsage.set(cacheKey, texture);
|
|
330
359
|
return texture;
|
|
331
360
|
}
|
|
332
361
|
|
|
@@ -415,6 +444,7 @@ export async function loadGlbGeometryFlatScene(
|
|
|
415
444
|
`Could not parse final GLB bytes: ${error instanceof Error ? error.message : String(error)}`,
|
|
416
445
|
);
|
|
417
446
|
}
|
|
447
|
+
await validateAtlasDocumentProvenance(document);
|
|
418
448
|
const scene = document.getRoot().getDefaultScene() ?? document.getRoot().listScenes()[0];
|
|
419
449
|
if (!scene) throw new GlbGeometryFlatError('GLB_FLAT_NO_SCENE', 'Final GLB has no scene.');
|
|
420
450
|
|
|
@@ -430,16 +460,29 @@ export async function loadGlbGeometryFlatScene(
|
|
|
430
460
|
};
|
|
431
461
|
for (const child of scene.listChildren()) visit(child);
|
|
432
462
|
|
|
463
|
+
// Skinning needs the joint hierarchy and inverse bind matrices. Keep the
|
|
464
|
+
// historical flat adapter for rigid scenes, but share the review loader for
|
|
465
|
+
// a posed derivative instead of silently rasterizing its bind-pose positions.
|
|
466
|
+
if (
|
|
467
|
+
[...sceneNodes].some(
|
|
468
|
+
(node) =>
|
|
469
|
+
node.getSkin() ||
|
|
470
|
+
node
|
|
471
|
+
.getMesh()
|
|
472
|
+
?.listPrimitives()
|
|
473
|
+
.some((p) => p.listTargets().length),
|
|
474
|
+
)
|
|
475
|
+
) {
|
|
476
|
+
await validateAtlasDocumentProvenance(document);
|
|
477
|
+
return reviewSceneFromDocument(document);
|
|
478
|
+
}
|
|
479
|
+
|
|
433
480
|
for (const node of sceneNodes) {
|
|
434
481
|
const mesh = node.getMesh();
|
|
435
482
|
if (!mesh) continue;
|
|
436
|
-
if (node.getSkin()) reasons.add(GLB_GEOMETRY_FLAT_REASON.SKIN_DEFORMATION_UNSUPPORTED);
|
|
437
483
|
const instances = instanceMatrices(node);
|
|
438
484
|
instanceCount += instances.count;
|
|
439
485
|
for (const primitive of mesh.listPrimitives()) {
|
|
440
|
-
if (primitive.listTargets().length > 0) {
|
|
441
|
-
reasons.add(GLB_GEOMETRY_FLAT_REASON.MORPH_TARGETS_UNSUPPORTED);
|
|
442
|
-
}
|
|
443
486
|
const positionAccessor = primitive.getAttribute('POSITION');
|
|
444
487
|
if (positionAccessor?.getElementSize() !== 3) continue;
|
|
445
488
|
const positions = decodedAccessor(positionAccessor);
|
|
@@ -536,20 +579,55 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
536
579
|
} catch {
|
|
537
580
|
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', 'Could not parse final GLB bytes.');
|
|
538
581
|
}
|
|
582
|
+
await validateAtlasDocumentProvenance(document);
|
|
583
|
+
return reviewSceneFromDocument(document);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function reviewSceneFromDocument(
|
|
587
|
+
document: import('@gltf-transform/core').Document,
|
|
588
|
+
): LoadedGlbReviewScene {
|
|
539
589
|
const sourceScene = document.getRoot().getDefaultScene() ?? document.getRoot().listScenes()[0];
|
|
540
590
|
if (!sourceScene) throw new GlbGeometryFlatError('GLB_FLAT_NO_SCENE', 'Final GLB has no scene.');
|
|
541
591
|
const reasons = new Set<GlbGeometryFlatReasonCode>();
|
|
542
592
|
const root = new Group();
|
|
543
593
|
root.name = sourceScene.getName() || 'Scene';
|
|
544
594
|
const nodeMap = new Map<import('@gltf-transform/core').Node, Object3D>();
|
|
545
|
-
const
|
|
595
|
+
const atlas = atlasDocumentTable(document);
|
|
596
|
+
const textureCache = new Map<GltfTexture, Map<string, Texture>>();
|
|
597
|
+
const morphMeshes = new Map<import('@gltf-transform/core').Node, Mesh[]>();
|
|
598
|
+
// Morph primitives need unique, binding-safe names: the historical ':primitive'
|
|
599
|
+
// suffix is a namespace separator to Three.PropertyBinding. Parent names stay intact.
|
|
600
|
+
const usedNames = new Set(
|
|
601
|
+
document
|
|
602
|
+
.getRoot()
|
|
603
|
+
.listNodes()
|
|
604
|
+
.map((node) => node.getName()),
|
|
605
|
+
);
|
|
606
|
+
const nodeIds = new Map(
|
|
607
|
+
document
|
|
608
|
+
.getRoot()
|
|
609
|
+
.listNodes()
|
|
610
|
+
.map((node, index) => [node, index]),
|
|
611
|
+
);
|
|
612
|
+
let morphValues = 0;
|
|
613
|
+
const morphError = (detail: string): never => {
|
|
614
|
+
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', `Invalid morph review data: ${detail}`);
|
|
615
|
+
};
|
|
616
|
+
const joints = new Set(
|
|
617
|
+
document
|
|
618
|
+
.getRoot()
|
|
619
|
+
.listSkins()
|
|
620
|
+
.flatMap((skin) => skin.listJoints()),
|
|
621
|
+
);
|
|
622
|
+
const pendingSkins: Array<{ source: import('@gltf-transform/core').Node; mesh: SkinnedMesh }> =
|
|
623
|
+
[];
|
|
546
624
|
let meshCount = 0;
|
|
547
625
|
let instanceCount = 0;
|
|
548
626
|
|
|
549
627
|
const buildNode = (source: import('@gltf-transform/core').Node): Object3D => {
|
|
550
628
|
const existing = nodeMap.get(source);
|
|
551
629
|
if (existing) return existing;
|
|
552
|
-
const target = new Group();
|
|
630
|
+
const target = joints.has(source) ? new Bone() : new Group();
|
|
553
631
|
target.name = source.getName();
|
|
554
632
|
target.position.fromArray(source.getTranslation());
|
|
555
633
|
target.quaternion.fromArray(source.getRotation());
|
|
@@ -560,13 +638,29 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
560
638
|
nodeMap.set(source, target);
|
|
561
639
|
const sourceMesh = source.getMesh();
|
|
562
640
|
if (sourceMesh) {
|
|
641
|
+
const targetCount = sourceMesh.listPrimitives()[0]?.listTargets().length ?? 0;
|
|
642
|
+
if (sourceMesh.listPrimitives().some((p) => p.listTargets().length !== targetCount))
|
|
643
|
+
morphError('primitives must have equal target counts.');
|
|
644
|
+
const nodeWeights = source.getWeights(),
|
|
645
|
+
meshWeights = sourceMesh.getWeights();
|
|
646
|
+
for (const weights of [nodeWeights, meshWeights])
|
|
647
|
+
if (weights.length && (weights.length !== targetCount || !weights.every(Number.isFinite)))
|
|
648
|
+
morphError('default weight count or values.');
|
|
649
|
+
const weights = nodeWeights.length
|
|
650
|
+
? nodeWeights
|
|
651
|
+
: meshWeights.length
|
|
652
|
+
? meshWeights
|
|
653
|
+
: Array<number>(targetCount).fill(0);
|
|
654
|
+
// glTF Transform decodes extras.targetNames into PrimitiveTarget names.
|
|
655
|
+
const targetNames =
|
|
656
|
+
sourceMesh
|
|
657
|
+
.listPrimitives()[0]
|
|
658
|
+
?.listTargets()
|
|
659
|
+
.map((target, index) => target.getName() || String(index)) ?? [];
|
|
563
660
|
target.userData.kilnGlbMeshNode = true;
|
|
564
|
-
if (source.getSkin()) reasons.add(GLB_GEOMETRY_FLAT_REASON.SKIN_DEFORMATION_UNSUPPORTED);
|
|
565
661
|
const matrices = localInstanceMatrices(source);
|
|
566
662
|
instanceCount += matrices.length;
|
|
567
663
|
for (const [primitiveIndex, primitive] of sourceMesh.listPrimitives().entries()) {
|
|
568
|
-
if (primitive.listTargets().length)
|
|
569
|
-
reasons.add(GLB_GEOMETRY_FLAT_REASON.MORPH_TARGETS_UNSUPPORTED);
|
|
570
664
|
const position = primitive.getAttribute('POSITION');
|
|
571
665
|
if (position?.getElementSize() !== 3) continue;
|
|
572
666
|
const indices = triangleIndices(primitive, position.getCount());
|
|
@@ -578,6 +672,9 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
578
672
|
noteMaterialTextures(material, reasons);
|
|
579
673
|
const flat = flatMaterial(material);
|
|
580
674
|
const geometry = new BufferGeometry();
|
|
675
|
+
const atlasBinding = primitive.getExtras()['kilnAtlasBinding'];
|
|
676
|
+
if (atlasBinding !== undefined && atlas)
|
|
677
|
+
geometry.userData.kilnAtlasBinding = expandAtlasBinding(atlasBinding, atlas);
|
|
581
678
|
geometry.setAttribute(
|
|
582
679
|
'position',
|
|
583
680
|
new BufferAttribute(Float32Array.from(decodedAccessor(position)), 3),
|
|
@@ -604,6 +701,72 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
604
701
|
);
|
|
605
702
|
}
|
|
606
703
|
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
704
|
+
if (targetCount) {
|
|
705
|
+
const targets = primitive.listTargets();
|
|
706
|
+
for (const target of targets)
|
|
707
|
+
for (const semantic of target.listSemantics()) {
|
|
708
|
+
const attribute = target.getAttribute(semantic)!;
|
|
709
|
+
if (
|
|
710
|
+
!primitive.getAttribute(semantic) ||
|
|
711
|
+
attribute.getCount() !== position.getCount() ||
|
|
712
|
+
((semantic === 'POSITION' || semantic === 'NORMAL' || semantic === 'TANGENT') &&
|
|
713
|
+
attribute.getElementSize() !== 3)
|
|
714
|
+
)
|
|
715
|
+
morphError(`target attribute ${semantic} does not match the base primitive.`);
|
|
716
|
+
if (semantic !== 'POSITION' && semantic !== 'NORMAL')
|
|
717
|
+
reasons.add(GLB_GEOMETRY_FLAT_REASON.MORPH_TARGETS_UNSUPPORTED);
|
|
718
|
+
}
|
|
719
|
+
const semantics = targets.some((target) => target.getAttribute('NORMAL'))
|
|
720
|
+
? (['POSITION', 'NORMAL'] as const)
|
|
721
|
+
: (['POSITION'] as const);
|
|
722
|
+
// Bound zero-filled missing attributes as well as decoded ones: a tiny
|
|
723
|
+
// file with many omitted deltas must not expand without limit.
|
|
724
|
+
morphValues += targetCount * position.getCount() * 3 * semantics.length;
|
|
725
|
+
if (morphValues > 16_777_216)
|
|
726
|
+
morphError('expanded morph attributes exceed the 64 MiB review budget.');
|
|
727
|
+
geometry.morphTargetsRelative = true;
|
|
728
|
+
for (const semantic of semantics) {
|
|
729
|
+
const attributes = targets.map((target) => {
|
|
730
|
+
const attribute = target.getAttribute(semantic);
|
|
731
|
+
const values = attribute
|
|
732
|
+
? Float32Array.from(decodedAccessor(attribute))
|
|
733
|
+
: new Float32Array(position.getCount() * 3);
|
|
734
|
+
if (!values.every(Number.isFinite)) morphError(`nonfinite ${semantic} displacement.`);
|
|
735
|
+
return new BufferAttribute(values, 3);
|
|
736
|
+
});
|
|
737
|
+
if (semantic === 'POSITION') geometry.morphAttributes.position = attributes;
|
|
738
|
+
else geometry.morphAttributes.normal = attributes;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (source.getSkin()) {
|
|
742
|
+
const skinIndices = primitive.getAttribute('JOINTS_0');
|
|
743
|
+
const skinWeights = primitive.getAttribute('WEIGHTS_0');
|
|
744
|
+
if (
|
|
745
|
+
!skinIndices ||
|
|
746
|
+
!skinWeights ||
|
|
747
|
+
skinIndices.getElementSize() !== 4 ||
|
|
748
|
+
skinWeights.getElementSize() !== 4 ||
|
|
749
|
+
skinIndices.getCount() !== position.getCount() ||
|
|
750
|
+
skinWeights.getCount() !== position.getCount()
|
|
751
|
+
)
|
|
752
|
+
throw new GlbGeometryFlatError(
|
|
753
|
+
'GLB_FLAT_PARSE_FAILED',
|
|
754
|
+
`Invalid skin attributes on ${source.getName()}.`,
|
|
755
|
+
);
|
|
756
|
+
if (primitive.getAttribute('JOINTS_1') || primitive.getAttribute('WEIGHTS_1'))
|
|
757
|
+
throw new GlbGeometryFlatError(
|
|
758
|
+
'GLB_FLAT_PARSE_FAILED',
|
|
759
|
+
`Skin review supports four influences per vertex; ${source.getName()} has additional joint/weight sets.`,
|
|
760
|
+
);
|
|
761
|
+
geometry.setAttribute(
|
|
762
|
+
'skinIndex',
|
|
763
|
+
new BufferAttribute(Uint16Array.from(decodedAccessor(skinIndices)), 4),
|
|
764
|
+
);
|
|
765
|
+
geometry.setAttribute(
|
|
766
|
+
'skinWeight',
|
|
767
|
+
new BufferAttribute(Float32Array.from(decodedAccessor(skinWeights)), 4),
|
|
768
|
+
);
|
|
769
|
+
}
|
|
607
770
|
const threeMaterial = new MeshStandardMaterial({
|
|
608
771
|
opacity: flat.opacity,
|
|
609
772
|
transparent: flat.alphaMode === 'BLEND',
|
|
@@ -621,17 +784,23 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
621
784
|
material.getBaseColorTexture(),
|
|
622
785
|
'color',
|
|
623
786
|
textureCache,
|
|
787
|
+
material.getBaseColorTextureInfo(),
|
|
788
|
+
atlas,
|
|
624
789
|
);
|
|
625
790
|
threeMaterial.normalMap = preserveTexture(
|
|
626
791
|
material.getNormalTexture(),
|
|
627
792
|
'data',
|
|
628
793
|
textureCache,
|
|
794
|
+
material.getNormalTextureInfo(),
|
|
795
|
+
atlas,
|
|
629
796
|
);
|
|
630
797
|
threeMaterial.normalScale.setScalar(material.getNormalScale());
|
|
631
798
|
const metallicRoughness = preserveTexture(
|
|
632
799
|
material.getMetallicRoughnessTexture(),
|
|
633
800
|
'data',
|
|
634
801
|
textureCache,
|
|
802
|
+
material.getMetallicRoughnessTextureInfo(),
|
|
803
|
+
atlas,
|
|
635
804
|
);
|
|
636
805
|
threeMaterial.metalnessMap = metallicRoughness;
|
|
637
806
|
threeMaterial.roughnessMap = metallicRoughness;
|
|
@@ -639,22 +808,47 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
639
808
|
material.getEmissiveTexture(),
|
|
640
809
|
'color',
|
|
641
810
|
textureCache,
|
|
811
|
+
material.getEmissiveTextureInfo(),
|
|
812
|
+
atlas,
|
|
642
813
|
);
|
|
643
814
|
threeMaterial.aoMap = preserveTexture(
|
|
644
815
|
material.getOcclusionTexture(),
|
|
645
816
|
'data',
|
|
646
817
|
textureCache,
|
|
818
|
+
material.getOcclusionTextureInfo(),
|
|
819
|
+
atlas,
|
|
647
820
|
);
|
|
648
821
|
threeMaterial.aoMapIntensity = material.getOcclusionStrength();
|
|
649
822
|
}
|
|
650
823
|
for (const [index, matrix] of matrices.entries()) {
|
|
651
|
-
const mesh =
|
|
824
|
+
const mesh = source.getSkin()
|
|
825
|
+
? new SkinnedMesh(geometry, threeMaterial)
|
|
826
|
+
: new Mesh(geometry, threeMaterial);
|
|
827
|
+
if (mesh instanceof SkinnedMesh) pendingSkins.push({ source, mesh });
|
|
652
828
|
// A deduplicated mesh resource can serve many distinctly named nodes.
|
|
653
829
|
// QA repair hints must identify this owning node, not the shared resource.
|
|
654
830
|
const baseName = source.getName() || sourceMesh.getName() || 'Mesh';
|
|
655
831
|
mesh.name = `${baseName}:primitive-${primitiveIndex}${
|
|
656
832
|
matrices.length === 1 ? '' : `:instance-${index}`
|
|
657
833
|
}`;
|
|
834
|
+
if (targetCount) {
|
|
835
|
+
let bindingName = `${PropertyBinding.sanitizeNodeName(baseName)}_morph_${nodeIds.get(source)}_${primitiveIndex}_${index}`;
|
|
836
|
+
while (usedNames.has(bindingName)) bindingName += '_';
|
|
837
|
+
usedNames.add(bindingName);
|
|
838
|
+
mesh.name = bindingName;
|
|
839
|
+
mesh.morphTargetInfluences = [...weights];
|
|
840
|
+
if (
|
|
841
|
+
Array.isArray(targetNames) &&
|
|
842
|
+
targetNames.length === targetCount &&
|
|
843
|
+
targetNames.every((name) => typeof name === 'string')
|
|
844
|
+
)
|
|
845
|
+
mesh.morphTargetDictionary = Object.fromEntries(
|
|
846
|
+
targetNames.map((name, index) => [name, index]),
|
|
847
|
+
);
|
|
848
|
+
const siblings = morphMeshes.get(source) ?? [];
|
|
849
|
+
siblings.push(mesh);
|
|
850
|
+
morphMeshes.set(source, siblings);
|
|
851
|
+
}
|
|
658
852
|
mesh.matrixAutoUpdate = false;
|
|
659
853
|
mesh.matrix.copy(matrix);
|
|
660
854
|
target.add(mesh);
|
|
@@ -685,6 +879,41 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
685
879
|
})),
|
|
686
880
|
);
|
|
687
881
|
|
|
882
|
+
for (const { source, mesh } of pendingSkins) {
|
|
883
|
+
const skin = source.getSkin()!;
|
|
884
|
+
const bones = skin.listJoints().map((joint) => nodeMap.get(joint));
|
|
885
|
+
const inverse = skin.getInverseBindMatrices();
|
|
886
|
+
if (
|
|
887
|
+
!bones.length ||
|
|
888
|
+
bones.some((bone) => !(bone instanceof Bone)) ||
|
|
889
|
+
(inverse && (inverse.getElementSize() !== 16 || inverse.getCount() < bones.length))
|
|
890
|
+
)
|
|
891
|
+
throw new GlbGeometryFlatError(
|
|
892
|
+
'GLB_FLAT_PARSE_FAILED',
|
|
893
|
+
`Invalid or unavailable skin joints/bind matrices on ${source.getName()}.`,
|
|
894
|
+
);
|
|
895
|
+
const indices = mesh.geometry.getAttribute('skinIndex');
|
|
896
|
+
const weights = mesh.geometry.getAttribute('skinWeight');
|
|
897
|
+
for (let i = 0; i < indices.array.length; i++)
|
|
898
|
+
if (
|
|
899
|
+
indices.array[i]! >= bones.length ||
|
|
900
|
+
!Number.isFinite(weights.array[i]) ||
|
|
901
|
+
weights.array[i]! < 0
|
|
902
|
+
)
|
|
903
|
+
throw new GlbGeometryFlatError(
|
|
904
|
+
'GLB_FLAT_PARSE_FAILED',
|
|
905
|
+
`Invalid skin joint index or weight on ${source.getName()}.`,
|
|
906
|
+
);
|
|
907
|
+
const matrices = bones.map((_, index) =>
|
|
908
|
+
new Matrix4().fromArray(inverse ? inverse.getElement(index, []) : new Matrix4().elements),
|
|
909
|
+
);
|
|
910
|
+
// glTF's inverse bind already maps mesh-local positions into joint space.
|
|
911
|
+
// Identity bindMatrix and AttachedBindMode cancel the mesh world transform,
|
|
912
|
+
// matching GLTFLoader even under translated/scaled mesh ancestors.
|
|
913
|
+
mesh.bind(new Skeleton(bones as Bone[], matrices), new Matrix4());
|
|
914
|
+
}
|
|
915
|
+
root.updateMatrixWorld(true);
|
|
916
|
+
|
|
688
917
|
const nativeClips = document
|
|
689
918
|
.getRoot()
|
|
690
919
|
.listAnimations()
|
|
@@ -701,7 +930,7 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
701
930
|
const output = sampler?.getOutput()?.getArray();
|
|
702
931
|
const property =
|
|
703
932
|
path === 'translation' ? 'position' : path === 'rotation' ? 'quaternion' : path;
|
|
704
|
-
if (!node || !property ||
|
|
933
|
+
if (!node || !property || !input || !output) return [];
|
|
705
934
|
const interpolation: string = sampler!.getInterpolation();
|
|
706
935
|
if (
|
|
707
936
|
interpolation !== 'STEP' &&
|
|
@@ -713,6 +942,33 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
713
942
|
`Animation review does not support ${interpolation} interpolation; use LINEAR, STEP or CUBICSPLINE.`,
|
|
714
943
|
);
|
|
715
944
|
}
|
|
945
|
+
if (property === 'weights') {
|
|
946
|
+
const targets = morphMeshes.get(node);
|
|
947
|
+
if (!targets?.length) return [];
|
|
948
|
+
const width = targets[0]!.morphTargetInfluences!.length;
|
|
949
|
+
const values = decodedAccessor(sampler!.getOutput()!);
|
|
950
|
+
if (
|
|
951
|
+
input.length < 1 ||
|
|
952
|
+
input.length > REVIEW_CLIP_LIMITS.samplesPerTrack ||
|
|
953
|
+
values.length > REVIEW_CLIP_LIMITS.valuesPerTrack ||
|
|
954
|
+
values.length !==
|
|
955
|
+
input.length * width * (interpolation === 'CUBICSPLINE' ? 3 : 1) ||
|
|
956
|
+
!Array.from(input).every(
|
|
957
|
+
(v, i) => Number.isFinite(v) && v >= 0 && (i === 0 || v > input[i - 1]!),
|
|
958
|
+
) ||
|
|
959
|
+
!Array.from(values).every(Number.isFinite)
|
|
960
|
+
)
|
|
961
|
+
morphError('animation weight sampler width or values.');
|
|
962
|
+
return targets.map((mesh) => {
|
|
963
|
+
const track = new NumberKeyframeTrack(
|
|
964
|
+
`${mesh.name}.morphTargetInfluences`,
|
|
965
|
+
input,
|
|
966
|
+
values,
|
|
967
|
+
interpolation === 'STEP' ? InterpolateDiscrete : InterpolateLinear,
|
|
968
|
+
);
|
|
969
|
+
return interpolation === 'CUBICSPLINE' ? useCubicSpline(track) : track;
|
|
970
|
+
});
|
|
971
|
+
}
|
|
716
972
|
const Track = path === 'rotation' ? QuaternionKeyframeTrack : VectorKeyframeTrack;
|
|
717
973
|
if (interpolation === 'CUBICSPLINE') {
|
|
718
974
|
if (input.length < 2 || output.length !== input.length * cubicTuple(property)) {
|
|
@@ -736,7 +992,28 @@ export async function loadGlbReviewScene(bytes: Uint8Array): Promise<LoadedGlbRe
|
|
|
736
992
|
(animation.getExtras() as { kilnLoopIntent?: unknown }).kilnLoopIntent,
|
|
737
993
|
),
|
|
738
994
|
);
|
|
739
|
-
const
|
|
995
|
+
const extrasClips = reviewClipsFromExtras(sourceScene.getExtras());
|
|
996
|
+
if (extrasClips)
|
|
997
|
+
for (const clip of extrasClips)
|
|
998
|
+
clip.tracks = clip.tracks.flatMap((track) => {
|
|
999
|
+
const dot = track.name.lastIndexOf('.'),
|
|
1000
|
+
property = track.name.slice(dot + 1);
|
|
1001
|
+
if (!/^morphTargetInfluences(?:\[[^\]]+\])?$/.test(property)) return [track];
|
|
1002
|
+
const sources = [...morphMeshes.keys()].filter(
|
|
1003
|
+
(node) => node.getName() === track.name.slice(0, dot),
|
|
1004
|
+
);
|
|
1005
|
+
if (sources.length !== 1) return [track]; // Preserve unresolved/ambiguous author evidence.
|
|
1006
|
+
return morphMeshes.get(sources[0]!)!.map((mesh) => {
|
|
1007
|
+
const copy = new NumberKeyframeTrack(
|
|
1008
|
+
`${mesh.name}.${property}`,
|
|
1009
|
+
track.times,
|
|
1010
|
+
track.values,
|
|
1011
|
+
track.getInterpolation(),
|
|
1012
|
+
);
|
|
1013
|
+
return isCubicSplineTrack(track) ? useCubicSpline(copy) : copy;
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
const clips = extrasClips ?? nativeClips;
|
|
740
1017
|
return {
|
|
741
1018
|
root,
|
|
742
1019
|
clips,
|
package/src/views/pose.ts
CHANGED
|
@@ -19,6 +19,9 @@ import { cubicSegment, isCubicSplineTrack, normalizeQuaternion } from '../animat
|
|
|
19
19
|
/** Minimal duck-typed view of a Three.js node we pose. */
|
|
20
20
|
interface DuckNode {
|
|
21
21
|
name?: string;
|
|
22
|
+
uuid?: string;
|
|
23
|
+
morphTargetInfluences?: number[];
|
|
24
|
+
morphTargetDictionary?: Record<string, number>;
|
|
22
25
|
position?: { set(x: number, y: number, z: number): void };
|
|
23
26
|
quaternion?: { set(x: number, y: number, z: number, w: number): void };
|
|
24
27
|
scale?: { set(x: number, y: number, z: number): void };
|
|
@@ -44,7 +47,7 @@ export interface DuckClip {
|
|
|
44
47
|
userData?: { kilnLoopIntent?: unknown };
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
type Prop = 'position' | 'quaternion' | 'scale';
|
|
50
|
+
type Prop = 'position' | 'quaternion' | 'scale' | 'morphTargetInfluences';
|
|
48
51
|
|
|
49
52
|
interface PreparedTrack {
|
|
50
53
|
nodeName: string;
|
|
@@ -54,6 +57,9 @@ interface PreparedTrack {
|
|
|
54
57
|
/** One value of `stride` per key; a CUBICSPLINE key is [in-tangent, value, out-tangent]. */
|
|
55
58
|
values: number[];
|
|
56
59
|
interpolation: 'LINEAR' | 'STEP' | 'CUBICSPLINE';
|
|
60
|
+
/** Original spelling retained for unresolved-track evidence. */
|
|
61
|
+
sourceName?: string;
|
|
62
|
+
morphIndex?: number;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/** A clip prepared for repeated time-sampling: parsed tracks + a derived duration. */
|
|
@@ -65,7 +71,7 @@ export interface PreparedClip {
|
|
|
65
71
|
unresolved: string[];
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
const PROP_STRIDE
|
|
74
|
+
const PROP_STRIDE = { position: 3, quaternion: 4, scale: 3 } as const;
|
|
69
75
|
|
|
70
76
|
/** Find a clip by name (case-insensitive, trimmed). Returns the first exact match,
|
|
71
77
|
* else the first whose name contains the query, else undefined. */
|
|
@@ -90,10 +96,11 @@ export function clipNames(clips: readonly DuckClip[]): string[] {
|
|
|
90
96
|
* clip doesn't carry one.
|
|
91
97
|
*/
|
|
92
98
|
export function prepareClip(root: DuckNode, clip: DuckClip): PreparedClip {
|
|
93
|
-
const
|
|
99
|
+
const nodes = new Map<string, DuckNode>();
|
|
94
100
|
root.traverse?.((obj) => {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
101
|
+
const node = obj as DuckNode;
|
|
102
|
+
if (node.name && !nodes.has(node.name)) nodes.set(node.name, node);
|
|
103
|
+
if (node.uuid) nodes.set(node.uuid, node);
|
|
97
104
|
});
|
|
98
105
|
|
|
99
106
|
const tracks: PreparedTrack[] = [];
|
|
@@ -106,8 +113,11 @@ export function prepareClip(root: DuckNode, clip: DuckClip): PreparedClip {
|
|
|
106
113
|
const dot = raw.lastIndexOf('.');
|
|
107
114
|
if (dot === -1) continue;
|
|
108
115
|
const nodeName = raw.slice(0, dot);
|
|
109
|
-
const
|
|
110
|
-
|
|
116
|
+
const suffix = raw.slice(dot + 1);
|
|
117
|
+
const morph = /^morphTargetInfluences(?:\[([^\]]+)\])?$/.exec(suffix);
|
|
118
|
+
const property = (morph ? 'morphTargetInfluences' : suffix) as Prop;
|
|
119
|
+
if (property !== 'position' && property !== 'quaternion' && property !== 'scale' && !morph)
|
|
120
|
+
continue;
|
|
111
121
|
|
|
112
122
|
const times = track.times ? Array.from(track.times) : [];
|
|
113
123
|
const values = track.values ? Array.from(track.values) : [];
|
|
@@ -125,16 +135,49 @@ export function prepareClip(root: DuckNode, clip: DuckClip): PreparedClip {
|
|
|
125
135
|
}
|
|
126
136
|
const interpolation = cubic ? 'CUBICSPLINE' : mode === 2300 ? 'STEP' : 'LINEAR';
|
|
127
137
|
|
|
128
|
-
|
|
138
|
+
const node = nodes.get(nodeName);
|
|
139
|
+
const indexed = morph?.[1];
|
|
140
|
+
const morphIndex =
|
|
141
|
+
indexed === undefined
|
|
142
|
+
? undefined
|
|
143
|
+
: /^\d+$/.test(indexed)
|
|
144
|
+
? Number(indexed)
|
|
145
|
+
: node?.morphTargetDictionary?.[indexed];
|
|
146
|
+
let stride = morph
|
|
147
|
+
? indexed === undefined
|
|
148
|
+
? (node?.morphTargetInfluences?.length ?? 0)
|
|
149
|
+
: 1
|
|
150
|
+
: PROP_STRIDE[property as keyof typeof PROP_STRIDE];
|
|
151
|
+
let resolved = !!node;
|
|
152
|
+
if (morph) {
|
|
153
|
+
resolved =
|
|
154
|
+
!!node?.morphTargetInfluences?.length &&
|
|
155
|
+
(indexed === undefined ||
|
|
156
|
+
(Number.isInteger(morphIndex) &&
|
|
157
|
+
morphIndex! >= 0 &&
|
|
158
|
+
morphIndex! < node.morphTargetInfluences.length));
|
|
159
|
+
// Unresolved morph tracks remain visible diagnostics, but never get sampled.
|
|
160
|
+
if (!resolved) stride = values.length / (times.length * (cubic ? 3 : 1));
|
|
161
|
+
if (
|
|
162
|
+
!Number.isInteger(stride) ||
|
|
163
|
+
stride < 1 ||
|
|
164
|
+
values.length !== times.length * stride * (cubic ? 3 : 1) ||
|
|
165
|
+
!values.every(Number.isFinite) ||
|
|
166
|
+
!times.every((v, i) => Number.isFinite(v) && v >= 0 && (i === 0 || v > times[i - 1]!))
|
|
167
|
+
)
|
|
168
|
+
throw new Error(`Invalid morph animation values on ${raw}.`);
|
|
169
|
+
}
|
|
170
|
+
if (!resolved) unresolved.push(raw);
|
|
129
171
|
if (times[times.length - 1]! > maxTime) maxTime = times[times.length - 1]!;
|
|
130
172
|
|
|
131
173
|
tracks.push({
|
|
132
174
|
nodeName,
|
|
133
175
|
prop: property,
|
|
134
|
-
stride
|
|
176
|
+
stride,
|
|
135
177
|
times,
|
|
136
178
|
values,
|
|
137
179
|
interpolation,
|
|
180
|
+
...(morph ? { sourceName: raw, ...(morphIndex === undefined ? {} : { morphIndex }) } : {}),
|
|
138
181
|
});
|
|
139
182
|
}
|
|
140
183
|
|
|
@@ -228,7 +271,7 @@ function sampleTrack(track: PreparedTrack, t: number, out: number[]): void {
|
|
|
228
271
|
}
|
|
229
272
|
if (track.interpolation === 'CUBICSPLINE') {
|
|
230
273
|
cubicSegment(values, stride, i, times[i]!, times[i + 1]!, t, out);
|
|
231
|
-
if (
|
|
274
|
+
if (track.prop === 'quaternion') normalizeQuaternion(out);
|
|
232
275
|
return;
|
|
233
276
|
}
|
|
234
277
|
if (track.interpolation === 'STEP') {
|
|
@@ -243,7 +286,7 @@ function sampleTrack(track: PreparedTrack, t: number, out: number[]): void {
|
|
|
243
286
|
const o0 = i * stride;
|
|
244
287
|
const o1 = (i + 1) * stride;
|
|
245
288
|
|
|
246
|
-
if (
|
|
289
|
+
if (track.prop === 'quaternion') {
|
|
247
290
|
slerpFlat(quatTmp, values, o0, values, o1, a);
|
|
248
291
|
out[0] = quatTmp[0]!;
|
|
249
292
|
out[1] = quatTmp[1]!;
|
|
@@ -328,6 +371,12 @@ export function measureLoopClosure(root: DuckNode, clip: DuckClip): LoopClosureE
|
|
|
328
371
|
}
|
|
329
372
|
result.unassessedTracks = total - prepared.tracks.length;
|
|
330
373
|
for (const track of prepared.tracks) {
|
|
374
|
+
// This established evidence contract measures transforms only. Weight-loop
|
|
375
|
+
// continuity needs a separately versioned evidence field, not a scale label.
|
|
376
|
+
if (track.prop === 'morphTargetInfluences') {
|
|
377
|
+
result.unassessedTracks++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
331
380
|
const name = `${track.nodeName}.${track.prop}`;
|
|
332
381
|
const valid =
|
|
333
382
|
!prepared.unresolved.includes(name) &&
|
|
@@ -400,16 +449,25 @@ export function poseSceneAtTime(root: DuckNode, prepared: PreparedClip, t: numbe
|
|
|
400
449
|
root.traverse?.((obj) => {
|
|
401
450
|
const node = obj as DuckNode;
|
|
402
451
|
if (node.name && !byName.has(node.name)) byName.set(node.name, node);
|
|
452
|
+
if (node.uuid) byName.set(node.uuid, node);
|
|
403
453
|
});
|
|
404
454
|
|
|
405
455
|
const buf: number[] = [0, 0, 0, 0];
|
|
406
456
|
for (const track of prepared.tracks) {
|
|
407
457
|
const node = byName.get(track.nodeName);
|
|
408
|
-
if (
|
|
458
|
+
if (
|
|
459
|
+
!node ||
|
|
460
|
+
prepared.unresolved.includes(track.sourceName ?? `${track.nodeName}.${track.prop}`)
|
|
461
|
+
)
|
|
462
|
+
continue;
|
|
409
463
|
sampleTrack(track, t, buf);
|
|
410
464
|
if (track.prop === 'position') node.position?.set(buf[0]!, buf[1]!, buf[2]!);
|
|
411
465
|
else if (track.prop === 'scale') node.scale?.set(buf[0]!, buf[1]!, buf[2]!);
|
|
412
|
-
else node.quaternion?.set(buf[0]!, buf[1]!, buf[2]!, buf[3]!);
|
|
466
|
+
else if (track.prop === 'quaternion') node.quaternion?.set(buf[0]!, buf[1]!, buf[2]!, buf[3]!);
|
|
467
|
+
else if (node.morphTargetInfluences) {
|
|
468
|
+
if (track.morphIndex !== undefined) node.morphTargetInfluences[track.morphIndex] = buf[0]!;
|
|
469
|
+
else for (let i = 0; i < track.stride; i++) node.morphTargetInfluences[i] = buf[i]!;
|
|
470
|
+
}
|
|
413
471
|
}
|
|
414
472
|
|
|
415
473
|
root.updateMatrixWorld?.(true);
|