@rig-c/pose 0.20.0 → 0.20.2
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/legs.ts +164 -2
- package/src/map.ts +2 -0
- package/src/retarget.ts +10 -5
- package/src/rigc.ts +1 -1
- package/src/run.ts +12 -1
- package/src/switch.ts +274 -48
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ and what the run cannot decide for you it lists:
|
|
|
136
136
|
- `RAISED` — with `--raised-by-head` ([below](#alternate-drawings-alternates)): one line per span of a set that draws an arm raised, per chain. It gives the frames and the median eye distances (the rig's in units, the reference's in px). It gives the wrist's offset from the head centre in eye distances: the median of the reference's, of where the wrist was placed, of the rig's on the pose's angles, and of the rig's as keyed. It gives the worst |rig − reference| before and after, the worst |after − placed|, and the frames whose upper arm the set's `turn` held. `RAISED none` when the flag is given and no such set showed.
|
|
137
137
|
- `BY-HIP` — with a set's `"place": "by-hip"` ([below](#alternate-drawings-alternates)): the `RAISED` line against the hip, one per span of such a set per chain it places. The unit is the hip width, the offset is from the mid-hip. `BY-HIP none` when the map has such a set and none of its spans showed.
|
|
138
138
|
- `REACH` — with `--raised-by-head`, or a `"by-hip"` set: a placed span whose target was out of the arm's reach, too far or too near. It names the frames, which way, and the shortfall in eye distances (hip widths for `"by-hip"`). The wrist was placed at the reach limit instead. This is not `ART-RANGE`: the rig's proportions were the limit, not the art's range. A too-near frame is placed at the limit like a too-far one, and graded at the placed distance.
|
|
139
|
-
- `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length, the spread of its picture length over it,
|
|
139
|
+
- `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length, the spread of its picture length over it, the frames clamped at each limit, and (with `--plant-feet`) one `past the floor (support)` line per supporting run that asked under the floor ([#133](https://github.com/rig-c/pose/issues/133), `legScale[].support` in the report).
|
|
140
140
|
- `PIVOT` — when the map translates a bone and has both legs: the translated bone's origin against its thigh roots' midpoint at setup, in units and hip widths — the pivot every hip turn swings the legs about.
|
|
141
141
|
- `FEET` — when the map has both legs: the supports and jumps read off the reference ankles (planted or not), and on the posed build, outside the jumps, the lowest ankle's vertical range and each support span's worst vertical range and horizontal drift of the supporting ankle, in leg lengths.
|
|
142
142
|
- `STANCE` — with `--stance` ([below](#the-legs-foreshortening-and-planted-feet)): one line per run of frames planted both (with the stretch keyed, its largest k), lifted, or past the tolerance below (the pose's height kept, [#114](https://github.com/rig-c/pose/issues/114)), with the free side, the frames and the tolerance; and one per run whose stretch fell past its bounds (`past the bounds`), with the k the floor needed, the bound it had and how far the foot was left from it; and, where the leg's own range set a lower bound below 0.92, one `bound from the leg's own range` line with each side's lowest.
|
|
@@ -199,7 +199,7 @@ A knee bending toward the camera is seen from the front only as a shorter leg, a
|
|
|
199
199
|
|
|
200
200
|
rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inherit mode takes its parent's whole world matrix. So under a thigh scaled by s, a shin at angle φ in the thigh's axes points at atan2(sin φ, s·cos φ), and is itself scaled by |(s·cos φ, sin φ)|. The rotate key `turn − turn above` assumes unit scale. A mapped bone under a scaled bone is therefore keyed through its parent's posed matrix: a `rotate` key k and a `scalex` key s' make its local matrix R(k) · L · diag(s', 1), L its setup local matrix, and k and s' are solved so its world x axis lands on its target at the scale asked. On HQ's dancer the unit-scale formula missed the shins by up to 2.4°, their length by 0.16 of setup and the ankles by 38 units; solved, every bone lands. The child's art is still sheared by its parent's scale, and a bone under it (a foot) is squashed along the shin: `SHEAR` reads both.
|
|
201
201
|
|
|
202
|
-
**`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed legs at the scale the pose asked for (when a segment asks for a scale below `--leg-scale`'s clamp floor, the clamp
|
|
202
|
+
**`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed legs at the scale the pose asked for (when a segment asks for a scale below `--leg-scale`'s clamp floor, the clamp does not lift the hip by what it withheld, [#107](https://github.com/rig-c/pose/issues/107); on a supporting leg, the segment's `scalex` follows the asked scale down to the bound #95 derives from the leg's own range so the supporting ankle stays on #85's floor while the free leg keeps the configured clamp, and any frame clamped at #95's bound lifts the hip by the remainder so the ankle never sinks, listed as `LEG-SCALE past the floor (support)`, [#133](https://github.com/rig-c/pose/issues/133)), instead of moving it by the pose's mid_hip travel:
|
|
203
203
|
|
|
204
204
|
- The supporting foot is the one whose reference ankle is lower in the picture. It changes only when the other ankle has been at least **0.02 leg lengths** lower for at least **0.1 s** (timed as `END-ON` runs are), and once confirmed takes support from the frame it crossed that margin ([#102](https://github.com/rig-c/pose/issues/102)); a leg length is each side's median picture length hip → ankle, and the switch compares against their mean.
|
|
205
205
|
- The new support is held where it is on the frame it takes over, and the hip blends in y from the old derivation to the new over **3 frames** while taking the new derivation's x immediately ([#102](https://github.com/rig-c/pose/issues/102)), so the planted foot does not slide across the blend.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
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/legs.ts
CHANGED
|
@@ -253,6 +253,17 @@ export function rootOf(rest: Rest, b: string): string {
|
|
|
253
253
|
|
|
254
254
|
// --- leg scale -------------------------------------------------------------
|
|
255
255
|
|
|
256
|
+
export interface SupportScaleRun {
|
|
257
|
+
side: Side;
|
|
258
|
+
frames: [number, number];
|
|
259
|
+
/** The lowest and highest scale asked on the run's frames. */
|
|
260
|
+
asked: { min: number; max: number };
|
|
261
|
+
/** The lowest and highest scale keyed on the run's frames (clamped at `bound` when `asked` went under #95's bound). */
|
|
262
|
+
used: { min: number; max: number };
|
|
263
|
+
/** Present when `asked` went under #95's bound on any frame of the run: the lowest bound on the run, and the largest hip rise (world units, ≥ 0). */
|
|
264
|
+
clamped?: { bound: number; hipRise: number };
|
|
265
|
+
}
|
|
266
|
+
|
|
256
267
|
export interface LegScale {
|
|
257
268
|
chain: string;
|
|
258
269
|
bone: string;
|
|
@@ -270,6 +281,13 @@ export interface LegScale {
|
|
|
270
281
|
scale: number[];
|
|
271
282
|
/** The scale the pose asked for on each frame before the clamp floor (`min(ratio, limits[1])`, rig-c/pose#107). */
|
|
272
283
|
asked: number[];
|
|
284
|
+
/**
|
|
285
|
+
* rig-c/pose#133: under `--plant-feet`, runs of supporting frames on this
|
|
286
|
+
* segment where the asked scale went under `limits[0]` and followed `asked`
|
|
287
|
+
* down to the bound #95 derives from the leg's own range (`min(limits[0], r(i))`).
|
|
288
|
+
* Absent when no supporting frame on this segment asked under `limits[0]`.
|
|
289
|
+
*/
|
|
290
|
+
support?: SupportScaleRun[];
|
|
273
291
|
}
|
|
274
292
|
|
|
275
293
|
/** One segment's scale per frame: picture length ÷ its 95th percentile, clamped; its asked scale without the clamp floor (#107); and the runs clamped. */
|
|
@@ -800,6 +818,96 @@ export function stanceRangeOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Si
|
|
|
800
818
|
return out;
|
|
801
819
|
}
|
|
802
820
|
|
|
821
|
+
/**
|
|
822
|
+
* Under `--plant-feet` and `--leg-scale`, a supporting leg segment that asks a
|
|
823
|
+
* scale under `--leg-scale`'s floor (`limits[0]`) follows its asked scale down
|
|
824
|
+
* to the bound #95 derives from the leg's own range (`min(limits[0], r(i))`) so
|
|
825
|
+
* the supporting ankle stays on #85's floor after #107 (rig-c/pose#133). The
|
|
826
|
+
* free leg keeps the configured clamp. When an asked scale goes under #95's
|
|
827
|
+
* bound (`lower`, defaulting to the segment's own range `asked[i]`), it is
|
|
828
|
+
* clamped there and the hip rises by the vertical remainder (`askedOffset −
|
|
829
|
+
* usedOffset`), never sinks.
|
|
830
|
+
*/
|
|
831
|
+
export function scaleSupport(
|
|
832
|
+
legScale: LegScale[],
|
|
833
|
+
scales: Map<string, number[]>,
|
|
834
|
+
feet: Feet,
|
|
835
|
+
legs: Record<Side, { chain: Chain; ankleBone: string }>,
|
|
836
|
+
hip: Pt[],
|
|
837
|
+
askedOffset: (i: number, s: Side) => Pt,
|
|
838
|
+
usedOffset: (i: number, s: Side) => Pt,
|
|
839
|
+
lower?: StanceLower,
|
|
840
|
+
): void {
|
|
841
|
+
const n = feet.support.length;
|
|
842
|
+
const tracked = new Array<Side | null>(n).fill(null);
|
|
843
|
+
for (const t of feet.tracked ?? []) for (let i = t.frames[0]; i <= t.frames[1]; i++) tracked[i] = t.side;
|
|
844
|
+
const byBone = new Map(legScale.map((ls) => [ls.bone, ls]));
|
|
845
|
+
const copied = new Set<string>();
|
|
846
|
+
const ensureCopy = (b: string): number[] => {
|
|
847
|
+
const cur = scales.get(b)!;
|
|
848
|
+
if (copied.has(b)) return cur;
|
|
849
|
+
const copy = [...cur];
|
|
850
|
+
scales.set(b, copy);
|
|
851
|
+
copied.add(b);
|
|
852
|
+
return copy;
|
|
853
|
+
};
|
|
854
|
+
const perBone = new Map<string, Array<{ frame: number; side: Side; asked: number; used: number; bound: number; clamped: boolean; hipRise: number }>>();
|
|
855
|
+
for (let i = 0; i < n; i++) {
|
|
856
|
+
const s = feet.support[i] ?? tracked[i] ?? null;
|
|
857
|
+
if (s === null) continue;
|
|
858
|
+
const touched: Array<{ bone: string; asked: number; used: number; bound: number; clamped: boolean }> = [];
|
|
859
|
+
for (const b of legs[s].chain.bones) {
|
|
860
|
+
const ls = byBone.get(b);
|
|
861
|
+
if (ls === undefined) continue;
|
|
862
|
+
const asked = ls.asked[i]!;
|
|
863
|
+
const floor = ls.limits[0];
|
|
864
|
+
if (!(asked < floor)) continue;
|
|
865
|
+
const r = lower !== undefined ? lower[s][i]! : asked;
|
|
866
|
+
const bound = Math.min(floor, r);
|
|
867
|
+
const used = Math.max(asked, bound);
|
|
868
|
+
const clamped = used > asked + 1e-12;
|
|
869
|
+
ensureCopy(b)[i] = used;
|
|
870
|
+
touched.push({ bone: b, asked, used, bound, clamped });
|
|
871
|
+
}
|
|
872
|
+
if (touched.length === 0) continue;
|
|
873
|
+
let hipRise = 0;
|
|
874
|
+
if (touched.some((t) => t.clamped)) {
|
|
875
|
+
hipRise = Math.max(0, askedOffset(i, s)[1] - usedOffset(i, s)[1]);
|
|
876
|
+
hip[i]![1] += hipRise;
|
|
877
|
+
}
|
|
878
|
+
for (const t of touched) {
|
|
879
|
+
const list = perBone.get(t.bone) ?? [];
|
|
880
|
+
list.push({ frame: i, side: s, ...t, hipRise });
|
|
881
|
+
perBone.set(t.bone, list);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
for (const ls of legScale) {
|
|
885
|
+
const entries = perBone.get(ls.bone);
|
|
886
|
+
if (entries === undefined || entries.length === 0) continue;
|
|
887
|
+
const out: SupportScaleRun[] = [];
|
|
888
|
+
for (const e of entries) {
|
|
889
|
+
const last = out[out.length - 1];
|
|
890
|
+
if (last !== undefined && last.side === e.side && last.frames[1] === e.frame - 1 && (last.clamped !== undefined) === e.clamped) {
|
|
891
|
+
last.frames[1] = e.frame;
|
|
892
|
+
last.asked = { min: Math.min(last.asked.min, e.asked), max: Math.max(last.asked.max, e.asked) };
|
|
893
|
+
last.used = { min: Math.min(last.used.min, e.used), max: Math.max(last.used.max, e.used) };
|
|
894
|
+
if (last.clamped !== undefined) {
|
|
895
|
+
last.clamped = { bound: Math.min(last.clamped.bound, e.bound), hipRise: Math.max(last.clamped.hipRise, e.hipRise) };
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
out.push({
|
|
899
|
+
side: e.side,
|
|
900
|
+
frames: [e.frame, e.frame],
|
|
901
|
+
asked: { min: e.asked, max: e.asked },
|
|
902
|
+
used: { min: e.used, max: e.used },
|
|
903
|
+
...(e.clamped ? { clamped: { bound: e.bound, hipRise: e.hipRise } } : {}),
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
ls.support = out;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
803
911
|
/** The lower bound on frame i for side s: the leg's own (#95), or the default. */
|
|
804
912
|
function lowerOf(lower: StanceLower | undefined, s: Side, i: number): number {
|
|
805
913
|
return lower?.[s][i] ?? STANCE_BOUNDS[0];
|
|
@@ -906,7 +1014,61 @@ export interface StancePosed {
|
|
|
906
1014
|
* free side and with no frame between, takes their state, and is listed with
|
|
907
1015
|
* `absorbed`.
|
|
908
1016
|
*/
|
|
909
|
-
|
|
1017
|
+
/**
|
|
1018
|
+
* Under `--stance` and `--leg-scale` (rig-c/pose#137), a free foot read `both`
|
|
1019
|
+
* (planted beside the supporting one within `tolerance`) whose posed ankle sits
|
|
1020
|
+
* above the floor (`ankle[free][1] > ankle[support][1]`) while neither the
|
|
1021
|
+
* whole leg (`lo >= STANCE_BOUNDS[0]`) nor its thigh (`s0 >= STANCE_BOUNDS[0]`)
|
|
1022
|
+
* is folded below `STANCE_BOUNDS[0]`, yet its shin was shortened below it
|
|
1023
|
+
* (`s1 < STANCE_BOUNDS[0]`), owes that shin shortening to a low knee / high
|
|
1024
|
+
* ankle split along a straight stance leg in the picture. Keeping the shortened
|
|
1025
|
+
* shin scale makes `--stance` multiply both thigh and shin by `needed`,
|
|
1026
|
+
* over-stretching the thigh (pushing the knee down), leaving the shin and foot
|
|
1027
|
+
* squashed, and exceeding `STANCE_BOUNDS[1]` so the free ankle floats off #85's
|
|
1028
|
+
* floor. Lifting the shin's scale to `min(1, s0)` before `stanceOf` reads
|
|
1029
|
+
* `needed` restores the straight leg's shin proportion first so the chain is
|
|
1030
|
+
* stretched only by the remaining gap.
|
|
1031
|
+
*/
|
|
1032
|
+
export function unshortenStanceShin(
|
|
1033
|
+
scales: Map<string, number[]>,
|
|
1034
|
+
legs: Record<Side, { chain: Chain; ankleBone: string }>,
|
|
1035
|
+
posed: (i: number) => StancePosed,
|
|
1036
|
+
lower?: StanceLower,
|
|
1037
|
+
): (i: number, free?: Side) => StancePosed {
|
|
1038
|
+
const copied = new Set<string>();
|
|
1039
|
+
const ensureCopy = (b: string): number[] => {
|
|
1040
|
+
const cur = scales.get(b)!;
|
|
1041
|
+
if (copied.has(b)) return cur;
|
|
1042
|
+
const copy = [...cur];
|
|
1043
|
+
scales.set(b, copy);
|
|
1044
|
+
copied.add(b);
|
|
1045
|
+
return copy;
|
|
1046
|
+
};
|
|
1047
|
+
return (i: number, free?: Side): StancePosed => {
|
|
1048
|
+
const p = posed(i);
|
|
1049
|
+
if (free === undefined) return p;
|
|
1050
|
+
if (lowerOf(lower, free, i) < STANCE_BOUNDS[0]) return p;
|
|
1051
|
+
const support: Side = free === 'r' ? 'l' : 'r';
|
|
1052
|
+
const floor = p.ankle[support][1];
|
|
1053
|
+
const root = p.root[free][1];
|
|
1054
|
+
const a = p.ankle[free][1];
|
|
1055
|
+
if (!(root - a > 1e-9 && a > floor + 1e-9)) return p;
|
|
1056
|
+
const bones = legs[free].chain.bones;
|
|
1057
|
+
if (bones.length < 2) return p;
|
|
1058
|
+
const b0 = bones[0]!;
|
|
1059
|
+
const b1 = bones[bones.length - 1]!;
|
|
1060
|
+
const arr0 = scales.get(b0);
|
|
1061
|
+
const arr1 = scales.get(b1);
|
|
1062
|
+
if (arr0 === undefined || arr1 === undefined) return p;
|
|
1063
|
+
const s0 = arr0[i]!;
|
|
1064
|
+
const s1 = arr1[i]!;
|
|
1065
|
+
if (!(s0 >= STANCE_BOUNDS[0] && s1 < STANCE_BOUNDS[0])) return p;
|
|
1066
|
+
ensureCopy(b1)[i] = Math.min(1, s0);
|
|
1067
|
+
return posed(i);
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, tolerance: number, posed: (i: number, free?: Side) => StancePosed, secondsOf: (run: [number, number]) => number, lower?: StanceLower): Stance {
|
|
910
1072
|
const n = feet.support.length;
|
|
911
1073
|
const stretch: Stance['stretch'] = new Array(n).fill(null);
|
|
912
1074
|
const tracked = new Array<Side | null>(n).fill(null);
|
|
@@ -977,7 +1139,7 @@ export function stanceOf(feet: Feet, ankle: Record<Side, readonly Pt[]>, toleran
|
|
|
977
1139
|
rows[i] = { state: r.state, side: f, height, k: null, needed: null, over: false, left: 0, lo };
|
|
978
1140
|
continue;
|
|
979
1141
|
}
|
|
980
|
-
const p = posed(i);
|
|
1142
|
+
const p = posed(i, f);
|
|
981
1143
|
const floor = p.ankle[s][1];
|
|
982
1144
|
const root = p.root[f][1];
|
|
983
1145
|
const a = p.ankle[f][1];
|
package/src/map.ts
CHANGED
|
@@ -111,6 +111,8 @@ export interface JointMap {
|
|
|
111
111
|
export interface RigSlot {
|
|
112
112
|
setup: string | null;
|
|
113
113
|
attachments: ReadonlySet<string>;
|
|
114
|
+
/** The bone the slot is attached to, when read from a compiled build. */
|
|
115
|
+
bone?: string;
|
|
114
116
|
}
|
|
115
117
|
export type RigSlots = ReadonlyMap<string, RigSlot>;
|
|
116
118
|
|
package/src/retarget.ts
CHANGED
|
@@ -82,7 +82,7 @@ import { Problems, Refusal } from './problems.ts';
|
|
|
82
82
|
import type { ArtRange, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
83
|
import { clampArt, coveredChains, isRaised, measureArt, overflow, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
84
84
|
import type { BlendHold, Closure, Feet, Floor, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
85
|
-
import { holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, SIDES, stanceOf, stanceRangeOf, supportsOf, tipOf } from './legs.ts';
|
|
85
|
+
import { holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, scaleSupport, SIDES, stanceOf, stanceRangeOf, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
|
|
86
86
|
|
|
87
87
|
const DEG = 180 / Math.PI;
|
|
88
88
|
|
|
@@ -739,8 +739,13 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
739
739
|
const hip = planted.hip;
|
|
740
740
|
floor = planted.floor;
|
|
741
741
|
closure = planted.closure;
|
|
742
|
-
for (let i = 0; i < n; i++) travel[i] = [hip[i]![0] - tb.worldX, hip[i]![1] - tb.worldY];
|
|
743
742
|
const L = (legs as { legs: Record<Side, { chain: Chain; ankleBone: string }> }).legs;
|
|
743
|
+
// #133: on a frame where the supporting leg asks under --leg-scale's floor, its scalex follows the asked
|
|
744
|
+
// value down to #95's bound so the ankle stays on #85's floor; the free leg keeps the configured clamp.
|
|
745
|
+
if (legScaleOn && feet !== null) {
|
|
746
|
+
scaleSupport(legScale, scales, feet, L, hip, (i, s) => offsets[i]![s], (i, s) => tipOf(rest, poseLegs(i), L[s].ankleBone));
|
|
747
|
+
}
|
|
748
|
+
for (let i = 0; i < n; i++) travel[i] = [hip[i]![0] - tb.worldX, hip[i]![1] - tb.worldY];
|
|
744
749
|
const world = (i: number, v: Pt): Pt => [hip[i]![0] + v[0], hip[i]![1] + v[1]];
|
|
745
750
|
/** Both legs posed on frame i by the scales as they stand, world (y up): each chain's root and ankle. */
|
|
746
751
|
const posedFeet = (i: number) => {
|
|
@@ -774,7 +779,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
774
779
|
// --stance (#60): with the hip placed, a free ankle within the tolerance of the floor has its leg
|
|
775
780
|
// stretched (every bone of its chain's scalex, about the chain's root) until it reaches it.
|
|
776
781
|
if (opts.stance !== undefined && opts.stance !== 'off') {
|
|
777
|
-
stance = stanceOf(feet!, ankleSeries, opts.stance, posedFeet, secondsOf, lower);
|
|
782
|
+
stance = stanceOf(feet!, ankleSeries, opts.stance, legScaleOn ? unshortenStanceShin(scales, L, posedFeet, lower) : posedFeet, secondsOf, lower);
|
|
778
783
|
// The supporting leg (the blend's) and the free leg (the stance's) are different chains on a frame.
|
|
779
784
|
for (let i = 0; i < n; i++) {
|
|
780
785
|
const b = blendHold.stretch[i];
|
|
@@ -892,7 +897,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
892
897
|
return null;
|
|
893
898
|
};
|
|
894
899
|
const turn = (bone: string, i: number) => targets.get(bone)![i]! - restOf.get(bone)!;
|
|
895
|
-
const covered = coveredChains(sw.switches, n);
|
|
900
|
+
const covered = coveredChains(sw.switches, n, sw.chainHolds);
|
|
896
901
|
if (opts.short !== 'off') {
|
|
897
902
|
for (const [b, info] of fractions) {
|
|
898
903
|
for (const s of capShortSteps(targets.get(b)!, info.fraction, covered.get(info.chain))) {
|
|
@@ -1183,7 +1188,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1183
1188
|
const own = measured.filter((e) => e.set === s.set && e.frames[0] >= s.frames[0] && e.frames[1] <= s.frames[1]);
|
|
1184
1189
|
artRange.push(...own.filter((e) => !solvedBy(e, s)), ...(raisedArt.find((x) => x.sw === s)?.entries ?? []));
|
|
1185
1190
|
}
|
|
1186
|
-
clampArt(sw.switches, map, targets);
|
|
1191
|
+
clampArt(sw.switches, map, targets, sw.chainHolds);
|
|
1187
1192
|
// Where each raised wrist lands as keyed: after the solve and the set's `turn`.
|
|
1188
1193
|
for (const job of raisedJobs) {
|
|
1189
1194
|
const after: Pt[] = [];
|
package/src/rigc.ts
CHANGED
|
@@ -143,7 +143,7 @@ export function slotsOf(doc: CompiledDocument): RigSlots {
|
|
|
143
143
|
for (const s of doc.slots) {
|
|
144
144
|
const attachments = new Set<string>();
|
|
145
145
|
for (const k of doc.skins) for (const name of Object.keys(k.attachments[s.name] ?? {})) attachments.add(name);
|
|
146
|
-
out.set(s.name, { setup: s.setup, attachments });
|
|
146
|
+
out.set(s.name, { setup: s.setup, attachments, bone: s.bone });
|
|
147
147
|
}
|
|
148
148
|
return out;
|
|
149
149
|
}
|
package/src/run.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { artRangeLine, chainsMoving, raisedLine, RAISED_WITHIN, reachLine, switc
|
|
|
18
18
|
import { landed, measureFeet, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
19
19
|
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
20
20
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
21
|
-
import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance } from './legs.ts';
|
|
21
|
+
import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
|
|
22
22
|
|
|
23
23
|
export interface RetargetArgs extends RetargetOptions {
|
|
24
24
|
rig: string;
|
|
@@ -138,6 +138,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
138
138
|
for (const e of r.byHip) if (e.short.length > 0) say(` REACH ${reachLine(e, r.times)}`);
|
|
139
139
|
for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
|
|
140
140
|
for (const l of r.legScale) say(` LEG-SCALE ${legScaleLine(l, r.times)}`);
|
|
141
|
+
for (const l of r.legScale) for (const s of l.support ?? []) say(` LEG-SCALE ${supportScaleLine(l, s, r.times)}`);
|
|
141
142
|
if (r.pivot !== null) say(` PIVOT bone "${r.pivot.bone}"'s origin is ${r.pivot.distance.toFixed(2)} units (${r.pivot.hipWidths.toFixed(3)} hip widths) from its thigh roots' midpoint (${r.pivot.roots.join(' + ')}): dx ${r.pivot.offset[0].toFixed(2)}, dy ${r.pivot.offset[1].toFixed(2)}, at setup — the pivot every hip turn swings the legs about`);
|
|
142
143
|
if (r.feet !== null) {
|
|
143
144
|
const f = r.feet;
|
|
@@ -673,6 +674,16 @@ function legScaleLine(l: LegScale, times: readonly number[]): string {
|
|
|
673
674
|
return `chain "${l.chain}" bone "${l.bone}" (${l.joints[0]} → ${l.joints[1]}): straight length ${l.straight.toFixed(1)}px (95th percentile of ${times.length} frames); picture length ÷ straight ${l.ratio.min.toFixed(3)} … ${l.ratio.max.toFixed(3)} (median ${l.ratio.median.toFixed(3)}); scalex keyed in [${l.limits[0]}, ${l.limits[1]}]: ${clamp(l.low, l.limits[0], 'under')}; ${clamp(l.high, l.limits[1], 'over')}`;
|
|
674
675
|
}
|
|
675
676
|
|
|
677
|
+
/** A LEG-SCALE past the floor (support) line (rig-c/pose#133): the supporting leg segment scaled below `--leg-scale`'s floor down to #95's bound. */
|
|
678
|
+
export function supportScaleLine(l: LegScale, s: SupportScaleRun, times: readonly number[]): string {
|
|
679
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `frame ${x[0]} (t=${times[x[0]]!.toFixed(3)}s)` : `frames ${x[0]}–${x[1]} (t=${times[x[0]]!.toFixed(3)}–${times[x[1]]!.toFixed(3)}s)`);
|
|
680
|
+
const val = (r: { min: number; max: number }) => (r.min === r.max ? r.min.toFixed(4) : `${r.min.toFixed(4)}–${r.max.toFixed(4)}`);
|
|
681
|
+
if (s.clamped !== undefined) {
|
|
682
|
+
return `past the floor (support): chain "${l.chain}" bone "${l.bone}" (supporting foot ${s.side}) ${span(s.frames)} — asked ${val(s.asked)} under ${l.limits[0]}, clamped at the leg's own range ${s.clamped.bound.toFixed(4)} (#95), used ${val(s.used)}; hip rose by up to +${s.clamped.hipRise.toFixed(2)} units (never sinks)`;
|
|
683
|
+
}
|
|
684
|
+
return `past the floor (support): chain "${l.chain}" bone "${l.bone}" (supporting foot ${s.side}) ${span(s.frames)} — asked ${val(s.asked)} under ${l.limits[0]}, used ${val(s.used)} (within the leg's own range, #95; ankle stays on the floor)`;
|
|
685
|
+
}
|
|
686
|
+
|
|
676
687
|
/**
|
|
677
688
|
* POSE_NOT_CLEANED (#67): retarget reads only a sequence `rig-pose clean` wrote.
|
|
678
689
|
* The message is the command to run on this file; the output name is a
|
package/src/switch.ts
CHANGED
|
@@ -154,19 +154,35 @@ export interface Switches {
|
|
|
154
154
|
shown: Map<string, Array<string | null>>;
|
|
155
155
|
/** Per set, its distance on each frame, degrees. */
|
|
156
156
|
distances: Map<string, number[]>;
|
|
157
|
+
/** Per chain, the behind-the-torso set spans (including same-set gap holds, #136) for `clampArt`. */
|
|
158
|
+
chainHolds?: Array<{ set: string; chain: string; frames: [number, number] }>;
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
/** Per set, its distance to the pose on every frame: the largest segment difference over
|
|
160
|
-
export function
|
|
161
|
-
const out = new Map<string, number[]
|
|
161
|
+
/** Per set and chain, its distance to the pose on every frame: the largest segment difference over that chain. */
|
|
162
|
+
export function chainDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, Map<string, number[]>> {
|
|
163
|
+
const out = new Map<string, Map<string, number[]>>();
|
|
162
164
|
for (const a of map.alternates) {
|
|
163
|
-
const
|
|
165
|
+
const byChain = new Map<string, number[]>();
|
|
164
166
|
for (const cn of a.chains) {
|
|
165
167
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
166
|
-
c.bones.
|
|
168
|
+
const segs = c.bones.map((b, k) => ({ asked: targets.get(b)!, key: pictureAngle(a.pose[c.joints[k]!]!, a.pose[c.joints[k + 1]!]!) }));
|
|
169
|
+
const d: number[] = [];
|
|
170
|
+
for (let i = 0; i < n; i++) d.push(Math.max(...segs.map((s) => Math.abs(wrap(s.asked[i]! - s.key)))));
|
|
171
|
+
byChain.set(cn, d);
|
|
167
172
|
}
|
|
173
|
+
out.set(a.name, byChain);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Per set, its distance to the pose on every frame: the largest segment difference over its chains. */
|
|
179
|
+
export function setDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, number[]> {
|
|
180
|
+
const byChain = chainDistances(map, targets, n);
|
|
181
|
+
const out = new Map<string, number[]>();
|
|
182
|
+
for (const a of map.alternates) {
|
|
183
|
+
const cs = a.chains.map((cn) => byChain.get(a.name)!.get(cn)!);
|
|
168
184
|
const d: number[] = [];
|
|
169
|
-
for (let i = 0; i < n; i++) d.push(Math.max(...
|
|
185
|
+
for (let i = 0; i < n; i++) d.push(Math.max(...cs.map((c) => c[i]!)));
|
|
170
186
|
out.set(a.name, d);
|
|
171
187
|
}
|
|
172
188
|
return out;
|
|
@@ -210,6 +226,59 @@ function competing(sets: readonly AlternateSet[]): AlternateSet[][] {
|
|
|
210
226
|
return [...groups.values()];
|
|
211
227
|
}
|
|
212
228
|
|
|
229
|
+
/**
|
|
230
|
+
* How a competing group's slots sit against the torso in the rig's draw order
|
|
231
|
+
* (`slots` is in draw order, back to front, #136): which slot is the never-hidden
|
|
232
|
+
* torso slot (when the group touches or sits beside one), which sets draw their
|
|
233
|
+
* limb slots behind it vs in front of it, whether the base setup slots sit in
|
|
234
|
+
* front of it, and which chain owns each non-torso slot.
|
|
235
|
+
*/
|
|
236
|
+
function groupLayering(group: readonly AlternateSet[], map: JointMap, slots: RigSlots) {
|
|
237
|
+
const rank = new Map([...slots.keys()].map((s, i) => [s, i]));
|
|
238
|
+
const touched = [...new Set(group.flatMap((a) => [...Object.keys(a.slots), ...a.hides]))].filter((s) => rank.has(s));
|
|
239
|
+
const hiddenSetup = touched.filter((s) => slots.get(s)!.setup !== null && group.some((a) => a.hides.includes(s)));
|
|
240
|
+
const extra = touched.filter((s) => slots.get(s)!.setup === null);
|
|
241
|
+
const torsoSlot = touched.find((s) => slots.get(s)!.setup !== null && !group.some((a) => a.hides.includes(s))) ?? null;
|
|
242
|
+
let torsoRank: number | null = torsoSlot !== null ? rank.get(torsoSlot)! : null;
|
|
243
|
+
if (torsoRank === null && hiddenSetup.length > 0 && extra.length > 0) {
|
|
244
|
+
const minFront = Math.min(...hiddenSetup.map((s) => rank.get(s)!));
|
|
245
|
+
const maxBack = Math.max(...extra.filter((s) => rank.get(s)! < minFront).map((s) => rank.get(s)!));
|
|
246
|
+
if (Number.isFinite(maxBack)) {
|
|
247
|
+
const between = [...slots.keys()].find((s) => !touched.includes(s) && slots.get(s)!.setup !== null && rank.get(s)! > maxBack && rank.get(s)! < minFront);
|
|
248
|
+
torsoRank = between !== undefined ? rank.get(between)! : minFront - 0.5;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const limbSlotsOf = (a: AlternateSet) => Object.keys(a.slots).filter((s) => s !== torsoSlot && rank.has(s));
|
|
252
|
+
const isBehind = (a: AlternateSet): boolean => {
|
|
253
|
+
if (torsoRank === null) return false;
|
|
254
|
+
const ls = limbSlotsOf(a);
|
|
255
|
+
return ls.length > 0 && ls.every((s) => rank.get(s)! < torsoRank) && a.hides.some((h) => (rank.get(h) ?? -1) > torsoRank);
|
|
256
|
+
};
|
|
257
|
+
const isFront = (a: AlternateSet): boolean => {
|
|
258
|
+
if (torsoRank === null) return false;
|
|
259
|
+
const ls = limbSlotsOf(a);
|
|
260
|
+
return ls.length > 0 && ls.every((s) => rank.get(s)! > torsoRank);
|
|
261
|
+
};
|
|
262
|
+
const baseInFront = torsoRank !== null && hiddenSetup.length > 0 && hiddenSetup.every((s) => rank.get(s)! > torsoRank);
|
|
263
|
+
const groupChains = [...new Set(group.flatMap((a) => a.chains))];
|
|
264
|
+
const boneOwner = new Map<string, string>();
|
|
265
|
+
for (const cn of groupChains) for (const b of map.chains.find((c) => c.name === cn)!.bones) boneOwner.set(b, cn);
|
|
266
|
+
const slotChain = new Map<string, string>();
|
|
267
|
+
for (const s of touched) {
|
|
268
|
+
if (s === torsoSlot) continue;
|
|
269
|
+
const b = slots.get(s)?.bone;
|
|
270
|
+
if (b !== undefined && boneOwner.has(b)) {
|
|
271
|
+
slotChain.set(s, boneOwner.get(b)!);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const side = /(?:^|_)([rl])(?:_|$)/.exec(s)?.[1];
|
|
275
|
+
const bySide = side === undefined ? undefined : groupChains.find((c) => new RegExp(`(?:^|_)${side}(?:_|$)`).test(c));
|
|
276
|
+
if (bySide !== undefined) slotChain.set(s, bySide);
|
|
277
|
+
else if (groupChains.length === 1) slotChain.set(s, groupChains[0]!);
|
|
278
|
+
}
|
|
279
|
+
return { torsoSlot, torsoRank, touched, isBehind, isFront, baseInFront, slotChain };
|
|
280
|
+
}
|
|
281
|
+
|
|
213
282
|
/**
|
|
214
283
|
* Per frame, the largest |angular speed| over the given bones' target world
|
|
215
284
|
* angles, degrees per frame: a central difference, one-sided on the first and
|
|
@@ -305,9 +374,12 @@ const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 :
|
|
|
305
374
|
*/
|
|
306
375
|
export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, times: readonly number[], duration: number, slots: RigSlots, opts: SwitchOptions): Switches {
|
|
307
376
|
const n = times.length;
|
|
377
|
+
const byChain = chainDistances(map, targets, n);
|
|
308
378
|
const distances = setDistances(map, targets, n);
|
|
309
379
|
const switches: Switch[] = [];
|
|
310
|
-
|
|
380
|
+
const groups = competing(map.alternates);
|
|
381
|
+
for (const group of groups) {
|
|
382
|
+
const layer = groupLayering(group, map, slots);
|
|
311
383
|
const pick: Array<AlternateSet | null> = [];
|
|
312
384
|
for (let i = 0; i < n; i++) {
|
|
313
385
|
// A set with a `frames` window shows only inside it, and there it is chosen
|
|
@@ -328,22 +400,25 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
328
400
|
}
|
|
329
401
|
pick.push(best);
|
|
330
402
|
}
|
|
403
|
+
const bridgeSame = (arr: Array<AlternateSet | null>) => {
|
|
404
|
+
for (let i = 0; i < n;) {
|
|
405
|
+
const a = arr[i];
|
|
406
|
+
let j = i;
|
|
407
|
+
while (j + 1 < n && arr[j + 1] === a) j++;
|
|
408
|
+
const before = i > 0 ? arr[i - 1] : null;
|
|
409
|
+
const after = j + 1 < n ? arr[j + 1] : null;
|
|
410
|
+
if (before !== null && before !== undefined && before === after && a !== before) {
|
|
411
|
+
const gap = times[j + 1]! - times[i]!;
|
|
412
|
+
if (gap < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) arr[k] = before;
|
|
413
|
+
}
|
|
414
|
+
i = j + 1;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
331
417
|
// A gap shorter than the minimum span between two runs of one set is that set
|
|
332
418
|
// too: the base art flashing for a frame between them is the flicker the
|
|
333
419
|
// minimum span exists to prevent (measured on the dancer: one-frame breaks at
|
|
334
420
|
// a blurred frame inside a span of one set).
|
|
335
|
-
|
|
336
|
-
const a = pick[i];
|
|
337
|
-
let j = i;
|
|
338
|
-
while (j + 1 < n && pick[j + 1] === a) j++;
|
|
339
|
-
const before = i > 0 ? pick[i - 1] : null;
|
|
340
|
-
const after = j + 1 < n ? pick[j + 1] : null;
|
|
341
|
-
if (before !== null && before !== undefined && before === after && a !== before) {
|
|
342
|
-
const gap = times[j + 1]! - times[i]!;
|
|
343
|
-
if (gap < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = before;
|
|
344
|
-
}
|
|
345
|
-
i = j + 1;
|
|
346
|
-
}
|
|
421
|
+
bridgeSame(pick);
|
|
347
422
|
// A run shorter than the minimum span shows the base art.
|
|
348
423
|
for (let i = 0; i < n;) {
|
|
349
424
|
const a = pick[i];
|
|
@@ -353,6 +428,31 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
353
428
|
if (a !== null && a !== undefined && to - times[i]! < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = null;
|
|
354
429
|
i = j + 1;
|
|
355
430
|
}
|
|
431
|
+
// #136: a short front-set window (< 2 × switchMin) between two behind-set windows is dropped.
|
|
432
|
+
if (layer.torsoRank !== null) {
|
|
433
|
+
let dropped = false;
|
|
434
|
+
for (let i = 0; i < n;) {
|
|
435
|
+
const a = pick[i] ?? null;
|
|
436
|
+
let j = i;
|
|
437
|
+
while (j + 1 < n && (pick[j + 1] ?? null) === a) j++;
|
|
438
|
+
if (a !== null && layer.isFront(a)) {
|
|
439
|
+
let p = i - 1;
|
|
440
|
+
while (p >= 0 && (pick[p] ?? null) === null) p--;
|
|
441
|
+
let q = j + 1;
|
|
442
|
+
while (q < n && (pick[q] ?? null) === null) q++;
|
|
443
|
+
const prev = p >= 0 ? (pick[p] ?? null) : null;
|
|
444
|
+
const next = q < n ? (pick[q] ?? null) : null;
|
|
445
|
+
const to = j + 1 < n ? times[j + 1]! : duration;
|
|
446
|
+
if (prev !== null && next !== null && layer.isBehind(prev) && layer.isBehind(next) && to - times[i]! < 2 * opts.switchMin - 1e-9) {
|
|
447
|
+
const fill = prev === next && p === i - 1 && q === j + 1 && Array.from({ length: j - i + 1 }, (_, k) => distances.get(prev.name)![i + k]! <= opts.switchTolerance).every(Boolean) ? prev : null;
|
|
448
|
+
for (let k = i; k <= j; k++) pick[k] = fill;
|
|
449
|
+
dropped = true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
i = j + 1;
|
|
453
|
+
}
|
|
454
|
+
if (dropped) bridgeSame(pick);
|
|
455
|
+
}
|
|
356
456
|
// The segments between boundaries, then each boundary moved (#31).
|
|
357
457
|
const vals: Array<AlternateSet | null> = [pick[0] ?? null];
|
|
358
458
|
const pos: number[] = [];
|
|
@@ -398,6 +498,112 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
398
498
|
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = o;
|
|
399
499
|
}
|
|
400
500
|
}
|
|
501
|
+
// #136: when a group's base setup slots sit in front of the torso, an unswitched gap between two
|
|
502
|
+
// behind-the-torso switches keeps each chain on its behind-the-torso slots on the frames where that
|
|
503
|
+
// chain stays within tolerance of the surrounding behind sets (with the same minimum-span bridging),
|
|
504
|
+
// and across a joint behind-to-behind transition where all chains move from one behind set to another.
|
|
505
|
+
const chainHolds: Array<{ set: string; chain: string; frames: [number, number] }> = [];
|
|
506
|
+
for (const group of groups) {
|
|
507
|
+
const layer = groupLayering(group, map, slots);
|
|
508
|
+
if (!layer.baseInFront) continue;
|
|
509
|
+
const names = new Set(group.map((a) => a.name));
|
|
510
|
+
const gSwitches = switches.filter((s) => names.has(s.set));
|
|
511
|
+
const groupChains = [...new Set(group.flatMap((a) => a.chains))];
|
|
512
|
+
const cClamp = new Map<string, Array<AlternateSet | null>>();
|
|
513
|
+
for (const cn of groupChains) {
|
|
514
|
+
const arr: Array<AlternateSet | null> = Array.from({ length: n }, () => null);
|
|
515
|
+
for (const sw of gSwitches) {
|
|
516
|
+
if (!sw.chains.includes(cn)) continue;
|
|
517
|
+
const set = group.find((a) => a.name === sw.set)!;
|
|
518
|
+
if (layer.isBehind(set)) for (let i = sw.frames[0]; i <= sw.frames[1]; i++) arr[i] = set;
|
|
519
|
+
}
|
|
520
|
+
cClamp.set(cn, arr);
|
|
521
|
+
}
|
|
522
|
+
for (let m = 0; m + 1 < gSwitches.length; m++) {
|
|
523
|
+
const sw0 = gSwitches[m]!;
|
|
524
|
+
const sw1 = gSwitches[m + 1]!;
|
|
525
|
+
const set0 = group.find((a) => a.name === sw0.set)!;
|
|
526
|
+
const set1 = group.find((a) => a.name === sw1.set)!;
|
|
527
|
+
if (!layer.isBehind(set0) || !layer.isBehind(set1)) continue;
|
|
528
|
+
const g0 = sw0.frames[1] + 1;
|
|
529
|
+
const g1 = sw1.frames[0] - 1;
|
|
530
|
+
if (g0 > g1) continue;
|
|
531
|
+
const chains = [...new Set([...set0.chains, ...set1.chains])];
|
|
532
|
+
const cPick = new Map<string, Array<AlternateSet | null>>();
|
|
533
|
+
for (const cn of chains) {
|
|
534
|
+
const arr: Array<AlternateSet | null> = Array.from({ length: n }, () => null);
|
|
535
|
+
for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) arr[i] = set0;
|
|
536
|
+
for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) arr[i] = set1;
|
|
537
|
+
const d0s = byChain.get(set0.name)?.get(cn);
|
|
538
|
+
const d1s = byChain.get(set1.name)?.get(cn);
|
|
539
|
+
for (let i = g0; i <= g1; i++) {
|
|
540
|
+
const d0 = d0s?.[i] ?? Infinity;
|
|
541
|
+
const d1 = d1s?.[i] ?? Infinity;
|
|
542
|
+
if (Math.min(d0, d1) <= opts.switchTolerance) arr[i] = d0 <= d1 ? set0 : set1;
|
|
543
|
+
}
|
|
544
|
+
for (let i = g0; i <= g1;) {
|
|
545
|
+
const a = arr[i];
|
|
546
|
+
let j = i;
|
|
547
|
+
while (j + 1 <= g1 && arr[j + 1] === a) j++;
|
|
548
|
+
const before = arr[i - 1] ?? null;
|
|
549
|
+
const after = arr[j + 1] ?? null;
|
|
550
|
+
if (before !== null && before === after && a !== before) {
|
|
551
|
+
const gap = times[j + 1]! - times[i]!;
|
|
552
|
+
if (gap < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) arr[k] = before;
|
|
553
|
+
}
|
|
554
|
+
i = j + 1;
|
|
555
|
+
}
|
|
556
|
+
for (let i = g0; i <= g1;) {
|
|
557
|
+
const a = arr[i];
|
|
558
|
+
let j = i;
|
|
559
|
+
while (j + 1 <= g1 && arr[j + 1] === a) j++;
|
|
560
|
+
if (a !== null && i > g0 && j < g1 && times[j + 1]! - times[i]! < opts.switchMin - 1e-9) {
|
|
561
|
+
for (let k = i; k <= j; k++) arr[k] = null;
|
|
562
|
+
}
|
|
563
|
+
i = j + 1;
|
|
564
|
+
}
|
|
565
|
+
for (let i = g0; i <= g1; i++) {
|
|
566
|
+
if (arr[i] !== null) cClamp.get(cn)![i] = arr[i]!;
|
|
567
|
+
}
|
|
568
|
+
cPick.set(cn, arr);
|
|
569
|
+
}
|
|
570
|
+
const anyHeldBehind = chains.some((cn) => {
|
|
571
|
+
const arr = cPick.get(cn)!;
|
|
572
|
+
for (let i = g0; i <= g1; i++) if (arr[i] === null) return false;
|
|
573
|
+
return true;
|
|
574
|
+
});
|
|
575
|
+
if (!anyHeldBehind && set0 !== set1) {
|
|
576
|
+
const frontNear = group.filter((a) => layer.isFront(a)).some((a) => {
|
|
577
|
+
for (let i = g0; i <= g1; i++) if (distances.get(a.name)![i]! <= opts.switchTolerance) return true;
|
|
578
|
+
return false;
|
|
579
|
+
});
|
|
580
|
+
if (!frontNear) for (const cn of chains) for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = set0;
|
|
581
|
+
}
|
|
582
|
+
for (const s of layer.touched) {
|
|
583
|
+
if (!shown.has(s)) shown.set(s, Array.from({ length: n }, () => slots.get(s)!.setup));
|
|
584
|
+
}
|
|
585
|
+
for (let i = g0; i <= g1; i++) {
|
|
586
|
+
for (const [s, cn] of layer.slotChain) {
|
|
587
|
+
const a = cPick.get(cn)?.[i] ?? null;
|
|
588
|
+
if (a !== null) shown.get(s)![i] = a.slots[s] ?? null;
|
|
589
|
+
}
|
|
590
|
+
if (layer.torsoSlot !== null && shown.has(layer.torsoSlot)) {
|
|
591
|
+
const picked = chains.map((cn) => cPick.get(cn)![i]);
|
|
592
|
+
if (picked.every((a) => a !== null)) shown.get(layer.torsoSlot)![i] = picked[0]!.slots[layer.torsoSlot] ?? slots.get(layer.torsoSlot)!.setup;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
for (const cn of groupChains) {
|
|
597
|
+
const arr = cClamp.get(cn)!;
|
|
598
|
+
for (let i = 0; i < n;) {
|
|
599
|
+
const a = arr[i];
|
|
600
|
+
let j = i;
|
|
601
|
+
while (j + 1 < n && arr[j + 1] === a) j++;
|
|
602
|
+
if (a !== null) chainHolds.push({ set: a.name, chain: cn, frames: [i, j] });
|
|
603
|
+
i = j + 1;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
401
607
|
const slotTracks: SlotTrack[] = [];
|
|
402
608
|
for (const s of slotOrder) {
|
|
403
609
|
const row = shown.get(s);
|
|
@@ -425,11 +631,11 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
425
631
|
for (let k = 0; k + 1 < keys.length; k++) keys[k]!.ease = 'stepped';
|
|
426
632
|
offsetTracks.push({ bone: b, property: 'translate', keys });
|
|
427
633
|
}
|
|
428
|
-
return { switches, slotTracks, offsetTracks, shown, distances };
|
|
634
|
+
return { switches, slotTracks, offsetTracks, shown, distances, chainHolds };
|
|
429
635
|
}
|
|
430
636
|
|
|
431
637
|
/** Per chain, the frames on which a switch of a set depicting it shows: its art was painted for that pose. */
|
|
432
|
-
export function coveredChains(switches: readonly Switch[], n: number): Map<string, boolean[]> {
|
|
638
|
+
export function coveredChains(switches: readonly Switch[], n: number, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): Map<string, boolean[]> {
|
|
433
639
|
const out = new Map<string, boolean[]>();
|
|
434
640
|
for (const sw of switches) {
|
|
435
641
|
for (const c of sw.chains) {
|
|
@@ -438,6 +644,11 @@ export function coveredChains(switches: readonly Switch[], n: number): Map<strin
|
|
|
438
644
|
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = true;
|
|
439
645
|
}
|
|
440
646
|
}
|
|
647
|
+
for (const h of chainHolds ?? []) {
|
|
648
|
+
if (!out.has(h.chain)) out.set(h.chain, Array.from({ length: n }, () => false));
|
|
649
|
+
const row = out.get(h.chain)!;
|
|
650
|
+
for (let i = h.frames[0]; i <= h.frames[1]; i++) row[i] = true;
|
|
651
|
+
}
|
|
441
652
|
return out;
|
|
442
653
|
}
|
|
443
654
|
|
|
@@ -606,35 +817,50 @@ export const ART_EASE_STEP = 3;
|
|
|
606
817
|
* own world angle, and keys are differences of world angles, so a forearm
|
|
607
818
|
* under a clamped upper arm keeps its own target.
|
|
608
819
|
*/
|
|
609
|
-
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>): void {
|
|
820
|
+
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): void {
|
|
821
|
+
const boneChain = new Map<string, string>();
|
|
822
|
+
for (const c of map.chains) for (const b of c.bones) boneChain.set(b, c.name);
|
|
823
|
+
const clampSpan = (set: AlternateSet, bone: string, [a, b]: [number, number]) => {
|
|
824
|
+
const limits = set.turn[bone];
|
|
825
|
+
if (limits === undefined) return;
|
|
826
|
+
const drawn = set.drawn[bone]!;
|
|
827
|
+
const target = targets.get(bone)!;
|
|
828
|
+
const n = target.length;
|
|
829
|
+
const delta = (i: number): number => {
|
|
830
|
+
const t = wrap(target[i]! - drawn);
|
|
831
|
+
const c = Math.min(Math.max(t, limits[0]), limits[1]);
|
|
832
|
+
return Math.abs(c - t) > RANGE_SLACK ? c - t : 0;
|
|
833
|
+
};
|
|
834
|
+
const da = delta(a);
|
|
835
|
+
const db = delta(b);
|
|
836
|
+
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da)
|
|
837
|
+
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(da) / ART_EASE_STEP) - 1)
|
|
838
|
+
: 0;
|
|
839
|
+
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db)
|
|
840
|
+
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(db) / ART_EASE_STEP) - 1)
|
|
841
|
+
: 0;
|
|
842
|
+
for (let i = a; i <= b; i++) {
|
|
843
|
+
const d = delta(i);
|
|
844
|
+
if (d === 0) continue;
|
|
845
|
+
const wIn = i - a < easeIn ? (i - a + 1) / (easeIn + 1) : 1;
|
|
846
|
+
const wOut = b - i < easeOut ? (b - i + 1) / (easeOut + 1) : 1;
|
|
847
|
+
const w = Math.min(wIn, wOut);
|
|
848
|
+
target[i] = wrap(target[i]! + w * d);
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const heldChains = new Set((chainHolds ?? []).map((h) => `${h.set}:${h.chain}`));
|
|
610
852
|
for (const sw of switches) {
|
|
611
853
|
const set = map.alternates.find((a) => a.name === sw.set)!;
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const da = delta(a);
|
|
623
|
-
const db = delta(b);
|
|
624
|
-
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da)
|
|
625
|
-
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(da) / ART_EASE_STEP) - 1)
|
|
626
|
-
: 0;
|
|
627
|
-
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db)
|
|
628
|
-
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(db) / ART_EASE_STEP) - 1)
|
|
629
|
-
: 0;
|
|
630
|
-
for (let i = a; i <= b; i++) {
|
|
631
|
-
const d = delta(i);
|
|
632
|
-
if (d === 0) continue;
|
|
633
|
-
const wIn = i - a < easeIn ? (i - a + 1) / (easeIn + 1) : 1;
|
|
634
|
-
const wOut = b - i < easeOut ? (b - i + 1) / (easeOut + 1) : 1;
|
|
635
|
-
const w = Math.min(wIn, wOut);
|
|
636
|
-
target[i] = wrap(target[i]! + w * d);
|
|
637
|
-
}
|
|
854
|
+
for (const bone of Object.keys(set.turn)) {
|
|
855
|
+
const cn = boneChain.get(bone);
|
|
856
|
+
if (cn !== undefined && heldChains.has(`${sw.set}:${cn}`)) continue;
|
|
857
|
+
clampSpan(set, bone, sw.frames);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (const h of chainHolds ?? []) {
|
|
861
|
+
const set = map.alternates.find((a) => a.name === h.set)!;
|
|
862
|
+
for (const bone of Object.keys(set.turn)) {
|
|
863
|
+
if (boneChain.get(bone) === h.chain) clampSpan(set, bone, h.frames);
|
|
638
864
|
}
|
|
639
865
|
}
|
|
640
866
|
}
|