@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,238 @@
|
|
|
1
|
+
// parse-skin.ts - glTF skin parser (feat-20260523-skin-skeleton-animation M0).
|
|
2
|
+
//
|
|
3
|
+
// Implements parseSkin(): parses glTF `skins[]` array into GltfSkeletonRecord[]
|
|
4
|
+
// with IBM (inverse bind matrices) and jointPaths (Name-component path from
|
|
5
|
+
// scene root). Used by parseGltfWithBin to populate GltfDoc.
|
|
6
|
+
//
|
|
7
|
+
// Decision anchors:
|
|
8
|
+
// - plan-strategy D-1 (3-asset separation: IBM / skin binding / animation clip)
|
|
9
|
+
// - plan-strategy D-2 (skin index dedupe via reimport reuse managed by toAssetPack)
|
|
10
|
+
// - requirements AC-03 (skin index dedupe), AC-05 (jointPaths + Name missing fail-fast)
|
|
11
|
+
// - requirements AC-10 (IR extension), AC-27 (BindPose static AABB)
|
|
12
|
+
// - plan-strategy D-11 (BindPose AABB importer-phase, per-frame zero cost)
|
|
13
|
+
// - charter P3 (fail-fast on invalid data)
|
|
14
|
+
|
|
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
|
+
/** Maximum joints per skin (glTF spec practical limit). */
|
|
20
|
+
const MAX_JOINTS = 256;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Skeleton intermediate representation produced by parseSkin.
|
|
24
|
+
*
|
|
25
|
+
* Each entry corresponds to one glTF skin[] index. `jointPaths` is a
|
|
26
|
+
* parallel array to the skeleton's joints; each path is the sequence of
|
|
27
|
+
* Name-component values from the scene root to the joint node.
|
|
28
|
+
*/
|
|
29
|
+
export interface GltfSkeletonRecord {
|
|
30
|
+
/** Number of joints in this skeleton (= IBM length / 16). */
|
|
31
|
+
readonly jointCount: number;
|
|
32
|
+
/** Inverse bind matrices, Float32Array of length jointCount * 16. */
|
|
33
|
+
readonly inverseBindMatrices: Float32Array;
|
|
34
|
+
/** Per-joint Name path from scene root (parallel to joints array). */
|
|
35
|
+
readonly jointPaths: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SkinJson {
|
|
39
|
+
readonly name?: string;
|
|
40
|
+
readonly joints: readonly number[];
|
|
41
|
+
readonly inverseBindMatrices?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface NodeJson {
|
|
45
|
+
readonly name?: string;
|
|
46
|
+
readonly mesh?: number;
|
|
47
|
+
readonly children?: readonly number[];
|
|
48
|
+
readonly skin?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface AccessorJson {
|
|
52
|
+
readonly bufferView?: number;
|
|
53
|
+
readonly componentType: number;
|
|
54
|
+
readonly type: string;
|
|
55
|
+
readonly count: number;
|
|
56
|
+
readonly byteOffset?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface BufferViewJson {
|
|
60
|
+
readonly buffer: number;
|
|
61
|
+
readonly byteOffset?: number;
|
|
62
|
+
readonly byteLength: number;
|
|
63
|
+
readonly byteStride?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const SKIN_ACCESSOR_TYPES = ['MAT4'] as const;
|
|
67
|
+
|
|
68
|
+
/** Build a column-major mat4 identity Float32Array (16 floats). */
|
|
69
|
+
function identityMat4(): Float32Array {
|
|
70
|
+
const m = new Float32Array(16);
|
|
71
|
+
m[0] = 1;
|
|
72
|
+
m[5] = 1;
|
|
73
|
+
m[10] = 1;
|
|
74
|
+
m[15] = 1;
|
|
75
|
+
return m;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build the jointPath for a single joint: find the Name-component path from
|
|
80
|
+
* the root of the scene graph to the joint node.
|
|
81
|
+
*
|
|
82
|
+
* Uses recursive BFS-like upward traversal: for each joint node index,
|
|
83
|
+
* find the node's name and verify it's reachable from root.
|
|
84
|
+
* Returns the sequence of names from root to joint.
|
|
85
|
+
*
|
|
86
|
+
* On first miss (node has no name), emits 'gltf-skin-joint-name-missing'.
|
|
87
|
+
*/
|
|
88
|
+
function resolveJointPath(
|
|
89
|
+
nodeIndex: number,
|
|
90
|
+
nodes: readonly NodeJson[],
|
|
91
|
+
parentOf: ReadonlyMap<number, number>,
|
|
92
|
+
skinIndex: number,
|
|
93
|
+
jointPathIndex: number,
|
|
94
|
+
): Result<readonly string[], GltfError> {
|
|
95
|
+
const path = resolveNamedNodePath(nodes, parentOf, nodeIndex);
|
|
96
|
+
if (!path.ok) {
|
|
97
|
+
return err(
|
|
98
|
+
gltfErr('gltf-skin-joint-name-missing', {
|
|
99
|
+
reason: path.reason,
|
|
100
|
+
skinIndex,
|
|
101
|
+
jointPathIndex,
|
|
102
|
+
nodeIndex: path.nodeIndex,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return ok(path.value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse glTF skins[] array into GltfSkeletonRecord[].
|
|
111
|
+
*
|
|
112
|
+
* For each skin:
|
|
113
|
+
* 1. Decode IBM accessor (or fill identity if absent)
|
|
114
|
+
* 2. Resolve jointPaths from scene node hierarchy
|
|
115
|
+
* 3. Fail-fast on joint count > MAX_JOINTS or missing joint names
|
|
116
|
+
*/
|
|
117
|
+
export function parseSkin(
|
|
118
|
+
skinsJson: readonly SkinJson[] | undefined,
|
|
119
|
+
nodesJson: readonly NodeJson[],
|
|
120
|
+
accessors: readonly AccessorJson[],
|
|
121
|
+
bufferViews: readonly BufferViewJson[],
|
|
122
|
+
buffers: readonly Uint8Array[],
|
|
123
|
+
): Result<readonly GltfSkeletonRecord[], GltfError> {
|
|
124
|
+
if (skinsJson === undefined || skinsJson.length === 0) {
|
|
125
|
+
return ok([]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const records: GltfSkeletonRecord[] = [];
|
|
129
|
+
const parentOf = buildNodeParentMap(nodesJson);
|
|
130
|
+
|
|
131
|
+
for (let skinIdx = 0; skinIdx < skinsJson.length; skinIdx++) {
|
|
132
|
+
const skin = skinsJson[skinIdx];
|
|
133
|
+
if (skin === undefined) continue;
|
|
134
|
+
|
|
135
|
+
const joints = skin.joints;
|
|
136
|
+
if (joints.length > MAX_JOINTS) {
|
|
137
|
+
return err(
|
|
138
|
+
gltfErr('gltf-skin-joint-count-exceeded', {
|
|
139
|
+
skinIndex: skinIdx,
|
|
140
|
+
jointCount: joints.length,
|
|
141
|
+
maxJoints: MAX_JOINTS,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Decode IBM, or fill identity matrix.
|
|
147
|
+
let ibm: Float32Array;
|
|
148
|
+
if (skin.inverseBindMatrices !== undefined) {
|
|
149
|
+
const ibmAccIdx = skin.inverseBindMatrices;
|
|
150
|
+
const ibmAcc = accessors[ibmAccIdx];
|
|
151
|
+
if (ibmAcc === undefined) {
|
|
152
|
+
return err(
|
|
153
|
+
gltfErr('gltf-buffer-out-of-bounds', {
|
|
154
|
+
accessor: ibmAccIdx,
|
|
155
|
+
byteOffset: 0,
|
|
156
|
+
byteLength: 0,
|
|
157
|
+
bufferIndex: 0,
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const ibmResult = decodeF32Accessor(
|
|
162
|
+
ibmAccIdx,
|
|
163
|
+
ibmAcc,
|
|
164
|
+
SKIN_ACCESSOR_TYPES,
|
|
165
|
+
bufferViews,
|
|
166
|
+
buffers,
|
|
167
|
+
);
|
|
168
|
+
if (!ibmResult.ok) return err(ibmResult.error);
|
|
169
|
+
ibm = ibmResult.value;
|
|
170
|
+
} else {
|
|
171
|
+
// Fill identity: each joint gets a 4x4 identity.
|
|
172
|
+
ibm = new Float32Array(joints.length * 16);
|
|
173
|
+
for (let j = 0; j < joints.length; j++) {
|
|
174
|
+
ibm.set(identityMat4(), j * 16);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Resolve jointPaths.
|
|
179
|
+
const jointPaths: string[] = [];
|
|
180
|
+
for (let j = 0; j < joints.length; j++) {
|
|
181
|
+
const jointNodeIndex = joints[j];
|
|
182
|
+
if (jointNodeIndex === undefined) continue;
|
|
183
|
+
const pathResult = resolveJointPath(jointNodeIndex, nodesJson, parentOf, skinIdx, j);
|
|
184
|
+
if (!pathResult.ok) return err(pathResult.error);
|
|
185
|
+
jointPaths.push(pathResult.value.join('/'));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
records.push({
|
|
189
|
+
jointCount: joints.length,
|
|
190
|
+
inverseBindMatrices: ibm,
|
|
191
|
+
jointPaths,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return ok(records);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Compute the BindPose static AABB for a skinned mesh's vertex positions.
|
|
200
|
+
*
|
|
201
|
+
* At bind pose, joint_bind = IBM^{-1}, so skinning collapses:
|
|
202
|
+
* world_pos = Sum(w_i * joint_bind_i * IBM_i * local_pos) = local_pos
|
|
203
|
+
* Therefore the BindPose AABB is simply the local position bounds.
|
|
204
|
+
*
|
|
205
|
+
* Per-frame zero cost: stored in mesh asset metadata at importer time.
|
|
206
|
+
* Dynamic AABB for animated poses is deferred to OOS-skin-dyn-bounds.
|
|
207
|
+
*
|
|
208
|
+
* Returns { min: [x,y,z], max: [x,y,z] } or undefined if positions is empty.
|
|
209
|
+
*/
|
|
210
|
+
export function computeBindPoseAABB(positions: Float32Array):
|
|
211
|
+
| {
|
|
212
|
+
readonly min: readonly [number, number, number];
|
|
213
|
+
readonly max: readonly [number, number, number];
|
|
214
|
+
}
|
|
215
|
+
| undefined {
|
|
216
|
+
if (positions.length < 3) return undefined;
|
|
217
|
+
let minX = positions[0] ?? 0;
|
|
218
|
+
let minY = positions[1] ?? 0;
|
|
219
|
+
let minZ = positions[2] ?? 0;
|
|
220
|
+
let maxX = minX;
|
|
221
|
+
let maxY = minY;
|
|
222
|
+
let maxZ = minZ;
|
|
223
|
+
for (let i = 3; i < positions.length; i += 3) {
|
|
224
|
+
const x = positions[i] ?? 0;
|
|
225
|
+
const y = positions[i + 1] ?? 0;
|
|
226
|
+
const z = positions[i + 2] ?? 0;
|
|
227
|
+
if (x < minX) minX = x;
|
|
228
|
+
if (y < minY) minY = y;
|
|
229
|
+
if (z < minZ) minZ = z;
|
|
230
|
+
if (x > maxX) maxX = x;
|
|
231
|
+
if (y > maxY) maxY = y;
|
|
232
|
+
if (z > maxZ) maxZ = z;
|
|
233
|
+
}
|
|
234
|
+
return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Re-export MAX_JOINTS for use by downstream modules. */
|
|
238
|
+
export { MAX_JOINTS };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// reimport-reuse-meta.ts - two-stage GUID matching for reimport-stable
|
|
2
|
+
// `<source>.meta.json` with `importer: 'gltf'` (w13).
|
|
3
|
+
//
|
|
4
|
+
// Matching algorithm (plan-strategy decision section 2.4 + bevy comparison
|
|
5
|
+
// wiki section 3):
|
|
6
|
+
//
|
|
7
|
+
// For each new item produced by parseGltf in source iteration order:
|
|
8
|
+
// stage 1: existing entry with same (kind, name, indexFallback) -> reuse
|
|
9
|
+
// stage 2: existing entry with same (kind, indexFallback) only -> reuse
|
|
10
|
+
// fallback: AssetGuid.random() (UUIDv7 monotonic clock)
|
|
11
|
+
//
|
|
12
|
+
// Double-name hazard: when the SAME (kind, name) pair appears more than
|
|
13
|
+
// once in the new items list, stage 1 cannot disambiguate. The producer
|
|
14
|
+
// returns a structured source-key conflict before it can mint GUIDs or
|
|
15
|
+
// publish output; sourceIndex remains a locator and is never promoted to
|
|
16
|
+
// identity.
|
|
17
|
+
//
|
|
18
|
+
// AC anchors:
|
|
19
|
+
// - AC-05: byte-identical reimport when nothing changes (stage 1 reuse path)
|
|
20
|
+
// - AC-06: double-name conflict returns a structured source-key error
|
|
21
|
+
// - AC-13: reimport idempotency overall (no spurious GUID churn)
|
|
22
|
+
|
|
23
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
24
|
+
import { err, ok, type Result } from './errors.js';
|
|
25
|
+
import {
|
|
26
|
+
deriveGltfSourceKeys,
|
|
27
|
+
type GltfSourceKeyError,
|
|
28
|
+
sourceKeyForGltfOutput,
|
|
29
|
+
} from './source-key.js';
|
|
30
|
+
import { type GltfDocItemLike, subAssetKey } from './sub-asset-key.js';
|
|
31
|
+
|
|
32
|
+
export type { SubAssetKey } from './sub-asset-key.js';
|
|
33
|
+
export { subAssetKey };
|
|
34
|
+
export type GltfDocItem = GltfDocItemLike;
|
|
35
|
+
|
|
36
|
+
/** Subset of `<source>.meta.json` fields (importer=gltf arm) touched by the reuse algorithm. */
|
|
37
|
+
export interface GltfSubAssetEntry {
|
|
38
|
+
readonly guid: string;
|
|
39
|
+
readonly sourceIndex: number;
|
|
40
|
+
readonly kind: string;
|
|
41
|
+
readonly sourceKey?: string;
|
|
42
|
+
/** Optional display name copied from the current glTF output. */
|
|
43
|
+
readonly name?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GltfMetaJson {
|
|
47
|
+
readonly schemaVersion: 1;
|
|
48
|
+
readonly kind: 'external-asset-package';
|
|
49
|
+
readonly importer: 'gltf';
|
|
50
|
+
readonly source: string;
|
|
51
|
+
readonly subAssets: readonly GltfSubAssetEntry[];
|
|
52
|
+
readonly sourceOverrides?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
53
|
+
readonly sourceOverrideDescriptors?: readonly import('@forgeax/engine-types').SourceOverrideDescriptor[];
|
|
54
|
+
readonly importSettings: {
|
|
55
|
+
readonly defaultSceneIndex: number;
|
|
56
|
+
readonly standardMaterialGuid?: string;
|
|
57
|
+
readonly downscaleMaxDimension?: number;
|
|
58
|
+
readonly diagnostics: {
|
|
59
|
+
readonly nodeNames: readonly string[];
|
|
60
|
+
readonly unsupportedExtensions: readonly string[];
|
|
61
|
+
readonly matrixTrsCoexistNodes: readonly number[];
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ReimportReuseValue {
|
|
67
|
+
readonly subAssets: readonly GltfSubAssetEntry[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ReimportReuseResult = Result<ReimportReuseValue, GltfSourceKeyError>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Apply the two-stage reuse algorithm to a freshly parsed item list.
|
|
74
|
+
* Returns the new `subAssets[]` array (preserving item order), or a
|
|
75
|
+
* structured source-key conflict before any output is published.
|
|
76
|
+
*
|
|
77
|
+
* Pure function: no fs / network / global state. `AssetGuid.random()` is
|
|
78
|
+
* the only entropy source; deterministic in absence of new items.
|
|
79
|
+
*/
|
|
80
|
+
export function reimportReuseMeta(
|
|
81
|
+
items: readonly GltfDocItem[],
|
|
82
|
+
existingMeta: GltfMetaJson | undefined,
|
|
83
|
+
): ReimportReuseResult {
|
|
84
|
+
const sourceKeys = deriveGltfSourceKeys(items);
|
|
85
|
+
if (!sourceKeys.ok) return err(sourceKeys.error);
|
|
86
|
+
|
|
87
|
+
const stage1Index = new Map<string, GltfSubAssetEntry>();
|
|
88
|
+
const stage2Index = new Map<string, GltfSubAssetEntry>();
|
|
89
|
+
if (existingMeta !== undefined) {
|
|
90
|
+
for (const entry of existingMeta.subAssets) {
|
|
91
|
+
// Source names are display metadata, not identity. Semantic sourceKey
|
|
92
|
+
// remains the stage-1 identity and (kind, sourceIndex) remains the
|
|
93
|
+
// stage-2 locator fallback.
|
|
94
|
+
if (entry.sourceKey !== undefined) stage1Index.set(entry.sourceKey, entry);
|
|
95
|
+
stage2Index.set(`${entry.kind} ${entry.sourceIndex}`, entry);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const subAssets: GltfSubAssetEntry[] = [];
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
const sourceKey = sourceKeyForGltfOutput(item);
|
|
102
|
+
const indexKey = `${item.kind} ${item.sourceIndex}`;
|
|
103
|
+
let reused: GltfSubAssetEntry | undefined;
|
|
104
|
+
|
|
105
|
+
// Stage 1: semantic sourceKey match. Names are copied from the current
|
|
106
|
+
// source output below, but never participate in identity matching.
|
|
107
|
+
if (sourceKey !== undefined) reused = stage1Index.get(sourceKey);
|
|
108
|
+
// Stage 2: (kind, indexFallback) only.
|
|
109
|
+
if (reused === undefined) {
|
|
110
|
+
reused = stage2Index.get(indexKey);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (reused !== undefined) {
|
|
114
|
+
subAssets.push({
|
|
115
|
+
guid: reused.guid,
|
|
116
|
+
sourceIndex: item.sourceIndex,
|
|
117
|
+
kind: item.kind,
|
|
118
|
+
...(sourceKey === undefined ? {} : { sourceKey }),
|
|
119
|
+
...(item.name === undefined ? {} : { name: item.name }),
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
const fresh = AssetGuid.random();
|
|
123
|
+
subAssets.push({
|
|
124
|
+
guid: AssetGuid.format(fresh),
|
|
125
|
+
sourceIndex: item.sourceIndex,
|
|
126
|
+
kind: item.kind,
|
|
127
|
+
...(sourceKey === undefined ? {} : { sourceKey }),
|
|
128
|
+
...(item.name === undefined ? {} : { name: item.name }),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return ok({ subAssets });
|
|
134
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @forgeax/engine-gltf/src/serialize-meta.ts — byte-stable meta JSON
|
|
2
|
+
// serialization (sorted-keys, LF ending) for the
|
|
3
|
+
// `<source>.meta.json` sidecar. Extracted from cli-gltf.ts so it can
|
|
4
|
+
// be re-exported from the package barrel as a reusable library
|
|
5
|
+
// function (plan-strategy D-3; feat-20260705-editor-core-engine-convergence-store-ts-decompose AC-04).
|
|
6
|
+
//
|
|
7
|
+
// Anchors: plan-strategy D-3, requirements AC-04, research F-7.
|
|
8
|
+
|
|
9
|
+
function sortKeysDeep(value: unknown): unknown {
|
|
10
|
+
if (Array.isArray(value)) return value.map(sortKeysDeep);
|
|
11
|
+
if (value !== null && typeof value === 'object') {
|
|
12
|
+
const obj = value as Record<string, unknown>;
|
|
13
|
+
const sorted: Record<string, unknown> = {};
|
|
14
|
+
for (const key of Object.keys(obj).sort()) {
|
|
15
|
+
sorted[key] = sortKeysDeep(obj[key]);
|
|
16
|
+
}
|
|
17
|
+
return sorted;
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function serializeMetaJson(meta: unknown): string {
|
|
23
|
+
return `${JSON.stringify(sortKeysDeep(meta), null, 2)}\n`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { GltfDocItemLike } from './sub-asset-key.js';
|
|
2
|
+
|
|
3
|
+
interface GltfSourceKeyErrorBase {
|
|
4
|
+
readonly expected: string;
|
|
5
|
+
readonly hint: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface GltfSourceKeyConflictEntry {
|
|
9
|
+
readonly kind: string;
|
|
10
|
+
readonly name: string | null;
|
|
11
|
+
readonly sourceIndex: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type GltfSourceKeyError =
|
|
15
|
+
| (GltfSourceKeyErrorBase & {
|
|
16
|
+
readonly code: 'missing-source-key';
|
|
17
|
+
readonly detail: { readonly sourceIndices: readonly number[] };
|
|
18
|
+
})
|
|
19
|
+
| (GltfSourceKeyErrorBase & {
|
|
20
|
+
readonly code: 'duplicate-source-key' | 'ambiguous-source-key';
|
|
21
|
+
readonly detail: {
|
|
22
|
+
readonly key: string;
|
|
23
|
+
readonly sourceIndices: readonly number[];
|
|
24
|
+
readonly entries: readonly GltfSourceKeyConflictEntry[];
|
|
25
|
+
};
|
|
26
|
+
})
|
|
27
|
+
| (GltfSourceKeyErrorBase & {
|
|
28
|
+
readonly code: 'mesh-material-slot-topology-change';
|
|
29
|
+
readonly detail: {
|
|
30
|
+
readonly sourceIndices: readonly number[];
|
|
31
|
+
readonly previousIndices: readonly number[];
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type GltfSourceKeyErrorCode = GltfSourceKeyError['code'];
|
|
36
|
+
|
|
37
|
+
export type GltfSourceKeyResult =
|
|
38
|
+
| { readonly ok: true; readonly keys: readonly string[]; readonly conflicts: readonly [] }
|
|
39
|
+
| { readonly ok: false; readonly error: GltfSourceKeyError };
|
|
40
|
+
|
|
41
|
+
/** Derive a semantic glTF key; sourceIndex and source path are never inputs. */
|
|
42
|
+
export function sourceKeyForGltfOutput(
|
|
43
|
+
item: Pick<GltfDocItemLike, 'kind' | 'name'>,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
const kind = item.kind.trim();
|
|
46
|
+
if (kind.length === 0) return undefined;
|
|
47
|
+
const name = item.name?.trim();
|
|
48
|
+
return name === undefined || name.length === 0 ? kind : `${kind}:${name}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Require every output in a multi-output declaration to carry a unique key. */
|
|
52
|
+
export function deriveGltfSourceKeys(items: readonly GltfDocItemLike[]): GltfSourceKeyResult {
|
|
53
|
+
const keys = items.map((item) => sourceKeyForGltfOutput(item));
|
|
54
|
+
const missing = items
|
|
55
|
+
.map((item, index) => (keys[index] === undefined ? item.sourceIndex : undefined))
|
|
56
|
+
.filter((index): index is number => index !== undefined);
|
|
57
|
+
if (missing.length > 0) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: {
|
|
61
|
+
code: 'missing-source-key',
|
|
62
|
+
expected: 'every glTF output needs a stable semantic kind or name',
|
|
63
|
+
hint: 'publish a semantic kind/name key; do not use sourceIndex',
|
|
64
|
+
detail: { sourceIndices: missing },
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const seen = new Map<string, GltfDocItemLike>();
|
|
70
|
+
for (let index = 0; index < keys.length; index++) {
|
|
71
|
+
const key = keys[index];
|
|
72
|
+
if (key === undefined) continue;
|
|
73
|
+
const current = items[index];
|
|
74
|
+
if (current === undefined) continue;
|
|
75
|
+
const prior = seen.get(key);
|
|
76
|
+
if (prior !== undefined) {
|
|
77
|
+
const entry = (item: GltfDocItemLike): GltfSourceKeyConflictEntry => {
|
|
78
|
+
const name = item.name?.trim();
|
|
79
|
+
return {
|
|
80
|
+
kind: item.kind.trim(),
|
|
81
|
+
name: name === undefined || name.length === 0 ? null : name,
|
|
82
|
+
sourceIndex: item.sourceIndex,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const ambiguous = key === current.kind.trim();
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: {
|
|
89
|
+
code: ambiguous ? 'ambiguous-source-key' : 'duplicate-source-key',
|
|
90
|
+
expected: 'sourceKey values must be unique within one glTF package',
|
|
91
|
+
hint: ambiguous
|
|
92
|
+
? 'name each otherwise anonymous output; do not use sourceIndex'
|
|
93
|
+
: 'rename duplicate outputs before publishing topology facts',
|
|
94
|
+
detail: {
|
|
95
|
+
key,
|
|
96
|
+
sourceIndices: [prior.sourceIndex, current.sourceIndex],
|
|
97
|
+
entries: [entry(prior), entry(current)],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
seen.set(key, current);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { ok: true, keys: keys as string[], conflicts: [] };
|
|
106
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// sub-asset-key.ts - sub-asset addressing key (w13).
|
|
2
|
+
//
|
|
3
|
+
// `subAssetKey` builds the deterministic three-tuple
|
|
4
|
+
// `{ kind, name, indexFallback }` from a parsed GltfDoc item; the tuple
|
|
5
|
+
// is the input to the two-stage matching algorithm in
|
|
6
|
+
// `reimport-reuse-meta.ts`. Pure function, no I/O.
|
|
7
|
+
//
|
|
8
|
+
// `indexFallback` literal shape: `${pluralKind}/${sourceIndex}` (per
|
|
9
|
+
// plan-decisions.md L-2 and bevy_gltf comparison wiki section 3 - forgeax
|
|
10
|
+
// keeps the bevy `meshes/<i>` style anchor as the second-stage match key).
|
|
11
|
+
// Pluralisation is closed: glTF 2.0 sub-asset categories are `mesh`,
|
|
12
|
+
// `material`, `scene`, `node`, `texture`, `image`, `sampler`, `animation`,
|
|
13
|
+
// `camera`, `skin`. Tier-B v1 only emits the first three; the closed map
|
|
14
|
+
// fails closed for unrecognised inputs (charter proposition 4 explicit
|
|
15
|
+
// failure - any new kind the importer learns to emit MUST be registered
|
|
16
|
+
// here, otherwise the build break is the warning).
|
|
17
|
+
|
|
18
|
+
export interface GltfDocItemLike {
|
|
19
|
+
readonly kind: string;
|
|
20
|
+
readonly sourceIndex: number;
|
|
21
|
+
readonly name?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SubAssetKey {
|
|
25
|
+
readonly kind: string;
|
|
26
|
+
readonly name: string | null;
|
|
27
|
+
readonly indexFallback: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const PLURAL: Readonly<Record<string, string>> = {
|
|
31
|
+
mesh: 'meshes',
|
|
32
|
+
material: 'materials',
|
|
33
|
+
scene: 'scenes',
|
|
34
|
+
node: 'nodes',
|
|
35
|
+
texture: 'textures',
|
|
36
|
+
image: 'images',
|
|
37
|
+
sampler: 'samplers',
|
|
38
|
+
animation: 'animations',
|
|
39
|
+
camera: 'cameras',
|
|
40
|
+
skin: 'skins',
|
|
41
|
+
skeleton: 'skeletons',
|
|
42
|
+
'animation-clip': 'animation-clips',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function pluraliseKind(kind: string): string {
|
|
46
|
+
const known = PLURAL[kind];
|
|
47
|
+
if (known !== undefined) return known;
|
|
48
|
+
// Unknown kinds fall back to a naive `+s` so `subAssetKey` stays total
|
|
49
|
+
// (no exception path); the importer will normally never emit an
|
|
50
|
+
// unknown kind because parseGltf only constructs items from the closed
|
|
51
|
+
// Tier-B list.
|
|
52
|
+
return `${kind}s`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function subAssetKey(item: GltfDocItemLike): SubAssetKey {
|
|
56
|
+
return {
|
|
57
|
+
kind: item.kind,
|
|
58
|
+
name: item.name ?? null,
|
|
59
|
+
indexFallback: `${pluraliseKind(item.kind)}/${item.sourceIndex}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// to-asset-pack.ts - toAssetPack re-export shell (w17).
|
|
2
|
+
//
|
|
3
|
+
// `toAssetPack` itself lives in parse-gltf.ts (shares the GltfDoc IR
|
|
4
|
+
// closure with parseGltf / parseGlb). This file is the named entry point
|
|
5
|
+
// listed in plan-tasks.json target files; it re-exports the surface so
|
|
6
|
+
// the package layout matches plan-strategy section 3.1 / gltf_pkg.Pure.
|
|
7
|
+
|
|
8
|
+
export { toAssetPack } from './parse-gltf.js';
|
package/src/transform.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// transform.ts - decomposeNodeTransform (w11).
|
|
2
|
+
//
|
|
3
|
+
// glTF 2.0 spec section 3.7 (node):
|
|
4
|
+
// - node.matrix : 16 floats column-major (mutually exclusive with TRS,
|
|
5
|
+
// per spec, but real-world DCC tools occasionally
|
|
6
|
+
// export both - forgeax tolerates this with stderr
|
|
7
|
+
// warn + diagnostics record per requirements section
|
|
8
|
+
// boundary cases)
|
|
9
|
+
// - node.translation: VEC3 (default identity translation)
|
|
10
|
+
// - node.rotation : VEC4 quaternion (default identity rotation)
|
|
11
|
+
// - node.scale : VEC3 (default identity scale)
|
|
12
|
+
//
|
|
13
|
+
// This wrapper hides the math package's in-place 3-arg `mat4.decompose`
|
|
14
|
+
// signature behind a POD readonly tuple return so the importer surface
|
|
15
|
+
// stays Vec3/Quat brand-free at the API boundary (charter proposition 5
|
|
16
|
+
// consistent abstraction; plan-strategy decision section 2.7 / OQ-2).
|
|
17
|
+
|
|
18
|
+
import { mat4, quat, vec3 } from '@forgeax/engine-math';
|
|
19
|
+
|
|
20
|
+
/** glTF VEC3 literal accepted by node.translation / node.scale. */
|
|
21
|
+
export type Vec3Tuple = readonly [number, number, number];
|
|
22
|
+
/** glTF VEC4 quaternion literal accepted by node.rotation. */
|
|
23
|
+
export type Vec4Tuple = readonly [number, number, number, number];
|
|
24
|
+
|
|
25
|
+
/** Minimal node JSON shape touched by the decomposer. */
|
|
26
|
+
export interface NodeTransformJson {
|
|
27
|
+
readonly matrix?: readonly number[];
|
|
28
|
+
readonly translation?: readonly number[];
|
|
29
|
+
readonly rotation?: readonly number[];
|
|
30
|
+
readonly scale?: readonly number[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Mutable diagnostics accumulator (writes to .matrixTrsCoexistNodes only). */
|
|
34
|
+
export interface TransformDiagnostics {
|
|
35
|
+
matrixTrsCoexistNodes: number[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DecomposedTransform {
|
|
39
|
+
readonly translation: Vec3Tuple;
|
|
40
|
+
readonly rotation: Vec4Tuple;
|
|
41
|
+
readonly scale: Vec3Tuple;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const IDENTITY: DecomposedTransform = {
|
|
45
|
+
translation: [0, 0, 0] as const,
|
|
46
|
+
rotation: [0, 0, 0, 1] as const,
|
|
47
|
+
scale: [1, 1, 1] as const,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function tuple3(arr: readonly number[] | undefined, fallback: Vec3Tuple): Vec3Tuple {
|
|
51
|
+
if (arr === undefined || arr.length < 3) return fallback;
|
|
52
|
+
return [arr[0] ?? fallback[0], arr[1] ?? fallback[1], arr[2] ?? fallback[2]];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tuple4(arr: readonly number[] | undefined, fallback: Vec4Tuple): Vec4Tuple {
|
|
56
|
+
if (arr === undefined || arr.length < 4) return fallback;
|
|
57
|
+
return [
|
|
58
|
+
arr[0] ?? fallback[0],
|
|
59
|
+
arr[1] ?? fallback[1],
|
|
60
|
+
arr[2] ?? fallback[2],
|
|
61
|
+
arr[3] ?? fallback[3],
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decompose a glTF node's transform into a POD readonly TRS triple.
|
|
67
|
+
*
|
|
68
|
+
* Decision tree:
|
|
69
|
+
* 1. matrix present -> mat4.decompose 3-arg in-place; if
|
|
70
|
+
* any TRS field is also present,
|
|
71
|
+
* stderr warn + push nodeIndex into
|
|
72
|
+
* diagnostics.matrixTrsCoexistNodes.
|
|
73
|
+
* 2. only TRS fields present -> use them verbatim (defaults filled).
|
|
74
|
+
* 3. nothing present -> identity (0/0/0, 0/0/0/1, 1/1/1).
|
|
75
|
+
*
|
|
76
|
+
* Return type is `readonly` POD tuple, deliberately not Vec3/Quat from
|
|
77
|
+
* @forgeax/engine-math, so the importer's IR stays brand-free
|
|
78
|
+
* (plan-strategy decision section 2.7 / OQ-2).
|
|
79
|
+
*/
|
|
80
|
+
export function decomposeNodeTransform(
|
|
81
|
+
node: NodeTransformJson,
|
|
82
|
+
nodeIndex: number,
|
|
83
|
+
diagnostics: TransformDiagnostics,
|
|
84
|
+
): DecomposedTransform {
|
|
85
|
+
const hasMatrix = node.matrix !== undefined && node.matrix.length === 16;
|
|
86
|
+
const hasTrs =
|
|
87
|
+
node.translation !== undefined || node.rotation !== undefined || node.scale !== undefined;
|
|
88
|
+
|
|
89
|
+
if (hasMatrix) {
|
|
90
|
+
if (hasTrs) {
|
|
91
|
+
console.error(`[warn] node[${nodeIndex}] has both matrix and TRS, matrix takes precedence`);
|
|
92
|
+
diagnostics.matrixTrsCoexistNodes.push(nodeIndex);
|
|
93
|
+
}
|
|
94
|
+
const out_t = vec3.create();
|
|
95
|
+
const out_r = quat.create();
|
|
96
|
+
const out_s = vec3.create(1, 1, 1);
|
|
97
|
+
// node.matrix is glTF column-major float[16]; mat4.decompose accepts
|
|
98
|
+
// Mat4Like (any indexable [0..15] number container).
|
|
99
|
+
const m = node.matrix as readonly number[];
|
|
100
|
+
mat4.decompose(out_t, out_r, out_s, m as unknown as Parameters<typeof mat4.decompose>[3]);
|
|
101
|
+
return {
|
|
102
|
+
translation: [out_t[0] ?? 0, out_t[1] ?? 0, out_t[2] ?? 0] as const,
|
|
103
|
+
rotation: [out_r[0] ?? 0, out_r[1] ?? 0, out_r[2] ?? 0, out_r[3] ?? 1] as const,
|
|
104
|
+
scale: [out_s[0] ?? 1, out_s[1] ?? 1, out_s[2] ?? 1] as const,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!hasTrs) {
|
|
109
|
+
return IDENTITY;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
translation: tuple3(node.translation, IDENTITY.translation),
|
|
114
|
+
rotation: tuple4(node.rotation, IDENTITY.rotation),
|
|
115
|
+
scale: tuple3(node.scale, IDENTITY.scale),
|
|
116
|
+
};
|
|
117
|
+
}
|