@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/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';
|
|
71
|
-
import type { Feet, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
72
|
-
import { isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, SIDES, stanceOf, supportsOf, tipOf } from './legs.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';
|
|
79
|
+
import type { BlendHold, Feet, Floor, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
80
|
+
import { holdBlend, 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,10 @@ 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
|
+
/** With plantFeet: the floor's world y every support span is planted on (#85); absent, the median of the spans' planted heights. */
|
|
112
|
+
floor?: number;
|
|
113
|
+
/** 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. */
|
|
114
|
+
raisedByHead?: boolean;
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
export interface Hold {
|
|
@@ -268,10 +290,16 @@ export interface Retarget {
|
|
|
268
290
|
planted: boolean;
|
|
269
291
|
/** With --stance: the frames planted both and lifted, and the free leg's stretch (#60). */
|
|
270
292
|
stance: Stance | null;
|
|
293
|
+
/** With --plant-feet: the one floor every support span was planted on, and each span's shift onto it (#85). */
|
|
294
|
+
floor: Floor | null;
|
|
295
|
+
/** With --plant-feet: the supporting leg stretched on the blend frames so its ankle stays on the floor (#85). */
|
|
296
|
+
blendHold: BlendHold | null;
|
|
271
297
|
/** Each ankle's world position on every frame, as the keys put it, when legs are scaled or feet planted. */
|
|
272
298
|
ankles: { bones: Record<Side, string>; at: Record<Side, Pt[]> } | null;
|
|
273
299
|
/** The translated bone's origin against its thigh roots' midpoint, at setup. */
|
|
274
300
|
pivot: Pivot | null;
|
|
301
|
+
/** With --raised-by-head: each raised set's span on each raised chain, its wrist placed against the head. */
|
|
302
|
+
raised: Raised[];
|
|
275
303
|
}
|
|
276
304
|
|
|
277
305
|
/** The hip's pivot against the thigh roots, at setup (rig-c/pose#47). */
|
|
@@ -368,6 +396,11 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
368
396
|
const endOf = (i: number): number => (i + 1 < n ? keyTimes[i + 1]! : n > 1 ? 2 * keyTimes[i]! - keyTimes[i - 1]! : keyTimes[i]!);
|
|
369
397
|
const secondsOf = (run: [number, number]): number => Math.round((endOf(run[1]) - keyTimes[run[0]]!) * 1e6) / 1e6;
|
|
370
398
|
|
|
399
|
+
// --raised-by-head (#62) measures against the head in eye distances: the map's head chain must carry the
|
|
400
|
+
// eye line with an "a>b" aim, whose two ends are the rig's eyes.
|
|
401
|
+
const head = opts.raisedByHead === true ? headOf(map) : null;
|
|
402
|
+
if (head !== null && 'problem' in head) p.add('RAISED_HEAD', head.problem);
|
|
403
|
+
|
|
371
404
|
// Every point the map reads, held where it is not good.
|
|
372
405
|
const needed = new Set<string>();
|
|
373
406
|
for (const c of map.chains) c.joints.forEach((j) => needed.add(j));
|
|
@@ -582,6 +615,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
582
615
|
const legScale: LegScale[] = [];
|
|
583
616
|
const scales = new Map<string, number[]>();
|
|
584
617
|
let stance: Stance | null = null;
|
|
618
|
+
let floor: Floor | null = null;
|
|
619
|
+
let blendHold: BlendHold | null = null;
|
|
585
620
|
let feet: Feet | null = null;
|
|
586
621
|
let legs: ReturnType<typeof legsOf> | null = null;
|
|
587
622
|
let legPath: string[] | null = null;
|
|
@@ -637,6 +672,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
637
672
|
if (legScale.length === 0 && !p.any) p.add('LEGS_MAP', '--leg-scale scales the leg segments (two of one side\'s hip, knee and ankle), and the map aims none');
|
|
638
673
|
}
|
|
639
674
|
if (opts.stance !== undefined && opts.stance !== 'off' && !plantOn) p.add('STANCE', `--stance ${opts.stance} plants the free foot beside the supporting one that --plant-feet holds, and --plant-feet is not given`);
|
|
675
|
+
if (opts.floor !== undefined && !plantOn) p.add('FLOOR', `--floor ${opts.floor} is the floor --plant-feet plants every support span on, and --plant-feet is not given`);
|
|
676
|
+
if (opts.floor !== undefined && !Number.isFinite(opts.floor)) p.add('FLOOR', `--floor is ${opts.floor}; it must be a finite world y`);
|
|
640
677
|
p.throwIfAny();
|
|
641
678
|
/** Every bone on the legs' path posed on frame i, by the targets as they stand. */
|
|
642
679
|
const poseLegs = (i: number): PathPose => {
|
|
@@ -657,32 +694,52 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
657
694
|
return { r: tipOf(rest, lp, legs !== null && 'legs' in legs ? legs.legs.r.ankleBone : ''), l: tipOf(rest, lp, legs !== null && 'legs' in legs ? legs.legs.l.ankleBone : '') };
|
|
658
695
|
});
|
|
659
696
|
const tb = rest.get(t!.bone)!;
|
|
660
|
-
|
|
697
|
+
// #85: every support span on one floor — the median of their planted heights, or --floor.
|
|
698
|
+
const planted = plantHip(feet!, (i, s) => offsets[i]![s], (i) => poseTravel[i]!, [tb.worldX, tb.worldY], opts.floor);
|
|
699
|
+
const hip = planted.hip;
|
|
700
|
+
floor = planted.floor;
|
|
661
701
|
for (let i = 0; i < n; i++) travel[i] = [hip[i]![0] - tb.worldX, hip[i]![1] - tb.worldY];
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
});
|
|
702
|
+
const L = (legs as { legs: Record<Side, { chain: Chain; ankleBone: string }> }).legs;
|
|
703
|
+
const world = (i: number, v: Pt): Pt => [hip[i]![0] + v[0], hip[i]![1] + v[1]];
|
|
704
|
+
/** Both legs posed on frame i by the scales as they stand, world (y up): each chain's root and ankle. */
|
|
705
|
+
const posedFeet = (i: number) => {
|
|
706
|
+
const lp = poseLegs(i);
|
|
707
|
+
return {
|
|
708
|
+
root: { r: world(i, lp.origin.get(L.r.chain.bones[0]!)!), l: world(i, lp.origin.get(L.l.chain.bones[0]!)!) },
|
|
709
|
+
ankle: { r: world(i, tipOf(rest, lp, L.r.ankleBone)), l: world(i, tipOf(rest, lp, L.l.ankleBone)) },
|
|
710
|
+
};
|
|
711
|
+
};
|
|
712
|
+
/** Every bone of a side's chain has its scalex multiplied by the stretch keyed on that side. */
|
|
713
|
+
const stretchChain = (stretch: ReadonlyArray<{ side: Side; k: number } | null>) => {
|
|
675
714
|
for (const side of SIDES) {
|
|
676
|
-
if (!
|
|
715
|
+
if (!stretch.some((x) => x?.side === side)) continue;
|
|
677
716
|
for (const b of L[side].chain.bones) {
|
|
678
717
|
const own = scales.get(b);
|
|
679
718
|
const s = own === undefined ? new Array<number>(n).fill(1) : [...own];
|
|
680
|
-
|
|
719
|
+
stretch.forEach((x, i) => {
|
|
681
720
|
if (x?.side === side) s[i] = s[i]! * x.k;
|
|
682
721
|
});
|
|
683
722
|
scales.set(b, s);
|
|
684
723
|
}
|
|
685
724
|
}
|
|
725
|
+
};
|
|
726
|
+
// #85: on a blend frame the hip is between two derivations; the supporting leg is stretched
|
|
727
|
+
// (stanceOf's mechanism, its bounds) so its ankle stays on the floor. Before --stance, whose
|
|
728
|
+
// floor is that supporting ankle.
|
|
729
|
+
blendHold = holdBlend(planted.blends, n, posedFeet);
|
|
730
|
+
stretchChain(blendHold.stretch);
|
|
731
|
+
// --stance (#60): with the hip placed, a free ankle within the tolerance of the floor has its leg
|
|
732
|
+
// stretched (every bone of its chain's scalex, about the chain's root) until it reaches it.
|
|
733
|
+
if (opts.stance !== undefined && opts.stance !== 'off') {
|
|
734
|
+
const ankleSeries = { r: series.get('r_ankle')!, l: series.get('l_ankle')! };
|
|
735
|
+
stance = stanceOf(feet!, ankleSeries, opts.stance, posedFeet, secondsOf);
|
|
736
|
+
// The supporting leg (the blend's) and the free leg (the stance's) are different chains on a frame.
|
|
737
|
+
for (let i = 0; i < n; i++) {
|
|
738
|
+
const b = blendHold.stretch[i];
|
|
739
|
+
if (b !== null && b !== undefined && stance.stretch[i]?.side === b.side) p.add('LEGS_STRETCH', `frame ${i}: chain "${L[b.side].chain.name}" is stretched both as the supporting leg of a blend (k ${b.k}) and as the free leg of --stance (k ${stance.stretch[i]!.k}); one chain takes one stretch`);
|
|
740
|
+
}
|
|
741
|
+
p.throwIfAny();
|
|
742
|
+
stretchChain(stance.stretch);
|
|
686
743
|
}
|
|
687
744
|
}
|
|
688
745
|
if (t !== null && scale !== null) {
|
|
@@ -713,29 +770,52 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
713
770
|
for (let a = rest.get(b)!.parent; a !== null; a = rest.get(a)!.parent) d++;
|
|
714
771
|
return d;
|
|
715
772
|
};
|
|
773
|
+
/** The world turn of `b`'s nearest mapped bone-or-ancestor, degrees, on frame i, by the targets as they stand. */
|
|
774
|
+
const turnAbove = (b: string, i: number): number => {
|
|
775
|
+
for (let a: string | null = b; a !== null; a = rest.get(a)!.parent) {
|
|
776
|
+
const tg = targets.get(a);
|
|
777
|
+
if (tg !== undefined) return tg[i]! - restOf.get(a)!;
|
|
778
|
+
}
|
|
779
|
+
return 0;
|
|
780
|
+
};
|
|
781
|
+
/**
|
|
782
|
+
* Where the poser will put `b`'s origin on frame i: forward from the
|
|
783
|
+
* translated bone (moved by its travel) or the root (at setup), each rest
|
|
784
|
+
* offset turned by the nearest mapped bone above — the key formula's own
|
|
785
|
+
* assumption (unit scale, no shear), which the landing check holds to account.
|
|
786
|
+
*/
|
|
787
|
+
const posedOrigin = (b: string, i: number): Pt => {
|
|
788
|
+
const own = rest.get(b)!;
|
|
789
|
+
if (t !== null && b === t.bone) return [own.worldX + (travel[i]?.[0] ?? 0), own.worldY + (travel[i]?.[1] ?? 0)];
|
|
790
|
+
if (own.parent === null) return [own.worldX, own.worldY];
|
|
791
|
+
const parent = own.parent;
|
|
792
|
+
const po = posedOrigin(parent, i);
|
|
793
|
+
const r = turnAbove(parent, i) / DEG;
|
|
794
|
+
const ox = own.worldX - rest.get(parent)!.worldX;
|
|
795
|
+
const oy = own.worldY - rest.get(parent)!.worldY;
|
|
796
|
+
return [po[0] + Math.cos(r) * ox - Math.sin(r) * oy, po[1] + Math.sin(r) * ox + Math.cos(r) * oy];
|
|
797
|
+
};
|
|
798
|
+
/** A rig point (a bone, `name:tip`, `a+b`) posed on frame i the same way. */
|
|
799
|
+
const posedPoint = (spec: string, i: number): Pt => {
|
|
800
|
+
let x = 0;
|
|
801
|
+
let y = 0;
|
|
802
|
+
const parts = pointParts(spec);
|
|
803
|
+
for (const part of parts) {
|
|
804
|
+
const name = partBone(part);
|
|
805
|
+
const o = posedOrigin(name, i);
|
|
806
|
+
const bone = rest.get(name)!;
|
|
807
|
+
const l = part.endsWith(':tip') ? bone.length : 0;
|
|
808
|
+
const r = turnAbove(name, i) / DEG;
|
|
809
|
+
x += o[0] + l * (Math.cos(r) * bone.a - Math.sin(r) * bone.c);
|
|
810
|
+
y += o[1] + l * (Math.sin(r) * bone.a + Math.cos(r) * bone.c);
|
|
811
|
+
}
|
|
812
|
+
return [x / parts.length, y / parts.length];
|
|
813
|
+
};
|
|
716
814
|
const reaches = map.chains.filter((c) => c.reach).sort((x, y) => depth(x.bones[0]!) - depth(y.bones[0]!));
|
|
717
815
|
let aimReach: ((c: Chain) => void) | null = null;
|
|
718
816
|
if (reaches.length > 0 && t !== null && scale !== null) {
|
|
719
817
|
const s = scale.factor;
|
|
720
818
|
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
819
|
aimReach = (c: Chain): void => {
|
|
740
820
|
const b = c.bones[0]!;
|
|
741
821
|
const anchor = series.get(c.joints[0]!)!;
|
|
@@ -793,8 +873,214 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
793
873
|
}
|
|
794
874
|
}
|
|
795
875
|
p.throwIfAny();
|
|
796
|
-
|
|
876
|
+
|
|
877
|
+
// --raised-by-head (#62, HQ's ruling): while a set whose arm is drawn raised shows, its wrist goes where
|
|
878
|
+
// the reference's is against the head, in eye distances (each side's median over the span). Solved last
|
|
879
|
+
// of the bones: every bone above the arm, the head and the trunk's reach included, has its final target
|
|
880
|
+
// (range clamp done), so the head and the shoulder are posed from them frame by frame; the arm moves
|
|
881
|
+
// neither. A target out of reach is placed at the reach limit along the head-relative direction
|
|
882
|
+
// (`reachLimit`, a REACH line). The set's `turn` is part of the solve: a held upper arm has its forearm
|
|
883
|
+
// re-aimed at the placed wrist, and only a forearm the turn still holds is ART-RANGE.
|
|
884
|
+
const raised: Raised[] = [];
|
|
885
|
+
const raisedArt: Array<{ sw: Switch; entries: ArtRange[] }> = [];
|
|
886
|
+
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] }> = [];
|
|
887
|
+
if (head !== null && !('problem' in head)) {
|
|
888
|
+
const eyesOn = (i: number): { centre: Pt; dist: number } => {
|
|
889
|
+
const a = posedPoint(head.points[0], i);
|
|
890
|
+
const b = posedPoint(head.points[1], i);
|
|
891
|
+
return { centre: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2], dist: Math.hypot(b[0] - a[0], b[1] - a[1]) };
|
|
892
|
+
};
|
|
893
|
+
const headBones = new Set([...head.points.flatMap((x) => pointParts(x).map(partBone))]);
|
|
894
|
+
const ra = series.get(head.joints[0])!;
|
|
895
|
+
const rb = series.get(head.joints[1])!;
|
|
896
|
+
const refCentre = (i: number): Pt => [(ra[i]![0] + rb[i]![0]) / 2, (ra[i]![1] + rb[i]![1]) / 2];
|
|
897
|
+
for (const s of sw.switches) {
|
|
898
|
+
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
899
|
+
const art: ArtRange[] = [];
|
|
900
|
+
for (const cn of s.chains) {
|
|
901
|
+
const c = map.chains.find((x) => x.name === cn)!;
|
|
902
|
+
const drawn = set.drawn[c.bones[0]!];
|
|
903
|
+
if (drawn === undefined || !isRaised(drawn)) continue;
|
|
904
|
+
const label = `--raised-by-head: set "${set.name}" draws chain "${c.name}" raised (bone "${c.bones[0]}" at ${drawn}°)`;
|
|
905
|
+
if (c.bones.length !== 2) {
|
|
906
|
+
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)`);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (c.reach || c.aim.some((x) => x.includes('>'))) {
|
|
910
|
+
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`);
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const [b0, b1] = c.bones as [string, string];
|
|
914
|
+
const moved = [...headBones].filter((h) => h === b0 || h === b1 || descends(rest, h, b0));
|
|
915
|
+
if (moved.length > 0) {
|
|
916
|
+
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`);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
const wristSpec = c.aim[1] === 'x' ? `${b1}:tip` : c.aim[1]!;
|
|
920
|
+
if (c.aim[1] === 'x' && !(rest.get(b1)!.length > 1e-9)) {
|
|
921
|
+
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`);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const wristOf = (i: number): Pt => posedPoint(wristSpec, i);
|
|
925
|
+
const refWrist = series.get(c.joints[2]!)!;
|
|
926
|
+
// The unit, each side's median over the span (HQ's ruling).
|
|
927
|
+
const span: number[] = [];
|
|
928
|
+
for (let i = s.frames[0]; i <= s.frames[1]; i++) span.push(i);
|
|
929
|
+
const refEye = median(span.map((i) => Math.hypot(rb[i]![0] - ra[i]![0], rb[i]![1] - ra[i]![1])));
|
|
930
|
+
const rigEye = median(span.map((i) => eyesOn(i).dist));
|
|
931
|
+
if (!(refEye > 1e-9) || !(rigEye > 1e-9)) {
|
|
932
|
+
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`);
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
// The rig's own lengths, and each bone's aim against the vector it carries, at setup.
|
|
936
|
+
const S0 = rigPoint(rest, b0)!;
|
|
937
|
+
const E0 = rigPoint(rest, b1)!;
|
|
938
|
+
const W0 = rigPoint(rest, wristSpec)!;
|
|
939
|
+
const l0 = Math.hypot(E0[0] - S0[0], E0[1] - S0[1]);
|
|
940
|
+
const l1 = Math.hypot(W0[0] - E0[0], W0[1] - E0[1]);
|
|
941
|
+
const d0 = wrap(Math.atan2(E0[1] - S0[1], E0[0] - S0[0]) * DEG - restOf.get(b0)!);
|
|
942
|
+
const d1 = wrap(Math.atan2(W0[1] - E0[1], W0[0] - E0[0]) * DEG - restOf.get(b1)!);
|
|
943
|
+
const t0 = targets.get(b0)!;
|
|
944
|
+
const t1 = targets.get(b1)!;
|
|
945
|
+
const turn0 = set.turn[b0];
|
|
946
|
+
const turn1 = set.turn[b1];
|
|
947
|
+
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 };
|
|
948
|
+
/** Per frame of the span: the forearm's art turn the re-aim asked, and how far past its turn. */
|
|
949
|
+
const foreAsked: number[] = [];
|
|
950
|
+
const foreOver: number[] = [];
|
|
951
|
+
/** 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). */
|
|
952
|
+
const upperAsked: number[] = [];
|
|
953
|
+
const upperOver: number[] = [];
|
|
954
|
+
const heldMiss: number[] = [];
|
|
955
|
+
for (const i of span) {
|
|
956
|
+
const h = eyesOn(i);
|
|
957
|
+
const rc = refCentre(i);
|
|
958
|
+
// The reference's wrist from its head centre, in its eye distances, y turned up.
|
|
959
|
+
const ref: Pt = [(refWrist[i]![0] - rc[0]) / refEye, -(refWrist[i]![1] - rc[1]) / refEye];
|
|
960
|
+
const w = wristOf(i);
|
|
961
|
+
job.reference.push(ref);
|
|
962
|
+
job.before.push([(w[0] - h.centre[0]) / rigEye, (w[1] - h.centre[1]) / rigEye]);
|
|
963
|
+
const target: Pt = [h.centre[0] + ref[0] * rigEye, h.centre[1] + ref[1] * rigEye];
|
|
964
|
+
const shoulder = posedOrigin(b0, i);
|
|
965
|
+
job.farthest = Math.max(job.farthest, Math.hypot(target[0] - shoulder[0], target[1] - shoulder[1]));
|
|
966
|
+
const place = reachLimit(h.centre, target, shoulder, l0, l1);
|
|
967
|
+
const P = place.point;
|
|
968
|
+
job.placed.push([(P[0] - h.centre[0]) / rigEye, (P[1] - h.centre[1]) / rigEye]);
|
|
969
|
+
if (place.how !== 'in') job.short.push({ i, how: place.how, shortfall: Math.hypot(target[0] - P[0], target[1] - P[1]) / rigEye });
|
|
970
|
+
// The pose's bend side, read on the vectors the two bones carry.
|
|
971
|
+
const side: 1 | -1 = wrap(t1[i]! + d1 - (t0[i]! + d0)) >= 0 ? 1 : -1;
|
|
972
|
+
const sol = twoBone(shoulder, P, l0, l1, side);
|
|
973
|
+
if (sol.upper === null) {
|
|
974
|
+
foreAsked.push(wrap(t1[i]! - (set.drawn[b1] ?? 0)));
|
|
975
|
+
foreOver.push(0);
|
|
976
|
+
upperAsked.push(0);
|
|
977
|
+
upperOver.push(0);
|
|
978
|
+
heldMiss.push(0);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
let a0 = wrap(sol.upper - d0);
|
|
982
|
+
let a1 = wrap(sol.fore - d1);
|
|
983
|
+
// 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.
|
|
984
|
+
let held = false;
|
|
985
|
+
const tt = turn0 === undefined ? 0 : wrap(a0 - set.drawn[b0]!);
|
|
986
|
+
upperAsked.push(tt);
|
|
987
|
+
upperOver.push(turn0 === undefined ? 0 : overflow(tt, turn0));
|
|
988
|
+
if (turn0 !== undefined) {
|
|
989
|
+
const cl = Math.min(Math.max(tt, turn0[0]), turn0[1]);
|
|
990
|
+
if (Math.abs(cl - tt) > RANGE_SLACK) {
|
|
991
|
+
held = true;
|
|
992
|
+
a0 = wrap(set.drawn[b0]! + cl);
|
|
993
|
+
job.upperHeld.push(i);
|
|
994
|
+
const r = (a0 + d0) / DEG;
|
|
995
|
+
const E: Pt = [shoulder[0] + l0 * Math.cos(r), shoulder[1] + l0 * Math.sin(r)];
|
|
996
|
+
a1 = wrap(Math.atan2(P[1] - E[1], P[0] - E[0]) * DEG - d1);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// A forearm the turn still holds: the re-aim failed within the art's range — ART-RANGE.
|
|
1000
|
+
const asked = turn1 === undefined ? 0 : wrap(a1 - set.drawn[b1]!);
|
|
1001
|
+
const over = turn1 === undefined ? 0 : overflow(asked, turn1);
|
|
1002
|
+
foreAsked.push(asked);
|
|
1003
|
+
foreOver.push(over);
|
|
1004
|
+
if (turn1 !== undefined && over > RANGE_SLACK) a1 = wrap(set.drawn[b1]! + Math.min(Math.max(asked, turn1[0]), turn1[1]));
|
|
1005
|
+
// Where the held arm leaves the wrist: the re-aimed forearm points at the placed point, but from a held
|
|
1006
|
+
// elbow its length need not reach it (rule 3, "cannot reach within turn").
|
|
1007
|
+
const r0 = (a0 + d0) / DEG;
|
|
1008
|
+
const r1 = (a1 + d1) / DEG;
|
|
1009
|
+
const W: Pt = [shoulder[0] + l0 * Math.cos(r0) + l1 * Math.cos(r1), shoulder[1] + l0 * Math.sin(r0) + l1 * Math.sin(r1)];
|
|
1010
|
+
heldMiss.push(held ? Math.hypot(W[0] - P[0], W[1] - P[1]) / rigEye : 0);
|
|
1011
|
+
t0[i] = a0;
|
|
1012
|
+
t1[i] = a1;
|
|
1013
|
+
}
|
|
1014
|
+
// 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.
|
|
1015
|
+
if (turn0 !== undefined) {
|
|
1016
|
+
for (const [a, b] of runsOf(heldMiss.map((m) => m > HELD_MISS), RANGE_GAP)) {
|
|
1017
|
+
let worst = a;
|
|
1018
|
+
for (let k = a; k <= b; k++) if (heldMiss[k]! > heldMiss[worst]!) worst = k;
|
|
1019
|
+
const first = s.frames[0] + a;
|
|
1020
|
+
const last = s.frames[0] + b;
|
|
1021
|
+
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]!) });
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (turn1 !== undefined) {
|
|
1025
|
+
for (const [a, b] of runsOf(foreOver.map((o) => o > RANGE_SLACK), RANGE_GAP)) {
|
|
1026
|
+
let worst = a;
|
|
1027
|
+
for (let k = a; k <= b; k++) if (foreOver[k]! > foreOver[worst]!) worst = k;
|
|
1028
|
+
const first = s.frames[0] + a;
|
|
1029
|
+
const last = s.frames[0] + b;
|
|
1030
|
+
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 });
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
raisedJobs.push(job);
|
|
1034
|
+
}
|
|
1035
|
+
art.sort((x, y) => x.frames[0] - y.frames[0]);
|
|
1036
|
+
if (art.length > 0) raisedArt.push({ sw: s, entries: art });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
p.throwIfAny();
|
|
1040
|
+
// 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).
|
|
1041
|
+
const measured = measureArt(sw.switches, map, targets, endOf, keyTimes, RANGE_GAP);
|
|
1042
|
+
const solvedBy = (e: ArtRange, s: Switch) => raisedJobs.some((j) => j.sw === s && j.chain.name === e.chain);
|
|
1043
|
+
const artRange: ArtRange[] = [];
|
|
1044
|
+
for (const s of sw.switches) {
|
|
1045
|
+
const own = measured.filter((e) => e.set === s.set && e.frames[0] >= s.frames[0] && e.frames[1] <= s.frames[1]);
|
|
1046
|
+
artRange.push(...own.filter((e) => !solvedBy(e, s)), ...(raisedArt.find((x) => x.sw === s)?.entries ?? []));
|
|
1047
|
+
}
|
|
797
1048
|
clampArt(sw.switches, map, targets);
|
|
1049
|
+
// Where each raised wrist lands as keyed: after the solve and the set's `turn`.
|
|
1050
|
+
for (const job of raisedJobs) {
|
|
1051
|
+
const after: Pt[] = [];
|
|
1052
|
+
for (let i = job.sw.frames[0]; i <= job.sw.frames[1]; i++) {
|
|
1053
|
+
const a = posedPoint(job.eyePoints[0], i);
|
|
1054
|
+
const b = posedPoint(job.eyePoints[1], i);
|
|
1055
|
+
const w = job.wrist(i);
|
|
1056
|
+
after.push([(w[0] - (a[0] + b[0]) / 2) / job.rigEye, (w[1] - (a[1] + b[1]) / 2) / job.rigEye]);
|
|
1057
|
+
}
|
|
1058
|
+
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;
|
|
1059
|
+
const round = (vs: readonly Pt[]): Pt[] => vs.map((v) => [r6(v[0]), r6(v[1])]);
|
|
1060
|
+
const short: ReachShort[] = [];
|
|
1061
|
+
for (let k = 0; k < job.short.length;) {
|
|
1062
|
+
let m = k;
|
|
1063
|
+
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++;
|
|
1064
|
+
const fs = job.short.slice(k, m + 1).map((x) => x.shortfall);
|
|
1065
|
+
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)) } });
|
|
1066
|
+
k = m + 1;
|
|
1067
|
+
}
|
|
1068
|
+
raised.push({
|
|
1069
|
+
set: job.sw.set,
|
|
1070
|
+
chain: job.chain.name,
|
|
1071
|
+
frames: job.sw.frames,
|
|
1072
|
+
seconds: secondsOf(job.sw.frames),
|
|
1073
|
+
eyes: { rig: r6(job.rigEye), reference: r6(job.refEye) },
|
|
1074
|
+
reference: round(job.reference),
|
|
1075
|
+
placed: round(job.placed),
|
|
1076
|
+
before: round(job.before),
|
|
1077
|
+
after: round(after),
|
|
1078
|
+
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])))) },
|
|
1079
|
+
short,
|
|
1080
|
+
upperHeld: job.upperHeld,
|
|
1081
|
+
reach: { min: r6(Math.abs(job.l0 - job.l1)), max: r6(job.l0 + job.l1), farthest: r6(job.farthest) },
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
798
1084
|
|
|
799
1085
|
// The legs on their final targets: a bone under a scaled bone is keyed through
|
|
800
1086
|
// its parent's posed matrix (legs.ts), and each ankle's world position is read.
|
|
@@ -854,10 +1140,34 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
854
1140
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
855
1141
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale,
|
|
856
1142
|
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),
|
|
1143
|
+
legScale, scales, feet, planted: plantOn, stance, floor, blendHold, ankles, pivot: pivotOf(rest, t?.bone ?? null, legs), raised,
|
|
858
1144
|
};
|
|
859
1145
|
}
|
|
860
1146
|
|
|
1147
|
+
/** 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. */
|
|
1148
|
+
interface Head {
|
|
1149
|
+
chain: string;
|
|
1150
|
+
joints: [string, string];
|
|
1151
|
+
points: [string, string];
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** 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. */
|
|
1155
|
+
function headOf(map: JointMap): Head | { problem: string } {
|
|
1156
|
+
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';
|
|
1157
|
+
for (const c of map.chains) {
|
|
1158
|
+
for (let k = 0; k + 1 < c.joints.length; k++) {
|
|
1159
|
+
const a = c.joints[k]!;
|
|
1160
|
+
const b = c.joints[k + 1]!;
|
|
1161
|
+
if (!((a === 'r_eye' && b === 'l_eye') || (a === 'l_eye' && b === 'r_eye'))) continue;
|
|
1162
|
+
const aim = c.aim[k]!;
|
|
1163
|
+
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` };
|
|
1164
|
+
const [pa, pb] = aimEnds(c.bones[k]!, aim);
|
|
1165
|
+
return { chain: c.name, joints: [a, b], points: [pa, pb] };
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
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` };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
861
1171
|
type ShortSpan = Omit<Short, 'chain' | 'bone' | 'joints'>;
|
|
862
1172
|
|
|
863
1173
|
/**
|