@flighthq/scene3d-formats 0.2.1-next.840.857425c
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/dist/awd2Inflate.d.ts +4 -0
- package/dist/awd2Inflate.d.ts.map +1 -0
- package/dist/awd2Inflate.js +249 -0
- package/dist/awd2Inflate.js.map +1 -0
- package/dist/awd2Parse.d.ts +7 -0
- package/dist/awd2Parse.d.ts.map +1 -0
- package/dist/awd2Parse.js +1533 -0
- package/dist/awd2Parse.js.map +1 -0
- package/dist/awd2Schema.d.ts +44 -0
- package/dist/awd2Schema.d.ts.map +1 -0
- package/dist/awd2Schema.js +67 -0
- package/dist/awd2Schema.js.map +1 -0
- package/dist/gltfParse.d.ts +9 -0
- package/dist/gltfParse.d.ts.map +1 -0
- package/dist/gltfParse.js +1456 -0
- package/dist/gltfParse.js.map +1 -0
- package/dist/gltfPunctualLights.d.ts +3 -0
- package/dist/gltfPunctualLights.d.ts.map +1 -0
- package/dist/gltfPunctualLights.js +103 -0
- package/dist/gltfPunctualLights.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/md2Parse.d.ts +5 -0
- package/dist/md2Parse.d.ts.map +1 -0
- package/dist/md2Parse.js +408 -0
- package/dist/md2Parse.js.map +1 -0
- package/dist/md2Schema.d.ts +12 -0
- package/dist/md2Schema.d.ts.map +1 -0
- package/dist/md2Schema.js +193 -0
- package/dist/md2Schema.js.map +1 -0
- package/dist/md5AnimParse.d.ts +3 -0
- package/dist/md5AnimParse.d.ts.map +1 -0
- package/dist/md5AnimParse.js +385 -0
- package/dist/md5AnimParse.js.map +1 -0
- package/dist/md5Parse.d.ts +6 -0
- package/dist/md5Parse.d.ts.map +1 -0
- package/dist/md5Parse.js +635 -0
- package/dist/md5Parse.js.map +1 -0
- package/dist/mtlParse.d.ts +3 -0
- package/dist/mtlParse.d.ts.map +1 -0
- package/dist/mtlParse.js +256 -0
- package/dist/mtlParse.js.map +1 -0
- package/dist/objParse.d.ts +5 -0
- package/dist/objParse.d.ts.map +1 -0
- package/dist/objParse.js +464 -0
- package/dist/objParse.js.map +1 -0
- package/dist/sceneSkeleton.d.ts +3 -0
- package/dist/sceneSkeleton.d.ts.map +1 -0
- package/dist/sceneSkeleton.js +34 -0
- package/dist/sceneSkeleton.js.map +1 -0
- package/dist/shared.d.ts +18 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +143 -0
- package/dist/shared.js.map +1 -0
- package/dist/threeDsParse.d.ts +4 -0
- package/dist/threeDsParse.d.ts.map +1 -0
- package/dist/threeDsParse.js +718 -0
- package/dist/threeDsParse.js.map +1 -0
- package/package.json +55 -0
- package/src/awd2Inflate.test.ts +118 -0
- package/src/awd2Parse.test.ts +2431 -0
- package/src/gltfParse.test.ts +2339 -0
- package/src/gltfPunctualLights.test.ts +184 -0
- package/src/gltfTreeShaking.test.ts +45 -0
- package/src/md2Parse.test.ts +1033 -0
- package/src/md5AnimParse.test.ts +673 -0
- package/src/md5Parse.test.ts +1094 -0
- package/src/mtlParse.test.ts +244 -0
- package/src/objParse.test.ts +690 -0
- package/src/sceneSkeleton.test.ts +35 -0
- package/src/shared.test.ts +210 -0
- package/src/threeDsParse.test.ts +1089 -0
package/dist/md5Parse.js
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { composeMatrix4, conjugateQuaternion, createMatrix4, createQuaternion, createTransform3D, inverseMatrix4, multiplyQuaternion, rotateVector3ByQuaternion, setQuaternion, setVector3, } from '@flighthq/geometry';
|
|
2
|
+
import { reportImportDiagnostic } from '@flighthq/importdiagnostics';
|
|
3
|
+
import { createBlinnPhongMaterial } from '@flighthq/materials';
|
|
4
|
+
import { CANONICAL_SKINNED_MESH_GEOMETRY_LAYOUT, computeMeshGeometryNormals, createMeshGeometry } from '@flighthq/mesh';
|
|
5
|
+
import { createScene3DFromDocument } from '@flighthq/scene3d';
|
|
6
|
+
import { ImportDiagnosticSeverity, MeshKind, Node3DKind } from '@flighthq/types';
|
|
7
|
+
import { parseMd5Anim } from './md5AnimParse';
|
|
8
|
+
import { findScene3DSkeletonJoints } from './sceneSkeleton';
|
|
9
|
+
import { convertPositionsZUpToYUp, convertQuaternionsZUpToYUp, createExternalTextureRef, MAX_SKIN_INFLUENCES, packSkinInfluences, reverseTriangleWinding, SKINNED_FLOATS_PER_VERTEX, } from './shared';
|
|
10
|
+
// Parses an id Tech 4 MD5 mesh file (.md5mesh) into a Scene3D. Convenience over
|
|
11
|
+
// `createScene3DFromDocument(parseMd5Mesh(source, diagnostics))`. See parseMd5Mesh for the import model.
|
|
12
|
+
export function createScene3DFromMd5Mesh(source, diagnostics) {
|
|
13
|
+
return createScene3DFromDocument(parseMd5Mesh(source, diagnostics));
|
|
14
|
+
}
|
|
15
|
+
// One-call MD5 import: builds the Scene3D from the `.md5mesh` source and, when a `.md5anim` source is
|
|
16
|
+
// given, binds its skeletal animation to that mesh's skeleton and stores it in `scene.animations`. MD5
|
|
17
|
+
// splits mesh and animation across two files that must be composed against the same skeleton — the mesh
|
|
18
|
+
// supplies the joint nodes the animation's channels bind to — so this is the composition callers would
|
|
19
|
+
// otherwise hand-write (createScene3DFromMd5Mesh, then findScene3DSkeletonJoints, then parseMd5Anim). The
|
|
20
|
+
// `.md5anim` carries no name of its own, so the clip is keyed 'default'; a caller loading several
|
|
21
|
+
// animations against one mesh uses parseMd5Anim directly and keys each as it likes. Warns (and skips the
|
|
22
|
+
// animation) when `animSource` is given but the mesh carries no skeleton to bind it to.
|
|
23
|
+
export function importMd5Mesh(meshSource, animSource, diagnostics) {
|
|
24
|
+
const scene = createScene3DFromMd5Mesh(meshSource, diagnostics);
|
|
25
|
+
if (animSource == null)
|
|
26
|
+
return scene;
|
|
27
|
+
const joints = findScene3DSkeletonJoints(scene.root);
|
|
28
|
+
if (joints === null) {
|
|
29
|
+
// The mesh carried no skeleton, so a recognized-but-unbindable animation is skipped (the mesh is fine).
|
|
30
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, 'md5mesh.animation-no-skeleton', 'importMd5Mesh');
|
|
31
|
+
return scene;
|
|
32
|
+
}
|
|
33
|
+
const clip = parseMd5Anim(animSource, joints, diagnostics);
|
|
34
|
+
if (clip !== null)
|
|
35
|
+
scene.animations.default = clip;
|
|
36
|
+
return scene;
|
|
37
|
+
}
|
|
38
|
+
// Parses an id Tech 4 MD5 mesh file (.md5mesh) into a format-neutral Scene3DDocument. The ASCII
|
|
39
|
+
// line-oriented format contains a skeleton (joints) and one or more mesh sections. Each mesh section
|
|
40
|
+
// becomes one document Mesh node (skinned layout, joints0/weights0), and the joints become a "skeleton"
|
|
41
|
+
// group + joint node subtree in the document `nodes` table plus one entry in `skins` (joints by node
|
|
42
|
+
// index, inverse-bind per joint). The subtlety that MD5 skinning gets wrong: .md5mesh joint transforms
|
|
43
|
+
// are ABSOLUTE (object-space), but .md5anim frames are parent-RELATIVE — so each joint node here carries
|
|
44
|
+
// its parent-RELATIVE local transform (parent × child rebuilds the absolute world), while parseMd5Anim
|
|
45
|
+
// drives its already-relative values onto the same joints. Both then pose one consistent hierarchy. The
|
|
46
|
+
// skin's inverse-bind is derived directly from the ABSOLUTE bind world (the document requires it explicit,
|
|
47
|
+
// where the live scene path let createSkeleton3D derive it from the joint nodes' world transforms).
|
|
48
|
+
//
|
|
49
|
+
// Vertex positions are computed from weighted joint influences: for each vertex, the final position is the
|
|
50
|
+
// sum of each weight's bias multiplied by the joint-space-transformed weight position plus the joint
|
|
51
|
+
// position. MD5's per-section `shader` becomes a BlinnPhongMaterial (the id Tech texture-and-lighting
|
|
52
|
+
// model) whose diffuseMap references the shader path as an unresolved External ref — the parser
|
|
53
|
+
// references, it does not load; resolution is @flighthq/scene3d-resources's explicit step.
|
|
54
|
+
//
|
|
55
|
+
// MD5 splits mesh and animation across two files, so the document's `animations` table is empty. Use the
|
|
56
|
+
// `importMd5Mesh(meshSource, animSource?)` composer to bind a paired `.md5anim` in one call, or compose
|
|
57
|
+
// onto the assembled scene directly:
|
|
58
|
+
// `scene.animations['walk'] = parseMd5Anim(animSource, findScene3DSkeletonJoints(scene.root)!)`.
|
|
59
|
+
//
|
|
60
|
+
// Malformed lines record a diagnostic and are skipped; the function never throws on bad input.
|
|
61
|
+
export function parseMd5Mesh(source, diagnostics) {
|
|
62
|
+
const document = emptyMd5Document();
|
|
63
|
+
// Repeated malformed lines/indices are tallied here and flushed as ONE crumb per (kind, discriminator)
|
|
64
|
+
// at the end — parseMd5Mesh is the emitting function, so it is every aggregated crumb's origin. Null
|
|
65
|
+
// (and every tally call a no-op) when no collector is engaged, so an unopted parse stays allocation-free.
|
|
66
|
+
const md5Drops = diagnostics ? new Map() : null;
|
|
67
|
+
const joints = [];
|
|
68
|
+
const meshes = [];
|
|
69
|
+
const lines = source.split('\n');
|
|
70
|
+
let i = 0;
|
|
71
|
+
while (i < lines.length) {
|
|
72
|
+
const line = lines[i].trim();
|
|
73
|
+
i++;
|
|
74
|
+
if (line.length === 0 || line.startsWith('//'))
|
|
75
|
+
continue;
|
|
76
|
+
if (line.startsWith('MD5Version')) {
|
|
77
|
+
const version = parseInt(line.split(/\s+/)[1], 10);
|
|
78
|
+
if (Number.isFinite(version) && version !== 10) {
|
|
79
|
+
// The block layout is assumed to be v10 and parsing continues regardless — a degraded recovery.
|
|
80
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Recover, 'md5mesh.unsupported-version', '', { version });
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (line.startsWith('commandline') || line.startsWith('numJoints') || line.startsWith('numMeshes')) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (line === 'joints {') {
|
|
88
|
+
i = parseJointsBlock(lines, i, joints, md5Drops);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (line === 'mesh {') {
|
|
92
|
+
const mesh = parseMeshBlock(lines, i, md5Drops);
|
|
93
|
+
i = mesh.nextLine;
|
|
94
|
+
meshes.push(mesh.result);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Emit the skeleton into the document node table (skeleton group + joint nodes) and its skin (joints by
|
|
99
|
+
// node index + inverse-bind). The skin index every mesh section binds to.
|
|
100
|
+
let skinIndex;
|
|
101
|
+
if (joints.length > 0) {
|
|
102
|
+
skinIndex = document.skins.length;
|
|
103
|
+
document.skins.push(buildMd5SkeletonDocument(joints, document, md5Drops));
|
|
104
|
+
}
|
|
105
|
+
// Build mesh geometry from weighted vertices. Each vertex bakes its bind-pose position from the joint
|
|
106
|
+
// influences and keeps those influences as the skinned layout's joints0/weights0 channels so the mesh can
|
|
107
|
+
// be re-deformed every frame from an animated skeleton. Linear-blend skinning carries at most
|
|
108
|
+
// MAX_SKIN_INFLUENCES influences per vertex; vertices with more are reduced to the highest-weight four,
|
|
109
|
+
// counted here so the truncation is reported once rather than staying silent.
|
|
110
|
+
let truncatedVertexCount = 0;
|
|
111
|
+
let maxObservedInfluences = 0;
|
|
112
|
+
for (let m = 0; m < meshes.length; m++) {
|
|
113
|
+
const md5Mesh = meshes[m];
|
|
114
|
+
const vertices = [];
|
|
115
|
+
const indices = [];
|
|
116
|
+
// Reused per-vertex influence scratch (up to 4 joint/weight pairs, top-by-bias).
|
|
117
|
+
const jointScratch = [0, 0, 0, 0];
|
|
118
|
+
const weightScratch = [0, 0, 0, 0];
|
|
119
|
+
for (let v = 0; v < md5Mesh.vertices.length; v++) {
|
|
120
|
+
const vert = md5Mesh.vertices[v];
|
|
121
|
+
// Each influence keeps its joint index, bias, and the weight's model-space position (mx,my,mz) so
|
|
122
|
+
// the bind position can be baked from the SAME reduced set the skin stores (see below).
|
|
123
|
+
const influences = [];
|
|
124
|
+
for (let w = 0; w < vert.countWeights; w++) {
|
|
125
|
+
const weightIndex = vert.startWeight + w;
|
|
126
|
+
if (weightIndex >= md5Mesh.weights.length) {
|
|
127
|
+
// The vertex keeps the influences read so far (a partial, degraded vertex) — Recover.
|
|
128
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Recover, 'md5mesh.vertex-weight-out-of-range', '', {
|
|
129
|
+
firstIndex: weightIndex,
|
|
130
|
+
firstVertex: v,
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
const weight = md5Mesh.weights[weightIndex];
|
|
135
|
+
if (weight.jointIndex < 0 || weight.jointIndex >= joints.length) {
|
|
136
|
+
// The bad weight is skipped; the vertex keeps its other influences — Recover.
|
|
137
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Recover, 'md5mesh.weight-joint-out-of-range', '', {
|
|
138
|
+
firstIndex: weight.jointIndex,
|
|
139
|
+
firstWeight: weightIndex,
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const joint = joints[weight.jointIndex];
|
|
144
|
+
// Rotate weight position by joint orientation quaternion.
|
|
145
|
+
const rx = quatRotateVec3X(joint.orientationX, joint.orientationY, joint.orientationZ, joint.orientationW, weight.positionX, weight.positionY, weight.positionZ);
|
|
146
|
+
const ry = quatRotateVec3Y(joint.orientationX, joint.orientationY, joint.orientationZ, joint.orientationW, weight.positionX, weight.positionY, weight.positionZ);
|
|
147
|
+
const rz = quatRotateVec3Z(joint.orientationX, joint.orientationY, joint.orientationZ, joint.orientationW, weight.positionX, weight.positionY, weight.positionZ);
|
|
148
|
+
influences.push({
|
|
149
|
+
jointIndex: weight.jointIndex,
|
|
150
|
+
mx: joint.positionX + rx,
|
|
151
|
+
my: joint.positionY + ry,
|
|
152
|
+
mz: joint.positionZ + rz,
|
|
153
|
+
weight: weight.bias,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// Bake the bind position from the top-MAX_SKIN_INFLUENCES highest-bias influences, renormalized —
|
|
157
|
+
// the exact reduced set packSkinInfluences emits as joints0/weights0. Baking from all influences
|
|
158
|
+
// while skinning from only the top four would leave a >4-influence vertex's stored bind position
|
|
159
|
+
// disagreeing with what the CPU/GPU skin reproduces at bind pose. Sorting once here also fixes the
|
|
160
|
+
// reduction order packSkinInfluences (which re-sorts identically) will keep.
|
|
161
|
+
influences.sort((a, b) => b.weight - a.weight);
|
|
162
|
+
const kept = Math.min(influences.length, MAX_SKIN_INFLUENCES);
|
|
163
|
+
if (influences.length > MAX_SKIN_INFLUENCES) {
|
|
164
|
+
truncatedVertexCount++;
|
|
165
|
+
if (influences.length > maxObservedInfluences)
|
|
166
|
+
maxObservedInfluences = influences.length;
|
|
167
|
+
}
|
|
168
|
+
let biasSum = 0;
|
|
169
|
+
for (let i = 0; i < kept; i++)
|
|
170
|
+
biasSum += influences[i].weight;
|
|
171
|
+
let px = 0;
|
|
172
|
+
let py = 0;
|
|
173
|
+
let pz = 0;
|
|
174
|
+
for (let i = 0; i < kept; i++) {
|
|
175
|
+
const normWeight = biasSum > 0 ? influences[i].weight / biasSum : 0;
|
|
176
|
+
px += normWeight * influences[i].mx;
|
|
177
|
+
py += normWeight * influences[i].my;
|
|
178
|
+
pz += normWeight * influences[i].mz;
|
|
179
|
+
}
|
|
180
|
+
packSkinInfluences(influences, jointScratch, weightScratch);
|
|
181
|
+
// Position (3 floats) in MD5's native Z-up space; batch-converted below.
|
|
182
|
+
vertices.push(px, py, pz);
|
|
183
|
+
// Normal (3 floats) — MD5 mesh carries none; zero-filled here, regenerated after Y-up convert.
|
|
184
|
+
vertices.push(0, 0, 0);
|
|
185
|
+
// Tangent (4 floats) — MD5 mesh does not carry tangents; zero-filled.
|
|
186
|
+
vertices.push(0, 0, 0, 0);
|
|
187
|
+
// UV (2 floats).
|
|
188
|
+
vertices.push(vert.u, vert.v);
|
|
189
|
+
// joints0 (4 floats) — the influencing joint indices, carried as float indices.
|
|
190
|
+
vertices.push(jointScratch[0], jointScratch[1], jointScratch[2], jointScratch[3]);
|
|
191
|
+
// weights0 (4 floats) — the blend weights, renormalized to sum 1 (or all-zero for no influence).
|
|
192
|
+
vertices.push(weightScratch[0], weightScratch[1], weightScratch[2], weightScratch[3]);
|
|
193
|
+
}
|
|
194
|
+
// Convert vertex positions from Z-up to Y-up in the interleaved buffer (positions only; the
|
|
195
|
+
// stride walk skips the other channels).
|
|
196
|
+
convertPositionsZUpToYUp(vertices, SKINNED_FLOATS_PER_VERTEX, 0);
|
|
197
|
+
for (let t = 0; t < md5Mesh.indices.length; t++) {
|
|
198
|
+
indices.push(md5Mesh.indices[t]);
|
|
199
|
+
}
|
|
200
|
+
// id Tech 4 winds MD5 triangles clockwise (front-facing under D3's convention); the Z-up→Y-up
|
|
201
|
+
// position conversion is a rotation (determinant +1) so it preserves that winding, leaving the
|
|
202
|
+
// triangles clockwise in Flight's counter-clockwise-front space. Reverse them here so front faces
|
|
203
|
+
// stay front (backface culling) and computeMeshGeometryNormals — which crosses (v1−v0)×(v2−v0),
|
|
204
|
+
// the CCW convention — derives outward normals rather than inward. Mirrors the AWD importer.
|
|
205
|
+
reverseTriangleWinding(indices);
|
|
206
|
+
if (indices.length > 0) {
|
|
207
|
+
const geometry = createMeshGeometry({
|
|
208
|
+
indices: Uint32Array.from(indices),
|
|
209
|
+
layout: CANONICAL_SKINNED_MESH_GEOMETRY_LAYOUT,
|
|
210
|
+
vertices: new Float32Array(vertices),
|
|
211
|
+
});
|
|
212
|
+
// MD5 carries no normals; derive them from the Y-up bind-pose positions and winding.
|
|
213
|
+
computeMeshGeometryNormals(geometry, geometry);
|
|
214
|
+
// MD5's per-section `shader` names the material/texture the mesh uses. MD5 has no lighting-model
|
|
215
|
+
// parameters, so decode it as a BlinnPhongMaterial (the id Tech texture-and-lighting model) whose
|
|
216
|
+
// diffuseMap references the shader path; resolution of that path is the caller's step.
|
|
217
|
+
const meshMaterials = [];
|
|
218
|
+
if (md5Mesh.shader.length > 0) {
|
|
219
|
+
const material = createBlinnPhongMaterial({
|
|
220
|
+
diffuseMap: createExternalTextureRef(md5Mesh.shader, null, document.resources),
|
|
221
|
+
});
|
|
222
|
+
// MD5's shader path is the material's authored identity — preserve it as the name.
|
|
223
|
+
material.name = md5Mesh.shader;
|
|
224
|
+
meshMaterials.push(document.materials.length);
|
|
225
|
+
document.materials.push(material);
|
|
226
|
+
}
|
|
227
|
+
const documentMesh = { geometry, materials: meshMaterials };
|
|
228
|
+
if (skinIndex !== undefined)
|
|
229
|
+
documentMesh.skin = skinIndex;
|
|
230
|
+
const meshIndex = document.meshes.length;
|
|
231
|
+
document.meshes.push(documentMesh);
|
|
232
|
+
const nodeIndex = document.nodes.length;
|
|
233
|
+
document.nodes.push({ children: [], kind: MeshKind, mesh: meshIndex, transform: createTransform3D() });
|
|
234
|
+
document.scenes[0].rootNodes.push(nodeIndex);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// A parsed mesh section with no triangles produces no node — the section's geometry is dropped.
|
|
238
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.mesh-empty', '', {});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (truncatedVertexCount > 0) {
|
|
242
|
+
// Pre-counted total (not a per-occurrence tally): linear-blend skinning keeps at most 4 influences, so
|
|
243
|
+
// the extras are reduced to the highest-weight four — a universal, benign clamp. Emitted from parseMd5Mesh.
|
|
244
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Recover, 'md5mesh.vertex-over-influenced', 'parseMd5Mesh', {
|
|
245
|
+
count: truncatedVertexCount,
|
|
246
|
+
maxInfluences: maxObservedInfluences,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// Flush the aggregated tallies as one crumb per (kind, discriminator) with its total count.
|
|
250
|
+
if (md5Drops !== null) {
|
|
251
|
+
for (const tally of md5Drops.values()) {
|
|
252
|
+
reportImportDiagnostic(diagnostics, tally.severity, tally.kind, 'parseMd5Mesh', {
|
|
253
|
+
...tally.detail,
|
|
254
|
+
count: tally.count,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return document;
|
|
259
|
+
}
|
|
260
|
+
// Emits an MD5 joint list into a Scene3DDocument as a "skeleton" group node + one joint node per MD5 joint
|
|
261
|
+
// (with its parent-RELATIVE local transform), and returns the Scene3DDocumentSkin whose joints are those node
|
|
262
|
+
// indices and whose inverse-bind is derived from the ABSOLUTE bind world. Appends the nodes to
|
|
263
|
+
// `document.nodes` and wires the skeleton-group node as a scene root plus each joint under its parent joint
|
|
264
|
+
// (roots under the group) via `children` index lists.
|
|
265
|
+
function buildMd5SkeletonDocument(joints, document, md5Drops) {
|
|
266
|
+
const skeletonRootIndex = document.nodes.length;
|
|
267
|
+
document.nodes.push({ children: [], kind: Node3DKind, name: 'skeleton', transform: createTransform3D() });
|
|
268
|
+
document.scenes[0].rootNodes.push(skeletonRootIndex);
|
|
269
|
+
// Convert joint positions and orientations from Z-up to Y-up.
|
|
270
|
+
const jointPositions = [];
|
|
271
|
+
const jointOrientations = [];
|
|
272
|
+
for (const joint of joints) {
|
|
273
|
+
jointPositions.push(joint.positionX, joint.positionY, joint.positionZ);
|
|
274
|
+
jointOrientations.push(joint.orientationX, joint.orientationY, joint.orientationZ, joint.orientationW);
|
|
275
|
+
}
|
|
276
|
+
convertPositionsZUpToYUp(jointPositions);
|
|
277
|
+
convertQuaternionsZUpToYUp(jointOrientations);
|
|
278
|
+
const jointNodeIndices = [];
|
|
279
|
+
for (let j = 0; j < joints.length; j++) {
|
|
280
|
+
jointNodeIndices.push(document.nodes.length);
|
|
281
|
+
document.nodes.push({ children: [], kind: Node3DKind, name: joints[j].name, transform: createTransform3D() });
|
|
282
|
+
}
|
|
283
|
+
// The .md5mesh joints are ABSOLUTE (object-space) transforms, but the Node3D hierarchy composes parent
|
|
284
|
+
// × child, so each joint's LOCAL transform must be its transform relative to its parent: localQuat =
|
|
285
|
+
// parentAbsQuat⁻¹ · absQuat, localPos = parentAbsQuat⁻¹ · (absPos − parentAbsPos). This is the crux MD5
|
|
286
|
+
// skinning gets wrong two ways: setting the absolute transform directly as the local (double-accumulates →
|
|
287
|
+
// explodes under animation), or flattening the skeleton (breaks the .md5anim frames, which are
|
|
288
|
+
// parent-RELATIVE and rely on the hierarchy to compose to absolute — see parseMd5Anim). With bind
|
|
289
|
+
// converted to relative here and anim already relative, both pose the same nested joints consistently.
|
|
290
|
+
// Roots (parentIndex < 0) keep their absolute transform as local.
|
|
291
|
+
const parentConj = createQuaternion();
|
|
292
|
+
const relPos = { x: 0, y: 0, z: 0 };
|
|
293
|
+
const relQuat = createQuaternion();
|
|
294
|
+
for (let j = 0; j < joints.length; j++) {
|
|
295
|
+
const pi = j * 3;
|
|
296
|
+
const qi = j * 4;
|
|
297
|
+
const parentIndex = joints[j].parentIndex;
|
|
298
|
+
let localPx = jointPositions[pi];
|
|
299
|
+
let localPy = jointPositions[pi + 1];
|
|
300
|
+
let localPz = jointPositions[pi + 2];
|
|
301
|
+
let localQx = jointOrientations[qi];
|
|
302
|
+
let localQy = jointOrientations[qi + 1];
|
|
303
|
+
let localQz = jointOrientations[qi + 2];
|
|
304
|
+
let localQw = jointOrientations[qi + 3];
|
|
305
|
+
if (parentIndex >= 0 && parentIndex < joints.length) {
|
|
306
|
+
const ppi = parentIndex * 3;
|
|
307
|
+
const pqi = parentIndex * 4;
|
|
308
|
+
conjugateQuaternion(parentConj, {
|
|
309
|
+
w: jointOrientations[pqi + 3],
|
|
310
|
+
x: jointOrientations[pqi],
|
|
311
|
+
y: jointOrientations[pqi + 1],
|
|
312
|
+
z: jointOrientations[pqi + 2],
|
|
313
|
+
});
|
|
314
|
+
rotateVector3ByQuaternion(relPos, {
|
|
315
|
+
x: localPx - jointPositions[ppi],
|
|
316
|
+
y: localPy - jointPositions[ppi + 1],
|
|
317
|
+
z: localPz - jointPositions[ppi + 2],
|
|
318
|
+
}, parentConj);
|
|
319
|
+
multiplyQuaternion(relQuat, parentConj, { w: localQw, x: localQx, y: localQy, z: localQz });
|
|
320
|
+
localPx = relPos.x;
|
|
321
|
+
localPy = relPos.y;
|
|
322
|
+
localPz = relPos.z;
|
|
323
|
+
localQx = relQuat.x;
|
|
324
|
+
localQy = relQuat.y;
|
|
325
|
+
localQz = relQuat.z;
|
|
326
|
+
localQw = relQuat.w;
|
|
327
|
+
}
|
|
328
|
+
else if (parentIndex >= joints.length) {
|
|
329
|
+
// The joint keeps its absolute transform as local (degraded parenting) — Recover.
|
|
330
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Recover, 'md5mesh.joint-parent-out-of-range', '', {
|
|
331
|
+
firstJoint: j,
|
|
332
|
+
firstParent: parentIndex,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const transform = document.nodes[jointNodeIndices[j]].transform;
|
|
336
|
+
setVector3(transform.position, localPx, localPy, localPz);
|
|
337
|
+
setQuaternion(transform.rotation, localQx, localQy, localQz, localQw);
|
|
338
|
+
}
|
|
339
|
+
// Nest by parent index so parent × child composition reconstructs each joint's absolute world transform
|
|
340
|
+
// from the parent-relative locals set above; roots hang under the skeleton group.
|
|
341
|
+
for (let j = 0; j < joints.length; j++) {
|
|
342
|
+
const parentIndex = joints[j].parentIndex;
|
|
343
|
+
if (parentIndex >= 0 && parentIndex < joints.length) {
|
|
344
|
+
document.nodes[jointNodeIndices[parentIndex]].children.push(jointNodeIndices[j]);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
document.nodes[skeletonRootIndex].children.push(jointNodeIndices[j]);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Derive each joint's inverse-bind matrix from its ABSOLUTE (Y-up) bind world transform: inverseBind =
|
|
351
|
+
// (compose(absPos, absQuat, 1))⁻¹. MD5 joints are already absolute, so no hierarchy walk is needed — this
|
|
352
|
+
// is exactly what the live scene path produced by letting createSkeleton3D derive the palette from the
|
|
353
|
+
// joint nodes' world transforms (which recompose to these absolutes).
|
|
354
|
+
const inverseBind = [];
|
|
355
|
+
const bindWorld = createMatrix4();
|
|
356
|
+
for (let j = 0; j < joints.length; j++) {
|
|
357
|
+
const pi = j * 3;
|
|
358
|
+
const qi = j * 4;
|
|
359
|
+
composeMatrix4(bindWorld, { x: jointPositions[pi], y: jointPositions[pi + 1], z: jointPositions[pi + 2] }, {
|
|
360
|
+
w: jointOrientations[qi + 3],
|
|
361
|
+
x: jointOrientations[qi],
|
|
362
|
+
y: jointOrientations[qi + 1],
|
|
363
|
+
z: jointOrientations[qi + 2],
|
|
364
|
+
}, { x: 1, y: 1, z: 1 });
|
|
365
|
+
const inv = createMatrix4();
|
|
366
|
+
inverseMatrix4(inv, bindWorld);
|
|
367
|
+
inverseBind.push(inv);
|
|
368
|
+
}
|
|
369
|
+
return { inverseBind, joints: jointNodeIndices };
|
|
370
|
+
}
|
|
371
|
+
// The empty Scene3DDocument returned before assembly begins — every table present.
|
|
372
|
+
function emptyMd5Document() {
|
|
373
|
+
return {
|
|
374
|
+
animations: [],
|
|
375
|
+
cameras: [],
|
|
376
|
+
lights: [],
|
|
377
|
+
materials: [],
|
|
378
|
+
meshes: [],
|
|
379
|
+
metadata: null,
|
|
380
|
+
nodes: [],
|
|
381
|
+
resources: [],
|
|
382
|
+
scenes: [{ rootNodes: [] }],
|
|
383
|
+
skins: [],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
// Parses the joints { ... } block. Returns the line index after the closing brace.
|
|
387
|
+
function parseJointsBlock(lines, startLine, joints, md5Drops) {
|
|
388
|
+
let i = startLine;
|
|
389
|
+
while (i < lines.length) {
|
|
390
|
+
const line = lines[i].trim();
|
|
391
|
+
i++;
|
|
392
|
+
if (line === '}')
|
|
393
|
+
return i;
|
|
394
|
+
if (line.length === 0 || line.startsWith('//'))
|
|
395
|
+
continue;
|
|
396
|
+
const joint = parseJointLine(line, md5Drops, i - 1);
|
|
397
|
+
if (joint !== null)
|
|
398
|
+
joints.push(joint);
|
|
399
|
+
}
|
|
400
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Recover, 'md5mesh.joints-block-unclosed', '', {});
|
|
401
|
+
return i;
|
|
402
|
+
}
|
|
403
|
+
// Parses a single joint line: "name" parentIndex ( px py pz ) ( qx qy qz )
|
|
404
|
+
function parseJointLine(line, md5Drops, lineIndex) {
|
|
405
|
+
// Extract the quoted name.
|
|
406
|
+
const nameStart = line.indexOf('"');
|
|
407
|
+
const nameEnd = line.indexOf('"', nameStart + 1);
|
|
408
|
+
if (nameStart < 0 || nameEnd < 0) {
|
|
409
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.malformed-joint', 'missing-name-quotes', {
|
|
410
|
+
firstLine: lineIndex + 1,
|
|
411
|
+
reason: 'missing-name-quotes',
|
|
412
|
+
});
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const name = line.slice(nameStart + 1, nameEnd);
|
|
416
|
+
// The remainder after the closing quote contains: parentIndex ( px py pz ) ( qx qy qz )
|
|
417
|
+
const rest = line.slice(nameEnd + 1).trim();
|
|
418
|
+
const tokens = rest
|
|
419
|
+
.replace(/[()]/g, '')
|
|
420
|
+
.split(/\s+/)
|
|
421
|
+
.filter((t) => t.length > 0);
|
|
422
|
+
if (tokens.length < 7) {
|
|
423
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.malformed-joint', 'not-enough-components', {
|
|
424
|
+
firstLine: lineIndex + 1,
|
|
425
|
+
reason: 'not-enough-components',
|
|
426
|
+
});
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const parentIndex = parseInt(tokens[0], 10);
|
|
430
|
+
const positionX = parseFloat(tokens[1]);
|
|
431
|
+
const positionY = parseFloat(tokens[2]);
|
|
432
|
+
const positionZ = parseFloat(tokens[3]);
|
|
433
|
+
const orientationX = parseFloat(tokens[4]);
|
|
434
|
+
const orientationY = parseFloat(tokens[5]);
|
|
435
|
+
const orientationZ = parseFloat(tokens[6]);
|
|
436
|
+
if (!Number.isFinite(parentIndex) ||
|
|
437
|
+
!Number.isFinite(positionX) ||
|
|
438
|
+
!Number.isFinite(positionY) ||
|
|
439
|
+
!Number.isFinite(positionZ) ||
|
|
440
|
+
!Number.isFinite(orientationX) ||
|
|
441
|
+
!Number.isFinite(orientationY) ||
|
|
442
|
+
!Number.isFinite(orientationZ)) {
|
|
443
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.malformed-joint', 'non-numeric', {
|
|
444
|
+
firstLine: lineIndex + 1,
|
|
445
|
+
reason: 'non-numeric',
|
|
446
|
+
});
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
// Reconstruct quaternion W from XYZ. w = sqrt(1 - x^2 - y^2 - z^2), clamped to 0 when
|
|
450
|
+
// the squared sum exceeds 1 (numerical precision).
|
|
451
|
+
const sumSq = orientationX * orientationX + orientationY * orientationY + orientationZ * orientationZ;
|
|
452
|
+
const orientationW = sumSq < 1 ? -Math.sqrt(1 - sumSq) : 0;
|
|
453
|
+
return {
|
|
454
|
+
name,
|
|
455
|
+
orientationW,
|
|
456
|
+
orientationX,
|
|
457
|
+
orientationY,
|
|
458
|
+
orientationZ,
|
|
459
|
+
parentIndex,
|
|
460
|
+
positionX,
|
|
461
|
+
positionY,
|
|
462
|
+
positionZ,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
// Parses a mesh { ... } block. Returns the parsed mesh and the line index after the closing brace.
|
|
466
|
+
function parseMeshBlock(lines, startLine, md5Drops) {
|
|
467
|
+
let shader = '';
|
|
468
|
+
const vertices = [];
|
|
469
|
+
const weights = [];
|
|
470
|
+
const indices = [];
|
|
471
|
+
let i = startLine;
|
|
472
|
+
while (i < lines.length) {
|
|
473
|
+
const line = lines[i].trim();
|
|
474
|
+
i++;
|
|
475
|
+
if (line === '}')
|
|
476
|
+
return { nextLine: i, result: { indices, shader, vertices, weights } };
|
|
477
|
+
if (line.length === 0 || line.startsWith('//'))
|
|
478
|
+
continue;
|
|
479
|
+
if (line.startsWith('shader')) {
|
|
480
|
+
const nameStart = line.indexOf('"');
|
|
481
|
+
const nameEnd = line.indexOf('"', nameStart + 1);
|
|
482
|
+
if (nameStart >= 0 && nameEnd > nameStart) {
|
|
483
|
+
shader = line.slice(nameStart + 1, nameEnd);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
// A `shader` directive with no quoted name: the mesh is emitted WITHOUT a material (the shader path
|
|
487
|
+
// is the mesh's only material/texture reference). Recover — geometry survives, the binding is lost.
|
|
488
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Recover, 'md5mesh.shader-unquoted', '', { firstLine: i });
|
|
489
|
+
}
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (line.startsWith('numverts') || line.startsWith('numtris') || line.startsWith('numweights')) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (line.startsWith('vert ')) {
|
|
496
|
+
const vert = parseVertLine(line, md5Drops, i - 1);
|
|
497
|
+
if (vert !== null)
|
|
498
|
+
vertices.push(vert);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (line.startsWith('tri ')) {
|
|
502
|
+
const tri = parseTriLine(line, md5Drops, i - 1);
|
|
503
|
+
if (tri !== null) {
|
|
504
|
+
indices.push(tri[0], tri[1], tri[2]);
|
|
505
|
+
}
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (line.startsWith('weight ')) {
|
|
509
|
+
const weight = parseWeightLine(line, md5Drops, i - 1);
|
|
510
|
+
if (weight !== null)
|
|
511
|
+
weights.push(weight);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Recover, 'md5mesh.mesh-block-unclosed', '', {});
|
|
516
|
+
return { nextLine: i, result: { indices, shader, vertices, weights } };
|
|
517
|
+
}
|
|
518
|
+
// Parses: vert vertIndex ( u v ) startWeight countWeights
|
|
519
|
+
function parseVertLine(line, md5Drops, lineIndex) {
|
|
520
|
+
const tokens = line
|
|
521
|
+
.replace(/[()]/g, '')
|
|
522
|
+
.split(/\s+/)
|
|
523
|
+
.filter((t) => t.length > 0);
|
|
524
|
+
// tokens: ["vert", vertIndex, u, v, startWeight, countWeights]
|
|
525
|
+
if (tokens.length < 6) {
|
|
526
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.malformed-vert', 'not-enough-components', {
|
|
527
|
+
firstLine: lineIndex + 1,
|
|
528
|
+
reason: 'not-enough-components',
|
|
529
|
+
});
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
const u = parseFloat(tokens[2]);
|
|
533
|
+
const v = parseFloat(tokens[3]);
|
|
534
|
+
const startWeight = parseInt(tokens[4], 10);
|
|
535
|
+
const countWeights = parseInt(tokens[5], 10);
|
|
536
|
+
if (!Number.isFinite(u) || !Number.isFinite(v) || !Number.isFinite(startWeight) || !Number.isFinite(countWeights)) {
|
|
537
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.malformed-vert', 'non-numeric', {
|
|
538
|
+
firstLine: lineIndex + 1,
|
|
539
|
+
reason: 'non-numeric',
|
|
540
|
+
});
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
return { countWeights, startWeight, u, v };
|
|
544
|
+
}
|
|
545
|
+
// Parses: tri triIndex v0 v1 v2
|
|
546
|
+
function parseTriLine(line, md5Drops, lineIndex) {
|
|
547
|
+
const tokens = line.split(/\s+/).filter((t) => t.length > 0);
|
|
548
|
+
// tokens: ["tri", triIndex, v0, v1, v2]
|
|
549
|
+
if (tokens.length < 5) {
|
|
550
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.malformed-tri', 'not-enough-components', {
|
|
551
|
+
firstLine: lineIndex + 1,
|
|
552
|
+
reason: 'not-enough-components',
|
|
553
|
+
});
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
const v0 = parseInt(tokens[2], 10);
|
|
557
|
+
const v1 = parseInt(tokens[3], 10);
|
|
558
|
+
const v2 = parseInt(tokens[4], 10);
|
|
559
|
+
if (!Number.isFinite(v0) || !Number.isFinite(v1) || !Number.isFinite(v2)) {
|
|
560
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.malformed-tri', 'non-numeric', {
|
|
561
|
+
firstLine: lineIndex + 1,
|
|
562
|
+
reason: 'non-numeric',
|
|
563
|
+
});
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
return [v0, v1, v2];
|
|
567
|
+
}
|
|
568
|
+
// Parses: weight weightIndex jointIndex bias ( px py pz )
|
|
569
|
+
function parseWeightLine(line, md5Drops, lineIndex) {
|
|
570
|
+
const tokens = line
|
|
571
|
+
.replace(/[()]/g, '')
|
|
572
|
+
.split(/\s+/)
|
|
573
|
+
.filter((t) => t.length > 0);
|
|
574
|
+
// tokens: ["weight", weightIndex, jointIndex, bias, px, py, pz]
|
|
575
|
+
if (tokens.length < 7) {
|
|
576
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.malformed-weight', 'not-enough-components', {
|
|
577
|
+
firstLine: lineIndex + 1,
|
|
578
|
+
reason: 'not-enough-components',
|
|
579
|
+
});
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
const jointIndex = parseInt(tokens[2], 10);
|
|
583
|
+
const bias = parseFloat(tokens[3]);
|
|
584
|
+
const positionX = parseFloat(tokens[4]);
|
|
585
|
+
const positionY = parseFloat(tokens[5]);
|
|
586
|
+
const positionZ = parseFloat(tokens[6]);
|
|
587
|
+
if (!Number.isFinite(jointIndex) ||
|
|
588
|
+
!Number.isFinite(bias) ||
|
|
589
|
+
!Number.isFinite(positionX) ||
|
|
590
|
+
!Number.isFinite(positionY) ||
|
|
591
|
+
!Number.isFinite(positionZ)) {
|
|
592
|
+
tallyMd5Drop(md5Drops, ImportDiagnosticSeverity.Drop, 'md5mesh.malformed-weight', 'non-numeric', {
|
|
593
|
+
firstLine: lineIndex + 1,
|
|
594
|
+
reason: 'non-numeric',
|
|
595
|
+
});
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
return { bias, jointIndex, positionX, positionY, positionZ };
|
|
599
|
+
}
|
|
600
|
+
// Rotates a vector (vx, vy, vz) by quaternion (qx, qy, qz, qw) and returns the X component.
|
|
601
|
+
// Formula: t = 2 * cross(q.xyz, v); result = v + qw * t + cross(q.xyz, t)
|
|
602
|
+
function quatRotateVec3X(qx, qy, qz, qw, vx, vy, vz) {
|
|
603
|
+
const tx = 2 * (qy * vz - qz * vy);
|
|
604
|
+
const ty = 2 * (qz * vx - qx * vz);
|
|
605
|
+
const tz = 2 * (qx * vy - qy * vx);
|
|
606
|
+
return vx + qw * tx + (qy * tz - qz * ty);
|
|
607
|
+
}
|
|
608
|
+
// Returns the Y component of quaternion-rotated vector.
|
|
609
|
+
function quatRotateVec3Y(qx, qy, qz, qw, vx, vy, vz) {
|
|
610
|
+
const tx = 2 * (qy * vz - qz * vy);
|
|
611
|
+
const ty = 2 * (qz * vx - qx * vz);
|
|
612
|
+
const tz = 2 * (qx * vy - qy * vx);
|
|
613
|
+
return vy + qw * ty + (qz * tx - qx * tz);
|
|
614
|
+
}
|
|
615
|
+
// Returns the Z component of quaternion-rotated vector.
|
|
616
|
+
function quatRotateVec3Z(qx, qy, qz, qw, vx, vy, vz) {
|
|
617
|
+
const tx = 2 * (qy * vz - qz * vy);
|
|
618
|
+
const ty = 2 * (qz * vx - qx * vz);
|
|
619
|
+
const tz = 2 * (qx * vy - qy * vx);
|
|
620
|
+
return vz + qw * tz + (qx * ty - qy * tx);
|
|
621
|
+
}
|
|
622
|
+
// Records one offender against its (kind, discriminator) tally — the aggregate-once alternative to a
|
|
623
|
+
// per-line/per-index `reportImportDiagnostic`. No-op (never allocates) when no collector is engaged.
|
|
624
|
+
// `firstDetail` is kept from the FIRST offender; later ones only bump the count.
|
|
625
|
+
function tallyMd5Drop(tallies, severity, kind, discriminator, firstDetail) {
|
|
626
|
+
if (tallies === null)
|
|
627
|
+
return;
|
|
628
|
+
const key = `${kind}|${discriminator}`;
|
|
629
|
+
const existing = tallies.get(key);
|
|
630
|
+
if (existing === undefined)
|
|
631
|
+
tallies.set(key, { count: 1, detail: firstDetail, kind, severity });
|
|
632
|
+
else
|
|
633
|
+
existing.count++;
|
|
634
|
+
}
|
|
635
|
+
//# sourceMappingURL=md5Parse.js.map
|