@rig-c/pose 0.24.0 → 0.25.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 +2 -2
- package/package.json +1 -1
- package/src/retarget.ts +158 -23
- package/src/run.ts +1 -1
- package/src/switch.ts +234 -20
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ The threshold was fixed by measuring first ([#15](https://github.com/rig-c/pose/
|
|
|
182
182
|
|
|
183
183
|
Re-measured with the guard on, against the guarded unsmoothed run, on #11's criteria (power above 6 Hz down ≥ 2× on every point, timing lag 0, p95 speed ≥ 0.85×): no window passes. `--smooth 7` comes closest — the left wrist's power drops 1.99× and the hip's p95 speed is 0.847× — so `--smooth` stays off by default, and so does the guard: the rule posted before the run made a default for either one depend on a window passing. **Use `--flip 90`**: with it, no bone of the gated build turns more than 90° in one frame (without it, 148.6°), and every bone still lands.
|
|
184
184
|
|
|
185
|
-
**The short hold.** A segment pointing at the camera is short in the picture, and its picture direction is then detector noise: the rig turns the bone to follow it. So after holds, and before the flip guard, every
|
|
185
|
+
**The short hold.** A segment pointing at the camera is short in the picture, and its picture direction is then detector noise: the rig turns the bone to follow it. So after holds, and before the flip guard, every mapped segment is measured on every frame (not a `reach`) — its picture length, between the same two points its angle is read from, over its own median length across the pose. On a frame under `--short <fraction>` its angle is not read from the pose: it is interpolated, the short way round, from the last frame at or above the cutoff to the next one, and held flat at the first or last frame of the pose. A horizontal line across the body from a left joint to its right twin (`a>b`, such as the hip line or the eye line) also holds its last confident angle and interpolates across the span whenever its picture length falls under `--short` or its 2D reading reverses sign within one frame past `--flip` (when the left and right joints cross in 2D during a full side turn, [#184](https://github.com/rig-c/pose/issues/184)); its hold is printed as `HOLD chain "..."` and is not counted toward limb `END-ON` runs. The flip guard, the hinge check and the keys all see the held angle. `--short off` (the default) reads every frame unless an across-body line reverses past `--flip`. The report lists each span under `short` (chain, bone, segment, first and last frame, the lowest fraction, and `across` / `reversed` when across-body).
|
|
186
186
|
|
|
187
187
|
The cutoff was fixed by measuring first ([#22](https://github.com/rig-c/pose/issues/22)), over both upper arms, forearms, thighs and shins. On the 13 s dance (24 fps, 2,504 segment-frames) the fraction's 0.5th percentile is 0.51, its 5th 0.82, and 7 segment-frames (0.28%) are under 0.35; on a 16 s second sequence (30 fps, 3,936) they are 0.27 and 0.75, and 34 (0.86%). Every frame under 0.35 was looked at, and was one of two kinds:
|
|
188
188
|
|
|
@@ -191,7 +191,7 @@ The cutoff was fixed by measuring first ([#22](https://github.com/rig-c/pose/iss
|
|
|
191
191
|
|
|
192
192
|
Both have a meaningless direction; only the first is a pose the rig cannot draw. **0.35** is the smallest round value that takes in every end-on frame (the highest is 0.35, on the dance) without reaching normal motion (the 5th percentiles are 0.75 and 0.82). A run under the cutoff — spans merged across a gap of one frame — lasting **≥ 0.15 s** (3.6 frames at 24 fps, 4.5 at 30) is the end-on kind: it excludes every blur collapse above (at most 3 frames, 0.10 s at 30 fps) and keeps every end-on span (6–8 frames). The run prints it as `END-ON` and the report lists it under `endOn` (chain, bone, segment, frames, seconds, the lowest fraction): it is the list of moments that need an alternate drawing ([#6](https://github.com/rig-c/pose/issues/6)).
|
|
193
193
|
|
|
194
|
-
Re-checked on the exam01 test rig with `--short 0.35 --flip 90 --bends clamp`. On the second sequence the three spreads the flip guard made out of end-on segments are SHORT holds now, not FLIPs: right upper arm 399–400, right thigh 368–369, right shin 368–371. Of the flip guard's other spreads only one is left, the left forearm at 322–323; every other one fell on frames under the cutoff and is a SHORT hold instead. No guarded bone of the gated build turns more than 81.4° in one frame (85.1° with `--flip 90` alone), and every bone lands. `endOn` there lists the right upper arm (395–401) and the right shin (365–370). It does not list the right thigh, which is under 0.35 on one frame only (369, 0.16). The plan before the run expected the thigh too, and that expectation was wrong. On the dance `endOn` lists exactly S2's left upper arm (30–37); the three FLIPs are #15's, the largest one-frame turn is 78.2°, and every bone lands.
|
|
194
|
+
Re-checked on the exam01 test rig with `--short 0.35 --flip 90 --bends clamp`. On the second sequence the three spreads the flip guard made out of end-on segments are SHORT holds now, not FLIPs: right upper arm 399–400, right thigh 368–369, right shin 368–371. Of the flip guard's other spreads only one is left, the left forearm at 322–323; every other one fell on frames under the cutoff and is a SHORT hold instead. No guarded bone of the gated build turns more than 81.4° in one frame (85.1° with `--flip 90` alone), and every bone lands. `endOn` there lists the right upper arm (395–401) and the right shin (365–370). It does not list the right thigh, which is under 0.35 on one frame only (369, 0.16). The plan before the run expected the thigh too, and that expectation was wrong. On the dance `endOn` lists exactly S2's left upper arm (30–37); the three FLIPs are #15's, the largest one-frame turn is 78.2°, and every bone lands. When the second sequence's pelvis is mapped (`r_hip → l_hip`), its side-on turn (frames 463–491, body yaw +79.5°) foreshortens the hip line to 0.17 of standing and crosses `r_hip.x` past `l_hip.x` on frames 482–484 (+177.4° / −179.4°); `holdAcross` ([#184](https://github.com/rig-c/pose/issues/184)) holds `hip.rotate` across the unconfident span (`[−2.2°, +2.6°]` over 463–491, `0` `FLIP`s) without listing horizontal lines as limb `END-ON`. The default stays off: a default needs its own criterion and a third sequence.
|
|
195
195
|
|
|
196
196
|
### The legs: foreshortening and planted feet
|
|
197
197
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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
|
@@ -182,6 +182,10 @@ export interface Short {
|
|
|
182
182
|
/** Present when a foreshortened segment (< `SHORT_STEP_FRACTION`) had its per-frame step capped (#110): the one-frame step after capping and the largest step asked, degrees, signed. */
|
|
183
183
|
step?: number;
|
|
184
184
|
asked?: number;
|
|
185
|
+
/** Present when a horizontal chain across the body (`isAcross`) held its last confident angle (#184). */
|
|
186
|
+
across?: boolean;
|
|
187
|
+
/** Largest one-frame turn (degrees, signed) inside a held horizontal-chain span when it reversed beyond `--flip` (#184). */
|
|
188
|
+
reversed?: number;
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
/** A limb segment shorter than this fraction of its median picture length has its per-frame angle step capped at `SHORT_STEP_CAP` degrees (rig-c/pose#110). */
|
|
@@ -610,30 +614,40 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
610
614
|
}
|
|
611
615
|
p.throwIfAny();
|
|
612
616
|
|
|
613
|
-
// The short hold, on every
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
617
|
+
// The short hold, on every segment the pose aims directly (not a reach): its
|
|
618
|
+
// picture length against its own median, on the points the angle was read from.
|
|
619
|
+
// A horizontal line across the body (`isAcross`: pelvis, shoulders, head line)
|
|
620
|
+
// holds and interpolates across spans under `--short` or reversed within a frame
|
|
621
|
+
// beyond `--flip` (#184), without listing the run as `endOn` (`TURN` reads turned
|
|
622
|
+
// body/head lines).
|
|
618
623
|
const short: Short[] = [];
|
|
619
624
|
const endOn: EndOn[] = [];
|
|
620
625
|
const fractions = new Map<string, { chain: string; joints: [string, string]; fraction: number[] }>();
|
|
621
|
-
if (opts.short !== 'off') {
|
|
622
|
-
const cutoff = opts.short;
|
|
626
|
+
if (opts.short !== 'off' || opts.flip !== 'off') {
|
|
623
627
|
for (const c of map.chains) {
|
|
624
628
|
if (c.reach) continue;
|
|
625
629
|
c.bones.forEach((b, k) => {
|
|
626
630
|
const joints: [string, string] = [c.joints[k]!, c.joints[k + 1]!];
|
|
627
|
-
|
|
631
|
+
const across = isAcross(joints[0], joints[1]);
|
|
632
|
+
if (!across && opts.short === 'off') return;
|
|
628
633
|
const from = series.get(joints[0])!;
|
|
629
634
|
const to = series.get(joints[1])!;
|
|
630
635
|
const lengths = from.map((f, i) => Math.hypot(to[i]![0] - f[0], to[i]![1] - f[1]));
|
|
631
636
|
const med = median(lengths);
|
|
632
637
|
if (med < 1e-6) {
|
|
633
|
-
|
|
638
|
+
if (opts.short !== 'off') {
|
|
639
|
+
p.add('POSE_SHORT_MEDIAN', `chain "${c.name}" — bone "${b}"'s segment ${joints[0]} → ${joints[1]} has median picture length ${med}px over the ${n} frames; --short measures a frame's length against that median, and there is none to measure against`);
|
|
640
|
+
}
|
|
634
641
|
return;
|
|
635
642
|
}
|
|
636
643
|
const fraction = lengths.map((l) => l / med);
|
|
644
|
+
if (across) {
|
|
645
|
+
const restDir = joints[0].startsWith('r_') ? 0 : 180;
|
|
646
|
+
const spans = holdAcross(targets.get(b)!, fraction, opts.short, opts.flip, restDir);
|
|
647
|
+
for (const s of spans) short.push({ chain: c.name, bone: b, joints, ...s });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const cutoff = opts.short as number;
|
|
637
651
|
fractions.set(b, { chain: c.name, joints, fraction });
|
|
638
652
|
const spans = holdShort(targets.get(b)!, fraction, cutoff);
|
|
639
653
|
for (const s of spans) short.push({ chain: c.name, bone: b, joints, ...s });
|
|
@@ -1366,6 +1380,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1366
1380
|
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
1367
1381
|
const art: ArtRange[] = [];
|
|
1368
1382
|
for (const cn of s.chains) {
|
|
1383
|
+
if (sw.chainHolds?.some((h) => h.chain === cn && h.set !== s.set && h.frames[0] <= s.frames[0] && h.frames[1] >= s.frames[1]) === true) continue;
|
|
1369
1384
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
1370
1385
|
const drawn = set.drawn[c.bones[0]!];
|
|
1371
1386
|
const up = isChainHeadPlaced(set, c, drawn);
|
|
@@ -1619,6 +1634,45 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1619
1634
|
if (job.anchor.kind === 'head') raised.push({ set, chain, frames, seconds, eyes: unit, ...body });
|
|
1620
1635
|
else byHip.push({ set, chain, frames, seconds, hips: unit, ...body });
|
|
1621
1636
|
}
|
|
1637
|
+
if (opts.short !== 'off') {
|
|
1638
|
+
for (const s of short) {
|
|
1639
|
+
if (s.step === undefined || s.across === true) continue;
|
|
1640
|
+
const cov = covered.get(s.chain);
|
|
1641
|
+
const [lo, hi] = s.frames;
|
|
1642
|
+
if (lo <= 0 || hi + 1 >= n) continue;
|
|
1643
|
+
if (cov?.[lo - 1] !== true && cov?.[hi + 1] !== true) continue;
|
|
1644
|
+
const tg = targets.get(s.bone)!;
|
|
1645
|
+
const startA = tg[lo - 1]!;
|
|
1646
|
+
const total = wrap(tg[hi + 1]! - startA);
|
|
1647
|
+
const steps = hi - lo + 2;
|
|
1648
|
+
for (let k = lo; k <= hi; k++) {
|
|
1649
|
+
tg[k] = wrap(startA + ((k - lo + 1) / steps) * total);
|
|
1650
|
+
}
|
|
1651
|
+
s.step = r3(total / steps);
|
|
1652
|
+
}
|
|
1653
|
+
const holds = sw.chainHolds ?? [];
|
|
1654
|
+
for (const h of holds) {
|
|
1655
|
+
const swParent = sw.switches.find((s) => s.set === h.set && s.frames[0] === h.frames[0] && s.frames[1] > h.frames[1]);
|
|
1656
|
+
if (!swParent) continue;
|
|
1657
|
+
const nextH = holds.find((nh) => nh.chain === h.chain && nh.frames[0] > h.frames[1] && nh.frames[0] - h.frames[1] <= 10);
|
|
1658
|
+
if (!nextH) continue;
|
|
1659
|
+
const lo = h.frames[1] + 1;
|
|
1660
|
+
const hi = nextH.frames[0] - 1;
|
|
1661
|
+
if (lo > hi || lo <= 0 || hi + 1 >= n) continue;
|
|
1662
|
+
const ch = map.chains.find((c) => c.name === h.chain);
|
|
1663
|
+
if (!ch) continue;
|
|
1664
|
+
const steps = hi - lo + 2;
|
|
1665
|
+
for (const b of ch.bones) {
|
|
1666
|
+
const tg = targets.get(b);
|
|
1667
|
+
if (!tg) continue;
|
|
1668
|
+
const startA = tg[lo - 1]!;
|
|
1669
|
+
const total = wrap(tg[hi + 1]! - startA);
|
|
1670
|
+
for (let k = lo; k <= hi; k++) {
|
|
1671
|
+
tg[k] = wrap(startA + ((k - lo + 1) / steps) * total);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1622
1676
|
|
|
1623
1677
|
// The legs on their final targets: a bone under a scaled bone is keyed through
|
|
1624
1678
|
// its parent's posed matrix (legs.ts), and each ankle's world position is read.
|
|
@@ -1912,6 +1966,79 @@ export function holdShort(angles: number[], fraction: readonly number[], cutoff:
|
|
|
1912
1966
|
return out;
|
|
1913
1967
|
}
|
|
1914
1968
|
|
|
1969
|
+
/**
|
|
1970
|
+
* Hold and interpolate across unconfident spans on a horizontal chain (`isAcross`,
|
|
1971
|
+
* e.g. `r_hip → l_hip`, `r_eye → l_eye`, `r_shoulder → l_shoulder`, rig-c/pose#184),
|
|
1972
|
+
* in place. A frame is unconfident when its picture length over its median is under
|
|
1973
|
+
* `shortCutoff` (with `--short`) or when its angle turns more than `flipThreshold`
|
|
1974
|
+
* from the last confident angle or reverses more than 90° from the line's rest
|
|
1975
|
+
* direction `restDir` (with `--flip`). Each contiguous unconfident run has its
|
|
1976
|
+
* angles interpolated between the confident frames either side (flat at the ends),
|
|
1977
|
+
* the short way round, exactly as `holdShort` does on limb chains.
|
|
1978
|
+
*/
|
|
1979
|
+
export function holdAcross(
|
|
1980
|
+
angles: number[],
|
|
1981
|
+
fraction: readonly number[],
|
|
1982
|
+
shortCutoff: number | 'off',
|
|
1983
|
+
flipThreshold: number | 'off',
|
|
1984
|
+
restDir = 0,
|
|
1985
|
+
): ShortSpan[] {
|
|
1986
|
+
const n = angles.length;
|
|
1987
|
+
if (n === 0 || (shortCutoff === 'off' && flipThreshold === 'off')) return [];
|
|
1988
|
+
const raw = [...angles];
|
|
1989
|
+
const unconf = new Array<boolean>(n).fill(false);
|
|
1990
|
+
let lastConfident = raw[0]!;
|
|
1991
|
+
for (let i = 0; i < n; i++) {
|
|
1992
|
+
const s = shortCutoff !== 'off' && fraction[i]! < shortCutoff;
|
|
1993
|
+
const rev = Math.abs(wrap(raw[i]! - restDir)) > 90;
|
|
1994
|
+
if (!s && !rev) {
|
|
1995
|
+
lastConfident = raw[i]!;
|
|
1996
|
+
break;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
for (let i = 0; i < n; i++) {
|
|
2000
|
+
const isShort = shortCutoff !== 'off' && fraction[i]! < shortCutoff;
|
|
2001
|
+
const isFlipped =
|
|
2002
|
+
flipThreshold !== 'off' &&
|
|
2003
|
+
(Math.abs(wrap(raw[i]! - lastConfident)) > flipThreshold || Math.abs(wrap(raw[i]! - restDir)) > 90);
|
|
2004
|
+
if (isShort || isFlipped) {
|
|
2005
|
+
unconf[i] = true;
|
|
2006
|
+
} else {
|
|
2007
|
+
lastConfident = raw[i]!;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
const out: ShortSpan[] = [];
|
|
2011
|
+
for (let i = 0; i < n; i++) {
|
|
2012
|
+
if (!unconf[i]) continue;
|
|
2013
|
+
let j = i;
|
|
2014
|
+
while (j + 1 < n && unconf[j + 1]) j++;
|
|
2015
|
+
const lo = i - 1;
|
|
2016
|
+
const hi = j + 1;
|
|
2017
|
+
let minFraction = fraction[i]!;
|
|
2018
|
+
for (let k = i + 1; k <= j; k++) minFraction = Math.min(minFraction, fraction[k]!);
|
|
2019
|
+
let maxTurn = 0;
|
|
2020
|
+
for (let k = Math.max(1, i); k <= Math.min(n - 1, j + 1); k++) {
|
|
2021
|
+
const jmp = wrap(raw[k]! - raw[k - 1]!);
|
|
2022
|
+
if (Math.abs(jmp) > Math.abs(maxTurn)) maxTurn = jmp;
|
|
2023
|
+
}
|
|
2024
|
+
if (lo >= 0 || hi < n) {
|
|
2025
|
+
const a = lo >= 0 ? angles[lo]! : angles[hi]!;
|
|
2026
|
+
const b = hi < n ? angles[hi]! : a;
|
|
2027
|
+
const turn = wrap(b - a);
|
|
2028
|
+
for (let k = i; k <= j; k++) angles[k] = lo < 0 ? b : wrap(a + (turn * (k - lo)) / (hi - lo));
|
|
2029
|
+
}
|
|
2030
|
+
const revThreshold = flipThreshold !== 'off' ? flipThreshold : 90;
|
|
2031
|
+
out.push({
|
|
2032
|
+
frames: [i, j],
|
|
2033
|
+
minFraction,
|
|
2034
|
+
across: true,
|
|
2035
|
+
...(Math.abs(maxTurn) > revThreshold ? { reversed: r3(maxTurn) } : {}),
|
|
2036
|
+
});
|
|
2037
|
+
i = j;
|
|
2038
|
+
}
|
|
2039
|
+
return out;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1915
2042
|
/** Short spans `gap` frames apart or fewer, as one run each: its first and last frame, and the lowest fraction. */
|
|
1916
2043
|
export function mergeRuns(spans: readonly ShortSpan[], gap: number): ShortSpan[] {
|
|
1917
2044
|
const out: ShortSpan[] = [];
|
|
@@ -1951,24 +2078,26 @@ export function capShortSteps(
|
|
|
1951
2078
|
}
|
|
1952
2079
|
let segEnd = segStart;
|
|
1953
2080
|
while (segEnd + 1 < n && covered?.[segEnd + 1] !== true) segEnd++;
|
|
1954
|
-
|
|
2081
|
+
const loBound = Math.max(0, segStart - 1);
|
|
2082
|
+
const hiBound = Math.min(n - 1, segEnd + 1);
|
|
2083
|
+
if (hiBound - loBound >= 2) {
|
|
1955
2084
|
const u: number[] = new Array<number>(n).fill(0);
|
|
1956
|
-
u[
|
|
1957
|
-
for (let i =
|
|
2085
|
+
u[loBound] = angles[loBound]!;
|
|
2086
|
+
for (let i = loBound + 1; i <= hiBound; i++) u[i] = u[i - 1]! + wrap(angles[i]! - angles[i - 1]!);
|
|
1958
2087
|
const fits = (lo: number, hi: number) => Math.abs(u[hi]! - u[lo]!) <= stepCap * (hi - lo);
|
|
1959
2088
|
interface Win { lo: number; hi: number; asked: number }
|
|
1960
2089
|
const wins: Win[] = [];
|
|
1961
|
-
for (let k =
|
|
2090
|
+
for (let k = loBound + 1; k <= hiBound; k++) {
|
|
1962
2091
|
if (!(Math.min(fraction[k - 1]!, fraction[k]!) < fractionCutoff)) continue;
|
|
1963
2092
|
const jump = u[k]! - u[k - 1]!;
|
|
1964
2093
|
if (Math.abs(jump) <= stepCap) continue;
|
|
1965
2094
|
let h = 1;
|
|
1966
|
-
let lo = Math.max(
|
|
1967
|
-
let hi = Math.min(
|
|
1968
|
-
while (!fits(lo, hi) && (lo >
|
|
2095
|
+
let lo = Math.max(loBound, k - h - 1);
|
|
2096
|
+
let hi = Math.min(hiBound, k + h);
|
|
2097
|
+
while (!fits(lo, hi) && (lo > loBound || hi < hiBound)) {
|
|
1969
2098
|
h++;
|
|
1970
|
-
lo = Math.max(
|
|
1971
|
-
hi = Math.min(
|
|
2099
|
+
lo = Math.max(loBound, k - h - 1);
|
|
2100
|
+
hi = Math.min(hiBound, k + h);
|
|
1972
2101
|
}
|
|
1973
2102
|
wins.push({ lo, hi, asked: jump });
|
|
1974
2103
|
}
|
|
@@ -1985,9 +2114,9 @@ export function capShortSteps(
|
|
|
1985
2114
|
}
|
|
1986
2115
|
if (changed) continue;
|
|
1987
2116
|
for (const w of wins) {
|
|
1988
|
-
if (fits(w.lo, w.hi) || (w.lo ===
|
|
1989
|
-
w.lo = Math.max(
|
|
1990
|
-
w.hi = Math.min(
|
|
2117
|
+
if (fits(w.lo, w.hi) || (w.lo === loBound && w.hi === hiBound)) continue;
|
|
2118
|
+
w.lo = Math.max(loBound, w.lo - 1);
|
|
2119
|
+
w.hi = Math.min(hiBound, w.hi + 1);
|
|
1991
2120
|
changed = true;
|
|
1992
2121
|
}
|
|
1993
2122
|
}
|
|
@@ -2011,9 +2140,9 @@ export function capShortSteps(
|
|
|
2011
2140
|
const spanOf = (frames: [number, number], times: readonly number[]): string =>
|
|
2012
2141
|
frames[0] === frames[1] ? `frame ${frames[0]} (t=${times[frames[0]]!.toFixed(3)}s)` : `frames ${frames[0]}–${frames[1]} (t=${times[frames[0]]!.toFixed(3)}–${times[frames[1]]!.toFixed(3)}s)`;
|
|
2013
2142
|
|
|
2014
|
-
export function shortLine(s: Short, times: readonly number[], cutoff: number): string {
|
|
2143
|
+
export function shortLine(s: Short, times: readonly number[], cutoff: number | 'off'): string {
|
|
2144
|
+
const sg = (v: number) => `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)}°`;
|
|
2015
2145
|
if (s.step !== undefined && s.asked !== undefined) {
|
|
2016
|
-
const sg = (v: number) => `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)}°`;
|
|
2017
2146
|
return `chain "${s.chain}" bone "${s.bone}" (${s.joints[0]} → ${s.joints[1]}) ${spanOf(s.frames, times)}: picture length down to ${s.minFraction.toFixed(2)} of its median, under ${SHORT_STEP_FRACTION.toFixed(2)}, turned ${sg(s.asked)} in one frame (over ${SHORT_STEP_CAP}°); spread to ${sg(s.step)} per frame`;
|
|
2018
2147
|
}
|
|
2019
2148
|
const lo = s.frames[0] - 1;
|
|
@@ -2023,6 +2152,12 @@ export function shortLine(s: Short, times: readonly number[], cutoff: number): s
|
|
|
2023
2152
|
: lo < 0 ? `held flat at frame ${hi}'s angle (the pose starts short)`
|
|
2024
2153
|
: hi >= times.length ? `held flat at frame ${lo}'s angle (the pose ends short)`
|
|
2025
2154
|
: `interpolated from frame ${lo}'s angle to frame ${hi}'s, the short way round`;
|
|
2155
|
+
if (s.across === true) {
|
|
2156
|
+
const shortWhy = cutoff !== 'off' && s.minFraction < cutoff ? `picture length down to ${s.minFraction.toFixed(2)} of its median, under ${cutoff}` : null;
|
|
2157
|
+
const revWhy = s.reversed !== undefined ? `reversed ${sg(s.reversed)} in one frame` : null;
|
|
2158
|
+
const why = shortWhy !== null && revWhy !== null ? `${shortWhy}, ${revWhy}` : shortWhy ?? `${revWhy ?? 'reversed'} (picture length down to ${s.minFraction.toFixed(2)} of its median)`;
|
|
2159
|
+
return `chain "${s.chain}" bone "${s.bone}" (${s.joints[0]} → ${s.joints[1]}) ${spanOf(s.frames, times)}: ${why}; its direction is not read — ${held}`;
|
|
2160
|
+
}
|
|
2026
2161
|
return `chain "${s.chain}" bone "${s.bone}" (${s.joints[0]} → ${s.joints[1]}) ${spanOf(s.frames, times)}: picture length down to ${s.minFraction.toFixed(2)} of its median, under ${cutoff}; its direction is not read — ${held}`;
|
|
2027
2162
|
}
|
|
2028
2163
|
|
package/src/run.ts
CHANGED
|
@@ -135,8 +135,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
135
135
|
else if (a.plantFeet === true) say(' CAMERA none in the sequence: --plant-feet reads the picture as a fixed camera, so a zoom or a pan in the video moves the floor with it and the floor may step with the picture (tools/scan.py reads the camera, rig-c/pose#63)');
|
|
136
136
|
if (r.scale !== null) say(` SCALE ${r.scale.factor.toFixed(5)} rig units per picture px (rig ${r.scale.rig.toFixed(2)} / pose median ${r.scale.pose.toFixed(2)})`);
|
|
137
137
|
for (const h of r.holds) say(` HOLD ${holdLine(h, r.times)}`);
|
|
138
|
+
for (const s of r.short) say(` ${s.across === true ? 'HOLD ' : 'SHORT '} ${shortLine(s, r.times, a.short)}`);
|
|
138
139
|
if (a.short !== 'off') {
|
|
139
|
-
for (const s of r.short) say(` SHORT ${shortLine(s, r.times, a.short)}`);
|
|
140
140
|
for (const e of r.endOn) say(` END-ON ${endOnLine(e, r.times, a.short)}`);
|
|
141
141
|
}
|
|
142
142
|
if (a.flip !== 'off') for (const fl of r.flips) say(` FLIP ${flipLine(fl, r.times, a.flip)}`);
|
package/src/switch.ts
CHANGED
|
@@ -322,6 +322,7 @@ function groupLayering(group: readonly AlternateSet[], map: JointMap, slots: Rig
|
|
|
322
322
|
const setupSlots = hiddenSetup.filter((s) => slotChain.get(s) === cn && slots.get(s)?.bone !== torsoSlot);
|
|
323
323
|
const setSlots = Object.keys(a.slots).filter((s) => slotChain.get(s) === cn && rank.has(s));
|
|
324
324
|
if (setupSlots.length === 0 || setSlots.length === 0) return false;
|
|
325
|
+
if (setupSlots.every((s) => a.slots[s] !== undefined && a.slots[s] !== slots.get(s)?.setup)) return true;
|
|
325
326
|
const boneSetup = setupSlots.filter((s) => boneOwner.has(slots.get(s)?.bone ?? ''));
|
|
326
327
|
const refSetup = boneSetup.length > 0 ? boneSetup : setupSlots;
|
|
327
328
|
return setupSlots.every((s) => a.hides.includes(s)) && Math.max(...setSlots.map((s) => rank.get(s)!)) < Math.min(...refSetup.map((s) => rank.get(s)!));
|
|
@@ -626,8 +627,9 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
626
627
|
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = o;
|
|
627
628
|
}
|
|
628
629
|
}
|
|
629
|
-
// #136/#139/#162/#180: between two sets
|
|
630
|
-
//
|
|
630
|
+
// #136/#139/#162/#180/#187: between two gesture sets, switch both arms on one frame t_switch
|
|
631
|
+
// where the measured path crosses halfway between their drawn poses without dropping to the base arm,
|
|
632
|
+
// and in a one-sided gesture set keep an arm whose wrist stays behind the torso on its previous behind set.
|
|
631
633
|
const chainHolds: Array<{ set: string; chain: string; frames: [number, number] }> = [];
|
|
632
634
|
for (const group of groups) {
|
|
633
635
|
const layer = groupLayering(group, map, slots);
|
|
@@ -645,6 +647,56 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
645
647
|
}
|
|
646
648
|
cClamp.set(cn, arr);
|
|
647
649
|
}
|
|
650
|
+
for (const s of layer.touched) {
|
|
651
|
+
if (!shown.has(s)) shown.set(s, Array.from({ length: n }, () => slots.get(s)!.setup));
|
|
652
|
+
}
|
|
653
|
+
// #187 Rule 3: in a one-sided gesture set (one chain raised with drawn > 0 while another chain has all
|
|
654
|
+
// bones drawn < 0 within switchTolerance of the preceding behind-the-torso set), an arm whose wrist the
|
|
655
|
+
// scan places behind the torso keeps its previous behind-the-torso set and angles.
|
|
656
|
+
const heldBehindOnSwitch = new Map<number, Set<string>>();
|
|
657
|
+
for (let idx = 1; idx < gSwitches.length; idx++) {
|
|
658
|
+
const sw = gSwitches[idx]!;
|
|
659
|
+
const prevSw = gSwitches[idx - 1]!;
|
|
660
|
+
const set = group.find((a) => a.name === sw.set)!;
|
|
661
|
+
const prevSet = group.find((a) => a.name === prevSw.set)!;
|
|
662
|
+
if (!layer.isBehind(prevSet) || set === prevSet || set.chains.length < 2) continue;
|
|
663
|
+
const hasUpChain = set.chains.some((cn) => {
|
|
664
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
665
|
+
return c !== undefined && c.bones.some((b) => (set.drawn[b] ?? -180) > 0);
|
|
666
|
+
});
|
|
667
|
+
if (!hasUpChain) continue;
|
|
668
|
+
for (const cn of set.chains) {
|
|
669
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
670
|
+
if (c === undefined) continue;
|
|
671
|
+
const allDown = c.bones.every((b) => (set.drawn[b] ?? 0) < 0);
|
|
672
|
+
if (!allDown) continue;
|
|
673
|
+
const keyNearPrev =
|
|
674
|
+
c.bones.every((b) => set.drawn[b] !== undefined && prevSet.drawn[b] !== undefined) &&
|
|
675
|
+
c.bones.reduce((s, b) => s + Math.abs(wrap(set.drawn[b]! - prevSet.drawn[b]!)), 0) / c.bones.length <= opts.switchTolerance;
|
|
676
|
+
if (!keyNearPrev) continue;
|
|
677
|
+
const ho = opts.handOffsets?.get(cn);
|
|
678
|
+
if (ho === undefined) continue;
|
|
679
|
+
const slice = ho.slice(sw.frames[0], sw.frames[1] + 1);
|
|
680
|
+
const medDy = median(slice.map((p) => p[1]));
|
|
681
|
+
const medDx = median(slice.map((p) => p[0]));
|
|
682
|
+
const isRight = /(?:^|_)r(?:_|$)/.test(cn);
|
|
683
|
+
const inward = isRight ? medDx >= -0.05 : medDx <= 0.05;
|
|
684
|
+
if (!(medDy > 0.5 && inward)) continue;
|
|
685
|
+
let setHeld = heldBehindOnSwitch.get(idx);
|
|
686
|
+
if (setHeld === undefined) {
|
|
687
|
+
setHeld = new Set();
|
|
688
|
+
heldBehindOnSwitch.set(idx, setHeld);
|
|
689
|
+
}
|
|
690
|
+
setHeld.add(cn);
|
|
691
|
+
const cArr = cClamp.get(cn)!;
|
|
692
|
+
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) {
|
|
693
|
+
cArr[i] = prevSet;
|
|
694
|
+
for (const [s, sc] of layer.slotChain) {
|
|
695
|
+
if (sc === cn && shown.has(s)) shown.get(s)![i] = prevSet.slots[s] ?? null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
648
700
|
for (let m = 0; m + 1 < gSwitches.length; m++) {
|
|
649
701
|
const sw0 = gSwitches[m]!;
|
|
650
702
|
const sw1 = gSwitches[m + 1]!;
|
|
@@ -656,6 +708,14 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
656
708
|
if (g0 > g1) continue;
|
|
657
709
|
const chains = [...new Set([...set0.chains, ...set1.chains])].filter((cn) => layer.isChainBehindBase(set0, cn) && layer.isChainBehindBase(set1, cn));
|
|
658
710
|
if (chains.length === 0) continue;
|
|
711
|
+
const heldOn0 = heldBehindOnSwitch.get(m);
|
|
712
|
+
const heldOn1 = heldBehindOnSwitch.get(m + 1);
|
|
713
|
+
const heldSetFor = (cn: string): AlternateSet | null => {
|
|
714
|
+
if (heldOn1?.has(cn)) return set0;
|
|
715
|
+
if (heldOn0?.has(cn) && m > 0 && set1 === group.find((a) => a.name === gSwitches[m - 1]!.set)) return set1;
|
|
716
|
+
return null;
|
|
717
|
+
};
|
|
718
|
+
const switchingChains = chains.filter((cn) => heldSetFor(cn) === null);
|
|
659
719
|
for (const cn of chains) {
|
|
660
720
|
if (!layer.isBehind(set0)) for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) cClamp.get(cn)![i] = set0;
|
|
661
721
|
if (!layer.isBehind(set1)) for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) cClamp.get(cn)![i] = set1;
|
|
@@ -673,11 +733,29 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
673
733
|
}
|
|
674
734
|
return d0 <= d1 ? set0 : set1;
|
|
675
735
|
};
|
|
736
|
+
const rawChainMax = (a: AlternateSet, cn: string, i: number): number => {
|
|
737
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
738
|
+
if (c === undefined) return Infinity;
|
|
739
|
+
return Math.max(...c.bones.map((b) => (a.drawn[b] !== undefined ? Math.abs(wrap(targets.get(b)![i]! - a.drawn[b]!)) : 0)));
|
|
740
|
+
};
|
|
741
|
+
const isTightSet = (a: AlternateSet, cn: string): boolean => {
|
|
742
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
743
|
+
return c !== undefined && c.bones.some((b) => a.turn[b] !== undefined && a.turn[b]![1] - a.turn[b]![0] <= 10);
|
|
744
|
+
};
|
|
676
745
|
const cPick = new Map<string, Array<AlternateSet | null>>();
|
|
677
746
|
for (const cn of chains) {
|
|
747
|
+
const heldSet = heldSetFor(cn);
|
|
678
748
|
const arr: Array<AlternateSet | null> = Array.from({ length: n }, () => null);
|
|
679
|
-
for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) arr[i] = set0;
|
|
680
|
-
for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) arr[i] = set1;
|
|
749
|
+
for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) arr[i] = heldSet ?? set0;
|
|
750
|
+
for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) arr[i] = heldSet ?? set1;
|
|
751
|
+
if (heldSet !== null) {
|
|
752
|
+
for (let i = g0; i <= g1; i++) {
|
|
753
|
+
arr[i] = heldSet;
|
|
754
|
+
cClamp.get(cn)![i] = heldSet;
|
|
755
|
+
}
|
|
756
|
+
cPick.set(cn, arr);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
681
759
|
const d0s = byChain.get(set0.name)?.get(cn);
|
|
682
760
|
const d1s = byChain.get(set1.name)?.get(cn);
|
|
683
761
|
for (let i = g0; i <= g1; i++) {
|
|
@@ -706,24 +784,142 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
706
784
|
}
|
|
707
785
|
i = j + 1;
|
|
708
786
|
}
|
|
709
|
-
|
|
710
|
-
|
|
787
|
+
const tight0 = isTightSet(set0, cn);
|
|
788
|
+
const tight1 = isTightSet(set1, cn);
|
|
789
|
+
if (tight0 && set0 !== set1) {
|
|
790
|
+
let i = g0;
|
|
791
|
+
while (i <= g1 && rawChainMax(set0, cn, i) <= opts.switchTolerance + hyst) {
|
|
792
|
+
cClamp.get(cn)![i] = set0;
|
|
793
|
+
i++;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (tight1 && set0 !== set1) {
|
|
797
|
+
let enter1 = g1 + 1;
|
|
798
|
+
for (let i = g0; i <= g1; i++) {
|
|
799
|
+
if (rawChainMax(set1, cn, i) <= opts.switchTolerance) {
|
|
800
|
+
enter1 = i;
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
for (let i = enter1; i <= g1; i++) cClamp.get(cn)![i] = set1;
|
|
805
|
+
}
|
|
806
|
+
if (!tight0 && !tight1) {
|
|
807
|
+
for (let i = g0; i <= g1; i++) {
|
|
808
|
+
if (arr[i] !== null) cClamp.get(cn)![i] = arr[i]!;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (!tight0 && set0 !== set1) {
|
|
812
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
813
|
+
if (c !== undefined) {
|
|
814
|
+
let k = sw0.frames[1];
|
|
815
|
+
while (
|
|
816
|
+
k >= sw0.frames[0] &&
|
|
817
|
+
c.bones.some((b) => {
|
|
818
|
+
const lim = set0.turn[b];
|
|
819
|
+
const dr = set0.drawn[b];
|
|
820
|
+
const ndr = set1.drawn[b];
|
|
821
|
+
if (lim === undefined || dr === undefined || ndr === undefined) return false;
|
|
822
|
+
const t = wrap(targets.get(b)![k]! - dr);
|
|
823
|
+
return overflow(t, lim) > RANGE_SLACK && Math.abs(wrap(targets.get(b)![k + 1]! - ndr)) < Math.abs(wrap(targets.get(b)![k]! - ndr));
|
|
824
|
+
})
|
|
825
|
+
) {
|
|
826
|
+
cClamp.get(cn)![k] = null;
|
|
827
|
+
k--;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
711
830
|
}
|
|
712
831
|
cPick.set(cn, arr);
|
|
713
832
|
}
|
|
714
|
-
|
|
833
|
+
// #187 Rule 1 & Rule 2: when two gesture sets are adjacent (gap <= 10 frames, or the arm is moving
|
|
834
|
+
// toward the next set's pose), unwrap any bone that turned the long way (> 180° through inverted angles)
|
|
835
|
+
// onto the short arc and pick one shared switch frame t_switch where the measured path crosses halfway
|
|
836
|
+
// between set0.drawn and set1.drawn.
|
|
837
|
+
const movingTowardNext =
|
|
838
|
+
set0 !== set1 &&
|
|
839
|
+
distances.get(set1.name)![g1]! < distances.get(set1.name)![g0]! &&
|
|
840
|
+
distances.get(set0.name)![g1]! > distances.get(set0.name)![g0]!;
|
|
841
|
+
const bridgeDirect = set0 !== set1 && (g1 - g0 + 1 <= 10 || movingTowardNext);
|
|
842
|
+
let tSwitch = g1 + 1;
|
|
843
|
+
if (bridgeDirect && switchingChains.length > 0) {
|
|
844
|
+
const swBones = switchingChains.flatMap((cn) => map.chains.find((c) => c.name === cn)?.bones ?? []);
|
|
845
|
+
for (const b of swBones) {
|
|
846
|
+
const tg = targets.get(b) as number[] | undefined;
|
|
847
|
+
const d0 = set0.drawn[b];
|
|
848
|
+
const d1 = set1.drawn[b];
|
|
849
|
+
if (tg === undefined || d0 === undefined || d1 === undefined) continue;
|
|
850
|
+
const i0 = sw0.frames[1];
|
|
851
|
+
const i1 = sw1.frames[0];
|
|
852
|
+
const startA = tg[i0]!;
|
|
853
|
+
const endA = tg[i1]!;
|
|
854
|
+
const unwrapped: number[] = [startA];
|
|
855
|
+
for (let i = i0 + 1; i <= i1; i++) {
|
|
856
|
+
unwrapped.push(unwrapped[unwrapped.length - 1]! + wrap(tg[i]! - tg[i - 1]!));
|
|
857
|
+
}
|
|
858
|
+
const totalTurn = unwrapped[unwrapped.length - 1]! - startA;
|
|
859
|
+
const shortTurn = wrap(endA - startA);
|
|
860
|
+
const invertsBetween = Array.from({ length: g1 - g0 + 1 }, (_, k) => {
|
|
861
|
+
const a = tg[g0 + k]!;
|
|
862
|
+
return Math.abs(wrap(a - d0)) > 90 && Math.abs(wrap(a - d1)) > 90;
|
|
863
|
+
}).some(Boolean);
|
|
864
|
+
if (Math.abs(totalTurn) > 180 && Math.abs(totalTurn - shortTurn) > 1e-6 && invertsBetween && Math.abs(shortTurn) <= 150) {
|
|
865
|
+
for (let i = g0; i <= g1; i++) {
|
|
866
|
+
const p = (unwrapped[i - i0]! - startA) / totalTurn;
|
|
867
|
+
tg[i] = wrap(startA + p * shortTurn);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
let clampEnd0 = g0 - 1;
|
|
872
|
+
while (clampEnd0 + 1 <= g1 && switchingChains.every((cn) => cClamp.get(cn)![clampEnd0 + 1] === set0)) {
|
|
873
|
+
clampEnd0++;
|
|
874
|
+
}
|
|
875
|
+
const effAngle = (b: string, i: number): number => {
|
|
876
|
+
const raw = targets.get(b)![i]!;
|
|
877
|
+
if (clampEnd0 >= g0 && i > clampEnd0) {
|
|
878
|
+
const frac = opts.shortInfo?.fractions.get(b)?.[i] ?? 1;
|
|
879
|
+
const d0 = set0.drawn[b];
|
|
880
|
+
const d1 = set1.drawn[b];
|
|
881
|
+
if (frac < 0.6 && d0 !== undefined && d1 !== undefined) {
|
|
882
|
+
const steps = g1 + 1 - clampEnd0;
|
|
883
|
+
return wrap(d0 + ((i - clampEnd0) / steps) * wrap(d1 - d0));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return raw;
|
|
887
|
+
};
|
|
888
|
+
const pairs = swBones.filter((b) => set0.drawn[b] !== undefined && set1.drawn[b] !== undefined);
|
|
889
|
+
const span = pairs.length > 0
|
|
890
|
+
? pairs.reduce((s, b) => s + Math.abs(wrap(set1.drawn[b]! - set0.drawn[b]!)), 0) / pairs.length
|
|
891
|
+
: 0;
|
|
892
|
+
for (let i = Math.max(g0, clampEnd0 + 1); i <= g1; i++) {
|
|
893
|
+
const d0 = pairs.length > 0
|
|
894
|
+
? pairs.reduce((s, b) => s + Math.abs(wrap(effAngle(b, i) - set0.drawn[b]!)), 0) / pairs.length
|
|
895
|
+
: (distances.get(set0.name)?.[i] ?? Infinity);
|
|
896
|
+
const d1 = pairs.length > 0
|
|
897
|
+
? pairs.reduce((s, b) => s + Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)), 0) / pairs.length
|
|
898
|
+
: (distances.get(set1.name)?.[i] ?? Infinity);
|
|
899
|
+
if (d1 <= d0 || (span > 1e-6 && d0 >= 0.5 * span)) {
|
|
900
|
+
tSwitch = i;
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const anyHeldBehind = switchingChains.some((cn) => {
|
|
715
906
|
const arr = cPick.get(cn)!;
|
|
716
907
|
for (let i = g0; i <= g1; i++) if (arr[i] === null) return false;
|
|
717
908
|
return true;
|
|
718
909
|
});
|
|
719
|
-
if (!anyHeldBehind
|
|
720
|
-
const frontNear = group.filter((a) => layer.isFront(a)).some((a) => {
|
|
910
|
+
if (!anyHeldBehind) {
|
|
911
|
+
const frontNear = group.filter((a) => layer.isFront(a) && a !== set0 && a !== set1).some((a) => {
|
|
721
912
|
for (let i = g0; i <= g1; i++) if (distances.get(a.name)![i]! <= opts.switchTolerance) return true;
|
|
722
913
|
return false;
|
|
723
914
|
});
|
|
724
|
-
if (
|
|
915
|
+
if (bridgeDirect) {
|
|
916
|
+
for (const cn of switchingChains) {
|
|
917
|
+
for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = i < tSwitch ? set0 : set1;
|
|
918
|
+
}
|
|
919
|
+
} else if (!frontNear) for (const cn of chains) for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = set0;
|
|
725
920
|
} else {
|
|
726
921
|
for (const cn of chains) {
|
|
922
|
+
if (heldSetFor(cn) !== null) continue;
|
|
727
923
|
const arr = cPick.get(cn)!;
|
|
728
924
|
const frontSets = group.filter((a) => layer.isFront(a) && a.chains.includes(cn) && a !== set0 && a !== set1);
|
|
729
925
|
const d0s = byChain.get(set0.name)?.get(cn);
|
|
@@ -771,21 +967,20 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
771
967
|
}
|
|
772
968
|
i = j + 1;
|
|
773
969
|
}
|
|
774
|
-
|
|
775
|
-
|
|
970
|
+
}
|
|
971
|
+
if (bridgeDirect) {
|
|
972
|
+
for (const cn of switchingChains) {
|
|
973
|
+
for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = i < tSwitch ? set0 : set1;
|
|
776
974
|
}
|
|
777
975
|
}
|
|
778
976
|
}
|
|
779
|
-
for (const s of layer.touched) {
|
|
780
|
-
if (!shown.has(s)) shown.set(s, Array.from({ length: n }, () => slots.get(s)!.setup));
|
|
781
|
-
}
|
|
782
977
|
for (let i = g0; i <= g1; i++) {
|
|
783
978
|
for (const [s, cn] of layer.slotChain) {
|
|
784
979
|
const a = cPick.get(cn)?.[i] ?? null;
|
|
785
980
|
if (a !== null) shown.get(s)![i] = a.slots[s] ?? null;
|
|
786
981
|
}
|
|
787
|
-
if (layer.torsoSlot !== null && shown.has(layer.torsoSlot)
|
|
788
|
-
const picked = chains.map((cn) => cPick.get(cn)![i]);
|
|
982
|
+
if (layer.torsoSlot !== null && shown.has(layer.torsoSlot)) {
|
|
983
|
+
const picked = (switchingChains.length > 0 ? switchingChains : chains).map((cn) => cPick.get(cn)![i]);
|
|
789
984
|
if (picked.every((a) => a !== null)) shown.get(layer.torsoSlot)![i] = picked[0]!.slots[layer.torsoSlot] ?? slots.get(layer.torsoSlot)!.setup;
|
|
790
985
|
}
|
|
791
986
|
}
|
|
@@ -834,8 +1029,10 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
834
1029
|
/** Per chain, the frames on which a switch of a set depicting it shows: its art was painted for that pose. */
|
|
835
1030
|
export function coveredChains(switches: readonly Switch[], n: number, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): Map<string, boolean[]> {
|
|
836
1031
|
const out = new Map<string, boolean[]>();
|
|
1032
|
+
const held = new Set((chainHolds ?? []).map((h) => `${h.set}:${h.chain}`));
|
|
837
1033
|
for (const sw of switches) {
|
|
838
1034
|
for (const c of sw.chains) {
|
|
1035
|
+
if (held.has(`${sw.set}:${c}`)) continue;
|
|
839
1036
|
if (!out.has(c)) out.set(c, Array.from({ length: n }, () => false));
|
|
840
1037
|
const row = out.get(c)!;
|
|
841
1038
|
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = true;
|
|
@@ -1017,7 +1214,7 @@ export const ART_EASE_STEP = 3;
|
|
|
1017
1214
|
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): void {
|
|
1018
1215
|
const boneChain = new Map<string, string>();
|
|
1019
1216
|
for (const c of map.chains) for (const b of c.bones) boneChain.set(b, c.name);
|
|
1020
|
-
const clampSpan = (set: AlternateSet, bone: string, [a,
|
|
1217
|
+
const clampSpan = (set: AlternateSet, bone: string, [a, b0]: [number, number]) => {
|
|
1021
1218
|
const limits = set.turn[bone];
|
|
1022
1219
|
if (limits === undefined) return;
|
|
1023
1220
|
const drawn = set.drawn[bone]!;
|
|
@@ -1028,12 +1225,29 @@ export function clampArt(switches: readonly Switch[], map: JointMap, targets: Ma
|
|
|
1028
1225
|
const c = Math.min(Math.max(t, limits[0]), limits[1]);
|
|
1029
1226
|
return Math.abs(c - t) > RANGE_SLACK ? c - t : 0;
|
|
1030
1227
|
};
|
|
1228
|
+
const tightHold = limits[1] - limits[0] <= 10;
|
|
1229
|
+
let b = b0;
|
|
1230
|
+
let trimmedExit = false;
|
|
1231
|
+
if (!tightHold && b + 1 < n && delta(b) !== 0) {
|
|
1232
|
+
const nextSw = switches.find((s) => s.frames[0] > b && s.frames[0] - b <= 10 && s.set !== set.name);
|
|
1233
|
+
const nextDrawn = nextSw !== undefined ? map.alternates.find((x) => x.name === nextSw.set)?.drawn[bone] : undefined;
|
|
1234
|
+
if (nextDrawn !== undefined) {
|
|
1235
|
+
const distNext = (i: number) => Math.abs(wrap(target[i]! - nextDrawn));
|
|
1236
|
+
if (distNext(b + 1) < distNext(b)) {
|
|
1237
|
+
while (b >= a && delta(b) !== 0 && (b === a || distNext(b) < distNext(b - 1))) {
|
|
1238
|
+
b--;
|
|
1239
|
+
trimmedExit = true;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (a > b) return;
|
|
1031
1245
|
const da = delta(a);
|
|
1032
1246
|
const db = delta(b);
|
|
1033
|
-
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da)
|
|
1247
|
+
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da) && !tightHold
|
|
1034
1248
|
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(da) / ART_EASE_STEP) - 1)
|
|
1035
1249
|
: 0;
|
|
1036
|
-
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db)
|
|
1250
|
+
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db) && !tightHold && !trimmedExit
|
|
1037
1251
|
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(db) / ART_EASE_STEP) - 1)
|
|
1038
1252
|
: 0;
|
|
1039
1253
|
for (let i = a; i <= b; i++) {
|