spine-rigc 1.6.2 → 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.
- package/cli.ts +166 -60
- package/docs/AUTHORING.md +25 -16
- package/package.json +1 -1
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/skin_entries.ts +35 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/kinds.ts +102 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/index.ts +130 -0
- package/src/assertions/model/parse.ts +136 -0
- package/src/assertions/model/skin_entries.ts +58 -0
- package/src/assertions/model/slot_colour.ts +80 -0
- package/src/assertions/reasons.ts +127 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +26 -16
- package/src/check.ts +70 -47
- package/src/compile.ts +118 -93
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +102 -15
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +80 -24
- package/src/deformmeasure.ts +383 -157
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +114 -13
- package/src/render.ts +520 -59
- package/src/render_core.ts +178 -21
- package/src/transform.ts +113 -128
- package/src/types.ts +6 -5
- package/src/validate.ts +200 -490
package/src/deformreport.ts
CHANGED
|
@@ -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/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,
|
|
@@ -884,8 +896,11 @@ const MODEL_DOCUMENT_LEFT_OUT: readonly string[] = ['setupWorld'];
|
|
|
884
896
|
*
|
|
885
897
|
* - `setupWorld` — computed from `bones` by `computeWorldTransforms`
|
|
886
898
|
* (`src/transform.ts`), so it states nothing `bones` does not, and a core
|
|
887
|
-
* is to compute it rather than read it; and it is the one field
|
|
888
|
-
* `-0` (a
|
|
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.)
|
|
889
904
|
* - `images[].absPath` — where the part was on this machine's disk, for the
|
|
890
905
|
* size assertions.
|
|
891
906
|
* - `droppedStates[].why` — the sentence names the `--atlas-in` file by its
|
|
@@ -894,7 +909,7 @@ const MODEL_DOCUMENT_LEFT_OUT: readonly string[] = ['setupWorld'];
|
|
|
894
909
|
* What the Spine emitter adds (`emitSkeleton`'s header, its spellings, its
|
|
895
910
|
* orders and omissions) is not in the model and so not here.
|
|
896
911
|
*/
|
|
897
|
-
export function modelDocument(model: CompiledModel, skeletonText: string): string {
|
|
912
|
+
export function modelDocument(model: CompiledModel, skeletonText: string, atlasText: string): string {
|
|
898
913
|
for (const key of Object.keys(model)) {
|
|
899
914
|
if (!MODEL_DOCUMENT_FIELDS.includes(key) && !MODEL_DOCUMENT_LEFT_OUT.includes(key)) {
|
|
900
915
|
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 +936,97 @@ export function modelDocument(model: CompiledModel, skeletonText: string): strin
|
|
|
921
936
|
deformTransforms: plain(model.deformTransforms, 'deformTransforms'),
|
|
922
937
|
trackDerivations: plain(model.trackDerivations, 'trackDerivations'),
|
|
923
938
|
rig: plain(model.rig, 'rig'),
|
|
939
|
+
pages: plain(pagesOfAtlas(atlasText), 'pages'),
|
|
924
940
|
spine: { sha256: spineFileSha256(skeletonText) },
|
|
925
941
|
};
|
|
926
942
|
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
927
943
|
}
|
|
928
944
|
|
|
945
|
+
// --- #1016 where each region sits on its page: begin ---
|
|
946
|
+
/**
|
|
947
|
+
* One region of a page, as the document's `pages` section states it: the
|
|
948
|
+
* region's name exactly as the atlas line spells it (untrimmed — the core finds
|
|
949
|
+
* a region by that spelling, `./core/uvs.ts`), its rectangle's top-left `x`,
|
|
950
|
+
* `y` on the page (y down) and `width`, `height` in the drawing's orientation,
|
|
951
|
+
* the trim (`offsetX` from the drawing's left, `offsetY` from its bottom) and
|
|
952
|
+
* the untrimmed size, its turn in `degrees`, and its `index:` field — every
|
|
953
|
+
* number in the page's texels, exactly as `parseAtlasText` reads it.
|
|
954
|
+
*/
|
|
955
|
+
export interface ModelPageRegion {
|
|
956
|
+
name: string;
|
|
957
|
+
x: number;
|
|
958
|
+
y: number;
|
|
959
|
+
width: number;
|
|
960
|
+
height: number;
|
|
961
|
+
offsetX: number;
|
|
962
|
+
offsetY: number;
|
|
963
|
+
originalWidth: number;
|
|
964
|
+
originalHeight: number;
|
|
965
|
+
degrees: number;
|
|
966
|
+
index: number;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** One page: its name (a path from the directory `build` writes into, trimmed), its `size`, and its regions in file order. */
|
|
970
|
+
export interface ModelPage {
|
|
971
|
+
name: string;
|
|
972
|
+
width: number;
|
|
973
|
+
height: number;
|
|
974
|
+
regions: ModelPageRegion[];
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** The fields of a page and of a region, in the order the document writes them — `readModel` mirrors both lists. */
|
|
978
|
+
export const MODEL_PAGE_FIELDS = ['name', 'width', 'height', 'regions'] as const;
|
|
979
|
+
export const MODEL_PAGE_REGION_FIELDS = ['name', 'x', 'y', 'width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight', 'degrees', 'index'] as const;
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* The document's `pages` section (issue #1016): every page of `atlasText` and
|
|
983
|
+
* every region on it, in file order, with the numbers the draw reads — where
|
|
984
|
+
* each drawing sits, which `./core/uvs.ts` turns into page UVs. `atlasText`
|
|
985
|
+
* is the atlas `build` writes beside the document, the one that run's gate
|
|
986
|
+
* last read: after `--pack` the packed text, after `--copy-images` the text
|
|
987
|
+
* with the copies' page names.
|
|
988
|
+
*
|
|
989
|
+
* ⭐ **Why it is a function of the written atlas rather than a model field.**
|
|
990
|
+
* Issue #939 left the page, `x`, `y` and `rotate` out of `ModelAtlasRect`
|
|
991
|
+
* because `--pack` moves them after `compile` returns and `--copy-images`
|
|
992
|
+
* renames every page — a value the model held from `compile` would state a
|
|
993
|
+
* place the written atlas does not have. The section is spelled from the
|
|
994
|
+
* atlas text instead, so it moves exactly when that text does, and `build`
|
|
995
|
+
* spells the document from the text it writes (`cli.ts`): the document a pack
|
|
996
|
+
* writes is spelled from the packed text and gated by the packed pass, where
|
|
997
|
+
* `A18` compares it with a second compile's document spelled from a second,
|
|
998
|
+
* independent pack.
|
|
999
|
+
*
|
|
1000
|
+
* 🔸 **What it leaves out.** The page's `format`, `filter`, `repeat`, `pma`
|
|
1001
|
+
* and `scale:` lines: nothing that draws reads them (the rasteriser samples
|
|
1002
|
+
* one way, `src/render.ts`'s header; the pose reads only the ratios the
|
|
1003
|
+
* trimmed and original sizes make, #939's decision 2). Every region of every
|
|
1004
|
+
* page is written, drawn or not: which regions a rig draws is the core's
|
|
1005
|
+
* lookup rule (`./core/uvs.ts`, *Which region, on which page*), and a writer
|
|
1006
|
+
* choosing a subset would restate it.
|
|
1007
|
+
*/
|
|
1008
|
+
export function pagesOfAtlas(atlasText: string): ModelPage[] {
|
|
1009
|
+
return parseAtlasText(atlasText).pages.map((page) => ({
|
|
1010
|
+
name: page.name,
|
|
1011
|
+
width: page.width,
|
|
1012
|
+
height: page.height,
|
|
1013
|
+
regions: page.regions.map((r) => ({
|
|
1014
|
+
name: r.name,
|
|
1015
|
+
x: r.x,
|
|
1016
|
+
y: r.y,
|
|
1017
|
+
width: r.width,
|
|
1018
|
+
height: r.height,
|
|
1019
|
+
offsetX: r.offsetX,
|
|
1020
|
+
offsetY: r.offsetY,
|
|
1021
|
+
originalWidth: r.originalWidth,
|
|
1022
|
+
originalHeight: r.originalHeight,
|
|
1023
|
+
degrees: r.degrees,
|
|
1024
|
+
index: r.index,
|
|
1025
|
+
})),
|
|
1026
|
+
}));
|
|
1027
|
+
}
|
|
1028
|
+
// --- #1016 where each region sits on its page: end ---
|
|
1029
|
+
|
|
929
1030
|
// --- #968 the Spine file the document was written beside: begin ---
|
|
930
1031
|
/**
|
|
931
1032
|
* The document's last section, `spine`: `{ "sha256": "<64 lowercase hex>" }`,
|