@rig-c/pose 0.15.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 +66 -9
- package/cli.ts +70 -7
- 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 +351 -41
- package/src/run.ts +63 -10
- package/src/switch.ts +164 -0
- package/src/verify.ts +29 -4
package/src/run.ts
CHANGED
|
@@ -14,11 +14,11 @@ import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './
|
|
|
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
|
-
import { artRangeLine, chainsMoving, switchLine, uncoveredFolds } from './switch.ts';
|
|
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
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;
|
|
@@ -108,7 +108,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
108
108
|
resolveMap(map, rest, a.map, slots);
|
|
109
109
|
|
|
110
110
|
const r = retarget(rest, map, frames, times, a, slots);
|
|
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.carryIdle === true ? ', carry-idle' : ''}${a.merge !== undefined ? `, merge ${a.merge}` : ''}${a.reduce !== undefined ? `, reduce ${a.reduce}°${a.reduceHip !== undefined ? ` (hip ${a.reduceHip} units)` : ''}` : ''}`);
|
|
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!)}`);
|
|
114
114
|
if (a.smoothAlias !== undefined) say(` SMOOTH --smooth is carried by clean; ignored (given ${a.smoothAlias}; this sequence was cleaned with legs ${seq.cleaned!.params.legs}, rest ${seq.cleaned!.params.rest})`);
|
|
@@ -130,6 +130,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
130
130
|
for (const l of windowLines(map, r.switches, r.distances, r.times, a.switchTolerance)) say(` FRAMES ${l}`);
|
|
131
131
|
}
|
|
132
132
|
for (const e of r.range) say(` RANGE ${rangeLine(e, r.times)}`);
|
|
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
|
+
for (const e of r.raised) say(` RAISED ${raisedLine(e, r.times)}`);
|
|
135
|
+
for (const e of r.raised) if (e.short.length > 0) say(` REACH ${reachLine(e, r.times)}`);
|
|
133
136
|
for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
|
|
134
137
|
for (const l of r.legScale) say(` LEG-SCALE ${legScaleLine(l, r.times)}`);
|
|
135
138
|
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`);
|
|
@@ -140,6 +143,11 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
140
143
|
say(` FEET supports: ${f.supports.map((x) => `${x.side} ${span(x.frames)}`).join(', ') || 'none'}`);
|
|
141
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'}`);
|
|
142
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}`);
|
|
143
151
|
}
|
|
144
152
|
if (r.stance !== null) for (const l of stanceLines(r.stance, r.times)) say(` STANCE ${l}`);
|
|
145
153
|
|
|
@@ -225,7 +233,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
225
233
|
p.throwIfAny();
|
|
226
234
|
|
|
227
235
|
const lengths = new Map(doc.bones.map((b) => [b.name, b.length ?? 0]));
|
|
228
|
-
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);
|
|
229
238
|
if (posture !== null) for (const l of postureLines(posture)) say(` POSTURE ${l}`);
|
|
230
239
|
const legBones = legBonesOf(map);
|
|
231
240
|
const feetReading = legBones === null ? null : measureFeet(posed, lengths, r, legBones);
|
|
@@ -273,7 +282,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
273
282
|
frames: seq.frames.length,
|
|
274
283
|
duration: r.animation.duration,
|
|
275
284
|
cleaned: seq.cleaned,
|
|
276
|
-
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', 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' },
|
|
277
286
|
scale: r.scale,
|
|
278
287
|
holds: r.holds,
|
|
279
288
|
short: r.short,
|
|
@@ -289,10 +298,11 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
289
298
|
turns: r.turns,
|
|
290
299
|
range: r.range,
|
|
291
300
|
artRange: r.artRange,
|
|
301
|
+
raised: r.raised,
|
|
292
302
|
legScale: r.legScale.map(({ scale: _scale, ...rest }) => rest),
|
|
293
303
|
pivot: r.pivot,
|
|
294
|
-
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, bounds: r.stance.bounds, runs: r.stance.runs, over: r.stance.over },
|
|
295
|
-
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 },
|
|
296
306
|
shear,
|
|
297
307
|
idleDropped: idle === null ? null : idle.dropped,
|
|
298
308
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
@@ -412,7 +422,7 @@ export function cameraLine(camera: readonly CameraFrame[], times: readonly numbe
|
|
|
412
422
|
const fastest = [...steps].sort((x, y) => Math.abs(y.d) - Math.abs(x.d) || x.to - y.to).slice(0, 3);
|
|
413
423
|
const sign = (d: number) => `${d < 0 ? '−' : '+'}${Math.abs(d).toFixed(4)}`;
|
|
414
424
|
const inl = camera.reduce((m, c) => Math.min(m, c.inliers), Infinity);
|
|
415
|
-
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}`}`;
|
|
416
426
|
}
|
|
417
427
|
|
|
418
428
|
/** Each leg's first bone (its root, the thigh) and last bone (whose tip is the ankle), when the map has both. */
|
|
@@ -422,6 +432,45 @@ function legBonesOf(map: JointMap): { r: { root: string; ankle: string }; l: { r
|
|
|
422
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 } };
|
|
423
433
|
}
|
|
424
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
|
+
|
|
425
474
|
/**
|
|
426
475
|
* The STANCE lines (#60): one per run of planted-both or lifted frames, with
|
|
427
476
|
* the tolerance and the stretch keyed; one per run a stretch was capped.
|
|
@@ -429,9 +478,13 @@ function legBonesOf(map: JointMap): { r: { root: string; ankle: string }; l: { r
|
|
|
429
478
|
export function stanceLines(st: Stance, times: readonly number[]): string[] {
|
|
430
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)`);
|
|
431
480
|
const tol = `the tolerance ${st.tolerance} leg lengths`;
|
|
432
|
-
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'
|
|
433
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)})`
|
|
434
|
-
: `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`));
|
|
435
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`);
|
|
436
489
|
return out;
|
|
437
490
|
}
|
package/src/switch.ts
CHANGED
|
@@ -129,6 +129,8 @@ export interface ArtRange {
|
|
|
129
129
|
limits: [number, number];
|
|
130
130
|
/** Whether the bone was held at the limit (the set's `turn`) or followed the pose (its chain's `range` only). */
|
|
131
131
|
clamped: boolean;
|
|
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
|
+
shortfall?: number;
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
export interface Uncovered {
|
|
@@ -580,6 +582,9 @@ function moveText(m: Move): string {
|
|
|
580
582
|
}
|
|
581
583
|
|
|
582
584
|
export function artRangeLine(e: ArtRange, times: readonly number[]): string {
|
|
585
|
+
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`;
|
|
587
|
+
}
|
|
583
588
|
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`;
|
|
584
589
|
}
|
|
585
590
|
|
|
@@ -603,3 +608,162 @@ export function clampArt(switches: readonly Switch[], map: JointMap, targets: Ma
|
|
|
603
608
|
}
|
|
604
609
|
}
|
|
605
610
|
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* A set's arm is raised (rig-c/pose#62) when its `drawn` world angle on the
|
|
614
|
+
* chain's first bone (the upper arm) is within this many degrees of straight up.
|
|
615
|
+
*/
|
|
616
|
+
export const RAISED_WITHIN = 30;
|
|
617
|
+
|
|
618
|
+
/** Whether a drawn world angle, Spine degrees, is within `RAISED_WITHIN` of straight up (90°). */
|
|
619
|
+
export const isRaised = (drawn: number): boolean => Math.abs(wrap(drawn - 90)) <= RAISED_WITHIN;
|
|
620
|
+
|
|
621
|
+
type P2 = [number, number];
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* A two-bone solve in the world (y up): the directions, degrees, of the vector
|
|
625
|
+
* from the shoulder to the elbow (`upper`) and from the elbow to the wrist
|
|
626
|
+
* (`fore`) that put the wrist on `target`, for bones of lengths `l0` and `l1`.
|
|
627
|
+
* `side` is the elbow's bend: +1 when the forearm turns counter-clockwise from
|
|
628
|
+
* the upper arm, −1 clockwise. Out of reach (farther than l0 + l1, nearer than
|
|
629
|
+
* |l0 − l1|, beyond a relative 1e-6) both point straight at the target and
|
|
630
|
+
* `reached` is false; on the shoulder, where there is no direction, `upper` is
|
|
631
|
+
* null. `reachLimit` puts a target inside first, so the raised solve always reaches.
|
|
632
|
+
*/
|
|
633
|
+
export function twoBone(shoulder: P2, target: P2, l0: number, l1: number, side: 1 | -1): { upper: number; fore: number; reached: boolean } | { upper: null; reached: false } {
|
|
634
|
+
const dx = target[0] - shoulder[0];
|
|
635
|
+
const dy = target[1] - shoulder[1];
|
|
636
|
+
const d = Math.hypot(dx, dy);
|
|
637
|
+
if (d < 1e-9) return { upper: null, reached: false };
|
|
638
|
+
const theta = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
639
|
+
const eps = 1e-6 * Math.max(1, l0 + l1);
|
|
640
|
+
if (d > l0 + l1 + eps || d < Math.abs(l0 - l1) - eps) return { upper: theta, fore: theta, reached: false };
|
|
641
|
+
const cos = Math.min(1, Math.max(-1, (l0 * l0 + d * d - l1 * l1) / (2 * l0 * d)));
|
|
642
|
+
const upper = theta - side * Math.acos(cos) * (180 / Math.PI);
|
|
643
|
+
const r = upper * (Math.PI / 180);
|
|
644
|
+
const ex = shoulder[0] + l0 * Math.cos(r);
|
|
645
|
+
const ey = shoulder[1] + l0 * Math.sin(r);
|
|
646
|
+
return { upper: wrap(upper), fore: Math.atan2(target[1] - ey, target[0] - ex) * (180 / Math.PI), reached: true };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Where a raised wrist is placed: the target itself, or the reach limit (rig-c/pose#62, HQ's ruling). */
|
|
650
|
+
export interface Placement {
|
|
651
|
+
point: P2;
|
|
652
|
+
/** `in`: the target is within reach. `far` / `near`: past l0 + l1, or nearer than |l0 − l1|, and placed on the ray from the head centre. `missed`: that ray never enters the reach; placed at the reach's point nearest the target. */
|
|
653
|
+
how: 'in' | 'far' | 'near' | 'missed';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* The raised wrist's placement. The arm reaches the annulus about the shoulder
|
|
658
|
+
* between |l0 − l1| and l0 + l1. A target inside it is kept. Otherwise the wrist
|
|
659
|
+
* goes on the ray from the head centre through the target, at the point of
|
|
660
|
+
* that ray inside the annulus nearest the target — the reach limit along the
|
|
661
|
+
* head-relative direction. The ray can cross the annulus twice (out and back
|
|
662
|
+
* through the inner disc, or through the whole ring): of the reachable stretches
|
|
663
|
+
* the one nearest the target wins, a tie going to the point nearer the head. A
|
|
664
|
+
* ray that never enters it (`missed`) gives the annulus point nearest the
|
|
665
|
+
* target, on the line from the shoulder through it (toward the head centre when
|
|
666
|
+
* the target is on the shoulder).
|
|
667
|
+
*/
|
|
668
|
+
export function reachLimit(centre: P2, target: P2, shoulder: P2, l0: number, l1: number): Placement {
|
|
669
|
+
const rMin = Math.abs(l0 - l1);
|
|
670
|
+
const rMax = l0 + l1;
|
|
671
|
+
const eps = 1e-9 * Math.max(1, rMax);
|
|
672
|
+
const dT = Math.hypot(target[0] - shoulder[0], target[1] - shoulder[1]);
|
|
673
|
+
if (dT >= rMin - eps && dT <= rMax + eps) return { point: target, how: 'in' };
|
|
674
|
+
const how = dT > rMax ? 'far' : 'near';
|
|
675
|
+
const vx = target[0] - centre[0];
|
|
676
|
+
const vy = target[1] - centre[1];
|
|
677
|
+
const L = Math.hypot(vx, vy);
|
|
678
|
+
if (L > 1e-9) {
|
|
679
|
+
const u: P2 = [vx / L, vy / L];
|
|
680
|
+
// |C + s·u − S|² = s² + 2·s·b + c
|
|
681
|
+
const b = u[0] * (centre[0] - shoulder[0]) + u[1] * (centre[1] - shoulder[1]);
|
|
682
|
+
const c = (centre[0] - shoulder[0]) ** 2 + (centre[1] - shoulder[1]) ** 2;
|
|
683
|
+
const roots = (r: number): [number, number] | null => {
|
|
684
|
+
const disc = b * b - c + r * r;
|
|
685
|
+
return disc < 0 ? null : [-b - Math.sqrt(disc), -b + Math.sqrt(disc)];
|
|
686
|
+
};
|
|
687
|
+
const outer = roots(rMax);
|
|
688
|
+
const stretches: Array<[number, number]> = [];
|
|
689
|
+
if (outer !== null) {
|
|
690
|
+
const lo = Math.max(0, outer[0]);
|
|
691
|
+
const hi = outer[1];
|
|
692
|
+
const inner = rMin > 0 ? roots(rMin) : null;
|
|
693
|
+
if (hi >= lo) {
|
|
694
|
+
if (inner === null || inner[1] <= lo || inner[0] >= hi) stretches.push([lo, hi]);
|
|
695
|
+
else {
|
|
696
|
+
if (inner[0] > lo) stretches.push([lo, inner[0]]);
|
|
697
|
+
if (inner[1] < hi) stretches.push([inner[1], hi]);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
let best: number | null = null;
|
|
702
|
+
for (const [lo, hi] of stretches) {
|
|
703
|
+
const s = Math.min(Math.max(L, lo), hi);
|
|
704
|
+
if (best === null || Math.abs(s - L) < Math.abs(best - L) - 1e-9 || (Math.abs(Math.abs(s - L) - Math.abs(best - L)) <= 1e-9 && s < best)) best = s;
|
|
705
|
+
}
|
|
706
|
+
if (best !== null) return { point: [centre[0] + best * u[0], centre[1] + best * u[1]], how };
|
|
707
|
+
}
|
|
708
|
+
const from: P2 = dT > 1e-9 ? target : centre;
|
|
709
|
+
const k = Math.hypot(from[0] - shoulder[0], from[1] - shoulder[1]) || 1;
|
|
710
|
+
const r = Math.min(Math.max(dT, rMin), rMax);
|
|
711
|
+
return { point: [shoulder[0] + (r * (from[0] - shoulder[0])) / k, shoulder[1] + (r * (from[1] - shoulder[1])) / k], how: 'missed' };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** A run of frames whose target was out of reach, one way. */
|
|
715
|
+
export interface ReachShort {
|
|
716
|
+
frames: [number, number];
|
|
717
|
+
how: 'far' | 'near' | 'missed';
|
|
718
|
+
/** |target − placed| in the rig's eye distances: the most, and the median, over the run. */
|
|
719
|
+
shortfall: { max: number; median: number };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** One span of a raised set on one chain: its wrist placed against the head (rig-c/pose#62). */
|
|
723
|
+
export interface Raised {
|
|
724
|
+
set: string;
|
|
725
|
+
chain: string;
|
|
726
|
+
frames: [number, number];
|
|
727
|
+
seconds: number;
|
|
728
|
+
/** The unit: the eye distance's median over the span, the rig's in world units, the reference's in picture px. */
|
|
729
|
+
eyes: { rig: number; reference: number };
|
|
730
|
+
/**
|
|
731
|
+
* Per frame of the span, the wrist's offset from the head centre in eye
|
|
732
|
+
* distances (x right, y up): the reference's; where the wrist was placed (the
|
|
733
|
+
* target, or the reach limit); the rig's on the pose's angles; the rig's as
|
|
734
|
+
* keyed.
|
|
735
|
+
*/
|
|
736
|
+
reference: P2[];
|
|
737
|
+
placed: P2[];
|
|
738
|
+
before: P2[];
|
|
739
|
+
after: P2[];
|
|
740
|
+
/** The largest |rig − reference| over the span, x and y, before and after; and the largest |after − placed|, eye distances. */
|
|
741
|
+
worst: { before: P2; after: P2; placed: number };
|
|
742
|
+
/** The runs of frames out of the arm's reach (the REACH line). */
|
|
743
|
+
short: ReachShort[];
|
|
744
|
+
/** The frames whose upper arm the set's `turn` held: the forearm re-aimed at the placed wrist. */
|
|
745
|
+
upperHeld: number[];
|
|
746
|
+
/** The arm's reach (from |l0 − l1| to l0 + l1, world units from the shoulder) and the farthest target from the shoulder over the span. */
|
|
747
|
+
reach: { min: number; max: number; farthest: number };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const xy = (v: P2) => `(${v[0].toFixed(2)}, ${v[1].toFixed(2)})`;
|
|
751
|
+
const framesText = (fs: readonly number[]): string => {
|
|
752
|
+
const flag: boolean[] = [];
|
|
753
|
+
for (const f of fs) flag[f] = true;
|
|
754
|
+
return runsOf(Array.from({ length: flag.length }, (_, i) => flag[i] === true), 0).map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
export function raisedLine(e: Raised, times: readonly number[]): string {
|
|
758
|
+
const med = (vs: readonly P2[]): P2 => [median(vs.map((v) => v[0])), median(vs.map((v) => v[1]))];
|
|
759
|
+
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
|
+
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 the head; eye distance (median) rig ${e.eyes.rig.toFixed(2)} units, reference ${e.eyes.reference.toFixed(2)} px; wrist − head centre in eye distances (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
|
+
}
|
|
763
|
+
|
|
764
|
+
/** The REACH line: a raised span whose target was out of the arm's reach on some frames — the proportions, not the art's range, were the limit. */
|
|
765
|
+
export function reachLine(e: Raised, times: readonly number[]): string {
|
|
766
|
+
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 head centre never enters the reach; placed at the reach\'s point nearest it' } as const;
|
|
767
|
+
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)} eye distances short (median ${s.shortfall.median.toFixed(2)})`);
|
|
768
|
+
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 head-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
|
+
}
|
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');
|