@rig-c/pose 0.8.0 → 0.9.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/verify.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * a guess about which rigs are safe; it is posed, and the distance between the
9
9
  * aim each bone reached and the aim the pose asked for is the verdict.
10
10
  */
11
- import { poseRawAnimation, type CompiledDocument, type RawBone } from 'rig-c/core';
11
+ import { poseRawAnimation, poseRawSetup, type CompiledDocument, type RawBone } from 'rig-c/core';
12
12
  import type { Body18 } from './joints.ts';
13
13
  import { aimAngle, pointAt, type JointMap } from './map.ts';
14
14
  import type { Joint } from './pose.ts';
@@ -17,10 +17,13 @@ import { pictureAngle, pointOn, wrap } from './retarget.ts';
17
17
 
18
18
  export interface Landing {
19
19
  bone: string;
20
- /** The largest |reached − asked| over the frames, degrees (rotate) or world units (translate). */
20
+ /** The largest |reached − asked| over the frames: degrees (an aim), world units (an origin, an ankle), or a fraction of the setup length (a scale). */
21
21
  worst: number;
22
22
  frame: number;
23
- kind: 'angle' | 'origin';
23
+ /** `scale`: the bone's world x axis against its setup length times the leg scale asked (#47); `foot`: the ankle (the leg's last bone's tip) against where the keys put it. */
24
+ kind: 'angle' | 'origin' | 'scale' | 'foot';
25
+ /** The tolerance it is held to. */
26
+ tolerance: number;
24
27
  }
25
28
 
26
29
  /**
@@ -32,6 +35,20 @@ export interface Landing {
32
35
  */
33
36
  export const ANGLE_TOLERANCE = 0.01;
34
37
  export const ORIGIN_TOLERANCE = 0.01;
38
+ /**
39
+ * A scale key is rounded to 1e-6 and a bone under a scaled bone takes its
40
+ * parent's rounding too; rig-c stores float32 (≈ 6e-8 relative). 1e-4 sits
41
+ * two orders above that.
42
+ */
43
+ export const SCALE_TOLERANCE = 1e-4;
44
+ /**
45
+ * An ankle sits at the end of a lever as long as the leg: each keyed rotation
46
+ * on the way (the hip, the thigh, the shin; rounded to 0.001°, ±8.7e-6 rad)
47
+ * moves it by up to 8.7e-6 × its setup distance from the hip's origin, so three
48
+ * move it ≤ 2.6e-5 × that. The tolerance is the origin's plus 1e-4 × that
49
+ * distance, about four times the sum (0.06 units on a 520-unit leg).
50
+ */
51
+ export const FOOT_TOLERANCE_PER_UNIT = 1e-4;
35
52
 
36
53
  /** Every bone, world, on every key frame of the animation, by rig-c's poser. */
37
54
  export function poseFrames(doc: CompiledDocument, animation: string, times: readonly number[]): Array<Map<string, RawBone>> {
@@ -58,8 +75,24 @@ export function measureLanding(doc: CompiledDocument, animation: string, map: Jo
58
75
  frame = i;
59
76
  }
60
77
  });
61
- out.push({ bone: b, worst, frame, kind: 'angle' });
78
+ out.push({ bone: b, worst, frame, kind: 'angle', tolerance: ANGLE_TOLERANCE });
79
+ });
80
+ }
81
+ const setup = new Map(poseRawSetup(doc).bones.map((b) => [b.name, b]));
82
+ for (const [b, asked] of r.scales) {
83
+ const s0 = setup.get(b)!;
84
+ const len0 = Math.hypot(s0.a, s0.c);
85
+ let worst = -1;
86
+ let frame = 0;
87
+ posed.forEach((bones, i) => {
88
+ const o = bones.get(b)!;
89
+ const e = Math.abs(Math.hypot(o.a, o.c) / len0 - asked[i]!);
90
+ if (e > worst) {
91
+ worst = e;
92
+ frame = i;
93
+ }
62
94
  });
95
+ out.push({ bone: b, worst, frame, kind: 'scale', tolerance: SCALE_TOLERANCE });
63
96
  }
