@rig-c/pose 0.41.0 → 0.43.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/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 | walk: forward · run: back | the gait rule (rig-c/pose#281, HQ): a walk's lifted foot comes forward, a run's is kicked back — the knee alone cannot say, since it bends forward in both |
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 for `gait` (the table above). */
90
- export function deriveDepth(track: readonly Joints[], gait: DepthGait): DerivedDepth {
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 = gait === 'walk' ? 1 : -1;
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 [upper, lower] = LIMBS[limb];
151
- const du = depth[upper];
152
- const dl = depth[lower];
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
@@ -36,19 +36,38 @@
36
36
  * ordered, holds the order before; before the first decided frame, the setup
37
37
  * order — or, on a looped sequence, the order the clip ends in, held across
38
38
  * the seam. A key is written where the order changes, and none while it is the
39
- * 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).
40
55
  *
41
56
  * Pure: no clock, no randomness, no disk.
42
57
  */
43
58
  import type { Body18 } from './joints.ts';
44
- import { FLAT_BAND, LIMBS, type FrameLayers, type Limb } from './depth.ts';
59
+ import { FLAT_BAND, LIMBS, limbJointDepths, type FrameLayers, type Limb } from './depth.ts';
45
60
  import { knownSetsOf, type JointMap, type Rest, type RigSets } from './map.ts';
46
- 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';
47
63
 
48
64
  /** The limb pairs `--draw-order depth` keys. */
49
65
  export const DRAW_ORDER_PAIRS: ReadonlyArray<readonly [Limb, Limb]> = [['r_leg', 'l_leg']];
50
66
  /** What one segment inside the flat band can hide out of the picture, per unit of its rest length: √(1 − 0.95²). */
51
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';
52
71
  /** The DRAWORDER line's cap, characters (the span list is cut to it; the report carries every span). */
53
72
  export const DRAW_ORDER_LINE_MAX = 260;
54
73
 
@@ -56,7 +75,7 @@ export interface DrawOrderOffset {
56
75
  slot: string;
57
76
  offset: number;
58
77
  }
59
- /** One key as written into the animation (`t` the frame's time). */
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). */
60
79
  export interface DrawOrderKey {
61
80
  t: number;
62
81
  offsets?: DrawOrderOffset[];
@@ -71,6 +90,8 @@ export interface DrawOrderPair {
71
90
  keyed: boolean;
72
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). */
73
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;
74
95
  /** Why the pair is not keyed. */
75
96
  why?: string;
76
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). */
@@ -80,7 +101,7 @@ export interface DrawOrderPair {
80
101
  export interface DrawOrderGap {
81
102
  a: string;
82
103
  b: string;
83
- /** |depth a − depth b| over the frames both limbs read, px: the median and the largest. */
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). */
84
105
  median: number | null;
85
106
  max: number | null;
86
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. */
@@ -103,7 +124,7 @@ export interface DrawOrderSpan {
103
124
  }
104
125
  export interface DrawOrderReading {
105
126
  pairs: DrawOrderPair[];
106
- /** The `drawOrder` timeline, with the frame each key sits on. */
127
+ /** The `drawOrder` timeline as written, with the frame each key first shows on. */
107
128
  keys: Array<DrawOrderKey & { frame: number }>;
108
129
  /** Every span of one decided order, setup or keyed, in frame order per pair. */
109
130
  spans: DrawOrderSpan[];
@@ -225,12 +246,15 @@ function restOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | unde
225
246
 
226
247
  /**
227
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.
228
250
  * `leg` is that limb's upper + lower rest length (#324's unit for the gap).
229
251
  */
230
- function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, limbs: readonly [Limb, Limb]): { tie: number | null; leg: number | null } {
252
+ function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, limbs: readonly [Limb, Limb], by: LegJoint | undefined): { tie: number | null; leg: number | null } {
231
253
  const rests = limbs.map((l) => {
232
254
  const [u, w] = LIMBS[l].map((s) => restOf(track, depth, s));
233
- return u === null || u === undefined || w === null || w === undefined ? null : { hidden: u + w / 2, leg: u + w };
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 };
234
258
  });
235
259
  if (rests[0] == null || rests[1] == null) return { tie: null, leg: null };
236
260
  const r = rests[1].hidden < rests[0].hidden ? rests[1] : rests[0];
@@ -240,13 +264,13 @@ function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undef
240
264
  const round = (x: number, k: number): number => Math.round(x * 10 ** k) / 10 ** k;
241
265
 
242
266
  /** One pair's leg depth gap against its tie over the frames (the rule's own test: unequal and at least the tie). */
243
- function gapOf(pair: DrawOrderPair, layers: readonly FrameLayers[], leg: number | null): DrawOrderGap {
267
+ function gapOf(pair: DrawOrderPair, zs: ReadonlyArray<Partial<Record<Limb, number>>>, leg: number | null): DrawOrderGap {
244
268
  const tie = pair.tie ?? 0;
245
269
  const gaps: number[] = [];
246
270
  let passed = 0;
247
- for (const l of layers) {
248
- const za = l.depth[pair.limbs[0]];
249
- const zb = l.depth[pair.limbs[1]];
271
+ for (const z of zs) {
272
+ const za = z[pair.limbs[0]];
273
+ const zb = z[pair.limbs[1]];
250
274
  if (za === undefined || zb === undefined) continue;
251
275
  gaps.push(Math.abs(za - zb));
252
276
  if (za !== zb && Math.abs(za - zb) >= tie) passed++;
@@ -262,25 +286,42 @@ function gapOf(pair: DrawOrderPair, layers: readonly FrameLayers[], leg: number
262
286
  /**
263
287
  * The draw-order keys from the frames' layers. `track` and `depth` are the sequence's
264
288
  * joints and depth per frame (the rest lengths the tie reads); `order` is the rig's
265
- * 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).
266
292
  */
267
- 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];
268
295
  const resolved = DRAW_ORDER_PAIRS.map((p) => resolvePair(p, map, rest, order, sets));
269
296
  const legs = resolved.map((r) => {
270
- const t = tieOf(track, depth, r.pair.limbs);
297
+ const t = tieOf(track, depth, r.pair.limbs, by);
271
298
  r.pair.tie = t.tie;
299
+ if (by !== undefined) r.pair.by = by;
272
300
  return t.leg;
273
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
+ });
274
315
  const names = order.map((s) => s.name);
275
316
  // Per pair, per frame: who is behind ('a' | 'b'), or null for the setup order before any decision.
276
317
  const states = resolved.map((r) => {
277
318
  const tie = r.pair.tie ?? 0;
278
319
  const pass = (start: 'a' | 'b' | null): Array<'a' | 'b' | null> => {
279
320
  let state = start;
280
- return layers.map((l) => {
321
+ return zs.map((z) => {
281
322
  if (!r.pair.keyed) return null;
282
- const za = l.depth[r.pair.limbs[0]];
283
- const zb = l.depth[r.pair.limbs[1]];
323
+ const za = z[r.pair.limbs[0]];
324
+ const zb = z[r.pair.limbs[1]];
284
325
  if (za !== undefined && zb !== undefined && za !== zb && Math.abs(za - zb) >= tie) state = za < zb ? 'a' : 'b';
285
326
  return state;
286
327
  });
@@ -298,12 +339,14 @@ export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readon
298
339
  .sort((x, y) => names.indexOf(x.slot) - names.indexOf(y.slot));
299
340
  const keys: DrawOrderReading['keys'] = [];
300
341
  let last = '[]';
301
- for (let f = 0; f < layers.length; f++) {
342
+ for (let f = 0; f < zs.length; f++) {
302
343
  const offs = offsetsAt(f);
303
344
  const text = JSON.stringify(offs);
304
345
  if (text === last) continue;
305
346
  last = text;
306
- keys.push(offs.length === 0 ? { frame: f, t: times[f]! } : { frame: f, t: times[f]!, offsets: offs });
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 });
307
350
  }
308
351
  const spans: DrawOrderSpan[] = [];
309
352
  resolved.forEach((r, k) => {
@@ -314,7 +357,7 @@ export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readon
314
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 });
315
358
  });
316
359
  });
317
- return { pairs: resolved.map((r) => r.pair), keys, spans, gaps: resolved.map((r, k) => gapOf(r.pair, layers, legs[k]!)) };
360
+ return { pairs: resolved.map((r) => r.pair), keys, spans, gaps: resolved.map((r, k) => gapOf(r.pair, zs, legs[k]!)) };
318
361
  }
319
362
 
320
363
  /** The animation's `drawOrder` value: the keys without their frames. */
@@ -330,7 +373,9 @@ const span = (f: [number, number]): string => (f[0] === f[1] ? `${f[0]}` : `${f[
330
373
  */
331
374
  export function drawOrderLines(d: DrawOrderReading, prefix = 'DRAWORDER '): string[] {
332
375
  const keyed = d.spans.filter((s) => s.keyed);
333
- const head = `${prefix}from depth, ${d.keys.length} key(s) over ${keyed.length} span(s)`;
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)`;
334
379
  const groups = new Map<string, string[]>();
335
380
  for (const s of keyed) {
336
381
  const g = `${s.behind} behind ${s.front}`;
@@ -366,12 +411,18 @@ export function drawOrderLines(d: DrawOrderReading, prefix = 'DRAWORDER '): stri
366
411
  /** px with its fraction of the leg: "12.3 px (0.084 leg)". */
367
412
  const pxLeg = (px: number | null, legs: number | null): string => (px === null ? 'unread' : `${px} px${legs === null ? '' : ` (${legs} leg)`}`);
368
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
+
369
420
  /**
370
421
  * The DRAWGAP lines (rig-c/pose#324): one per pair, keyed or not — the leg depth gap's median and
371
422
  * max against the tie, and how many frames passed it. Cut to `DRAW_ORDER_LINE_MAX` characters.
372
423
  */
373
424
  export function drawGapLines(d: DrawOrderReading, prefix = 'DRAWGAP '): string[] {
374
425
  return (d.gaps ?? []).map((g) =>
375
- `${prefix}${g.a} / ${g.b}: leg depth gap 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),
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),
376
427
  );
377
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. */