spine-rigc 1.6.2 → 1.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/cli.ts +82 -43
- package/docs/AUTHORING.md +9 -6
- package/package.json +1 -1
- package/src/atlas.ts +26 -16
- package/src/check.ts +37 -39
- package/src/compile.ts +59 -74
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +95 -10
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +79 -24
- package/src/deformmeasure.ts +6 -3
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +109 -11
- package/src/render.ts +347 -34
- package/src/render_core.ts +115 -18
- package/src/transform.ts +62 -126
- package/src/types.ts +6 -5
- package/src/validate.ts +51 -29
package/src/core/world.ts
CHANGED
|
@@ -3,9 +3,23 @@
|
|
|
3
3
|
* the compiled model's bones (issue #925). Written from what a bone's fields
|
|
4
4
|
* mean and from measurement against the runtime's pose dump
|
|
5
5
|
* (`tools/pose_oracle.ts dump`, spine-core 4.3.13), and held to it by the core
|
|
6
|
-
* suite's equivalence controls
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* suite's equivalence controls.
|
|
7
|
+
*
|
|
8
|
+
* ⭐ **The compiler's setup transforms are this evaluator too** (issue #1015).
|
|
9
|
+
* `src/transform.ts` held a second evaluator of its own until then, and the
|
|
10
|
+
* two differed in exactly three places, none of them a different matrix: the
|
|
11
|
+
* degree factor (`Math.PI / 180` there, the runtime's 3.1415927 here), the
|
|
12
|
+
* frame of a bone with scale 1 and no shear (`[cos, −sin, sin, cos]` of the
|
|
13
|
+
* rotation there, the y column at `rotation + 90` here), and how a radian
|
|
14
|
+
* angle is turned into degrees (divided by the degree factor there,
|
|
15
|
+
* multiplied by `180 / pi` here). Every byte rigc emits was computed with the
|
|
16
|
+
* compiler's three, so they are a parameter of this evaluator
|
|
17
|
+
* (`WorldArithmetic`) rather than a rewrite of the emitter's numbers: the
|
|
18
|
+
* compiler passes its own and the posing core passes none, which is
|
|
19
|
+
* `RUNTIME_ARITHMETIC`, the measured choices below. Measured with the
|
|
20
|
+
* compiler's degree factor and frame taken from here instead: 2 of 19
|
|
21
|
+
* recipes moved, by at most 4e-5 (issue #1015's report) — whether the
|
|
22
|
+
* compiler should bind with the runtime's constant is its own question.
|
|
9
23
|
*
|
|
10
24
|
* ## What each measured choice is
|
|
11
25
|
*
|
|
@@ -99,6 +113,28 @@ export const RUNTIME_DEG = 180 / RUNTIME_PI;
|
|
|
99
113
|
/** 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
114
|
export const COLLAPSED_X_AXIS_SQ = 0.00001 * 0.00001;
|
|
101
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The three places an evaluator's arithmetic may differ in the last bit while
|
|
118
|
+
* computing the same matrix (issue #1015) — see the header. The core poses with
|
|
119
|
+
* `RUNTIME_ARITHMETIC`; `src/transform.ts` passes the compiler's, so that what
|
|
120
|
+
* rigc has always emitted is what it still emits.
|
|
121
|
+
*/
|
|
122
|
+
export interface WorldArithmetic {
|
|
123
|
+
/** Degrees to radians: every angle is multiplied by it. */
|
|
124
|
+
radiansPerDegree: number;
|
|
125
|
+
/** Radians to degrees — the angle `noRotationOrReflection` takes off a parent's axis. */
|
|
126
|
+
degreesOf: (radians: number) => number;
|
|
127
|
+
/** A bone with scale 1 and no shear takes `[cos r, −sin r, sin r, cos r]` instead of the y column at `r + 90`; the general frame everywhere else. */
|
|
128
|
+
rotationOnlyFrame: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The runtime's arithmetic, as measured (the header's three choices): what the core poses with. */
|
|
132
|
+
export const RUNTIME_ARITHMETIC: WorldArithmetic = {
|
|
133
|
+
radiansPerDegree: RAD,
|
|
134
|
+
degreesOf: (radians) => radians * RUNTIME_DEG,
|
|
135
|
+
rotationOnlyFrame: false,
|
|
136
|
+
};
|
|
137
|
+
|
|
102
138
|
/** One bone's world transform: the matrix `[a b; c d]` and the origin, y up. */
|
|
103
139
|
export interface CoreWorld {
|
|
104
140
|
a: number;
|
|
@@ -114,16 +150,27 @@ export type CoreInheritMode = 'normal' | 'onlyTranslation' | 'noRotationOrReflec
|
|
|
114
150
|
|
|
115
151
|
export type M2 = [number, number, number, number];
|
|
116
152
|
|
|
117
|
-
/** What computes a child's world matrix from its mode, its parent's world
|
|
118
|
-
export type InheritComputation = (mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone) => M2;
|
|
153
|
+
/** What computes a child's world matrix from its mode, its parent's world, its own fields and the arithmetic in force. */
|
|
154
|
+
export type InheritComputation = (mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone, arithmetic: WorldArithmetic) => M2;
|
|
119
155
|
|
|
120
156
|
/** 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) *
|
|
123
|
-
const yAngle = (rotation + 90 + shearY) *
|
|
157
|
+
function frame(rotation: number, shearX: number, shearY: number, scaleX: number, scaleY: number, rad: number = RAD): M2 {
|
|
158
|
+
const xAngle = (rotation + shearX) * rad;
|
|
159
|
+
const yAngle = (rotation + 90 + shearY) * rad;
|
|
124
160
|
return [Math.cos(xAngle) * scaleX, Math.cos(yAngle) * scaleY, Math.sin(xAngle) * scaleX, Math.sin(yAngle) * scaleY];
|
|
125
161
|
}
|
|
126
162
|
|
|
163
|
+
/** A bone's own frame — a root's matrix, and the local factor of `normal` and `onlyTranslation` — under `arithmetic`. */
|
|
164
|
+
function localFrame(arithmetic: WorldArithmetic, rotation: number, shearX: number, shearY: number, scaleX: number, scaleY: number): M2 {
|
|
165
|
+
const rad = arithmetic.radiansPerDegree;
|
|
166
|
+
if (arithmetic.rotationOnlyFrame && scaleX === 1 && scaleY === 1 && shearX === 0 && shearY === 0) {
|
|
167
|
+
const cos = Math.cos(rotation * rad);
|
|
168
|
+
const sin = Math.sin(rotation * rad);
|
|
169
|
+
return [cos, -sin, sin, cos];
|
|
170
|
+
}
|
|
171
|
+
return frame(rotation, shearX, shearY, scaleX, scaleY, rad);
|
|
172
|
+
}
|
|
173
|
+
|
|
127
174
|
function times(p: M2, q: M2): M2 {
|
|
128
175
|
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
176
|
}
|
|
@@ -135,8 +182,8 @@ function times(p: M2, q: M2): M2 {
|
|
|
135
182
|
* which builds the frame to read the bone's columns in from the rotation it
|
|
136
183
|
* has just read, through this same arithmetic (issue #966).
|
|
137
184
|
*/
|
|
138
|
-
export function noScaleDirection(p: M2, rotation: number): [number, number] {
|
|
139
|
-
const r = rotation *
|
|
185
|
+
export function noScaleDirection(p: M2, rotation: number, rad: number = RAD): [number, number] {
|
|
186
|
+
const r = rotation * rad;
|
|
140
187
|
const cos = Math.cos(r);
|
|
141
188
|
const sin = Math.sin(r);
|
|
142
189
|
const ux = p[0] * cos + p[1] * sin;
|
|
@@ -146,19 +193,20 @@ export function noScaleDirection(p: M2, rotation: number): [number, number] {
|
|
|
146
193
|
return [ux * inverse, uy * inverse];
|
|
147
194
|
}
|
|
148
195
|
|
|
149
|
-
/** The world matrix of a child under `parent` in `mode
|
|
150
|
-
function modeMatrix(mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone): M2 {
|
|
196
|
+
/** The world matrix of a child under `parent` in `mode`, under `arithmetic` (the runtime's unless a caller passes its own). */
|
|
197
|
+
function modeMatrix(mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone, arithmetic: WorldArithmetic = RUNTIME_ARITHMETIC): M2 {
|
|
151
198
|
const rotation = bone.rotation ?? 0;
|
|
152
199
|
const shearX = bone.shearX ?? 0;
|
|
153
200
|
const shearY = bone.shearY ?? 0;
|
|
154
201
|
const scaleX = bone.scaleX ?? 1;
|
|
155
202
|
const scaleY = bone.scaleY ?? 1;
|
|
203
|
+
const rad = arithmetic.radiansPerDegree;
|
|
156
204
|
const p: M2 = [parent.a, parent.b, parent.c, parent.d];
|
|
157
205
|
switch (mode) {
|
|
158
206
|
case 'normal':
|
|
159
|
-
return times(p,
|
|
207
|
+
return times(p, localFrame(arithmetic, rotation, shearX, shearY, scaleX, scaleY));
|
|
160
208
|
case 'onlyTranslation':
|
|
161
|
-
return
|
|
209
|
+
return localFrame(arithmetic, rotation, shearX, shearY, scaleX, scaleY);
|
|
162
210
|
case 'noRotationOrReflection': {
|
|
163
211
|
const xLengthSq = p[0] * p[0] + p[2] * p[2];
|
|
164
212
|
let conformal: M2;
|
|
@@ -166,23 +214,23 @@ function modeMatrix(mode: CoreInheritMode, parent: CoreWorld, bone: ModelBone):
|
|
|
166
214
|
if (xLengthSq > COLLAPSED_X_AXIS_SQ) {
|
|
167
215
|
const k = Math.abs(p[0] * p[3] - p[1] * p[2]) / xLengthSq;
|
|
168
216
|
conformal = [p[0], -p[2] * k, p[2], p[0] * k];
|
|
169
|
-
r = rotation - Math.atan2(p[2], p[0])
|
|
217
|
+
r = rotation - arithmetic.degreesOf(Math.atan2(p[2], p[0]));
|
|
170
218
|
} else {
|
|
171
219
|
// 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
220
|
conformal = [0, -p[1], 0, p[3]];
|
|
173
|
-
r = rotation - 90 + Math.atan2(p[3], p[1])
|
|
221
|
+
r = rotation - 90 + arithmetic.degreesOf(Math.atan2(p[3], p[1]));
|
|
174
222
|
}
|
|
175
223
|
// 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) *
|
|
177
|
-
const yAngle = (r + shearY + 90) *
|
|
224
|
+
const xAngle = (r + shearX) * rad;
|
|
225
|
+
const yAngle = (r + shearY + 90) * rad;
|
|
178
226
|
return times(conformal, [Math.cos(xAngle) * scaleX, Math.cos(yAngle) * scaleY, Math.sin(xAngle) * scaleX, Math.sin(yAngle) * scaleY]);
|
|
179
227
|
}
|
|
180
228
|
case 'noScale':
|
|
181
229
|
case 'noScaleOrReflection': {
|
|
182
|
-
const [ux, uy] = noScaleDirection(p, rotation);
|
|
230
|
+
const [ux, uy] = noScaleDirection(p, rotation, rad);
|
|
183
231
|
const flip = mode === 'noScale' && p[0] * p[3] - p[1] * p[2] < 0 ? -1 : 1;
|
|
184
232
|
const turned: M2 = [ux, -uy * flip, uy, ux * flip];
|
|
185
|
-
return times(turned, frame(0, shearX, shearY, scaleX, scaleY));
|
|
233
|
+
return times(turned, frame(0, shearX, shearY, scaleX, scaleY, rad));
|
|
186
234
|
}
|
|
187
235
|
}
|
|
188
236
|
}
|
|
@@ -205,9 +253,16 @@ function modeOf(bone: ModelBone): CoreInheritMode {
|
|
|
205
253
|
* world as a skeleton is created — and a bone under it, active or not, is
|
|
206
254
|
* posed from those zeros like any child. `inherit` replaces the mode
|
|
207
255
|
* computation — the core suite's plant passes a copy with one mode's sign
|
|
208
|
-
* flipped, and nothing else does.
|
|
256
|
+
* flipped, and nothing else does. `arithmetic` is the runtime's unless a
|
|
257
|
+
* caller states its own: `src/transform.ts` passes the compiler's (issue
|
|
258
|
+
* #1015), and no posing path does.
|
|
209
259
|
*/
|
|
210
|
-
export function worldTransforms(
|
|
260
|
+
export function worldTransforms(
|
|
261
|
+
bones: readonly ModelBone[],
|
|
262
|
+
active: ReadonlySet<string> | null = null,
|
|
263
|
+
inherit: InheritComputation = modeMatrix,
|
|
264
|
+
arithmetic: WorldArithmetic = RUNTIME_ARITHMETIC,
|
|
265
|
+
): Map<string, CoreWorld> {
|
|
211
266
|
const out = new Map<string, CoreWorld>();
|
|
212
267
|
for (const bone of bones) {
|
|
213
268
|
if (active !== null && !active.has(bone.name)) {
|
|
@@ -217,13 +272,13 @@ export function worldTransforms(bones: readonly ModelBone[], active: ReadonlySet
|
|
|
217
272
|
const x = bone.x ?? 0;
|
|
218
273
|
const y = bone.y ?? 0;
|
|
219
274
|
if (bone.parent === undefined) {
|
|
220
|
-
const [a, b, c, d] =
|
|
275
|
+
const [a, b, c, d] = localFrame(arithmetic, bone.rotation ?? 0, bone.shearX ?? 0, bone.shearY ?? 0, bone.scaleX ?? 1, bone.scaleY ?? 1);
|
|
221
276
|
out.set(bone.name, { a, b, c, d, worldX: x, worldY: y });
|
|
222
277
|
continue;
|
|
223
278
|
}
|
|
224
279
|
const parent = out.get(bone.parent);
|
|
225
280
|
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);
|
|
281
|
+
const [a, b, c, d] = inherit(modeOf(bone), parent, bone, arithmetic);
|
|
227
282
|
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
283
|
}
|
|
229
284
|
return out;
|
package/src/deformmeasure.ts
CHANGED
|
@@ -1610,9 +1610,12 @@ interface PoseOfFrame {
|
|
|
1610
1610
|
/**
|
|
1611
1611
|
* What `Slider.update` stores in `SliderPose.time` for a wanted animation time.
|
|
1612
1612
|
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1613
|
+
* On a slider driven by a bone: the time wrapped into the animation under
|
|
1614
|
+
* `loop`, else held at 0 or above. Not assumed — `poseDial`'s step 3 reads
|
|
1615
|
+
* `SliderPose.time` off the skeleton each time it poses a dial and compares
|
|
1616
|
+
* it with this, and a disagreement is reported, never absorbed. It is stated at all because that
|
|
1617
|
+
* verification compares against what the runtime STORED and not against what
|
|
1618
|
+
* was asked for. ⚠️ `loop` on a zero-length animation gives NaN here exactly
|
|
1616
1619
|
* as it does there, which is `A37_SLIDER_CONSTRAINT_EFFECTIVE`'s refusal.
|
|
1617
1620
|
*/
|
|
1618
1621
|
function sliderTimeFor(slider: SliderData, time: number): number {
|
package/src/emit.ts
CHANGED
|
@@ -91,7 +91,24 @@ export interface CopyImagesResult {
|
|
|
91
91
|
*/
|
|
92
92
|
export function copyAtlasPages(atlasText: string, outDir: string): CopyImagesResult {
|
|
93
93
|
mkdirSync(outDir, { recursive: true });
|
|
94
|
+
const planned = plannedPageCopies(atlasText, outDir);
|
|
95
|
+
for (const page of planned.pages) {
|
|
96
|
+
const source = resolve(outDir, page.from);
|
|
97
|
+
const destination = join(outDir, page.to);
|
|
98
|
+
if (source !== resolve(destination)) copyFileSync(source, destination);
|
|
99
|
+
}
|
|
100
|
+
return planned;
|
|
101
|
+
}
|
|
94
102
|
|
|
103
|
+
/**
|
|
104
|
+
* What `copyAtlasPages` will do, without doing it: the rewritten atlas text
|
|
105
|
+
* and each page's source and copy name, from the text and `outDir` alone — no
|
|
106
|
+
* file is read or written (issue #1016). `build --copy-images` spells the
|
|
107
|
+
* model document from this text BEFORE the gate, because the document states
|
|
108
|
+
* where each region sits on its page (`pages`) and the copy renames every
|
|
109
|
+
* page; `copyAtlasPages` returns exactly this, after copying.
|
|
110
|
+
*/
|
|
111
|
+
export function plannedPageCopies(atlasText: string, outDir: string): CopyImagesResult {
|
|
95
112
|
const parsed = parseAtlasText(atlasText);
|
|
96
113
|
const claimedBy = new Map<string, string>(); // destination filename -> absolute source holding it
|
|
97
114
|
const pages: CopiedPage[] = [];
|
|
@@ -113,11 +130,5 @@ export function copyAtlasPages(atlasText: string, outDir: string): CopyImagesRes
|
|
|
113
130
|
pages.push({ from: page.name, to: name, regions: page.regions.length });
|
|
114
131
|
}
|
|
115
132
|
|
|
116
|
-
for (const page of pages) {
|
|
117
|
-
const source = resolve(outDir, page.from);
|
|
118
|
-
const destination = join(outDir, page.to);
|
|
119
|
-
if (source !== resolve(destination)) copyFileSync(source, destination);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
133
|
return { atlasText: rewritePageNames(parsed, (_name, index) => pages[index].to), pages };
|
|
123
134
|
}
|
package/src/ingest.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
SPINE_VERSION,
|
|
53
53
|
} from './compile.ts';
|
|
54
54
|
import { CompileError } from './errors.ts';
|
|
55
|
+
import { physicsDrives } from './core/constraints_physics.ts';
|
|
55
56
|
import { CHANNELS_BY_KIND } from './timelines.ts';
|
|
56
57
|
import {
|
|
57
58
|
LEGACY_BONE_INHERIT_KEY,
|
|
@@ -303,13 +304,14 @@ const CONSTRAINT_FIELDS: Record<string, string[]> = {
|
|
|
303
304
|
* The physics constraints that drive nothing, by name, each with the component
|
|
304
305
|
* fields it DOES state — every one of them at most 0 (issue #731).
|
|
305
306
|
*
|
|
306
|
-
* 🔑 The
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* `
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
307
|
+
* 🔑 The question is the core's `physicsDrives` (`src/core/constraints_physics.ts`,
|
|
308
|
+
* issue #1015): a component above 0 drives its part of the step and nothing
|
|
309
|
+
* else does, so a constraint with none of `PHYSICS_COMPONENTS` above 0 moves no
|
|
310
|
+
* bone — `PhysicsConstraint.update` applies a part only on that test
|
|
311
|
+
* (`PhysicsConstraint.js:112`). An unstated component is the parser's 0. `build`
|
|
312
|
+
* refuses exactly that shape by name — `A23_PHYSICS_CONSTRAINT_EFFECTIVE` at the
|
|
313
|
+
* gate — so carrying one through made the decompiled spec of a file an editor
|
|
314
|
+
* exports unbuildable as a whole, over a constraint that did nothing in it.
|
|
313
315
|
*
|
|
314
316
|
* ⚠️ A component that is present and NOT a number is not inert: the parser takes
|
|
315
317
|
* it as written and `"0.5" > 0` is true in the runtime's comparison, so such a
|
|
@@ -321,7 +323,10 @@ function inertPhysics(root: JsonObject): Map<string, string[]> {
|
|
|
321
323
|
const constraint = obj(raw);
|
|
322
324
|
if (constraint.type !== 'physics' || typeof constraint.name !== 'string') continue;
|
|
323
325
|
const stated = PHYSICS_COMPONENTS.filter((field) => constraint[field] !== undefined);
|
|
324
|
-
if (!stated.every((field) => typeof constraint[field] === 'number'
|
|
326
|
+
if (!stated.every((field) => typeof constraint[field] === 'number')) continue;
|
|
327
|
+
const value = (field: (typeof PHYSICS_COMPONENTS)[number]): number => (constraint[field] === undefined ? 0 : (constraint[field] as number));
|
|
328
|
+
const drives = physicsDrives({ x: value('x'), y: value('y'), rotate: value('rotate'), shearX: value('shearX'), scaleX: value('scaleX') });
|
|
329
|
+
if (drives.x || drives.y || drives.rotateOrShearX || drives.scaleX) continue;
|
|
325
330
|
out.set(
|
|
326
331
|
constraint.name,
|
|
327
332
|
stated.map((field) => `${field} ${String(constraint[field])}`),
|
|
@@ -1305,9 +1310,9 @@ function ingestAttachment(
|
|
|
1305
1310
|
const type = att.type === undefined ? 'region' : String(att.type);
|
|
1306
1311
|
const out: JsonObject = {};
|
|
1307
1312
|
|
|
1308
|
-
// A LINKED mesh, in either of the format's two spellings. The
|
|
1309
|
-
//
|
|
1310
|
-
// decides (`SkeletonJson.js:568-569`, `:582`) — so `type: "mesh"` carrying
|
|
1313
|
+
// A LINKED mesh, in either of the format's two spellings. The format decides
|
|
1314
|
+
// it — one branch of the reader for `mesh` and `linkedmesh`, and a truthy
|
|
1315
|
+
// `source` decides (`SkeletonJson.js:568-569`, `:582`) — so `type: "mesh"` carrying
|
|
1311
1316
|
// `source` inverts to a link, and a `linkedmesh` with none does NOT: that map
|
|
1312
1317
|
// is read as an ordinary mesh, whose `uvs` it does not have, and the parser
|
|
1313
1318
|
// throws on it. Reading the second as a link would be this module inventing a
|
|
@@ -1327,8 +1332,8 @@ function ingestAttachment(
|
|
|
1327
1332
|
// `buildRigRegion` writes `path` when the image basename differs from the
|
|
1328
1333
|
// name the attachment carries — its stated `name`, else the placeholder —
|
|
1329
1334
|
// so naming the image after the region this attachment RESOLVES
|
|
1330
|
-
// (`path ?? name ?? placeholder`, the
|
|
1331
|
-
// `SkeletonJson.js:526`, `:529`, `:560`) reproduces the same `path`
|
|
1335
|
+
// (`path ?? name ?? placeholder`, the defaults the format's reader applies
|
|
1336
|
+
// at `SkeletonJson.js:526`, `:529`, `:560`) reproduces the same `path`
|
|
1332
1337
|
// decision AND the same atlas region name.
|
|
1333
1338
|
if (opts.art === 'loose') {
|
|
1334
1339
|
const region = att.path ?? (typeof att.name === 'string' ? att.name : placeholder);
|
package/src/keyorder.ts
CHANGED
|
@@ -288,8 +288,8 @@ export function inEditorKeyOrder<T extends object>(skeleton: T, table: KeyOrderT
|
|
|
288
288
|
// ## Why this is a table and not a parse
|
|
289
289
|
//
|
|
290
290
|
// The parser is the only authority on what a default is, and this table is
|
|
291
|
-
// not a second one: it is a
|
|
292
|
-
// by row (`S103` on the in-tree builds, `IG82` on the corpus) by loading an
|
|
291
|
+
// not a second one: it is a table of the parser's readings that the selftest
|
|
292
|
+
// holds to the parser row by row (`S103` on the in-tree builds, `IG82` on the corpus) by loading an
|
|
293
293
|
// object of each kind with the key at the row's value and without it, and
|
|
294
294
|
// comparing everything `SkeletonData` holds — and then one float32 step off,
|
|
295
295
|
// which must differ. It cannot BE a parse: `compile.ts` must not link the
|
package/src/model.ts
CHANGED
|
@@ -82,10 +82,12 @@
|
|
|
82
82
|
* they are the atlas emitter's constants, and the model does not carry them.
|
|
83
83
|
*
|
|
84
84
|
* 📄 **It is written as a document** (issue #922, cut 1f): `modelDocument` below
|
|
85
|
-
* spells it as `rigc-compiled/1
|
|
86
|
-
* `skeleton.model.json` beside the
|
|
85
|
+
* spells it as `rigc-compiled/2` (`/1` until issue #1016 added `pages`), and
|
|
86
|
+
* `build` writes that text into `--out` as `skeleton.model.json` beside the
|
|
87
|
+
* Spine files, after the gate, like them.
|
|
87
88
|
*/
|
|
88
89
|
import { createHash } from 'node:crypto';
|
|
90
|
+
import { parseAtlasText } from './atlas.ts';
|
|
89
91
|
import { CompileError } from './errors.ts';
|
|
90
92
|
import type { BoneTransform } from './transform.ts';
|
|
91
93
|
import type { RigSkinConstraintKey } from './rig.ts';
|
|
@@ -211,10 +213,12 @@ export interface ModelSequence {
|
|
|
211
213
|
* (#931: the atlas's `rotate` transposed into the corners was exact on 2018 of
|
|
212
214
|
* 3000 probes, ignored on 3000 of 3000). The trim and the original size do not
|
|
213
215
|
* move under either: rigc's packer never trims or rotates (`src/atlas.ts`).
|
|
214
|
-
* The draw, which does need the four — a region's page and page UVs —
|
|
216
|
+
* The draw, which does need the four — a region's page and page UVs — read
|
|
215
217
|
* them off the atlas written beside this document, as a second input
|
|
216
|
-
* (`src/core/uvs.ts`, issue #967)
|
|
217
|
-
*
|
|
218
|
+
* (`src/core/uvs.ts`, issue #967), until issue #1016 gave the document a
|
|
219
|
+
* `pages` section spelled from the atlas text `build` writes (`pagesOfAtlas`
|
|
220
|
+
* below): the four are still not this record's, because they are the written
|
|
221
|
+
* arrangement's, and the section moves with it.
|
|
218
222
|
*/
|
|
219
223
|
export interface ModelAtlasRect {
|
|
220
224
|
width: number;
|
|
@@ -613,11 +617,17 @@ export interface CompiledModel extends CarriedFromCompileResult {
|
|
|
613
617
|
}
|
|
614
618
|
|
|
615
619
|
// ---------------------------------------------------------------------------
|
|
616
|
-
// the document: `rigc-compiled/
|
|
620
|
+
// the document: `rigc-compiled/2` (issue #922, cut 1f; `pages` and `/2`, issue #1016)
|
|
617
621
|
// ---------------------------------------------------------------------------
|
|
618
622
|
|
|
619
|
-
/**
|
|
620
|
-
|
|
623
|
+
/**
|
|
624
|
+
* The document's `spec` value. `/2` since issue #1016 added the `pages`
|
|
625
|
+
* section: a `/1` reader (`readModel` of rigc 1.6) refuses a section it does
|
|
626
|
+
* not know by name, so the same spec over a new section would be refused by
|
|
627
|
+
* every reader already installed rather than read wrongly — a new spec says so
|
|
628
|
+
* before the first section is opened. `readModel` reads both.
|
|
629
|
+
*/
|
|
630
|
+
export const MODEL_DOCUMENT_SPEC = 'rigc-compiled/2';
|
|
621
631
|
|
|
622
632
|
/** The file `build` writes the document to, in `--out` beside `skeleton.json` and `skeleton.atlas`. */
|
|
623
633
|
export const MODEL_DOCUMENT_FILE = 'skeleton.model.json';
|
|
@@ -809,7 +819,7 @@ const MODEL_DOCUMENT_FIELDS: readonly string[] = [
|
|
|
809
819
|
const MODEL_DOCUMENT_LEFT_OUT: readonly string[] = ['setupWorld'];
|
|
810
820
|
|
|
811
821
|
/**
|
|
812
|
-
* The compiled model as a document: `rigc-compiled/
|
|
822
|
+
* The compiled model as a document: `rigc-compiled/2`, `JSON.stringify(doc,
|
|
813
823
|
* null, 2)` and a newline — the text `build` writes to `skeleton.model.json`,
|
|
814
824
|
* and the record rigc's own posing core reads (`readModel` in
|
|
815
825
|
* `src/core/index.ts`, issue #380, step 2). Its cost in a build is measured in
|
|
@@ -820,7 +830,9 @@ const MODEL_DOCUMENT_LEFT_OUT: readonly string[] = ['setupWorld'];
|
|
|
820
830
|
* `constraints`, `events`, `animations` — then the fields carried from `CompileResult` in
|
|
821
831
|
* `CarriedFromCompileResult`'s order: `images`, `pageGrids`, `droppedStates`,
|
|
822
832
|
* `absentParts`, `meshBones`, `meshes`, `physics`, `deformTransforms`,
|
|
823
|
-
* `trackDerivations`, `rig`;
|
|
833
|
+
* `trackDerivations`, `rig`; then `pages`, where each region sits on its page
|
|
834
|
+
* in the atlas written beside it (`pagesOfAtlas`, issue #1016), which is why
|
|
835
|
+
* the atlas text is the third argument; and last `spine`, the digest of the
|
|
824
836
|
* `skeleton.json` written beside it (`spineFileSha256`, issue #968), which is
|
|
825
837
|
* why the Spine text is the second argument. Inside a model record, its interface's field
|
|
826
838
|
* order (`ModelBone`, `ModelSlot`, `ModelSkin`, each attachment kind,
|
|
@@ -894,7 +906,7 @@ const MODEL_DOCUMENT_LEFT_OUT: readonly string[] = ['setupWorld'];
|
|
|
894
906
|
* What the Spine emitter adds (`emitSkeleton`'s header, its spellings, its
|
|
895
907
|
* orders and omissions) is not in the model and so not here.
|
|
896
908
|
*/
|
|
897
|
-
export function modelDocument(model: CompiledModel, skeletonText: string): string {
|
|
909
|
+
export function modelDocument(model: CompiledModel, skeletonText: string, atlasText: string): string {
|
|
898
910
|
for (const key of Object.keys(model)) {
|
|
899
911
|
if (!MODEL_DOCUMENT_FIELDS.includes(key) && !MODEL_DOCUMENT_LEFT_OUT.includes(key)) {
|
|
900
912
|
throw new CompileError(`internal: the model document has no place for the model's field "${key}"; it writes [${MODEL_DOCUMENT_FIELDS.join(', ')}] and leaves out [${MODEL_DOCUMENT_LEFT_OUT.join(', ')}]`);
|
|
@@ -921,11 +933,97 @@ export function modelDocument(model: CompiledModel, skeletonText: string): strin
|
|
|
921
933
|
deformTransforms: plain(model.deformTransforms, 'deformTransforms'),
|
|
922
934
|
trackDerivations: plain(model.trackDerivations, 'trackDerivations'),
|
|
923
935
|
rig: plain(model.rig, 'rig'),
|
|
936
|
+
pages: plain(pagesOfAtlas(atlasText), 'pages'),
|
|
924
937
|
spine: { sha256: spineFileSha256(skeletonText) },
|
|
925
938
|
};
|
|
926
939
|
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
927
940
|
}
|
|
928
941
|
|
|
942
|
+
// --- #1016 where each region sits on its page: begin ---
|
|
943
|
+
/**
|
|
944
|
+
* One region of a page, as the document's `pages` section states it: the
|
|
945
|
+
* region's name exactly as the atlas line spells it (untrimmed — the core finds
|
|
946
|
+
* a region by that spelling, `./core/uvs.ts`), its rectangle's top-left `x`,
|
|
947
|
+
* `y` on the page (y down) and `width`, `height` in the drawing's orientation,
|
|
948
|
+
* the trim (`offsetX` from the drawing's left, `offsetY` from its bottom) and
|
|
949
|
+
* the untrimmed size, its turn in `degrees`, and its `index:` field — every
|
|
950
|
+
* number in the page's texels, exactly as `parseAtlasText` reads it.
|
|
951
|
+
*/
|
|
952
|
+
export interface ModelPageRegion {
|
|
953
|
+
name: string;
|
|
954
|
+
x: number;
|
|
955
|
+
y: number;
|
|
956
|
+
width: number;
|
|
957
|
+
height: number;
|
|
958
|
+
offsetX: number;
|
|
959
|
+
offsetY: number;
|
|
960
|
+
originalWidth: number;
|
|
961
|
+
originalHeight: number;
|
|
962
|
+
degrees: number;
|
|
963
|
+
index: number;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** One page: its name (a path from the directory `build` writes into, trimmed), its `size`, and its regions in file order. */
|
|
967
|
+
export interface ModelPage {
|
|
968
|
+
name: string;
|
|
969
|
+
width: number;
|
|
970
|
+
height: number;
|
|
971
|
+
regions: ModelPageRegion[];
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** The fields of a page and of a region, in the order the document writes them — `readModel` mirrors both lists. */
|
|
975
|
+
export const MODEL_PAGE_FIELDS = ['name', 'width', 'height', 'regions'] as const;
|
|
976
|
+
export const MODEL_PAGE_REGION_FIELDS = ['name', 'x', 'y', 'width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight', 'degrees', 'index'] as const;
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* The document's `pages` section (issue #1016): every page of `atlasText` and
|
|
980
|
+
* every region on it, in file order, with the numbers the draw reads — where
|
|
981
|
+
* each drawing sits, which `./core/uvs.ts` turns into page UVs. `atlasText`
|
|
982
|
+
* is the atlas `build` writes beside the document, the one that run's gate
|
|
983
|
+
* last read: after `--pack` the packed text, after `--copy-images` the text
|
|
984
|
+
* with the copies' page names.
|
|
985
|
+
*
|
|
986
|
+
* ⭐ **Why it is a function of the written atlas rather than a model field.**
|
|
987
|
+
* Issue #939 left the page, `x`, `y` and `rotate` out of `ModelAtlasRect`
|
|
988
|
+
* because `--pack` moves them after `compile` returns and `--copy-images`
|
|
989
|
+
* renames every page — a value the model held from `compile` would state a
|
|
990
|
+
* place the written atlas does not have. The section is spelled from the
|
|
991
|
+
* atlas text instead, so it moves exactly when that text does, and `build`
|
|
992
|
+
* spells the document from the text it writes (`cli.ts`): the document a pack
|
|
993
|
+
* writes is spelled from the packed text and gated by the packed pass, where
|
|
994
|
+
* `A18` compares it with a second compile's document spelled from a second,
|
|
995
|
+
* independent pack.
|
|
996
|
+
*
|
|
997
|
+
* 🔸 **What it leaves out.** The page's `format`, `filter`, `repeat`, `pma`
|
|
998
|
+
* and `scale:` lines: nothing that draws reads them (the rasteriser samples
|
|
999
|
+
* one way, `src/render.ts`'s header; the pose reads only the ratios the
|
|
1000
|
+
* trimmed and original sizes make, #939's decision 2). Every region of every
|
|
1001
|
+
* page is written, drawn or not: which regions a rig draws is the core's
|
|
1002
|
+
* lookup rule (`./core/uvs.ts`, *Which region, on which page*), and a writer
|
|
1003
|
+
* choosing a subset would restate it.
|
|
1004
|
+
*/
|
|
1005
|
+
export function pagesOfAtlas(atlasText: string): ModelPage[] {
|
|
1006
|
+
return parseAtlasText(atlasText).pages.map((page) => ({
|
|
1007
|
+
name: page.name,
|
|
1008
|
+
width: page.width,
|
|
1009
|
+
height: page.height,
|
|
1010
|
+
regions: page.regions.map((r) => ({
|
|
1011
|
+
name: r.name,
|
|
1012
|
+
x: r.x,
|
|
1013
|
+
y: r.y,
|
|
1014
|
+
width: r.width,
|
|
1015
|
+
height: r.height,
|
|
1016
|
+
offsetX: r.offsetX,
|
|
1017
|
+
offsetY: r.offsetY,
|
|
1018
|
+
originalWidth: r.originalWidth,
|
|
1019
|
+
originalHeight: r.originalHeight,
|
|
1020
|
+
degrees: r.degrees,
|
|
1021
|
+
index: r.index,
|
|
1022
|
+
})),
|
|
1023
|
+
}));
|
|
1024
|
+
}
|
|
1025
|
+
// --- #1016 where each region sits on its page: end ---
|
|
1026
|
+
|
|
929
1027
|
// --- #968 the Spine file the document was written beside: begin ---
|
|
930
1028
|
/**
|
|
931
1029
|
* The document's last section, `spine`: `{ "sha256": "<64 lowercase hex>" }`,
|