64
97
  if (r.origin !== null) {
65
98
  let worst = -1;
@@ -73,13 +106,132 @@ export function measureLanding(doc: CompiledDocument, animation: string, map: Jo
73
106
  frame = i;
74
107
  }
75
108
  });
76
- out.push({ bone: r.origin.bone, worst, frame, kind: 'origin' });
109
+ out.push({ bone: r.origin.bone, worst, frame, kind: 'origin', tolerance: ORIGIN_TOLERANCE });
110
+ }
111
+ if (r.ankles !== null) {
112
+ const top = r.origin?.bone;
113
+ for (const side of ['r', 'l'] as const) {
114
+ const bone = r.ankles.bones[side];
115
+ const s0 = setup.get(bone)!;
116
+ const tip0: [number, number] = [s0.worldX + s0.a * (lengths.get(bone) ?? 0), s0.worldY + s0.c * (lengths.get(bone) ?? 0)];
117
+ const h0 = top === undefined ? null : setup.get(top)!;
118
+ const lever = h0 === null ? Math.hypot(tip0[0], tip0[1]) : Math.hypot(tip0[0] - h0.worldX, tip0[1] - h0.worldY);
119
+ let worst = -1;
120
+ let frame = 0;
121
+ posed.forEach((bones, i) => {
122
+ const got = pointAt(bones, lengths, `${bone}:tip`)!;
123
+ const want = r.ankles!.at[side][i]!;
124
+ const e = Math.hypot(got[0] - want[0], got[1] - want[1]);
125
+ if (e > worst) {
126
+ worst = e;
127
+ frame = i;
128
+ }
129
+ });
130
+ out.push({ bone: `${bone}:tip`, worst, frame, kind: 'foot', tolerance: ORIGIN_TOLERANCE + FOOT_TOLERANCE_PER_UNIT * lever });
131
+ }
77
132
  }
78
133
  return out;
79
134
  }
80
135
 
