@rig-c/pose 0.24.1 → 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/package.json +1 -1
- package/src/retarget.ts +54 -12
- package/src/switch.ts +234 -20
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
|
@@ -1380,6 +1380,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1380
1380
|
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
1381
1381
|
const art: ArtRange[] = [];
|
|
1382
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;
|
|
1383
1384
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
1384
1385
|
const drawn = set.drawn[c.bones[0]!];
|
|
1385
1386
|
const up = isChainHeadPlaced(set, c, drawn);
|
|
@@ -1633,6 +1634,45 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1633
1634
|
if (job.anchor.kind === 'head') raised.push({ set, chain, frames, seconds, eyes: unit, ...body });
|
|
1634
1635
|
else byHip.push({ set, chain, frames, seconds, hips: unit, ...body });
|
|
1635
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
|
+
}
|
|
1636
1676
|
|
|
1637
1677
|
// The legs on their final targets: a bone under a scaled bone is keyed through
|
|
1638
1678
|
// its parent's posed matrix (legs.ts), and each ankle's world position is read.
|
|
@@ -2038,24 +2078,26 @@ export function capShortSteps(
|
|
|
2038
2078
|
}
|
|
2039
2079
|
let segEnd = segStart;
|
|
2040
2080
|
while (segEnd + 1 < n && covered?.[segEnd + 1] !== true) segEnd++;
|
|
2041
|
-
|
|
2081
|
+
const loBound = Math.max(0, segStart - 1);
|
|
2082
|
+
const hiBound = Math.min(n - 1, segEnd + 1);
|
|
2083
|
+
if (hiBound - loBound >= 2) {
|
|
2042
2084
|
const u: number[] = new Array<number>(n).fill(0);
|
|
2043
|
-
u[
|
|
2044
|
-
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]!);
|
|
2045
2087
|
const fits = (lo: number, hi: number) => Math.abs(u[hi]! - u[lo]!) <= stepCap * (hi - lo);
|
|
2046
2088
|
interface Win { lo: number; hi: number; asked: number }
|
|
2047
2089
|
const wins: Win[] = [];
|
|
2048
|
-
for (let k =
|
|
2090
|
+
for (let k = loBound + 1; k <= hiBound; k++) {
|
|
2049
2091
|
if (!(Math.min(fraction[k - 1]!, fraction[k]!) < fractionCutoff)) continue;
|
|
2050
2092
|
const jump = u[k]! - u[k - 1]!;
|
|
2051
2093
|
if (Math.abs(jump) <= stepCap) continue;
|
|
2052
2094
|
let h = 1;
|
|
2053
|
-
let lo = Math.max(
|
|
2054
|
-
let hi = Math.min(
|
|
2055
|
-
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)) {
|
|
2056
2098
|
h++;
|
|
2057
|
-
lo = Math.max(
|
|
2058
|
-
hi = Math.min(
|
|
2099
|
+
lo = Math.max(loBound, k - h - 1);
|
|
2100
|
+
hi = Math.min(hiBound, k + h);
|
|
2059
2101
|
}
|
|
2060
2102
|
wins.push({ lo, hi, asked: jump });
|
|
2061
2103
|
}
|
|
@@ -2072,9 +2114,9 @@ export function capShortSteps(
|
|
|
2072
2114
|
}
|
|
2073
2115
|
if (changed) continue;
|
|
2074
2116
|
for (const w of wins) {
|
|
2075
|
-
if (fits(w.lo, w.hi) || (w.lo ===
|
|
2076
|
-
w.lo = Math.max(
|
|
2077
|
-
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);
|
|
2078
2120
|
changed = true;
|
|
2079
2121
|
}
|
|
2080
2122
|
}
|
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++) {
|