@rig-c/pose 0.3.0 → 0.5.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/switch.ts ADDED
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Attachment switches: when the pose is in a pose the rig has an alternate set
3
+ * of drawings for, show that set. Pure — no file, no clock, no process.
4
+ *
5
+ * The metric, per frame and per set: the largest |wrap(pose − key)| over every
6
+ * segment of the set's chains, where "pose" is the segment's angle exactly as
7
+ * the retarget asked the bone for it (after holds, smoothing, the short hold,
8
+ * the flip guard and the hinge check; before any range clamp) and "key" is the
9
+ * same segment's angle in the set's key skeleton, both by `pictureAngle`. Per
10
+ * frame the candidate is the set with the smallest distance within the
11
+ * tolerance (ties: map order); a run of one candidate shorter than the minimum
12
+ * span is dropped, and the base art shows there; a gap shorter than the minimum
13
+ * span between two runs of one set is bridged by that set. No other hysteresis.
14
+ *
15
+ * Sets compete only with the sets they share a slot with (shown or hidden,
16
+ * transitively), or a bone they offset: two that touch nothing in common — a
17
+ * drawing of each arm — can show at once, and two that do never overlap, so
18
+ * each slot has one value per frame and one `attachment` track, and each
19
+ * offset bone one `translate` track.
20
+ *
21
+ * While a set shows, every slot in its `slots` shows that attachment and every
22
+ * slot in its `hides` is empty; everywhere else a slot shows its setup
23
+ * attachment, as the compiled model states it. A direct switch from one set to
24
+ * another therefore puts the first set's slots back to setup (an extra slot
25
+ * whose setup is empty goes empty) on the frame the second set shows.
26
+ *
27
+ * Keys are stepped (rig-c's attachment timeline has no curve; the offsets'
28
+ * `translate` keys are written `stepped`). A switch onto frame i is keyed half
29
+ * a frame before it, at the midpoint of frames i−1 and i: a stepped key that
30
+ * sits ON a frame's time fires a frame late wherever the sampler's accumulated
31
+ * time lands below it (rig-c docs/AUTHORING.md §4.5), and the pose's times and
32
+ * a renderer's `i / fps` are two grids that disagree by more than that.
33
+ * Halfway, both see it on frame i.
34
+ */
35
+ import type { AlternateSet, JointMap, Rest, RigSlots } from './map.ts';
36
+ import { descends } from './map.ts';
37
+ import type { MotionKey, MotionTrack } from './retarget.ts';
38
+ import { pictureAngle, wrap } from './retarget.ts';
39
+ import type { MeshFolds } from './rigc.ts';
40
+
41
+ export interface SwitchOptions {
42
+ /** Degrees: the largest segment difference at which a set may show. */
43
+ switchTolerance: number;
44
+ /** Seconds: a run of one set shorter than this is dropped. */
45
+ switchMin: number;
46
+ }
47
+
48
+ export interface Switch {
49
+ set: string;
50
+ chains: string[];
51
+ /** Slot → the attachment it shows over the span. */
52
+ slots: Record<string, string>;
53
+ /** Slots empty over the span. */
54
+ hides: string[];
55
+ /** Bone → the translate it holds over the span. */
56
+ offsets: Record<string, [number, number]>;
57
+ /** The frame time it shows from, and the time the base art comes back (the animation's end when it runs to the end). */
58
+ from: number;
59
+ to: number;
60
+ /** First and last frame index shown. */
61
+ frames: [number, number];
62
+ /** Over the frames shown, degrees. */
63
+ distance: { max: number; median: number };
64
+ /** Per bone the set has a `drawn` angle for: |wrap(target − drawn)| over the frames shown, degrees. */
65
+ artTurn: Record<string, { max: number; median: number }>;
66
+ }
67
+
68
+ export interface AttachmentKey {
69
+ t: number;
70
+ /** The attachment shown from `t`, or null for nothing. */
71
+ v: string | null;
72
+ }
73
+ export interface SlotTrack {
74
+ slot: string;
75
+ property: 'attachment';
76
+ keys: AttachmentKey[];
77
+ }
78
+
79
+ /** Inside a set's span, a bone turned from its drawn angle past its range: the demand for another drawing of that set. */
80
+ export interface ArtRange {
81
+ set: string;
82
+ chain: string;
83
+ bone: string;
84
+ /** First and last frame of the run (spans merged across a gap of one frame). */
85
+ frames: [number, number];
86
+ seconds: number;
87
+ /** Degrees past the range, the most on any frame of the run. */
88
+ maxOverflow: number;
89
+ /** The signed art turn, wrap(target − drawn), on that frame. */
90
+ turn: number;
91
+ /** The bone's range, degrees. */
92
+ limits: [number, number];
93
+ /** Whether the bone was held at the limit (the set's `turn`) or followed the pose (its chain's `range` only). */
94
+ clamped: boolean;
95
+ }
96
+
97
+ export interface Uncovered {
98
+ slot: string;
99
+ attachment: string;
100
+ /** The map's chains that move the mesh (the deepest of them). */
101
+ chains: string[];
102
+ /** The fold times, seconds, on which the slot still shows this mesh. */
103
+ times: number[];
104
+ }
105
+
106
+ export interface Switches {
107
+ switches: Switch[];
108
+ /** The `attachment` tracks, one per slot whose picture changes. */
109
+ slotTracks: SlotTrack[];
110
+ /** The offsets' `translate` tracks, one per bone a shown set offsets. */
111
+ offsetTracks: MotionTrack[];
112
+ /** Per switched or hidden slot, what it shows on each frame (null: nothing). */
113
+ shown: Map<string, Array<string | null>>;
114
+ /** Per set, its distance on each frame, degrees. */
115
+ distances: Map<string, number[]>;
116
+ }
117
+
118
+ /** Per set, its distance to the pose on every frame: the largest segment difference over its chains. */
119
+ export function setDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, number[]> {
120
+ const out = new Map<string, number[]>();
121
+ for (const a of map.alternates) {
122
+ const segs: Array<{ asked: readonly number[]; key: number }> = [];
123
+ for (const cn of a.chains) {
124
+ const c = map.chains.find((x) => x.name === cn)!;
125
+ c.bones.forEach((b, k) => segs.push({ asked: targets.get(b)!, key: pictureAngle(a.pose[c.joints[k]!]!, a.pose[c.joints[k + 1]!]!) }));
126
+ }
127
+ const d: number[] = [];
128
+ for (let i = 0; i < n; i++) d.push(Math.max(...segs.map((s) => Math.abs(wrap(s.asked[i]! - s.key)))));
129
+ out.set(a.name, d);
130
+ }
131
+ return out;
132
+ }
133
+
134
+ export function median(xs: readonly number[]): number {
135
+ const s = [...xs].sort((a, b) => a - b);
136
+ const m = s.length >> 1;
137
+ return s.length % 2 === 1 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
138
+ }
139
+
140
+ const r6 = (v: number) => Math.round(v * 1e6) / 1e6;
141
+ const r3 = (v: number) => {
142
+ const r = Math.round(v * 1000) / 1000;
143
+ return Object.is(r, -0) ? 0 : r;
144
+ };
145
+
146
+ /** What a set switches: its slots shown or hidden, and the bones it offsets — each a value that has one track. */
147
+ function touches(a: AlternateSet): string[] {
148
+ return [...Object.keys(a.slots), ...a.hides].map((s) => `slot:${s}`).concat(Object.keys(a.offsets).map((b) => `bone:${b}`));
149
+ }
150
+
151
+ /** Groups of sets that switch a common slot or offset a common bone, transitively; each in map order. */
152
+ function competing(sets: readonly AlternateSet[]): AlternateSet[][] {
153
+ const parent = sets.map((_, i) => i);
154
+ const find = (i: number): number => (parent[i] === i ? i : (parent[i] = find(parent[i]!)));
155
+ const owner = new Map<string, number>();
156
+ sets.forEach((a, i) => {
157
+ for (const s of touches(a)) {
158
+ const j = owner.get(s);
159
+ if (j === undefined) owner.set(s, i);
160
+ else parent[find(i)] = find(j);
161
+ }
162
+ });
163
+ const groups = new Map<number, AlternateSet[]>();
164
+ sets.forEach((a, i) => {
165
+ const g = find(i);
166
+ if (!groups.has(g)) groups.set(g, []);
167
+ groups.get(g)!.push(a);
168
+ });
169
+ return [...groups.values()];
170
+ }
171
+
172
+ /** The key time for a change that shows on frame i: half a frame before it, 0 on the first frame. */
173
+ const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 : r6((times[i - 1]! + times[i]!) / 2));
174
+
175
+ /**
176
+ * Select the switches, and key them: per frame and per group of competing
177
+ * sets, the nearest within the tolerance; runs shorter than the minimum span
178
+ * dropped; one `attachment` track per slot whose picture changes, back to the
179
+ * slot's setup attachment at each end; one stepped `translate` track per bone
180
+ * a shown set offsets, 0 outside its spans.
181
+ */
182
+ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, times: readonly number[], duration: number, slots: RigSlots, opts: SwitchOptions): Switches {
183
+ const n = times.length;
184
+ const distances = setDistances(map, targets, n);
185
+ const switches: Switch[] = [];
186
+ for (const group of competing(map.alternates)) {
187
+ const pick: Array<AlternateSet | null> = [];
188
+ for (let i = 0; i < n; i++) {
189
+ let best: AlternateSet | null = null;
190
+ let bestD = Infinity;
191
+ for (const a of group) {
192
+ const d = distances.get(a.name)![i]!;
193
+ if (d <= opts.switchTolerance && d < bestD) {
194
+ best = a;
195
+ bestD = d;
196
+ }
197
+ }
198
+ pick.push(best);
199
+ }
200
+ // A gap shorter than the minimum span between two runs of one set is that set
201
+ // too: the base art flashing for a frame between them is the flicker the
202
+ // minimum span exists to prevent (measured on the dancer: one-frame breaks at
203
+ // a blurred frame inside a span of one set).
204
+ for (let i = 0; i < n;) {
205
+ const a = pick[i];
206
+ let j = i;
207
+ while (j + 1 < n && pick[j + 1] === a) j++;
208
+ const before = i > 0 ? pick[i - 1] : null;
209
+ const after = j + 1 < n ? pick[j + 1] : null;
210
+ if (before !== null && before !== undefined && before === after && a !== before) {
211
+ const gap = times[j + 1]! - times[i]!;
212
+ if (gap < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = before;
213
+ }
214
+ i = j + 1;
215
+ }
216
+ for (let i = 0; i < n;) {
217
+ const a = pick[i];
218
+ let j = i;
219
+ while (j + 1 < n && pick[j + 1] === a) j++;
220
+ if (a !== null && a !== undefined) {
221
+ const from = times[i]!;
222
+ const to = j + 1 < n ? times[j + 1]! : duration;
223
+ if (to - from >= opts.switchMin - 1e-9) {
224
+ const ds = distances.get(a.name)!.slice(i, j + 1);
225
+ 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: {} });
226
+ }
227
+ }
228
+ i = j + 1;
229
+ }
230
+ }
231
+ const order = (name: string) => map.alternates.findIndex((a) => a.name === name);
232
+ switches.sort((x, y) => x.frames[0] - y.frames[0] || order(x.set) - order(y.set));
233
+
234
+ // What each touched slot shows per frame, then a key wherever that changes.
235
+ const slotOrder: string[] = [];
236
+ const boneOrder: string[] = [];
237
+ for (const a of map.alternates) {
238
+ for (const s of [...Object.keys(a.slots), ...a.hides]) if (!slotOrder.includes(s)) slotOrder.push(s);
239
+ for (const b of Object.keys(a.offsets)) if (!boneOrder.includes(b)) boneOrder.push(b);
240
+ }
241
+ const shown = new Map<string, Array<string | null>>();
242
+ const moved = new Map<string, Array<[number, number]>>();
243
+ for (const sw of switches) {
244
+ const values: Array<[string, string | null]> = [...Object.entries(sw.slots), ...sw.hides.map((h): [string, null] => [h, null])];
245
+ for (const [s, v] of values) {
246
+ if (!shown.has(s)) shown.set(s, Array.from({ length: n }, () => slots.get(s)!.setup));
247
+ const row = shown.get(s)!;
248
+ for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = v;
249
+ }
250
+ for (const [b, o] of Object.entries(sw.offsets)) {
251
+ if (!moved.has(b)) moved.set(b, Array.from({ length: n }, (): [number, number] => [0, 0]));
252
+ const row = moved.get(b)!;
253
+ for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = o;
254
+ }
255
+ }
256
+ const slotTracks: SlotTrack[] = [];
257
+ for (const s of slotOrder) {
258
+ const row = shown.get(s);
259
+ if (row === undefined) continue;
260
+ let prev = slots.get(s)!.setup;
261
+ const keys: AttachmentKey[] = [];
262
+ row.forEach((v, i) => {
263
+ if (v === prev) return;
264
+ keys.push({ t: changeAt(times, i), v });
265
+ prev = v;
266
+ });
267
+ if (keys.length > 0) slotTracks.push({ slot: s, property: 'attachment', keys });
268
+ }
269
+ const offsetTracks: MotionTrack[] = [];
270
+ for (const b of boneOrder) {
271
+ const row = moved.get(b);
272
+ if (row === undefined) continue;
273
+ const keys: MotionKey[] = [{ t: 0, v: [r3(row[0]![0]), r3(row[0]![1])] }];
274
+ row.forEach((v, i) => {
275
+ const prev = row[i - 1];
276
+ if (i === 0 || (prev![0] === v[0] && prev![1] === v[1])) return;
277
+ keys.push({ t: changeAt(times, i), v: [r3(v[0]), r3(v[1])] });
278
+ });
279
+ // Every key but the last holds until the next (a stepped curve on the last has nothing to ease to).
280
+ for (let k = 0; k + 1 < keys.length; k++) keys[k]!.ease = 'stepped';
281
+ offsetTracks.push({ bone: b, property: 'translate', keys });
282
+ }
283
+ return { switches, slotTracks, offsetTracks, shown, distances };
284
+ }
285
+
286
+ /** Per chain, the frames on which a switch of a set depicting it shows: its art was painted for that pose. */
287
+ export function coveredChains(switches: readonly Switch[], n: number): Map<string, boolean[]> {
288
+ const out = new Map<string, boolean[]>();
289
+ for (const sw of switches) {
290
+ for (const c of sw.chains) {
291
+ if (!out.has(c)) out.set(c, Array.from({ length: n }, () => false));
292
+ const row = out.get(c)!;
293
+ for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = true;
294
+ }
295
+ }
296
+ return out;
297
+ }
298
+
299
+ /** Runs of flagged frames, merged across a gap of `gap` frames or fewer. */
300
+ export function runsOf(flag: readonly boolean[], gap: number): Array<[number, number]> {
301
+ const out: Array<[number, number]> = [];
302
+ for (let i = 0; i < flag.length; i++) {
303
+ if (!flag[i]) continue;
304
+ let j = i;
305
+ while (j + 1 < flag.length && flag[j + 1]) j++;
306
+ const last = out[out.length - 1];
307
+ if (last !== undefined && i - last[1] - 1 <= gap) last[1] = j;
308
+ else out.push([i, j]);
309
+ i = j;
310
+ }
311
+ return out;
312
+ }
313
+
314
+ /**
315
+ * Degrees past a range that do not count: the writer rounds a rotate key to
316
+ * 0.001°, and the rest angles come from rig-c's float32 model (≈ 1e-5° here),
317
+ * so a key the pose puts exactly on a limit can read a hair past it.
318
+ */
319
+ export const RANGE_SLACK = 0.001;
320
+
321
+ /** How far a value is past [min, max], degrees; 0 inside. */
322
+ export const overflow = (v: number, [min, max]: readonly [number, number]): number => (v > max ? v - max : v < min ? min - v : 0);
323
+
324
+ /**
325
+ * The art turn of each switch, in place (`artTurn`), and the art-range demand:
326
+ * inside a span, per bone with a `drawn` angle in the set and limits — the
327
+ * set's `turn` for it, else its chain's `range` — every frame whose signed art
328
+ * turn wrap(target − drawn) falls outside them, in runs merged across a gap of
329
+ * `gap` frames. Measured on the targets as the pose asks them, before
330
+ * `clampArt` holds the set's `turn` limits.
331
+ */
332
+ export function measureArt(switches: Switch[], map: JointMap, targets: ReadonlyMap<string, readonly number[]>, endOf: (i: number) => number, times: readonly number[], gap: number): ArtRange[] {
333
+ const out: ArtRange[] = [];
334
+ for (const sw of switches) {
335
+ const set = map.alternates.find((a) => a.name === sw.set)!;
336
+ for (const [bone, drawn] of Object.entries(set.drawn)) {
337
+ const target = targets.get(bone)!;
338
+ const turns: number[] = [];
339
+ for (let i = sw.frames[0]; i <= sw.frames[1]; i++) turns.push(wrap(target[i]! - drawn));
340
+ const abs = turns.map(Math.abs);
341
+ sw.artTurn[bone] = { max: r3(Math.max(...abs)), median: r3(median(abs)) };
342
+ const chain = map.chains.find((c) => c.bones.includes(bone))!;
343
+ const limits = set.turn[bone] ?? chain.range[bone];
344
+ if (limits === undefined) continue;
345
+ const flag = turns.map((t) => overflow(t, limits) > RANGE_SLACK);
346
+ for (const [a, b] of runsOf(flag, gap)) {
347
+ let worst = a;
348
+ for (let k = a; k <= b; k++) if (overflow(turns[k]!, limits) > overflow(turns[worst]!, limits)) worst = k;
349
+ const first = sw.frames[0] + a;
350
+ const last = sw.frames[0] + b;
351
+ out.push({ set: sw.set, chain: chain.name, bone, frames: [first, last], seconds: r6(endOf(last) - times[first]!), maxOverflow: r3(overflow(turns[worst]!, limits)), turn: r3(turns[worst]!), limits, clamped: bone in set.turn });
352
+ }
353
+ }
354
+ }
355
+ return out;
356
+ }
357
+
358
+ /** The frame nearest a time: the one whose half-frame-either-side window holds it, as the switch keys are placed. */
359
+ function nearestFrame(times: readonly number[], t: number): number {
360
+ let lo = 0;
361
+ let hi = times.length - 1;
362
+ while (lo < hi) {
363
+ const mid = (lo + hi) >> 1;
364
+ if (times[mid + 1]! <= t) lo = mid + 1;
365
+ else if (times[mid]! >= t) hi = mid;
366
+ else return t - times[mid]! < times[mid + 1]! - t ? mid : mid + 1;
367
+ }
368
+ return lo;
369
+ }
370
+
371
+ /**
372
+ * The folds no switch covers: every fold time on which the slot still shows
373
+ * the folding attachment. A fold of a slot's base mesh while a set (or
374
+ * nothing) shows there is covered; a fold of the set's own mesh while it
375
+ * shows is not.
376
+ */
377
+ export function uncoveredFolds(meshes: readonly MeshFolds[], shown: ReadonlyMap<string, ReadonlyArray<string | null>>, times: readonly number[], slots: RigSlots, chainsOf: (slot: string, attachment: string) => string[]): Uncovered[] {
378
+ const out: Uncovered[] = [];
379
+ for (const m of meshes) {
380
+ const row = shown.get(m.slot);
381
+ const left = m.times.filter((t) => (row === undefined ? slots.get(m.slot)?.setup ?? null : row[nearestFrame(times, t)]) === m.attachment);
382
+ if (left.length > 0) out.push({ slot: m.slot, attachment: m.attachment, chains: chainsOf(m.slot, m.attachment), times: left });
383
+ }
384
+ return out;
385
+ }
386
+
387
+ /**
388
+ * The map's chains that move a set of bones: for each bone, the chain owning
389
+ * it or its nearest mapped ancestor, keeping only the deepest (a chain another
390
+ * listed chain hangs from is dropped — the arm, not the trunk it hangs from).
391
+ */
392
+ export function chainsMoving(bones: readonly string[], rest: Rest, map: JointMap): string[] {
393
+ const owner = new Map<string, string>();
394
+ for (const c of map.chains) for (const b of c.bones) owner.set(b, c.name);
395
+ const found = new Set<string>();
396
+ for (const bone of bones) {
397
+ for (let b: string | null = bone; b !== null; b = rest.get(b)?.parent ?? null) {
398
+ const c = owner.get(b);
399
+ if (c !== undefined) {
400
+ found.add(c);
401
+ break;
402
+ }
403
+ }
404
+ }
405
+ const chainOf = (name: string) => map.chains.find((c) => c.name === name)!;
406
+ return map.chains
407
+ .map((c) => c.name)
408
+ .filter((x) => found.has(x))
409
+ .filter((x) => ![...found].some((y) => y !== x && chainOf(x).bones.some((b) => descends(rest, chainOf(y).bones[0]!, b))));
410
+ }
411
+
412
+ const deg = (v: number) => `${v.toFixed(1)}°`;
413
+ const sg = (v: number) => `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)}°`;
414
+ const spanOf = (frames: [number, number], times: readonly number[]): string =>
415
+ 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)`;
416
+
417
+ export function switchLine(s: Switch, times: readonly number[]): string {
418
+ const span = s.frames[0] === s.frames[1] ? `frame ${s.frames[0]}` : `frames ${s.frames[0]}–${s.frames[1]}`;
419
+ const shows = Object.entries(s.slots).map(([slot, att]) => `"${slot}" = "${att}"`);
420
+ const parts = [
421
+ `set "${s.set}" — ${shows.length === 0 ? 'shows no slot' : `shows ${shows.join(', ')}`}${s.hides.length > 0 ? `, hides ${s.hides.map((h) => `"${h}"`).join(', ')}` : ''}`,
422
+ `chains ${s.chains.join(', ')}`,
423
+ `${span} (t=${times[s.frames[0]]!.toFixed(3)}s until ${s.to.toFixed(3)}s) — distance max ${deg(s.distance.max)}, median ${deg(s.distance.median)}`,
424
+ ];
425
+ const offs = Object.entries(s.offsets);
426
+ if (offs.length > 0) parts.push(`offsets ${offs.map(([b, o]) => `"${b}" (${o[0]}, ${o[1]})`).join(', ')}`);
427
+ const art = Object.entries(s.artTurn);
428
+ if (art.length > 0) parts.push(`art turn ${art.map(([b, a]) => `"${b}" max ${deg(a.max)}, median ${deg(a.median)}`).join('; ')}`);
429
+ return parts.join('; ');
430
+ }
431
+
432
+ export function artRangeLine(e: ArtRange, times: readonly number[]): string {
433
+ 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`;
434
+ }
435
+
436
+ /**
437
+ * Inside each span, a bone with a `turn` in the set is held within it: its
438
+ * target world angle is drawn + clamp(wrap(target − drawn), min, max). Each
439
+ * bone is clamped on its own world angle, and keys are differences of world
440
+ * angles, so a forearm under a clamped upper arm keeps its own target.
441
+ */
442
+ export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>): void {
443
+ for (const sw of switches) {
444
+ const set = map.alternates.find((a) => a.name === sw.set)!;
445
+ for (const [bone, limits] of Object.entries(set.turn)) {
446
+ const drawn = set.drawn[bone]!;
447
+ const target = targets.get(bone)!;
448
+ for (let i = sw.frames[0]; i <= sw.frames[1]; i++) {
449
+ const t = wrap(target[i]! - drawn);
450
+ const c = Math.min(Math.max(t, limits[0]), limits[1]);
451
+ if (Math.abs(c - t) > RANGE_SLACK) target[i] = wrap(drawn + c);
452
+ }
453
+ }
454
+ }
455
+ }