spine-rigc 1.7.0 → 1.8.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.
@@ -49,6 +49,9 @@ function sameKeyTime(specTime: number, loaded: number): boolean {
49
49
  export function deformReportBlock(survey: DeformSurvey, deformTransforms: CompileResult['deformTransforms'], exempt: ReadonlySet<string>): string[] {
50
50
  if (survey.timelines === 0) return [];
51
51
  const out: string[] = ['', 'deform (what each key does to the geometry — figures with names, never a bar; issue #316)'];
52
+ // A survey that fell back from the reader it was meant to use says so where it is read (issue #1019): the
53
+ // figures are the same survey either way, and a fallback nobody can see is a second opinion nobody asked for.
54
+ if (survey.source.why !== null) out.push(` .. read and posed through ${survey.source.used}: ${survey.source.why}`);
52
55
  // The legend costs six lines and is worth them exactly once — on a report that
53
56
  // has figures in it. A bounding box or a clipping polygon deformed and nothing
54
57
  // else gets the reason it has no figures and no essay about them.
@@ -0,0 +1,335 @@
1
+ /**
2
+ * What the deform survey reads of a skeleton's STRUCTURE — as opposed to what
3
+ * it reads off a posed one — behind one interface, and the model document's
4
+ * implementation of it (issue #1019, step 4b of issue #380).
5
+ *
6
+ * ## Why there are two readers
7
+ *
8
+ * The survey (`src/deformmeasure.ts`) poses a skeleton and measures what each
9
+ * deform key does to it, and its posing has run through rigc's own core since
10
+ * issue #969. Everything else it read was still spine-core's: the animations
11
+ * and their deform timelines, each key's time and expanded vertex array, the
12
+ * runtime's curve storage, which slot a timeline names and which other slots it
13
+ * reaches, which skin holds the mesh, and each slider's settings — so `explain`
14
+ * on a rigc build parsed the pair through the runtime to learn what the model
15
+ * document it carries already states. This module states those reads once
16
+ * (`SurveyStructure`), and gives the model document's reading of them
17
+ * (`modelStructure`). The runtime's reading stays in `src/deformmeasure.ts`,
18
+ * which links spine-core; the survey's body is written once, against the
19
+ * interface, and the survey's record names which pair it ran through.
20
+ *
21
+ * ## What the model states, and what is derived from it — each rule measured
22
+ *
23
+ * Every read below was compared with spine-core's, value for value at
24
+ * tolerance 0 (`tools/survey_hashes.ts structure` over the nineteen recipes
25
+ * `tools/emit_hashes.ts` generates; the deform-core suite's `DM12` over the
26
+ * gallery's builds and 80 hand-written rigs — sliders, second skins and linked
27
+ * meshes in slots of their own among them). Where the model holds the
28
+ * runtime's value as it is, the reader passes it on; where it holds the value
29
+ * the runtime COMPUTED from, the computation is one rigc already runs — the
30
+ * core's, measured against the runtime for posing, or the emitter's own order —
31
+ * and not a second copy. Each derivation, with how often the stated value alone
32
+ * would have been the runtime's on the nineteen:
33
+ *
34
+ * - **A key's time** is the float32 of the stated time (`CoreDeformKey.time`,
35
+ * which the core's reader stores) — the runtime's `frames` are a
36
+ * `Float32Array`. The stated double was the runtime's on 43 of 88 keys.
37
+ * - **A key's vertex array** is what the runtime holds for the key, not the
38
+ * run the document states: positions unweighted (each the float32 sum of the
39
+ * setup coordinate and the offset), offsets weighted (each float32), the run
40
+ * placed at its `offset` into the attachment's whole array and zeros (setup
41
+ * positions, unweighted) around it — `heldArray` in `./core/deform.ts`. The
42
+ * stated run was the runtime's array on 0 of 88.
43
+ * - **A Bézier segment's polyline** is the nine points the runtime samples into
44
+ * its curve storage: the cubic through the key's four handles, from the key's
45
+ * stated time at fraction 0 to the next key's at a far end of `0.99999999` —
46
+ * `bezierPolyline` with `DEFORM_CURVE_END`, the core's deform curve. The four
47
+ * handles are, of course, never the eighteen numbers (0 of 23).
48
+ * - **The animations' order** is the Spine file's, which is not the model's:
49
+ * the emitter keys `animations` in the editor's order over the model's names
50
+ * (`editorAnimationOrder` in `src/compile.ts`, the one call `emitAnimations`
51
+ * is handed), and the survey iterates the file's order. The model's own order
52
+ * was the file's on 14 of 19 rows. Every other order the survey reads — a
53
+ * skin's, an animation's deform timelines, the sliders — the file keeps from
54
+ * the model.
55
+ * - **A slider's duration** is the runtime's (`CoreAnimationTimelines.duration`:
56
+ * the last key time of every timeline, float32), not the document's declared
57
+ * one, which was the runtime's on 1 of 2.
58
+ * - **The slots a deform reaches besides its own** (`timelineSlots`) are the
59
+ * slots of every linked mesh whose source is the deformed record and which
60
+ * plays its timelines (`timelines: true`) — the rule `./core/deform.ts`'s
61
+ * *Which slot a deform timeline moves* measured. The survey reads them as a
62
+ * set: whether any of them draws the mesh, and their slot timelines' key
63
+ * times. No corpus row has one; `DM12`'s linked rigs do, on 20 of 20.
64
+ *
65
+ * ⛔ Nothing here defaults a value the document does not state: a record the
66
+ * timeline names that the document does not hold, or an order the editor's
67
+ * comparator cannot settle, is a `CoreInputError` naming it — and the survey's
68
+ * seam then falls back to spine-core naming why, as the posing does.
69
+ *
70
+ * ## Purity
71
+ *
72
+ * Nothing from the Spine runtime package: what the runtime reader needs to
73
+ * carry beside these handles (the parsed objects themselves) is kept by that
74
+ * reader, in `src/deformmeasure.ts`.
75
+ */
76
+ import { CompileError } from './errors.ts';
77
+ import { editorAnimationOrder } from './compile.ts';
78
+ import { bezierPolyline } from './core/animation.ts';
79
+ import { DEFORM_CURVE_END, heldArray, type CoreAttachmentTimeline, type CoreDeformKey } from './core/deform.ts';
80
+ import type { DialBoneField } from './core/hooks.ts';
81
+ import { CoreInputError, type CompiledDocument, type CoreAnimation, type CoreAttachment } from './core/index.ts';
82
+
83
+ /** The record a mesh is in the model document — the identity a deform timeline keys and the core poses by. */
84
+ export interface SurveyRecord {
85
+ skin: string;
86
+ slot: string;
87
+ placeholder: string;
88
+ }
89
+
90
+ /** A skin, as the survey wears it. One handle per skin: the survey keys its caches on the handle. */
91
+ export interface SurveySkin {
92
+ readonly name: string;
93
+ }
94
+
95
+ /** A mesh a deform timeline moves, as far as the survey reads it. */
96
+ export interface SurveyMesh {
97
+ /** The attachment's name — the record's stated `name`, else its placeholder. */
98
+ readonly name: string;
99
+ readonly triangles: ArrayLike<number>;
100
+ /** Two numbers per vertex. */
101
+ readonly worldVerticesLength: number;
102
+ /** Whether its vertices are bound to bones (`MeshAttachment.bones !== null`). */
103
+ readonly weighted: boolean;
104
+ /** The other slots the deform reaches (`Attachment.timelineSlots`), read as a set. */
105
+ readonly timelineSlots: readonly number[];
106
+ /** The model record it is, or `null` when no skin holds it — the core then refuses to pose it, by name. */
107
+ readonly record: SurveyRecord | null;
108
+ }
109
+
110
+ /** How the runtime gets from one key's geometry to the next. */
111
+ export type SurveyCurve = { kind: 'linear' } | { kind: 'stepped' } | { kind: 'bezier'; points: ArrayLike<number> };
112
+
113
+ /** One deform timeline of one animation. */
114
+ export interface SurveyDeformTimeline {
115
+ /** The slot it is filed under, by index into `SurveyStructure.slotName`. */
116
+ readonly slotIndex: number;
117
+ /** The mesh it moves, or `null` when the attachment has a vertex array and no triangles (a bounding box, a clipping polygon, a path). */
118
+ readonly mesh: SurveyMesh | null;
119
+ /** Each key's time, as the runtime stores it. */
120
+ readonly frames: ArrayLike<number>;
121
+ /** Key `frame`'s vertex array, as the runtime holds it. */
122
+ vertices(frame: number): ArrayLike<number> | undefined;
123
+ /** The curve from key `frame` to key `frame + 1`. */
124
+ curve(frame: number): SurveyCurve;
125
+ /** The skin that holds the mesh, by name and handle (`null` when none does), and the placeholder the timeline names. */
126
+ placement(): { skin: string; holder: SurveySkin | null; placeholder: string };
127
+ }
128
+
129
+ /** One animation, as far as the survey reads it. */
130
+ export interface SurveyAnimation {
131
+ readonly name: string;
132
+ /** Its deform timelines, in the runtime's order. */
133
+ readonly deforms: readonly SurveyDeformTimeline[];
134
+ /** Every key time of every timeline filed under one of `slots` — the times a slot's visibility can change at. */
135
+ slotKeyTimes(slots: ReadonlySet<number>): number[];
136
+ }
137
+
138
+ /**
139
+ * One slider constraint, as far as the survey reads it.
140
+ *
141
+ * ⚠️ `stated`, `reader`, `from`, `to` and `scale` are the dial's mapping and are
142
+ * read only for a slider with a `bone`: a bone-less one's dial is its time. The
143
+ * two readers part there and nothing reads the difference — spine-core sets no
144
+ * property on a bone-less slider and leaves its `scale` at 0, where the model
145
+ * document states the parser's 1 (measured, `tools/survey_hashes.ts structure`).
146
+ */
147
+ export interface SurveySlider {
148
+ readonly name: string;
149
+ /** Its mix at setup. */
150
+ readonly mix: number;
151
+ /** The animation it applies, by name. */
152
+ readonly animation: string;
153
+ /** That animation's duration, as the runtime computes it. */
154
+ readonly duration: number;
155
+ /** Its driving bone, or `null` on the bone-less form. */
156
+ readonly bone: string | null;
157
+ readonly local: boolean;
158
+ /** The bone field its property reads — `null` on a reader this file does not know. */
159
+ readonly stated: DialBoneField | null;
160
+ /** What that reader is called, for a report that has to name an unknown one. */
161
+ readonly reader: string;
162
+ /** `from` — the property value at which the animation's time is `to`. */
163
+ readonly from: number;
164
+ /** `to`. */
165
+ readonly to: number;
166
+ readonly scale: number;
167
+ readonly loop: boolean;
168
+ }
169
+
170
+ /** Everything the survey reads of a skeleton's structure. */
171
+ export interface SurveyStructure {
172
+ /** Every skin, one handle each, in the skeleton's order — not read by the survey, which wears the skin a timeline names; the instruments walk it. */
173
+ readonly skins: readonly SurveySkin[];
174
+ /** In the runtime's order. */
175
+ readonly animations: readonly SurveyAnimation[];
176
+ /** In the skeleton's constraint order. */
177
+ readonly sliders: readonly SurveySlider[];
178
+ /** A slot's name, or `#<index>` for an index that is not a slot. */
179
+ slotName(index: number): string;
180
+ /** How many skins carry this name — the core wears a skin by its name, so two of one name cannot be posed. */
181
+ skinsNamed(name: string): number;
182
+ }
183
+
184
+ /** The bone field each rig-spec property reads. */
185
+ const PROPERTY_FIELD: Record<string, DialBoneField> = { rotate: 'rotation', x: 'x', y: 'y', scaleX: 'scaleX', scaleY: 'scaleY', shearY: 'shearY' };
186
+
187
+ /** The model document's reading of the survey's structure (the header). */
188
+ export function modelStructure(doc: CompiledDocument): SurveyStructure {
189
+ const slotIndex = new Map(doc.slots.map((s, i) => [s.name, i] as const));
190
+ const skinHandles = new Map<string, SurveySkin>();
191
+ const skinOf = (name: string): SurveySkin | null => {
192
+ if (!doc.skins.some((k) => k.name === name)) return null;
193
+ let handle = skinHandles.get(name);
194
+ if (handle === undefined) {
195
+ handle = { name };
196
+ skinHandles.set(name, handle);
197
+ }
198
+ return handle;
199
+ };
200
+ const recordAt = (skin: string, slot: string, placeholder: string): CoreAttachment | undefined => doc.skins.find((k) => k.name === skin)?.attachments[slot]?.[placeholder];
201
+ /** The slots of every linked mesh that plays this record's timelines (the header's last rule). */
202
+ const linkedSlots = (record: SurveyRecord): number[] => {
203
+ const out: number[] = [];
204
+ for (const skin of doc.skins) {
205
+ for (const [slot, table] of Object.entries(skin.attachments)) {
206
+ for (const entry of Object.values(table)) {
207
+ const g = entry.geometry;
208
+ if (g?.kind !== 'linkedmesh' || entry.timelines !== true) continue;
209
+ if (g.skin !== record.skin || g.slot !== record.slot || g.source !== record.placeholder) continue;
210
+ const at = slotIndex.get(slot);
211
+ if (at !== undefined && !out.includes(at)) out.push(at);
212
+ }
213
+ }
214
+ }
215
+ return out;
216
+ };
217
+ const meshes = new Map<string, SurveyMesh>();
218
+ const meshOf = (t: CoreAttachmentTimeline): SurveyMesh | null => {
219
+ const key = `${t.skin}/${t.slot}/${t.attachment}`;
220
+ const already = meshes.get(key);
221
+ if (already !== undefined) return already;
222
+ const entry = recordAt(t.skin, t.slot, t.attachment);
223
+ if (entry === undefined) throw new CoreInputError(`the deform timeline ${key} names no record of the model document`);
224
+ const g = entry.geometry;
225
+ if (g?.kind !== 'mesh') return null;
226
+ const record = { skin: t.skin, slot: t.slot, placeholder: t.attachment };
227
+ const mesh: SurveyMesh = {
228
+ name: entry.name ?? t.attachment,
229
+ triangles: g.triangles,
230
+ worldVerticesLength: g.vertices.weighted ? 2 * g.vertices.bindings.length : g.vertices.xy.length,
231
+ weighted: g.vertices.weighted,
232
+ timelineSlots: linkedSlots(record),
233
+ record,
234
+ };
235
+ meshes.set(key, mesh);
236
+ return mesh;
237
+ };
238
+ const deformOf = (t: CoreAttachmentTimeline, keys: readonly CoreDeformKey[]): SurveyDeformTimeline => {
239
+ const at = slotIndex.get(t.slot);
240
+ if (at === undefined) throw new CoreInputError(`the deform timeline ${t.skin}/${t.slot}/${t.attachment} names slot "${t.slot}", which is not a slot of the model document`);
241
+ const mesh = meshOf(t);
242
+ const entry = recordAt(t.skin, t.slot, t.attachment);
243
+ const g = entry?.geometry;
244
+ const vertices = g !== undefined && g.kind !== 'region' && g.kind !== 'linkedmesh' ? g.vertices : null;
245
+ const held = new Map<number, number[]>();
246
+ return {
247
+ slotIndex: at,
248
+ mesh,
249
+ frames: keys.map((k) => k.time),
250
+ vertices: (frame) => {
251
+ const key = keys[frame];
252
+ if (key === undefined || vertices === null) return undefined;
253
+ let array = held.get(frame);
254
+ if (array === undefined) {
255
+ array = heldArray(vertices, key);
256
+ held.set(frame, array);
257
+ }
258
+ return array;
259
+ },
260
+ curve: (frame) => {
261
+ const a = keys[frame];
262
+ const b = keys[frame + 1];
263
+ if (a === undefined || b === undefined) throw new CoreInputError(`the deform timeline ${t.skin}/${t.slot}/${t.attachment} has no segment after key ${frame}`);
264
+ if (a.curve === 'linear') return { kind: 'linear' };
265
+ if (a.curve === 'stepped') return { kind: 'stepped' };
266
+ const c = a.curve;
267
+ return { kind: 'bezier', points: bezierPolyline(a.stated, 0, c[0], c[1], c[2], c[3], b.stated, DEFORM_CURVE_END) };
268
+ },
269
+ placement: () => ({ skin: t.skin, holder: skinOf(t.skin), placeholder: t.attachment }),
270
+ };
271
+ };
272
+ const animationOf = (a: CoreAnimation): SurveyAnimation => {
273
+ const deforms: SurveyDeformTimeline[] = [];
274
+ for (const t of a.timelines.attachments) if (t.deform !== null) deforms.push(deformOf(t, t.deform));
275
+ return {
276
+ name: a.name,
277
+ deforms,
278
+ slotKeyTimes: (slots) => {
279
+ const times: number[] = [];
280
+ for (const target of a.timelines.slots) {
281
+ const at = slotIndex.get(target.name);
282
+ if (at === undefined || !slots.has(at)) continue;
283
+ for (const tl of target.timelines) for (const k of tl.keys) times.push(k.time);
284
+ }
285
+ for (const t of a.timelines.attachments) {
286
+ const at = slotIndex.get(t.slot);
287
+ if (at === undefined || !slots.has(at)) continue;
288
+ for (const k of [...(t.deform ?? []), ...(t.sequence ?? [])]) times.push(k.time);
289
+ }
290
+ return times;
291
+ },
292
+ };
293
+ };
294
+ let order: string[];
295
+ try {
296
+ order = editorAnimationOrder(doc.animations.map((a) => a.name));
297
+ } catch (err) {
298
+ if (!(err instanceof CompileError)) throw err;
299
+ throw new CoreInputError(`the animations' order in the Spine file is not settled by the model's names — ${err.message}`);
300
+ }
301
+ const animations = order.map((name) => {
302
+ const a = doc.animations.find((x) => x.name === name);
303
+ if (a === undefined) throw new CoreInputError(`the editor's order named animation "${name}", which is not in the model document`);
304
+ return animationOf(a);
305
+ });
306
+ const sliders: SurveySlider[] = [];
307
+ for (const c of doc.constraints) {
308
+ const r = c.record;
309
+ if (c.kind !== 'slider' || r?.kind !== 'slider') continue;
310
+ sliders.push({
311
+ name: r.name,
312
+ mix: r.mix,
313
+ animation: r.animation,
314
+ duration: r.timelines.duration,
315
+ bone: r.bone,
316
+ local: r.local,
317
+ stated: PROPERTY_FIELD[r.property] ?? null,
318
+ reader: r.property,
319
+ from: r.from,
320
+ to: r.to,
321
+ scale: r.scale,
322
+ loop: r.loop,
323
+ });
324
+ }
325
+ return {
326
+ skins: doc.skins.flatMap((k) => {
327
+ const handle = skinOf(k.name);
328
+ return handle === null || doc.skins.find((x) => x.name === k.name) !== k ? [] : [handle];
329
+ }),
330
+ animations,
331
+ sliders,
332
+ slotName: (index) => doc.slots[index]?.name ?? `#${index}`,
333
+ skinsNamed: (name) => doc.skins.filter((k) => k.name === name).length,
334
+ };
335
+ }
package/src/model.ts CHANGED
@@ -896,8 +896,11 @@ const MODEL_DOCUMENT_LEFT_OUT: readonly string[] = ['setupWorld'];
896
896
  *
897
897
  * - `setupWorld` — computed from `bones` by `computeWorldTransforms`
898
898
  * (`src/transform.ts`), so it states nothing `bones` does not, and a core
899
- * is to compute it rather than read it; and it is the one field holding a
900
- * `-0` (a root bone's `b` is `-sin 0`), which JSON cannot spell.
899
+ * is to compute it rather than read it; and it is the one field that can
900
+ * hold a `-0` (a bone's `c` is `sin 0 · scaleX`, which is `-0` at a
901
+ * `scaleX` of −1), which JSON cannot spell. (A root's `b` was `-sin 0`
902
+ * until issue #1021; under the runtime's arithmetic it is `cos 90°` at
903
+ * pi = 3.1415927, −2.3e-8.)
901
904
  * - `images[].absPath` — where the part was on this machine's disk, for the
902
905
  * size assertions.
903
906
  * - `droppedStates[].why` — the sentence names the `--atlas-in` file by its