@rig-c/pose 0.2.1 → 0.4.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 +70 -2
- package/cli.ts +24 -2
- package/package.json +1 -1
- package/src/map.ts +194 -6
- package/src/retarget.ts +356 -21
- package/src/rigc.ts +36 -1
- package/src/run.ts +37 -10
- package/src/switch.ts +430 -0
package/src/retarget.ts
CHANGED
|
@@ -23,11 +23,38 @@
|
|
|
23
23
|
* evaluated at the frame, on each joint's positions after its holds. Centred,
|
|
24
24
|
* it moves no motion in time; a cubic keeps a peak a moving average flattens.
|
|
25
25
|
*
|
|
26
|
+
* The short hold (rig-c/pose#22) comes next: a segment whose picture length on
|
|
27
|
+
* a frame is under `short` of its own median length over the sequence is seen
|
|
28
|
+
* end-on, or collapsed by blur, and its picture direction there is noise. Its
|
|
29
|
+
* angle on those frames is not read from the pose but interpolated, the short
|
|
30
|
+
* way round, between the frames either side at or above the cutoff (flat at the
|
|
31
|
+
* ends: `holdShort`). A run under the cutoff lasting ≥ `END_ON_SECONDS` (spans
|
|
32
|
+
* merged across a gap of `END_ON_GAP` frame) is listed as `endOn`: a pose the
|
|
33
|
+
* rig cannot draw, the demand for an alternate drawing (#6).
|
|
34
|
+
*
|
|
26
35
|
* The flip guard (rig-c/pose#15) then spreads a segment's turn greater than
|
|
27
36
|
* `flip` degrees in one frame over the frames around it, linearly and centred on
|
|
28
37
|
* the jump (`spreadFlips`). It reads the mapped segments' target angles after
|
|
29
|
-
* holds and
|
|
30
|
-
* later, from posed positions, and is
|
|
38
|
+
* holds, smoothing and the short hold, and before the hinge check. A reach's
|
|
39
|
+
* target is aimed later, from posed positions, and is neither held short nor
|
|
40
|
+
* guarded.
|
|
41
|
+
*
|
|
42
|
+
* Then the switches (rig-c/pose#6, `switch.ts`): with every target known —
|
|
43
|
+
* reaches included — each alternate set's distance to the pose is read, and the
|
|
44
|
+
* spans each set shows are chosen. Only then the range clamp: outside the spans
|
|
45
|
+
* of a set depicting its chain, a bone whose rotate key would leave its
|
|
46
|
+
* `range` is clamped at the limit (its target becomes the clamped world angle),
|
|
47
|
+
* in depth order, so a bone below takes the clamped parent into its own key
|
|
48
|
+
* and still follows the pose; a reach below is aimed again from where the
|
|
49
|
+
* clamped bones put its origin. Every overflow is listed as `range` demand.
|
|
50
|
+
* Inside a span nothing is clamped, but a bone with a `drawn` angle is read
|
|
51
|
+
* against it (`artTurn`, and `artRange` where it leaves its range).
|
|
52
|
+
*
|
|
53
|
+
* The lines across the body — hips, shoulders, eyes — are read from the pose
|
|
54
|
+
* after holds and smoothing, whether the map uses them or not: a line under
|
|
55
|
+
* `TURN_FRACTION` of its median picture length, or reversed (pointing more
|
|
56
|
+
* than 90° from the subject's right to left), for ≥ `TURN_SECONDS`, is a `TURN`:
|
|
57
|
+
* the body or head turned, a pose a flat rig cannot draw. It changes no key.
|
|
31
58
|
*
|
|
32
59
|
* What this cannot know is held to account afterwards: `verify.ts` poses the
|
|
33
60
|
* gated build with rig-c's poser and measures where each bone landed.
|
|
@@ -35,14 +62,16 @@
|
|
|
35
62
|
import { screenToSpineDegrees } from 'rig-c/transform';
|
|
36
63
|
import type { Body18 } from './joints.ts';
|
|
37
64
|
import { isBody18, VIRTUAL } from './joints.ts';
|
|
38
|
-
import type { Chain, JointMap, Rest } from './map.ts';
|
|
65
|
+
import type { Chain, JointMap, Rest, RigSlots } from './map.ts';
|
|
39
66
|
import { aimAngle, rigPoint } from './map.ts';
|
|
40
67
|
import type { Joint } from './pose.ts';
|
|
41
68
|
import { Problems } from './problems.ts';
|
|
69
|
+
import type { ArtRange, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
70
|
+
import { coveredChains, measureArt, overflow, RANGE_SLACK, runsOf, selectSwitches } from './switch.ts';
|
|
42
71
|
|
|
43
72
|
const DEG = 180 / Math.PI;
|
|
44
73
|
|
|
45
|
-
export interface RetargetOptions {
|
|
74
|
+
export interface RetargetOptions extends SwitchOptions {
|
|
46
75
|
/** A joint with a score below this is not good on that frame (a joint with no score is). */
|
|
47
76
|
minScore: number;
|
|
48
77
|
/** Savitzky–Golay window over joint positions, in frames: 1 = none, else odd and ≥ 5. */
|
|
@@ -53,6 +82,8 @@ export interface RetargetOptions {
|
|
|
53
82
|
bendTolerance: number;
|
|
54
83
|
/** A segment turning more than this many degrees in one frame is spread (`spreadFlips`); 'off' keeps every turn. */
|
|
55
84
|
flip: number | 'off';
|
|
85
|
+
/** A segment shorter in the picture than this fraction of its median length has its angle held there (`holdShort`); 'off' reads every frame. */
|
|
86
|
+
short: number | 'off';
|
|
56
87
|
}
|
|
57
88
|
|
|
58
89
|
export interface Hold {
|
|
@@ -97,9 +128,44 @@ export interface Flip {
|
|
|
97
128
|
clipped: 'start' | 'end' | 'both' | null;
|
|
98
129
|
}
|
|
99
130
|
|
|
131
|
+
/** One span of frames under the short cutoff: the segment's angle there was interpolated, not read (`holdShort`). */
|
|
132
|
+
export interface Short {
|
|
133
|
+
chain: string;
|
|
134
|
+
bone: string;
|
|
135
|
+
/** The segment the bone carries: from joint, to joint. */
|
|
136
|
+
joints: [string, string];
|
|
137
|
+
/** First and last frame under the cutoff; every one of them had its angle replaced, and the frames either side keep the input's. */
|
|
138
|
+
frames: [number, number];
|
|
139
|
+
/** The lowest picture length on those frames, as a fraction of the segment's median picture length. */
|
|
140
|
+
minFraction: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A segment under the short cutoff long enough to be a pose (the limb pointing
|
|
145
|
+
* at the camera), not a blur: short spans merged across a gap of `END_ON_GAP`
|
|
146
|
+
* frame, lasting ≥ `END_ON_SECONDS`. The demand for an alternate drawing (#6).
|
|
147
|
+
*/
|
|
148
|
+
export interface EndOn {
|
|
149
|
+
chain: string;
|
|
150
|
+
bone: string;
|
|
151
|
+
joints: [string, string];
|
|
152
|
+
/** First and last frame of the merged run (a gap frame inside it is at or above the cutoff). */
|
|
153
|
+
frames: [number, number];
|
|
154
|
+
/** How long it lasts: from the first frame's key time to the end of the last frame (the next frame's key time; for the pose's last frame, one frame on). */
|
|
155
|
+
seconds: number;
|
|
156
|
+
minFraction: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A run under the short cutoff lasting at least this long, in seconds, is listed as `endOn` (rig-c/pose#22). */
|
|
160
|
+
export const END_ON_SECONDS = 0.15;
|
|
161
|
+
/** Short spans this many frames apart or fewer merge into one `endOn` run. */
|
|
162
|
+
export const END_ON_GAP = 1;
|
|
163
|
+
|
|
100
164
|
export interface MotionKey {
|
|
101
165
|
t: number;
|
|
102
166
|
v: number[];
|
|
167
|
+
/** `stepped`: the value holds until the next key (the offsets' translate keys). */
|
|
168
|
+
ease?: 'stepped';
|
|
103
169
|
}
|
|
104
170
|
export interface MotionTrack {
|
|
105
171
|
bone: string;
|
|
@@ -108,9 +174,51 @@ export interface MotionTrack {
|
|
|
108
174
|
}
|
|
109
175
|
export interface Animation {
|
|
110
176
|
duration: number;
|
|
111
|
-
tracks
|
|
177
|
+
/** The bone tracks, then any slot `attachment` tracks the switches add (`switch.ts`). */
|
|
178
|
+
tracks: Array<MotionTrack | SlotTrack>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A line across the body turned: short in the picture, or reversed, long enough to be a pose (the demand for a turned-body drawing). */
|
|
182
|
+
export interface Turn {
|
|
183
|
+
/** `hip`, `shoulder` or `eye`. */
|
|
184
|
+
line: string;
|
|
185
|
+
/** The subject's right joint, then its left twin. */
|
|
186
|
+
joints: [string, string];
|
|
187
|
+
/** First and last frame of the run (merged across a gap of `TURN_GAP` frame). */
|
|
188
|
+
frames: [number, number];
|
|
189
|
+
seconds: number;
|
|
190
|
+
/** The lowest picture length on the run's turned frames, as a fraction of the line's median. */
|
|
191
|
+
minFraction: number;
|
|
192
|
+
/** Whether the line points from the right joint to the left more than 90° off the subject's right-to-left on any frame of the run. */
|
|
193
|
+
reversed: boolean;
|
|
112
194
|
}
|
|
113
195
|
|
|
196
|
+
/** A bone's rotate key past its range outside every span of a set depicting its chain: clamped there, and the demand for an alternate. */
|
|
197
|
+
export interface RangeDemand {
|
|
198
|
+
chain: string;
|
|
199
|
+
bone: string;
|
|
200
|
+
/** First and last frame of the run (merged across a gap of one frame; a gap frame inside is within range or inside a span). */
|
|
201
|
+
frames: [number, number];
|
|
202
|
+
seconds: number;
|
|
203
|
+
/** Degrees past the range, the most on any frame of the run. */
|
|
204
|
+
maxOverflow: number;
|
|
205
|
+
/** The rotate key the pose asked for on that frame, degrees relative to setup, before the clamp. */
|
|
206
|
+
key: number;
|
|
207
|
+
/** The bone's range, degrees. */
|
|
208
|
+
limits: [number, number];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The lines across the body a `TURN` is read on: the subject's right joint, then its left twin. */
|
|
212
|
+
export const TURN_LINES: ReadonlyArray<[string, string, string]> = [['hip', 'r_hip', 'l_hip'], ['shoulder', 'r_shoulder', 'l_shoulder'], ['eye', 'r_eye', 'l_eye']];
|
|
213
|
+
/** A line shorter in the picture than this fraction of its median is turned (rig-c/pose#6: between the front-facing dance's floor, 0.81, and a side-on body's 0.17–0.49). */
|
|
214
|
+
export const TURN_FRACTION = 0.5;
|
|
215
|
+
/** A turned run lasting at least this long, in seconds, is listed (as `endOn`). */
|
|
216
|
+
export const TURN_SECONDS = 0.15;
|
|
217
|
+
/** Turned spans this many frames apart or fewer merge into one run. */
|
|
218
|
+
export const TURN_GAP = 1;
|
|
219
|
+
/** Range overflow and art-range spans this many frames apart or fewer merge into one run. */
|
|
220
|
+
export const RANGE_GAP = 1;
|
|
221
|
+
|
|
114
222
|
export interface Retarget {
|
|
115
223
|
animation: Animation;
|
|
116
224
|
/** Key times, seconds from the first frame, as written. */
|
|
@@ -121,9 +229,27 @@ export interface Retarget {
|
|
|
121
229
|
origin: { bone: string; at: Array<[number, number]> } | null;
|
|
122
230
|
holds: Hold[];
|
|
123
231
|
bends: BendEvent[];
|
|
232
|
+
/** The short hold's spans, per segment in map order. */
|
|
233
|
+
short: Short[];
|
|
234
|
+
/** The short runs long enough to be a pose seen end-on. */
|
|
235
|
+
endOn: EndOn[];
|
|
124
236
|
flips: Flip[];
|
|
125
237
|
/** The figure scale, rig units per picture pixel, and the lengths it came from. */
|
|
126
238
|
scale: { factor: number; pose: number; rig: number } | null;
|
|
239
|
+
/** The spans each alternate set shows, with their art turn. */
|
|
240
|
+
switches: Switch[];
|
|
241
|
+
/** Per switched or hidden slot, what it shows on each frame (null: nothing). */
|
|
242
|
+
shown: Map<string, Array<string | null>>;
|
|
243
|
+
/** Per set, its distance to the pose on each frame, degrees. */
|
|
244
|
+
distances: Map<string, number[]>;
|
|
245
|
+
/** The lines across the body turned long enough to be a pose. */
|
|
246
|
+
turns: Turn[];
|
|
247
|
+
/** The lines across the body that could not be read, and why. */
|
|
248
|
+
turnsUnread: Array<{ line: string; joints: [string, string]; why: string }>;
|
|
249
|
+
/** Every run a bone's key was clamped at its range. */
|
|
250
|
+
range: RangeDemand[];
|
|
251
|
+
/** Every run a bone inside a span turned from its drawn angle past its range. */
|
|
252
|
+
artRange: ArtRange[];
|
|
127
253
|
}
|
|
128
254
|
|
|
129
255
|
type Pt = [number, number];
|
|
@@ -237,7 +363,7 @@ const r3 = (v: number) => {
|
|
|
237
363
|
* good, when a hinge's side cannot be read off a straight rest, and — with
|
|
238
364
|
* `bends: 'refuse'` — when a hinge bends the wrong way.
|
|
239
365
|
*/
|
|
240
|
-
export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, times: readonly number[], opts: RetargetOptions): Retarget {
|
|
366
|
+
export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, times: readonly number[], opts: RetargetOptions, slots: RigSlots = new Map()): Retarget {
|
|
241
367
|
const p = new Problems();
|
|
242
368
|
const n = frames.length;
|
|
243
369
|
const t0 = times[0]!;
|
|
@@ -245,6 +371,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
245
371
|
for (let i = 1; i < n; i++) {
|
|
246
372
|
if (keyTimes[i]! <= keyTimes[i - 1]!) p.add('POSE_TIME', `frames ${i - 1} and ${i} are ${keyTimes[i - 1]}s and ${keyTimes[i]}s after rounding to the microsecond; keys must strictly increase`);
|
|
247
373
|
}
|
|
374
|
+
/** The end of frame i: the next frame's key time; the last frame lasts as long as the one before it. */
|
|
375
|
+
const endOf = (i: number): number => (i + 1 < n ? keyTimes[i + 1]! : n > 1 ? 2 * keyTimes[i]! - keyTimes[i - 1]! : keyTimes[i]!);
|
|
376
|
+
const secondsOf = (run: [number, number]): number => Math.round((endOf(run[1]) - keyTimes[run[0]]!) * 1e6) / 1e6;
|
|
248
377
|
|
|
249
378
|
// Every point the map reads, held where it is not good.
|
|
250
379
|
const needed = new Set<string>();
|
|
@@ -256,15 +385,13 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
256
385
|
const series = new Map<string, Pt[]>();
|
|
257
386
|
const good = new Map<string, boolean[]>();
|
|
258
387
|
const holds: Hold[] = [];
|
|
259
|
-
|
|
388
|
+
/** A point held where it is not good, then smoothed; or why it is good on no frame. */
|
|
389
|
+
const heldOf = (name: string): { at: Pt[]; good: boolean[]; holds: Hold[] } | { never: string } => {
|
|
260
390
|
const raw = frames.map((f) => pointOn(f, name, opts.minScore));
|
|
261
391
|
const firstGood = raw.findIndex((v) => typeof v !== 'string');
|
|
262
|
-
if (firstGood < 0) {
|
|
263
|
-
const why = [...new Set(raw as string[])].slice(0, 3).join('; ');
|
|
264
|
-
p.add('POSE_POINT_NEVER_GOOD', `point "${name}" is good on none of the ${n} frames (${why}); the map needs it, and there is no value to hold`);
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
392
|
+
if (firstGood < 0) return { never: [...new Set(raw as string[])].slice(0, 3).join('; ') };
|
|
267
393
|
const out: Pt[] = [];
|
|
394
|
+
const spans: Hold[] = [];
|
|
268
395
|
let last = firstGood;
|
|
269
396
|
let span: Hold | null = null;
|
|
270
397
|
for (let i = 0; i < n; i++) {
|
|
@@ -279,14 +406,66 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
279
406
|
if (span !== null && span.source === last && span.why === v) span.to = i;
|
|
280
407
|
else {
|
|
281
408
|
span = { point: name, from: i, to: i, source: last, why: v };
|
|
282
|
-
|
|
409
|
+
spans.push(span);
|
|
283
410
|
}
|
|
284
411
|
}
|
|
285
|
-
|
|
286
|
-
|
|
412
|
+
return { at: savitzkyGolay(out, opts.smooth), good: raw.map((v) => typeof v !== 'string'), holds: spans };
|
|
413
|
+
};
|
|
414
|
+
for (const name of [...needed].sort()) {
|
|
415
|
+
const h = heldOf(name);
|
|
416
|
+
if ('never' in h) {
|
|
417
|
+
p.add('POSE_POINT_NEVER_GOOD', `point "${name}" is good on none of the ${n} frames (${h.never}); the map needs it, and there is no value to hold`);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
holds.push(...h.holds);
|
|
421
|
+
series.set(name, h.at);
|
|
422
|
+
good.set(name, h.good);
|
|
287
423
|
}
|
|
288
424
|
p.throwIfAny();
|
|
289
425
|
|
|
426
|
+
// TURN: the lines across the body, read from the pose (held and smoothed as the
|
|
427
|
+
// map's points are, whether or not the map uses them). A reading; no key moves.
|
|
428
|
+
const turns: Turn[] = [];
|
|
429
|
+
const turnsUnread: Retarget['turnsUnread'] = [];
|
|
430
|
+
for (const [line, a, b] of TURN_LINES) {
|
|
431
|
+
const ends: Pt[][] = [];
|
|
432
|
+
for (const j of [a, b]) {
|
|
433
|
+
const known = series.get(j);
|
|
434
|
+
if (known !== undefined) ends.push(known);
|
|
435
|
+
else {
|
|
436
|
+
const h = heldOf(j);
|
|
437
|
+
if ('never' in h) {
|
|
438
|
+
turnsUnread.push({ line, joints: [a, b], why: `${j} is good on none of the ${n} frames (${h.never})` });
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
ends.push(h.at);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (ends.length < 2) continue;
|
|
445
|
+
const [A, B] = ends as [Pt[], Pt[]];
|
|
446
|
+
const lengths = A.map((f, i) => Math.hypot(B[i]![0] - f[0], B[i]![1] - f[1]));
|
|
447
|
+
const med = median(lengths);
|
|
448
|
+
if (med < 1e-6) {
|
|
449
|
+
turnsUnread.push({ line, joints: [a, b], why: `its median picture length is ${med}px; a fraction of it is not a measure` });
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const fraction = lengths.map((l) => l / med);
|
|
453
|
+
const reversed = A.map((f, i) => lengths[i]! > 1e-9 && Math.abs(pictureAngle(f, B[i]!)) > 90);
|
|
454
|
+
const turned = fraction.map((f, i) => f < TURN_FRACTION || reversed[i]!);
|
|
455
|
+
for (const run of runsOf(turned, TURN_GAP)) {
|
|
456
|
+
const seconds = secondsOf(run);
|
|
457
|
+
if (seconds < TURN_SECONDS - 1e-9) continue;
|
|
458
|
+
let minFraction = Infinity;
|
|
459
|
+
let rev = false;
|
|
460
|
+
for (let i = run[0]; i <= run[1]; i++) {
|
|
461
|
+
if (!turned[i]) continue;
|
|
462
|
+
minFraction = Math.min(minFraction, fraction[i]!);
|
|
463
|
+
rev ||= reversed[i]!;
|
|
464
|
+
}
|
|
465
|
+
turns.push({ line, joints: [a, b], frames: run, seconds, minFraction, reversed: rev });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
290
469
|
// The hip: the travel of one point, scaled, in its parent's axes. It comes
|
|
291
470
|
// first, because a reach is aimed from where the hip has put its bone.
|
|
292
471
|
let origin: Retarget['origin'] = null;
|
|
@@ -358,6 +537,39 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
358
537
|
}
|
|
359
538
|
p.throwIfAny();
|
|
360
539
|
|
|
540
|
+
// The short hold, on every limb segment the pose aims directly (not a reach, and
|
|
541
|
+
// not a line across the body from a left joint to its right twin — the hip line,
|
|
542
|
+
// the eye line — whose shortening is the body or head turning, not a limb seen
|
|
543
|
+
// end-on; #22's cutoff was measured on limb segments only): its picture length
|
|
544
|
+
// against its own median, on the points the angle was read from.
|
|
545
|
+
const short: Short[] = [];
|
|
546
|
+
const endOn: EndOn[] = [];
|
|
547
|
+
if (opts.short !== 'off') {
|
|
548
|
+
const cutoff = opts.short;
|
|
549
|
+
for (const c of map.chains) {
|
|
550
|
+
if (c.reach) continue;
|
|
551
|
+
c.bones.forEach((b, k) => {
|
|
552
|
+
const joints: [string, string] = [c.joints[k]!, c.joints[k + 1]!];
|
|
553
|
+
if (isAcross(joints[0], joints[1])) return;
|
|
554
|
+
const from = series.get(joints[0])!;
|
|
555
|
+
const to = series.get(joints[1])!;
|
|
556
|
+
const lengths = from.map((f, i) => Math.hypot(to[i]![0] - f[0], to[i]![1] - f[1]));
|
|
557
|
+
const med = median(lengths);
|
|
558
|
+
if (med < 1e-6) {
|
|
559
|
+
p.add('POSE_SHORT_MEDIAN', `chain "${c.name}" — bone "${b}"'s segment ${joints[0]} → ${joints[1]} has median picture length ${med}px over the ${n} frames; --short measures a frame's length against that median, and there is none to measure against`);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const spans = holdShort(targets.get(b)!, lengths.map((l) => l / med), cutoff);
|
|
563
|
+
for (const s of spans) short.push({ chain: c.name, bone: b, joints, ...s });
|
|
564
|
+
for (const run of mergeRuns(spans, END_ON_GAP)) {
|
|
565
|
+
const seconds = Math.round((endOf(run.frames[1]) - keyTimes[run.frames[0]]!) * 1e6) / 1e6;
|
|
566
|
+
if (seconds >= END_ON_SECONDS - 1e-9) endOn.push({ chain: c.name, bone: b, joints, frames: run.frames, seconds, minFraction: run.minFraction });
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
p.throwIfAny();
|
|
572
|
+
|
|
361
573
|
// The flip guard, on every segment the pose aims directly (not a reach).
|
|
362
574
|
const flips: Flip[] = [];
|
|
363
575
|
if (opts.flip !== 'off') {
|
|
@@ -390,6 +602,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
390
602
|
return d;
|
|
391
603
|
};
|
|
392
604
|
const reaches = map.chains.filter((c) => c.reach).sort((x, y) => depth(x.bones[0]!) - depth(y.bones[0]!));
|
|
605
|
+
let aimReach: ((c: Chain) => void) | null = null;
|
|
393
606
|
if (reaches.length > 0 && t !== null && scale !== null) {
|
|
394
607
|
const s = scale.factor;
|
|
395
608
|
const tb = rest.get(t.bone)!;
|
|
@@ -411,7 +624,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
411
624
|
const oy = rest.get(b)!.worldY - rest.get(parent)!.worldY;
|
|
412
625
|
return [po[0] + Math.cos(r) * ox - Math.sin(r) * oy, po[1] + Math.sin(r) * ox + Math.cos(r) * oy];
|
|
413
626
|
};
|
|
414
|
-
|
|
627
|
+
aimReach = (c: Chain): void => {
|
|
415
628
|
const b = c.bones[0]!;
|
|
416
629
|
const anchor = series.get(c.joints[0]!)!;
|
|
417
630
|
const end = series.get(c.joints[1]!)!;
|
|
@@ -425,20 +638,56 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
425
638
|
}
|
|
426
639
|
fillLeading(angles, () => p.add('POSE_SEGMENT_ZERO', `chain "${c.name}" — bone "${b}" reaches for ${c.joints[1]}, which the pose puts on the bone's posed origin on every frame; it has no direction`));
|
|
427
640
|
targets.set(b, angles);
|
|
428
|
-
}
|
|
641
|
+
};
|
|
642
|
+
for (const c of reaches) aimReach(c);
|
|
429
643
|
}
|
|
430
644
|
p.throwIfAny();
|
|
431
645
|
|
|
432
|
-
//
|
|
646
|
+
// The switches: with every target known, each set's distance and its spans.
|
|
647
|
+
const sw = selectSwitches(map, targets, keyTimes, keyTimes[n - 1]!, slots, opts);
|
|
648
|
+
|
|
649
|
+
// The range clamp, outside the spans of a set depicting the bone's chain. Parents
|
|
650
|
+
// first, so a bone below takes its clamped parent into its key; a reach is aimed
|
|
651
|
+
// again from where the clamped bones above put its origin.
|
|
433
652
|
const mappedAbove = (b: string): string | null => {
|
|
434
653
|
for (let a = rest.get(b)!.parent; a !== null; a = rest.get(a)!.parent) if (targets.has(a)) return a;
|
|
435
654
|
return null;
|
|
436
655
|
};
|
|
437
|
-
const
|
|
656
|
+
const turn = (bone: string, i: number) => targets.get(bone)![i]! - restOf.get(bone)!;
|
|
657
|
+
const covered = coveredChains(sw.switches, n);
|
|
658
|
+
const range: RangeDemand[] = [];
|
|
659
|
+
const order = map.chains.flatMap((c) => c.bones.map((b) => ({ c, b }))).filter(({ c, b }) => c.reach || b in c.range);
|
|
660
|
+
order.sort((x, y) => depth(x.b) - depth(y.b));
|
|
661
|
+
for (const { c, b } of order) {
|
|
662
|
+
if (c.reach && aimReach !== null) aimReach(c);
|
|
663
|
+
const limits = c.range[b];
|
|
664
|
+
if (limits === undefined) continue;
|
|
665
|
+
const above = mappedAbove(b);
|
|
666
|
+
const tg = targets.get(b)!;
|
|
667
|
+
const cov = covered.get(c.name);
|
|
668
|
+
const over: number[] = [];
|
|
669
|
+
const asked: number[] = [];
|
|
670
|
+
for (let i = 0; i < n; i++) {
|
|
671
|
+
const key = wrap(turn(b, i) - (above === null ? 0 : turn(above, i)));
|
|
672
|
+
asked.push(key);
|
|
673
|
+
const o = cov?.[i] === true ? 0 : overflow(key, limits);
|
|
674
|
+
over.push(o);
|
|
675
|
+
if (o > RANGE_SLACK) tg[i] = wrap(tg[i]! + Math.min(Math.max(key, limits[0]), limits[1]) - key);
|
|
676
|
+
}
|
|
677
|
+
for (const run of runsOf(over.map((o) => o > RANGE_SLACK), RANGE_GAP)) {
|
|
678
|
+
let worst = run[0];
|
|
679
|
+
for (let i = run[0]; i <= run[1]; i++) if (over[i]! > over[worst]!) worst = i;
|
|
680
|
+
range.push({ chain: c.name, bone: b, frames: run, seconds: secondsOf(run), maxOverflow: r3(over[worst]!), key: r3(asked[worst]!), limits });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
p.throwIfAny();
|
|
684
|
+
const artRange = measureArt(sw.switches, map, targets, endOf, keyTimes, RANGE_GAP);
|
|
685
|
+
|
|
686
|
+
// Keys: the bone's turn less the turn of the nearest mapped bone above it.
|
|
687
|
+
const tracks: Array<MotionTrack | SlotTrack> = [];
|
|
438
688
|
for (const c of map.chains) {
|
|
439
689
|
for (const b of c.bones) {
|
|
440
690
|
const above = mappedAbove(b);
|
|
441
|
-
const turn = (bone: string, i: number) => targets.get(bone)![i]! - restOf.get(bone)!;
|
|
442
691
|
let prev = 0;
|
|
443
692
|
const keys: MotionKey[] = [];
|
|
444
693
|
for (let i = 0; i < n; i++) {
|
|
@@ -451,8 +700,94 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
451
700
|
}
|
|
452
701
|
}
|
|
453
702
|
if (translateTrack !== null) tracks.push(translateTrack);
|
|
703
|
+
tracks.push(...sw.offsetTracks, ...sw.slotTracks);
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
707
|
+
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale,
|
|
708
|
+
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
type ShortSpan = Omit<Short, 'chain' | 'bone' | 'joints'>;
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* The short hold on one angle series (degrees), in place. `fraction` is the
|
|
716
|
+
* segment's picture length on each frame over its median. Each run of frames
|
|
717
|
+
* under `cutoff` has its angles replaced by the line from the last frame before
|
|
718
|
+
* it at or above the cutoff to the first one after it, the short way round; a
|
|
719
|
+
* run at the start or the end of the series holds the one reliable end flat.
|
|
720
|
+
* With the fraction against the median and a cutoff under 1, half the frames
|
|
721
|
+
* at least are at or above it, so every run has a reliable end. Returns the runs.
|
|
722
|
+
*/
|
|
723
|
+
/** A line across the body: a left joint and its right twin (r_hip → l_hip, r_eye → l_eye). */
|
|
724
|
+
export function isAcross(a: string, b: string): boolean {
|
|
725
|
+
const twin = (x: string): string | null => (x.startsWith('r_') ? `l_${x.slice(2)}` : x.startsWith('l_') ? `r_${x.slice(2)}` : null);
|
|
726
|
+
return twin(a) === b;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function holdShort(angles: number[], fraction: readonly number[], cutoff: number): ShortSpan[] {
|
|
730
|
+
const n = angles.length;
|
|
731
|
+
const out: ShortSpan[] = [];
|
|
732
|
+
for (let i = 0; i < n; i++) {
|
|
733
|
+
if (!(fraction[i]! < cutoff)) continue;
|
|
734
|
+
let j = i;
|
|
735
|
+
while (j + 1 < n && fraction[j + 1]! < cutoff) j++;
|
|
736
|
+
const lo = i - 1;
|
|
737
|
+
const hi = j + 1;
|
|
738
|
+
let minFraction = fraction[i]!;
|
|
739
|
+
for (let k = i + 1; k <= j; k++) minFraction = Math.min(minFraction, fraction[k]!);
|
|
740
|
+
if (lo >= 0 || hi < n) {
|
|
741
|
+
const a = lo >= 0 ? angles[lo]! : angles[hi]!;
|
|
742
|
+
const b = hi < n ? angles[hi]! : a;
|
|
743
|
+
const turn = wrap(b - a);
|
|
744
|
+
for (let k = i; k <= j; k++) angles[k] = lo < 0 ? b : wrap(a + (turn * (k - lo)) / (hi - lo));
|
|
745
|
+
}
|
|
746
|
+
out.push({ frames: [i, j], minFraction });
|
|
747
|
+
i = j;
|
|
748
|
+
}
|
|
749
|
+
return out;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** Short spans `gap` frames apart or fewer, as one run each: its first and last frame, and the lowest fraction. */
|
|
753
|
+
export function mergeRuns(spans: readonly ShortSpan[], gap: number): ShortSpan[] {
|
|
754
|
+
const out: ShortSpan[] = [];
|
|
755
|
+
for (const s of spans) {
|
|
756
|
+
const last = out[out.length - 1];
|
|
757
|
+
if (last !== undefined && s.frames[0] - last.frames[1] - 1 <= gap) {
|
|
758
|
+
last.frames = [last.frames[0], s.frames[1]];
|
|
759
|
+
last.minFraction = Math.min(last.minFraction, s.minFraction);
|
|
760
|
+
} else out.push({ frames: [s.frames[0], s.frames[1]], minFraction: s.minFraction });
|
|
761
|
+
}
|
|
762
|
+
return out;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const spanOf = (frames: [number, number], times: readonly number[]): string =>
|
|
766
|
+
frames[0] === frames[1] ? `frame ${frames[0]} (t=${times[frames[0]]!.toFixed(3)}s)` : `frames ${frames[0]}–${frames[1]} (t=${times[frames[0]]!.toFixed(3)}–${times[frames[1]]!.toFixed(3)}s)`;
|
|
767
|
+
|
|
768
|
+
export function shortLine(s: Short, times: readonly number[], cutoff: number): string {
|
|
769
|
+
const lo = s.frames[0] - 1;
|
|
770
|
+
const hi = s.frames[1] + 1;
|
|
771
|
+
const held = lo < 0 && hi >= times.length
|
|
772
|
+
? 'no frame of the pose is at or above it, so nothing was held'
|
|
773
|
+
: lo < 0 ? `held flat at frame ${hi}'s angle (the pose starts short)`
|
|
774
|
+
: hi >= times.length ? `held flat at frame ${lo}'s angle (the pose ends short)`
|
|
775
|
+
: `interpolated from frame ${lo}'s angle to frame ${hi}'s, the short way round`;
|
|
776
|
+
return `chain "${s.chain}" bone "${s.bone}" (${s.joints[0]} → ${s.joints[1]}) ${spanOf(s.frames, times)}: picture length down to ${s.minFraction.toFixed(2)} of its median, under ${cutoff}; its direction is not read — ${held}`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export function turnLine(e: Turn, times: readonly number[]): string {
|
|
780
|
+
const how = e.reversed ? `reversed (pointing more than 90° from ${e.joints[0]} → ${e.joints[1]}'s own way), picture length down to ${e.minFraction.toFixed(2)} of its median` : `picture length down to ${e.minFraction.toFixed(2)} of its median, under ${TURN_FRACTION}`;
|
|
781
|
+
return `line "${e.line}" (${e.joints[0]} → ${e.joints[1]}) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s ${how}, ≥ ${TURN_SECONDS}s — the body or head turned, a pose the rig cannot draw`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export function rangeLine(e: RangeDemand, times: readonly number[]): string {
|
|
785
|
+
const sg = (v: number) => `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)}°`;
|
|
786
|
+
return `chain "${e.chain}" bone "${e.bone}" ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s the pose asks its rotate key past its range [${e.limits[0]}, ${e.limits[1]}] (${sg(e.key)}, up to ${e.maxOverflow.toFixed(1)}° past); clamped at the limit — demand for an alternate`;
|
|
787
|
+
}
|
|
454
788
|
|
|
455
|
-
|
|
789
|
+
export function endOnLine(e: EndOn, times: readonly number[], cutoff: number): string {
|
|
790
|
+
return `chain "${e.chain}" bone "${e.bone}" (${e.joints[0]} → ${e.joints[1]}) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s under ${cutoff} of its median picture length (down to ${e.minFraction.toFixed(2)}), ≥ ${END_ON_SECONDS}s — seen end-on, a pose the rig cannot draw`;
|
|
456
791
|
}
|
|
457
792
|
|
|
458
793
|
type Spread = Omit<Flip, 'chain' | 'bone' | 'joints'>;
|
package/src/rigc.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
|
|
|
13
13
|
import { dirname, join, resolve } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { poseRawSetup, readModel, type CompiledDocument } from 'rig-c/core';
|
|
16
|
-
import type { Rest, RestBone } from './map.ts';
|
|
16
|
+
import type { Rest, RestBone, RigSlot, RigSlots } from './map.ts';
|
|
17
17
|
import { isObject, parseJson, Problems, Refusal, typeName } from './problems.ts';
|
|
18
18
|
|
|
19
19
|
export interface RigInput {
|
|
@@ -137,6 +137,41 @@ export function restOf(doc: CompiledDocument): Rest {
|
|
|
137
137
|
return out;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** Every slot of the build: what it shows at setup, as the compiled model states it, and every attachment any skin gives it. */
|
|
141
|
+
export function slotsOf(doc: CompiledDocument): RigSlots {
|
|
142
|
+
const out = new Map<string, RigSlot>();
|
|
143
|
+
for (const s of doc.slots) {
|
|
144
|
+
const attachments = new Set<string>();
|
|
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 });
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The bones that move an attachment: a weighted mesh's bindings (a linked mesh
|
|
153
|
+
* its source's), or the slot's bone for anything else — what an uncovered fold
|
|
154
|
+
* names its chains by.
|
|
155
|
+
*/
|
|
156
|
+
export function attachmentBones(doc: CompiledDocument, slot: string, attachment: string): string[] {
|
|
157
|
+
const slotBone = doc.slots.find((s) => s.name === slot)?.bone;
|
|
158
|
+
const find = (sl: string, name: string) => {
|
|
159
|
+
for (const k of doc.skins) {
|
|
160
|
+
const g = k.attachments[sl]?.[name]?.geometry;
|
|
161
|
+
if (g !== undefined) return g;
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
};
|
|
165
|
+
let g = find(slot, attachment);
|
|
166
|
+
if (g?.kind === 'linkedmesh') g = find(g.slot, g.source);
|
|
167
|
+
if (g !== undefined && 'vertices' in g && g.vertices.weighted) {
|
|
168
|
+
const bones = new Set<string>();
|
|
169
|
+
for (const v of g.vertices.bindings) for (const b of v) bones.add(b.bone);
|
|
170
|
+
return [...bones];
|
|
171
|
+
}
|
|
172
|
+
return slotBone === undefined ? [] : [slotBone];
|
|
173
|
+
}
|
|
174
|
+
|
|
140
175
|
export interface MeshFolds {
|
|
141
176
|
slot: string;
|
|
142
177
|
attachment: string;
|
package/src/run.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `rig-pose retarget`, end to end: read, gate the rig as it is, retarget
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `rig-pose retarget`, end to end: read, gate the rig as it is, retarget (the
|
|
3
|
+
* switches and the range clamp included), gate it with the animation in,
|
|
4
|
+
* measure where the rig landed, read the folds (and which no switch covers),
|
|
5
|
+
* and write — only when every check passed.
|
|
5
6
|
*/
|
|
6
7
|
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
8
|
import { tmpdir } from 'node:os';
|
|
@@ -9,8 +10,9 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
9
10
|
import { readMap, resolveMap } from './map.ts';
|
|
10
11
|
import { convertCoco17, personTrack, readPoseSequence } from './pose.ts';
|
|
11
12
|
import { lineOf, Problems, Refusal, type Problem } from './problems.ts';
|
|
12
|
-
import { bendLine, flipLine, holdLine, retarget, type RetargetOptions } from './retarget.ts';
|
|
13
|
-
import { baseMotionOf, buildInto, folds, gate, readBuild, resolveRig, restOf } from './rigc.ts';
|
|
13
|
+
import { bendLine, endOnLine, flipLine, holdLine, rangeLine, retarget, shortLine, turnLine, type RetargetOptions } from './retarget.ts';
|
|
14
|
+
import { attachmentBones, baseMotionOf, buildInto, folds, gate, readBuild, resolveRig, restOf, slotsOf } from './rigc.ts';
|
|
15
|
+
import { artRangeLine, chainsMoving, switchLine, uncoveredFolds } from './switch.ts';
|
|
14
16
|
import { landed, measureLanding, measurePosture, poseFrames, postureLines, ANGLE_TOLERANCE, ORIGIN_TOLERANCE } from './verify.ts';
|
|
15
17
|
|
|
16
18
|
export interface RetargetArgs extends RetargetOptions {
|
|
@@ -74,15 +76,29 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
74
76
|
if (!g0.green) {
|
|
75
77
|
refuse([{ code: 'RIG_GATE_BASE', text: `${input.rig} with ${input.motion ?? 'no motion spec'} is red under rigc build --profile ${a.profile} before any animation is added (exit ${g0.run.status})` }, ...g0.fails.map((f) => ({ code: 'RIGC', text: f }))]);
|
|
76
78
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
+
const doc0 = readBuild(g0.build);
|
|
80
|
+
const rest = restOf(doc0);
|
|
81
|
+
const slots = slotsOf(doc0);
|
|
82
|
+
resolveMap(map, rest, a.map, slots);
|
|
79
83
|
|
|
80
|
-
const r = retarget(rest, map, frames, times, a);
|
|
81
|
-
say(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, smooth ${a.smooth}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, bends ${a.bends} (tolerance ${a.bendTolerance}°)`);
|
|
84
|
+
const r = retarget(rest, map, frames, times, a, slots);
|
|
85
|
+
say(`rig-pose retarget: ${seq.frames.length} frame(s), ${r.animation.duration.toFixed(3)}s, onto "${input.name}" as "${a.name}" — ${r.animation.tracks.length} track(s); min-score ${a.minScore}, smooth ${a.smooth}, flip ${a.flip === 'off' ? 'off' : `${a.flip}°`}, short ${a.short}, bends ${a.bends} (tolerance ${a.bendTolerance}°)`);
|
|
82
86
|
if (r.scale !== null) say(` SCALE ${r.scale.factor.toFixed(5)} rig units per picture px (rig ${r.scale.rig.toFixed(2)} / pose median ${r.scale.pose.toFixed(2)})`);
|
|
83
87
|
for (const h of r.holds) say(` HOLD ${holdLine(h, r.times)}`);
|
|
88
|
+
if (a.short !== 'off') {
|
|
89
|
+
for (const s of r.short) say(` SHORT ${shortLine(s, r.times, a.short)}`);
|
|
90
|
+
for (const e of r.endOn) say(` END-ON ${endOnLine(e, r.times, a.short)}`);
|
|
91
|
+
}
|
|
84
92
|
if (a.flip !== 'off') for (const fl of r.flips) say(` FLIP ${flipLine(fl, r.times, a.flip)}`);
|
|
85
93
|
for (const e of r.bends) say(` BEND ${bendLine(e)} — clamped straight`);
|
|
94
|
+
for (const e of r.turns) say(` TURN ${turnLine(e, r.times)}`);
|
|
95
|
+
for (const u of r.turnsUnread) say(` TURN line "${u.line}" (${u.joints[0]} → ${u.joints[1]}) not measured: ${u.why}`);
|
|
96
|
+
if (map.alternates.length > 0) {
|
|
97
|
+
if (r.switches.length === 0) say(` SWITCH none — no set within ${a.switchTolerance}° of the pose for ${a.switchMin}s`);
|
|
98
|
+
for (const s of r.switches) say(` SWITCH ${switchLine(s, r.times)}`);
|
|
99
|
+
}
|
|
100
|
+
for (const e of r.range) say(` RANGE ${rangeLine(e, r.times)}`);
|
|
101
|
+
for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
|
|
86
102
|
|
|
87
103
|
const motion = { ...base, animations: { ...animations, [a.name]: r.animation } };
|
|
88
104
|
const g1 = gate(input.rig, motion, join(scratch, 'with'), a.profile);
|
|
@@ -115,6 +131,10 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
115
131
|
else if (f.meshes === null) say(` FOLD rigc render --folds did not read (exit ${f.run.status}); folds not measured`);
|
|
116
132
|
else if (f.meshes.length === 0) say(` FOLD rigc render --folds at ${fps} fps: no mesh folds`);
|
|
117
133
|
else for (const m of f.meshes) say(` FOLD mesh "${m.attachment}" (slot "${m.slot}") folds on ${m.times.length} frame(s) at ${fps} fps: t=${spans(m.times, 1 / fps)}`);
|
|
134
|
+
const uncovered = f.meshes === null ? null : uncoveredFolds(f.meshes, r.shown, r.times, slots, (slot, att) => chainsMoving(attachmentBones(doc, slot, att), rest, map));
|
|
135
|
+
if (uncovered !== null && map.alternates.length > 0) {
|
|
136
|
+
for (const u of uncovered) say(` UNCOVERED mesh "${u.attachment}" (slot "${u.slot}"; chains ${u.chains.length === 0 ? 'none' : u.chains.join(', ')}) folds with no switch covering it on ${u.times.length} frame(s): t=${spans(u.times, 1 / fps)}`);
|
|
137
|
+
}
|
|
118
138
|
|
|
119
139
|
if (a.build !== undefined) {
|
|
120
140
|
// Built again where it will live, not copied: rigc's page paths are relative to the build (#18).
|
|
@@ -134,15 +154,22 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
134
154
|
animation: a.name,
|
|
135
155
|
frames: seq.frames.length,
|
|
136
156
|
duration: r.animation.duration,
|
|
137
|
-
options: { minScore: a.minScore, smooth: a.smooth, flip: a.flip, bends: a.bends, bendTolerance: a.bendTolerance, profile: a.profile },
|
|
157
|
+
options: { minScore: a.minScore, smooth: a.smooth, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, profile: a.profile },
|
|
138
158
|
scale: r.scale,
|
|
139
159
|
holds: r.holds,
|
|
160
|
+
short: r.short,
|
|
161
|
+
endOn: r.endOn,
|
|
140
162
|
bends: r.bends,
|
|
141
163
|
flips: r.flips,
|
|
142
164
|
gate: g1.summary,
|
|
143
165
|
landings,
|
|
144
166
|
posture,
|
|
145
167
|
folds: f.meshes,
|
|
168
|
+
switches: r.switches,
|
|
169
|
+
uncovered,
|
|
170
|
+
turns: r.turns,
|
|
171
|
+
range: r.range,
|
|
172
|
+
artRange: r.artRange,
|
|
146
173
|
}, null, 2)}\n`);
|
|
147
174
|
}
|
|
148
175
|
say(` wrote ${a.out} — animations: ${Object.keys(motion.animations).join(', ')}`);
|