@babylonjs/loaders 9.26.0 → 9.26.1

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.
Files changed (72) hide show
  1. package/FBX/fbxConstraintBehavior.d.ts +188 -0
  2. package/FBX/fbxConstraintBehavior.js +532 -0
  3. package/FBX/fbxConstraintBehavior.js.map +1 -0
  4. package/FBX/fbxFileLoader.pure.d.ts +175 -6
  5. package/FBX/fbxFileLoader.pure.js +1521 -286
  6. package/FBX/fbxFileLoader.pure.js.map +1 -1
  7. package/FBX/index.d.ts +4 -1
  8. package/FBX/index.js +1 -0
  9. package/FBX/index.js.map +1 -1
  10. package/FBX/interpreter/animation.d.ts +73 -33
  11. package/FBX/interpreter/animation.js +620 -223
  12. package/FBX/interpreter/animation.js.map +1 -1
  13. package/FBX/interpreter/animationCurve.d.ts +91 -0
  14. package/FBX/interpreter/animationCurve.js +623 -0
  15. package/FBX/interpreter/animationCurve.js.map +1 -0
  16. package/FBX/interpreter/blendShapes.js +9 -18
  17. package/FBX/interpreter/blendShapes.js.map +1 -1
  18. package/FBX/interpreter/connections.d.ts +1 -1
  19. package/FBX/interpreter/connections.js +173 -34
  20. package/FBX/interpreter/connections.js.map +1 -1
  21. package/FBX/interpreter/constraints.d.ts +68 -0
  22. package/FBX/interpreter/constraints.js +143 -0
  23. package/FBX/interpreter/constraints.js.map +1 -0
  24. package/FBX/interpreter/fbxInterpreter.d.ts +89 -4
  25. package/FBX/interpreter/fbxInterpreter.js +474 -122
  26. package/FBX/interpreter/fbxInterpreter.js.map +1 -1
  27. package/FBX/interpreter/geometry.d.ts +1 -1
  28. package/FBX/interpreter/geometry.js +150 -92
  29. package/FBX/interpreter/geometry.js.map +1 -1
  30. package/FBX/interpreter/legacyDocument.d.ts +11 -0
  31. package/FBX/interpreter/legacyDocument.js +425 -0
  32. package/FBX/interpreter/legacyDocument.js.map +1 -0
  33. package/FBX/interpreter/materialModel.d.ts +71 -0
  34. package/FBX/interpreter/materialModel.js +622 -0
  35. package/FBX/interpreter/materialModel.js.map +1 -0
  36. package/FBX/interpreter/materials.d.ts +13 -1
  37. package/FBX/interpreter/materials.js +88 -5
  38. package/FBX/interpreter/materials.js.map +1 -1
  39. package/FBX/interpreter/nodeTransform.d.ts +32 -0
  40. package/FBX/interpreter/nodeTransform.js +54 -0
  41. package/FBX/interpreter/nodeTransform.js.map +1 -0
  42. package/FBX/interpreter/nurbs.d.ts +99 -0
  43. package/FBX/interpreter/nurbs.js +627 -0
  44. package/FBX/interpreter/nurbs.js.map +1 -0
  45. package/FBX/interpreter/propertyTemplates.d.ts +23 -0
  46. package/FBX/interpreter/propertyTemplates.js +109 -2
  47. package/FBX/interpreter/propertyTemplates.js.map +1 -1
  48. package/FBX/interpreter/rig.js +16 -0
  49. package/FBX/interpreter/rig.js.map +1 -1
  50. package/FBX/interpreter/sceneDiagnostics.js +28 -3
  51. package/FBX/interpreter/sceneDiagnostics.js.map +1 -1
  52. package/FBX/interpreter/skeleton.d.ts +4 -18
  53. package/FBX/interpreter/skeleton.js +16 -94
  54. package/FBX/interpreter/skeleton.js.map +1 -1
  55. package/FBX/parsers/fbxAsciiParser.js +119 -9
  56. package/FBX/parsers/fbxAsciiParser.js.map +1 -1
  57. package/FBX/parsers/fbxBinaryParser.js +74 -22
  58. package/FBX/parsers/fbxBinaryParser.js.map +1 -1
  59. package/FBX/pure.d.ts +1 -0
  60. package/FBX/pure.js +1 -0
  61. package/FBX/pure.js.map +1 -1
  62. package/FBX/types/fbxTypes.d.ts +18 -2
  63. package/FBX/types/fbxTypes.js +40 -12
  64. package/FBX/types/fbxTypes.js.map +1 -1
  65. package/USD/usdCommandProtocol.d.ts +20 -4
  66. package/USD/usdCommandProtocol.js +25 -3
  67. package/USD/usdCommandProtocol.js.map +1 -1
  68. package/USD/usdFileLoader.pure.js +2 -0
  69. package/USD/usdFileLoader.pure.js.map +1 -1
  70. package/USD/usdSceneMaterializer.js +430 -92
  71. package/USD/usdSceneMaterializer.js.map +1 -1
  72. package/package.json +3 -3
@@ -1,32 +1,32 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention, jsdoc/require-param, jsdoc/require-returns */
2
- import { findChildByName, getPropertyValue, cleanFBXName } from "../types/fbxTypes.js";
2
+ import { findChildByName, findDocumentNode, getNodeArray, getPropertyValue, cleanFBXName } from "../types/fbxTypes.js";
3
3
  import { getChildren } from "./connections.js";
4
- /** FBX time units: 46186158000 ticks per second */
5
- const FBX_TIME_UNIT = 46186158000;
4
+ import { getPropertyEntries } from "./propertyTemplates.js";
5
+ import { FBX_TIME_UNIT, buildKeyframes, evaluateCurve, keyframesFromSlopes, parseExtrapolation, combineLayerValue, } from "./animationCurve.js";
6
+ export { FBX_TIME_UNIT } from "./animationCurve.js";
7
+ export { combineLayerValue, evaluateCurve, eulerToQuat, quatToEuler } from "./animationCurve.js";
6
8
  const KEY_ATTR_DATA_STRIDE = 4;
7
- const SAMPLED_CURVE_MIN_KEY_COUNT = 8;
8
- const SAMPLED_CURVE_MAX_INTERVAL_SECONDS = 1 / 23;
9
- const SAMPLED_CURVE_UNIFORM_TOLERANCE_RATIO = 0.05;
10
- const SAMPLED_CURVE_LINEAR_DEVIATION_RATIO = 0.01;
11
- const SAMPLED_CURVE_LINEAR_DEVIATION_ABSOLUTE = 1e-4;
12
- const SAMPLED_CURVE_DEGENERATE_SLOPE_ABSOLUTE = 1e-5;
13
- const SAMPLED_CURVE_COMMON_FPS = [24, 25, 30, 48, 50, 60, 100, 120];
14
9
  /**
15
10
  * Extract all animation stacks from the FBX scene.
16
11
  */