81
136
  export function landed(l: Landing): boolean {
82
- return l.worst <= (l.kind === 'angle' ? ANGLE_TOLERANCE : ORIGIN_TOLERANCE);
137
+ return l.worst <= l.tolerance;
138
+ }
139
+
140
+ /**
141
+ * How a scaled bone, or a bone under one, has its art distorted: the angle
142
+ * between its world x and y axes less 90° (shear), and each world axis's length
143
+ * against setup (a foot under a scaled shin is squashed along the shin). A
144
+ * reading, not a gate: rig-c's `scalex` on a bone reaches every bone below it.
145
+ */
146
+ export interface Shear {
147
+ bone: string;
148
+ /** Worst |angle(x, y) − 90°|, degrees, and its frame. */
149
+ shear: number;
150
+ frame: number;
151
+ /** The world x and y axes' lengths over setup's: lowest and highest over the frames. */
152
+ x: [number, number];
153
+ y: [number, number];
154
+ }
155
+
156
+ export function measureShear(doc: CompiledDocument, posed: Array<Map<string, RawBone>>, scaled: Iterable<string>): Shear[] {
157
+ const setup = new Map(poseRawSetup(doc).bones.map((b) => [b.name, b]));
158
+ const under = new Set<string>(scaled);
159
+ for (const b of doc.bones) if (b.parent !== undefined && b.parent !== null && under.has(b.parent)) under.add(b.name);
160
+ const out: Shear[] = [];
161
+ for (const b of doc.bones) {
162
+ if (!under.has(b.name)) continue;
163
+ const s0 = setup.get(b.name)!;
164
+ const x0 = Math.hypot(s0.a, s0.c);
165
+ const y0 = Math.hypot(s0.b, s0.d);
166
+ let shear = -1;
167
+ let frame = 0;
168
+ const xs: number[] = [];
169
+ const ys: number[] = [];
170
+ posed.forEach((bones, i) => {
171
+ const o = bones.get(b.name)!;
172
+ const e = Math.abs(wrap((Math.atan2(o.d, o.b) - Math.atan2(o.c, o.a)) * (180 / Math.PI) - 90));
173
+ if (e > shear) {
174
+ shear = e;
175
+ frame = i;
176
+ }
177
+ xs.push(Math.hypot(o.a, o.c) / x0);
178
+ ys.push(Math.hypot(o.b, o.d) / y0);
179
+ });
180
+ const rx = rangeOf(xs);
181
+ const ry = rangeOf(ys);
182
+ out.push({ bone: b.name, shear, frame, x: [rx.min, rx.max], y: [ry.min, ry.max] });
183
+ }
184
+ return out;
185
+ }
186
+
187
+ /**
188
+ * The feet on the posed build (#43, #47), outside the jumps read off the
189
+ * reference: the lowest ankle's vertical range, and per support span, the
190
+ * supporting ankle's vertical range and horizontal drift — in world units and
191
+ * in that side's leg length (the median, over the frames, of its thigh's origin
192
+ * to its ankle).
193
+ */
194
+ export interface FeetReading {
195
+ legLength: { r: number; l: number };
196
+ /** The lowest ankle's world y range over the frames outside jumps. */
197
+ lowest: { range: number; legs: number; frames: number };
198
+ /** Per support span: the supporting ankle's ranges outside jumps. */
199
+ plants: Array<{ side: 'r' | 'l'; frames: [number, number]; vertical: number; horizontal: number; verticalLegs: number; horizontalLegs: number }>;
200
+ }
201
+
202
+ export function measureFeet(posed: Array<Map<string, RawBone>>, lengths: ReadonlyMap<string, number>, r: Retarget, legs: { r: { root: string; ankle: string }; l: { root: string; ankle: string } }): FeetReading | null {
203
+ if (r.feet === null) return null;
204
+ const tip = (bones: Map<string, RawBone>, side: 'r' | 'l') => pointAt(bones, lengths, `${legs[side].ankle}:tip`)!;
205
+ const legLength = { r: 0, l: 0 };
206
+ for (const side of ['r', 'l'] as const) {
207
+ legLength[side] = medianOf(posed.map((bones) => {
208
+ const a = tip(bones, side);
209
+ const o = bones.get(legs[side].root)!;
210
+ return Math.hypot(a[0] - o.worldX, a[1] - o.worldY);
211
+ }));
212
+ }
213
+ const jump = new Set<number>();
214
+ for (const [a, b] of r.feet.jumps) for (let i = a; i <= b; i++) jump.add(i);
215
+ const ys: number[] = [];
216
+ posed.forEach((bones, i) => {
217
+ if (jump.has(i)) return;
218
+ ys.push(Math.min(tip(bones, 'r')[1], tip(bones, 'l')[1]));
219
+ });
220
+ const lr = rangeOf(ys);
221
+ const mean = (legLength.r + legLength.l) / 2;
222
+ const plants = r.feet.supports.map((s) => {
223
+ const xs: number[] = [];
224
+ const yy: number[] = [];
225
+ for (let i = s.frames[0]; i <= s.frames[1]; i++) {
226
+ const a = tip(posed[i]!, s.side);
227
+ xs.push(a[0]);
228
+ yy.push(a[1]);
229
+ }
230
+ const v = rangeOf(yy).range;
231
+ const h = rangeOf(xs).range;
232
+ return { side: s.side, frames: s.frames, vertical: v, horizontal: h, verticalLegs: v / legLength[s.side], horizontalLegs: h / legLength[s.side] };
233
+ });
234
+ return { legLength, lowest: { range: lr.range, legs: lr.range / mean, frames: ys.length }, plants };
83
235
  }
84
236
 
85
237
  export interface Range {