@rig-c/pose 0.15.0 → 0.16.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 +15 -2
- package/cli.ts +15 -2
- package/package.json +1 -1
- package/src/retarget.ts +303 -23
- package/src/run.ts +7 -3
- package/src/switch.ts +164 -0
package/README.md
CHANGED
|
@@ -96,6 +96,8 @@ and what the run cannot decide for you it lists:
|
|
|
96
96
|
- `UNCOVERED` — with `alternates` in the map: every fold on a frame where no switch took the folding mesh off screen. This is the list of poses still to paint.
|
|
97
97
|
- `RANGE` — a chain's `range` ([below](#a-joints-range-range)): a bone whose rotate key the pose asks past its range, outside every span of a set depicting its chain, is clamped at the limit; one line per run, with the frames, the seconds and the largest overflow. The demand for an alternate.
|
|
98
98
|
- `ART-RANGE` — inside a set's span: a bone with a `drawn` angle in the set, turned from it past its limits — the set's `turn` for that bone (held at the limit) or, without one, its chain's `range` (the bone follows the pose). Listed as the pose asked it either way: the demand for another drawing of that set.
|
|
99
|
+
- `RAISED` — with `--raised-by-head` ([below](#alternate-drawings-alternates)): one line per span of a set that draws an arm raised, per chain. It gives the frames and the median eye distances (the rig's in units, the reference's in px). It gives the wrist's offset from the head centre in eye distances: the median of the reference's, of where the wrist was placed, of the rig's on the pose's angles, and of the rig's as keyed. It gives the worst |rig − reference| before and after, the worst |after − placed|, and the frames whose upper arm the set's `turn` held. `RAISED none` when the flag is given and no such set showed.
|
|
100
|
+
- `REACH` — with `--raised-by-head`: a raised span whose target was out of the arm's reach, too far or too near. It names the frames, which way, and the shortfall in eye distances. The wrist was placed at the reach limit instead. This is not `ART-RANGE`: the rig's proportions were the limit, not the art's range. A too-near frame is placed at the limit like a too-far one, and graded at the placed distance.
|
|
99
101
|
- `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length, the spread of its picture length over it, and the frames clamped at each limit.
|
|
100
102
|
- `PIVOT` — when the map translates a bone and has both legs: the translated bone's origin against its thigh roots' midpoint at setup, in units and hip widths — the pivot every hip turn swings the legs about.
|
|
101
103
|
- `FEET` — when the map has both legs: the supports and jumps read off the reference ankles (planted or not), and on the posed build, outside the jumps, the lowest ankle's vertical range and each support span's worst vertical range and horizontal drift of the supporting ankle, in leg lengths.
|
|
@@ -105,7 +107,7 @@ and what the run cannot decide for you it lists:
|
|
|
105
107
|
- `REDUCE` — with `--reduce` ([below](#few-keys---reduce)): per bone, its keys before and after and its largest error, with the frame; then the total.
|
|
106
108
|
- `TURN` — a line across the body (hips, shoulders, eyes) shorter in the picture than 0.5 of its median length, or reversed, for at least 0.15 s: the body or the head turned. A reading of the pose, made whether or not the map uses the line; it changes no key. The demand for a turned-body drawing, as `END-ON` is for a foreshortened limb.
|
|
107
109
|
|
|
108
|
-
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet` and `
|
|
110
|
+
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet`, `stance` and `raisedByHead` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate`, `landings`, `posture`, `folds`, `legScale`, `pivot`, `feet` (`planted`, `legLength`, `floor`, `supports`, `jumps`, `changes`, and the posed `reading`), `stance` (`tolerance`, `bounds`, `runs`: `{ state, side, frames, maxHeight, k }`, `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances and each row of `REDUCE`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
|
|
109
111
|
|
|
110
112
|
| field | each entry |
|
|
111
113
|
| --- | --- |
|
|
@@ -122,7 +124,8 @@ The demand lists:
|
|
|
122
124
|
- `endOn` — above, under the short hold.
|
|
123
125
|
- `turns: [{ line, joints, frames, seconds, minFraction, reversed }]` — `line` is `hip`, `shoulder` or `eye`.
|
|
124
126
|
- `range: [{ chain, bone, frames, seconds, maxOverflow, key, limits }]` — `key` is the rotate key the pose asked for on the frame of the largest overflow, before the clamp.
|
|
125
|
-
- `artRange: [{ set, chain, bone, frames, seconds, maxOverflow, turn, limits, clamped }]` — `turn` is the signed art turn the pose asked on that frame; `clamped` says whether the set's `turn` held the bone there.
|
|
127
|
+
- `artRange: [{ set, chain, bone, frames, seconds, maxOverflow, turn, limits, clamped, shortfall? }]` — `turn` is the signed art turn the pose asked on that frame; `clamped` says whether the set's `turn` held the bone there. `shortfall` (with `--raised-by-head`) marks a held upper arm whose re-aimed forearm left the wrist short of its placed point: the most, in eye distances, with `turn` and `maxOverflow` the upper arm's on that frame.
|
|
128
|
+
- `raised: [{ set, chain, frames, seconds, eyes: { rig, reference }, reference, placed, before, after, worst: { before, after, placed }, short, upperHeld, reach: { min, max, farthest } }]` — with `--raised-by-head`. `reference`, `placed`, `before` and `after` are the wrist's offset from the head centre on each frame of the span, `[x, y]` in eye distances, x right and y up. `eyes` holds each side's median eye distance over the span. `worst.before` and `worst.after` are the largest |rig − reference| in x and in y; `worst.placed` is the largest |after − placed|. `short` lists the `REACH` runs, each `{ frames, how, shortfall: { max, median } }`, where `how` is `far`, `near` or `missed`. `upperHeld` lists the frames whose upper arm the set's `turn` held. `reach` gives |l0 − l1|, l0 + l1 and the farthest target from the shoulder, in world units.
|
|
126
129
|
|
|
127
130
|
Runs in `turns`, `range` and `artRange` merge across a gap of one frame, as `endOn` does; `seconds` runs from the first frame's key time to the end of the last frame.
|
|
128
131
|
|
|
@@ -309,6 +312,16 @@ Either way the `SWITCH` line ends with `moved on …, off …`: each boundary's
|
|
|
309
312
|
**A stopgap window: `frames`.** A set may state `"frames": [[first, last], …]`: it shows only inside those windows, and on them it is chosen over any set within tolerance that has no window. It is for two sets whose key skeletons the angles cannot tell apart. Measured on HQ's dancer, the hands clasped behind the back and the hands low in front differ by only 2–21°, and the wrist and hand-point scores do not separate them either; the cue that might is #33's depth state. It is recorded in the map as a stopgap, not a selection rule. A window **restricts** a set; it never forces one. Where the set does not show inside its window, the run says why in a `FRAMES` line: the stretch, and whether the pose there was past the tolerance (with the largest distance), or within it but the run was dropped.
|
|
310
313
|
|
|
311
314
|
**How far a set's art may turn: `turn`.** A drawing reads right only near the angle it was painted at. A set may state, per bone with a `drawn` angle, how far its art may turn from it, `"turn": { "sleeve_r0": [-20, 20] }`. While the set shows, the bone is held within it: its target becomes drawn + clamp(wrap(target − drawn), min, max), each bone on its own world angle, so a forearm under a held upper arm keeps its own target. The landing check measures against the held angle. The overflow the pose asked for is listed as `ART-RANGE`. A `turn` on a bone the set has no `drawn` angle for is refused (`MAP_ALT_TURN`).
|
|
315
|
+
|
|
316
|
+
**A raised arm placed against the head: `--raised-by-head`.** The pose's angles put a hand where it is against the dancer's own shoulder. On a rig whose shoulders sit nearer its head, or whose head is larger, the same angles put a hand raised beside the head on top of it. With `--raised-by-head` (off by default, [#62](https://github.com/rig-c/pose/issues/62)) a set draws an arm **raised** when its `drawn` angle for the chain's first bone (the upper arm) is within 30° of straight up (a world angle of 60°–120°). While that set shows, the arm's wrist goes where the reference's is **against the head**:
|
|
317
|
+
|
|
318
|
+
- The unit is the **eye distance**: on each side, its median over the span. That is the reference's |r_eye − l_eye|, and on the rig the distance between the two points the head chain's `"a>b"` aim names. The head centre is the eyes' midpoint on each frame. The head chain is the one carrying the eye line (r_eye → l_eye, either way round). The flag is refused without one, or with one not aimed `"a>b"` (`RAISED_HEAD`).
|
|
319
|
+
- The target is rig head centre + (reference wrist − reference head centre) × (rig eye distance ÷ reference eye distance), the picture's y turned up, per frame.
|
|
320
|
+
- **The order of solving.** The arm is solved last of the bones. By then the hip's travel, every reach, the head and every range clamp are final, and the arm moves neither the head nor its own shoulder. On each frame the rig's eye points and the shoulder are posed forward from those targets, as a reach's origin is: each rest offset is turned by the nearest mapped bone above.
|
|
321
|
+
- **Reach.** The arm reaches the annulus about the shoulder from |l0 − l1| to l0 + l1. Here l0 runs from the upper arm's origin to the forearm's, and l1 from there to the wrist (the forearm's tip, or the point it aims at). A target outside it is replaced by the reach limit along the head-relative direction. That is the point inside the annulus, on the ray from the head centre through the target, nearest the target. Where the ray crosses the annulus twice, the nearer stretch wins, and a tie goes to the point nearer the head. A ray that never enters it gives the annulus point nearest the target. Every such frame is listed in `REACH`. A too-near frame is treated as a too-far one: the wrist is placed at the reach limit, and the distance the rule grades it at is the placed one. That is |placed − head centre|, not the reference's, which the arm cannot fold to.
|
|
322
|
+
- **The solve.** Two bones on the rig's own lengths put the wrist on that point, the elbow bending to the side the pose's does. The set's `turn` is part of the solve. When it holds the upper arm, the forearm is re-aimed from the held elbow at the placed wrist rather than held on its own. `ART-RANGE` lists where the re-aimed forearm still cannot reach within `turn`, in two cases. One is a forearm past its own `turn`, held there. The other is a held upper arm whose re-aimed forearm leaves the wrist more than 0.01 eye distances short of its placed point; 0.01 is the float and rounding scale. That case is listed on the upper arm, with the shortfall in eye distances. The solved angles become the targets, which the keys and the landing check follow.
|
|
323
|
+
- Frames outside those spans, and sets that draw no arm raised, are keyed as without the flag, byte for byte. A raised chain must have two bones, each aimed `"x"` or at one point, and the eye points must not turn with it (`RAISED_CHAIN`).
|
|
324
|
+
|
|
312
325
|
## Compare: the rig's render against the video
|
|
313
326
|
|
|
314
327
|
```sh
|
package/cli.ts
CHANGED
|
@@ -98,6 +98,18 @@ retarget
|
|
|
98
98
|
reaches the floor, raised or lowered, k within [0.92, 1.08] (past it, reported, not forced); above
|
|
99
99
|
it the foot is lifted and keeps the pose's height (STANCE lines; default: off; 0.07 is
|
|
100
100
|
the card's, rig-c/pose#60)
|
|
101
|
+
--raised-by-head while a set whose arm is drawn raised shows (its "drawn" upper arm within 30° of straight
|
|
102
|
+
up), that arm's wrist goes where the reference's is against the head, in eye distances:
|
|
103
|
+
rig head centre + (reference wrist − reference head centre) × rig eye distance ÷ the
|
|
104
|
+
reference's, per frame; each eye distance is its median over the span. The head centre is
|
|
105
|
+
the eyes' midpoint; the rig's eyes are the two points the head chain's "a>b" aim names
|
|
106
|
+
(refused without one, RAISED_HEAD), posed from the targets of the bones above. A target out
|
|
107
|
+
of the arm's reach, too far or too near, goes to the reach limit on the ray from the head
|
|
108
|
+
centre toward it and is graded at that placed distance (REACH lines, in eye distances). A
|
|
109
|
+
two-bone solve on the rig's own lengths keeps the pose's elbow side; when the set's "turn"
|
|
110
|
+
holds the upper arm the forearm is re-aimed at the wrist. ART-RANGE: a forearm still past
|
|
111
|
+
its turn, or a held arm leaving the wrist > 0.01 eye distances short of its placed point
|
|
112
|
+
(the shortfall listed). RAISED lines (default: off; rig-c/pose#62)
|
|
101
113
|
--idle <name> the rig's animation read as its idle (default: idle). Every idle track the new
|
|
102
114
|
animation does not key, on any channel, is listed as IDLE-DROPPED (a blink, a
|
|
103
115
|
breath); one it keys is IDLE-KEPT-OUT, naming the track that keys it, and so is a
|
|
@@ -129,7 +141,7 @@ retarget
|
|
|
129
141
|
--build <dir> keep the gated build (skeleton.json, .atlas, .model.json) here
|
|
130
142
|
--render <dir> keep rigc's render of the animation (frames, contact.png, folds.json) here
|
|
131
143
|
--report <file> write the run's measurements (holds, short, endOn, flips, bends, landings, folds,
|
|
132
|
-
switches, uncovered, turns, range, artRange,
|
|
144
|
+
switches, uncovered, turns, range, artRange, raised,
|
|
133
145
|
idleDropped, idleKeptOut, merged, reduce) as JSON
|
|
134
146
|
|
|
135
147
|
It writes nothing until rigc's gate is green with the animation in, every mapped bone lands on
|
|
@@ -228,7 +240,7 @@ function legScaleOf(f: Map<string, string>): [number, number] | 'off' {
|
|
|
228
240
|
}
|
|
229
241
|
|
|
230
242
|
function retargetArgs(argv: string[]): RetargetArgs {
|
|
231
|
-
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'render', 'report', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance'], ['coco', 'plant-feet', 'carry-idle']);
|
|
243
|
+
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'render', 'report', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head']);
|
|
232
244
|
const bends = f.get('bends') ?? 'refuse';
|
|
233
245
|
if (bends !== 'refuse' && bends !== 'clamp') throw new Usage(`--bends is "${bends}"; it must be refuse or clamp`);
|
|
234
246
|
const switchAt = f.get('switch-at') ?? 'first';
|
|
@@ -254,6 +266,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
|
|
|
254
266
|
legScale: legScaleOf(f),
|
|
255
267
|
plantFeet: f.has('plant-feet'),
|
|
256
268
|
stance: stanceOf(f),
|
|
269
|
+
raisedByHead: f.has('raised-by-head'),
|
|
257
270
|
bends,
|
|
258
271
|
bendTolerance: num(f, 'bend-tolerance', 0, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
|
|
259
272
|
switchTolerance: num(f, 'switch-tolerance', 25, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/retarget.ts
CHANGED
|
@@ -50,6 +50,14 @@
|
|
|
50
50
|
* Inside a span nothing is clamped, but a bone with a `drawn` angle is read
|
|
51
51
|
* against it (`artTurn`, and `artRange` where it leaves its range).
|
|
52
52
|
*
|
|
53
|
+
* With `raisedByHead` (rig-c/pose#62), after the range clamp and before the art
|
|
54
|
+
* turn is read: inside each span of a set that draws an arm raised, the arm is
|
|
55
|
+
* solved last, so its wrist lands where the reference's is against the head, in
|
|
56
|
+
* eye distances (each side's median over the span). The head and the shoulder
|
|
57
|
+
* are posed from the final targets of the bones above. A target out of reach
|
|
58
|
+
* goes to the reach limit along the head-relative direction (REACH); the set's
|
|
59
|
+
* `turn` is part of the solve, and only a forearm it still holds is ART-RANGE.
|
|
60
|
+
*
|
|
53
61
|
* The lines across the body — hips, shoulders, eyes — are read from the pose
|
|
54
62
|
* after holds, whether the map uses them or not: a line under
|
|
55
63
|
* `TURN_FRACTION` of its median picture length, or reversed (pointing more
|
|
@@ -63,16 +71,26 @@ import { screenToSpineDegrees } from 'rig-c/transform';
|
|
|
63
71
|
import type { Body18 } from './joints.ts';
|
|
64
72
|
import { isBody18, VIRTUAL } from './joints.ts';
|
|
65
73
|
import type { Chain, JointMap, Rest, RigSlots } from './map.ts';
|
|
66
|
-
import { aimAngle, descends, rigPoint } from './map.ts';
|
|
74
|
+
import { aimAngle, aimEnds, descends, partBone, pointParts, rigPoint } from './map.ts';
|
|
67
75
|
import type { Joint } from './pose.ts';
|
|
68
76
|
import { Problems, Refusal } from './problems.ts';
|
|
69
|
-
import type { ArtRange, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
70
|
-
import { clampArt, coveredChains, measureArt, overflow, RANGE_SLACK, runsOf, selectSwitches } from './switch.ts';
|
|
77
|
+
import type { ArtRange, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
78
|
+
import { clampArt, coveredChains, isRaised, measureArt, overflow, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
71
79
|
import type { Feet, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
72
80
|
import { isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, SIDES, stanceOf, supportsOf, tipOf } from './legs.ts';
|
|
73
81
|
|
|
74
82
|
const DEG = 180 / Math.PI;
|
|
75
83
|
|
|
84
|
+
/**
|
|
85
|
+
* --raised-by-head (#62): a held upper arm whose re-aimed forearm leaves the
|
|
86
|
+
* wrist farther than this from its placed point, in eye distances, "cannot
|
|
87
|
+
* reach within turn" — ART-RANGE. It sits at the float and rounding scale, so
|
|
88
|
+
* any real miss counts: the solve itself closes to ~1e-12, and the writer's
|
|
89
|
+
* 0.001° rounding of two keys moves a wrist by ≤ 2 × 1.75e-5 × (l0 + l1), which
|
|
90
|
+
* is 0.0004 eye distances on the tests' stick rig (a 130-unit arm, 12-unit eyes).
|
|
91
|
+
*/
|
|
92
|
+
export const HELD_MISS = 0.01;
|
|
93
|
+
|
|
76
94
|
export interface RetargetOptions extends SwitchOptions {
|
|
77
95
|
/** A joint with a score below this is not good on that frame (a joint with no score is). */
|
|
78
96
|
minScore: number;
|
|
@@ -90,6 +108,8 @@ export interface RetargetOptions extends SwitchOptions {
|
|
|
90
108
|
plantFeet?: boolean;
|
|
91
109
|
/** With plantFeet: a free ankle within this many leg lengths of the floor plants both feet, its leg stretched to reach it (`legs.ts` stanceOf, #60); 'off' (the default) plants one. */
|
|
92
110
|
stance?: number | 'off';
|
|
111
|
+
/** While a set whose arm is drawn raised shows, its wrist is placed against the rig's head in eye distances (rig-c/pose#62); false (the default) keeps the pose's angles. */
|
|
112
|
+
raisedByHead?: boolean;
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
export interface Hold {
|
|
@@ -272,6 +292,8 @@ export interface Retarget {
|
|
|
272
292
|
ankles: { bones: Record<Side, string>; at: Record<Side, Pt[]> } | null;
|
|
273
293
|
/** The translated bone's origin against its thigh roots' midpoint, at setup. */
|
|
274
294
|
pivot: Pivot | null;
|
|
295
|
+
/** With --raised-by-head: each raised set's span on each raised chain, its wrist placed against the head. */
|
|
296
|
+
raised: Raised[];
|
|
275
297
|
}
|
|
276
298
|
|
|
277
299
|
/** The hip's pivot against the thigh roots, at setup (rig-c/pose#47). */
|
|
@@ -368,6 +390,11 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
368
390
|
const endOf = (i: number): number => (i + 1 < n ? keyTimes[i + 1]! : n > 1 ? 2 * keyTimes[i]! - keyTimes[i - 1]! : keyTimes[i]!);
|
|
369
391
|
const secondsOf = (run: [number, number]): number => Math.round((endOf(run[1]) - keyTimes[run[0]]!) * 1e6) / 1e6;
|
|
370
392
|
|
|
393
|
+
// --raised-by-head (#62) measures against the head in eye distances: the map's head chain must carry the
|
|
394
|
+
// eye line with an "a>b" aim, whose two ends are the rig's eyes.
|
|
395
|
+
const head = opts.raisedByHead === true ? headOf(map) : null;
|
|
396
|
+
if (head !== null && 'problem' in head) p.add('RAISED_HEAD', head.problem);
|
|
397
|
+
|
|
371
398
|
// Every point the map reads, held where it is not good.
|
|
372
399
|
const needed = new Set<string>();
|
|
373
400
|
for (const c of map.chains) c.joints.forEach((j) => needed.add(j));
|
|
@@ -713,29 +740,52 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
713
740
|
for (let a = rest.get(b)!.parent; a !== null; a = rest.get(a)!.parent) d++;
|
|
714
741
|
return d;
|
|
715
742
|
};
|
|
743
|
+
/** The world turn of `b`'s nearest mapped bone-or-ancestor, degrees, on frame i, by the targets as they stand. */
|
|
744
|
+
const turnAbove = (b: string, i: number): number => {
|
|
745
|
+
for (let a: string | null = b; a !== null; a = rest.get(a)!.parent) {
|
|
746
|
+
const tg = targets.get(a);
|
|
747
|
+
if (tg !== undefined) return tg[i]! - restOf.get(a)!;
|
|
748
|
+
}
|
|
749
|
+
return 0;
|
|
750
|
+
};
|
|
751
|
+
/**
|
|
752
|
+
* Where the poser will put `b`'s origin on frame i: forward from the
|
|
753
|
+
* translated bone (moved by its travel) or the root (at setup), each rest
|
|
754
|
+
* offset turned by the nearest mapped bone above — the key formula's own
|
|
755
|
+
* assumption (unit scale, no shear), which the landing check holds to account.
|
|
756
|
+
*/
|
|
757
|
+
const posedOrigin = (b: string, i: number): Pt => {
|
|
758
|
+
const own = rest.get(b)!;
|
|
759
|
+
if (t !== null && b === t.bone) return [own.worldX + (travel[i]?.[0] ?? 0), own.worldY + (travel[i]?.[1] ?? 0)];
|
|
760
|
+
if (own.parent === null) return [own.worldX, own.worldY];
|
|
761
|
+
const parent = own.parent;
|
|
762
|
+
const po = posedOrigin(parent, i);
|
|
763
|
+
const r = turnAbove(parent, i) / DEG;
|
|
764
|
+
const ox = own.worldX - rest.get(parent)!.worldX;
|
|
765
|
+
const oy = own.worldY - rest.get(parent)!.worldY;
|
|
766
|
+
return [po[0] + Math.cos(r) * ox - Math.sin(r) * oy, po[1] + Math.sin(r) * ox + Math.cos(r) * oy];
|
|
767
|
+
};
|
|
768
|
+
/** A rig point (a bone, `name:tip`, `a+b`) posed on frame i the same way. */
|
|
769
|
+
const posedPoint = (spec: string, i: number): Pt => {
|
|
770
|
+
let x = 0;
|
|
771
|
+
let y = 0;
|
|
772
|
+
const parts = pointParts(spec);
|
|
773
|
+
for (const part of parts) {
|
|
774
|
+
const name = partBone(part);
|
|
775
|
+
const o = posedOrigin(name, i);
|
|
776
|
+
const bone = rest.get(name)!;
|
|
777
|
+
const l = part.endsWith(':tip') ? bone.length : 0;
|
|
778
|
+
const r = turnAbove(name, i) / DEG;
|
|
779
|
+
x += o[0] + l * (Math.cos(r) * bone.a - Math.sin(r) * bone.c);
|
|
780
|
+
y += o[1] + l * (Math.sin(r) * bone.a + Math.cos(r) * bone.c);
|
|
781
|
+
}
|
|
782
|
+
return [x / parts.length, y / parts.length];
|
|
783
|
+
};
|
|
716
784
|
const reaches = map.chains.filter((c) => c.reach).sort((x, y) => depth(x.bones[0]!) - depth(y.bones[0]!));
|
|
717
785
|
let aimReach: ((c: Chain) => void) | null = null;
|
|
718
786
|
if (reaches.length > 0 && t !== null && scale !== null) {
|
|
719
787
|
const s = scale.factor;
|
|
720
788
|
const tb = rest.get(t.bone)!;
|
|
721
|
-
/** The world turn of `b`'s nearest mapped bone-or-ancestor, degrees, on frame i. */
|
|
722
|
-
const turnAbove = (b: string, i: number): number => {
|
|
723
|
-
for (let a: string | null = b; a !== null; a = rest.get(a)!.parent) {
|
|
724
|
-
const tg = targets.get(a);
|
|
725
|
-
if (tg !== undefined) return tg[i]! - restOf.get(a)!;
|
|
726
|
-
}
|
|
727
|
-
return 0;
|
|
728
|
-
};
|
|
729
|
-
/** Where the poser will put `b`'s origin on frame i: forward from the translated bone. */
|
|
730
|
-
const posedOrigin = (b: string, i: number): Pt => {
|
|
731
|
-
if (b === t.bone) return [tb.worldX + travel[i]![0], tb.worldY + travel[i]![1]];
|
|
732
|
-
const parent = rest.get(b)!.parent!;
|
|
733
|
-
const po = posedOrigin(parent, i);
|
|
734
|
-
const r = turnAbove(parent, i) / DEG;
|
|
735
|
-
const ox = rest.get(b)!.worldX - rest.get(parent)!.worldX;
|
|
736
|
-
const oy = rest.get(b)!.worldY - rest.get(parent)!.worldY;
|
|
737
|
-
return [po[0] + Math.cos(r) * ox - Math.sin(r) * oy, po[1] + Math.sin(r) * ox + Math.cos(r) * oy];
|
|
738
|
-
};
|
|
739
789
|
aimReach = (c: Chain): void => {
|
|
740
790
|
const b = c.bones[0]!;
|
|
741
791
|
const anchor = series.get(c.joints[0]!)!;
|
|
@@ -793,8 +843,214 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
793
843
|
}
|
|
794
844
|
}
|
|
795
845
|
p.throwIfAny();
|
|
796
|
-
|
|
846
|
+
|
|
847
|
+
// --raised-by-head (#62, HQ's ruling): while a set whose arm is drawn raised shows, its wrist goes where
|
|
848
|
+
// the reference's is against the head, in eye distances (each side's median over the span). Solved last
|
|
849
|
+
// of the bones: every bone above the arm, the head and the trunk's reach included, has its final target
|
|
850
|
+
// (range clamp done), so the head and the shoulder are posed from them frame by frame; the arm moves
|
|
851
|
+
// neither. A target out of reach is placed at the reach limit along the head-relative direction
|
|
852
|
+
// (`reachLimit`, a REACH line). The set's `turn` is part of the solve: a held upper arm has its forearm
|
|
853
|
+
// re-aimed at the placed wrist, and only a forearm the turn still holds is ART-RANGE.
|
|
854
|
+
const raised: Raised[] = [];
|
|
855
|
+
const raisedArt: Array<{ sw: Switch; entries: ArtRange[] }> = [];
|
|
856
|
+
const raisedJobs: Array<{ sw: Switch; chain: Chain; wrist: (i: number) => Pt; before: Pt[]; reference: Pt[]; placed: Pt[]; refEye: number; rigEye: number; short: Array<{ i: number; how: 'far' | 'near' | 'missed'; shortfall: number }>; upperHeld: number[]; l0: number; l1: number; farthest: number; eyePoints: [string, string] }> = [];
|
|
857
|
+
if (head !== null && !('problem' in head)) {
|
|
858
|
+
const eyesOn = (i: number): { centre: Pt; dist: number } => {
|
|
859
|
+
const a = posedPoint(head.points[0], i);
|
|
860
|
+
const b = posedPoint(head.points[1], i);
|
|
861
|
+
return { centre: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2], dist: Math.hypot(b[0] - a[0], b[1] - a[1]) };
|
|
862
|
+
};
|
|
863
|
+
const headBones = new Set([...head.points.flatMap((x) => pointParts(x).map(partBone))]);
|
|
864
|
+
const ra = series.get(head.joints[0])!;
|
|
865
|
+
const rb = series.get(head.joints[1])!;
|
|
866
|
+
const refCentre = (i: number): Pt => [(ra[i]![0] + rb[i]![0]) / 2, (ra[i]![1] + rb[i]![1]) / 2];
|
|
867
|
+
for (const s of sw.switches) {
|
|
868
|
+
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
869
|
+
const art: ArtRange[] = [];
|
|
870
|
+
for (const cn of s.chains) {
|
|
871
|
+
const c = map.chains.find((x) => x.name === cn)!;
|
|
872
|
+
const drawn = set.drawn[c.bones[0]!];
|
|
873
|
+
if (drawn === undefined || !isRaised(drawn)) continue;
|
|
874
|
+
const label = `--raised-by-head: set "${set.name}" draws chain "${c.name}" raised (bone "${c.bones[0]}" at ${drawn}°)`;
|
|
875
|
+
if (c.bones.length !== 2) {
|
|
876
|
+
p.add('RAISED_CHAIN', `${label}, and the chain has ${c.bones.length} bone(s); the wrist is placed by a two-bone solve, so it must have two (upper arm, forearm)`);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (c.reach || c.aim.some((x) => x.includes('>'))) {
|
|
880
|
+
p.add('RAISED_CHAIN', `${label}, and it aims ${JSON.stringify(c.aim)}${c.reach ? ' with "reach"' : ''}; the solve turns each bone about its own origin toward the next point, so each must aim "x" or at one point`);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const [b0, b1] = c.bones as [string, string];
|
|
884
|
+
const moved = [...headBones].filter((h) => h === b0 || h === b1 || descends(rest, h, b0));
|
|
885
|
+
if (moved.length > 0) {
|
|
886
|
+
p.add('RAISED_CHAIN', `${label}, and the head's eye point ${moved.map((h) => `"${h}"`).join(', ')} turns with it; the wrist cannot be placed against a head the arm moves`);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const wristSpec = c.aim[1] === 'x' ? `${b1}:tip` : c.aim[1]!;
|
|
890
|
+
if (c.aim[1] === 'x' && !(rest.get(b1)!.length > 1e-9)) {
|
|
891
|
+
p.add('RAISED_CHAIN', `${label}, and its last bone "${b1}" aims "x" with no length; its wrist (the tip) is its origin, so there is nothing to place`);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const wristOf = (i: number): Pt => posedPoint(wristSpec, i);
|
|
895
|
+
const refWrist = series.get(c.joints[2]!)!;
|
|
896
|
+
// The unit, each side's median over the span (HQ's ruling).
|
|
897
|
+
const span: number[] = [];
|
|
898
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) span.push(i);
|
|
899
|
+
const refEye = median(span.map((i) => Math.hypot(rb[i]![0] - ra[i]![0], rb[i]![1] - ra[i]![1])));
|
|
900
|
+
const rigEye = median(span.map((i) => eyesOn(i).dist));
|
|
901
|
+
if (!(refEye > 1e-9) || !(rigEye > 1e-9)) {
|
|
902
|
+
p.add('RAISED_EYES', `${label}: over frames ${s.frames[0]}–${s.frames[1]} the median eye distance is ${refEye > 1e-9 ? `${rigEye} units on the rig (points "${head.points[0]}", "${head.points[1]}")` : `${refEye}px in the reference (${head.joints[0]}, ${head.joints[1]})`}; an eye distance of 0 is no unit`);
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
// The rig's own lengths, and each bone's aim against the vector it carries, at setup.
|
|
906
|
+
const S0 = rigPoint(rest, b0)!;
|
|
907
|
+
const E0 = rigPoint(rest, b1)!;
|
|
908
|
+
const W0 = rigPoint(rest, wristSpec)!;
|
|
909
|
+
const l0 = Math.hypot(E0[0] - S0[0], E0[1] - S0[1]);
|
|
910
|
+
const l1 = Math.hypot(W0[0] - E0[0], W0[1] - E0[1]);
|
|
911
|
+
const d0 = wrap(Math.atan2(E0[1] - S0[1], E0[0] - S0[0]) * DEG - restOf.get(b0)!);
|
|
912
|
+
const d1 = wrap(Math.atan2(W0[1] - E0[1], W0[0] - E0[0]) * DEG - restOf.get(b1)!);
|
|
913
|
+
const t0 = targets.get(b0)!;
|
|
914
|
+
const t1 = targets.get(b1)!;
|
|
915
|
+
const turn0 = set.turn[b0];
|
|
916
|
+
const turn1 = set.turn[b1];
|
|
917
|
+
const job: (typeof raisedJobs)[number] = { sw: s, chain: c, wrist: wristOf, before: [], reference: [], placed: [], refEye, rigEye, short: [], upperHeld: [], l0, l1, farthest: 0, eyePoints: head.points };
|
|
918
|
+
/** Per frame of the span: the forearm's art turn the re-aim asked, and how far past its turn. */
|
|
919
|
+
const foreAsked: number[] = [];
|
|
920
|
+
const foreOver: number[] = [];
|
|
921
|
+
/** Per frame of the span: the upper arm's art turn the solve asked, how far past its turn, and — where the turn held it — how far the wrist ends from its placed point, eye distances (0 elsewhere). */
|
|
922
|
+
const upperAsked: number[] = [];
|
|
923
|
+
const upperOver: number[] = [];
|
|
924
|
+
const heldMiss: number[] = [];
|
|
925
|
+
for (const i of span) {
|
|
926
|
+
const h = eyesOn(i);
|
|
927
|
+
const rc = refCentre(i);
|
|
928
|
+
// The reference's wrist from its head centre, in its eye distances, y turned up.
|
|
929
|
+
const ref: Pt = [(refWrist[i]![0] - rc[0]) / refEye, -(refWrist[i]![1] - rc[1]) / refEye];
|
|
930
|
+
const w = wristOf(i);
|
|
931
|
+
job.reference.push(ref);
|
|
932
|
+
job.before.push([(w[0] - h.centre[0]) / rigEye, (w[1] - h.centre[1]) / rigEye]);
|
|
933
|
+
const target: Pt = [h.centre[0] + ref[0] * rigEye, h.centre[1] + ref[1] * rigEye];
|
|
934
|
+
const shoulder = posedOrigin(b0, i);
|
|
935
|
+
job.farthest = Math.max(job.farthest, Math.hypot(target[0] - shoulder[0], target[1] - shoulder[1]));
|
|
936
|
+
const place = reachLimit(h.centre, target, shoulder, l0, l1);
|
|
937
|
+
const P = place.point;
|
|
938
|
+
job.placed.push([(P[0] - h.centre[0]) / rigEye, (P[1] - h.centre[1]) / rigEye]);
|
|
939
|
+
if (place.how !== 'in') job.short.push({ i, how: place.how, shortfall: Math.hypot(target[0] - P[0], target[1] - P[1]) / rigEye });
|
|
940
|
+
// The pose's bend side, read on the vectors the two bones carry.
|
|
941
|
+
const side: 1 | -1 = wrap(t1[i]! + d1 - (t0[i]! + d0)) >= 0 ? 1 : -1;
|
|
942
|
+
const sol = twoBone(shoulder, P, l0, l1, side);
|
|
943
|
+
if (sol.upper === null) {
|
|
944
|
+
foreAsked.push(wrap(t1[i]! - (set.drawn[b1] ?? 0)));
|
|
945
|
+
foreOver.push(0);
|
|
946
|
+
upperAsked.push(0);
|
|
947
|
+
upperOver.push(0);
|
|
948
|
+
heldMiss.push(0);
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
let a0 = wrap(sol.upper - d0);
|
|
952
|
+
let a1 = wrap(sol.fore - d1);
|
|
953
|
+
// The set's turn holds the upper arm; the forearm is then re-aimed at the placed wrist from where the held upper arm puts the elbow.
|
|
954
|
+
let held = false;
|
|
955
|
+
const tt = turn0 === undefined ? 0 : wrap(a0 - set.drawn[b0]!);
|
|
956
|
+
upperAsked.push(tt);
|
|
957
|
+
upperOver.push(turn0 === undefined ? 0 : overflow(tt, turn0));
|
|
958
|
+
if (turn0 !== undefined) {
|
|
959
|
+
const cl = Math.min(Math.max(tt, turn0[0]), turn0[1]);
|
|
960
|
+
if (Math.abs(cl - tt) > RANGE_SLACK) {
|
|
961
|
+
held = true;
|
|
962
|
+
a0 = wrap(set.drawn[b0]! + cl);
|
|
963
|
+
job.upperHeld.push(i);
|
|
964
|
+
const r = (a0 + d0) / DEG;
|
|
965
|
+
const E: Pt = [shoulder[0] + l0 * Math.cos(r), shoulder[1] + l0 * Math.sin(r)];
|
|
966
|
+
a1 = wrap(Math.atan2(P[1] - E[1], P[0] - E[0]) * DEG - d1);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// A forearm the turn still holds: the re-aim failed within the art's range — ART-RANGE.
|
|
970
|
+
const asked = turn1 === undefined ? 0 : wrap(a1 - set.drawn[b1]!);
|
|
971
|
+
const over = turn1 === undefined ? 0 : overflow(asked, turn1);
|
|
972
|
+
foreAsked.push(asked);
|
|
973
|
+
foreOver.push(over);
|
|
974
|
+
if (turn1 !== undefined && over > RANGE_SLACK) a1 = wrap(set.drawn[b1]! + Math.min(Math.max(asked, turn1[0]), turn1[1]));
|
|
975
|
+
// Where the held arm leaves the wrist: the re-aimed forearm points at the placed point, but from a held
|
|
976
|
+
// elbow its length need not reach it (rule 3, "cannot reach within turn").
|
|
977
|
+
const r0 = (a0 + d0) / DEG;
|
|
978
|
+
const r1 = (a1 + d1) / DEG;
|
|
979
|
+
const W: Pt = [shoulder[0] + l0 * Math.cos(r0) + l1 * Math.cos(r1), shoulder[1] + l0 * Math.sin(r0) + l1 * Math.sin(r1)];
|
|
980
|
+
heldMiss.push(held ? Math.hypot(W[0] - P[0], W[1] - P[1]) / rigEye : 0);
|
|
981
|
+
t0[i] = a0;
|
|
982
|
+
t1[i] = a1;
|
|
983
|
+
}
|
|
984
|
+
// A held upper arm whose re-aimed forearm leaves the wrist more than HELD_MISS short of its placed point: ART-RANGE on the upper arm, with the shortfall.
|
|
985
|
+
if (turn0 !== undefined) {
|
|
986
|
+
for (const [a, b] of runsOf(heldMiss.map((m) => m > HELD_MISS), RANGE_GAP)) {
|
|
987
|
+
let worst = a;
|
|
988
|
+
for (let k = a; k <= b; k++) if (heldMiss[k]! > heldMiss[worst]!) worst = k;
|
|
989
|
+
const first = s.frames[0] + a;
|
|
990
|
+
const last = s.frames[0] + b;
|
|
991
|
+
art.push({ set: set.name, chain: c.name, bone: b0, frames: [first, last], seconds: r6(endOf(last) - keyTimes[first]!), maxOverflow: r3(upperOver[worst]!), turn: r3(upperAsked[worst]!), limits: turn0, clamped: true, shortfall: r3(heldMiss[worst]!) });
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (turn1 !== undefined) {
|
|
995
|
+
for (const [a, b] of runsOf(foreOver.map((o) => o > RANGE_SLACK), RANGE_GAP)) {
|
|
996
|
+
let worst = a;
|
|
997
|
+
for (let k = a; k <= b; k++) if (foreOver[k]! > foreOver[worst]!) worst = k;
|
|
998
|
+
const first = s.frames[0] + a;
|
|
999
|
+
const last = s.frames[0] + b;
|
|
1000
|
+
art.push({ set: set.name, chain: c.name, bone: b1, frames: [first, last], seconds: r6(endOf(last) - keyTimes[first]!), maxOverflow: r3(foreOver[worst]!), turn: r3(foreAsked[worst]!), limits: turn1, clamped: true });
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
raisedJobs.push(job);
|
|
1004
|
+
}
|
|
1005
|
+
art.sort((x, y) => x.frames[0] - y.frames[0]);
|
|
1006
|
+
if (art.length > 0) raisedArt.push({ sw: s, entries: art });
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
p.throwIfAny();
|
|
1010
|
+
// The art turn of every span; a raised chain's bones were held by the solve, and only its re-aimed forearm is listed (above).
|
|
1011
|
+
const measured = measureArt(sw.switches, map, targets, endOf, keyTimes, RANGE_GAP);
|
|
1012
|
+
const solvedBy = (e: ArtRange, s: Switch) => raisedJobs.some((j) => j.sw === s && j.chain.name === e.chain);
|
|
1013
|
+
const artRange: ArtRange[] = [];
|
|
1014
|
+
for (const s of sw.switches) {
|
|
1015
|
+
const own = measured.filter((e) => e.set === s.set && e.frames[0] >= s.frames[0] && e.frames[1] <= s.frames[1]);
|
|
1016
|
+
artRange.push(...own.filter((e) => !solvedBy(e, s)), ...(raisedArt.find((x) => x.sw === s)?.entries ?? []));
|
|
1017
|
+
}
|
|
797
1018
|
clampArt(sw.switches, map, targets);
|
|
1019
|
+
// Where each raised wrist lands as keyed: after the solve and the set's `turn`.
|
|
1020
|
+
for (const job of raisedJobs) {
|
|
1021
|
+
const after: Pt[] = [];
|
|
1022
|
+
for (let i = job.sw.frames[0]; i <= job.sw.frames[1]; i++) {
|
|
1023
|
+
const a = posedPoint(job.eyePoints[0], i);
|
|
1024
|
+
const b = posedPoint(job.eyePoints[1], i);
|
|
1025
|
+
const w = job.wrist(i);
|
|
1026
|
+
after.push([(w[0] - (a[0] + b[0]) / 2) / job.rigEye, (w[1] - (a[1] + b[1]) / 2) / job.rigEye]);
|
|
1027
|
+
}
|
|
1028
|
+
const worst = (rig: readonly Pt[]): Pt => [Math.max(...rig.map((v, k) => Math.abs(v[0] - job.reference[k]![0]))), Math.max(...rig.map((v, k) => Math.abs(v[1] - job.reference[k]![1])))].map(r6) as Pt;
|
|
1029
|
+
const round = (vs: readonly Pt[]): Pt[] => vs.map((v) => [r6(v[0]), r6(v[1])]);
|
|
1030
|
+
const short: ReachShort[] = [];
|
|
1031
|
+
for (let k = 0; k < job.short.length;) {
|
|
1032
|
+
let m = k;
|
|
1033
|
+
while (m + 1 < job.short.length && job.short[m + 1]!.i === job.short[m]!.i + 1 && job.short[m + 1]!.how === job.short[k]!.how) m++;
|
|
1034
|
+
const fs = job.short.slice(k, m + 1).map((x) => x.shortfall);
|
|
1035
|
+
short.push({ frames: [job.short[k]!.i, job.short[m]!.i], how: job.short[k]!.how, shortfall: { max: r6(Math.max(...fs)), median: r6(median(fs)) } });
|
|
1036
|
+
k = m + 1;
|
|
1037
|
+
}
|
|
1038
|
+
raised.push({
|
|
1039
|
+
set: job.sw.set,
|
|
1040
|
+
chain: job.chain.name,
|
|
1041
|
+
frames: job.sw.frames,
|
|
1042
|
+
seconds: secondsOf(job.sw.frames),
|
|
1043
|
+
eyes: { rig: r6(job.rigEye), reference: r6(job.refEye) },
|
|
1044
|
+
reference: round(job.reference),
|
|
1045
|
+
placed: round(job.placed),
|
|
1046
|
+
before: round(job.before),
|
|
1047
|
+
after: round(after),
|
|
1048
|
+
worst: { before: worst(job.before), after: worst(after), placed: r6(Math.max(...after.map((v, k) => Math.hypot(v[0] - job.placed[k]![0], v[1] - job.placed[k]![1])))) },
|
|
1049
|
+
short,
|
|
1050
|
+
upperHeld: job.upperHeld,
|
|
1051
|
+
reach: { min: r6(Math.abs(job.l0 - job.l1)), max: r6(job.l0 + job.l1), farthest: r6(job.farthest) },
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
798
1054
|
|
|
799
1055
|
// The legs on their final targets: a bone under a scaled bone is keyed through
|
|
800
1056
|
// its parent's posed matrix (legs.ts), and each ankle's world position is read.
|
|
@@ -854,10 +1110,34 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
854
1110
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
855
1111
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale,
|
|
856
1112
|
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
857
|
-
legScale, scales, feet, planted: plantOn, stance, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs),
|
|
1113
|
+
legScale, scales, feet, planted: plantOn, stance, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised,
|
|
858
1114
|
};
|
|
859
1115
|
}
|
|
860
1116
|
|
|
1117
|
+
/** The head as --raised-by-head reads it: the chain carrying the eye line, its two pose joints and the rig's two eye points. */
|
|
1118
|
+
interface Head {
|
|
1119
|
+
chain: string;
|
|
1120
|
+
joints: [string, string];
|
|
1121
|
+
points: [string, string];
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/** The map's head chain (the one carrying r_eye → l_eye, either way round) and its "a>b" aim; or why there is none to read. */
|
|
1125
|
+
function headOf(map: JointMap): Head | { problem: string } {
|
|
1126
|
+
const lead = '--raised-by-head places a raised arm\'s wrist against the head in eye distances, read on the rig between the two points the head chain\'s "a>b" aim names';
|
|
1127
|
+
for (const c of map.chains) {
|
|
1128
|
+
for (let k = 0; k + 1 < c.joints.length; k++) {
|
|
1129
|
+
const a = c.joints[k]!;
|
|
1130
|
+
const b = c.joints[k + 1]!;
|
|
1131
|
+
if (!((a === 'r_eye' && b === 'l_eye') || (a === 'l_eye' && b === 'r_eye'))) continue;
|
|
1132
|
+
const aim = c.aim[k]!;
|
|
1133
|
+
if (!aim.includes('>')) return { problem: `${lead}; chain "${c.name}" carries ${a} → ${b} on bone "${c.bones[k]}", and aims it ${JSON.stringify(aim)}, not "a>b" between the rig's eye points` };
|
|
1134
|
+
const [pa, pb] = aimEnds(c.bones[k]!, aim);
|
|
1135
|
+
return { chain: c.name, joints: [a, b], points: [pa, pb] };
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return { problem: `${lead}; no chain of the map carries the eye line (joints r_eye → l_eye), so the rig has no eye points to measure` };
|
|
1139
|
+
}
|
|
1140
|
+
|
|
861
1141
|
type ShortSpan = Omit<Short, 'chain' | 'bone' | 'joints'>;
|
|
862
1142
|
|
|
863
1143
|
/**
|
package/src/run.ts
CHANGED
|
@@ -14,7 +14,7 @@ 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';
|
|
@@ -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`);
|
|
@@ -273,7 +276,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
273
276
|
frames: seq.frames.length,
|
|
274
277
|
duration: r.animation.duration,
|
|
275
278
|
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' },
|
|
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' },
|
|
277
280
|
scale: r.scale,
|
|
278
281
|
holds: r.holds,
|
|
279
282
|
short: r.short,
|
|
@@ -289,6 +292,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
289
292
|
turns: r.turns,
|
|
290
293
|
range: r.range,
|
|
291
294
|
artRange: r.artRange,
|
|
295
|
+
raised: r.raised,
|
|
292
296
|
legScale: r.legScale.map(({ scale: _scale, ...rest }) => rest),
|
|
293
297
|
pivot: r.pivot,
|
|
294
298
|
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, bounds: r.stance.bounds, runs: r.stance.runs, over: r.stance.over },
|
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
|
+
}
|