@rig-c/pose 0.17.0 → 0.19.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 +35 -16
- package/cli.ts +18 -12
- package/package.json +1 -1
- package/src/clean.ts +15 -4
- package/src/legs.ts +273 -68
- package/src/map.ts +49 -3
- package/src/pose.ts +86 -32
- package/src/reduce.ts +167 -7
- package/src/retarget.ts +118 -37
- package/src/run.ts +202 -27
- package/src/switch.ts +31 -8
- package/src/verify.ts +66 -16
package/src/run.ts
CHANGED
|
@@ -9,16 +9,16 @@ import { tmpdir } from 'node:os';
|
|
|
9
9
|
import { dirname, join, resolve } from 'node:path';
|
|
10
10
|
import { readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
11
11
|
import { cleanLine, inferredSource } from './clean.ts';
|
|
12
|
-
import { convertCoco17, personTrack, readPoseSequence, type CameraFrame } from './pose.ts';
|
|
12
|
+
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
13
13
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
14
14
|
import { boneIdentity, droppedLine, keptOutLine, mergeProblems, readFragment, readIdle, targetOf, trackLabel, type IdleReading, type RigcTrack, type Target } from './idle.ts';
|
|
15
15
|
import { bendLine, endOnLine, flipLine, holdLine, rangeLine, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions } from './retarget.ts';
|
|
16
16
|
import { attachmentBones, baseMotionOf, buildInto, rigcVersion, folds, gate, readBuild, resolveRig, restOf, slotsOf } from './rigc.ts';
|
|
17
17
|
import { artRangeLine, chainsMoving, raisedLine, RAISED_WITHIN, reachLine, switchLine, uncoveredFolds } from './switch.ts';
|
|
18
18
|
import { landed, measureFeet, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
19
|
-
import { legsOf } from './legs.ts';
|
|
20
|
-
import { reduceKeys, type ReduceBone, type Reduced } from './reduce.ts';
|
|
21
|
-
import type { BlendHold, Floor, LegScale, Stance } from './legs.ts';
|
|
19
|
+
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
20
|
+
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
21
|
+
import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance } from './legs.ts';
|
|
22
22
|
|
|
23
23
|
export interface RetargetArgs extends RetargetOptions {
|
|
24
24
|
rig: string;
|
|
@@ -107,7 +107,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
107
107
|
const slots = slotsOf(doc0);
|
|
108
108
|
resolveMap(map, rest, a.map, slots);
|
|
109
109
|
|
|
110
|
-
const r = retarget(rest, map, frames, times, a, slots);
|
|
110
|
+
const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}) }, slots);
|
|
111
111
|
say(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, short ${a.short}, bends ${a.bends} (tolerance ${a.bendTolerance}°)${a.legScale !== undefined && a.legScale !== 'off' ? `, leg-scale ${a.legScale[0]}–${a.legScale[1]}` : ''}${a.plantFeet === true ? ', plant-feet' : ''}${a.stance !== undefined && a.stance !== 'off' ? `, stance ${a.stance}` : ''}${a.raisedByHead === true ? ', raised-by-head' : ''}${a.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}` : ''}`);
|
|
112
112
|
say(` RIGC rig-c ${rigc.version} (${rigc.entry}) gates and poses this run`);
|
|
113
113
|
say(` CLEAN ${cleanLine(seq.cleaned!)}`);
|
|
@@ -133,6 +133,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
133
133
|
if (a.raisedByHead === true && r.raised.length === 0) say(` RAISED none — no set drawing an arm within ${RAISED_WITHIN}° of straight up showed; no key moved`);
|
|
134
134
|
for (const e of r.raised) say(` RAISED ${raisedLine(e, r.times)}`);
|
|
135
135
|
for (const e of r.raised) if (e.short.length > 0) say(` REACH ${reachLine(e, r.times)}`);
|
|
136
|
+
if (map.alternates.some((x) => x.place !== undefined && x.place !== null) && r.byHip.length === 0) say(` BY-HIP none — no set with "place": "by-hip" showed; no key moved`);
|
|
137
|
+
for (const e of r.byHip) say(` BY-HIP ${raisedLine(e, r.times)}`);
|
|
138
|
+
for (const e of r.byHip) if (e.short.length > 0) say(` REACH ${reachLine(e, r.times)}`);
|
|
136
139
|
for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
|
|
137
140
|
for (const l of r.legScale) say(` LEG-SCALE ${legScaleLine(l, r.times)}`);
|
|
138
141
|
if (r.pivot !== null) say(` PIVOT bone "${r.pivot.bone}"'s origin is ${r.pivot.distance.toFixed(2)} units (${r.pivot.hipWidths.toFixed(3)} hip widths) from its thigh roots' midpoint (${r.pivot.roots.join(' + ')}): dx ${r.pivot.offset[0].toFixed(2)}, dy ${r.pivot.offset[1].toFixed(2)}, at setup — the pivot every hip turn swings the legs about`);
|
|
@@ -142,7 +145,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
142
145
|
say(` FEET ${r.planted ? 'planted' : 'read, not planted (no --plant-feet)'}: leg length (median hip → ankle) r ${f.legLength.r.toFixed(1)}px, l ${f.legLength.l.toFixed(1)}px; ${f.supports.length} support span(s), ${f.changes.filter((c) => c.from !== null).length} change(s) of support, ${f.jumps.length} jump(s), ${f.steps.length} step(s)`);
|
|
143
146
|
say(` FEET supports: ${f.supports.map((x) => `${x.side} ${span(x.frames)}`).join(', ') || 'none'}`);
|
|
144
147
|
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
|
-
|
|
148
|
+
if (f.lowered !== undefined) say(` FEET ${loweredLine(f.lowered)}`);
|
|
149
|
+
if (r.closure !== null) say(` FEET ${travelLine(r.closure)}`);
|
|
150
|
+
else 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
151
|
if (r.floor !== null) {
|
|
147
152
|
const legBones = legBonesOf(map);
|
|
148
153
|
if (legBones !== null) say(` FEET ${floorLine(r.floor, setupLegs(rest, legBones))}`);
|
|
@@ -154,9 +159,13 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
154
159
|
// --reduce (#32): the dense keys thinned top-down, before anything reads the tracks.
|
|
155
160
|
const reduced = a.reduce === undefined ? null : reduceTracks(r, map, rest, a.reduce, a.reduceHip);
|
|
156
161
|
if (reduced !== null) {
|
|
157
|
-
for (const row of reduced.rows)
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
for (const row of reduced.rows) {
|
|
163
|
+
const [who, track, what, unit] = row.kind === 'angle' ? ['bone', 'rotate', 'aim', '°'] : row.kind === 'origin' ? ['hip', 'translate', 'origin', ' units'] : ['bone', 'scalex', 'scale', ' of its setup length'];
|
|
164
|
+
say(` REDUCE ${who} "${row.bone}" ${track}: ${row.before} → ${row.after} key(s); worst ${what} error ${row.worst.toPrecision(3)}${unit} at frame ${row.frame} (t=${r.times[row.frame]!.toFixed(3)}s)`);
|
|
165
|
+
}
|
|
166
|
+
const sum = (kind: ReduceRow['kind'], k: 'before' | 'after') => reduced.rows.filter((x) => x.kind === kind).reduce((t, x) => t + x[k], 0);
|
|
167
|
+
const scales = reduced.rows.some((x) => x.kind === 'scale') ? `; scalex keys ${sum('scale', 'before')} → ${sum('scale', 'after')} within ${SCALE_REDUCE} of the setup length` : '';
|
|
168
|
+
say(` REDUCE rotate keys ${sum('angle', 'before')} → ${sum('angle', 'after')} (${(sum('angle', 'before') / sum('angle', 'after')).toFixed(2)}×) within ${a.reduce}°${scales}${a.reduceHip === undefined ? '; the hip stays dense (no --reduce-hip)' : ''}`);
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
// The idle against the new animation (#43 item 2): what it drops, and with --carry-idle, looped back in.
|
|
@@ -209,8 +218,13 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
209
218
|
|
|
210
219
|
const doc = readBuild(g1.build);
|
|
211
220
|
const posed = poseFrames(doc, a.name, r.times);
|
|
212
|
-
// A reduced track is held to the dense target plus its tolerance (#32)
|
|
213
|
-
|
|
221
|
+
// A reduced track is held to the dense target plus its tolerance (#32); an ankle, to what the
|
|
222
|
+
// reduced bones above it may move it by (#94, `footReduce`). A scale keeps its own: it is reduced within half of it.
|
|
223
|
+
const landings = measureLanding(doc, a.name, map, r, posed).map((l) => (a.reduce === undefined ? l
|
|
224
|
+
: l.kind === 'angle' ? { ...l, tolerance: l.tolerance + a.reduce }
|
|
225
|
+
: l.kind === 'origin' && a.reduceHip !== undefined ? { ...l, tolerance: l.tolerance + a.reduceHip }
|
|
226
|
+
: l.kind === 'foot' ? { ...l, tolerance: l.tolerance + footReduce(rest, r, l.bone.replace(/:tip$/, ''), a.reduce, a.reduceHip) }
|
|
227
|
+
: l));
|
|
214
228
|
const p = new Problems();
|
|
215
229
|
// A carried idle track on a bone above a mapped one moves it where the keys did not ask (the key formula holds unmapped bones at setup).
|
|
216
230
|
const carriedAbove = (bone: string): string => {
|
|
@@ -299,10 +313,12 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
299
313
|
range: r.range,
|
|
300
314
|
artRange: r.artRange,
|
|
301
315
|
raised: r.raised,
|
|
316
|
+
// Only when a set declares `place` (#93): a map without one writes the report it always has.
|
|
317
|
+
...(map.alternates.some((x) => x.place !== undefined && x.place !== null) ? { byHip: r.byHip } : {}),
|
|
302
318
|
legScale: r.legScale.map(({ scale: _scale, ...rest }) => rest),
|
|
303
319
|
pivot: r.pivot,
|
|
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 },
|
|
320
|
+
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, release: r.stance.release, minSeconds: r.stance.minSeconds, bounds: r.stance.bounds, ...(r.stance.range === undefined ? {} : { range: r.stance.range }), runs: r.stance.runs, over: r.stance.over },
|
|
321
|
+
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, ...(r.feet.lowered === undefined ? {} : { lowered: r.feet.lowered }), floor: r.floor?.y ?? null, shifts: r.floor?.shifts ?? [], blend: r.blendHold === null ? null : { bounds: r.blendHold.bounds, ...(r.blendHold.range === undefined ? {} : { range: r.blendHold.range }), runs: r.blendHold.runs, over: r.blendHold.over }, reading: feetReading, closure: r.closure },
|
|
306
322
|
shear,
|
|
307
323
|
idleDropped: idle === null ? null : idle.dropped,
|
|
308
324
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
@@ -319,9 +335,18 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
319
335
|
|
|
320
336
|
/**
|
|
321
337
|
* The retarget's rotate tracks (and with a hip tolerance, its translate) thinned
|
|
322
|
-
* in place (#32)
|
|
323
|
-
*
|
|
324
|
-
*
|
|
338
|
+
* in place (#32), on the final keys: the plant, the one floor, the stretches and
|
|
339
|
+
* the in-place closure all live inside `retarget`, so they are done (#94). Kept
|
|
340
|
+
* regardless, per bone: the frames either side of each switch boundary on the
|
|
341
|
+
* chains it changes, and the edges of every FLIP spread, SHORT span and RANGE
|
|
342
|
+
* run on it (the first and last frame the reducer keeps).
|
|
343
|
+
*
|
|
344
|
+
* With scaled bones (`--leg-scale`, the stretches; #94): a scaled bone's
|
|
345
|
+
* `scalex` is thinned within `SCALE_REDUCE`, and a mapped bone under a scaled
|
|
346
|
+
* bone — keyed through its parent's posed matrix (legs.ts) — is re-solved
|
|
347
|
+
* through its parent as reduced and thinned with its error read as the world
|
|
348
|
+
* aim and length it lands on (`reduceUnderScale`). With none, this is #32's
|
|
349
|
+
* reduction, unchanged.
|
|
325
350
|
*/
|
|
326
351
|
function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest, tolerance: number, hipTolerance: number | undefined): Reduced {
|
|
327
352
|
const n = r.times.length;
|
|
@@ -341,8 +366,19 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
341
366
|
return null;
|
|
342
367
|
};
|
|
343
368
|
const rotate = new Map<string, MotionTrack>();
|
|
344
|
-
|
|
345
|
-
|
|
369
|
+
const scalex = new Map<string, MotionTrack>();
|
|
370
|
+
for (const tr of r.animation.tracks) {
|
|
371
|
+
if (!('bone' in tr)) continue;
|
|
372
|
+
if (tr.property === 'rotate') rotate.set(tr.bone, tr);
|
|
373
|
+
else if (tr.property === 'scalex') scalex.set(tr.bone, tr);
|
|
374
|
+
}
|
|
375
|
+
/** A bone with a scaled bone above it: keyed through its parent's posed matrix (legs.ts), not by the key formula. */
|
|
376
|
+
const under = (b: string): boolean => {
|
|
377
|
+
for (let p = rest.get(b)!.parent; p !== null; p = rest.get(p)!.parent) if (scalex.has(p)) return true;
|
|
378
|
+
return false;
|
|
379
|
+
};
|
|
380
|
+
const dense = new Map([...rotate].map(([b, tr]) => [b, tr.keys.map((k) => k.v[0]!)]));
|
|
381
|
+
const bones: ReduceBone[] = [...rotate.values()].filter((tr) => !under(tr.bone)).map((tr) => ({ bone: tr.bone, above: above(tr.bone), dense: dense.get(tr.bone)!, keep: keep.get(tr.bone) ?? new Set<number>() }));
|
|
346
382
|
const hipTrack = r.origin === null || hipTolerance === undefined ? undefined : r.animation.tracks.find((tr): tr is MotionTrack => 'bone' in tr && tr.property === 'translate' && tr.bone === r.origin!.bone && tr.keys.every((k) => k.ease === undefined));
|
|
347
383
|
const parent = hipTrack === undefined ? null : rest.get(hipTrack.bone)!.parent;
|
|
348
384
|
const pm = parent === null ? null : rest.get(parent)!;
|
|
@@ -355,7 +391,101 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
355
391
|
});
|
|
356
392
|
for (const [bone, keys] of out.rotate) rotate.get(bone)!.keys = keys;
|
|
357
393
|
if (hipTrack !== undefined && out.hip !== null) hipTrack.keys = out.hip;
|
|
358
|
-
return out;
|
|
394
|
+
if (scalex.size === 0) return out;
|
|
395
|
+
|
|
396
|
+
// #94: the scales of the bones the key formula holds for, then the bones under a scaled bone, parents first.
|
|
397
|
+
const rows = new Map<string, ReduceRow>();
|
|
398
|
+
for (const row of out.rows) if (row.kind === 'angle') rows.set(`rotate ${row.bone}`, row);
|
|
399
|
+
const scaleAt = new Map<string, number[]>();
|
|
400
|
+
const keepOf = (b: string) => keep.get(b) ?? new Set<number>();
|
|
401
|
+
for (const [bone, tr] of scalex) {
|
|
402
|
+
if (under(bone)) continue;
|
|
403
|
+
const red = reduceScale(r.times, bone, r.scales.get(bone) ?? tr.keys.map((k) => k.v[0]!), keepOf(bone), SCALE_REDUCE);
|
|
404
|
+
tr.keys = red.keys;
|
|
405
|
+
scaleAt.set(bone, red.at);
|
|
406
|
+
rows.set(`scalex ${bone}`, red.row);
|
|
407
|
+
}
|
|
408
|
+
/** A bone the key formula holds for: its world turn from setup on every frame as reduced — its dense turn (the keys summed down the mapped chain) plus its world error; an unmapped bone turns with the nearest mapped bone above it. */
|
|
409
|
+
const turnOf = (b: string): number[] => {
|
|
410
|
+
const m = mapped.has(b) ? b : above(b);
|
|
411
|
+
if (m === null) return new Array<number>(n).fill(0);
|
|
412
|
+
const denseTurn = (x: string): number[] => {
|
|
413
|
+
const up = above(x);
|
|
414
|
+
const own = dense.get(x)!;
|
|
415
|
+
if (up === null) return [...own];
|
|
416
|
+
const t = denseTurn(up);
|
|
417
|
+
return own.map((v, i) => v + t[i]!);
|
|
418
|
+
};
|
|
419
|
+
const e = out.errors.get(m)!;
|
|
420
|
+
return denseTurn(m).map((v, i) => v + e[i]!);
|
|
421
|
+
};
|
|
422
|
+
const world0 = (b: string): M2 => {
|
|
423
|
+
const x = rest.get(b)!;
|
|
424
|
+
return [x.a, x.b, x.c, x.d];
|
|
425
|
+
};
|
|
426
|
+
const depth = (b: string): number => {
|
|
427
|
+
let d = 0;
|
|
428
|
+
for (let p = rest.get(b)!.parent; p !== null; p = rest.get(p)!.parent) d++;
|
|
429
|
+
return d;
|
|
430
|
+
};
|
|
431
|
+
const underBones = [...rotate.keys()].filter(under).sort((x, y) => depth(x) - depth(y));
|
|
432
|
+
const given: UnderBone[] = underBones.map((b) => {
|
|
433
|
+
if (!scalex.has(b)) throw new Refusal([{ code: 'POSE_REDUCE', text: `bone "${b}" hangs under a scaled bone and has no scalex track, so it was not keyed through its parent's posed matrix; --reduce re-solves only a bone the legs keyed that way` }]);
|
|
434
|
+
// Up through the unmapped bones between (their setup locals), to a bone given before it or one the key formula holds for.
|
|
435
|
+
let via: M2 = M2_IDENTITY;
|
|
436
|
+
let p = rest.get(b)!.parent!;
|
|
437
|
+
while (under(p) && !mapped.has(p)) {
|
|
438
|
+
via = mulM2(setupLocal(world0(rest.get(p)!.parent!), world0(p)), via);
|
|
439
|
+
p = rest.get(p)!.parent!;
|
|
440
|
+
}
|
|
441
|
+
const parent: UnderBone['parent'] = under(p) ? { under: p, via } : { rigid: { turn: turnOf(p), world0: world0(p), scale: scaleAt.get(p) ?? new Array<number>(n).fill(1) }, via };
|
|
442
|
+
const w0 = world0(b);
|
|
443
|
+
return {
|
|
444
|
+
bone: b,
|
|
445
|
+
parent,
|
|
446
|
+
local: setupLocal(world0(rest.get(b)!.parent!), w0),
|
|
447
|
+
len0: Math.hypot(w0[0], w0[2]),
|
|
448
|
+
target: r.targets.get(b)!,
|
|
449
|
+
asked: r.scales.get(b) ?? new Array<number>(n).fill(1),
|
|
450
|
+
dense: dense.get(b)!,
|
|
451
|
+
keep: keepOf(b),
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
const red = reduceUnderScale(r.times, given, tolerance, SCALE_REDUCE);
|
|
455
|
+
for (const [bone, keys] of red.rotate) rotate.get(bone)!.keys = keys;
|
|
456
|
+
for (const [bone, keys] of red.scale) scalex.get(bone)!.keys = keys;
|
|
457
|
+
for (const row of red.rows) rows.set(`${row.kind === 'angle' ? 'rotate' : 'scalex'} ${row.bone}`, row);
|
|
458
|
+
// The rows: every rotate track in the animation's order, the hip, then every scalex track.
|
|
459
|
+
const hipRow = out.rows.filter((x) => x.kind === 'origin');
|
|
460
|
+
return {
|
|
461
|
+
...out,
|
|
462
|
+
rows: [...[...rotate.keys()].map((b) => rows.get(`rotate ${b}`)!), ...hipRow, ...[...scalex.keys()].map((b) => rows.get(`scalex ${b}`)!)],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* How far `--reduce` may move an ankle (the tip of `ankle`), world units (#94):
|
|
468
|
+
* the hip's tolerance when its translate is reduced, plus every segment from
|
|
469
|
+
* the legs' top down to the ankle — each bone's setup offset from its parent,
|
|
470
|
+
* and the ankle bone's length — times the largest scale asked of any bone, each
|
|
471
|
+
* turned by at most the angle tolerance (a vector of length ℓ turned by θ moves
|
|
472
|
+
* its end ≤ ℓ·θ) and lengthened by at most `SCALE_REDUCE`. Every reduced bone's
|
|
473
|
+
* world aim is within the angle tolerance of its dense one, top-down, so the
|
|
474
|
+
* segments' errors are each bounded and the sum bounds the ankle's.
|
|
475
|
+
*/
|
|
476
|
+
export function footReduce(rest: Rest, r: ReturnType<typeof retarget>, ankle: string, tolerance: number, hipTolerance: number | undefined): number {
|
|
477
|
+
const top = r.origin?.bone ?? null;
|
|
478
|
+
let sMax = 1;
|
|
479
|
+
for (const s of r.scales.values()) for (const v of s) sMax = Math.max(sMax, v);
|
|
480
|
+
const a = rest.get(ankle)!;
|
|
481
|
+
let lever = a.length * Math.hypot(a.a, a.c);
|
|
482
|
+
for (let b: string | null = ankle; b !== null && b !== top; b = rest.get(b)!.parent) {
|
|
483
|
+
const x = rest.get(b)!;
|
|
484
|
+
const p = x.parent === null ? null : rest.get(x.parent)!;
|
|
485
|
+
if (p === null) break;
|
|
486
|
+
lever += Math.hypot(x.worldX - p.worldX, x.worldY - p.worldY);
|
|
487
|
+
}
|
|
488
|
+
return (hipTolerance ?? 0) + lever * sMax * ((tolerance * Math.PI) / 180 + SCALE_REDUCE);
|
|
359
489
|
}
|
|
360
490
|
|
|
361
491
|
function spans(ts: number[], step: number): string {
|
|
@@ -462,33 +592,78 @@ export function floorLine(floor: Floor, leg: { r: number; l: number }): string {
|
|
|
462
592
|
* was stretched to keep its ankle on the floor, with the stretch keyed; one per
|
|
463
593
|
* run whose stretch fell past its bounds (`past the bounds`, as STANCE says it).
|
|
464
594
|
*/
|
|
595
|
+
/**
|
|
596
|
+
* The lower-ankle line (rig-c/pose#113): under --plant-feet on a sequence not
|
|
597
|
+
* declared in place, the frames whose supporting foot differs from the reading
|
|
598
|
+
* before #113 — today's side, then the side read from the lower ankle ("none":
|
|
599
|
+
* no foot supports, the hip follows the pose).
|
|
600
|
+
*/
|
|
601
|
+
const CAUSE: Record<NonNullable<Lowered['cause']>, string> = {
|
|
602
|
+
released: 'released: the lower ankle could not take support, released by a step or moving',
|
|
603
|
+
higher: 'the only foot free to take support was the higher one',
|
|
604
|
+
};
|
|
605
|
+
function loweredLine(lowered: readonly Lowered[]): string {
|
|
606
|
+
const side = (s: Side | null) => s ?? 'none';
|
|
607
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|
|
608
|
+
const head = `support read from the lower ankle (a foot ≥ ${PLANT_SWITCH} leg lengths higher than the other does not take support, rig-c/pose#113)`;
|
|
609
|
+
if (lowered.length === 0) return `${head}: no frame reads differently from before`;
|
|
610
|
+
const frames = lowered.reduce((k, x) => k + x.frames[1] - x.frames[0] + 1, 0);
|
|
611
|
+
return `${head}: ${frames} frame(s) read differently from before — ${lowered.map((x) => `frames ${span(x.frames)} ${side(x.was)} → ${side(x.now)}${x.cause === undefined ? '' : ` (${CAUSE[x.cause]})`}`).join(', ')}`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** The in-place line (rig-c/pose#92): what --plant-feet did instead of holding the foot, and the loop's closure. */
|
|
615
|
+
export function travelLine(c: Closure): string {
|
|
616
|
+
const per = c.frames > 1 ? Math.abs(c.drift) / (c.frames - 1) : 0;
|
|
617
|
+
return `in place (the sequence's travel "${TRAVEL_IN_PLACE}"): each supporting foot follows its own ankle's picture x, scaled, instead of being held where it planted, and no step releases it; the hip's x travelled ${c.drift >= 0 ? '+' : ''}${c.drift.toFixed(2)} units from the first frame to the last, closed — spread back over the ${c.frames} frames (${per.toFixed(3)} units a frame), so the last frame's hip x is the first's`;
|
|
618
|
+
}
|
|
619
|
+
|
|
465
620
|
export function blendLines(h: BlendHold, times: readonly number[]): string[] {
|
|
466
621
|
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
622
|
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
|
|
623
|
+
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 ${boundText([r.lower ?? h.bounds[0], h.bounds[1]])} to keep the ankle on the floor`);
|
|
469
624
|
if (h.runs.length === 0) out.push('blend held on the floor: no blend frame left a supporting ankle off the floor');
|
|
470
|
-
|
|
625
|
+
if (h.range !== undefined) out.push(legRangeLine('supporting', h.range, h.bounds));
|
|
626
|
+
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 ${boundText(o.bound ?? h.bounds)}; held at the bound`}, the foot left up to ${o.left.toFixed(2)} units from the floor`);
|
|
471
627
|
return out;
|
|
472
628
|
}
|
|
473
629
|
|
|
474
630
|
/**
|
|
475
|
-
* The STANCE lines (#60): one per run of planted-both or
|
|
476
|
-
* the tolerance and the stretch keyed; one per run a stretch was
|
|
631
|
+
* The STANCE lines (#60): one per run of planted-both, lifted or (#114) below
|
|
632
|
+
* frames, with the tolerance and the stretch keyed; one per run a stretch was
|
|
633
|
+
* capped.
|
|
477
634
|
*/
|
|
478
635
|
export function stanceLines(st: Stance, times: readonly number[]): string[] {
|
|
479
636
|
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)`);
|
|
480
637
|
const tol = `the tolerance ${st.tolerance} leg lengths`;
|
|
481
638
|
const release = `planted again under ${st.release} of it, ${(st.release * st.tolerance).toFixed(4)}`;
|
|
482
|
-
const
|
|
639
|
+
const NAME: Record<Stance['runs'][number]['state'], string> = { both: 'planted-both', lifted: 'lifted', below: 'below' };
|
|
640
|
+
// An absorbed run lies inside a run of the state it took; the line names that state.
|
|
641
|
+
const around = (r: Stance['runs'][number]) => st.runs.find((x) => x.absorbed !== true && x.side === r.side && x.frames[0] <= r.frames[0] && r.frames[1] <= x.frames[1]);
|
|
642
|
+
const other = (r: Stance['runs'][number]) => NAME[around(r)?.state ?? (r.state === 'both' ? 'lifted' : 'both')];
|
|
643
|
+
const where = (r: Stance['runs'][number]) => (r.state === 'below' ? `its ankle up to ${r.maxDepth!.toFixed(4)} leg lengths below the supporting one in the picture` : `its ankle up to ${r.maxHeight.toFixed(4)} leg lengths above the supporting one in the picture`);
|
|
483
644
|
const out = st.runs.map((r) => (r.absorbed === true
|
|
484
|
-
? `absorbed: free foot ${r.side} ${span(r.frames)} — read ${r.state === 'both' ? 'planted both' : '
|
|
645
|
+
? `absorbed: free foot ${r.side} ${span(r.frames)} — read ${r.state === 'both' ? 'planted both' : r.state === 'below' ? 'past the tolerance below' : 'lifted'} (${where(r)}; ${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
646
|
: r.state === 'both'
|
|
486
647
|
? `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)})`
|
|
487
|
-
:
|
|
488
|
-
|
|
648
|
+
: r.state === 'below'
|
|
649
|
+
? `past the tolerance below: free foot ${r.side} ${span(r.frames)} — ${where(r)}, past ${tol} (${release}); a free foot that far below the supporting one is a support error, not a stance: never planted both, the pose's height kept`
|
|
650
|
+
: `lifted: free foot ${r.side} ${span(r.frames)} — ${where(r)}, over ${tol} (${release}); the pose's height kept`));
|
|
651
|
+
if (st.range !== undefined) out.push(legRangeLine('free', st.range, st.bounds));
|
|
652
|
+
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 ${boundText(o.bound ?? st.bounds)}; held at the bound`}, the foot left up to ${o.left.toFixed(2)} units from the floor`);
|
|
489
653
|
return out;
|
|
490
654
|
}
|
|
491
655
|
|
|
656
|
+
/** A bound as the lines print it: the default's figures as written, a derived lower bound to 4 places (#95). */
|
|
657
|
+
function boundText(b: readonly [number, number]): string {
|
|
658
|
+
return `[${b[0] === STANCE_BOUNDS[0] ? `${b[0]}` : b[0].toFixed(4)}, ${b[1]}]`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** The #95 line: where the picture folds a leg below the default lower bound, that fold is the frame's lower bound. */
|
|
662
|
+
function legRangeLine(which: 'free' | 'supporting', range: Partial<Record<'r' | 'l', number>>, bounds: readonly [number, number]): string {
|
|
663
|
+
const sides = (['r', 'l'] as const).filter((s) => range[s] !== undefined).map((s) => `${s} ${range[s]!.toFixed(4)}`);
|
|
664
|
+
return `bound from the leg's own range: on a frame where the picture folds the ${which} leg below ${bounds[0]} of its straight length (hip → ankle, its 95th percentile), the lower bound is that fold — lowest ${sides.join(', ')}; the upper stays ${bounds[1]}`;
|
|
665
|
+
}
|
|
666
|
+
|
|
492
667
|
/** A LEG-SCALE line: the segment, its straight length, its ratio's spread, and the frames clamped. */
|
|
493
668
|
function legScaleLine(l: LegScale, times: readonly number[]): string {
|
|
494
669
|
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|
package/src/switch.ts
CHANGED
|
@@ -131,6 +131,8 @@ export interface ArtRange {
|
|
|
131
131
|
clamped: boolean;
|
|
132
132
|
/** --raised-by-head (#62): on a held upper arm, the most the re-aimed forearm left the wrist short of its placed point, eye distances; `turn` and `maxOverflow` are then the upper arm's on that frame. */
|
|
133
133
|
shortfall?: number;
|
|
134
|
+
/** `place: "by-hip"` (#93): the shortfall is in hip widths, the wrist placed against the hip. Absent: against the head, eye distances. */
|
|
135
|
+
anchor?: 'hip';
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
export interface Uncovered {
|
|
@@ -583,7 +585,7 @@ function moveText(m: Move): string {
|
|
|
583
585
|
|
|
584
586
|
export function artRangeLine(e: ArtRange, times: readonly number[]): string {
|
|
585
587
|
if (e.shortfall !== undefined) {
|
|
586
|
-
return `set "${e.set}" chain "${e.chain}" bone "${e.bone}" held at its turn (range [${e.limits[0]}, ${e.limits[1]}]) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s the re-aimed forearm leaves the wrist up to ${e.shortfall.toFixed(2)} eye distances short of its placed point (the upper arm asked ${sg(e.turn)} from drawn, ${deg(e.maxOverflow)} past) — demand for another drawing of the set`;
|
|
588
|
+
return `set "${e.set}" chain "${e.chain}" bone "${e.bone}" held at its turn (range [${e.limits[0]}, ${e.limits[1]}]) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s the re-aimed forearm leaves the wrist up to ${e.shortfall.toFixed(2)} ${e.anchor === 'hip' ? 'hip widths' : 'eye distances'} short of its placed point (the upper arm asked ${sg(e.turn)} from drawn, ${deg(e.maxOverflow)} past) — demand for another drawing of the set`;
|
|
587
589
|
}
|
|
588
590
|
return `set "${e.set}" chain "${e.chain}" bone "${e.bone}" turned ${sg(e.turn)} from drawn (range [${e.limits[0]}, ${e.limits[1]}]) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s, up to ${deg(e.maxOverflow)} past it; ${e.clamped ? 'held at the limit' : 'the bone follows the pose'} — demand for another drawing of the set`;
|
|
589
591
|
}
|
|
@@ -747,6 +749,17 @@ export interface Raised {
|
|
|
747
749
|
reach: { min: number; max: number; farthest: number };
|
|
748
750
|
}
|
|
749
751
|
|
|
752
|
+
/**
|
|
753
|
+
* One span of a `place: "by-hip"` set on one of its placed chains: its wrist
|
|
754
|
+
* placed against the hip (rig-c/pose#93), as `Raised` against the head, in hip
|
|
755
|
+
* widths. `reference`, `placed`, `before` and `after` are the wrist's offset
|
|
756
|
+
* from the mid-hip, x right and y up; `short` is in hip widths.
|
|
757
|
+
*/
|
|
758
|
+
export interface ByHip extends Omit<Raised, 'eyes'> {
|
|
759
|
+
/** The unit: the hip width's median over the span, the rig's in world units, the reference's in picture px. */
|
|
760
|
+
hips: { rig: number; reference: number };
|
|
761
|
+
}
|
|
762
|
+
|
|
750
763
|
const xy = (v: P2) => `(${v[0].toFixed(2)}, ${v[1].toFixed(2)})`;
|
|
751
764
|
const framesText = (fs: readonly number[]): string => {
|
|
752
765
|
const flag: boolean[] = [];
|
|
@@ -754,16 +767,26 @@ const framesText = (fs: readonly number[]): string => {
|
|
|
754
767
|
return runsOf(Array.from({ length: flag.length }, (_, i) => flag[i] === true), 0).map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
755
768
|
};
|
|
756
769
|
|
|
757
|
-
|
|
770
|
+
/** The words a placement's lines use: what the wrist is placed against, its centre, and the unit. */
|
|
771
|
+
const ANCHOR = {
|
|
772
|
+
head: { against: 'the head', centre: 'head centre', unit: 'eye distances', unitOne: 'eye distance', relative: 'head-relative' },
|
|
773
|
+
hip: { against: 'the hip', centre: 'mid-hip', unit: 'hip widths', unitOne: 'hip width', relative: 'hip-relative' },
|
|
774
|
+
} as const;
|
|
775
|
+
const anchorOf = (e: Raised | ByHip) => ('hips' in e ? ANCHOR.hip : ANCHOR.head);
|
|
776
|
+
|
|
777
|
+
export function raisedLine(e: Raised | ByHip, times: readonly number[]): string {
|
|
778
|
+
const w = anchorOf(e);
|
|
779
|
+
const units = 'hips' in e ? e.hips : e.eyes;
|
|
758
780
|
const med = (vs: readonly P2[]): P2 => [median(vs.map((v) => v[0])), median(vs.map((v) => v[1]))];
|
|
759
781
|
const held = e.upperHeld.length === 0 ? 'the upper arm within its turn on every frame' : `the upper arm held by the set's turn on ${e.upperHeld.length} frame(s) (${framesText(e.upperHeld)}), the forearm re-aimed`;
|
|
760
782
|
const out = e.short.length === 0 ? 'every target in reach' : `out of reach on ${e.short.reduce((k, s) => k + s.frames[1] - s.frames[0] + 1, 0)} frame(s) (REACH)`;
|
|
761
|
-
return `set "${e.set}" chain "${e.chain}" ${spanOf(e.frames, times)}: the wrist placed against
|
|
783
|
+
return `set "${e.set}" chain "${e.chain}" ${spanOf(e.frames, times)}: the wrist placed against ${w.against}; ${w.unitOne} (median) rig ${units.rig.toFixed(2)} units, reference ${units.reference.toFixed(2)} px; wrist − ${w.centre} in ${w.unit} (x right, y up), median reference ${xy(med(e.reference))}, placed ${xy(med(e.placed))}, before ${xy(med(e.before))}, after ${xy(med(e.after))}; worst |rig − reference| before ${xy(e.worst.before)}, after ${xy(e.worst.after)}; worst |after − placed| ${e.worst.placed.toFixed(2)}; ${held}; ${out}`;
|
|
762
784
|
}
|
|
763
785
|
|
|
764
|
-
/** The REACH line: a
|
|
765
|
-
export function reachLine(e: Raised, times: readonly number[]): string {
|
|
766
|
-
const
|
|
767
|
-
const
|
|
768
|
-
|
|
786
|
+
/** The REACH line: a placed span (raised, or by the hip) whose target was out of the arm's reach on some frames — the proportions, not the art's range, were the limit. */
|
|
787
|
+
export function reachLine(e: Raised | ByHip, times: readonly number[]): string {
|
|
788
|
+
const w = anchorOf(e);
|
|
789
|
+
const why = { far: `farther than l0 + l1 = ${e.reach.max.toFixed(1)}`, near: `nearer than |l0 − l1| = ${e.reach.min.toFixed(1)}`, missed: `and the ray from the ${w.centre} never enters the reach; placed at the reach's point nearest it` } as const;
|
|
790
|
+
const runs = e.short.map((s) => `${spanOf(s.frames, times)} ${s.how === 'missed' ? 'out of reach' : why[s.how]}${s.how === 'missed' ? `, ${why.missed}` : ''}: up to ${s.shortfall.max.toFixed(2)} ${w.unit} short (median ${s.shortfall.median.toFixed(2)})`);
|
|
791
|
+
return `set "${e.set}" chain "${e.chain}": the target out of the arm's reach from the shoulder — ${runs.join('; ')}; the wrist placed at the reach limit along the ${w.relative} direction (the farthest target ${e.reach.farthest.toFixed(1)} units from the shoulder). Not ART-RANGE: the rig's proportions, not the art's range, were the limit`;
|
|
769
792
|
}
|
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 { HipsInferred, Joint, Run } from './pose.ts';
|
|
14
|
+
import type { HipsInferred, Joint, PelvisFit, Run } from './pose.ts';
|
|
15
15
|
import type { Retarget } from './retarget.ts';
|
|
16
16
|
import { pictureAngle, pointOn, wrap } from './retarget.ts';
|
|
17
17
|
|
|
@@ -260,13 +260,25 @@ export interface Posture {
|
|
|
260
260
|
*/
|
|
261
261
|
hipsInferred?: { frames: number; source: string };
|
|
262
262
|
/**
|
|
263
|
-
* Present only when the hips were measured (#86, `hipsInferred.by` is
|
|
263
|
+
* Present only when the hips were measured (#86, #106, `hipsInferred.by` is
|
|
264
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,
|
|
266
|
-
* class counted — and the record's
|
|
267
|
-
* inferred hip is left out
|
|
265
|
+
* record's read, interpolated, scaled and held frames with both hips good,
|
|
266
|
+
* each class counted — and the record's spans. Any `unmeasured` frame (corr
|
|
267
|
+
* < 0.9) or other frame with an inferred hip is left out of `tilt`.
|
|
268
268
|
*/
|
|
269
|
-
tiltMeasured?: {
|
|
269
|
+
tiltMeasured?: {
|
|
270
|
+
method: string;
|
|
271
|
+
frames: number;
|
|
272
|
+
read: number;
|
|
273
|
+
interpolated: number;
|
|
274
|
+
held: number;
|
|
275
|
+
heldSpans: Run[];
|
|
276
|
+
scaled?: number;
|
|
277
|
+
scaledSpans?: Run[];
|
|
278
|
+
unmeasured?: number;
|
|
279
|
+
unmeasuredSpans?: Run[];
|
|
280
|
+
fit?: PelvisFit;
|
|
281
|
+
};
|
|
270
282
|
/**
|
|
271
283
|
* Sway: the x-range of mid_hip − mid_shoulder, in rig units (the reference's
|
|
272
284
|
* picture px × the figure scale) and in each body's own shoulder widths.
|
|
@@ -311,11 +323,12 @@ const medianOf = (xs: number[]): number => {
|
|
|
311
323
|
* and the line counts the frames left out. Sway is read as before, inferred
|
|
312
324
|
* hips included: it is a different measure, and the card leaves it as it is.
|
|
313
325
|
*
|
|
314
|
-
* Measured hips (#86, `measured`): the hips are inferred on every frame,
|
|
315
|
-
* turned to a measured angle: read off the picture, interpolated between two
|
|
316
|
-
* close reads,
|
|
317
|
-
*
|
|
318
|
-
*
|
|
326
|
+
* Measured hips (#86, #106, `measured`): the hips are inferred on every frame,
|
|
327
|
+
* and turned to a measured angle: read off the picture, interpolated between two
|
|
328
|
+
* close reads, scaled from the detector's hip line when `fit.corr >= 0.9`, or
|
|
329
|
+
* held from the earlier read. Those four are the measurement, so the tilt is
|
|
330
|
+
* read on them; any `unmeasured` frame (`fit.corr < 0.9`) or other frame with
|
|
331
|
+
* an inferred hip is left out and counted.
|
|
319
332
|
*/
|
|
320
333
|
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 {
|
|
321
334
|
const pelvis = map.chains.find((c) => c.joints.length === 2 && c.joints[0] === 'r_hip' && c.joints[1] === 'l_hip');
|
|
@@ -330,16 +343,23 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
330
343
|
const refSw: number[] = [];
|
|
331
344
|
let inferred = 0;
|
|
332
345
|
const inClass = (rs: Run[], i: number) => rs.some(([a, b]) => a <= i && i <= b);
|
|
333
|
-
const counts = { read: 0, interpolated: 0, held: 0 };
|
|
346
|
+
const counts = { read: 0, interpolated: 0, held: 0, scaled: 0, unmeasured: 0 };
|
|
334
347
|
frames.forEach((f, i) => {
|
|
335
348
|
const rh = at(i, 'r_hip');
|
|
336
349
|
const lh = at(i, 'l_hip');
|
|
337
350
|
if (rh === null || lh === null) return;
|
|
338
351
|
const isInferred = f.r_hip?.state === 'inferred' || f.l_hip?.state === 'inferred';
|
|
339
352
|
const cls = measured === null ? undefined : (['read', 'interpolated', 'held'] as const).find((c) => inClass(measured[c], i));
|
|
353
|
+
const isScaled = measured !== null && measured.scaled !== undefined && inClass(measured.scaled, i);
|
|
354
|
+
const isUnmeasured = measured !== null && measured.unmeasured !== undefined && inClass(measured.unmeasured, i);
|
|
340
355
|
if (isInferred && cls !== undefined) {
|
|
341
356
|
counts[cls]++;
|
|
342
357
|
refTilt.push(pictureAngle(rh, lh));
|
|
358
|
+
} else if (isInferred && isScaled) {
|
|
359
|
+
counts.scaled++;
|
|
360
|
+
refTilt.push(pictureAngle(rh, lh));
|
|
361
|
+
} else if (isInferred && isUnmeasured) {
|
|
362
|
+
counts.unmeasured++;
|
|
343
363
|
} else if (isInferred) inferred++;
|
|
344
364
|
else refTilt.push(pictureAngle(rh, lh));
|
|
345
365
|
const rs = at(i, 'r_shoulder');
|
|
@@ -352,7 +372,7 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
352
372
|
const aim = pelvis.aim[0]!;
|
|
353
373
|
let tilt: Posture['tilt'] = null;
|
|
354
374
|
const why = `hips inferred (${inferredSource})`;
|
|
355
|
-
if (inferred === 0 || refTilt.length > 0) {
|
|
375
|
+
if ((inferred === 0 && counts.unmeasured === 0) || refTilt.length > 0) {
|
|
356
376
|
const rigTilt = posed.map((b) => aimAngle(b, lengths, bone, aim));
|
|
357
377
|
tilt = { reference: rangeOf(unwrapped(refTilt)), rig: rangeOf(unwrapped(rigTilt)), ratio: 0 };
|
|
358
378
|
tilt.ratio = tilt.rig.range / tilt.reference.range;
|
|
@@ -387,6 +407,7 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
387
407
|
ratioSw: rig === null || rig.rangeSw === null ? null : rig.rangeSw / reference.rangeSw,
|
|
388
408
|
};
|
|
389
409
|
}
|
|
410
|
+
const has106 = measured !== null && (measured.scaled !== undefined || measured.unmeasured !== undefined || measured.fit !== undefined);
|
|
390
411
|
return {
|
|
391
412
|
pelvis: pelvis.name,
|
|
392
413
|
rigHip: r.origin.bone,
|
|
@@ -395,7 +416,27 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
395
416
|
tilt,
|
|
396
417
|
...(tilt === null ? { tiltWhy: why } : {}),
|
|
397
418
|
...(inferred > 0 ? { hipsInferred: { frames: inferred, source: inferredSource } } : {}),
|
|
398
|
-
...(measured !== null
|
|
419
|
+
...(measured !== null
|
|
420
|
+
? {
|
|
421
|
+
tiltMeasured: {
|
|
422
|
+
method: measured.method,
|
|
423
|
+
frames: counts.read + counts.interpolated + counts.scaled + counts.held,
|
|
424
|
+
read: counts.read,
|
|
425
|
+
interpolated: counts.interpolated,
|
|
426
|
+
held: counts.held,
|
|
427
|
+
heldSpans: measured.held.map(([a, b]): Run => [a, b]),
|
|
428
|
+
...(has106
|
|
429
|
+
? {
|
|
430
|
+
scaled: counts.scaled,
|
|
431
|
+
scaledSpans: (measured.scaled ?? []).map(([a, b]): Run => [a, b]),
|
|
432
|
+
unmeasured: counts.unmeasured,
|
|
433
|
+
unmeasuredSpans: (measured.unmeasured ?? []).map(([a, b]): Run => [a, b]),
|
|
434
|
+
...(measured.fit !== undefined ? { fit: { frames: measured.fit.frames, gain: measured.fit.gain, corr: measured.fit.corr } } : {}),
|
|
435
|
+
}
|
|
436
|
+
: {}),
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
: {}),
|
|
399
440
|
sway,
|
|
400
441
|
};
|
|
401
442
|
}
|
|
@@ -403,14 +444,23 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
403
444
|
const f1 = (x: number) => x.toFixed(1);
|
|
404
445
|
const f3 = (x: number) => x.toFixed(3);
|
|
405
446
|
const sg = (x: number) => `${x >= 0 ? '+' : '−'}${Math.abs(x).toFixed(1)}`;
|
|
447
|
+
const spansText = (rs: Run[] | undefined) => (rs === undefined || rs.length === 0 ? 'none' : rs.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', '));
|
|
406
448
|
|
|
407
449
|
/** The POSTURE lines. */
|
|
408
450
|
export function postureLines(q: Posture): string[] {
|
|
409
451
|
const over = (k: number) => (k === q.frames.of ? '' : ` over ${k} of ${q.frames.of} frames`);
|
|
410
452
|
const t = q.tilt;
|
|
411
453
|
const m = q.tiltMeasured;
|
|
412
|
-
const heldSpans = m
|
|
413
|
-
const
|
|
454
|
+
const heldSpans = spansText(m?.heldSpans);
|
|
455
|
+
const scaledPart =
|
|
456
|
+
m?.scaled !== undefined && m.scaled > 0
|
|
457
|
+
? `, ${m.scaled} scaled (scaled: ${spansText(m.scaledSpans)}${m.fit?.gain !== undefined && m.fit.gain !== null && m.fit.corr !== null ? `, gain ${m.fit.gain.toFixed(2)}×, r=${m.fit.corr.toFixed(3)}` : ''})`
|
|
458
|
+
: '';
|
|
459
|
+
const unmeasuredPart =
|
|
460
|
+
m?.unmeasured !== undefined && (m.unmeasured > 0 || (m.fit !== undefined && (m.fit.corr === null || m.fit.corr < 0.9)))
|
|
461
|
+
? `; ${m.unmeasured} unmeasured (${spansText(m.unmeasuredSpans)}${m.fit !== undefined ? `, r=${m.fit.corr === null ? 'none' : m.fit.corr.toFixed(3)} < 0.9` : ''})`
|
|
462
|
+
: '';
|
|
463
|
+
const measured = m === undefined ? '' : `; tilt measured (${m.method}) on ${m.frames} frame(s): ${m.read} read, ${m.interpolated} interpolated${scaledPart}, ${m.held} held (held: ${heldSpans})${unmeasuredPart}`;
|
|
414
464
|
const left = q.hipsInferred === undefined ? '' : `; ${q.hipsInferred.frames} frame(s) with an inferred hip not read (${q.hipsInferred.source})`;
|
|
415
465
|
const out = [
|
|
416
466
|
t === null
|