@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/run.ts CHANGED
@@ -10,7 +10,7 @@ import { tmpdir } from 'node:os';
10
10
  import { dirname, join, relative, resolve } from 'node:path';
11
11
  import { knownSetsOf, readMap, resolveMap, type JointMap, type Rest } from './map.ts';
12
12
  import { cleanLine, cleanSummaryLine, inferredSource } from './clean.ts';
13
- import { depthLine, layersOf, type FrameLayers } from './depth.ts';
13
+ import { depthLine, depthRecleanText, forwardShinFrames, layersOf, type FrameLayers } from './depth.ts';
14
14
  import { drawGapLines, drawOrderFromDepth, drawOrderKeys, drawOrderLines, type DrawOrderReading } from './draworder.ts';
15
15
  import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
16
16
  import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
@@ -27,6 +27,7 @@ import { reportRenderDomain, writeReport, type ReportInput } from './report.ts';
27
27
  import { PROVENANCE_FILE, provenanceText } from './provenance.ts';
28
28
  import { madeLine, planOutDirs } from './outdir.ts';
29
29
  import { bodyLine, faceLine } from './face.ts';
30
+ import { contactKeysLines, readContactKeys } from './contacts.ts';
30
31
 
31
32
  export interface RetargetArgs extends RetargetOptions {
32
33
  rig: string;
@@ -81,6 +82,8 @@ export interface RetargetArgs extends RetargetOptions {
81
82
  output?: 'default' | 'verbose' | 'summary';
82
83
  /** `--draw-order depth` (rig-c/pose#304): key the animation's draw order from the sequence's depth. Absent: no draw-order key, the output as before. */
83
84
  drawOrder?: 'depth';
85
+ /** `--contact-keys` (rig-c/pose#303): the hand contact keys' file, `rig-pose-contact-keys/1`. Absent: the support as read, the output as before. */
86
+ contactKeysFile?: string;
84
87
  }
85
88
 
86
89
  export interface RunResult {
@@ -104,7 +107,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
104
107
  throw new Refusal(problems);
105
108
  };
106
109
  const args = new Problems();
107
- for (const [flag, path] of [['--map', a.map], ['--pose', a.pose], ['--merge', a.merge]] as const) {
110
+ for (const [flag, path] of [['--map', a.map], ['--pose', a.pose], ['--merge', a.merge], ['--contact-keys', a.contactKeysFile]] as const) {
108
111
  if (path !== undefined && !existsSync(path)) args.add('ARG_PATH', `${flag} ${path} does not exist`);
109
112
  }
110
113
  // rig-c/pose#239: the report is written unless asked elsewhere — beside the motion, as <out>.report.json.
@@ -141,6 +144,15 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
141
144
  // #304: the draw order is read off the sequence's depth; asked for on a sequence without it, refused by name.
142
145
  if (a.drawOrder === 'depth' && !seq.frames.some((x) => x.depth !== undefined)) refuse([{ code: 'DRAWORDER_NO_DEPTH', text: `--draw-order depth keys the draw order from the sequence's depth, and ${a.pose} carries none (rig-pose-sequence/${seq.cleaned!.params.depth === undefined ? '1 or 1.1, no "depth" on any frame' : '1.2 with no frame naming a segment'}); clean it with --depth walk|run, or retarget without --draw-order` }]);
143
146
  const frames = personTrack(seq, a.person, a.pose);
147
+ // #336: a derived depth whose shins lean the way the trunk faces was cleaned with the table before #336 (a walk's lifted
148
+ // foot forward); the draw order now reads a walk at the ankle, and would draw that foot in front. Refused by name.
149
+ if (a.drawOrder === 'depth') {
150
+ const old = forwardShinFrames(frames, seq.frames);
151
+ if (old.length > 0) refuse([{ code: 'DEPTH_RECLEAN', text: depthRecleanText(a.pose, seq.cleaned!.params.depth?.gait ?? null, old) }]);
152
+ }
153
+ // rig-c/pose#303: the contact keys, read against the sequence's frames before any gate runs.
154
+ const contactKeysSnapshot = a.contactKeysFile === undefined ? null : readFileSync(a.contactKeysFile);
155
+ const contactKeys = contactKeysSnapshot === null ? null : readContactKeys(contactKeysSnapshot.toString('utf8'), a.contactKeysFile!, seq.frames.length);
144
156
  const times = seq.frames.map((f) => f.t);
145
157
 
146
158
  const scratch = mkdtempSync(join(tmpdir(), 'rig-pose-'));
@@ -156,7 +168,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
156
168
  const slots = slotsOf(doc0);
157
169
  resolveMap(map, rest, a.map, slots, input.sets);
158
170
 
159
- const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.cleaned!.params.loop ? { loop: true } : {}), ...(seq.pelvisSource !== undefined ? { pelvis: { source: seq.pelvisSource, tilt: seq.frames.map((f) => f.pelvisTilt ?? null) } } : {}) }, slots, input.sets);
171
+ const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.cleaned!.params.loop ? { loop: true } : {}), ...(seq.pelvisSource !== undefined ? { pelvis: { source: seq.pelvisSource, tilt: seq.frames.map((f) => f.pelvisTilt ?? null) } } : {}), ...(contactKeys !== null ? { contactKeys } : {}), ...(seq.cleaned!.params.depth !== undefined ? { gait: seq.cleaned!.params.depth.gait } : {}) }, slots, input.sets);
160
172
  if (fragment.length > 0 && ((r.face?.sets?.length ?? 0) > 0 || (r.body?.sets?.length ?? 0) > 0)) {
161
173
  const known = knownSetsOf(map, input.sets);
162
174
  for (const s of [...(r.face?.sets ?? []), ...(r.body?.sets ?? [])]) {
@@ -227,8 +239,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
227
239
  // rig-c/pose#281: the limbs' front-to-back order from the sequence's depth, only when it carries depth.
228
240
  const layers: FrameLayers[] | null = seq.frames.some((x) => x.depth !== undefined) ? frames.map((j, i) => layersOf(j, seq.frames[i]!.depth ?? {})) : null;
229
241
  if (layers !== null) say(` ${depthLine(layers, seq.frames, seq.cleaned!.params.depth?.gait ?? null)}`);
230
- // rig-c/pose#304: with --draw-order depth, the legs' draw order keyed from those layers.
231
- const drawOrder: DrawOrderReading | null = a.drawOrder === 'depth' && layers !== null ? drawOrderFromDepth(layers, frames, seq.frames.map((x) => x.depth), r.times, map, rest, doc0.slots, input.sets, seq.cleaned!.params.loop) : null;
242
+ // rig-c/pose#304: with --draw-order depth, the legs' draw order keyed from the depth (#336: at the joint the recorded gait names).
243
+ const drawOrder: DrawOrderReading | null = a.drawOrder === 'depth' && layers !== null ? drawOrderFromDepth(layers, frames, seq.frames.map((x) => x.depth), r.times, map, rest, doc0.slots, input.sets, seq.cleaned!.params.loop, seq.cleaned!.params.depth?.gait ?? null) : null;
232
244
  if (drawOrder !== null) for (const l of [...drawOrderLines(drawOrder), ...drawGapLines(drawOrder)]) say(` ${l}`);
233
245
  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`);
234
246
  if (r.feet !== null) {
@@ -237,7 +249,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
237
249
  say(` FEET ${r.planted ? 'planted' : 'read, not planted (no --plant-feet)'}: leg length (median hip → ankle) r ${f.legLength.r.toFixed(1)}px, l ${f.legLength.l.toFixed(1)}px; ${f.supports.length} support span(s), ${f.changes.filter((c) => c.from !== null).length} change(s) of support, ${f.jumps.length} jump(s), ${f.steps.length} step(s)`);
238
250
  for (const sg of r.segments) say(` FEET ${segmentLine(sg)}`);
239
251
  say(` FEET supports: ${f.supports.map((x) => `${x.side} ${span(x.frames)}`).join(', ') || 'none'}`);
240
- if (f.signal !== undefined) for (const l of signalLines(f.signal, f.supports)) say(` FEET ${l}`);
252
+ // rig-c/pose#303: with contact keys, their stride lines stand in for #280's (the keys decide the frames they cover).
253
+ const signal = f.signal === undefined ? [] : signalLines(f.signal, f.supports);
254
+ for (const l of f.contactKeys === undefined ? signal : [...signal.filter((x) => !x.startsWith('stride ')), ...contactKeysLines(f)]) say(` FEET ${l}`);
241
255
  say(` FEET jumps (both ankles and mid_hip > 0.05 leg lengths above their rolling floor, ≥ 2 frames): ${f.jumps.map(span).join(', ') || 'none'}`);
242
256
  if (f.lowered !== undefined) say(` FEET ${loweredLine(f.lowered)}`);
243
257
  if (f.tracked !== undefined) say(` FEET tracked (the lower foot supports on the floor, following its scaled ankle track until still, rig-c/pose#119): ${f.tracked.map((x) => `${x.side} ${span(x.frames)}`).join(', ')}`);
@@ -423,7 +437,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
423
437
 
424
438
  assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
425
439
  // Every retarget option as resolved: the report's `options`, and with the name the build record's `flags` (#270).
426
- const options = { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', ...(a.reduceChains === undefined ? {} : { reduceChains: a.reduceChains }), reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}), ...(a.drawOrder !== undefined ? { drawOrder: a.drawOrder } : {}) };
440
+ const options = { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', ...(a.reduceChains === undefined ? {} : { reduceChains: a.reduceChains }), reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}), ...(a.drawOrder !== undefined ? { drawOrder: a.drawOrder } : {}), ...(a.contactKeysFile !== undefined ? { contactKeys: a.contactKeysFile } : {}) };
427
441
  const reportText = writeReport({
428
442
  spec: 'rig-pose-report/1',
429
443
  rigc,
@@ -485,8 +499,11 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
485
499
  pelvis: r.pelvis,
486
500
  // Only when the sequence carries depth (#281): a sequence without it writes the report it always has.
487
501
  ...(layers !== null ? { layers: layers.map((l, frame) => ({ frame, order: l.order, depth: l.depth })) } : {}),
488
- // Only with --draw-order depth (#304).
489
- ...(drawOrder !== null ? { drawOrder: { source: 'depth', pairs: drawOrder.pairs, keys: drawOrder.keys, spans: drawOrder.spans, gaps: drawOrder.gaps ?? [] } } : {}),
502
+ // Only with --draw-order depth (#304). A key's `t` here is its frame's time, the report's clock (#214); the
503
+ // motion's key is written half a frame before it (#325).
504
+ ...(drawOrder !== null ? { drawOrder: { source: 'depth', pairs: drawOrder.pairs, keys: drawOrder.keys.map((k) => ({ ...k, t: r.times[k.frame]! })), spans: drawOrder.spans, gaps: drawOrder.gaps ?? [] } } : {}),
505
+ // Only with --contact-keys (#303).
506
+ ...(r.feet?.contactKeys !== undefined && contactKeysSnapshot !== null ? { contactKeys: { path: a.contactKeysFile, sha256: createHash('sha256').update(contactKeysSnapshot).digest('hex'), keys: r.feet.contactKeys.keys.length, from: r.feet.contactKeys.from, changed: r.feet.contactKeys.changed, kept: r.feet.contactKeys.kept, unreached: r.feet.contactKeys.unreached } } : {}),
490
507
  stance: r.stance === null ? null : { tolerance: r.stance.tolerance, release: r.stance.release, minSeconds: r.stance.minSeconds, bounds: r.stance.bounds, ...(r.stance.range === undefined ? {} : { range: r.stance.range }), runs: r.stance.runs, over: r.stance.over },
491
508
  feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, straight: r.legStraight, rigLeg: r.rigLeg, segments: r.segments, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, ...(r.feet.lowered === undefined ? {} : { lowered: r.feet.lowered }), ...(r.feet.tracked === undefined ? {} : { tracked: r.feet.tracked }), floor: r.floor?.y ?? null, shifts: r.floor?.shifts ?? [], blend: r.blendHold === null ? null : { bounds: r.blendHold.bounds, ...(r.blendHold.range === undefined ? {} : { range: r.blendHold.range }), runs: r.blendHold.runs, over: r.blendHold.over }, reading: feetReading, closure: r.closure },
492
509
  shear,
package/src/switch.ts CHANGED
@@ -476,8 +476,15 @@ function moveBoundaries(vals: ReadonlyArray<AlternateSet | null>, pos: readonly
476
476
  return moves;
477
477
  }
478
478
 
479
- /** The key time for a change that shows on frame i: half a frame before it, 0 on the first frame. */
480
- const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 : r6((times[i - 1]! + times[i]!) / 2));
479
+ /**
480
+ * The key time for a stepped change that shows on frame i: half a frame before it, 0 on the first frame. A key
481
+ * exactly on a frame time is a tie the render breaks either way (its track time is a running sum of 1/fps, key
482
+ * times are held as float32, and a key applies when its time ≤ the track time), so about half such keys land a
483
+ * frame late (rig-c/pose#325); the midpoint sits half a frame from both ties. Every discrete key pose writes goes
484
+ * through it: the attachment and offset keys here, the draw-order keys in `draworder.ts`, and the face and body yaw
485
+ * sets' attachment keys in `retarget.ts` (rig-c/pose#334).
486
+ */
487
+ export const keyAtFrame = (times: readonly number[], i: number): number => (i === 0 ? 0 : r6((times[i - 1]! + times[i]!) / 2));
481
488
 
482
489
  function keyHandOffset(a: AlternateSet, map: JointMap, cn: string): [number, number] | null {
483
490
  const c = map.chains.find((x) => x.name === cn);
@@ -1116,7 +1123,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
1116
1123
  const keys: AttachmentKey[] = [];
1117
1124
  row.forEach((v, i) => {
1118
1125
  if (v === prev) return;
1119
- keys.push({ t: changeAt(times, i), v });
1126
+ keys.push({ t: keyAtFrame(times, i), v });
1120
1127
  prev = v;
1121
1128
  });
1122
1129
  if (keys.length > 0) slotTracks.push({ slot: s, property: 'attachment', keys });
@@ -1129,7 +1136,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
1129
1136
  row.forEach((v, i) => {
1130
1137
  const prev = row[i - 1];
1131
1138
  if (i === 0 || (prev![0] === v[0] && prev![1] === v[1])) return;
1132
- keys.push({ t: changeAt(times, i), v: [r3(v[0]), r3(v[1])] });
1139
+ keys.push({ t: keyAtFrame(times, i), v: [r3(v[0]), r3(v[1])] });
1133
1140
  });
1134
1141
  // Every key but the last holds until the next (a stepped curve on the last has nothing to ease to).
1135
1142
  for (let k = 0; k + 1 < keys.length; k++) keys[k]!.ease = 'stepped';