@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/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 posture = measurePosture(posed, lengths, map, r, frames, a.minScore, inferredSource(seq.cleaned));
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-labelled') === true ? '' : `; fewest background inliers ${inl}`}`;
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 out = st.runs.map((r) => (r.state === 'both'
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
- if (f.r_hip?.state === 'inferred' || f.l_hip?.state === 'inferred') inferred++;
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');