@rig-c/pose 0.16.0 → 0.17.0
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/README.md +52 -8
- package/cli.ts +56 -6
- package/package.json +1 -1
- package/src/clean.ts +339 -7
- package/src/legs.ts +211 -15
- package/src/pose.ts +307 -21
- package/src/retarget.ts +49 -19
- package/src/run.ts +57 -8
- package/src/verify.ts +29 -4
package/src/run.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { artRangeLine, chainsMoving, raisedLine, RAISED_WITHIN, reachLine, switc
|
|
|
18
18
|
import { landed, measureFeet, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
19
19
|
import { legsOf } from './legs.ts';
|
|
20
20
|
import { reduceKeys, type ReduceBone, type Reduced } from './reduce.ts';
|
|
21
|
-
import type { LegScale, Stance } from './legs.ts';
|
|
21
|
+
import type { BlendHold, Floor, LegScale, Stance } from './legs.ts';
|
|
22
22
|
|
|
23
23
|
export interface RetargetArgs extends RetargetOptions {
|
|
24
24
|
rig: string;
|
|
@@ -143,6 +143,11 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
143
143
|
say(` FEET supports: ${f.supports.map((x) => `${x.side} ${span(x.frames)}`).join(', ') || 'none'}`);
|
|
144
144
|
say(` FEET jumps (both ankles and mid_hip > 0.05 leg lengths above their rolling floor, ≥ 2 frames): ${f.jumps.map(span).join(', ') || 'none'}`);
|
|
145
145
|
say(` FEET steps (the supporting ankle > 0.03 leg lengths a frame; re-planted after 2 frames < 0.01): ${f.steps.map((x) => `${x.side} ${span(x.frames)} → ${x.to === null ? 'the pose\'s travel' : `${x.to} supports`}, ${x.replant === null ? 'never re-planted' : `re-planted at ${x.replant}`}`).join('; ') || 'none'}`);
|
|
146
|
+
if (r.floor !== null) {
|
|
147
|
+
const legBones = legBonesOf(map);
|
|
148
|
+
if (legBones !== null) say(` FEET ${floorLine(r.floor, setupLegs(rest, legBones))}`);
|
|
149
|
+
}
|
|
150
|
+
if (r.blendHold !== null) for (const l of blendLines(r.blendHold, r.times)) say(` FEET ${l}`);
|
|
146
151
|
}
|
|
147
152
|
if (r.stance !== null) for (const l of stanceLines(r.stance, r.times)) say(` STANCE ${l}`);
|
|
148
153
|
|
|
@@ -228,7 +233,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
228
233
|
p.throwIfAny();
|
|
229
234
|
|
|
230
235
|
const lengths = new Map(doc.bones.map((b) => [b.name, b.length ?? 0]));
|
|
231
|
-
const
|
|
236
|
+
const hips = seq.cleaned?.params.hipsInferred;
|
|
237
|
+
const posture = measurePosture(posed, lengths, map, r, frames, a.minScore, inferredSource(seq.cleaned), hips?.by === 'measured' ? hips : null);
|
|
232
238
|
if (posture !== null) for (const l of postureLines(posture)) say(` POSTURE ${l}`);
|
|
233
239
|
const legBones = legBonesOf(map);
|
|
234
240
|
const feetReading = legBones === null ? null : measureFeet(posed, lengths, r, legBones);
|
|
@@ -276,7 +282,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
276
282
|
frames: seq.frames.length,
|
|
277
283
|
duration: r.animation.duration,
|
|
278
284
|
cleaned: seq.cleaned,
|
|
279
|
-
options: { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', reduceHip: a.reduceHip ?? 'off' },
|
|
285
|
+
options: { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', reduceHip: a.reduceHip ?? 'off' },
|
|
280
286
|
scale: r.scale,
|
|
281
287
|
holds: r.holds,
|
|
282
288
|
short: r.short,
|
|
@@ -295,8 +301,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
295
301
|
raised: r.raised,
|
|
296
302
|
legScale: r.legScale.map(({ scale: _scale, ...rest }) => rest),
|
|
297
303
|
pivot: r.pivot,
|
|
298
|
-
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, bounds: r.stance.bounds, runs: r.stance.runs, over: r.stance.over },
|
|
299
|
-
feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, reading: feetReading },
|
|
304
|
+
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, release: r.stance.release, minSeconds: r.stance.minSeconds, bounds: r.stance.bounds, runs: r.stance.runs, over: r.stance.over },
|
|
305
|
+
feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, floor: r.floor?.y ?? null, shifts: r.floor?.shifts ?? [], blend: r.blendHold === null ? null : { bounds: r.blendHold.bounds, runs: r.blendHold.runs, over: r.blendHold.over }, reading: feetReading },
|
|
300
306
|
shear,
|
|
301
307
|
idleDropped: idle === null ? null : idle.dropped,
|
|
302
308
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
@@ -416,7 +422,7 @@ export function cameraLine(camera: readonly CameraFrame[], times: readonly numbe
|
|
|
416
422
|
const fastest = [...steps].sort((x, y) => Math.abs(y.d) - Math.abs(x.d) || x.to - y.to).slice(0, 3);
|
|
417
423
|
const sign = (d: number) => `${d < 0 ? '−' : '+'}${Math.abs(d).toFixed(4)}`;
|
|
418
424
|
const inl = camera.reduce((m, c) => Math.min(m, c.inliers), Infinity);
|
|
419
|
-
return `${source ?? '(source not stated)'}: the sequence carries the picture's camera against frame 0's; its joints are normalised by it (p′ = (p − t) / s): s ${ss[lo]!.toFixed(4)} (frame ${lo}) … ${ss[hi]!.toFixed(4)} (frame ${hi}) over ${camera.length} frames; s changes fastest at ${fastest.map((x) => `frame ${x.to - 1}→${x.to} (t=${times[x.to]!.toFixed(3)}s) ${sign(x.d)}`).join(', ') || 'no frame (one frame only)'}${source?.startsWith('hand
|
|
425
|
+
return `${source ?? '(source not stated)'}: the sequence carries the picture's camera against frame 0's; its joints are normalised by it (p′ = (p − t) / s): s ${ss[lo]!.toFixed(4)} (frame ${lo}) … ${ss[hi]!.toFixed(4)} (frame ${hi}) over ${camera.length} frames; s changes fastest at ${fastest.map((x) => `frame ${x.to - 1}→${x.to} (t=${times[x.to]!.toFixed(3)}s) ${sign(x.d)}`).join(', ') || 'no frame (one frame only)'}${source?.startsWith('hand') === true ? '' : `; fewest background inliers ${inl}`}`;
|
|
420
426
|
}
|
|
421
427
|
|
|
422
428
|
/** Each leg's first bone (its root, the thigh) and last bone (whose tip is the ankle), when the map has both. */
|
|
@@ -426,6 +432,45 @@ function legBonesOf(map: JointMap): { r: { root: string; ankle: string }; l: { r
|
|
|
426
432
|
return { r: { root: l.legs.r.chain.bones[0]!, ankle: l.legs.r.ankleBone }, l: { root: l.legs.l.chain.bones[0]!, ankle: l.legs.l.ankleBone } };
|
|
427
433
|
}
|
|
428
434
|
|
|
435
|
+
/** Each side's rig leg at setup, world units: its thigh's origin to its ankle (the leg's last bone's tip), by rig-c's setup pose. */
|
|
436
|
+
export function setupLegs(rest: Rest, legs: { r: { root: string; ankle: string }; l: { root: string; ankle: string } }): { r: number; l: number } {
|
|
437
|
+
const of = (s: 'r' | 'l') => {
|
|
438
|
+
const o = rest.get(legs[s].root)!;
|
|
439
|
+
const b = rest.get(legs[s].ankle)!;
|
|
440
|
+
return Math.hypot(b.worldX + b.a * b.length - o.worldX, b.worldY + b.c * b.length - o.worldY);
|
|
441
|
+
};
|
|
442
|
+
return { r: of('r'), l: of('l') };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The FEET floor line (#85): the floor's world y and where it came from, each
|
|
447
|
+
* support span's shift onto it in world units and in that side's rig leg at
|
|
448
|
+
* setup, and the shifts' range in the two legs' mean.
|
|
449
|
+
*/
|
|
450
|
+
export function floorLine(floor: Floor, leg: { r: number; l: number }): string {
|
|
451
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|
|
452
|
+
const sign = (v: number, d: number) => `${v < 0 ? '−' : '+'}${Math.abs(v).toFixed(d)}`;
|
|
453
|
+
const ss = floor.shifts.map((x) => x.shift);
|
|
454
|
+
const lo = Math.min(...ss);
|
|
455
|
+
const hi = Math.max(...ss);
|
|
456
|
+
const mean = (leg.r + leg.l) / 2;
|
|
457
|
+
return `floor: world y ${floor.y.toFixed(2)} (${floor.given ? 'given by --floor' : `the median of the ${floor.shifts.length} support span(s)' planted heights`}); each span's supporting ankle moved onto it, the hip and everything above with it — shift per span in units (leg lengths: the rig's at setup, thigh origin → ankle, r ${leg.r.toFixed(1)}, l ${leg.l.toFixed(1)} units): ${floor.shifts.map((x) => `${x.side} ${span(x.frames)} ${sign(x.shift, 2)} (${sign(x.shift / leg[x.side], 4)})`).join(', ')}; range ${sign(lo, 2)} … ${sign(hi, 2)} units = ${((hi - lo) / mean).toFixed(4)} leg lengths`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* The FEET blend lines (#85): one per run of blend frames whose supporting leg
|
|
462
|
+
* was stretched to keep its ankle on the floor, with the stretch keyed; one per
|
|
463
|
+
* run whose stretch fell past its bounds (`past the bounds`, as STANCE says it).
|
|
464
|
+
*/
|
|
465
|
+
export function blendLines(h: BlendHold, times: readonly number[]): string[] {
|
|
466
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `frame ${x[0]} (t=${times[x[0]]!.toFixed(3)}s)` : `frames ${x[0]}–${x[1]} (t=${times[x[0]]!.toFixed(3)}–${times[x[1]]!.toFixed(3)}s)`);
|
|
467
|
+
const out: string[] = [];
|
|
468
|
+
for (const r of h.runs) out.push(`blend held on the floor: supporting foot ${r.side} ${span(r.frames)} — the hip blends between two derivations there, so its leg is stretched by k ${r.k.min.toFixed(4)}–${r.k.max.toFixed(4)} within [${h.bounds[0]}, ${h.bounds[1]}] to keep the ankle on the floor`);
|
|
469
|
+
if (h.runs.length === 0) out.push('blend held on the floor: no blend frame left a supporting ankle off the floor');
|
|
470
|
+
for (const o of h.over) out.push(`past the bounds: supporting foot ${o.side} ${span(o.frames)} — ${o.needed === null ? 'its ankle is not below its leg\'s root, so no stretch reaches the floor' : `the floor needed k ${o.needed.toFixed(4)}, past [${h.bounds[0]}, ${h.bounds[1]}]; held at the bound`}, the foot left up to ${o.left.toFixed(2)} units from the floor`);
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
429
474
|
/**
|
|
430
475
|
* The STANCE lines (#60): one per run of planted-both or lifted frames, with
|
|
431
476
|
* the tolerance and the stretch keyed; one per run a stretch was capped.
|
|
@@ -433,9 +478,13 @@ function legBonesOf(map: JointMap): { r: { root: string; ankle: string }; l: { r
|
|
|
433
478
|
export function stanceLines(st: Stance, times: readonly number[]): string[] {
|
|
434
479
|
const span = (x: [number, number]) => (x[0] === x[1] ? `frame ${x[0]} (t=${times[x[0]]!.toFixed(3)}s)` : `frames ${x[0]}–${x[1]} (t=${times[x[0]]!.toFixed(3)}–${times[x[1]]!.toFixed(3)}s)`);
|
|
435
480
|
const tol = `the tolerance ${st.tolerance} leg lengths`;
|
|
436
|
-
const
|
|
481
|
+
const release = `planted again under ${st.release} of it, ${(st.release * st.tolerance).toFixed(4)}`;
|
|
482
|
+
const other = (r: Stance['runs'][number]) => (r.state === 'both' ? 'lifted' : 'planted-both');
|
|
483
|
+
const out = st.runs.map((r) => (r.absorbed === true
|
|
484
|
+
? `absorbed: free foot ${r.side} ${span(r.frames)} — read ${r.state === 'both' ? 'planted both' : 'lifted'} (its ankle up to ${r.maxHeight.toFixed(4)} leg lengths above the supporting one in the picture; ${tol}, ${release}) for ${r.frames[1] - r.frames[0] + 1} frame(s), under ${st.minSeconds} s between two ${other(r)} runs; it takes their state`
|
|
485
|
+
: r.state === 'both'
|
|
437
486
|
? `planted both: free foot ${r.side} ${span(r.frames)} — its ankle up to ${r.maxHeight.toFixed(4)} leg lengths above the supporting one in the picture, within ${tol}; its leg stretched by k ${r.k!.min === r.k!.max ? r.k!.max.toFixed(4) : `${r.k!.min.toFixed(4)} … ${r.k!.max.toFixed(4)}`} (largest ${r.k!.max.toFixed(4)})`
|
|
438
|
-
: `lifted: free foot ${r.side} ${span(r.frames)} — its ankle up to ${r.maxHeight.toFixed(4)} leg lengths above the supporting one in the picture, over ${tol}; the pose's height kept`));
|
|
487
|
+
: `lifted: free foot ${r.side} ${span(r.frames)} — its ankle up to ${r.maxHeight.toFixed(4)} leg lengths above the supporting one in the picture, over ${tol} (${release}); the pose's height kept`));
|
|
439
488
|
for (const o of st.over) out.push(`past the bounds: free foot ${o.side} ${span(o.frames)} — ${o.needed === null ? 'its ankle is not below its leg\'s root, so no stretch reaches the floor' : `the floor needed k ${o.needed.toFixed(4)}, past [${st.bounds[0]}, ${st.bounds[1]}]; held at the bound`}, the foot left up to ${o.left.toFixed(2)} units from the floor`);
|
|
440
489
|
return out;
|
|
441
490
|
}
|
package/src/verify.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { poseRawAnimation, poseRawSetup, type CompiledDocument, type RawBone } from 'rig-c/core';
|
|
12
12
|
import type { Body18 } from './joints.ts';
|
|
13
13
|
import { aimAngle, pointAt, type JointMap } from './map.ts';
|
|
14
|
-
import type { Joint } from './pose.ts';
|
|
14
|
+
import type { HipsInferred, Joint, Run } from './pose.ts';
|
|
15
15
|
import type { Retarget } from './retarget.ts';
|
|
16
16
|
import { pictureAngle, pointOn, wrap } from './retarget.ts';
|
|
17
17
|
|
|
@@ -259,6 +259,14 @@ export interface Posture {
|
|
|
259
259
|
* how many such frames the tilt left out, and where the state came from.
|
|
260
260
|
*/
|
|
261
261
|
hipsInferred?: { frames: number; source: string };
|
|
262
|
+
/**
|
|
263
|
+
* Present only when the hips were measured (#86, `hipsInferred.by` is
|
|
264
|
+
* `measured`): the method, how many frames the tilt was read on — the
|
|
265
|
+
* record's read, interpolated and held frames with both hips good, each
|
|
266
|
+
* class counted — and the record's held spans. Any other frame with an
|
|
267
|
+
* inferred hip is left out and counted in `hipsInferred`.
|
|
268
|
+
*/
|
|
269
|
+
tiltMeasured?: { method: string; frames: number; read: number; interpolated: number; held: number; heldSpans: Run[] };
|
|
262
270
|
/**
|
|
263
271
|
* Sway: the x-range of mid_hip − mid_shoulder, in rig units (the reference's
|
|
264
272
|
* picture px × the figure scale) and in each body's own shoulder widths.
|
|
@@ -302,8 +310,14 @@ const medianOf = (xs: number[]): number => {
|
|
|
302
310
|
* from the clean record). On some frames only, the tilt is read over the rest
|
|
303
311
|
* and the line counts the frames left out. Sway is read as before, inferred
|
|
304
312
|
* hips included: it is a different measure, and the card leaves it as it is.
|
|
313
|
+
*
|
|
314
|
+
* Measured hips (#86, `measured`): the hips are inferred on every frame, and
|
|
315
|
+
* turned to a measured angle: read off the picture, interpolated between two
|
|
316
|
+
* close reads, or held from the earlier read. Those three are the measurement,
|
|
317
|
+
* so the tilt is read on them; any other frame with an inferred hip is left
|
|
318
|
+
* out and counted, as above. The line names the held spans.
|
|
305
319
|
*/
|
|
306
|
-
export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: ReadonlyMap<string, number>, map: JointMap, r: Retarget, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, minScore: number, inferredSource = 'source not stated'): Posture | null {
|
|
320
|
+
export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: ReadonlyMap<string, number>, map: JointMap, r: Retarget, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, minScore: number, inferredSource = 'source not stated', measured: Extract<HipsInferred, { by: 'measured' }> | null = null): Posture | null {
|
|
307
321
|
const pelvis = map.chains.find((c) => c.joints.length === 2 && c.joints[0] === 'r_hip' && c.joints[1] === 'l_hip');
|
|
308
322
|
if (pelvis === undefined || map.translate === null || r.origin === null || r.scale === null) return null;
|
|
309
323
|
const s = r.scale.factor;
|
|
@@ -315,11 +329,18 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
315
329
|
const refSway: number[] = [];
|
|
316
330
|
const refSw: number[] = [];
|
|
317
331
|
let inferred = 0;
|
|
332
|
+
const inClass = (rs: Run[], i: number) => rs.some(([a, b]) => a <= i && i <= b);
|
|
333
|
+
const counts = { read: 0, interpolated: 0, held: 0 };
|
|
318
334
|
frames.forEach((f, i) => {
|
|
319
335
|
const rh = at(i, 'r_hip');
|
|
320
336
|
const lh = at(i, 'l_hip');
|
|
321
337
|
if (rh === null || lh === null) return;
|
|
322
|
-
|
|
338
|
+
const isInferred = f.r_hip?.state === 'inferred' || f.l_hip?.state === 'inferred';
|
|
339
|
+
const cls = measured === null ? undefined : (['read', 'interpolated', 'held'] as const).find((c) => inClass(measured[c], i));
|
|
340
|
+
if (isInferred && cls !== undefined) {
|
|
341
|
+
counts[cls]++;
|
|
342
|
+
refTilt.push(pictureAngle(rh, lh));
|
|
343
|
+
} else if (isInferred) inferred++;
|
|
323
344
|
else refTilt.push(pictureAngle(rh, lh));
|
|
324
345
|
const rs = at(i, 'r_shoulder');
|
|
325
346
|
const ls = at(i, 'l_shoulder');
|
|
@@ -374,6 +395,7 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
374
395
|
tilt,
|
|
375
396
|
...(tilt === null ? { tiltWhy: why } : {}),
|
|
376
397
|
...(inferred > 0 ? { hipsInferred: { frames: inferred, source: inferredSource } } : {}),
|
|
398
|
+
...(measured !== null ? { tiltMeasured: { method: measured.method, frames: counts.read + counts.interpolated + counts.held, ...counts, heldSpans: measured.held.map(([a, b]): Run => [a, b]) } } : {}),
|
|
377
399
|
sway,
|
|
378
400
|
};
|
|
379
401
|
}
|
|
@@ -386,11 +408,14 @@ const sg = (x: number) => `${x >= 0 ? '+' : '−'}${Math.abs(x).toFixed(1)}`;
|
|
|
386
408
|
export function postureLines(q: Posture): string[] {
|
|
387
409
|
const over = (k: number) => (k === q.frames.of ? '' : ` over ${k} of ${q.frames.of} frames`);
|
|
388
410
|
const t = q.tilt;
|
|
411
|
+
const m = q.tiltMeasured;
|
|
412
|
+
const heldSpans = m === undefined || m.heldSpans.length === 0 ? 'none' : m.heldSpans.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
413
|
+
const measured = m === undefined ? '' : `; tilt measured (${m.method}) on ${m.frames} frame(s): ${m.read} read, ${m.interpolated} interpolated, ${m.held} held (held: ${heldSpans})`;
|
|
389
414
|
const left = q.hipsInferred === undefined ? '' : `; ${q.hipsInferred.frames} frame(s) with an inferred hip not read (${q.hipsInferred.source})`;
|
|
390
415
|
const out = [
|
|
391
416
|
t === null
|
|
392
417
|
? `hip tilt not read: ${q.tiltWhy}`
|
|
393
|
-
: `hip tilt range (chain "${q.pelvis}", r_hip → l_hip, Spine degrees): reference ${f1(t.reference.range)}° (${sg(t.reference.min)}° … ${sg(t.reference.max)}°${over(q.frames.tilt)}), rig ${f1(t.rig.range)}° (${sg(t.rig.min)}° … ${sg(t.rig.max)}°) — rig/reference ${f3(t.ratio)}${left}`,
|
|
418
|
+
: `hip tilt range (chain "${q.pelvis}", r_hip → l_hip, Spine degrees): reference ${f1(t.reference.range)}° (${sg(t.reference.min)}° … ${sg(t.reference.max)}°${over(q.frames.tilt)}), rig ${f1(t.rig.range)}° (${sg(t.rig.min)}° … ${sg(t.rig.max)}°) — rig/reference ${f3(t.ratio)}${measured}${left}`,
|
|
394
419
|
];
|
|
395
420
|
const w = q.sway;
|
|
396
421
|
if (w === null) out.push('sway: the reference has no frame with both hips and both shoulders good; not measured');
|