@forgeax/engine-gltf 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +204 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts +2 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-test-helpers.d.ts +5 -0
- package/dist/__tests__/bridge-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/bridge.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge.unit.test.d.ts.map +1 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts +2 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts.map +1 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts +2 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts +2 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-importer.test.d.ts +2 -0
- package/dist/__tests__/gltf-importer.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts +2 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts +2 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts +2 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts +2 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts +2 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.integration.test.d.ts +2 -0
- package/dist/__tests__/morph-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.unit.test.d.ts +2 -0
- package/dist/__tests__/morph-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key.unit.test.d.ts +2 -0
- package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
- package/dist/accessor/decode-accessor.d.ts +75 -0
- package/dist/accessor/decode-accessor.d.ts.map +1 -0
- package/dist/accessor/decode-color.d.ts +15 -0
- package/dist/accessor/decode-color.d.ts.map +1 -0
- package/dist/accessor/index.d.ts +3 -0
- package/dist/accessor/index.d.ts.map +1 -0
- package/dist/bridge.d.ts +103 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/check-extensions.d.ts +29 -0
- package/dist/check-extensions.d.ts.map +1 -0
- package/dist/cli-gltf.d.ts +8 -0
- package/dist/cli-gltf.d.ts.map +1 -0
- package/dist/cli-gltf.mjs +2435 -0
- package/dist/cli-gltf.mjs.map +1 -0
- package/dist/data-uri.d.ts +6 -0
- package/dist/data-uri.d.ts.map +1 -0
- package/dist/errors.d.ts +201 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/extract-image-bytes.d.ts +42 -0
- package/dist/extract-image-bytes.d.ts.map +1 -0
- package/dist/gltf-importer.d.ts +21 -0
- package/dist/gltf-importer.d.ts.map +1 -0
- package/dist/image-color-space.d.ts +35 -0
- package/dist/image-color-space.d.ts.map +1 -0
- package/dist/importer-entry.d.ts +10 -0
- package/dist/importer-entry.d.ts.map +1 -0
- package/dist/importer-entry.mjs +3378 -0
- package/dist/importer-entry.mjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3673 -0
- package/dist/index.mjs.map +1 -0
- package/dist/meshopt-decode.d.ts +39 -0
- package/dist/meshopt-decode.d.ts.map +1 -0
- package/dist/node-file-entry.d.ts +14 -0
- package/dist/node-file-entry.d.ts.map +1 -0
- package/dist/node-file-entry.mjs +1946 -0
- package/dist/node-file-entry.mjs.map +1 -0
- package/dist/node-path.d.ts +14 -0
- package/dist/node-path.d.ts.map +1 -0
- package/dist/parse-animation.d.ts +70 -0
- package/dist/parse-animation.d.ts.map +1 -0
- package/dist/parse-glb-chunks.d.ts +25 -0
- package/dist/parse-glb-chunks.d.ts.map +1 -0
- package/dist/parse-glb.d.ts +3 -0
- package/dist/parse-glb.d.ts.map +1 -0
- package/dist/parse-gltf-header.d.ts +18 -0
- package/dist/parse-gltf-header.d.ts.map +1 -0
- package/dist/parse-gltf.d.ts +263 -0
- package/dist/parse-gltf.d.ts.map +1 -0
- package/dist/parse-skin.d.ts +70 -0
- package/dist/parse-skin.d.ts.map +1 -0
- package/dist/reimport-reuse-meta.d.ts +48 -0
- package/dist/reimport-reuse-meta.d.ts.map +1 -0
- package/dist/serialize-meta.d.ts +2 -0
- package/dist/serialize-meta.d.ts.map +1 -0
- package/dist/source-key.d.ts +44 -0
- package/dist/source-key.d.ts.map +1 -0
- package/dist/sub-asset-key.d.ts +12 -0
- package/dist/sub-asset-key.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +2 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/dist/transform.d.ts +37 -0
- package/dist/transform.d.ts.map +1 -0
- package/package.json +92 -0
- package/src/__tests__/animation-target-errors.unit.test.ts +51 -0
- package/src/__tests__/bridge-material-samplers.unit.test.ts +67 -0
- package/src/__tests__/bridge-material-values.unit.test.ts +106 -0
- package/src/__tests__/bridge-mixed-color.unit.test.ts +100 -0
- package/src/__tests__/bridge-sampler-required.test-d.ts +59 -0
- package/src/__tests__/bridge-skin-shader-route.unit.test.ts +42 -0
- package/src/__tests__/bridge-skin-stride.unit.test.ts +115 -0
- package/src/__tests__/bridge-test-helpers.ts +9 -0
- package/src/__tests__/bridge.unit.test.ts +1227 -0
- package/src/__tests__/cli-gltf.integration.test.ts +122 -0
- package/src/__tests__/color-0-accessor.unit.test.ts +174 -0
- package/src/__tests__/data-uri-surface.unit.test.ts +26 -0
- package/src/__tests__/extension-admission.unit.test.ts +19 -0
- package/src/__tests__/fixtures/color-0/float-vec3.gltf +13 -0
- package/src/__tests__/fixtures/color-0/interleaved-vec3.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ubyte.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ushort.gltf +6 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +152 -0
- package/src/__tests__/gltf-error-migration.test.ts +147 -0
- package/src/__tests__/gltf-error-policy-owner.unit.test.ts +185 -0
- package/src/__tests__/gltf-importer.test.ts +12 -0
- package/src/__tests__/gltf-local-artifacts.test.ts +11 -0
- package/src/__tests__/gltf.unit.test.ts +2975 -0
- package/src/__tests__/malformed-buffer-data-uri.integration.test.ts +140 -0
- package/src/__tests__/material-missing-uv.unit.test.ts +29 -0
- package/src/__tests__/material-pack-refs.integration.test.ts +58 -0
- package/src/__tests__/material-per-slot-ir.unit.test.ts +90 -0
- package/src/__tests__/material-texture-transform.unit.test.ts +105 -0
- package/src/__tests__/material-unused-uv.unit.test.ts +44 -0
- package/src/__tests__/meshopt-decode.unit.test.ts +87 -0
- package/src/__tests__/meshopt-falsifier.unit.test.ts +92 -0
- package/src/__tests__/meshopt-mode-filter-owner.test-d.ts +79 -0
- package/src/__tests__/meshopt-real-matrix.integration.test.ts +370 -0
- package/src/__tests__/morph-import.integration.test.ts +115 -0
- package/src/__tests__/morph-import.unit.test.ts +62 -0
- package/src/__tests__/parse-decode.unit.test.ts +919 -0
- package/src/__tests__/parse-gltf-multi-uv.test.ts +238 -0
- package/src/__tests__/parse-gltf-skin-attrs.unit.test.ts +191 -0
- package/src/__tests__/parse-gltf.unit.test.ts +133 -0
- package/src/__tests__/pick-e2e.integration.test.ts +238 -0
- package/src/__tests__/public-surface.unit.test.ts +10 -0
- package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
- package/src/__tests__/source-key-producer.integration.test.ts +171 -0
- package/src/__tests__/source-key.unit.test.ts +159 -0
- package/src/accessor/decode-accessor.ts +309 -0
- package/src/accessor/decode-color.ts +128 -0
- package/src/accessor/index.ts +15 -0
- package/src/bridge.ts +928 -0
- package/src/check-extensions.ts +85 -0
- package/src/cli-gltf.ts +339 -0
- package/src/data-uri.ts +29 -0
- package/src/errors.ts +386 -0
- package/src/extract-image-bytes.ts +343 -0
- package/src/gltf-importer.ts +976 -0
- package/src/image-color-space.ts +104 -0
- package/src/importer-entry.ts +24 -0
- package/src/index.ts +148 -0
- package/src/meshopt-decode.ts +200 -0
- package/src/node-file-entry.ts +98 -0
- package/src/node-path.ts +41 -0
- package/src/parse-animation.ts +286 -0
- package/src/parse-glb-chunks.ts +167 -0
- package/src/parse-glb.ts +16 -0
- package/src/parse-gltf-header.ts +58 -0
- package/src/parse-gltf.ts +1731 -0
- package/src/parse-skin.ts +238 -0
- package/src/reimport-reuse-meta.ts +134 -0
- package/src/serialize-meta.ts +24 -0
- package/src/source-key.ts +106 -0
- package/src/sub-asset-key.ts +61 -0
- package/src/to-asset-pack.ts +8 -0
- package/src/transform.ts +117 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// parse-animation.ts - glTF animation parser (feat-20260523-skin-skeleton-animation M0).
|
|
2
|
+
//
|
|
3
|
+
// Implements parseAnimation(): parses glTF `animations[]` array into
|
|
4
|
+
// GltfAnimationClipRecord[] with duration, channels, and sampler data.
|
|
5
|
+
// Used by parseGltfWithBin to populate GltfDoc.
|
|
6
|
+
//
|
|
7
|
+
// Decision anchors:
|
|
8
|
+
// - plan-strategy D-1 (AnimationClip independent asset)
|
|
9
|
+
// - requirements AC-07 (AnimationClip shape), AC-09 (CUBICSPLINE/morph fail-fast)
|
|
10
|
+
// - requirements AC-10 (IR extension)
|
|
11
|
+
// - charter P3 (fail-fast on unsupported interpolation/morph targets)
|
|
12
|
+
|
|
13
|
+
import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
|
|
14
|
+
import type { AnimationTargetIdValue } from '@forgeax/engine-types';
|
|
15
|
+
import { decodeF32Accessor } from './accessor/decode-accessor.js';
|
|
16
|
+
import { err, type GltfError, gltfErr, ok, type Result } from './errors.js';
|
|
17
|
+
import { buildNodeParentMap, resolveNamedNodePath } from './node-path.js';
|
|
18
|
+
|
|
19
|
+
/** Supported interpolation modes (CUBICSPLINE is deferred to OOS-skin-cubicspline). */
|
|
20
|
+
type Interpolation = 'LINEAR' | 'STEP';
|
|
21
|
+
|
|
22
|
+
interface ChannelJson {
|
|
23
|
+
readonly sampler: number;
|
|
24
|
+
readonly target: {
|
|
25
|
+
readonly node?: number;
|
|
26
|
+
readonly path: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SamplerJson {
|
|
31
|
+
readonly input: number;
|
|
32
|
+
readonly output: number;
|
|
33
|
+
readonly interpolation?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface AnimationJson {
|
|
37
|
+
readonly name?: string;
|
|
38
|
+
readonly channels: readonly ChannelJson[];
|
|
39
|
+
readonly samplers: readonly SamplerJson[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ANIMATION_ACCESSOR_TYPES = ['SCALAR', 'VEC2', 'VEC3', 'VEC4'] as const;
|
|
43
|
+
|
|
44
|
+
interface AccessorJson {
|
|
45
|
+
readonly bufferView?: number;
|
|
46
|
+
readonly componentType: number;
|
|
47
|
+
readonly type: string;
|
|
48
|
+
readonly count: number;
|
|
49
|
+
readonly byteOffset?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface BufferViewJson {
|
|
53
|
+
readonly buffer: number;
|
|
54
|
+
readonly byteOffset?: number;
|
|
55
|
+
readonly byteLength: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface GltfAnimationChannelRecord {
|
|
59
|
+
readonly targetId: AnimationTargetIdValue;
|
|
60
|
+
readonly targetNodeIndex: number;
|
|
61
|
+
/** 'translation' | 'rotation' | 'scale' | 'weights'. */
|
|
62
|
+
readonly property: 'translation' | 'rotation' | 'scale' | 'weights';
|
|
63
|
+
/** Sampler driving this channel. */
|
|
64
|
+
readonly sampler: GltfAnimationSamplerRecord;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface GltfAnimationSamplerRecord {
|
|
68
|
+
/** Keyframe timestamp array (ascending). */
|
|
69
|
+
readonly input: Float32Array;
|
|
70
|
+
/** Keyframe value array per element size. */
|
|
71
|
+
readonly output: Float32Array;
|
|
72
|
+
/** 'LINEAR' | 'STEP'. */
|
|
73
|
+
readonly interpolation: Interpolation;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface GltfAnimationClipRecord {
|
|
77
|
+
/** Producer-authored animation name, when the source provides one. */
|
|
78
|
+
readonly name?: string;
|
|
79
|
+
/** Clip duration = max(sampler.input[last]). */
|
|
80
|
+
readonly duration: number;
|
|
81
|
+
/** Per-joint-property channels. */
|
|
82
|
+
readonly channels: readonly GltfAnimationChannelRecord[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse glTF animations[] array into GltfAnimationClipRecord[].
|
|
87
|
+
*
|
|
88
|
+
* Only LINEAR and STEP interpolation are supported. CUBICSPLINE triggers
|
|
89
|
+
* fail-fast with 'gltf-animation-cubicspline-unsupported'. Channels targeting
|
|
90
|
+
* morph weights preserve their element-major sampler output for playback.
|
|
91
|
+
*/
|
|
92
|
+
export function parseAnimation(
|
|
93
|
+
animationsJson: readonly AnimationJson[] | undefined,
|
|
94
|
+
nodesJson: readonly { readonly name?: string; readonly children?: readonly number[] }[],
|
|
95
|
+
accessors: readonly AccessorJson[],
|
|
96
|
+
bufferViews: readonly BufferViewJson[],
|
|
97
|
+
buffers: readonly Uint8Array[],
|
|
98
|
+
): Result<readonly GltfAnimationClipRecord[], GltfError> {
|
|
99
|
+
if (animationsJson === undefined || animationsJson.length === 0) {
|
|
100
|
+
return ok([]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const clips: GltfAnimationClipRecord[] = [];
|
|
104
|
+
const parentOf = buildNodeParentMap(nodesJson);
|
|
105
|
+
const nodeByPath = new Map<string, number>();
|
|
106
|
+
const pathById = new Map<AnimationTargetIdValue, string>();
|
|
107
|
+
|
|
108
|
+
for (let animIdx = 0; animIdx < animationsJson.length; animIdx++) {
|
|
109
|
+
const anim = animationsJson[animIdx];
|
|
110
|
+
if (anim === undefined) continue;
|
|
111
|
+
|
|
112
|
+
// Decode samplers first (shared across channels).
|
|
113
|
+
const decodedSamplers: GltfAnimationSamplerRecord[] = [];
|
|
114
|
+
for (let sampIdx = 0; sampIdx < anim.samplers.length; sampIdx++) {
|
|
115
|
+
const sampler = anim.samplers[sampIdx];
|
|
116
|
+
if (sampler === undefined) continue;
|
|
117
|
+
|
|
118
|
+
const interpolation = (sampler.interpolation ?? 'LINEAR') as string;
|
|
119
|
+
if (interpolation === 'CUBICSPLINE') {
|
|
120
|
+
return err(
|
|
121
|
+
gltfErr('gltf-animation-cubicspline-unsupported', {
|
|
122
|
+
animationIndex: animIdx,
|
|
123
|
+
samplerIndex: sampIdx,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (interpolation !== 'LINEAR' && interpolation !== 'STEP') {
|
|
128
|
+
return err(
|
|
129
|
+
gltfErr('gltf-animation-cubicspline-unsupported', {
|
|
130
|
+
animationIndex: animIdx,
|
|
131
|
+
samplerIndex: sampIdx,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const inputAcc = accessors[sampler.input];
|
|
137
|
+
if (inputAcc === undefined) {
|
|
138
|
+
return err(
|
|
139
|
+
gltfErr('gltf-buffer-out-of-bounds', {
|
|
140
|
+
accessor: sampler.input,
|
|
141
|
+
byteOffset: 0,
|
|
142
|
+
byteLength: 0,
|
|
143
|
+
bufferIndex: 0,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const inputResult = decodeF32Accessor(
|
|
148
|
+
sampler.input,
|
|
149
|
+
inputAcc,
|
|
150
|
+
ANIMATION_ACCESSOR_TYPES,
|
|
151
|
+
bufferViews,
|
|
152
|
+
buffers,
|
|
153
|
+
);
|
|
154
|
+
if (!inputResult.ok) return err(inputResult.error);
|
|
155
|
+
|
|
156
|
+
const outputAcc = accessors[sampler.output];
|
|
157
|
+
if (outputAcc === undefined) {
|
|
158
|
+
return err(
|
|
159
|
+
gltfErr('gltf-buffer-out-of-bounds', {
|
|
160
|
+
accessor: sampler.output,
|
|
161
|
+
byteOffset: 0,
|
|
162
|
+
byteLength: 0,
|
|
163
|
+
bufferIndex: 0,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const outputResult = decodeF32Accessor(
|
|
168
|
+
sampler.output,
|
|
169
|
+
outputAcc,
|
|
170
|
+
ANIMATION_ACCESSOR_TYPES,
|
|
171
|
+
bufferViews,
|
|
172
|
+
buffers,
|
|
173
|
+
);
|
|
174
|
+
if (!outputResult.ok) return err(outputResult.error);
|
|
175
|
+
|
|
176
|
+
decodedSamplers.push({
|
|
177
|
+
input: inputResult.value,
|
|
178
|
+
output: outputResult.value,
|
|
179
|
+
interpolation: interpolation as Interpolation,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Decode channels.
|
|
184
|
+
const channels: GltfAnimationChannelRecord[] = [];
|
|
185
|
+
for (let chIdx = 0; chIdx < anim.channels.length; chIdx++) {
|
|
186
|
+
const ch = anim.channels[chIdx];
|
|
187
|
+
if (ch === undefined) continue;
|
|
188
|
+
|
|
189
|
+
const samplerRecord = decodedSamplers[ch.sampler];
|
|
190
|
+
if (samplerRecord === undefined) {
|
|
191
|
+
return err(
|
|
192
|
+
gltfErr('gltf-buffer-out-of-bounds', {
|
|
193
|
+
accessor: ch.sampler,
|
|
194
|
+
byteOffset: 0,
|
|
195
|
+
byteLength: 0,
|
|
196
|
+
bufferIndex: 0,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const targetNodeIdx = ch.target.node;
|
|
202
|
+
const path =
|
|
203
|
+
targetNodeIdx === undefined
|
|
204
|
+
? { ok: false as const, reason: 'name-missing' as const, nodeIndex: -1 }
|
|
205
|
+
: resolveNamedNodePath(nodesJson, parentOf, targetNodeIdx);
|
|
206
|
+
if (!path.ok) {
|
|
207
|
+
return err(
|
|
208
|
+
gltfErr('gltf-animation-target-invalid', {
|
|
209
|
+
reason: path.reason,
|
|
210
|
+
animationIndex: animIdx,
|
|
211
|
+
channelIndex: chIdx,
|
|
212
|
+
nodeIndex: path.nodeIndex,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (
|
|
217
|
+
ch.target.path !== 'weights' &&
|
|
218
|
+
ch.target.path !== 'translation' &&
|
|
219
|
+
ch.target.path !== 'rotation' &&
|
|
220
|
+
ch.target.path !== 'scale'
|
|
221
|
+
) {
|
|
222
|
+
return err(
|
|
223
|
+
gltfErr('gltf-morph-unsupported', {
|
|
224
|
+
animationIndex: animIdx,
|
|
225
|
+
channelIndex: chIdx,
|
|
226
|
+
nodeIndex: ch.target.node ?? -1,
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const pathKey = JSON.stringify(path.value);
|
|
231
|
+
const previousNode = nodeByPath.get(pathKey);
|
|
232
|
+
if (previousNode !== undefined && previousNode !== targetNodeIdx) {
|
|
233
|
+
return err(
|
|
234
|
+
gltfErr('gltf-animation-target-invalid', {
|
|
235
|
+
reason: 'path-duplicate',
|
|
236
|
+
animationIndex: animIdx,
|
|
237
|
+
channelIndex: chIdx,
|
|
238
|
+
nodeIndex: targetNodeIdx as number,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
nodeByPath.set(pathKey, targetNodeIdx as number);
|
|
243
|
+
const targetId = deriveAnimationTargetId(path.value);
|
|
244
|
+
const previousPath = pathById.get(targetId);
|
|
245
|
+
if (previousPath !== undefined && previousPath !== pathKey) {
|
|
246
|
+
return err(
|
|
247
|
+
gltfErr('gltf-animation-target-invalid', {
|
|
248
|
+
reason: 'id-collision',
|
|
249
|
+
animationIndex: animIdx,
|
|
250
|
+
channelIndex: chIdx,
|
|
251
|
+
nodeIndex: targetNodeIdx as number,
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
pathById.set(targetId, pathKey);
|
|
256
|
+
|
|
257
|
+
channels.push({
|
|
258
|
+
targetId,
|
|
259
|
+
targetNodeIndex: targetNodeIdx as number,
|
|
260
|
+
property: ch.target.path,
|
|
261
|
+
sampler: samplerRecord,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Compute duration = max(sampler.input[last]) across all channels.
|
|
266
|
+
let duration = 0;
|
|
267
|
+
for (const ch of channels) {
|
|
268
|
+
const input = ch.sampler.input;
|
|
269
|
+
if (input.length > 0) {
|
|
270
|
+
const last = input[input.length - 1];
|
|
271
|
+
if (last !== undefined && last > duration) {
|
|
272
|
+
duration = last;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const name = anim.name?.trim();
|
|
278
|
+
clips.push({
|
|
279
|
+
...(name === undefined || name.length === 0 ? {} : { name }),
|
|
280
|
+
duration,
|
|
281
|
+
channels,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return ok(clips);
|
|
286
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// parse-glb-chunks.ts - GLB 2.0 binary container parser (w8).
|
|
2
|
+
//
|
|
3
|
+
// Pure function: ArrayBuffer in, Result<{version, length, jsonChunk, binChunk?}, GltfError> out.
|
|
4
|
+
// No fs, no fetch, no global state. Caller owns I/O.
|
|
5
|
+
//
|
|
6
|
+
// GLB 2.0 layout (Khronos KHR glTF-2.0 GLB section):
|
|
7
|
+
//
|
|
8
|
+
// header (12 bytes, little-endian uint32 each):
|
|
9
|
+
// magic = 0x46546C67 ('glTF')
|
|
10
|
+
// version = 2
|
|
11
|
+
// length = total byte length of the GLB container (header + chunks)
|
|
12
|
+
//
|
|
13
|
+
// chunk (variable):
|
|
14
|
+
// chunkLength (uint32 LE)
|
|
15
|
+
// chunkType (uint32 LE)
|
|
16
|
+
// 0x4E4F534A = 'JSON' - exactly one, must be the first chunk
|
|
17
|
+
// 0x004E4942 = 'BIN' - at most one, must be the second chunk
|
|
18
|
+
// chunkData (chunkLength bytes; padded so chunkLength is a multiple of 4)
|
|
19
|
+
//
|
|
20
|
+
// `parseGltfHeader` (asset.version check) lives in parse-gltf-header.ts and
|
|
21
|
+
// is re-exported here so call sites can grab both binary and JSON gates from
|
|
22
|
+
// a single entry without walking module paths.
|
|
23
|
+
|
|
24
|
+
import { err, type GltfError, gltfErr, ok, type Result } from './errors.js';
|
|
25
|
+
import { parseGltfHeader } from './parse-gltf-header.js';
|
|
26
|
+
|
|
27
|
+
export type { GltfHeaderJson } from './parse-gltf-header.js';
|
|
28
|
+
export { parseGltfHeader };
|
|
29
|
+
|
|
30
|
+
const GLB_HEADER_SIZE = 12;
|
|
31
|
+
const GLB_MAGIC = 0x46546c67;
|
|
32
|
+
const CHUNK_HEADER_SIZE = 8;
|
|
33
|
+
const CHUNK_TYPE_JSON = 0x4e4f534a;
|
|
34
|
+
const CHUNK_TYPE_BIN = 0x004e4942;
|
|
35
|
+
|
|
36
|
+
/** Result payload of {@link parseGlbChunks}. */
|
|
37
|
+
export interface GlbChunks {
|
|
38
|
+
readonly version: number;
|
|
39
|
+
readonly length: number;
|
|
40
|
+
readonly jsonChunk: Uint8Array;
|
|
41
|
+
readonly binChunk?: Uint8Array;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Split a GLB 2.0 binary container into its `JSON` chunk and optional
|
|
46
|
+
* `BIN` chunk. Returns the declared `version` and `length` for the caller
|
|
47
|
+
* to surface in diagnostics.
|
|
48
|
+
*
|
|
49
|
+
* Failure path returns one of:
|
|
50
|
+
* - `gltf-malformed-header` (magic / chunk shape / declared length / missing JSON)
|
|
51
|
+
* - `gltf-version-unsupported` (version != 2)
|
|
52
|
+
*
|
|
53
|
+
* The function is pure: no fs / network access; the caller passes the
|
|
54
|
+
* already-loaded ArrayBuffer.
|
|
55
|
+
*/
|
|
56
|
+
export function parseGlbChunks(
|
|
57
|
+
buffer: ArrayBuffer,
|
|
58
|
+
filePath: string,
|
|
59
|
+
): Result<GlbChunks, GltfError> {
|
|
60
|
+
if (buffer.byteLength < GLB_HEADER_SIZE) {
|
|
61
|
+
return err(
|
|
62
|
+
gltfErr('gltf-malformed-header', {
|
|
63
|
+
filePath,
|
|
64
|
+
byteOffset: 0,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const view = new DataView(buffer);
|
|
69
|
+
const magic = view.getUint32(0, true);
|
|
70
|
+
if (magic !== GLB_MAGIC) {
|
|
71
|
+
return err(
|
|
72
|
+
gltfErr('gltf-malformed-header', {
|
|
73
|
+
filePath,
|
|
74
|
+
byteOffset: 0,
|
|
75
|
+
magic,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const version = view.getUint32(4, true);
|
|
80
|
+
if (version !== 2) {
|
|
81
|
+
return err(
|
|
82
|
+
gltfErr('gltf-version-unsupported', {
|
|
83
|
+
filePath,
|
|
84
|
+
actualVersion: String(version),
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const declaredLength = view.getUint32(8, true);
|
|
89
|
+
if (declaredLength !== buffer.byteLength) {
|
|
90
|
+
return err(
|
|
91
|
+
gltfErr('gltf-malformed-header', {
|
|
92
|
+
filePath,
|
|
93
|
+
byteOffset: 8,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// First chunk MUST be JSON.
|
|
99
|
+
let offset = GLB_HEADER_SIZE;
|
|
100
|
+
if (offset + CHUNK_HEADER_SIZE > buffer.byteLength) {
|
|
101
|
+
return err(
|
|
102
|
+
gltfErr('gltf-malformed-header', {
|
|
103
|
+
filePath,
|
|
104
|
+
byteOffset: offset,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const jsonChunkLength = view.getUint32(offset, true);
|
|
109
|
+
const jsonChunkType = view.getUint32(offset + 4, true);
|
|
110
|
+
if (jsonChunkType !== CHUNK_TYPE_JSON) {
|
|
111
|
+
return err(
|
|
112
|
+
gltfErr('gltf-malformed-header', {
|
|
113
|
+
filePath,
|
|
114
|
+
byteOffset: offset + 4,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (offset + CHUNK_HEADER_SIZE + jsonChunkLength > buffer.byteLength) {
|
|
119
|
+
return err(
|
|
120
|
+
gltfErr('gltf-malformed-header', {
|
|
121
|
+
filePath,
|
|
122
|
+
byteOffset: offset,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const jsonChunk = new Uint8Array(buffer, offset + CHUNK_HEADER_SIZE, jsonChunkLength);
|
|
127
|
+
offset += CHUNK_HEADER_SIZE + jsonChunkLength;
|
|
128
|
+
|
|
129
|
+
// Optional BIN chunk follows JSON.
|
|
130
|
+
let binChunk: Uint8Array | undefined;
|
|
131
|
+
if (offset < buffer.byteLength) {
|
|
132
|
+
if (offset + CHUNK_HEADER_SIZE > buffer.byteLength) {
|
|
133
|
+
return err(
|
|
134
|
+
gltfErr('gltf-malformed-header', {
|
|
135
|
+
filePath,
|
|
136
|
+
byteOffset: offset,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const binChunkLength = view.getUint32(offset, true);
|
|
141
|
+
const binChunkType = view.getUint32(offset + 4, true);
|
|
142
|
+
if (binChunkType !== CHUNK_TYPE_BIN) {
|
|
143
|
+
return err(
|
|
144
|
+
gltfErr('gltf-malformed-header', {
|
|
145
|
+
filePath,
|
|
146
|
+
byteOffset: offset + 4,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (offset + CHUNK_HEADER_SIZE + binChunkLength > buffer.byteLength) {
|
|
151
|
+
return err(
|
|
152
|
+
gltfErr('gltf-malformed-header', {
|
|
153
|
+
filePath,
|
|
154
|
+
byteOffset: offset,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
binChunk = new Uint8Array(buffer, offset + CHUNK_HEADER_SIZE, binChunkLength);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return ok({
|
|
162
|
+
version,
|
|
163
|
+
length: declaredLength,
|
|
164
|
+
jsonChunk,
|
|
165
|
+
...(binChunk === undefined ? {} : { binChunk }),
|
|
166
|
+
});
|
|
167
|
+
}
|
package/src/parse-glb.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// parse-glb.ts - GLB-entry re-export shell (w17 + w19).
|
|
2
|
+
//
|
|
3
|
+
// `parseGlb` itself lives in parse-gltf.ts (so it shares a single module
|
|
4
|
+
// closure with `parseGltf` + `toAssetPack` and avoids circular imports).
|
|
5
|
+
// This file is the named entry point listed in plan-tasks.json target
|
|
6
|
+
// files; it re-exports the surface so the package layout matches the
|
|
7
|
+
// component map in plan-strategy section 3.1 / gltf_pkg.Pure.
|
|
8
|
+
//
|
|
9
|
+
// w19 adds the `parseGlbFromFile` file-entry shell which performs the
|
|
10
|
+
// sidecar `<source>.meta.json` stat pre-step (plan-strategy section 2.8
|
|
11
|
+
// path a) before delegating to the pure `parseGlb` core. The file-entry
|
|
12
|
+
// wrapper now lives in `node-file-entry.ts` and ships under the
|
|
13
|
+
// `@forgeax/engine-gltf/node` sub-entry (browser-bundle hygiene).
|
|
14
|
+
|
|
15
|
+
export type { ExternalLoader, GltfDoc } from './parse-gltf.js';
|
|
16
|
+
export { parseGlb } from './parse-gltf.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// parse-gltf-header.ts - asset.version check for the parsed glTF JSON
|
|
2
|
+
// document (w8). Pure function, no I/O.
|
|
3
|
+
//
|
|
4
|
+
// glTF 2.0 spec section 3.1: every glTF JSON document MUST carry an
|
|
5
|
+
// `asset.version` literal. v1 / v3 are not parsed by this importer
|
|
6
|
+
// (v3 does not exist; v1 is a different format). This step is the gate
|
|
7
|
+
// before any deeper structural traversal.
|
|
8
|
+
//
|
|
9
|
+
// Producers route through `gltfErr` (errors.ts SSOT) so `expected` and
|
|
10
|
+
// `hint` fields stay aligned with the GLTF_ERROR_HINTS table.
|
|
11
|
+
|
|
12
|
+
import type { GltfError } from './errors.js';
|
|
13
|
+
import { err, gltfErr, ok, type Result } from './errors.js';
|
|
14
|
+
|
|
15
|
+
/** Minimal JSON shape touched by the version gate. */
|
|
16
|
+
export interface GltfHeaderJson {
|
|
17
|
+
readonly asset?: { readonly version?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate the top-level `asset.version` field of a parsed glTF document.
|
|
22
|
+
*
|
|
23
|
+
* On success the JSON object is echoed back unchanged (caller already owns
|
|
24
|
+
* the parsed representation). On failure, the closed `GltfErrorCode`
|
|
25
|
+
* surface narrows to either `gltf-malformed-header` (asset block missing)
|
|
26
|
+
* or `gltf-version-unsupported` (version != "2.0").
|
|
27
|
+
*/
|
|
28
|
+
export function parseGltfHeader(
|
|
29
|
+
json: unknown,
|
|
30
|
+
filePath: string,
|
|
31
|
+
): Result<GltfHeaderJson, GltfError> {
|
|
32
|
+
if (json === null || typeof json !== 'object' || !('asset' in json)) {
|
|
33
|
+
return err(
|
|
34
|
+
gltfErr('gltf-malformed-header', {
|
|
35
|
+
filePath,
|
|
36
|
+
byteOffset: 0,
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const asset = (json as GltfHeaderJson).asset;
|
|
41
|
+
if (!asset || typeof asset !== 'object' || typeof asset.version !== 'string') {
|
|
42
|
+
return err(
|
|
43
|
+
gltfErr('gltf-malformed-header', {
|
|
44
|
+
filePath,
|
|
45
|
+
byteOffset: 0,
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (asset.version !== '2.0') {
|
|
50
|
+
return err(
|
|
51
|
+
gltfErr('gltf-version-unsupported', {
|
|
52
|
+
filePath,
|
|
53
|
+
actualVersion: asset.version,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return ok(json as GltfHeaderJson);
|
|
58
|
+
}
|