@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/lib/views/camera.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Vector3, Matrix4, Euler, Group } from 'three';
|
|
2
2
|
import { collectTriangles, compositingOrder, measureBounds, orbitDir } from "./raster.js";
|
|
3
3
|
import { resolveBackdrop } from "./background.js";
|
|
4
|
+
import { cloneReviewScene } from "./scene-copy.js";
|
|
5
|
+
import { visibleNormalSign } from "./face-policy.js";
|
|
6
|
+
import { prepareTriangleCoverage } from "./triangle-coverage.js";
|
|
4
7
|
const vec = (a) => new Vector3(...a);
|
|
5
8
|
const tuple = (v) => [v.x || 0, v.y || 0, v.z || 0];
|
|
6
9
|
/** Common near-misses for camera keys, named in the unknown-key error. */
|
|
@@ -301,7 +304,11 @@ export function resolveAssetCamera(root, shot = {}) {
|
|
|
301
304
|
const padding = request.padding ?? 1.2;
|
|
302
305
|
const fit = cameraFromBounds(bounds, tuple(direction), padding, tuple(up), measureBounds(root));
|
|
303
306
|
if (request.projection === 'orthographic')
|
|
304
|
-
camera = {
|
|
307
|
+
camera = {
|
|
308
|
+
...fit,
|
|
309
|
+
near: request.near ?? fit.near,
|
|
310
|
+
far: request.far ?? fit.far,
|
|
311
|
+
};
|
|
305
312
|
else {
|
|
306
313
|
const fov = finite(request.fovDeg ?? 50, 'fovDeg');
|
|
307
314
|
if (fov <= 0 || fov >= 180)
|
|
@@ -375,9 +382,9 @@ export async function withCameraVisibility(root, shot, run) {
|
|
|
375
382
|
* A derivative GLB
|
|
376
383
|
* serialized from a scene with hidden nodes (authored, or isolated by
|
|
377
384
|
* {@link withCameraVisibility}) would ship them to the GPU. Every node with
|
|
378
|
-
* `visible = false`
|
|
379
|
-
*
|
|
380
|
-
* restores `.visible` itself.
|
|
385
|
+
* `visible = false` removes drawing from its subtree. Required skeleton ancestors remain
|
|
386
|
+
* as transforms without drawing payloads. Returns the root itself when nothing is hidden,
|
|
387
|
+
* else an independent pruned hierarchy/skeleton; the caller restores `.visible` itself.
|
|
381
388
|
*/
|
|
382
389
|
export function withoutHiddenMeshes(root) {
|
|
383
390
|
if (root.visible === false) {
|
|
@@ -394,15 +401,7 @@ export function withoutHiddenMeshes(root) {
|
|
|
394
401
|
});
|
|
395
402
|
if (!any)
|
|
396
403
|
return root;
|
|
397
|
-
|
|
398
|
-
const drop = [];
|
|
399
|
-
copy.traverse((node) => {
|
|
400
|
-
if (hidden(node))
|
|
401
|
-
drop.push(node);
|
|
402
|
-
});
|
|
403
|
-
for (const node of drop)
|
|
404
|
-
node.removeFromParent();
|
|
405
|
-
return copy;
|
|
404
|
+
return cloneReviewScene(root, { omitHidden: true });
|
|
406
405
|
}
|
|
407
406
|
/** Flat geometry renderer with exact orthographic/perspective projection and near/far clipping. */
|
|
408
407
|
export function rasterizeCamera(root, input, size = 384, backfaceCull = true, backdrop) {
|
|
@@ -415,6 +414,7 @@ export function rasterizeCamera(root, input, size = 384, backfaceCull = true, ba
|
|
|
415
414
|
for (let i = 0; i < size * size; i++)
|
|
416
415
|
out.set(bg, i * 3);
|
|
417
416
|
const depth = new Float64Array(size * size).fill(Infinity);
|
|
417
|
+
const weights = new Float64Array(3);
|
|
418
418
|
const clip = (points, plane, near) => {
|
|
419
419
|
const result = [];
|
|
420
420
|
for (let i = 0; i < points.length; i++) {
|
|
@@ -422,8 +422,13 @@ export function rasterizeCamera(root, input, size = 384, backfaceCull = true, ba
|
|
|
422
422
|
const insideA = near ? a.z >= plane : a.z <= plane, insideB = near ? b.z >= plane : b.z <= plane;
|
|
423
423
|
if (insideA)
|
|
424
424
|
result.push(a);
|
|
425
|
-
if (insideA !== insideB)
|
|
426
|
-
|
|
425
|
+
if (insideA !== insideB) {
|
|
426
|
+
// Shared intersections must round identically for either edge direction.
|
|
427
|
+
const low = a.z < b.z ? a : b, high = a.z < b.z ? b : a;
|
|
428
|
+
const intersection = low.clone().lerp(high, (plane - low.z) / (high.z - low.z));
|
|
429
|
+
intersection.z = plane;
|
|
430
|
+
result.push(intersection);
|
|
431
|
+
}
|
|
427
432
|
}
|
|
428
433
|
return result;
|
|
429
434
|
};
|
|
@@ -438,14 +443,14 @@ export function rasterizeCamera(root, input, size = 384, backfaceCull = true, ba
|
|
|
438
443
|
.clone()
|
|
439
444
|
.sub(world[0])
|
|
440
445
|
.cross(world[2].clone().sub(world[0]))
|
|
441
|
-
.normalize()
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
446
|
+
.normalize()
|
|
447
|
+
.multiplyScalar(tri.windingParity);
|
|
448
|
+
const normalSign = visibleNormalSign(normal.dot(camera.projection === 'perspective' ? position.clone().sub(world[0]) : z), tri.side, backfaceCull);
|
|
449
|
+
if (normalSign === 0)
|
|
445
450
|
continue;
|
|
446
451
|
if (tri.alpha <= 0)
|
|
447
452
|
continue;
|
|
448
|
-
const light = Math.min(1, 0.25 + 1.1 * Math.max(0, normal.dot(key)));
|
|
453
|
+
const light = Math.min(1, 0.25 + 1.1 * Math.max(0, normal.dot(key) * normalSign));
|
|
449
454
|
const color = tri.color.map((c) => Math.round(srgb(c * light) * 255));
|
|
450
455
|
let polygon = world.map((p) => {
|
|
451
456
|
const d = p.clone().sub(position);
|
|
@@ -464,17 +469,16 @@ export function rasterizeCamera(root, input, size = 384, backfaceCull = true, ba
|
|
|
464
469
|
];
|
|
465
470
|
});
|
|
466
471
|
const [a, b, c] = screen;
|
|
467
|
-
const
|
|
468
|
-
if (
|
|
472
|
+
const triangle = prepareTriangleCoverage(a[0], a[1], b[0], b[1], c[0], c[1], size);
|
|
473
|
+
if (!triangle)
|
|
469
474
|
continue;
|
|
470
|
-
for (let py =
|
|
471
|
-
for (let px =
|
|
472
|
-
|
|
473
|
-
if (w0 < 0 || w1 < 0 || w2 < 0)
|
|
475
|
+
for (let py = triangle.minY; py <= triangle.maxY; py++)
|
|
476
|
+
for (let px = triangle.minX; px <= triangle.maxX; px++) {
|
|
477
|
+
if (!triangle.sample(px + 0.5, py + 0.5, weights))
|
|
474
478
|
continue;
|
|
475
479
|
const d = camera.projection === 'perspective'
|
|
476
|
-
? 1 / (
|
|
477
|
-
:
|
|
480
|
+
? 1 / (weights[0] / p[0].z + weights[1] / p[1].z + weights[2] / p[2].z)
|
|
481
|
+
: weights[0] * p[0].z + weights[1] * p[1].z + weights[2] * p[2].z;
|
|
478
482
|
const index = py * size + px;
|
|
479
483
|
if (d >= depth[index])
|
|
480
484
|
continue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Standard material sides, kept numeric for cross-realm and flat GLB inputs. */
|
|
2
|
+
export type SurfaceSide = 0 | 1 | 2;
|
|
3
|
+
/** Match Three's affine front-face convention; a singular transform does not flip.
|
|
4
|
+
* Collapsed triangles are discarded by rasterization. No inverse is needed. */
|
|
5
|
+
export declare function worldWindingParity(m: ArrayLike<number>): 1 | -1;
|
|
6
|
+
/** Facing uses deformed triangle winding corrected for the mesh world transform.
|
|
7
|
+
* Zero means culled; +/-1 orients the visible flat lighting normal. Disabling
|
|
8
|
+
* culling is a diagnostic two-sided override, matching GPU review derivatives. */
|
|
9
|
+
export declare function visibleNormalSign(facing: number, side: SurfaceSide, cull: boolean): -1 | 0 | 1;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Match Three's affine front-face convention; a singular transform does not flip.
|
|
2
|
+
* Collapsed triangles are discarded by rasterization. No inverse is needed. */
|
|
3
|
+
export function worldWindingParity(m) {
|
|
4
|
+
const determinant = m[0] * (m[5] * m[10] - m[9] * m[6]) -
|
|
5
|
+
m[4] * (m[1] * m[10] - m[9] * m[2]) +
|
|
6
|
+
m[8] * (m[1] * m[6] - m[5] * m[2]);
|
|
7
|
+
return determinant < 0 ? -1 : 1;
|
|
8
|
+
}
|
|
9
|
+
/** Facing uses deformed triangle winding corrected for the mesh world transform.
|
|
10
|
+
* Zero means culled; +/-1 orients the visible flat lighting normal. Disabling
|
|
11
|
+
* culling is a diagnostic two-sided override, matching GPU review derivatives. */
|
|
12
|
+
export function visibleNormalSign(facing, side, cull) {
|
|
13
|
+
if (cull && ((side === 0 && facing <= 0) || (side === 1 && facing >= 0)))
|
|
14
|
+
return 0;
|
|
15
|
+
return facing < 0 ? -1 : 1;
|
|
16
|
+
}
|
package/lib/views/glb.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/** GLB-native input adapter for the deterministic CPU geometry-flat renderer. */
|
|
2
2
|
import { useCubicSpline } from "../animation-cubic.js";
|
|
3
|
+
import { gltfTextureBindingKey, restoreGltfTextureBinding } from "../gltf-texture-binding.js";
|
|
4
|
+
import { isCubicSplineTrack } from "../animation-spline.js";
|
|
3
5
|
import { createGltfIO } from "../gltf-io.js";
|
|
4
6
|
import { listLodChainNodes } from "../lod-export.js";
|
|
5
7
|
import { isHiddenGltfNode } from "../metrics.js";
|
|
6
8
|
import { registerReviewLodChains } from "./lod.js";
|
|
9
|
+
import { atlasDocumentTable, expandAtlasBinding, restoreAtlasTextureProvenance, validateAtlasDocumentProvenance, } from "../atlas-provenance.js";
|
|
7
10
|
import { Primitive, } from '@gltf-transform/core';
|
|
8
|
-
import { BufferAttribute, BufferGeometry, Group, Matrix4, Mesh, MeshStandardMaterial, NoColorSpace, SRGBColorSpace, Texture, AnimationClip, QuaternionKeyframeTrack, VectorKeyframeTrack, Quaternion, Vector3, InterpolateDiscrete, InterpolateLinear, } from 'three';
|
|
11
|
+
import { BufferAttribute, BufferGeometry, Group, Matrix4, Mesh, Bone, SkinnedMesh, Skeleton, MeshStandardMaterial, NoColorSpace, SRGBColorSpace, Texture, AnimationClip, QuaternionKeyframeTrack, NumberKeyframeTrack, PropertyBinding, VectorKeyframeTrack, Quaternion, Vector3, InterpolateDiscrete, InterpolateLinear, } from 'three';
|
|
9
12
|
export const GLB_GEOMETRY_FLAT_REASON = {
|
|
10
13
|
TEXTURE_SAMPLING_UNSUPPORTED: 'GLB_FLAT_TEXTURE_SAMPLING_UNSUPPORTED',
|
|
11
14
|
KTX2_SAMPLING_UNSUPPORTED: 'GLB_FLAT_KTX2_SAMPLING_UNSUPPORTED',
|
|
@@ -91,10 +94,18 @@ function reviewClipsFromExtras(extras) {
|
|
|
91
94
|
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', 'Invalid animation review track.');
|
|
92
95
|
}
|
|
93
96
|
const property = track.name.slice(track.name.lastIndexOf('.') + 1);
|
|
94
|
-
const
|
|
97
|
+
const morph = /^morphTargetInfluences(?:\[[^\]]+\])?$/.test(property);
|
|
98
|
+
const Track = morph
|
|
99
|
+
? NumberKeyframeTrack
|
|
100
|
+
: property === 'quaternion'
|
|
101
|
+
? QuaternionKeyframeTrack
|
|
102
|
+
: VectorKeyframeTrack;
|
|
95
103
|
if (track.interpolation === 'CUBICSPLINE') {
|
|
104
|
+
const tuple = morph ? track.values.length / track.times.length : cubicTuple(property);
|
|
96
105
|
if (track.times.length < 2 ||
|
|
97
|
-
track.values.length !== track.times.length *
|
|
106
|
+
track.values.length !== track.times.length * tuple ||
|
|
107
|
+
!Number.isInteger(tuple / 3) ||
|
|
108
|
+
tuple < 3) {
|
|
98
109
|
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', 'Invalid animation review track.');
|
|
99
110
|
}
|
|
100
111
|
return useCubicSpline(new Track(track.name, track.times, track.values));
|
|
@@ -182,7 +193,7 @@ function flatMaterial(material) {
|
|
|
182
193
|
?.getEmissiveStrength() ?? 1,
|
|
183
194
|
};
|
|
184
195
|
}
|
|
185
|
-
function preserveTexture(source, usage, cache) {
|
|
196
|
+
function preserveTexture(source, usage, cache, info, atlas) {
|
|
186
197
|
if (!source)
|
|
187
198
|
return null;
|
|
188
199
|
const encoded = source.getImage();
|
|
@@ -194,14 +205,21 @@ function preserveTexture(source, usage, cache) {
|
|
|
194
205
|
byUsage = new Map();
|
|
195
206
|
cache.set(source, byUsage);
|
|
196
207
|
}
|
|
197
|
-
const
|
|
208
|
+
const reference = info?.getExtras()['kilnAtlasReference'];
|
|
209
|
+
const cacheKey = `${usage}:${gltfTextureBindingKey(info)}`;
|
|
210
|
+
const existing = byUsage.get(cacheKey);
|
|
198
211
|
if (existing)
|
|
199
212
|
return existing;
|
|
200
213
|
const texture = new Texture();
|
|
201
214
|
texture.name = source.getName();
|
|
202
215
|
texture.colorSpace = usage === 'color' ? SRGBColorSpace : NoColorSpace;
|
|
203
216
|
texture.userData.encoded = { mime, bytes: Uint8Array.from(encoded) };
|
|
204
|
-
|
|
217
|
+
texture.flipY = false;
|
|
218
|
+
if (info)
|
|
219
|
+
restoreGltfTextureBinding(texture, info);
|
|
220
|
+
if (reference && info && atlas)
|
|
221
|
+
restoreAtlasTextureProvenance(texture, info, atlas);
|
|
222
|
+
byUsage.set(cacheKey, texture);
|
|
205
223
|
return texture;
|
|
206
224
|
}
|
|
207
225
|
function noteMaterialTextures(material, reasons) {
|
|
@@ -257,6 +275,7 @@ export async function loadGlbGeometryFlatScene(bytes) {
|
|
|
257
275
|
catch (error) {
|
|
258
276
|
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', `Could not parse final GLB bytes: ${error instanceof Error ? error.message : String(error)}`);
|
|
259
277
|
}
|
|
278
|
+
await validateAtlasDocumentProvenance(document);
|
|
260
279
|
const scene = document.getRoot().getDefaultScene() ?? document.getRoot().listScenes()[0];
|
|
261
280
|
if (!scene)
|
|
262
281
|
throw new GlbGeometryFlatError('GLB_FLAT_NO_SCENE', 'Final GLB has no scene.');
|
|
@@ -274,18 +293,24 @@ export async function loadGlbGeometryFlatScene(bytes) {
|
|
|
274
293
|
};
|
|
275
294
|
for (const child of scene.listChildren())
|
|
276
295
|
visit(child);
|
|
296
|
+
// Skinning needs the joint hierarchy and inverse bind matrices. Keep the
|
|
297
|
+
// historical flat adapter for rigid scenes, but share the review loader for
|
|
298
|
+
// a posed derivative instead of silently rasterizing its bind-pose positions.
|
|
299
|
+
if ([...sceneNodes].some((node) => node.getSkin() ||
|
|
300
|
+
node
|
|
301
|
+
.getMesh()
|
|
302
|
+
?.listPrimitives()
|
|
303
|
+
.some((p) => p.listTargets().length))) {
|
|
304
|
+
await validateAtlasDocumentProvenance(document);
|
|
305
|
+
return reviewSceneFromDocument(document);
|
|
306
|
+
}
|
|
277
307
|
for (const node of sceneNodes) {
|
|
278
308
|
const mesh = node.getMesh();
|
|
279
309
|
if (!mesh)
|
|
280
310
|
continue;
|
|
281
|
-
if (node.getSkin())
|
|
282
|
-
reasons.add(GLB_GEOMETRY_FLAT_REASON.SKIN_DEFORMATION_UNSUPPORTED);
|
|
283
311
|
const instances = instanceMatrices(node);
|
|
284
312
|
instanceCount += instances.count;
|
|
285
313
|
for (const primitive of mesh.listPrimitives()) {
|
|
286
|
-
if (primitive.listTargets().length > 0) {
|
|
287
|
-
reasons.add(GLB_GEOMETRY_FLAT_REASON.MORPH_TARGETS_UNSUPPORTED);
|
|
288
|
-
}
|
|
289
314
|
const positionAccessor = primitive.getAttribute('POSITION');
|
|
290
315
|
if (positionAccessor?.getElementSize() !== 3)
|
|
291
316
|
continue;
|
|
@@ -370,6 +395,10 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
370
395
|
catch {
|
|
371
396
|
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', 'Could not parse final GLB bytes.');
|
|
372
397
|
}
|
|
398
|
+
await validateAtlasDocumentProvenance(document);
|
|
399
|
+
return reviewSceneFromDocument(document);
|
|
400
|
+
}
|
|
401
|
+
function reviewSceneFromDocument(document) {
|
|
373
402
|
const sourceScene = document.getRoot().getDefaultScene() ?? document.getRoot().listScenes()[0];
|
|
374
403
|
if (!sourceScene)
|
|
375
404
|
throw new GlbGeometryFlatError('GLB_FLAT_NO_SCENE', 'Final GLB has no scene.');
|
|
@@ -377,14 +406,35 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
377
406
|
const root = new Group();
|
|
378
407
|
root.name = sourceScene.getName() || 'Scene';
|
|
379
408
|
const nodeMap = new Map();
|
|
409
|
+
const atlas = atlasDocumentTable(document);
|
|
380
410
|
const textureCache = new Map();
|
|
411
|
+
const morphMeshes = new Map();
|
|
412
|
+
// Morph primitives need unique, binding-safe names: the historical ':primitive'
|
|
413
|
+
// suffix is a namespace separator to Three.PropertyBinding. Parent names stay intact.
|
|
414
|
+
const usedNames = new Set(document
|
|
415
|
+
.getRoot()
|
|
416
|
+
.listNodes()
|
|
417
|
+
.map((node) => node.getName()));
|
|
418
|
+
const nodeIds = new Map(document
|
|
419
|
+
.getRoot()
|
|
420
|
+
.listNodes()
|
|
421
|
+
.map((node, index) => [node, index]));
|
|
422
|
+
let morphValues = 0;
|
|
423
|
+
const morphError = (detail) => {
|
|
424
|
+
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', `Invalid morph review data: ${detail}`);
|
|
425
|
+
};
|
|
426
|
+
const joints = new Set(document
|
|
427
|
+
.getRoot()
|
|
428
|
+
.listSkins()
|
|
429
|
+
.flatMap((skin) => skin.listJoints()));
|
|
430
|
+
const pendingSkins = [];
|
|
381
431
|
let meshCount = 0;
|
|
382
432
|
let instanceCount = 0;
|
|
383
433
|
const buildNode = (source) => {
|
|
384
434
|
const existing = nodeMap.get(source);
|
|
385
435
|
if (existing)
|
|
386
436
|
return existing;
|
|
387
|
-
const target = new Group();
|
|
437
|
+
const target = joints.has(source) ? new Bone() : new Group();
|
|
388
438
|
target.name = source.getName();
|
|
389
439
|
target.position.fromArray(source.getTranslation());
|
|
390
440
|
target.quaternion.fromArray(source.getRotation());
|
|
@@ -396,14 +446,27 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
396
446
|
nodeMap.set(source, target);
|
|
397
447
|
const sourceMesh = source.getMesh();
|
|
398
448
|
if (sourceMesh) {
|
|
449
|
+
const targetCount = sourceMesh.listPrimitives()[0]?.listTargets().length ?? 0;
|
|
450
|
+
if (sourceMesh.listPrimitives().some((p) => p.listTargets().length !== targetCount))
|
|
451
|
+
morphError('primitives must have equal target counts.');
|
|
452
|
+
const nodeWeights = source.getWeights(), meshWeights = sourceMesh.getWeights();
|
|
453
|
+
for (const weights of [nodeWeights, meshWeights])
|
|
454
|
+
if (weights.length && (weights.length !== targetCount || !weights.every(Number.isFinite)))
|
|
455
|
+
morphError('default weight count or values.');
|
|
456
|
+
const weights = nodeWeights.length
|
|
457
|
+
? nodeWeights
|
|
458
|
+
: meshWeights.length
|
|
459
|
+
? meshWeights
|
|
460
|
+
: Array(targetCount).fill(0);
|
|
461
|
+
// glTF Transform decodes extras.targetNames into PrimitiveTarget names.
|
|
462
|
+
const targetNames = sourceMesh
|
|
463
|
+
.listPrimitives()[0]
|
|
464
|
+
?.listTargets()
|
|
465
|
+
.map((target, index) => target.getName() || String(index)) ?? [];
|
|
399
466
|
target.userData.kilnGlbMeshNode = true;
|
|
400
|
-
if (source.getSkin())
|
|
401
|
-
reasons.add(GLB_GEOMETRY_FLAT_REASON.SKIN_DEFORMATION_UNSUPPORTED);
|
|
402
467
|
const matrices = localInstanceMatrices(source);
|
|
403
468
|
instanceCount += matrices.length;
|
|
404
469
|
for (const [primitiveIndex, primitive] of sourceMesh.listPrimitives().entries()) {
|
|
405
|
-
if (primitive.listTargets().length)
|
|
406
|
-
reasons.add(GLB_GEOMETRY_FLAT_REASON.MORPH_TARGETS_UNSUPPORTED);
|
|
407
470
|
const position = primitive.getAttribute('POSITION');
|
|
408
471
|
if (position?.getElementSize() !== 3)
|
|
409
472
|
continue;
|
|
@@ -416,6 +479,9 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
416
479
|
noteMaterialTextures(material, reasons);
|
|
417
480
|
const flat = flatMaterial(material);
|
|
418
481
|
const geometry = new BufferGeometry();
|
|
482
|
+
const atlasBinding = primitive.getExtras()['kilnAtlasBinding'];
|
|
483
|
+
if (atlasBinding !== undefined && atlas)
|
|
484
|
+
geometry.userData.kilnAtlasBinding = expandAtlasBinding(atlasBinding, atlas);
|
|
419
485
|
geometry.setAttribute('position', new BufferAttribute(Float32Array.from(decodedAccessor(position)), 3));
|
|
420
486
|
const normal = primitive.getAttribute('NORMAL');
|
|
421
487
|
if (normal) {
|
|
@@ -430,6 +496,59 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
430
496
|
geometry.setAttribute('tangent', new BufferAttribute(Float32Array.from(decodedAccessor(tangent)), 4));
|
|
431
497
|
}
|
|
432
498
|
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
499
|
+
if (targetCount) {
|
|
500
|
+
const targets = primitive.listTargets();
|
|
501
|
+
for (const target of targets)
|
|
502
|
+
for (const semantic of target.listSemantics()) {
|
|
503
|
+
const attribute = target.getAttribute(semantic);
|
|
504
|
+
if (!primitive.getAttribute(semantic) ||
|
|
505
|
+
attribute.getCount() !== position.getCount() ||
|
|
506
|
+
((semantic === 'POSITION' || semantic === 'NORMAL' || semantic === 'TANGENT') &&
|
|
507
|
+
attribute.getElementSize() !== 3))
|
|
508
|
+
morphError(`target attribute ${semantic} does not match the base primitive.`);
|
|
509
|
+
if (semantic !== 'POSITION' && semantic !== 'NORMAL')
|
|
510
|
+
reasons.add(GLB_GEOMETRY_FLAT_REASON.MORPH_TARGETS_UNSUPPORTED);
|
|
511
|
+
}
|
|
512
|
+
const semantics = targets.some((target) => target.getAttribute('NORMAL'))
|
|
513
|
+
? ['POSITION', 'NORMAL']
|
|
514
|
+
: ['POSITION'];
|
|
515
|
+
// Bound zero-filled missing attributes as well as decoded ones: a tiny
|
|
516
|
+
// file with many omitted deltas must not expand without limit.
|
|
517
|
+
morphValues += targetCount * position.getCount() * 3 * semantics.length;
|
|
518
|
+
if (morphValues > 16_777_216)
|
|
519
|
+
morphError('expanded morph attributes exceed the 64 MiB review budget.');
|
|
520
|
+
geometry.morphTargetsRelative = true;
|
|
521
|
+
for (const semantic of semantics) {
|
|
522
|
+
const attributes = targets.map((target) => {
|
|
523
|
+
const attribute = target.getAttribute(semantic);
|
|
524
|
+
const values = attribute
|
|
525
|
+
? Float32Array.from(decodedAccessor(attribute))
|
|
526
|
+
: new Float32Array(position.getCount() * 3);
|
|
527
|
+
if (!values.every(Number.isFinite))
|
|
528
|
+
morphError(`nonfinite ${semantic} displacement.`);
|
|
529
|
+
return new BufferAttribute(values, 3);
|
|
530
|
+
});
|
|
531
|
+
if (semantic === 'POSITION')
|
|
532
|
+
geometry.morphAttributes.position = attributes;
|
|
533
|
+
else
|
|
534
|
+
geometry.morphAttributes.normal = attributes;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (source.getSkin()) {
|
|
538
|
+
const skinIndices = primitive.getAttribute('JOINTS_0');
|
|
539
|
+
const skinWeights = primitive.getAttribute('WEIGHTS_0');
|
|
540
|
+
if (!skinIndices ||
|
|
541
|
+
!skinWeights ||
|
|
542
|
+
skinIndices.getElementSize() !== 4 ||
|
|
543
|
+
skinWeights.getElementSize() !== 4 ||
|
|
544
|
+
skinIndices.getCount() !== position.getCount() ||
|
|
545
|
+
skinWeights.getCount() !== position.getCount())
|
|
546
|
+
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', `Invalid skin attributes on ${source.getName()}.`);
|
|
547
|
+
if (primitive.getAttribute('JOINTS_1') || primitive.getAttribute('WEIGHTS_1'))
|
|
548
|
+
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', `Skin review supports four influences per vertex; ${source.getName()} has additional joint/weight sets.`);
|
|
549
|
+
geometry.setAttribute('skinIndex', new BufferAttribute(Uint16Array.from(decodedAccessor(skinIndices)), 4));
|
|
550
|
+
geometry.setAttribute('skinWeight', new BufferAttribute(Float32Array.from(decodedAccessor(skinWeights)), 4));
|
|
551
|
+
}
|
|
433
552
|
const threeMaterial = new MeshStandardMaterial({
|
|
434
553
|
opacity: flat.opacity,
|
|
435
554
|
transparent: flat.alphaMode === 'BLEND',
|
|
@@ -443,22 +562,41 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
443
562
|
threeMaterial.emissiveIntensity = flat.emissiveIntensity;
|
|
444
563
|
if (material) {
|
|
445
564
|
threeMaterial.userData = { ...material.getExtras() };
|
|
446
|
-
threeMaterial.map = preserveTexture(material.getBaseColorTexture(), 'color', textureCache);
|
|
447
|
-
threeMaterial.normalMap = preserveTexture(material.getNormalTexture(), 'data', textureCache);
|
|
565
|
+
threeMaterial.map = preserveTexture(material.getBaseColorTexture(), 'color', textureCache, material.getBaseColorTextureInfo(), atlas);
|
|
566
|
+
threeMaterial.normalMap = preserveTexture(material.getNormalTexture(), 'data', textureCache, material.getNormalTextureInfo(), atlas);
|
|
448
567
|
threeMaterial.normalScale.setScalar(material.getNormalScale());
|
|
449
|
-
const metallicRoughness = preserveTexture(material.getMetallicRoughnessTexture(), 'data', textureCache);
|
|
568
|
+
const metallicRoughness = preserveTexture(material.getMetallicRoughnessTexture(), 'data', textureCache, material.getMetallicRoughnessTextureInfo(), atlas);
|
|
450
569
|
threeMaterial.metalnessMap = metallicRoughness;
|
|
451
570
|
threeMaterial.roughnessMap = metallicRoughness;
|
|
452
|
-
threeMaterial.emissiveMap = preserveTexture(material.getEmissiveTexture(), 'color', textureCache);
|
|
453
|
-
threeMaterial.aoMap = preserveTexture(material.getOcclusionTexture(), 'data', textureCache);
|
|
571
|
+
threeMaterial.emissiveMap = preserveTexture(material.getEmissiveTexture(), 'color', textureCache, material.getEmissiveTextureInfo(), atlas);
|
|
572
|
+
threeMaterial.aoMap = preserveTexture(material.getOcclusionTexture(), 'data', textureCache, material.getOcclusionTextureInfo(), atlas);
|
|
454
573
|
threeMaterial.aoMapIntensity = material.getOcclusionStrength();
|
|
455
574
|
}
|
|
456
575
|
for (const [index, matrix] of matrices.entries()) {
|
|
457
|
-
const mesh =
|
|
576
|
+
const mesh = source.getSkin()
|
|
577
|
+
? new SkinnedMesh(geometry, threeMaterial)
|
|
578
|
+
: new Mesh(geometry, threeMaterial);
|
|
579
|
+
if (mesh instanceof SkinnedMesh)
|
|
580
|
+
pendingSkins.push({ source, mesh });
|
|
458
581
|
// A deduplicated mesh resource can serve many distinctly named nodes.
|
|
459
582
|
// QA repair hints must identify this owning node, not the shared resource.
|
|
460
583
|
const baseName = source.getName() || sourceMesh.getName() || 'Mesh';
|
|
461
584
|
mesh.name = `${baseName}:primitive-${primitiveIndex}${matrices.length === 1 ? '' : `:instance-${index}`}`;
|
|
585
|
+
if (targetCount) {
|
|
586
|
+
let bindingName = `${PropertyBinding.sanitizeNodeName(baseName)}_morph_${nodeIds.get(source)}_${primitiveIndex}_${index}`;
|
|
587
|
+
while (usedNames.has(bindingName))
|
|
588
|
+
bindingName += '_';
|
|
589
|
+
usedNames.add(bindingName);
|
|
590
|
+
mesh.name = bindingName;
|
|
591
|
+
mesh.morphTargetInfluences = [...weights];
|
|
592
|
+
if (Array.isArray(targetNames) &&
|
|
593
|
+
targetNames.length === targetCount &&
|
|
594
|
+
targetNames.every((name) => typeof name === 'string'))
|
|
595
|
+
mesh.morphTargetDictionary = Object.fromEntries(targetNames.map((name, index) => [name, index]));
|
|
596
|
+
const siblings = morphMeshes.get(source) ?? [];
|
|
597
|
+
siblings.push(mesh);
|
|
598
|
+
morphMeshes.set(source, siblings);
|
|
599
|
+
}
|
|
462
600
|
mesh.matrixAutoUpdate = false;
|
|
463
601
|
mesh.matrix.copy(matrix);
|
|
464
602
|
target.add(mesh);
|
|
@@ -484,6 +622,28 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
484
622
|
base: nodeMap.get(base),
|
|
485
623
|
levels: lower.some((level) => nodeMap.has(level)) ? [] : lower.map(buildNode),
|
|
486
624
|
})));
|
|
625
|
+
for (const { source, mesh } of pendingSkins) {
|
|
626
|
+
const skin = source.getSkin();
|
|
627
|
+
const bones = skin.listJoints().map((joint) => nodeMap.get(joint));
|
|
628
|
+
const inverse = skin.getInverseBindMatrices();
|
|
629
|
+
if (!bones.length ||
|
|
630
|
+
bones.some((bone) => !(bone instanceof Bone)) ||
|
|
631
|
+
(inverse && (inverse.getElementSize() !== 16 || inverse.getCount() < bones.length)))
|
|
632
|
+
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', `Invalid or unavailable skin joints/bind matrices on ${source.getName()}.`);
|
|
633
|
+
const indices = mesh.geometry.getAttribute('skinIndex');
|
|
634
|
+
const weights = mesh.geometry.getAttribute('skinWeight');
|
|
635
|
+
for (let i = 0; i < indices.array.length; i++)
|
|
636
|
+
if (indices.array[i] >= bones.length ||
|
|
637
|
+
!Number.isFinite(weights.array[i]) ||
|
|
638
|
+
weights.array[i] < 0)
|
|
639
|
+
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', `Invalid skin joint index or weight on ${source.getName()}.`);
|
|
640
|
+
const matrices = bones.map((_, index) => new Matrix4().fromArray(inverse ? inverse.getElement(index, []) : new Matrix4().elements));
|
|
641
|
+
// glTF's inverse bind already maps mesh-local positions into joint space.
|
|
642
|
+
// Identity bindMatrix and AttachedBindMode cancel the mesh world transform,
|
|
643
|
+
// matching GLTFLoader even under translated/scaled mesh ancestors.
|
|
644
|
+
mesh.bind(new Skeleton(bones, matrices), new Matrix4());
|
|
645
|
+
}
|
|
646
|
+
root.updateMatrixWorld(true);
|
|
487
647
|
const nativeClips = document
|
|
488
648
|
.getRoot()
|
|
489
649
|
.listAnimations()
|
|
@@ -494,7 +654,7 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
494
654
|
const input = sampler?.getInput()?.getArray();
|
|
495
655
|
const output = sampler?.getOutput()?.getArray();
|
|
496
656
|
const property = path === 'translation' ? 'position' : path === 'rotation' ? 'quaternion' : path;
|
|
497
|
-
if (!node || !property ||
|
|
657
|
+
if (!node || !property || !input || !output)
|
|
498
658
|
return [];
|
|
499
659
|
const interpolation = sampler.getInterpolation();
|
|
500
660
|
if (interpolation !== 'STEP' &&
|
|
@@ -502,6 +662,25 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
502
662
|
interpolation !== 'CUBICSPLINE') {
|
|
503
663
|
throw new GlbGeometryFlatError('GLB_FLAT_PARSE_FAILED', `Animation review does not support ${interpolation} interpolation; use LINEAR, STEP or CUBICSPLINE.`);
|
|
504
664
|
}
|
|
665
|
+
if (property === 'weights') {
|
|
666
|
+
const targets = morphMeshes.get(node);
|
|
667
|
+
if (!targets?.length)
|
|
668
|
+
return [];
|
|
669
|
+
const width = targets[0].morphTargetInfluences.length;
|
|
670
|
+
const values = decodedAccessor(sampler.getOutput());
|
|
671
|
+
if (input.length < 1 ||
|
|
672
|
+
input.length > REVIEW_CLIP_LIMITS.samplesPerTrack ||
|
|
673
|
+
values.length > REVIEW_CLIP_LIMITS.valuesPerTrack ||
|
|
674
|
+
values.length !==
|
|
675
|
+
input.length * width * (interpolation === 'CUBICSPLINE' ? 3 : 1) ||
|
|
676
|
+
!Array.from(input).every((v, i) => Number.isFinite(v) && v >= 0 && (i === 0 || v > input[i - 1])) ||
|
|
677
|
+
!Array.from(values).every(Number.isFinite))
|
|
678
|
+
morphError('animation weight sampler width or values.');
|
|
679
|
+
return targets.map((mesh) => {
|
|
680
|
+
const track = new NumberKeyframeTrack(`${mesh.name}.morphTargetInfluences`, input, values, interpolation === 'STEP' ? InterpolateDiscrete : InterpolateLinear);
|
|
681
|
+
return interpolation === 'CUBICSPLINE' ? useCubicSpline(track) : track;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
505
684
|
const Track = path === 'rotation' ? QuaternionKeyframeTrack : VectorKeyframeTrack;
|
|
506
685
|
if (interpolation === 'CUBICSPLINE') {
|
|
507
686
|
if (input.length < 2 || output.length !== input.length * cubicTuple(property)) {
|
|
@@ -513,7 +692,22 @@ export async function loadGlbReviewScene(bytes) {
|
|
|
513
692
|
new Track(`${node.getName()}.${property}`, input, output, interpolation === 'STEP' ? InterpolateDiscrete : InterpolateLinear),
|
|
514
693
|
];
|
|
515
694
|
})), animation.getExtras().kilnLoopIntent));
|
|
516
|
-
const
|
|
695
|
+
const extrasClips = reviewClipsFromExtras(sourceScene.getExtras());
|
|
696
|
+
if (extrasClips)
|
|
697
|
+
for (const clip of extrasClips)
|
|
698
|
+
clip.tracks = clip.tracks.flatMap((track) => {
|
|
699
|
+
const dot = track.name.lastIndexOf('.'), property = track.name.slice(dot + 1);
|
|
700
|
+
if (!/^morphTargetInfluences(?:\[[^\]]+\])?$/.test(property))
|
|
701
|
+
return [track];
|
|
702
|
+
const sources = [...morphMeshes.keys()].filter((node) => node.getName() === track.name.slice(0, dot));
|
|
703
|
+
if (sources.length !== 1)
|
|
704
|
+
return [track]; // Preserve unresolved/ambiguous author evidence.
|
|
705
|
+
return morphMeshes.get(sources[0]).map((mesh) => {
|
|
706
|
+
const copy = new NumberKeyframeTrack(`${mesh.name}.${property}`, track.times, track.values, track.getInterpolation());
|
|
707
|
+
return isCubicSplineTrack(track) ? useCubicSpline(copy) : copy;
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
const clips = extrasClips ?? nativeClips;
|
|
517
711
|
return {
|
|
518
712
|
root,
|
|
519
713
|
clips,
|
package/lib/views/pose.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Minimal duck-typed view of a Three.js node we pose. */
|
|
2
2
|
interface DuckNode {
|
|
3
3
|
name?: string;
|
|
4
|
+
uuid?: string;
|
|
5
|
+
morphTargetInfluences?: number[];
|
|
6
|
+
morphTargetDictionary?: Record<string, number>;
|
|
4
7
|
position?: {
|
|
5
8
|
set(x: number, y: number, z: number): void;
|
|
6
9
|
};
|
|
@@ -31,7 +34,7 @@ export interface DuckClip {
|
|
|
31
34
|
kilnLoopIntent?: unknown;
|
|
32
35
|
};
|
|
33
36
|
}
|
|
34
|
-
type Prop = 'position' | 'quaternion' | 'scale';
|
|
37
|
+
type Prop = 'position' | 'quaternion' | 'scale' | 'morphTargetInfluences';
|
|
35
38
|
interface PreparedTrack {
|
|
36
39
|
nodeName: string;
|
|
37
40
|
prop: Prop;
|
|
@@ -40,6 +43,9 @@ interface PreparedTrack {
|
|
|
40
43
|
/** One value of `stride` per key; a CUBICSPLINE key is [in-tangent, value, out-tangent]. */
|
|
41
44
|
values: number[];
|
|
42
45
|
interpolation: 'LINEAR' | 'STEP' | 'CUBICSPLINE';
|
|
46
|
+
/** Original spelling retained for unresolved-track evidence. */
|
|
47
|
+
sourceName?: string;
|
|
48
|
+
morphIndex?: number;
|
|
43
49
|
}
|
|
44
50
|
/** A clip prepared for repeated time-sampling: parsed tracks + a derived duration. */
|
|
45
51
|
export interface PreparedClip {
|