17
- export function extractAnimations(objectMap) {
12
+ export function extractAnimations(objectMap, doc, options = {}) {
18
13
  const stacks = [];
14
+ const rebaseKeyframes = options.rebaseKeyframes ?? true;
19
15
  for (const [id, node] of Array.from(objectMap.objects)) {
20
16
  if (node.name === "AnimationStack") {
21
- const stack = extractAnimStack(id, node, objectMap);
17
+ const stack = extractAnimStack(id, node, objectMap, rebaseKeyframes);
22
18
  if (stack) {
23
19
  stacks.push(stack);
24
20
  }
25
21
  }
26
22
  }
23
+ // Pre-7000 files have no AnimationStack objects; their animation lives in the top-level Takes block.
24
+ if (stacks.length === 0 && doc) {
25
+ stacks.push(...extractLegacyTakes(doc, objectMap, rebaseKeyframes));
26
+ }
27
27
  return stacks;
28
28
  }
29
- function extractAnimStack(stackId, stackNode, objectMap) {
29
+ function extractAnimStack(stackId, stackNode, objectMap, rebaseKeyframes) {
30
30
  const name = cleanFBXName(getPropertyValue(stackNode, 1) ?? "Animation");
31
31
  const declaredTimeSpan = extractAnimationStackTimeSpan(stackNode);
32
32
  // Find AnimationLayer children of this stack
@@ -40,43 +40,49 @@ function extractAnimStack(stackId, stackNode, objectMap) {
40
40
  const layers = [];
41
41
  const diagnostics = [];
42
42
  let minTime = Infinity;
43
- let maxTime = 0;
43
+ let maxTime = -Infinity;
44
44
  for (const { id: layerId, node: layerNode } of layerEntries) {
45
45
  // Extract layer properties
46
46
  const layerName = cleanFBXName(getPropertyValue(layerNode, 1) ?? "Layer");
47
47
  let weight = 100;
48
48
  let blendMode = 0;
49
- const props70 = findChildByName(layerNode, "Properties70");
50
- if (props70) {
51
- for (const p of props70.children) {
52
- if (p.name !== "P") {
53
- continue;
54
- }
55
- const pName = getPropertyValue(p, 0);
56
- if (pName === "Weight") {
57
- const v = p.properties[4]?.value;
58
- if (typeof v === "number") {
59
- weight = v;
60
- }
61
- }
62
- else if (pName === "BlendMode") {
63
- const v = p.properties[4]?.value;
64
- if (typeof v === "number") {
65
- blendMode = v;
66
- }
67
- }
49
+ let rotationAccumulationMode = 0;
50
+ let scaleAccumulationMode = 0;
51
+ for (const entry of getPropertyEntries(layerNode)) {
52
+ const v = entry.values[0];
53
+ if (typeof v !== "number") {
54
+ continue;
55
+ }
56
+ if (entry.name === "Weight") {
57
+ weight = v;
58
+ }
59
+ else if (entry.name === "BlendMode") {
60
+ blendMode = v;
61
+ }
62
+ else if (entry.name === "RotationAccumulationMode") {
63
+ rotationAccumulationMode = v;
64
+ }
65
+ else if (entry.name === "ScaleAccumulationMode") {
66
+ scaleAccumulationMode = v;
68
67
  }
69
68
  }
69
+ const layerIndex = layers.length;
70
70
  // AnimationCurveNodes are children of the layer
71
71
  const curveNodeEntries = getChildren(objectMap, layerId, "AnimationCurveNode");
72
72
  const layerCurveNodes = [];
73
73
  const layerUnsupportedCurveNodes = [];
74
74
  const layerDiagnostics = [];
75
+ let layerWeightCurve;
75
76
  for (const { id: curveNodeId, node: curveNodeNode } of curveNodeEntries) {
76
- const curveNodeData = extractCurveNode(curveNodeId, curveNodeNode, objectMap);
77
+ const curveNodeData = extractCurveNode(curveNodeId, curveNodeNode, objectMap, layerIndex);
77
78
  if (!curveNodeData) {
78
- const unsupported = extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap);
79
+ const unsupported = extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap, layerIndex);
79
80
  if (unsupported) {
81
+ // The layer's own Weight property can be animated; it drives blending rather than a target.
82
+ if (unsupported.targetId === layerId && unsupported.propertyName === "Weight" && unsupported.curves.length > 0) {
83
+ layerWeightCurve = unsupported.curves[0];
84
+ continue;
85
+ }
80
86
  scanCurveTimes(unsupported.curves, (time) => {
81
87
  if (time < minTime) {
82
88
  minTime = time;
@@ -119,6 +125,8 @@ function extractAnimStack(stackId, stackNode, objectMap) {
119
125
  weight,
120
126
  normalizedWeight: weight / 100,
121
127
  blendMode,
128
+ blend: makeLayerBlend(blendMode, weight, rotationAccumulationMode, scaleAccumulationMode),
129
+ weightCurve: layerWeightCurve,
122
130
  curveNodes: layerCurveNodes,
123
131
  unsupportedCurveNodes: layerUnsupportedCurveNodes,
124
132
  diagnostics: layerDiagnostics,
@@ -127,33 +135,15 @@ function extractAnimStack(stackId, stackNode, objectMap) {
127
135
  if (allCurveNodes.length === 0 && allUnsupportedCurveNodes.length === 0) {
128
136
  return null;
129
137
  }
130
- if (layers.length > 1) {
131
- diagnostics.push({
132
- type: "multiple-animation-layers",
133
- message: "Multiple animation layers are preserved, but runtime blending is not yet evaluated.",
134
- });
135
- }
136
- for (const layer of layers) {
137
- if (layer.blendMode !== 0) {
138
- const diagnostic = {
139
- type: "unsupported-layer-blend-mode",
140
- message: `Animation layer blend mode ${layer.blendMode} is preserved but not yet blended at runtime.`,
141
- layerName: layer.name,
142
- };
143
- layer.diagnostics.push(diagnostic);
144
- diagnostics.push(diagnostic);
145
- }
146
- if (layer.weight !== 100) {
147
- const diagnostic = {
148
- type: "partial-layer-weight",
149
- message: `Animation layer weight ${layer.weight} is preserved but not yet applied at runtime.`,
150
- layerName: layer.name,
151
- };
152
- layer.diagnostics.push(diagnostic);
153
- diagnostics.push(diagnostic);
154
- }
155
- }
156
- const timeOffset = minTime > 0 && isFinite(minTime) ? minTime : 0;
138
+ return finalizeAnimStack(name, layers, allCurveNodes, allUnsupportedCurveNodes, diagnostics, minTime, maxTime, declaredTimeSpan, rebaseKeyframes);
139
+ }
140
+ /**
141
+ * Resolves the clip span from the declared time span or the key range and, when requested, rebases key times so
142
+ * the first key sits at zero.
143
+ */
144
+ function finalizeAnimStack(name, layers, allCurveNodes, allUnsupportedCurveNodes, diagnostics, minTime, maxTime, declaredTimeSpan, rebaseKeyframes) {
145
+ const hasKeys = isFinite(minTime) && isFinite(maxTime);
146
+ const timeOffset = rebaseKeyframes && hasKeys && minTime > 0 ? minTime : 0;
157
147
  // Rebase all keyframe times so the animation starts at 0
158
148
  if (timeOffset > 0) {
159
149
  for (const cn of allCurveNodes) {
@@ -172,11 +162,14 @@ function extractAnimStack(stackId, stackNode, objectMap) {
172
162
  }
173
163
  maxTime -= timeOffset;
174
164
  }
175
- const declaredStart = declaredTimeSpan ? Math.max(declaredTimeSpan.start - timeOffset, 0) : 0;
165
+ const declaredStart = declaredTimeSpan ? (rebaseKeyframes ? Math.max(declaredTimeSpan.start - timeOffset, 0) : declaredTimeSpan.start) : 0;
176
166
  const declaredStop = declaredTimeSpan ? Math.max(declaredTimeSpan.stop - timeOffset, declaredStart) : 0;
177
167
  const hasDeclaredDuration = declaredStop > declaredStart;
178
- const startTime = hasDeclaredDuration ? declaredStart : 0;
179
- const stopTime = hasDeclaredDuration ? declaredStop : maxTime;
168
+ // Without a declared span the clip covers its keys (a clip whose keys are all negative ends before zero).
169
+ const keyStart = hasKeys ? minTime - timeOffset : 0;
170
+ const keyStop = hasKeys ? maxTime : 0;
171
+ const startTime = hasDeclaredDuration ? declaredStart : rebaseKeyframes ? Math.min(0, keyStart) : keyStart;
172
+ const stopTime = hasDeclaredDuration ? declaredStop : Math.max(keyStop, startTime);
180
173
  return {
181
174
  name,
182
175
  startTime,
@@ -209,24 +202,39 @@ function extractAnimationStackTimeSpan(stackNode) {
209
202
  }
210
203
  return stop !== null ? { start, stop } : null;
211
204
  }
212
- function extractCurveNode(curveNodeId, curveNodeNode, objectMap) {
205
+ function extractCurveNode(curveNodeId, curveNodeNode, objectMap, layerIndex) {
213
206
  const typeName = cleanFBXName(getPropertyValue(curveNodeNode, 1) ?? "");
207
+ const rawDefaults = extractCurveNodeDefaultValues(curveNodeNode);
208
+ const defaultValues = "d|X" in rawDefaults || "d|Y" in rawDefaults || "d|Z" in rawDefaults ? [rawDefaults["d|X"] ?? 0, rawDefaults["d|Y"] ?? 0, rawDefaults["d|Z"] ?? 0] : undefined;
214
209
  // Handle T (translation), R (rotation), S (scale) targeting Models
215
210
  if (typeName === "T" || typeName === "R" || typeName === "S") {
216
211
  const targetModelId = findCurveNodeTarget(curveNodeId, objectMap);
217
212
  if (targetModelId === null) {
218
213
  return null;
219
214
  }
215
+ // A curve node without curves still contributes its default values to layer blending (Maya writes such
216
+ // nodes on the base layer when only additive layers animate a property).
220
217
  const curves = extractCurves(curveNodeId, objectMap);
221
- if (curves.length === 0) {
218
+ if (curves.length === 0 && !defaultValues) {
222
219
  return null;
223
220
  }
224
221
  return {
225
222
  type: typeName,
226
223
  targetModelId,
227
224
  curves,
225
+ layerIndex,
226
+ defaultValues,
228
227
  };
229
228
  }
229
+ // FBX 7.1 writes blend shape weight curves against the geometry, named after the shape channel
230
+ const geometryChannelId = findCurveNodeGeometryShapeChannel(curveNodeId, objectMap);
231
+ if (geometryChannelId !== null) {
232
+ const curves = extractCurves(curveNodeId, objectMap);
233
+ if (curves.length === 0) {
234
+ return null;
235
+ }
236
+ return { type: "DeformPercent", targetModelId: geometryChannelId, curves, layerIndex, defaultValues };
237
+ }
230
238
  // Handle DeformPercent targeting BlendShapeChannels
231
239
  if (typeName === "DeformPercent") {
232
240
  const targetId = findCurveNodeBlendShapeTarget(curveNodeId, objectMap);
@@ -241,11 +249,13 @@ function extractCurveNode(curveNodeId, curveNodeNode, objectMap) {
241
249
  type: "DeformPercent",
242
250
  targetModelId: targetId,
243
251
  curves,
252
+ layerIndex,
253
+ defaultValues,
244
254
  };
245
255
  }
246
256
  return null;
247
257
  }
248
- function extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap) {
258
+ function extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap, layerIndex) {
249
259
  const typeName = cleanFBXName(getPropertyValue(curveNodeNode, 1) ?? "");
250
260
  const curves = extractCurves(curveNodeId, objectMap);
251
261
  const defaultValues = extractCurveNodeDefaultValues(curveNodeNode);
@@ -264,6 +274,7 @@ function extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap) {
264
274
  return {
265
275
  type: typeName,
266
276
  id: curveNodeId,
277
+ layerIndex,
267
278
  targetId,
268
279
  propertyName,
269
280
  curveCount: curves.length,
@@ -295,6 +306,29 @@ function findCurveNodeTarget(curveNodeId, objectMap) {
295
306
  }
296
307
  return null;
297
308
  }
309
+ /** When a curve node is connected to a Geometry through a property named like one of its blend shape channels. */
310
+ function findCurveNodeGeometryShapeChannel(curveNodeId, objectMap) {
311
+ for (const conn of objectMap.connections) {
312
+ if (conn.childId !== curveNodeId || conn.type !== "OP" || !conn.propertyName) {
313
+ continue;
314
+ }
315
+ const parentNode = objectMap.objects.get(conn.parentId);
316
+ if (!parentNode || parentNode.name !== "Geometry") {
317
+ continue;
318
+ }
319
+ for (const deformer of getChildren(objectMap, conn.parentId, "Deformer")) {
320
+ if (getPropertyValue(deformer.node, 2) !== "BlendShape") {
321
+ continue;
322
+ }
323
+ for (const channel of getChildren(objectMap, deformer.id, "Deformer")) {
324
+ if (getPropertyValue(channel.node, 2) === "BlendShapeChannel" && cleanFBXName(getPropertyValue(channel.node, 1) ?? "") === conn.propertyName) {
325
+ return channel.id;
326
+ }
327
+ }
328
+ }
329
+ }
330
+ return null;
331
+ }
298
332
  /**
299
333
  * Find the BlendShapeChannel that a DeformPercent AnimationCurveNode targets.
300
334
  */
@@ -340,8 +374,7 @@ function extractCurves(curveNodeId, objectMap) {
340
374
  const channel = conn.propertyName ?? "d|X";
341
375
  const keys = extractKeyframes(curveNode);
342
376
  if (keys.length > 0) {
343
- const isSampled = isSampledAnimationCurve(curveNode, keys);
344
- curves.push({ channel, keys: isSampled ? makeLinearSampleKeys(keys) : keys, isSampled });
377
+ curves.push({ channel, keys, ...extractCurveExtrapolation(curveNode) });
345
378
  }
346
379
  }
347
380
  }
@@ -353,8 +386,7 @@ function extractCurves(curveNodeId, objectMap) {
353
386
  for (let i = 0; i < ooChildren.length && i < 3; i++) {
354
387
  const keys = extractKeyframes(ooChildren[i].node);
355
388
  if (keys.length > 0) {
356
- const isSampled = isSampledAnimationCurve(ooChildren[i].node, keys);
357
- curves.push({ channel: channelNames[i], keys: isSampled ? makeLinearSampleKeys(keys) : keys, isSampled });
389
+ curves.push({ channel: channelNames[i], keys, ...extractCurveExtrapolation(ooChildren[i].node) });
358
390
  }
359
391
  }
360
392
  }
@@ -387,11 +419,11 @@ function extractKeyframes(curveNode) {
387
419
  if (!keyTimeNode || !keyValueNode) {
388
420
  return [];
389
421
  }
390
- const keyTimes = toInt64Array(keyTimeNode.properties[0]?.value);
391
- const keyValues = toFloat32Array(keyValueNode.properties[0]?.value);
392
- const keyAttrFlags = toInt32Array(findChildByName(curveNode, "KeyAttrFlags")?.properties[0]?.value);
393
- const keyAttrData = toFloat32Array(findChildByName(curveNode, "KeyAttrDataFloat")?.properties[0]?.value);
394
- const keyAttrRefCount = toInt32Array(findChildByName(curveNode, "KeyAttrRefCount")?.properties[0]?.value);
422
+ const keyTimes = toInt64Array(getNodeArray(keyTimeNode));
423
+ const keyValues = toFloat32Array(getNodeArray(keyValueNode));
424
+ const keyAttrFlags = toInt32Array(getNodeArray(findChildByName(curveNode, "KeyAttrFlags")));
425
+ const keyAttrData = toFloat32Array(getNodeArray(findChildByName(curveNode, "KeyAttrDataFloat")));
426
+ const keyAttrRefCount = toInt32Array(getNodeArray(findChildByName(curveNode, "KeyAttrRefCount")));
395
427
  if (!keyTimes || !keyValues) {
396
428
  return [];
397
429
  }
@@ -399,109 +431,50 @@ function extractKeyframes(curveNode) {
399
431
  return [];
400
432
  }
401
433
  const keyAttributeIndices = buildKeyAttributeIndices(keyTimes.length, keyAttrFlags, keyAttrRefCount);
402
- const keys = [];
403
- for (let i = 0; i < keyTimes.length; i++) {
434
+ return buildKeyframes(keyTimes, keyValues, (i) => {
404
435
  const attrIndex = keyAttributeIndices[i];
405
436
  const flag = attrIndex >= 0 ? (keyAttrFlags?.[attrIndex] ?? 0) : 0;
406
437
  const dataOffset = attrIndex * KEY_ATTR_DATA_STRIDE;
407
- keys.push({
408
- time: Number(keyTimes[i]) / FBX_TIME_UNIT,
409
- value: keyValues[i],
410
- interpolation: getInterpolationType(flag),
411
- constantMode: (flag & 0x00000100) !== 0 ? "next" : "standard",
412
- rightSlope: getFiniteKeyAttrData(keyAttrData, dataOffset),
413
- nextLeftSlope: getFiniteKeyAttrData(keyAttrData, dataOffset + 1),
414
- });
415
- }
416
- return keys;
438
+ return {
439
+ flags: flag,
440
+ data: [
441
+ getFiniteKeyAttrData(keyAttrData, dataOffset) ?? 0,
442
+ getFiniteKeyAttrData(keyAttrData, dataOffset + 1) ?? 0,
443
+ getFiniteKeyAttrData(keyAttrData, dataOffset + 2) ?? 0,
444
+ getFiniteKeyAttrData(keyAttrData, dataOffset + 3) ?? 0,
445
+ ],
446
+ };
447
+ });
417
448
  }
418
- function isSampledAnimationCurve(curveNode, keys) {
419
- const rawName = getPropertyValue(curveNode, 1) ?? "";
420
- return cleanFBXName(rawName) === "FbxMayaSample Curve" || isFrameBakedSampledCurve(keys);
449
+ function extractCurveExtrapolation(curveNode) {
450
+ const read = (name) => {
451
+ const node = findChildByName(curveNode, name);
452
+ return parseExtrapolation(findChildByName(node ?? curveNode, "Type")?.properties[0]?.value, findChildByName(node ?? curveNode, "Repetition")?.properties[0]?.value);
453
+ };
454
+ return { preExtrapolation: read("Pre-Extrapolation"), postExtrapolation: read("Post-Extrapolation") };
421
455
  }
422
- /**
423
- * Determines whether a key sequence appears to be a uniformly frame-baked sampled curve.
424
- * @param keys - Keyframes to inspect
425
- * @returns true if the keys look like sampled frame data rather than authored interpolation
426
- */
427
- export function isFrameBakedSampledCurve(keys) {
428
- if (keys.length < SAMPLED_CURVE_MIN_KEY_COUNT) {
429
- return false;
430
- }
431
- const deltas = [];
432
- for (let i = 1; i < keys.length; i++) {
433
- const delta = keys[i].time - keys[i - 1].time;
434
- if (!(delta > 0)) {
435
- return false;
436
- }
437
- deltas.push(delta);
438
- }
439
- const averageDelta = deltas.reduce((sum, delta) => sum + delta, 0) / deltas.length;
440
- if (averageDelta > SAMPLED_CURVE_MAX_INTERVAL_SECONDS) {
441
- return false;
442
- }
443
- const uniformTolerance = Math.max(1e-6, averageDelta * SAMPLED_CURVE_UNIFORM_TOLERANCE_RATIO);
444
- if (deltas.some((delta) => Math.abs(delta - averageDelta) > uniformTolerance)) {
445
- return false;
446
- }
447
- const sampledFps = 1 / averageDelta;
448
- const matchesCommonFps = SAMPLED_CURVE_COMMON_FPS.some((fps) => Math.abs(sampledFps - fps) <= Math.max(0.25, fps * 0.02));
449
- if (!matchesCommonFps) {
450
- return false;
451
- }
452
- return !hasMeaningfulCubicTangents(keys);
453
- }
454
- function makeLinearSampleKeys(keys) {
455
- return keys.map((key) => ({
456
- time: key.time,
457
- value: key.value,
458
- interpolation: "linear",
459
- }));
460
- }
461
- function hasMeaningfulCubicTangents(keys) {
462
- let hasCubicSegment = false;
463
- let hasCompleteTangents = true;
464
- let allSlopesDegenerate = true;
465
- let minValue = Number.POSITIVE_INFINITY;
466
- let maxValue = Number.NEGATIVE_INFINITY;
467
- let maxLinearDeviation = 0;
468
- for (const key of keys) {
469
- minValue = Math.min(minValue, key.value);
470
- maxValue = Math.max(maxValue, key.value);
471
- }
472
- for (let i = 0; i < keys.length - 1; i++) {
473
- const key = keys[i];
474
- const nextKey = keys[i + 1];
475
- if (key.interpolation !== "cubic") {
476
- continue;
477
- }
478
- hasCubicSegment = true;
479
- const segmentDuration = nextKey.time - key.time;
480
- if (!(segmentDuration > 0)) {
481
- continue;
482
- }
483
- const linearSlope = (nextKey.value - key.value) / segmentDuration;
484
- const rightSlope = key.rightSlope;
485
- const nextLeftSlope = key.nextLeftSlope;
486
- if (rightSlope === undefined || nextLeftSlope === undefined) {
487
- hasCompleteTangents = false;
488
- continue;
489
- }
490
- if (Math.abs(rightSlope) > SAMPLED_CURVE_DEGENERATE_SLOPE_ABSOLUTE || Math.abs(nextLeftSlope) > SAMPLED_CURVE_DEGENERATE_SLOPE_ABSOLUTE) {
491
- allSlopesDegenerate = false;
492
- }
493
- for (const t of [0.25, 0.5, 0.75]) {
494
- const cubic = cubicHermite(key.value, nextKey.value, rightSlope, nextLeftSlope, segmentDuration, t);
495
- const linear = key.value + t * segmentDuration * linearSlope;
496
- maxLinearDeviation = Math.max(maxLinearDeviation, Math.abs(cubic - linear));
497
- }
456
+ function makeLayerBlend(blendMode, weight, rotationAccumulationMode, scaleAccumulationMode) {
457
+ let blended = false;
458
+ let additive = false;
459
+ switch (blendMode) {
460
+ case 0:
461
+ blended = true;
462
+ additive = true;
463
+ break;
464
+ case 2:
465
+ blended = true;
466
+ break;
467
+ default:
468
+ break;
469
+ }
470
+ let w = weight / 100;
471
+ if (w < 0) {
472
+ w = 0;
498
473
  }
499
- if (!hasCubicSegment || !hasCompleteTangents || allSlopesDegenerate) {
500
- return false;
474
+ if (w > 0.99999) {
475
+ w = 1;
501
476
  }
502
- const range = maxValue - minValue;
503
- const deviationTolerance = Math.max(SAMPLED_CURVE_LINEAR_DEVIATION_ABSOLUTE, range * SAMPLED_CURVE_LINEAR_DEVIATION_RATIO);
504
- return maxLinearDeviation > deviationTolerance;
477
+ return { blended, additive, composeRotation: rotationAccumulationMode === 0, composeScale: scaleAccumulationMode === 0, weight: w };
505
478
  }
506
479
  /**
507
480
  * Samples an FBX animation curve at a specific time.
@@ -513,36 +486,7 @@ export function sampleFBXCurveAtTime(curveData, time) {
513
486
  if (!curveData || curveData.keys.length === 0) {
514
487
  return null;
515
488
  }
516
- const keys = curveData.keys;
517
- if (time <= keys[0].time) {
518
- return keys[0].value;
519
- }
520
- if (time >= keys[keys.length - 1].time) {
521
- return keys[keys.length - 1].value;
522
- }
523
- for (let i = 0; i < keys.length - 1; i++) {
524
- const key = keys[i];
525
- const nextKey = keys[i + 1];
526
- if (time < key.time || time > nextKey.time) {
527
- continue;
528
- }
529
- if (nextKey.time === key.time) {
530
- return key.value;
531
- }
532
- if (key.interpolation === "constant") {
533
- return key.constantMode === "next" ? nextKey.value : key.value;
534
- }
535
- const segmentDuration = nextKey.time - key.time;
536
- const t = (time - key.time) / segmentDuration;
537
- if (key.interpolation === "cubic" && !curveData.isSampled) {
538
- const linearSlope = (nextKey.value - key.value) / segmentDuration;
539
- const rightSlope = key.rightSlope ?? linearSlope;
540
- const nextLeftSlope = key.nextLeftSlope ?? linearSlope;
541
- return cubicHermite(key.value, nextKey.value, rightSlope, nextLeftSlope, segmentDuration, t);
542
- }
543
- return key.value + t * (nextKey.value - key.value);
544
- }
545
- return keys[keys.length - 1].value;
489
+ return evaluateCurve(curveData, time, curveData.keys[0].value);
546
490
  }
547
491
  // ── Utilities ──────────────────────────────────────────────────────────────────
548
492
  function toInt64Array(value) {
@@ -617,18 +561,6 @@ function buildKeyAttributeIndices(keyCount, keyAttrFlags, keyAttrRefCount) {
617
561
  }
618
562
  return Array.from({ length: keyCount }, (_, i) => Math.min(i, keyAttrFlags.length - 1));
619
563
  }
620
- function getInterpolationType(flag) {
621
- if ((flag & 0x00000008) !== 0) {
622
- return "cubic";
623
- }
624
- if ((flag & 0x00000004) !== 0) {
625
- return "linear";
626
- }
627
- if ((flag & 0x00000002) !== 0) {
628
- return "constant";
629
- }
630
- return "linear";
631
- }
632
564
  function getFiniteKeyAttrData(keyAttrData, index) {
633
565
  if (!keyAttrData || index < 0 || index >= keyAttrData.length) {
634
566
  return undefined;
@@ -636,13 +568,478 @@ function getFiniteKeyAttrData(keyAttrData, index) {
636
568
  const value = keyAttrData[index];
637
569
  return Number.isFinite(value) ? value : undefined;
638
570
  }
639
- function cubicHermite(value0, value1, slope0, slope1, segmentDuration, t) {
640
- const t2 = t * t;
641
- const t3 = t2 * t;
642
- const h00 = 2 * t3 - 3 * t2 + 1;
643
- const h10 = t3 - 2 * t2 + t;
644
- const h01 = -2 * t3 + 3 * t2;
645
- const h11 = t3 - t2;
646
- return h00 * value0 + h10 * segmentDuration * slope0 + h01 * value1 + h11 * segmentDuration * slope1;
571
+ // ── Legacy Takes (FBX 5.x / 6.x) ───────────────────────────────────────────────
572
+ /**
573
+ * Pre-7000 files store animation in a top-level `Takes` block instead of AnimationStack/Layer/CurveNode objects:
574
+ *
575
+ * Takes: { Take: "name" { LocalTime: start, stop
576
+ * Model: "Model::joint1" { Channel: "Transform" { Channel: "T" { Channel: "X" { Default, KeyVer, KeyCount, Key } } } } } }
577
+ *
578
+ * Each take becomes one animation stack with a single layer. Models are matched through the same legacy string ids
579
+ * that the connection resolver synthesizes for 6.x objects.
580
+ */
581
+ export function extractLegacyTakes(doc, objectMap, rebaseKeyframes = true) {
582
+ const takesNode = findDocumentNode(doc, "Takes");
583
+ if (!takesNode) {
584
+ return [];
585
+ }
586
+ // Take entries name models by their full "Class::Name" string; older writers may use the bare name.
587
+ const legacyIds = new Map();
588
+ const legacyIdsByCleanName = new Map();
589
+ for (const entry of objectMap.objectEntries) {
590
+ if (entry.legacyName !== undefined && !entry.synthetic && !legacyIds.has(entry.legacyName)) {
591
+ legacyIds.set(entry.legacyName, entry.id);
592
+ const clean = cleanFBXName(entry.legacyName);
593
+ if (!legacyIdsByCleanName.has(clean)) {
594
+ legacyIdsByCleanName.set(clean, entry.id);
595
+ }
596
+ }
597
+ }
598
+ const stacks = [];
599
+ let syntheticId = -1000000;
600
+ for (const take of takesNode.children) {
601
+ if (take.name !== "Take") {
602
+ continue;
603
+ }
604
+ const name = cleanFBXName(getPropertyValue(take, 0) ?? "Take");
605
+ const timeSpan = readLegacyTimeSpan(findChildByName(take, "LocalTime")) ?? readLegacyTimeSpan(findChildByName(take, "ReferenceTime"));
606
+ const curveNodes = [];
607
+ const unsupportedCurveNodes = [];
608
+ let minTime = Infinity;
609
+ let maxTime = -Infinity;
610
+ const track = (curves) => {
611
+ for (const curve of curves) {
612
+ for (const key of curve.keys) {
613
+ if (key.time < minTime) {
614
+ minTime = key.time;
615
+ }
616
+ if (key.time > maxTime) {
617
+ maxTime = key.time;
618
+ }
619
+ }
620
+ }
621
+ };
622
+ for (const model of take.children) {
623
+ if (model.name !== "Model") {
624
+ continue;
625
+ }
626
+ const rawModelName = getPropertyValue(model, 0) ?? "";
627
+ const targetModelId = legacyIds.get(rawModelName) ?? legacyIdsByCleanName.get(cleanFBXName(rawModelName));
628
+ if (targetModelId === undefined) {
629
+ continue;
630
+ }
631
+ for (const channel of model.children) {
632
+ if (channel.name !== "Channel") {
633
+ continue;
634
+ }
635
+ const channelName = getPropertyValue(channel, 0) ?? "";
636
+ if (channelName === "Transform") {
637
+ for (const sub of channel.children) {
638
+ if (sub.name !== "Channel") {
639
+ continue;
640
+ }
641
+ const subName = getPropertyValue(sub, 0);
642
+ if (subName !== "T" && subName !== "R" && subName !== "S") {
643
+ continue;
644
+ }
645
+ const curves = readLegacyCompoundChannel(sub, doc.version);
646
+ if (curves.length > 0) {
647
+ track(curves);
648
+ curveNodes.push({ type: subName, targetModelId, curves, layerIndex: 0 });
649
+ }
650
+ }
651
+ }
652
+ else {
653
+ const curves = readLegacyCompoundChannel(channel, doc.version);
654
+ if (curves.length > 0) {
655
+ track(curves);
656
+ // Blend shape weights are channels named after the shape ("name (Shape)" before 6.0).
657
+ const shapeChannelId = findLegacyShapeChannel(objectMap, targetModelId, channelName);
658
+ if (shapeChannelId !== undefined) {
659
+ curveNodes.push({ type: "DeformPercent", targetModelId: shapeChannelId, curves, layerIndex: 0 });
660
+ continue;
661
+ }
662
+ // Light and camera properties live on the synthesized node attribute.
663
+ const attributeId = LEGACY_ATTRIBUTE_CHANNELS.has(channelName) ? getChildren(objectMap, targetModelId, "NodeAttribute")[0]?.id : undefined;
664
+ unsupportedCurveNodes.push({
665
+ type: channelName,
666
+ id: syntheticId--,
667
+ layerIndex: 0,
668
+ targetId: attributeId ?? targetModelId,
669
+ propertyName: channelName,
670
+ curveCount: curves.length,
671
+ curves,
672
+ defaultValues: {},
673
+ });
674
+ }
675
+ }
676
+ }
677
+ }
678
+ if (curveNodes.length === 0 && unsupportedCurveNodes.length === 0) {
679
+ continue;
680
+ }
681
+ const layer = {
682
+ name: "BaseLayer",
683
+ weight: 100,
684
+ normalizedWeight: 1,
685
+ blendMode: 0,
686
+ blend: makeLayerBlend(0, 100, 0, 0),
687
+ curveNodes,
688
+ unsupportedCurveNodes,
689
+ diagnostics: [],
690
+ };
691
+ stacks.push(finalizeAnimStack(name, [layer], curveNodes, unsupportedCurveNodes, [], minTime, maxTime, timeSpan, rebaseKeyframes));
692
+ }
693
+ return stacks;
694
+ }
695
+ /** Take channel names that animate light or camera attribute properties rather than the model itself. */
696
+ const LEGACY_ATTRIBUTE_CHANNELS = new Set([
697
+ "Color",
698
+ "Intensity",
699
+ "OuterAngle",
700
+ "InnerAngle",
701
+ "ConeAngle",
702
+ "HotSpot",
703
+ "FieldOfView",
704
+ "FieldOfViewX",
705
+ "FieldOfViewY",
706
+ "FocalLength",
707
+ "NearPlane",
708
+ "FarPlane",
709
+ "OrthoZoom",
710
+ "Roll",
711
+ "FilmWidth",
712
+ "FilmHeight",
713
+ ]);
714
+ /** Finds the synthesized BlendShapeChannel of a legacy model whose shape is named like the take channel. */
715
+ function findLegacyShapeChannel(objectMap, modelId, channelName) {
716
+ const suffix = " (Shape)";
717
+ const name = channelName.endsWith(suffix) ? channelName.slice(0, -suffix.length) : channelName;
718
+ for (const geometry of getChildren(objectMap, modelId, "Geometry")) {
719
+ for (const deformer of getChildren(objectMap, geometry.id, "Deformer")) {
720
+ if (getPropertyValue(deformer.node, 2) !== "BlendShape") {
721
+ continue;
722
+ }
723
+ for (const channel of getChildren(objectMap, deformer.id, "Deformer")) {
724
+ if (getPropertyValue(channel.node, 2) === "BlendShapeChannel" && cleanFBXName(getPropertyValue(channel.node, 1) ?? "") === name) {
725
+ return channel.id;
726
+ }
727
+ }
728
+ }
729
+ }
730
+ return undefined;
731
+ }
732
+ function readLegacyTimeSpan(node) {
733
+ if (!node) {
734
+ return null;
735
+ }
736
+ const start = fbxTimeToSeconds(node.properties[0]?.value);
737
+ const stop = fbxTimeToSeconds(node.properties[1]?.value);
738
+ return start !== null && stop !== null ? { start, stop } : null;
739
+ }
740
+ /** A channel either holds keys directly (scalar property) or has X/Y/Z child channels (vector property). */
741
+ function readLegacyCompoundChannel(node, version) {
742
+ const hasKeys = (n) => n.children.some((c) => c.name === "Key" || c.name === "Default");
743
+ if (hasKeys(node)) {
744
+ const keys = decodeLegacyKeys(node, version);
745
+ return keys.length > 0 ? [{ channel: "d|X", keys, ...extractCurveExtrapolation(node) }] : [];
746
+ }
747
+ const curves = [];
748
+ for (const child of node.children) {
749
+ if (child.name !== "Channel" || !hasKeys(child)) {
750
+ continue;
751
+ }
752
+ const axis = getPropertyValue(child, 0) ?? "X";
753
+ const keys = decodeLegacyKeys(child, version);
754
+ if (keys.length > 0) {
755
+ curves.push({ channel: `d|${axis}`, keys, ...extractCurveExtrapolation(child) });
756
+ }
757
+ if (curves.length === 3) {
758
+ break;
759
+ }
760
+ }
761
+ return curves;
762
+ }
763
+ /**
764
+ * Decodes the heterogeneous legacy `Key:` list (int64 times, double values and bare mode characters).
765
+ * The layout per key is `time, value, mode[, params...]`, mirroring the FBX SDK's pre-7000 key encoding.
766
+ */
767
+ function decodeLegacyKeys(channel, version) {
768
+ const keyNode = findChildByName(channel, "Key");
769
+ if (!keyNode) {
770
+ return [];
771
+ }
772
+ const vals = keyNode.properties.map((p) => p.value);
773
+ const num = (v) => (typeof v === "number" ? v : typeof v === "boolean" ? (v ? 1 : 0) : NaN);
774
+ const chr = (v) => (typeof v === "string" ? v : typeof v === "number" ? String.fromCharCode(v) : typeof v === "boolean" ? (v ? "\x01" : "\0") : "?");
775
+ let keyVer = toNumber(findChildByName(channel, "KeyVer")?.properties[0]?.value) ?? 0;
776
+ if (keyVer <= 0) {
777
+ keyVer = version < 5000 ? 4003 : version < 6000 ? 4004 : 4005;
778
+ }
779
+ const keyCount = toNumber(findChildByName(channel, "KeyCount")?.properties[0]?.value) ?? 0;
780
+ const times = [];
781
+ const values = [];
782
+ const interp = [];
783
+ const constNext = [];
784
+ const leftSlope = [];
785
+ const rightSlope = [];
786
+ const leftWeight = [];
787
+ const rightWeight = [];
788
+ const autoFlag = [];
789
+ let pendingLeft = 0;
790
+ let pendingLeftWeight = 1 / 3;
791
+ let p = 0;
792
+ for (let i = 0; i < keyCount; i++) {
793
+ if (p + 3 > vals.length) {
794
+ break;
795
+ }
796
+ const time = num(vals[p]) / FBX_TIME_UNIT;
797
+ const value = num(vals[p + 1]);
798
+ const mode = chr(vals[p + 2]);
799
+ p += 3;
800
+ let right = 0;
801
+ let nextLeft = 0;
802
+ let weightRight = 1 / 3;
803
+ let nextWeightLeft = 1 / 3;
804
+ let auto = false;
805
+ let kind = "cubic";
806
+ let isNext = false;
807
+ if (mode === "U") {
808
+ const slopeMode = chr(vals[p]);
809
+ p += 1;
810
+ let numWeights = 1;
811
+ if (slopeMode === "s" || slopeMode === "b") {
812
+ right = num(vals[p]);
813
+ nextLeft = num(vals[p + 1]);
814
+ p += 2;
815
+ if (keyVer === 4003) {
816
+ numWeights = 0;
817
+ }
818
+ }
819
+ else if (slopeMode === "a") {
820
+ auto = true;
821
+ if (keyVer <= 4004) {
822
+ numWeights = 0;
823
+ }
824
+ }
825
+ else if (slopeMode === "p" || slopeMode === "q") {
826
+ auto = true;
827
+ p += 2;
828
+ numWeights = keyVer <= 4004 ? 1 : 2;
829
+ }
830
+ else if (slopeMode === "t") {
831
+ auto = true;
832
+ p += 3;
833
+ numWeights = 0;
834
+ }
835
+ else if (slopeMode === "d") {
836
+ auto = true;
837
+ p += 1;
838
+ }
839
+ else {
840
+ break;
841
+ }
842
+ for (; numWeights > 0; numWeights--) {
843
+ const weightMode = chr(vals[p]);
844
+ p += 1;
845
+ if (weightMode === "n" || weightMode === "c") {
846
+ // automatic weights
847
+ }
848
+ else if (weightMode === "a") {
849
+ weightRight = num(vals[p]);
850
+ nextWeightLeft = num(vals[p + 1]);
851
+ p += 2;
852
+ }
853
+ else if (weightMode === "l") {
854
+ nextWeightLeft = num(vals[p]);
855
+ p += 1;
856
+ }
857
+ else if (weightMode === "r") {
858
+ weightRight = num(vals[p]);
859
+ p += 1;
860
+ }
861
+ else {
862
+ p = vals.length;
863
+ break;
864
+ }
865
+ }
866
+ }
867
+ else if (mode === "L") {
868
+ kind = "linear";
869
+ }
870
+ else if (mode === "C") {
871
+ kind = "constant";
872
+ if (keyVer >= 4004) {
873
+ isNext = chr(vals[p]) === "n";
874
+ p += 1;
875
+ }
876
+ }
877
+ else {
878
+ break;
879
+ }
880
+ times.push(time);
881
+ values.push(value);
882
+ interp.push(kind);
883
+ constNext.push(isNext);
884
+ leftSlope.push(pendingLeft);
885
+ rightSlope.push(right);
886
+ leftWeight.push(pendingLeftWeight);
887
+ rightWeight.push(weightRight);
888
+ autoFlag.push(auto);
889
+ pendingLeft = nextLeft;
890
+ pendingLeftWeight = nextWeightLeft;
891
+ }
892
+ // Resolve automatic and linear tangents now that neighbouring keys are known.
893
+ for (let i = 0; i < times.length; i++) {
894
+ if (autoFlag[i]) {
895
+ const s = i > 0 && i + 1 < times.length
896
+ ? solveLegacyAutoTangent(times[i - 1], times[i], times[i + 1], values[i - 1], values[i], values[i + 1], leftWeight[i], rightWeight[i])
897
+ : 0;
898
+ leftSlope[i] = s;
899
+ rightSlope[i] = s;
900
+ }
901
+ if (interp[i] === "linear" && i + 1 < times.length) {
902
+ const dt = times[i + 1] - times[i];
903
+ const s = dt > 0 ? (values[i + 1] - values[i]) / dt : 0;
904
+ rightSlope[i] = s;
905
+ if (!autoFlag[i + 1]) {
906
+ leftSlope[i + 1] = s;
907
+ }
908
+ }
909
+ }
910
+ return keyframesFromSlopes(times, values, interp, constNext, leftSlope, rightSlope, leftWeight, rightWeight);
911
+ }
912
+ /** Time-independent auto tangent with progressive clamping and 1/3 weights, as the SDK computes legacy 'a' keys. */
913
+ function solveLegacyAutoTangent(t0, t1, t2, v0, v1, v2, weightLeft, weightRight) {
914
+ if (!(t2 > t0)) {
915
+ return 0;
916
+ }
917
+ const slope = (v2 - v0) / (t2 - t0);
918
+ const sign = slope >= 0 ? 1 : -1;
919
+ let abs = sign * slope;
920
+ const rangeLeft = weightLeft * (t1 - t0);
921
+ const rangeRight = weightRight * (t2 - t1);
922
+ let maxLeft = rangeLeft > 0 ? (sign * (v1 - v0)) / rangeLeft : 0;
923
+ let maxRight = rangeRight > 0 ? (sign * (v2 - v1)) / rangeRight : 0;
924
+ if (!(maxLeft > 0)) {
925
+ maxLeft = 0;
926
+ }
927
+ if (!(maxRight > 0)) {
928
+ maxRight = 0;
929
+ }
930
+ abs = Math.min(abs, maxLeft, maxRight);
931
+ return sign * abs;
932
+ }
933
+ // ── Layered evaluation ─────────────────────────────────────────────────────────
934
+ /**
935
+ * Evaluates one transform channel (T, R or S) of a target at `time`, blending every animation layer of the stack the
936
+ * way the FBX SDK does: the first layer animating the channel replaces the static value, later layers are combined
937
+ * according to their blend mode, weight and accumulation modes.
938
+ * @param curveNodes - Curve nodes targeting this model (any layers, any types)
939
+ * @param layers - Stack layers, in order
940
+ * @param type - Channel to evaluate
941
+ * @param staticValue - Value when nothing animates the channel
942
+ * @param rotationOrder - Rotation order of the target (for rotation composition)
943
+ * @param time - Time in seconds
944
+ */
945
+ export function evaluateLayeredChannel(curveNodes, layers, type, staticValue, rotationOrder, time) {
946
+ let result = [staticValue[0], staticValue[1], staticValue[2]];
947
+ for (let layerIndex = 0; layerIndex < Math.max(layers.length, 1); layerIndex++) {
948
+ const node = curveNodes.find((cn) => cn.type === type && cn.layerIndex === layerIndex);
949
+ if (!node) {
950
+ continue;
951
+ }
952
+ const defaults = node.defaultValues ?? result;
953
+ const value = [
954
+ evaluateCurve(node.curves.find((c) => c.channel === "d|X"), time, defaults[0]),
955
+ evaluateCurve(node.curves.find((c) => c.channel === "d|Y"), time, defaults[1]),
956
+ evaluateCurve(node.curves.find((c) => c.channel === "d|Z"), time, defaults[2]),
957
+ ];
958
+ const layer = layers[layerIndex];
959
+ // The base layer replaces the static value; every other layer blends onto the running result, even when the
960
+ // base layer does not animate this channel (then it blends with the static value), as in the SDK.
961
+ if (layerIndex === 0 || !layer) {
962
+ result = value;
963
+ }
964
+ else {
965
+ let blend = layer.blend;
966
+ if (layer.weightCurve && blend.blended) {
967
+ let w = evaluateCurve(layer.weightCurve, time, layer.weight) / 100;
968
+ w = w < 0 ? 0 : w > 0.99999 ? 1 : w;
969
+ blend = { ...blend, weight: w };
970
+ }
971
+ result = combineLayerValue(result, value, blend, type, rotationOrder);
972
+ }
973
+ }
974
+ return result;
975
+ }
976
+ /**
977
+ * Evaluates an animated property through the animation layers: the base layer replaces the static value, every
978
+ * further layer blends onto the running result according to its blend mode and (possibly animated) weight, like
979
+ * `evaluateLayeredChannel` does for transforms.
980
+ * @param sources - Per-layer curves of the property
981
+ * @param layers - Stack layers, in order
982
+ * @param channels - Channel names to evaluate, in output order
983
+ * @param staticValue - Value per channel when nothing animates it
984
+ * @param time - Time in seconds
985
+ * @returns One value per channel
986
+ */
987
+ export function evaluateLayeredProperty(sources, layers, channels, staticValue, time) {
988
+ const result = channels.map((_, i) => staticValue[i] ?? 0);
989
+ for (let layerIndex = 0; layerIndex < Math.max(layers.length, 1); layerIndex++) {
990
+ const source = sources.find((s) => s.layerIndex === layerIndex);
991
+ if (!source) {
992
+ continue;
993
+ }
994
+ const value = channels.map((channel, i) => evaluateCurve(source.curves.find((c) => c.channel === channel), time, source.defaultValues?.[channel] ?? result[i]));
995
+ const layer = layers[layerIndex];
996
+ if (layerIndex === 0 || !layer) {
997
+ for (let i = 0; i < result.length; i++) {
998
+ result[i] = value[i];
999
+ }
1000
+ continue;
1001
+ }
1002
+ let blend = layer.blend;
1003
+ if (layer.weightCurve && blend.blended) {
1004
+ let w = evaluateCurve(layer.weightCurve, time, layer.weight) / 100;
1005
+ w = w < 0 ? 0 : w > 0.99999 ? 1 : w;
1006
+ blend = { ...blend, weight: w };
1007
+ }
1008
+ for (let i = 0; i < result.length; i++) {
1009
+ result[i] = combineLayerValue([result[i], 0, 0], [value[i], 0, 0], blend, "other", 0)[0];
1010
+ }
1011
+ }
1012
+ return result;
1013
+ }
1014
+ /**
1015
+ * True when every curve of the given channel is inside a constant (stepped) segment at `time`, so a baked key at
1016
+ * that time should hold its value instead of interpolating towards the next sample.
1017
+ */
1018
+ export function isChannelSteppedAt(curveNodes, type, time) {
1019
+ let sawCurve = false;
1020
+ for (const node of curveNodes) {
1021
+ if (node.type !== type) {
1022
+ continue;
1023
+ }
1024
+ for (const curve of node.curves) {
1025
+ const keys = curve.keys;
1026
+ if (keys.length < 2) {
1027
+ continue;
1028
+ }
1029
+ sawCurve = true;
1030
+ // Outside the key range the curve extrapolates continuously; only interior constant segments step.
1031
+ if (time < keys[0].time || time >= keys[keys.length - 1].time) {
1032
+ return false;
1033
+ }
1034
+ let i = 0;
1035
+ while (i + 1 < keys.length && keys[i + 1].time <= time) {
1036
+ i++;
1037
+ }
1038
+ if (keys[i].interpolation !== "constant") {
1039
+ return false;
1040
+ }
1041
+ }
1042
+ }
1043
+ return sawCurve;
647
1044
  }
648
1045
  //# sourceMappingURL=animation.js.map