@rig-c/pose 0.6.0 → 0.7.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 +9 -3
- package/cli.ts +8 -1
- package/package.json +1 -1
- package/src/retarget.ts +2 -1
- package/src/run.ts +1 -1
- package/src/switch.ts +151 -8
package/README.md
CHANGED
|
@@ -45,15 +45,15 @@ and what the run cannot decide for you it lists:
|
|
|
45
45
|
- `BEND` — with `--bends clamp`, a hinge bent the wrong way is straightened on that frame instead of refusing.
|
|
46
46
|
- `FOLD` — `rigc render --folds` over the new animation: every mesh that folds, and when. A reading, not a gate.
|
|
47
47
|
- `POSTURE` — when the map has a pelvis chain (joints `r_hip`, `l_hip`) and a `translate`: the hip line's tilt range, and the sway — the x-range of mid_hip − mid_shoulder — in rig units and in shoulder widths, the pose's beside the posed rig's. A reading, not a gate.
|
|
48
|
-
- `SWITCH` — with `alternates` in the map ([below](#alternate-drawings-alternates)): each span on which a set of alternate drawings shows, its slots, its frames, how far the pose was from the set's key skeleton,
|
|
48
|
+
- `SWITCH` — with `alternates` in the map ([below](#alternate-drawings-alternates)): each span on which a set of alternate drawings shows, its slots, its frames, how far the pose was from the set's key skeleton, how far each bone with a `drawn` angle was turned from it, and where each of its boundaries was moved to (`moved`, [below](#alternate-drawings-alternates)).
|
|
49
49
|
- `UNCOVERED` — with `alternates` in the map: every fold on a frame where no switch took the folding mesh off screen. This is the list of poses still to paint.
|
|
50
50
|
- `RANGE` — a chain's `range` ([below](#a-joints-range-range)): a bone whose rotate key the pose asks past its range, outside every span of a set depicting its chain, is clamped at the limit; one line per run, with the frames, the seconds and the largest overflow. The demand for an alternate.
|
|
51
51
|
- `ART-RANGE` — inside a set's span: a bone with a `drawn` angle in the set, turned from it past its limits — the set's `turn` for that bone (held at the limit) or, without one, its chain's `range` (the bone follows the pose). Listed as the pose asked it either way: the demand for another drawing of that set.
|
|
52
52
|
- `TURN` — a line across the body (hips, shoulders, eyes) shorter in the picture than 0.5 of its median length, or reversed, for at least 0.15 s: the body or the head turned. A reading of the pose, made whether or not the map uses the line; it changes no key. The demand for a turned-body drawing, as `END-ON` is for a foreshortened limb.
|
|
53
53
|
|
|
54
|
-
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance` and `
|
|
54
|
+
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance`, `switchMin` and `switchAt` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate`, `landings`, `posture`, `folds`, and the demand lists:
|
|
55
55
|
|
|
56
|
-
- `switches: [{ set, chains, slots, hides, offsets, from, to, frames: [first, last], distance: { max, median }, artTurn: { <bone>: { max, median } } }]` — one per span shown; `from` is the first frame's time, `to` the time the base art comes back (the animation's end when the span runs to it), `distance` in degrees over the frames shown, `artTurn` |wrap(target − drawn)| in degrees per bone the set has a `drawn` angle for.
|
|
56
|
+
- `switches: [{ set, chains, slots, hides, offsets, from, to, frames: [first, last], distance: { max, median }, artTurn: { <bone>: { max, median } }, moved: { on, off } }]` — one per span shown; `from` is the first frame's time, `to` the time the base art comes back (the animation's end when the span runs to it), `distance` in degrees over the frames shown, `artTurn` |wrap(target − drawn)| in degrees per bone the set has a `drawn` angle for. `moved.on` is the boundary the span starts on (null from the first frame), `moved.off` the one the base art or the next set comes back on (null to the end), each `{ frame: [selected, keyed], frames, seconds, window: [first, last], speed: { old, new } }`: signed frames and seconds, the frames it could move to, and the speed in degrees per frame on the old and the new frame. A boundary between two sets is the same entry in both.
|
|
57
57
|
- `uncovered: [{ slot, attachment, chains, times }]` — every entry of `folds` on the times its slot still shows that mesh, with the map's chains that move it (the deepest: the arm, not the trunk it hangs from). `rigc render --folds` reads only what the pose shows, so a mesh swapped out or hidden by a switch is not read on those frames at all; `uncovered` is the fold times less the switched ones.
|
|
58
58
|
- `endOn` — above, under the short hold.
|
|
59
59
|
- `turns: [{ line, joints, frames, seconds, minFraction, reversed }]` — `line` is `hip`, `shoulder` or `eye`.
|
|
@@ -167,6 +167,12 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
|
|
|
167
167
|
|
|
168
168
|
**When one shows.** Per frame and per set, the distance is the largest difference, in degrees, between a segment of its chains as the retarget asked for it (after holds, smoothing, the short hold, the flip guard and the hinge check; before any range clamp) and the same segment in its key skeleton. On each frame the nearest set within `--switch-tolerance` (default 25°) shows, ties going to the map's order; a run of one set shorter than `--switch-min` (default 0.25 s) is dropped and the base art shows there. Both figures were fixed before a run ([#6](https://github.com/rig-c/pose/issues/6)). Sets compete only with those they share a slot (shown or hidden) or an offset bone with, so a drawing of each arm can show at once; two that compete never overlap.
|
|
169
169
|
|
|
170
|
+
**Where a switch is keyed: `--switch-at`.** By default (`first`) a boundary — the frame a set starts showing on, and the frame the base art or the next set comes back on — is where the selection above puts it: the first frame within the tolerance, often a frame where the arm is slowing into the pose, so the change of drawing can be seen on a nearly still arm. `--switch-at peak` moves each boundary to the frame within ±`--switch-min`/2 (±3 frames at 24 fps with the default 0.25 s) where the chains it changes turn fastest, so the cut lands in motion ([#31](https://github.com/rig-c/pose/issues/31)). The speed on a frame is the largest |angular speed| in degrees per frame, a central difference of the target world angles (one-sided on the first and last frame), over every segment of the leaving set's and the arriving set's chains; it is read on the same targets as the distance, before the range clamp and any set's `turn`, which depend on where the spans are. The window is clipped so every span keeps `--switch-min`, a gap between two spans of one set keeps it too (the bridging holds), no boundary reaches its neighbour, and a set with `frames` shows only inside them. In it, a frame qualifies only if the set the move newly shows is within **2 × `--switch-tolerance`** of its key skeleton on every frame it newly shows it on; the boundary goes to the fastest qualifying frame, and stays when none qualifies. A tie goes to the frame nearest the original, then the earlier. A boundary between two sets moves once.
|
|
171
|
+
|
|
172
|
+
The bound exists because speed alone rewarded showing art far from its pose. Unbounded, on HQ's dancer, the peak sat mid-swing: the largest distance over the frames a set showed rose from 25° to 135° (K2), 24° to 64° (K4), 21° to 59° (K1) and 21° to 38° (K3), the sets' `turn` held the arms hard against the pose (`ART-RANGE` runs 24 → 36), and the rig snapped to a hands-low drawing two frames before the arms came down. Bounded, every set stays within 50° (the largest is 43°, K4) and the median speed at the boundaries still rises from 9.7 to 14.2°/frame, but `ART-RANGE` runs still rise, 24 → 31 (K4 1 → 3, K1 6 → 8, K2 6 → 7, K3 2 → 4). That is why `first` is the default and `peak` an option ([#31](https://github.com/rig-c/pose/issues/31), round 2).
|
|
173
|
+
|
|
174
|
+
Either way the `SWITCH` line ends with `moved on …, off …`: each boundary's old and new frame (the same under `first`), the frames and seconds it moved, the speed on both and its window, and the report carries the same under `moved`.
|
|
175
|
+
|
|
170
176
|
**What is written.** An `attachment` track per slot whose picture changes: the set's attachment (or `null` for a hidden slot) where a span starts, and the slot's setup attachment, as the compiled model states it, where it ends. A direct switch from one set to another puts the first set's slots back to setup on the second's first frame, so an extra slot that is empty at setup goes empty. A switch onto frame *i* is keyed halfway between frames *i*−1 and *i*: a stepped key on the frame's own time fires a frame late wherever a player's accumulated time lands just below it (rig-c's docs/AUTHORING.md §4.5), and halfway both the pose's times and a renderer's `i / fps` see it on frame *i*. Before the first key a slot shows its setup attachment, so no key is written at 0 unless a span starts there. Offsets are `translate` keys at the same times, `stepped`. rig-c's gate checks these keys like any other.
|
|
171
177
|
|
|
172
178
|
**TURN.** The cutoff was measured on the three lines across the body before it was fixed ([#6](https://github.com/rig-c/pose/issues/6)): on the 13 s front-facing dance their picture length never drops under 0.81 of its median and none reverses; on a second sequence the hip line falls to 0.17 at frames 465–491 (side-on) and the eye line to 0.22 at 26–58 (head in profile), and 3 frames reverse. 0.5 lies between. A line is reversed when it points more than 90° from the subject's right joint to the left (front-facing, it points at 0°). The lines are read after holds and smoothing, held and smoothed as the map's points are even where the map does not use them; a line whose points are good on no frame is named as not measured.
|
package/cli.ts
CHANGED
|
@@ -49,6 +49,10 @@ retarget
|
|
|
49
49
|
--switch-tolerance <d> a set of alternate drawings (the map's "alternates") may show on a frame where
|
|
50
50
|
every segment of its chains is within d degrees of its key skeleton (default: 25)
|
|
51
51
|
--switch-min <s> a run of one set shorter than s seconds is dropped (default: 0.25)
|
|
52
|
+
--switch-at first|peak where a switch is keyed: first (default) keeps the frame the selection gave it;
|
|
53
|
+
peak moves each boundary to the frame within ±switch-min/2 where the chains it
|
|
54
|
+
changes turn fastest, among those that newly show a set only on frames within
|
|
55
|
+
2 × the switch tolerance of its key skeleton (it stays when none qualifies)
|
|
52
56
|
--profile spine|spine-html rigc's gate profile (default: spine, rigc's own default)
|
|
53
57
|
--build <dir> keep the gated build (skeleton.json, .atlas, .model.json) here
|
|
54
58
|
--render <dir> keep rigc's render of the animation (frames, contact.png, folds.json) here
|
|
@@ -124,9 +128,11 @@ function shortOf(f: Map<string, string>): number | 'off' {
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
function retargetArgs(argv: string[]): RetargetArgs {
|
|
127
|
-
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'profile', 'build', 'render', 'report'], ['coco']);
|
|
131
|
+
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'render', 'report'], ['coco']);
|
|
128
132
|
const bends = f.get('bends') ?? 'refuse';
|
|
129
133
|
if (bends !== 'refuse' && bends !== 'clamp') throw new Usage(`--bends is "${bends}"; it must be refuse or clamp`);
|
|
134
|
+
const switchAt = f.get('switch-at') ?? 'first';
|
|
135
|
+
if (switchAt !== 'peak' && switchAt !== 'first') throw new Usage(`--switch-at is "${switchAt}"; it must be peak or first`);
|
|
130
136
|
const profile = f.get('profile') ?? 'spine';
|
|
131
137
|
if (profile !== 'spine' && profile !== 'spine-html') throw new Usage(`--profile is "${profile}"; it must be spine or spine-html`);
|
|
132
138
|
const name = f.get('name') ?? 'dance';
|
|
@@ -148,6 +154,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
|
|
|
148
154
|
bendTolerance: num(f, 'bend-tolerance', 0, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
|
|
149
155
|
switchTolerance: num(f, 'switch-tolerance', 25, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
|
|
150
156
|
switchMin: num(f, 'switch-min', 0.25, (n) => n >= 0, 'seconds ≥ 0'),
|
|
157
|
+
switchAt,
|
|
151
158
|
profile,
|
|
152
159
|
build: f.get('build'),
|
|
153
160
|
render: f.get('render'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/retarget.ts
CHANGED
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
*
|
|
42
42
|
* Then the switches (rig-c/pose#6, `switch.ts`): with every target known —
|
|
43
43
|
* reaches included — each alternate set's distance to the pose is read, and the
|
|
44
|
-
* spans each set shows are chosen
|
|
44
|
+
* spans each set shows are chosen, each boundary keyed at its window's speed
|
|
45
|
+
* peak (#31). Only then the range clamp: outside the spans
|
|
45
46
|
* of a set depicting its chain, a bone whose rotate key would leave its
|
|
46
47
|
* `range` is clamped at the limit (its target becomes the clamped world angle),
|
|
47
48
|
* in depth order, so a bone below takes the clamped parent into its own key
|
package/src/run.ts
CHANGED
|
@@ -154,7 +154,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
154
154
|
animation: a.name,
|
|
155
155
|
frames: seq.frames.length,
|
|
156
156
|
duration: r.animation.duration,
|
|
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 },
|
|
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, switchAt: a.switchAt, profile: a.profile },
|
|
158
158
|
scale: r.scale,
|
|
159
159
|
holds: r.holds,
|
|
160
160
|
short: r.short,
|
package/src/switch.ts
CHANGED
|
@@ -31,6 +31,26 @@
|
|
|
31
31
|
* time lands below it (rig-c docs/AUTHORING.md §4.5), and the pose's times and
|
|
32
32
|
* a renderer's `i / fps` are two grids that disagree by more than that.
|
|
33
33
|
* Halfway, both see it on frame i.
|
|
34
|
+
*
|
|
35
|
+
* Where a switch is keyed (rig-c/pose#31): a boundary — a frame where a set
|
|
36
|
+
* starts showing, or where the base art or the next set comes back — is moved,
|
|
37
|
+
* after the selection above, to the fastest frame within ±`switchMin`/2 of it,
|
|
38
|
+
* so the change of drawing lands in motion, not on an arm slowing into the
|
|
39
|
+
* pose. The speed on a frame is the largest |angular speed|, degrees per frame
|
|
40
|
+
* (central difference of the target world angles; one-sided on the first and
|
|
41
|
+
* last frame), over the segments of the chains the boundary changes, the
|
|
42
|
+
* leaving set's and the arriving set's; read on the same targets as the
|
|
43
|
+
* distance, before the range clamp and any set's `turn`, which depend on where
|
|
44
|
+
* the spans are. The window is clipped so every span keeps `switchMin` (a gap
|
|
45
|
+
* between two spans of one set too, so the bridging holds), no boundary
|
|
46
|
+
* reaches its neighbour, and a set with `frames` shows only inside them; and
|
|
47
|
+
* a frame qualifies only if the set the move newly shows is within twice the
|
|
48
|
+
* tolerance of its key skeleton on every frame it newly shows it on (speed
|
|
49
|
+
* alone showed art up to 135° from its pose on the dancer). Ties
|
|
50
|
+
* go to the frame nearest the original, then the earlier. A boundary between
|
|
51
|
+
* two sets moves once. `switchAt: 'first'` keeps every boundary where the
|
|
52
|
+
* selection put it; it is the CLI's default, because on HQ's dancer the bounded
|
|
53
|
+
* peak still raised the sets' ART-RANGE runs from 24 to 31 (#31, round 2).
|
|
34
54
|
*/
|
|
35
55
|
import type { AlternateSet, JointMap, Rest, RigSlots } from './map.ts';
|
|
36
56
|
import { descends } from './map.ts';
|
|
@@ -43,6 +63,21 @@ export interface SwitchOptions {
|
|
|
43
63
|
switchTolerance: number;
|
|
44
64
|
/** Seconds: a run of one set shorter than this is dropped. */
|
|
45
65
|
switchMin: number;
|
|
66
|
+
/** Where a boundary is keyed: 'first' (the CLI's default) keeps the frame the selection gave it; 'peak' moves it to the fastest qualifying frame of its window (#31). */
|
|
67
|
+
switchAt: 'peak' | 'first';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One boundary of a span: where the selection put it, where it is keyed, and how fast the chains it changes move on each. */
|
|
71
|
+
export interface Move {
|
|
72
|
+
/** The frame the boundary showed on as selected, then as keyed. */
|
|
73
|
+
frame: [number, number];
|
|
74
|
+
/** Signed: frames, and the seconds between the two frames' times. */
|
|
75
|
+
frames: number;
|
|
76
|
+
seconds: number;
|
|
77
|
+
/** The frames it could move to, after clipping: first and last. */
|
|
78
|
+
window: [number, number];
|
|
79
|
+
/** Degrees per frame on the old and the new frame: the largest |angular speed| over the segments of the chains it changes. */
|
|
80
|
+
speed: { old: number; new: number };
|
|
46
81
|
}
|
|
47
82
|
|
|
48
83
|
export interface Switch {
|
|
@@ -63,6 +98,8 @@ export interface Switch {
|
|
|
63
98
|
distance: { max: number; median: number };
|
|
64
99
|
/** Per bone the set has a `drawn` angle for: |wrap(target − drawn)| over the frames shown, degrees. */
|
|
65
100
|
artTurn: Record<string, { max: number; median: number }>;
|
|
101
|
+
/** The boundary it starts on (null from the first frame) and the one it ends on (null to the end); a boundary between two sets is in both. */
|
|
102
|
+
moved: { on: Move | null; off: Move | null };
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
export interface AttachmentKey {
|
|
@@ -169,6 +206,89 @@ function competing(sets: readonly AlternateSet[]): AlternateSet[][] {
|
|
|
169
206
|
return [...groups.values()];
|
|
170
207
|
}
|
|
171
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Per frame, the largest |angular speed| over the given bones' target world
|
|
211
|
+
* angles, degrees per frame: a central difference, one-sided on the first and
|
|
212
|
+
* the last frame.
|
|
213
|
+
*/
|
|
214
|
+
export function speeds(bones: readonly string[], targets: ReadonlyMap<string, readonly number[]>, n: number): number[] {
|
|
215
|
+
const out: number[] = [];
|
|
216
|
+
for (let k = 0; k < n; k++) {
|
|
217
|
+
const lo = Math.max(0, k - 1);
|
|
218
|
+
const hi = Math.min(n - 1, k + 1);
|
|
219
|
+
let v = 0;
|
|
220
|
+
if (hi > lo) for (const b of bones) v = Math.max(v, Math.abs(wrap(targets.get(b)![hi]! - targets.get(b)![lo]!)) / (hi - lo));
|
|
221
|
+
out.push(v);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** A moved boundary may newly show a set only on frames within this many times the tolerance of its key skeleton (#31). */
|
|
227
|
+
export const MOVE_BOUND = 2;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Move each boundary of one group's segments to the fastest frame of its
|
|
231
|
+
* window (`switchAt: 'peak'`), in time order, each clipped against its
|
|
232
|
+
* neighbours as they stand; `vals[s]` is what segment s shows (null: the base
|
|
233
|
+
* art) and `pos[m]` the frame boundary m shows on, between segments m and m+1.
|
|
234
|
+
* A candidate frame qualifies only if every frame the move newly shows a set
|
|
235
|
+
* on is within `MOVE_BOUND` × the tolerance of it; none qualifying, the
|
|
236
|
+
* boundary stays. Returns one `Move` per boundary.
|
|
237
|
+
*/
|
|
238
|
+
function moveBoundaries(vals: ReadonlyArray<AlternateSet | null>, pos: readonly number[], map: JointMap, targets: ReadonlyMap<string, readonly number[]>, distances: ReadonlyMap<string, readonly number[]>, times: readonly number[], duration: number, opts: SwitchOptions): Move[] {
|
|
239
|
+
const n = times.length;
|
|
240
|
+
const at = [...pos];
|
|
241
|
+
const end = (b: number) => (b < n ? times[b]! : duration);
|
|
242
|
+
const len = (a: number, b: number) => end(b) - times[a]!;
|
|
243
|
+
/** What a segment must keep: the minimum span for a set's, and for a gap between two spans of one set (the bridging); never less than it had. */
|
|
244
|
+
const need = (s: number, had: number): number => {
|
|
245
|
+
const v = vals[s];
|
|
246
|
+
const kept = v !== null || (s > 0 && s + 1 < vals.length && vals[s - 1] !== null && vals[s - 1] === vals[s + 1]);
|
|
247
|
+
return kept ? Math.min(opts.switchMin, had) : 0;
|
|
248
|
+
};
|
|
249
|
+
const allowed = (a: AlternateSet | null, k: number) => a === null || a.frames === null || a.frames.some(([f, l]) => k >= f && k <= l);
|
|
250
|
+
const bonesOf = (a: AlternateSet | null): string[] => (a === null ? [] : a.chains.flatMap((cn) => map.chains.find((c) => c.name === cn)!.bones));
|
|
251
|
+
const moves: Move[] = [];
|
|
252
|
+
for (let m = 0; m < at.length; m++) {
|
|
253
|
+
const b = at[m]!;
|
|
254
|
+
const prev = m === 0 ? 0 : at[m - 1]!;
|
|
255
|
+
const next = m + 1 < at.length ? at[m + 1]! : n;
|
|
256
|
+
const leaving = vals[m] ?? null;
|
|
257
|
+
const arriving = vals[m + 1] ?? null;
|
|
258
|
+
const needPrev = need(m, len(prev, b));
|
|
259
|
+
const needNext = need(m + 1, len(b, next));
|
|
260
|
+
// The pose's times are read to the microsecond, so a frame exactly half a minimum span away may read a hair past it.
|
|
261
|
+
const half = opts.switchMin / 2 + 1e-6;
|
|
262
|
+
// Earlier: frames b' … b−1 show the arriving segment.
|
|
263
|
+
let lo = b;
|
|
264
|
+
while (lo - 1 > prev && times[b]! - times[lo - 1]! <= half && len(prev, lo - 1) >= needPrev - 1e-9 && allowed(arriving, lo - 1)) lo--;
|
|
265
|
+
// Later: frames b … b'−1 show the leaving one.
|
|
266
|
+
let hi = b;
|
|
267
|
+
while (hi + 1 < next && times[hi + 1]! - times[b]! <= half && len(hi + 1, next) >= needNext - 1e-9 && allowed(leaving, hi)) hi++;
|
|
268
|
+
const sp = speeds([...new Set([...bonesOf(leaving), ...bonesOf(arriving)])], targets, n);
|
|
269
|
+
// A frame qualifies only if the set it newly shows is within twice the tolerance of its key skeleton on every
|
|
270
|
+
// frame it newly shows it on (round 1 on the dancer: speed alone showed a set up to 135° from its pose).
|
|
271
|
+
const near = (a: AlternateSet | null, from: number, to: number): boolean => {
|
|
272
|
+
if (a === null) return true;
|
|
273
|
+
const d = distances.get(a.name)!;
|
|
274
|
+
for (let k = from; k < to; k++) if (d[k]! > MOVE_BOUND * opts.switchTolerance) return false;
|
|
275
|
+
return true;
|
|
276
|
+
};
|
|
277
|
+
const qualifies = (k: number) => (k < b ? near(arriving, k, b) : near(leaving, b, k));
|
|
278
|
+
let best = b;
|
|
279
|
+
if (opts.switchAt === 'peak') {
|
|
280
|
+
for (let k = lo; k <= hi; k++) {
|
|
281
|
+
if (!qualifies(k)) continue;
|
|
282
|
+
const d = sp[k]! - sp[best]!;
|
|
283
|
+
if (d > 1e-9 || (Math.abs(d) <= 1e-9 && (Math.abs(k - b) < Math.abs(best - b) || (Math.abs(k - b) === Math.abs(best - b) && k < best)))) best = k;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
at[m] = best;
|
|
287
|
+
moves.push({ frame: [b, best], frames: best - b, seconds: r6(times[best]! - times[b]!), window: [lo, hi], speed: { old: r3(sp[b]!), new: r3(sp[best]!) } });
|
|
288
|
+
}
|
|
289
|
+
return moves;
|
|
290
|
+
}
|
|
291
|
+
|
|
172
292
|
/** The key time for a change that shows on frame i: half a frame before it, 0 on the first frame. */
|
|
173
293
|
const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 : r6((times[i - 1]! + times[i]!) / 2));
|
|
174
294
|
|
|
@@ -220,20 +340,34 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
220
340
|
}
|
|
221
341
|
i = j + 1;
|
|
222
342
|
}
|
|
343
|
+
// A run shorter than the minimum span shows the base art.
|
|
223
344
|
for (let i = 0; i < n;) {
|
|
224
345
|
const a = pick[i];
|
|
225
346
|
let j = i;
|
|
226
347
|
while (j + 1 < n && pick[j + 1] === a) j++;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const to = j + 1 < n ? times[j + 1]! : duration;
|
|
230
|
-
if (to - from >= opts.switchMin - 1e-9) {
|
|
231
|
-
const ds = distances.get(a.name)!.slice(i, j + 1);
|
|
232
|
-
switches.push({ set: a.name, chains: a.chains, slots: a.slots, hides: a.hides, offsets: a.offsets, from, to, frames: [i, j], distance: { max: r3(Math.max(...ds)), median: r3(median(ds)) }, artTurn: {} });
|
|
233
|
-
}
|
|
234
|
-
}
|
|
348
|
+
const to = j + 1 < n ? times[j + 1]! : duration;
|
|
349
|
+
if (a !== null && a !== undefined && to - times[i]! < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = null;
|
|
235
350
|
i = j + 1;
|
|
236
351
|
}
|
|
352
|
+
// The segments between boundaries, then each boundary moved (#31).
|
|
353
|
+
const vals: Array<AlternateSet | null> = [pick[0] ?? null];
|
|
354
|
+
const pos: number[] = [];
|
|
355
|
+
for (let i = 1; i < n; i++) {
|
|
356
|
+
if (pick[i] === pick[i - 1]) continue;
|
|
357
|
+
pos.push(i);
|
|
358
|
+
vals.push(pick[i] ?? null);
|
|
359
|
+
}
|
|
360
|
+
const moves = moveBoundaries(vals, pos, map, targets, distances, times, duration, opts);
|
|
361
|
+
for (let s = 0; s < vals.length; s++) {
|
|
362
|
+
const a = vals[s];
|
|
363
|
+
if (a === null || a === undefined) continue;
|
|
364
|
+
const i = s === 0 ? 0 : moves[s - 1]!.frame[1];
|
|
365
|
+
const j = s < pos.length ? moves[s]!.frame[1] - 1 : n - 1;
|
|
366
|
+
const from = times[i]!;
|
|
367
|
+
const to = j + 1 < n ? times[j + 1]! : duration;
|
|
368
|
+
const ds = distances.get(a.name)!.slice(i, j + 1);
|
|
369
|
+
switches.push({ set: a.name, chains: a.chains, slots: a.slots, hides: a.hides, offsets: a.offsets, from, to, frames: [i, j], distance: { max: r3(Math.max(...ds)), median: r3(median(ds)) }, artTurn: {}, moved: { on: s === 0 ? null : moves[s - 1]!, off: s < pos.length ? moves[s]! : null } });
|
|
370
|
+
}
|
|
237
371
|
}
|
|
238
372
|
const order = (name: string) => map.alternates.findIndex((a) => a.name === name);
|
|
239
373
|
switches.sort((x, y) => x.frames[0] - y.frames[0] || order(x.set) - order(y.set));
|
|
@@ -433,9 +567,18 @@ export function switchLine(s: Switch, times: readonly number[]): string {
|
|
|
433
567
|
if (offs.length > 0) parts.push(`offsets ${offs.map(([b, o]) => `"${b}" (${o[0]}, ${o[1]})`).join(', ')}`);
|
|
434
568
|
const art = Object.entries(s.artTurn);
|
|
435
569
|
if (art.length > 0) parts.push(`art turn ${art.map(([b, a]) => `"${b}" max ${deg(a.max)}, median ${deg(a.median)}`).join('; ')}`);
|
|
570
|
+
const moves = (['on', 'off'] as const).filter((k) => s.moved[k] !== null).map((k) => `${k} ${moveText(s.moved[k]!)}`);
|
|
571
|
+
if (moves.length > 0) parts.push(`moved ${moves.join(', ')}`);
|
|
436
572
|
return parts.join('; ');
|
|
437
573
|
}
|
|
438
574
|
|
|
575
|
+
const spd = (v: number) => `${v.toFixed(1)}°/frame`;
|
|
576
|
+
const win = (w: [number, number]) => (w[0] === w[1] ? `window frame ${w[0]}` : `window frames ${w[0]}–${w[1]}`);
|
|
577
|
+
function moveText(m: Move): string {
|
|
578
|
+
if (m.frames === 0) return `frame ${m.frame[0]} stays (${spd(m.speed.old)}; ${win(m.window)})`;
|
|
579
|
+
return `frame ${m.frame[0]} → ${m.frame[1]} (${m.frames > 0 ? '+' : '−'}${Math.abs(m.frames)} frame${Math.abs(m.frames) === 1 ? '' : 's'}, ${m.seconds > 0 ? '+' : '−'}${Math.abs(m.seconds).toFixed(3)}s; ${spd(m.speed.old)} → ${spd(m.speed.new)}; ${win(m.window)})`;
|
|
580
|
+
}
|
|
581
|
+
|
|
439
582
|
export function artRangeLine(e: ArtRange, times: readonly number[]): string {
|
|
440
583
|
return `set "${e.set}" chain "${e.chain}" bone "${e.bone}" turned ${sg(e.turn)} from drawn (range [${e.limits[0]}, ${e.limits[1]}]) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s, up to ${deg(e.maxOverflow)} past it; ${e.clamped ? 'held at the limit' : 'the bone follows the pose'} — demand for another drawing of the set`;
|
|
441
584
|
}
|