spine-rigc 1.5.3 → 1.6.2

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.
@@ -0,0 +1,233 @@
1
+ /**
2
+ * The core's own setup evaluator: every bone's world matrix and origin from
3
+ * the compiled model's bones (issue #925). Written from what a bone's fields
4
+ * mean and from measurement against the runtime's pose dump
5
+ * (`tools/pose_oracle.ts dump`, spine-core 4.3.13), and held to it by the core
6
+ * suite's equivalence controls; it shares no code with `src/transform.ts`,
7
+ * which is the compiler's arithmetic and stays frozen so that no emitted byte
8
+ * moves.
9
+ *
10
+ * ## What each measured choice is
11
+ *
12
+ * - **Degrees to radians with pi written 3.1415927.** The runtime's root bone
13
+ * at rotation 0 reads `b = -2.3205103333142417e-8` and
14
+ * `d = 0.9999999999999998`: `cos` and `sin` of 90 degrees converted with
15
+ * pi = 3.1415927, since `(3.1415927 - pi) / 2 = 2.3205e-8`. It is that
16
+ * decimal in double arithmetic — not float32: pi rounded to float32 would
17
+ * give `4.37e-8`, and the bone fields read through `Math.fround` moved the
18
+ * dump further, not closer (measured on the nineteen recipes).
19
+ * - **The degree factor is taken once**, `pi / 180`, and every angle is
20
+ * multiplied by it.
21
+ * - **The y column is `cos(rotation + 90 + shearY)` and `sin(…)` on every
22
+ * bone**, never `-sin`/`cos` of the rotation. With the constant above but
23
+ * the `-sin` form, `2-the-12-principles` stays 61 millionths off.
24
+ * - **A root bone is its local matrix at its own `x`, `y`**: the skeleton is
25
+ * at the origin, unscaled, in every dump taken.
26
+ *
27
+ * Both halves together read IDENTICAL, worst delta exactly 0, on the 12
28
+ * recipes without constraints (`tools/core_gate.ts`); either half alone is
29
+ * exact on none of them.
30
+ *
31
+ * ## The five inherit modes
32
+ *
33
+ * What each mode takes from the parent, as the format's documentation words
34
+ * it, then how it is computed here. A child's ORIGIN is always the parent's
35
+ * matrix applied to the local `x`, `y` plus the parent's origin — the modes
36
+ * change only the matrix.
37
+ *
38
+ * - `normal` — everything: parent matrix times local matrix.
39
+ * - `onlyTranslation` — the parent's position only: the local matrix alone.
40
+ * - `noRotationOrReflection` — the parent's scale (and what shear leaves of
41
+ * it), not its rotation and not a reflection. The parent's matrix is made
42
+ * conformal on its x axis: that axis kept, the y axis turned to 90 degrees
43
+ * from it with length `|det| / |x axis|` (so the area, the scale the parent
44
+ * carries, is kept and its sign is not); the local matrix is built at the
45
+ * bone's rotation MINUS the parent's x-axis angle; the product is the world.
46
+ * ⚠️ **A parent x axis whose squared length is at most `1e-5` squared is
47
+ * collapsed** (issue #979, `COLLAPSED_X_AXIS_SQ`): the conformal frame is
48
+ * then the parent's y column with its x component negated, beside a zero x
49
+ * column, and the local matrix is built at the rotation less 90 plus the y
50
+ * column's angle, summed in that order. Measured on a child of such a
51
+ * parent: by bisection on the x axis's length, 1e-5 reads collapsed and the
52
+ * next double above does not, at parent rotations 0, 30 and 45 (whether the
53
+ * runtime compares the squared length with `1e-5 · 1e-5` or the length with
54
+ * `1e-5`, 200,000 parents searched separated no pair); 480 probes of x axes
55
+ * from 1e-4 down to 1e-20 and 0 under y axes up to 2e17, sheared, scaled and
56
+ * reflected, read exact, where the reading this replaced (collapsed only at
57
+ * length 0, the y column as it is, the angle less 90) read 71 of 96 off and
58
+ * the rotation-first sum `rotation − (90 − angle)` 10 of 480. The zero
59
+ * matrix of a bone below an inactive one is the collapsed case at length 0,
60
+ * and it fixes the signs of zero of every child in this mode there (`CC13`).
61
+ * - `noScale` — the parent's rotation (and reflection), not its scale: the
62
+ * bone's local rotation is carried through the parent's matrix as a
63
+ * direction, normalised to unit length; the y axis is that direction turned
64
+ * 90 degrees, turned the other way when the parent reflects (`det < 0`);
65
+ * the bone's own shear and scale are applied on that frame.
66
+ * - `noScaleOrReflection` — as `noScale`, never turned for a reflection.
67
+ *
68
+ * ## To the bit (issue #966)
69
+ *
70
+ * The rules above were measured on the oracle's six-decimal grid; three of
71
+ * the modes matched it and not the runtime's last bit (issue #959). Held
72
+ * against `pose_oracle.ts dump --raw` at tolerance 0 — a bone in the mode
73
+ * under a parent rotated, scaled, sheared and reflecting, two children 1e9
74
+ * units out (the core suite's `CR02`, 300 per mode) — three operation orders
75
+ * are the runtime's and the grid could not tell them:
76
+ *
77
+ * - a radian angle is turned into degrees by multiplying with `180 / pi`
78
+ * (`RUNTIME_DEG`), not by dividing by `pi / 180`: the division read 106
79
+ * of 300 `noRotationOrReflection` bones off;
80
+ * - `noRotationOrReflection`'s y angle is `r + shearY + 90`, the shear
81
+ * added before the right angle (`r + 90 + shearY` read 46 of 300 off);
82
+ * - `noScale`'s direction is normalised by multiplying with the reciprocal
83
+ * of its length (dividing read 138 of 300 off).
84
+ *
85
+ * Measured: `normal` on 12 recipes, `onlyTranslation` on 1 and
86
+ * `noRotationOrReflection` on 2 of the tree's corpus; all five, under rotated,
87
+ * scaled, sheared and reflecting parents, on the hand-written probe the core
88
+ * suite builds (`CO07`), because the corpus reaches `noScale` only on a rig
89
+ * with constraints and `noScaleOrReflection` on none.
90
+ */
91
+ import type { ModelBone } from '../model.ts';
92
+
93
+ /** Pi as the runtime converts degrees with it — see the header. */
94
+ export const RUNTIME_PI = 3.1415927;
95
+ const RAD = RUNTIME_PI / 180;
96
+ /** Radians to degrees as the runtime converts them (issue #966): multiplied by `180 / pi`, not divided by `pi / 180` — the two differ in the last bit. */
97
+ export const RUNTIME_DEG = 180 / RUNTIME_PI;
98
+
99
+ /** A parent x axis whose squared length is at most this is collapsed for `noRotationOrReflection` (issue #979): `1e-5` squared, as measured — see the header. */
100
+ export const COLLAPSED_X_AXIS_SQ = 0.00001 * 0.00001;
101
+
102
+ /** One bone's world transform: the matrix `[a b; c d]` and the origin, y up. */
103
+ export interface CoreWorld {
104
+ a: number;
105
+ b: number;
106
+ c: number;
107
+ d: number;
108
+ worldX: number;
109
+ worldY: number;
110
+ }
111
+
112
+ /** The five modes, after the first-letter fold the rig spec admits. */
113
+ export type CoreInheritMode = 'normal' | 'onlyTranslation' | 'noRotationOrReflection' | 'noScale' | 'noScaleOrReflection';
114
+
115
+ export type M2 = [number, number, number, number];
116
+
117
+ /** What computes a child's world matrix from its mode, its parent's world and its own fields. */
118
+ export type InheritComputation = (mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone) => M2;
119
+
120
+ /** The matrix of a rotation, shears and scales, in the column form the header states. */
121
+ function frame(rotation: number, shearX: number, shearY: number, scaleX: number, scaleY: number): M2 {
122
+ const xAngle = (rotation + shearX) * RAD;
123
+ const yAngle = (rotation + 90 + shearY) * RAD;
124
+ return [Math.cos(xAngle) * scaleX, Math.cos(yAngle) * scaleY, Math.sin(xAngle) * scaleX, Math.sin(yAngle) * scaleY];
125
+ }
126
+
127
+ function times(p: M2, q: M2): M2 {
128
+ return [p[0] * q[0] + p[1] * q[2], p[0] * q[1] + p[1] * q[3], p[2] * q[0] + p[3] * q[2], p[2] * q[1] + p[3] * q[3]];
129
+ }
130
+
131
+ /**
132
+ * The unit direction a `noScale` / `noScaleOrReflection` bone's x axis takes:
133
+ * its local rotation carried through the parent's matrix and normalised. Shared
134
+ * by the forward frame and `localFromWorld`'s read-back (`./constraints.ts`),
135
+ * which builds the frame to read the bone's columns in from the rotation it
136
+ * has just read, through this same arithmetic (issue #966).
137
+ */
138
+ export function noScaleDirection(p: M2, rotation: number): [number, number] {
139
+ const r = rotation * RAD;
140
+ const cos = Math.cos(r);
141
+ const sin = Math.sin(r);
142
+ const ux = p[0] * cos + p[1] * sin;
143
+ const uy = p[2] * cos + p[3] * sin;
144
+ // Normalised by multiplying with the reciprocal of the length, not by dividing by it (issue #966): the division reads 1 ulp off on 375 bone-samples of the corpus's `noScale` row.
145
+ const inverse = 1 / Math.sqrt(ux * ux + uy * uy);
146
+ return [ux * inverse, uy * inverse];
147
+ }
148
+
149
+ /** The world matrix of a child under `parent` in `mode`. */
150
+ function modeMatrix(mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone): M2 {
151
+ const rotation = bone.rotation ?? 0;
152
+ const shearX = bone.shearX ?? 0;
153
+ const shearY = bone.shearY ?? 0;
154
+ const scaleX = bone.scaleX ?? 1;
155
+ const scaleY = bone.scaleY ?? 1;
156
+ const p: M2 = [parent.a, parent.b, parent.c, parent.d];
157
+ switch (mode) {
158
+ case 'normal':
159
+ return times(p, frame(rotation, shearX, shearY, scaleX, scaleY));
160
+ case 'onlyTranslation':
161
+ return frame(rotation, shearX, shearY, scaleX, scaleY);
162
+ case 'noRotationOrReflection': {
163
+ const xLengthSq = p[0] * p[0] + p[2] * p[2];
164
+ let conformal: M2;
165
+ let r: number;
166
+ if (xLengthSq > COLLAPSED_X_AXIS_SQ) {
167
+ const k = Math.abs(p[0] * p[3] - p[1] * p[2]) / xLengthSq;
168
+ conformal = [p[0], -p[2] * k, p[2], p[0] * k];
169
+ r = rotation - Math.atan2(p[2], p[0]) * RUNTIME_DEG;
170
+ } else {
171
+ // A collapsed x axis (issue #979): the parent's y column with its x component negated, and the rotation less 90 plus the y column's angle — see the header.
172
+ conformal = [0, -p[1], 0, p[3]];
173
+ r = rotation - 90 + Math.atan2(p[3], p[1]) * RUNTIME_DEG;
174
+ }
175
+ // The y angle adds the shear before the right angle here, unlike `frame` (issue #966): `(r + 90 + shearY)` reads last-bit off on 81 of 205 sheared or scaled bones at a 1e9 amplifier, `(r + shearY + 90)` on none; `r = rotation − parentAngle` taken first (adding the shear before the parent's angle is taken off reads 102 of 205 off).
176
+ const xAngle = (r + shearX) * RAD;
177
+ const yAngle = (r + shearY + 90) * RAD;
178
+ return times(conformal, [Math.cos(xAngle) * scaleX, Math.cos(yAngle) * scaleY, Math.sin(xAngle) * scaleX, Math.sin(yAngle) * scaleY]);
179
+ }
180
+ case 'noScale':
181
+ case 'noScaleOrReflection': {
182
+ const [ux, uy] = noScaleDirection(p, rotation);
183
+ const flip = mode === 'noScale' && p[0] * p[3] - p[1] * p[2] < 0 ? -1 : 1;
184
+ const turned: M2 = [ux, -uy * flip, uy, ux * flip];
185
+ return times(turned, frame(0, shearX, shearY, scaleX, scaleY));
186
+ }
187
+ }
188
+ }
189
+
190
+ /** The fold the rig spec admits: the first letter lower-cased; anything else is not a mode. */
191
+ function modeOf(bone: ModelBone): CoreInheritMode {
192
+ const stated = bone.inheritMode;
193
+ if (stated === undefined) return 'normal';
194
+ const folded = stated.length === 0 ? stated : stated[0].toLowerCase() + stated.slice(1);
195
+ if (folded === 'normal' || folded === 'onlyTranslation' || folded === 'noRotationOrReflection' || folded === 'noScale' || folded === 'noScaleOrReflection') return folded;
196
+ throw new Error(`bone "${bone.name}": inheritMode ${JSON.stringify(stated)} is no mode (readModel refuses it first)`);
197
+ }
198
+
199
+ /**
200
+ * Every bone's setup world transform, parents first as the model lists them.
201
+ *
202
+ * `active`, when given, is the set of bones the applied skins leave active; a
203
+ * bone outside it is not posed. Measured on a hand-written skeleton: the
204
+ * runtime's dump reads an inactive bone as all zeros — matrix and origin, its
205
+ * world as a skeleton is created — and a bone under it, active or not, is
206
+ * posed from those zeros like any child. `inherit` replaces the mode
207
+ * computation — the core suite's plant passes a copy with one mode's sign
208
+ * flipped, and nothing else does.
209
+ */
210
+ export function worldTransforms(bones: readonly ModelBone[], active: ReadonlySet<string> | null = null, inherit: InheritComputation = modeMatrix): Map<string, CoreWorld> {
211
+ const out = new Map<string, CoreWorld>();
212
+ for (const bone of bones) {
213
+ if (active !== null && !active.has(bone.name)) {
214
+ out.set(bone.name, { a: 0, b: 0, c: 0, d: 0, worldX: 0, worldY: 0 });
215
+ continue;
216
+ }
217
+ const x = bone.x ?? 0;
218
+ const y = bone.y ?? 0;
219
+ if (bone.parent === undefined) {
220
+ const [a, b, c, d] = frame(bone.rotation ?? 0, bone.shearX ?? 0, bone.shearY ?? 0, bone.scaleX ?? 1, bone.scaleY ?? 1);
221
+ out.set(bone.name, { a, b, c, d, worldX: x, worldY: y });
222
+ continue;
223
+ }
224
+ const parent = out.get(bone.parent);
225
+ if (parent === undefined) throw new Error(`bone "${bone.name}": parent "${bone.parent}" is not posed before it`);
226
+ const [a, b, c, d] = inherit(modeOf(bone), parent, bone);
227
+ out.set(bone.name, { a, b, c, d, worldX: parent.a * x + parent.b * y + parent.worldX, worldY: parent.c * x + parent.d * y + parent.worldY });
228
+ }
229
+ return out;
230
+ }
231
+
232
+ /** The mode computation `worldTransforms` uses, exported so a control can wrap it. */
233
+ export { modeMatrix };