@rig-c/pose 0.40.0 → 0.42.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 +18 -8
- package/cli.ts +19 -6
- package/package.json +1 -1
- package/src/clean.ts +5 -4
- package/src/contacts.ts +250 -0
- package/src/depth.ts +53 -13
- package/src/draworder.ts +161 -23
- package/src/legs.ts +7 -0
- package/src/outdir.ts +5 -5
- package/src/pelvis.ts +205 -3
- package/src/pose.ts +1 -1
- package/src/reduce.ts +19 -1
- package/src/retarget.ts +17 -9
- package/src/run.ts +114 -14
- package/src/switch.ts +11 -4
package/src/depth.ts
CHANGED
|
@@ -13,10 +13,17 @@
|
|
|
13
13
|
* | segment | sense when foreshortened | why |
|
|
14
14
|
* | --------- | ------------------------ | --- |
|
|
15
15
|
* | thigh | forward | a hip flexes forward; it extends back too little to foreshorten past the band |
|
|
16
|
-
* | shin |
|
|
16
|
+
* | shin | back | the owner's walk statement (rig-c/pose#336): the lifted foot is behind the supporting leg — a knee folds the shin back |
|
|
17
17
|
* | upper arm | with the opposite leg | in a gait each arm swings with the opposite leg: forward when the other side's thigh is the more foreshortened of the two, back when its own is; forward (a shoulder flexes forward) when neither thigh is foreshortened |
|
|
18
18
|
* | forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
|
|
19
19
|
*
|
|
20
|
+
* The thigh's row is the owner's run statement (rig-c/pose#336): the lifted leg
|
|
21
|
+
* is in front, its knee protruding — a raised thigh brings the knee forward.
|
|
22
|
+
* The table is the same in a walk and a run, so `clean --depth walk|run` writes
|
|
23
|
+
* the same signs for both; the gait it records decides which joint
|
|
24
|
+
* `--draw-order depth` orders the legs by (`src/draworder.ts`): a walk by the
|
|
25
|
+
* ankle (the lifted foot behind), a run by the knee (the lifted knee in front).
|
|
26
|
+
*
|
|
20
27
|
* "Forward" is the trunk's facing: toward the camera (+1) when the figure faces
|
|
21
28
|
* it (its left shoulder and hip drawn right of its right ones), away (−1) when
|
|
22
29
|
* it faces away, and undecided (sign 0) when the trunk is side-on — shoulders
|
|
@@ -86,8 +93,8 @@ export interface DerivedDepth {
|
|
|
86
93
|
signs: { toward: number; away: number; flat: number };
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
/** Each frame's depth, derived from the picture's lengths
|
|
90
|
-
export function deriveDepth(track: readonly Joints[]
|
|
96
|
+
/** Each frame's depth, derived from the picture's lengths (the table above; the same for every gait). */
|
|
97
|
+
export function deriveDepth(track: readonly Joints[]): DerivedDepth {
|
|
91
98
|
const lengths = new Map(DEPTH_SEGMENT_NAMES.map((s) => [s, track.map((j) => drawn(j, s))]));
|
|
92
99
|
const rest = {} as DerivedDepth['rest'];
|
|
93
100
|
for (const k of ['upper_arm', 'forearm', 'thigh', 'shin'] as const) {
|
|
@@ -112,7 +119,7 @@ export function deriveDepth(track: readonly Joints[], gait: DepthGait): DerivedD
|
|
|
112
119
|
if (r === null) continue;
|
|
113
120
|
let sense: -1 | 1;
|
|
114
121
|
const kind = kindOf(s);
|
|
115
|
-
if (kind === 'shin') sense =
|
|
122
|
+
if (kind === 'shin') sense = -1;
|
|
116
123
|
else if (kind === 'upper_arm') {
|
|
117
124
|
// The arm swings with the opposite leg: its own thigh and the other side's, by how foreshortened each is.
|
|
118
125
|
const own = ratio(`${s.slice(0, 2)}thigh` as DepthSegment);
|
|
@@ -143,19 +150,52 @@ export function segmentDepth(d: SegmentDepth, length: number): number {
|
|
|
143
150
|
return d.sign * (length / d.ratio) * Math.sqrt(1 - r * r);
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
/**
|
|
154
|
+
* A limb's two joint depths on one frame, px (+ toward the camera): its middle joint (the upper segment's) and its end
|
|
155
|
+
* joint (the middle's plus the lower segment's), from the shoulder or hip at 0, unrounded. Null where the upper segment
|
|
156
|
+
* is not named and placed; `end` null where the lower one is not (the middle joint does not need it: rig-c/pose#336).
|
|
157
|
+
*/
|
|
158
|
+
export function limbJointDepths(joints: Joints, depth: FrameDepth, limb: Limb): { mid: number; end: number | null } | null {
|
|
159
|
+
const [upper, lower] = LIMBS[limb];
|
|
160
|
+
const du = depth[upper];
|
|
161
|
+
const dl = depth[lower];
|
|
162
|
+
const lu = drawn(joints, upper);
|
|
163
|
+
const ll = drawn(joints, lower);
|
|
164
|
+
if (du === undefined || lu === null) return null;
|
|
165
|
+
const mid = segmentDepth(du, lu);
|
|
166
|
+
return { mid, end: dl === undefined || ll === null ? null : mid + segmentDepth(dl, ll) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* rig-c/pose#336: the frames of a derived depth on which a shin leans the way the trunk faces — the sign table before
|
|
171
|
+
* #336, where a walk's foreshortened shin read forward. The table since leans it back (−facing) in both gaits, and a run's
|
|
172
|
+
* never changed, so only a record a release before #336 cleaned with `--depth walk` carries one. A record with no
|
|
173
|
+
* foreshortened shin reads the same under both tables and is not listed.
|
|
174
|
+
*/
|
|
175
|
+
export function forwardShinFrames(track: readonly Joints[], frames: ReadonlyArray<{ depth?: FrameDepth }>): number[] {
|
|
176
|
+
const out: number[] = [];
|
|
177
|
+
frames.forEach((f, i) => {
|
|
178
|
+
const face = facingOf(track[i] ?? {});
|
|
179
|
+
if (face === 0) return;
|
|
180
|
+
if ((['r_shin', 'l_shin'] as const).some((s) => f.depth?.[s]?.source === 'derived' && f.depth[s]!.sign === face)) out.push(i);
|
|
181
|
+
});
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The DEPTH_RECLEAN refusal (#336): the file, the gait it records, the frames (the first five), and what to run. */
|
|
186
|
+
export function depthRecleanText(file: string, gait: DepthGait | null, frames: readonly number[]): string {
|
|
187
|
+
const shown = frames.slice(0, 5).join(', ') + (frames.length > 5 ? `, … +${frames.length - 5} more` : '');
|
|
188
|
+
const g = gait ?? 'walk';
|
|
189
|
+
return `${file} carries derived depth (clean --depth ${gait ?? '?'}) whose foreshortened shin leans forward, the way the trunk faces, on ${frames.length} frame(s) (${shown}): the sign table before rig-c/pose#336, where a walk's lifted foot read forward. --draw-order depth reads a walk at the ankle and would draw that foot in front of the supporting leg. Clean it again with this version (rig-pose clean --depth ${g}), which leans a foreshortened shin back in both gaits; a sequence cleaned with --depth run by an earlier release is read as it is (its signs did not change), and measured depth is not checked`;
|
|
190
|
+
}
|
|
191
|
+
|
|
146
192
|
/** The limbs' front-to-back order on one frame, from its depth and its joints' drawn lengths. */
|
|
147
193
|
export function layersOf(joints: Joints, depth: FrameDepth): FrameLayers {
|
|
148
194
|
const z: Partial<Record<Limb, number>> = {};
|
|
149
195
|
for (const limb of LIMB_NAMES) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const lu = drawn(joints, upper);
|
|
154
|
-
const ll = drawn(joints, lower);
|
|
155
|
-
if (du === undefined || dl === undefined || lu === null || ll === null) continue;
|
|
156
|
-
const mid = segmentDepth(du, lu);
|
|
157
|
-
const end = mid + segmentDepth(dl, ll);
|
|
158
|
-
z[limb] = Math.round(((mid + end) / 2) * 100) / 100;
|
|
196
|
+
const j = limbJointDepths(joints, depth, limb);
|
|
197
|
+
if (j === null || j.end === null) continue;
|
|
198
|
+
z[limb] = Math.round(((j.mid + j.end) / 2) * 100) / 100;
|
|
159
199
|
}
|
|
160
200
|
const placed = LIMB_NAMES.filter((l) => z[l] !== undefined).sort((a, b) => z[b]! - z[a]! || LIMB_NAMES.indexOf(a) - LIMB_NAMES.indexOf(b));
|
|
161
201
|
const order: Limb[][] = [];
|
package/src/draworder.ts
CHANGED
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
* together they fill one unbroken run of the setup order. A slot of anything
|
|
20
20
|
* else drawn between them would be re-layered by the swap — that is changing
|
|
21
21
|
* the setup order, not keying the pair — so the pair is not keyed and the line
|
|
22
|
-
* names the
|
|
22
|
+
* counts the slots between them first, then names them as far as the line's
|
|
23
|
+
* cap allows (#324; the report's `why` and `between` carry every one).
|
|
24
|
+
* - **The gap (#324).** Per pair, keyed or not, the leg depth gap |depth a −
|
|
25
|
+
* depth b| over the frames both limbs read — its median and max, in px and as
|
|
26
|
+
* a fraction of the leg (upper + lower rest length of the limb the tie is read
|
|
27
|
+
* from) — against the tie, and the frames that passed it: the `DRAWGAP` line
|
|
28
|
+
* and the report's `drawOrder.gaps`.
|
|
23
29
|
* - **The rule.** On each frame the limb whose depth is the lower (farther from
|
|
24
30
|
* the camera) is drawn behind: its slots first in the pair's run, in their
|
|
25
31
|
* setup order, then the other's — when the two depths differ by at least
|
|
@@ -30,19 +36,38 @@
|
|
|
30
36
|
* ordered, holds the order before; before the first decided frame, the setup
|
|
31
37
|
* order — or, on a looped sequence, the order the clip ends in, held across
|
|
32
38
|
* the seam. A key is written where the order changes, and none while it is the
|
|
33
|
-
* setup order from the start.
|
|
39
|
+
* setup order from the start. Its time is half a frame before the frame it
|
|
40
|
+
* shows on (0 on frame 0), the rule the switches' attachment keys follow
|
|
41
|
+
* (`keyAtFrame` in `switch.ts`): a key exactly on a frame time is a tie the
|
|
42
|
+
* render breaks a frame late about half the time (rig-c/pose#325).
|
|
43
|
+
* - **By the joint the gait names** (rig-c/pose#336, the owner's two
|
|
44
|
+
* statements). On a sequence cleaned with `--depth walk` the pair is ordered
|
|
45
|
+
* by each leg's ankle depth (the hip at 0, plus the thigh's, plus the shin's:
|
|
46
|
+
* `limbJointDepths`' end) — in a walk the lifted foot is behind the supporting
|
|
47
|
+
* leg; with `--depth run` by the knee depth (the thigh's: its middle) — in a
|
|
48
|
+
* run the lifted leg is in front, its knee protruding. The limb mean cannot
|
|
49
|
+
* say either: a lifted leg's thigh forward and shin back cancel in it, and a
|
|
50
|
+
* walk's flat thigh leaves it at most half a shin (35 px on the fixtures),
|
|
51
|
+
* under the limb tie. The tie is scaled to the joint, what a flat chain can
|
|
52
|
+
* hold there: √(1 − 0.95²) × thigh at the knee, × (thigh + shin) at the
|
|
53
|
+
* ankle, the smaller leg's. Measured depth names no gait and keeps the limb
|
|
54
|
+
* mean and its tie (#304).
|
|
34
55
|
*
|
|
35
56
|
* Pure: no clock, no randomness, no disk.
|
|
36
57
|
*/
|
|
37
58
|
import type { Body18 } from './joints.ts';
|
|
38
|
-
import { FLAT_BAND, LIMBS, type FrameLayers, type Limb } from './depth.ts';
|
|
59
|
+
import { FLAT_BAND, LIMBS, limbJointDepths, type FrameLayers, type Limb } from './depth.ts';
|
|
39
60
|
import { knownSetsOf, type JointMap, type Rest, type RigSets } from './map.ts';
|
|
40
|
-
import { DEPTH_SEGMENTS, type DepthSegment, type FrameDepth, type Joint } from './pose.ts';
|
|
61
|
+
import { DEPTH_SEGMENTS, type DepthGait, type DepthSegment, type FrameDepth, type Joint } from './pose.ts';
|
|
62
|
+
import { keyAtFrame } from './switch.ts';
|
|
41
63
|
|
|
42
64
|
/** The limb pairs `--draw-order depth` keys. */
|
|
43
65
|
export const DRAW_ORDER_PAIRS: ReadonlyArray<readonly [Limb, Limb]> = [['r_leg', 'l_leg']];
|
|
44
66
|
/** What one segment inside the flat band can hide out of the picture, per unit of its rest length: √(1 − 0.95²). */
|
|
45
67
|
export const TIE_PER_LENGTH = Math.sqrt(1 - FLAT_BAND[0] * FLAT_BAND[0]);
|
|
68
|
+
/** The joint a gait's legs are ordered by (rig-c/pose#336): a walk's lifted foot is behind, a run's lifted knee in front. */
|
|
69
|
+
export const ORDER_JOINT: Record<DepthGait, LegJoint> = { walk: 'ankle', run: 'knee' };
|
|
70
|
+
export type LegJoint = 'knee' | 'ankle';
|
|
46
71
|
/** The DRAWORDER line's cap, characters (the span list is cut to it; the report carries every span). */
|
|
47
72
|
export const DRAW_ORDER_LINE_MAX = 260;
|
|
48
73
|
|
|
@@ -50,7 +75,7 @@ export interface DrawOrderOffset {
|
|
|
50
75
|
slot: string;
|
|
51
76
|
offset: number;
|
|
52
77
|
}
|
|
53
|
-
/** One key as written into the animation (`t` the frame
|
|
78
|
+
/** One key as written into the animation (`t` half a frame before the frame it shows on, 0 on frame 0: rig-c/pose#325). */
|
|
54
79
|
export interface DrawOrderKey {
|
|
55
80
|
t: number;
|
|
56
81
|
offsets?: DrawOrderOffset[];
|
|
@@ -65,8 +90,29 @@ export interface DrawOrderPair {
|
|
|
65
90
|
keyed: boolean;
|
|
66
91
|
/** #281's tie for the pair, px: depths closer than this hold the order before (null where a rest length is unread: no tie, equal depths only). */
|
|
67
92
|
tie: number | null;
|
|
93
|
+
/** #336: the joint the pair is ordered by, from the gait clean recorded (the tie then the joint's); absent, the limb mean (#304). */
|
|
94
|
+
by?: LegJoint;
|
|
68
95
|
/** Why the pair is not keyed. */
|
|
69
96
|
why?: string;
|
|
97
|
+
/** The slots of anything else drawn between the pair's slots, in setup order (#324: the line counts them first and cuts this list). */
|
|
98
|
+
between?: string[];
|
|
99
|
+
}
|
|
100
|
+
/** One pair's leg depth gap against its tie (#324): the figures that say why a pair keyed little or nothing. */
|
|
101
|
+
export interface DrawOrderGap {
|
|
102
|
+
a: string;
|
|
103
|
+
b: string;
|
|
104
|
+
/** |depth a − depth b| over the frames both limbs read, px: the median and the largest (at the pair's joint, when it has one). */
|
|
105
|
+
median: number | null;
|
|
106
|
+
max: number | null;
|
|
107
|
+
/** The same as a fraction of the leg (upper + lower rest length of the limb the tie is read from); null where a rest length is unread. */
|
|
108
|
+
medianLegs: number | null;
|
|
109
|
+
maxLegs: number | null;
|
|
110
|
+
/** The pair's tie, px and as a fraction of the leg. */
|
|
111
|
+
tie: number | null;
|
|
112
|
+
tieLegs: number | null;
|
|
113
|
+
/** Frames whose gap passed the tie (decided an order), of the frames both limbs read. */
|
|
114
|
+
passed: number;
|
|
115
|
+
frames: number;
|
|
70
116
|
}
|
|
71
117
|
export interface DrawOrderSpan {
|
|
72
118
|
/** The chain drawn behind, and the one in front. */
|
|
@@ -78,10 +124,12 @@ export interface DrawOrderSpan {
|
|
|
78
124
|
}
|
|
79
125
|
export interface DrawOrderReading {
|
|
80
126
|
pairs: DrawOrderPair[];
|
|
81
|
-
/** The `drawOrder` timeline, with the frame each key
|
|
127
|
+
/** The `drawOrder` timeline as written, with the frame each key first shows on. */
|
|
82
128
|
keys: Array<DrawOrderKey & { frame: number }>;
|
|
83
129
|
/** Every span of one decided order, setup or keyed, in frame order per pair. */
|
|
84
130
|
spans: DrawOrderSpan[];
|
|
131
|
+
/** Per pair, the leg depth gap against the tie (#324); keyed or not. */
|
|
132
|
+
gaps?: DrawOrderGap[];
|
|
85
133
|
}
|
|
86
134
|
|
|
87
135
|
interface Resolved {
|
|
@@ -164,10 +212,19 @@ function resolvePair(pairLimbs: readonly [Limb, Limb], map: JointMap, rest: Rest
|
|
|
164
212
|
const lo = idx[0]!;
|
|
165
213
|
const hi = idx[idx.length - 1]!;
|
|
166
214
|
const between = names.slice(lo, hi + 1).filter((n) => !slotsA.has(n) && !slotsB.has(n));
|
|
167
|
-
if (between.length > 0) return none(
|
|
215
|
+
if (between.length > 0) return { ...none(betweenWhy(between, between.length)), pair: { ...pair, why: betweenWhy(between, between.length), between } };
|
|
168
216
|
return { pair: { ...pair, keyed: true }, lo, hi, aBehind: arrangement(names, lo, a, b), bBehind: arrangement(names, lo, b, a) };
|
|
169
217
|
}
|
|
170
218
|
|
|
219
|
+
/** The not-keyed reason for slots drawn between the pair: the count first, then the first `shown` of the list (#324). */
|
|
220
|
+
function betweenWhy(between: readonly string[], shown: number): string {
|
|
221
|
+
const tail = 'the setup order is not changed here';
|
|
222
|
+
if (between.length === 1) return shown > 0 ? `1 slot: "${between[0]}" is drawn between them, and a swap would re-layer it (${tail})` : `1 slot is drawn between them (… +1 more), and a swap would re-layer it (${tail})`;
|
|
223
|
+
const more = between.length - shown;
|
|
224
|
+
const list = `${between.slice(0, shown).map((n) => `"${n}"`).join(', ')}${more > 0 ? `${shown > 0 ? ', ' : ''}… +${more} more` : ''}`;
|
|
225
|
+
return `${between.length} slots are drawn between them (${list}), and a swap would re-layer them (${tail})`;
|
|
226
|
+
}
|
|
227
|
+
|
|
171
228
|
type Joints = Partial<Record<Body18, Joint>>;
|
|
172
229
|
|
|
173
230
|
/** A segment's rest length as the depth states it (drawn ÷ ratio), the median over the frames that name it; null where none does. */
|
|
@@ -187,33 +244,84 @@ function restOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | unde
|
|
|
187
244
|
return vals.length % 2 === 1 ? vals[m]! : (vals[m - 1]! + vals[m]!) / 2;
|
|
188
245
|
}
|
|
189
246
|
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
|
|
247
|
+
/**
|
|
248
|
+
* #281's tie for two limbs, px: √(1 − 0.95²) × (upper + lower ÷ 2), the smaller limb's; null where a rest length is unread.
|
|
249
|
+
* By a joint (#336): √(1 − 0.95²) × upper at the knee, × (upper + lower) at the ankle — what a flat chain can hold there.
|
|
250
|
+
* `leg` is that limb's upper + lower rest length (#324's unit for the gap).
|
|
251
|
+
*/
|
|
252
|
+
function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, limbs: readonly [Limb, Limb], by: LegJoint | undefined): { tie: number | null; leg: number | null } {
|
|
253
|
+
const rests = limbs.map((l) => {
|
|
193
254
|
const [u, w] = LIMBS[l].map((s) => restOf(track, depth, s));
|
|
194
|
-
|
|
255
|
+
// The knee's tie needs the upper segment alone (#336: a leg whose ankle is never placed still orders at the knee).
|
|
256
|
+
if (by === 'knee') return u === null || u === undefined ? null : { hidden: u, leg: w === null || w === undefined ? null : u + w };
|
|
257
|
+
return u === null || u === undefined || w === null || w === undefined ? null : { hidden: by === 'ankle' ? u + w : u + w / 2, leg: u + w };
|
|
195
258
|
});
|
|
196
|
-
|
|
259
|
+
if (rests[0] == null || rests[1] == null) return { tie: null, leg: null };
|
|
260
|
+
const r = rests[1].hidden < rests[0].hidden ? rests[1] : rests[0];
|
|
261
|
+
return { tie: Math.round(TIE_PER_LENGTH * r.hidden * 100) / 100, leg: r.leg };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const round = (x: number, k: number): number => Math.round(x * 10 ** k) / 10 ** k;
|
|
265
|
+
|
|
266
|
+
/** One pair's leg depth gap against its tie over the frames (the rule's own test: unequal and at least the tie). */
|
|
267
|
+
function gapOf(pair: DrawOrderPair, zs: ReadonlyArray<Partial<Record<Limb, number>>>, leg: number | null): DrawOrderGap {
|
|
268
|
+
const tie = pair.tie ?? 0;
|
|
269
|
+
const gaps: number[] = [];
|
|
270
|
+
let passed = 0;
|
|
271
|
+
for (const z of zs) {
|
|
272
|
+
const za = z[pair.limbs[0]];
|
|
273
|
+
const zb = z[pair.limbs[1]];
|
|
274
|
+
if (za === undefined || zb === undefined) continue;
|
|
275
|
+
gaps.push(Math.abs(za - zb));
|
|
276
|
+
if (za !== zb && Math.abs(za - zb) >= tie) passed++;
|
|
277
|
+
}
|
|
278
|
+
gaps.sort((x, y) => x - y);
|
|
279
|
+
const m = gaps.length >> 1;
|
|
280
|
+
const median = gaps.length === 0 ? null : gaps.length % 2 === 1 ? gaps[m]! : (gaps[m - 1]! + gaps[m]!) / 2;
|
|
281
|
+
const max = gaps.length === 0 ? null : gaps[gaps.length - 1]!;
|
|
282
|
+
const legs = (x: number | null): number | null => (x === null || leg === null || !(leg > 0) ? null : round(x / leg, 3));
|
|
283
|
+
return { a: pair.a, b: pair.b, median: median === null ? null : round(median, 2), max: max === null ? null : round(max, 2), medianLegs: legs(median), maxLegs: legs(max), tie: pair.tie, tieLegs: legs(pair.tie), passed, frames: gaps.length };
|
|
197
284
|
}
|
|
198
285
|
|
|
199
286
|
/**
|
|
200
287
|
* The draw-order keys from the frames' layers. `track` and `depth` are the sequence's
|
|
201
288
|
* joints and depth per frame (the rest lengths the tie reads); `order` is the rig's
|
|
202
|
-
* setup draw order (the compiled model's slots); `times` the frames' times.
|
|
289
|
+
* setup draw order (the compiled model's slots); `times` the frames' times. `gait` is
|
|
290
|
+
* the gait clean recorded for its derived depth: a walk orders by the ankle, a run by
|
|
291
|
+
* the knee (#336); none (measured depth) by the limb mean (#304).
|
|
203
292
|
*/
|
|
204
|
-
export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, times: readonly number[], map: JointMap, rest: Rest, order: ReadonlyArray<{ name: string; bone: string }>, sets?: RigSets, loop = false): DrawOrderReading {
|
|
293
|
+
export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, times: readonly number[], map: JointMap, rest: Rest, order: ReadonlyArray<{ name: string; bone: string }>, sets?: RigSets, loop = false, gait: DepthGait | null = null): DrawOrderReading {
|
|
294
|
+
const by = gait === null ? undefined : ORDER_JOINT[gait];
|
|
205
295
|
const resolved = DRAW_ORDER_PAIRS.map((p) => resolvePair(p, map, rest, order, sets));
|
|
206
|
-
|
|
296
|
+
const legs = resolved.map((r) => {
|
|
297
|
+
const t = tieOf(track, depth, r.pair.limbs, by);
|
|
298
|
+
r.pair.tie = t.tie;
|
|
299
|
+
if (by !== undefined) r.pair.by = by;
|
|
300
|
+
return t.leg;
|
|
301
|
+
});
|
|
302
|
+
// #336: per frame, each limb's depth at the gait's joint (rounded as the layers are); without a gait, the layers' limb means.
|
|
303
|
+
const zs: ReadonlyArray<Partial<Record<Limb, number>>> =
|
|
304
|
+
by === undefined
|
|
305
|
+
? layers.map((l) => l.depth)
|
|
306
|
+
: track.map((j, i) => {
|
|
307
|
+
const z: Partial<Record<Limb, number>> = {};
|
|
308
|
+
for (const limb of new Set(DRAW_ORDER_PAIRS.flat())) {
|
|
309
|
+
const d = depth[i] === undefined ? null : limbJointDepths(j, depth[i]!, limb);
|
|
310
|
+
const v = d === null ? null : by === 'knee' ? d.mid : d.end;
|
|
311
|
+
if (v !== null) z[limb] = Math.round(v * 100) / 100;
|
|
312
|
+
}
|
|
313
|
+
return z;
|
|
314
|
+
});
|
|
207
315
|
const names = order.map((s) => s.name);
|
|
208
316
|
// Per pair, per frame: who is behind ('a' | 'b'), or null for the setup order before any decision.
|
|
209
317
|
const states = resolved.map((r) => {
|
|
210
318
|
const tie = r.pair.tie ?? 0;
|
|
211
319
|
const pass = (start: 'a' | 'b' | null): Array<'a' | 'b' | null> => {
|
|
212
320
|
let state = start;
|
|
213
|
-
return
|
|
321
|
+
return zs.map((z) => {
|
|
214
322
|
if (!r.pair.keyed) return null;
|
|
215
|
-
const za =
|
|
216
|
-
const zb =
|
|
323
|
+
const za = z[r.pair.limbs[0]];
|
|
324
|
+
const zb = z[r.pair.limbs[1]];
|
|
217
325
|
if (za !== undefined && zb !== undefined && za !== zb && Math.abs(za - zb) >= tie) state = za < zb ? 'a' : 'b';
|
|
218
326
|
return state;
|
|
219
327
|
});
|
|
@@ -231,12 +339,14 @@ export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readon
|
|
|
231
339
|
.sort((x, y) => names.indexOf(x.slot) - names.indexOf(y.slot));
|
|
232
340
|
const keys: DrawOrderReading['keys'] = [];
|
|
233
341
|
let last = '[]';
|
|
234
|
-
for (let f = 0; f <
|
|
342
|
+
for (let f = 0; f < zs.length; f++) {
|
|
235
343
|
const offs = offsetsAt(f);
|
|
236
344
|
const text = JSON.stringify(offs);
|
|
237
345
|
if (text === last) continue;
|
|
238
346
|
last = text;
|
|
239
|
-
|
|
347
|
+
// Half a frame before the frame it shows on, as the switches' keys (`keyAtFrame`, rig-c/pose#325).
|
|
348
|
+
const t = keyAtFrame(times, f);
|
|
349
|
+
keys.push(offs.length === 0 ? { frame: f, t } : { frame: f, t, offsets: offs });
|
|
240
350
|
}
|
|
241
351
|
const spans: DrawOrderSpan[] = [];
|
|
242
352
|
resolved.forEach((r, k) => {
|
|
@@ -247,7 +357,7 @@ export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readon
|
|
|
247
357
|
else spans.push({ behind: s === 'a' ? r.pair.a : r.pair.b, front: s === 'a' ? r.pair.b : r.pair.a, frames: [f, f], keyed: (s === 'a' ? r.aBehind : r.bBehind).length > 0 });
|
|
248
358
|
});
|
|
249
359
|
});
|
|
250
|
-
return { pairs: resolved.map((r) => r.pair), keys, spans };
|
|
360
|
+
return { pairs: resolved.map((r) => r.pair), keys, spans, gaps: resolved.map((r, k) => gapOf(r.pair, zs, legs[k]!)) };
|
|
251
361
|
}
|
|
252
362
|
|
|
253
363
|
/** The animation's `drawOrder` value: the keys without their frames. */
|
|
@@ -263,7 +373,9 @@ const span = (f: [number, number]): string => (f[0] === f[1] ? `${f[0]}` : `${f[
|
|
|
263
373
|
*/
|
|
264
374
|
export function drawOrderLines(d: DrawOrderReading, prefix = 'DRAWORDER '): string[] {
|
|
265
375
|
const keyed = d.spans.filter((s) => s.keyed);
|
|
266
|
-
|
|
376
|
+
// #336: a keyed pair ordered by a joint names it and its joint-scaled tie.
|
|
377
|
+
const joint = d.pairs.find((p) => p.by !== undefined && p.keyed);
|
|
378
|
+
const head = `${prefix}from depth${joint === undefined ? '' : ` by the ${joint.by} (${joint.by === 'ankle' ? 'walk' : 'run'}; tie ${joint.tie === null ? 'none' : `${joint.tie} px`})`}, ${d.keys.length} key(s) over ${keyed.length} span(s)`;
|
|
267
379
|
const groups = new Map<string, string[]>();
|
|
268
380
|
for (const s of keyed) {
|
|
269
381
|
const g = `${s.behind} behind ${s.front}`;
|
|
@@ -285,6 +397,32 @@ export function drawOrderLines(d: DrawOrderReading, prefix = 'DRAWORDER '): stri
|
|
|
285
397
|
while (n > 1 && text(n).length > DRAW_ORDER_LINE_MAX) n--;
|
|
286
398
|
const why = d.pairs.every((p) => !p.keyed) ? 'no pair keyed' : 'the setup order holds on every frame';
|
|
287
399
|
const lines = [keyed.length === 0 ? `${head} (${why})` : text(n)];
|
|
288
|
-
for (const p of d.pairs)
|
|
400
|
+
for (const p of d.pairs) {
|
|
401
|
+
if (p.keyed) continue;
|
|
402
|
+
const line = (why: string | undefined): string => `${prefix}${p.a} / ${p.b} not keyed: ${why}`;
|
|
403
|
+
// Slots between the pair: the count first, the list cut until the line fits (#324).
|
|
404
|
+
let shown = p.between?.length ?? 0;
|
|
405
|
+
while (p.between !== undefined && shown > 0 && line(betweenWhy(p.between, shown)).length > DRAW_ORDER_LINE_MAX) shown--;
|
|
406
|
+
lines.push(line(p.between === undefined ? p.why : betweenWhy(p.between, shown)).slice(0, DRAW_ORDER_LINE_MAX));
|
|
407
|
+
}
|
|
289
408
|
return lines;
|
|
290
409
|
}
|
|
410
|
+
|
|
411
|
+
/** px with its fraction of the leg: "12.3 px (0.084 leg)". */
|
|
412
|
+
const pxLeg = (px: number | null, legs: number | null): string => (px === null ? 'unread' : `${px} px${legs === null ? '' : ` (${legs} leg)`}`);
|
|
413
|
+
|
|
414
|
+
/** " at the knee" / " at the ankle" for a pair ordered by a joint (#336), else nothing. */
|
|
415
|
+
const gapAt = (d: DrawOrderReading, g: DrawOrderGap): string => {
|
|
416
|
+
const j = d.pairs.find((p) => p.a === g.a && p.b === g.b)?.by;
|
|
417
|
+
return j === undefined ? '' : ` at the ${j}`;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The DRAWGAP lines (rig-c/pose#324): one per pair, keyed or not — the leg depth gap's median and
|
|
422
|
+
* max against the tie, and how many frames passed it. Cut to `DRAW_ORDER_LINE_MAX` characters.
|
|
423
|
+
*/
|
|
424
|
+
export function drawGapLines(d: DrawOrderReading, prefix = 'DRAWGAP '): string[] {
|
|
425
|
+
return (d.gaps ?? []).map((g) =>
|
|
426
|
+
`${prefix}${g.a} / ${g.b}: leg depth gap${gapAt(d, g)} median ${pxLeg(g.median, g.medianLegs)}, max ${pxLeg(g.max, g.maxLegs)}, against the tie of ${g.tie === null ? 'none (a rest length is unread; equal depths only)' : pxLeg(g.tie, g.tieLegs)}; ${g.passed} of ${g.frames} frame(s) passed it`.slice(0, DRAW_ORDER_LINE_MAX),
|
|
427
|
+
);
|
|
428
|
+
}
|
package/src/legs.ts
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
* frames (`closeLoop`), so the loop carries no net travel.
|
|
59
59
|
*/
|
|
60
60
|
import type { Chain, JointMap, Rest } from './map.ts';
|
|
61
|
+
import type { ContactKeysReading } from './contacts.ts';
|
|
61
62
|
|
|
62
63
|
type Pt = [number, number];
|
|
63
64
|
/** A world 2×2: columns (a, c) — the x axis — and (b, d) — the y axis. */
|
|
@@ -522,6 +523,12 @@ export interface Feet {
|
|
|
522
523
|
* `supportsOf` on every reading.
|
|
523
524
|
*/
|
|
524
525
|
signal?: SupportSignal;
|
|
526
|
+
/**
|
|
527
|
+
* rig-c/pose#303: what `--contact-keys` did — the keys, the frames they cover,
|
|
528
|
+
* the support without them and the runs they changed. Set by `contactKeysOf`,
|
|
529
|
+
* applied last; absent without the flag.
|
|
530
|
+
*/
|
|
531
|
+
contactKeys?: ContactKeysReading;
|
|
525
532
|
}
|
|
526
533
|
|
|
527
534
|
/** A run of frames whose support the lower-ankle rule (rig-c/pose#113) reads differently from the reading before it. */
|
package/src/outdir.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type OutDirs = Map<string, string[]>;
|
|
|
14
14
|
|
|
15
15
|
const writable = (dir: string): boolean => {
|
|
16
16
|
try {
|
|
17
|
-
accessSync(dir, constants.W_OK);
|
|
17
|
+
accessSync(dir, constants.W_OK | constants.X_OK);
|
|
18
18
|
return true;
|
|
19
19
|
} catch {
|
|
20
20
|
return false;
|
|
@@ -22,9 +22,9 @@ const writable = (dir: string): boolean => {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Check where each named output lands: its directory is there
|
|
26
|
-
* is a directory this process can write into.
|
|
27
|
-
* is added to `dirs`. A directory a flag names itself (--build, --render) is checked by its parent.
|
|
25
|
+
* Check where each named output lands: its directory is there and this process can write into it
|
|
26
|
+
* (rig-c/pose#323), or the nearest existing ancestor is a directory this process can write into.
|
|
27
|
+
* A miss is added to `p`; a directory still to make is added to `dirs`. A directory a flag names itself (--build, --render) is checked by its parent.
|
|
28
28
|
*/
|
|
29
29
|
export function planOutDirs(outputs: ReadonlyArray<readonly [string, string | undefined]>, p: Problems, dirs: OutDirs = new Map()): OutDirs {
|
|
30
30
|
for (const [flag, path] of outputs) {
|
|
@@ -33,7 +33,7 @@ export function planOutDirs(outputs: ReadonlyArray<readonly [string, string | un
|
|
|
33
33
|
let up = dir;
|
|
34
34
|
while (!existsSync(up) && dirname(up) !== up) up = dirname(up);
|
|
35
35
|
if (!statSync(up).isDirectory()) p.add('ARG_PATH', up === dir ? `${flag} ${path} — ${dir} is not a directory` : `${flag} ${path} — ${up} is a file, so directory ${dir} cannot be made under it`);
|
|
36
|
-
else if (
|
|
36
|
+
else if (!writable(up)) p.add('ARG_PATH', up === dir ? `${flag} ${path} — ${dir} is not writable` : `${flag} ${path} — ${up} is not writable, so directory ${dir} cannot be made under it`);
|
|
37
37
|
else if (up !== dir) dirs.set(dir, [...(dirs.get(dir) ?? []), `${flag} ${path}`]);
|
|
38
38
|
}
|
|
39
39
|
return dirs;
|