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.
- package/cli.ts +87 -20
- package/docs/AUTHORING.md +18 -12
- 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/check.ts +33 -8
- package/src/compile.ts +59 -19
- package/src/core/index.ts +7 -5
- package/src/core/world.ts +21 -20
- package/src/deformmeasure.ts +377 -154
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/model.ts +5 -2
- package/src/render.ts +195 -47
- package/src/render_core.ts +68 -8
- package/src/transform.ts +79 -30
- package/src/validate.ts +149 -461
package/src/validate.ts
CHANGED
|
@@ -100,11 +100,34 @@ import {
|
|
|
100
100
|
walkTimelines,
|
|
101
101
|
} from './timelines.ts';
|
|
102
102
|
import type { RigInfo } from './types.ts';
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
103
|
+
import { ASSERTION_KIND, kindRunsUnder, type AssertionProfile } from './assertions/kinds.ts';
|
|
104
|
+
import { verdictHarness, type Failure } from './assertions/harness.ts';
|
|
105
|
+
import { atStoredKey, isObj, type Json } from './assertions/values.ts';
|
|
106
|
+
import type { SkinEntryFacts } from './assertions/facts/skin_entries.ts';
|
|
107
|
+
import type { AtlasPageFacts } from './assertions/facts/atlas_pages.ts';
|
|
108
|
+
import type { SlotColourFacts, SlotTimelines } from './assertions/facts/slot_colour.ts';
|
|
109
|
+
import { a03RegionWidthHeightFinite } from './assertions/bodies/a03.ts';
|
|
110
|
+
import { a11NoClippingAttachments } from './assertions/bodies/a11.ts';
|
|
111
|
+
import { a17AtlasPageFilesExist } from './assertions/bodies/a17.ts';
|
|
112
|
+
import { a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten } from './assertions/bodies/a45.ts';
|
|
113
|
+
import {
|
|
114
|
+
SKIP_NO_ANIMATION,
|
|
115
|
+
SKIP_NO_ATLAS,
|
|
116
|
+
SKIP_NO_ATLAS_PAGE,
|
|
117
|
+
SKIP_NO_ATLAS_REGION,
|
|
118
|
+
SKIP_NO_ATTACHMENT_REGION_JOIN,
|
|
119
|
+
SKIP_NO_DECLARED_DURATION,
|
|
120
|
+
SKIP_NO_LINKED_MESH,
|
|
121
|
+
SKIP_NO_MESH_ATTACHMENT,
|
|
122
|
+
SKIP_NO_PHYSICS_CONSTRAINT,
|
|
123
|
+
SKIP_NO_POSE,
|
|
124
|
+
SKIP_NO_SEQUENCE,
|
|
125
|
+
SKIP_NO_SKELETON,
|
|
126
|
+
SKIP_NO_TIMELINE,
|
|
127
|
+
SKIP_NO_TWO_COLOR_TINT,
|
|
128
|
+
} from './assertions/reasons.ts';
|
|
129
|
+
|
|
130
|
+
export type { Failure } from './assertions/harness.ts';
|
|
108
131
|
|
|
109
132
|
/**
|
|
110
133
|
* Which body of rules to hold the artifact to.
|
|
@@ -130,7 +153,7 @@ export interface Failure {
|
|
|
130
153
|
* no longer honestly serve both — so the choice is made at every call site now,
|
|
131
154
|
* by the caller who knows which question they are asking.
|
|
132
155
|
*/
|
|
133
|
-
export type ValidateProfile =
|
|
156
|
+
export type ValidateProfile = AssertionProfile;
|
|
134
157
|
|
|
135
158
|
export const VALIDATE_PROFILES: readonly ValidateProfile[] = ['spine', 'spine-html'];
|
|
136
159
|
|
|
@@ -147,84 +170,8 @@ export const VALIDATE_PROFILES: readonly ValidateProfile[] = ['spine', 'spine-ht
|
|
|
147
170
|
*/
|
|
148
171
|
export const CLI_DEFAULT_PROFILE: ValidateProfile = 'spine';
|
|
149
172
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
* `check()` throws on a name that has none — a new assertion must state its kind
|
|
153
|
-
* rather than defaulting into one, because the default would decide, silently,
|
|
154
|
-
* whether it runs on foreign data.
|
|
155
|
-
*
|
|
156
|
-
* validity — the file is wrong for any consumer. Runs under every profile.
|
|
157
|
-
* renderer — valid Spine that this project's renderer or frame budget refuses.
|
|
158
|
-
* archetype — a structural rule about rigc's own formations, meaningless to a
|
|
159
|
-
* skeleton rigc did not compile.
|
|
160
|
-
*
|
|
161
|
-
* Two assertions are MIXED and are marked `validity` here because their
|
|
162
|
-
* validity half must never stop running; their policy clauses are gated inside
|
|
163
|
-
* the assertion body against `profile`, and each such clause says so where it
|
|
164
|
-
* lives. They are A06 (size-vs-PNG and every region's rectangle inside the page
|
|
165
|
-
* it names are validity; pma / rotation / two regions over the same texels are
|
|
166
|
-
* policy — the rectangle moved across that line in issue #694, and the reason
|
|
167
|
-
* is stated where it now sits) and A20 (weight coherence is validity; requiring
|
|
168
|
-
* a mesh to be weighted at all is policy).
|
|
169
|
-
*
|
|
170
|
-
* A08 was the third until issue #574 retired its policy clause. It required a
|
|
171
|
-
* skin entry's placeholder to be spelled like the region it resolves to, under
|
|
172
|
-
* a renderer that resolves art by `path` and has never read a placeholder — so
|
|
173
|
-
* restating it as the renderer's own join made it a tautology over the validity
|
|
174
|
-
* half. The argument, with the renderer lines it is measured against, sits above
|
|
175
|
-
* `check('A08_…')`.
|
|
176
|
-
*/
|
|
177
|
-
const ASSERTION_KIND: Record<string, 'validity' | 'renderer' | 'archetype'> = {
|
|
178
|
-
A00_ROUNDTRIP_PARSE: 'validity',
|
|
179
|
-
A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS: 'validity',
|
|
180
|
-
A02_NO_BONE_TRANSFORM_KEY: 'validity',
|
|
181
|
-
A03_REGION_WIDTH_HEIGHT_FINITE: 'validity',
|
|
182
|
-
A04_MESH_TRIANGLES_AND_ENCODING: 'validity',
|
|
183
|
-
A05_CURVE_ARRAY_LENGTH: 'validity',
|
|
184
|
-
A06_ATLAS_PAGE_SIZE_MATCHES_PNG: 'validity', // mixed — see above
|
|
185
|
-
A07_ATLAS_TEXT_SHAPE: 'validity',
|
|
186
|
-
A08_REGION_NAMES_MATCH_ATTACHMENTS: 'validity', // no longer mixed — see above (#574)
|
|
187
|
-
A09_ANIMATION_DURATION_MATCHES_SPEC: 'validity',
|
|
188
|
-
A10_NO_NAN_AFTER_STEPPING: 'validity',
|
|
189
|
-
A11_NO_CLIPPING_ATTACHMENTS: 'renderer',
|
|
190
|
-
A12_NO_DARK_COLOR: 'renderer',
|
|
191
|
-
A13_MESH_BUDGET: 'renderer',
|
|
192
|
-
A14_NO_FULL_FRAME_MESH: 'renderer',
|
|
193
|
-
A15_IDLE_NO_MESH_BONE_KEYS: 'renderer',
|
|
194
|
-
A16_SKELETON_VERSION_4_3: 'validity',
|
|
195
|
-
A17_ATLAS_PAGE_FILES_EXIST: 'validity',
|
|
196
|
-
A18_DETERMINISTIC_EMIT: 'validity',
|
|
197
|
-
A19_OVERLAY_PNGS_HAVE_ALPHA: 'renderer',
|
|
198
|
-
A20_MESH_WEIGHTS_COHERENT: 'validity', // mixed — see above
|
|
199
|
-
A21_MESH_RIM_PINNED: 'archetype',
|
|
200
|
-
A22_MESH_UVS_IN_UNIT_RANGE: 'validity',
|
|
201
|
-
A23_PHYSICS_CONSTRAINT_EFFECTIVE: 'validity',
|
|
202
|
-
A24_AXIS_SPACE_STROKE: 'archetype',
|
|
203
|
-
A25_DETACHED_BONE_PARENTAGE: 'archetype',
|
|
204
|
-
A26_SLOT_DRAW_ORDER: 'archetype',
|
|
205
|
-
A27_REGION_NAME_MATCHES_PAGE_FILENAME: 'renderer',
|
|
206
|
-
A28_RIBBON_ROWS_SHARE_WEIGHTS: 'archetype',
|
|
207
|
-
A29_STROKE_WITHIN_CONTACT_DEPTH: 'archetype',
|
|
208
|
-
A30_STROKE_WITHIN_CAP_CONTAINMENT: 'archetype',
|
|
209
|
-
A31_DRAW_ORDER_OFFSETS_RESOLVE: 'validity',
|
|
210
|
-
A32_EVENT_KEYS_RESOLVE: 'validity',
|
|
211
|
-
A33_VERTEX_ATTACHMENT_GEOMETRY: 'validity',
|
|
212
|
-
A34_CONSTRAINT_TIMELINE_TARGETS: 'validity',
|
|
213
|
-
A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: 'validity',
|
|
214
|
-
A36_PATH_CONSTRAINT_EFFECTIVE: 'validity',
|
|
215
|
-
A37_SLIDER_CONSTRAINT_EFFECTIVE: 'validity',
|
|
216
|
-
A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED: 'validity',
|
|
217
|
-
A39_DEFORM_KEEPS_TRIANGLE_WINDING: 'archetype',
|
|
218
|
-
A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET: 'validity',
|
|
219
|
-
A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP: 'validity',
|
|
220
|
-
A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER: 'validity',
|
|
221
|
-
A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN: 'validity',
|
|
222
|
-
A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN: 'validity',
|
|
223
|
-
A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN: 'validity',
|
|
224
|
-
A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES: 'validity',
|
|
225
|
-
A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT: 'validity',
|
|
226
|
-
A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT: 'validity',
|
|
227
|
-
};
|
|
173
|
+
// What kind of rule each assertion is — `./assertions/kinds.ts` since issue
|
|
174
|
+
// #1025, where the model side's harness reads the same table.
|
|
228
175
|
|
|
229
176
|
/**
|
|
230
177
|
* Every assertion this validator knows, in registry order.
|
|
@@ -242,112 +189,27 @@ export function assertionCountForProfile(profile: ValidateProfile): number {
|
|
|
242
189
|
return ASSERTION_NAMES.filter((name) => ASSERTION_KIND[name] === 'validity').length;
|
|
243
190
|
}
|
|
244
191
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
/*
|
|
267
|
-
* **An empty subject list: SKIP or PASS, and which one is derived (issue #580).**
|
|
268
|
-
*
|
|
269
|
-
* `check()` records a pass when a body runs to its end without a `fail()` or a
|
|
270
|
-
* `skip()`, so a body whose main construct is a loop over a list that is EMPTY
|
|
271
|
-
* passes having measured nothing — the vacuous green one ring out from the bare
|
|
272
|
-
* `return` guards issue #568 converted. Two different things can be true of such
|
|
273
|
-
* a loop, and the criterion decides between them by reading the rule's own name
|
|
274
|
-
* and its `fail()` sentences rather than by preference:
|
|
275
|
-
*
|
|
276
|
-
* * A name of the form ⟨subject⟩_⟨property⟩ — `REGION_WIDTH_HEIGHT_FINITE`,
|
|
277
|
-
* `MESH_TRIANGLES_AND_ENCODING`, `PHYSICS_CONSTRAINT_EFFECTIVE`,
|
|
278
|
-
* `ATLAS_PAGE_SIZE_MATCHES_PNG` — quantifies over the subject it names, and
|
|
279
|
-
* its `fail()` names a member of that subject, the value found and the value
|
|
280
|
-
* required. With no member, nothing was measured: it **SKIPs**, and the
|
|
281
|
-
* reason names the subject that was absent.
|
|
282
|
-
* * A name of the form NO_⟨construct⟩ — `NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS`,
|
|
283
|
-
* `NO_BONE_TRANSFORM_KEY`, `NO_CLIPPING_ATTACHMENTS`, `NO_DARK_COLOR`,
|
|
284
|
-
* `NO_FULL_FRAME_MESH` — quantifies over occurrences of something a correct
|
|
285
|
-
* artifact has NONE of, and its `fail()` reports that the construct is
|
|
286
|
-
* present rather than measuring a value on it. Zero occurrences IS the
|
|
287
|
-
* measurement, so it **PASSes**: the loop is a search of the artifact, and
|
|
288
|
-
* the artifact is what it measured.
|
|
289
|
-
* * A name carrying both halves takes each at its word.
|
|
290
|
-
* `A15_IDLE_NO_MESH_BONE_KEYS` is the pattern and already reads this way: no
|
|
291
|
-
* `idle` animation, or an `idle` with no bone timeline, is the named subject
|
|
292
|
-
* absent and SKIPs; no mesh-driving bone among the bones `idle` does key is
|
|
293
|
-
* the construct absent and passes. `A10_NO_NAN_AFTER_STEPPING` was the same
|
|
294
|
-
* shape until issue #902, and is the next bullet's now: #882 gave it a
|
|
295
|
-
* setup-pose clause whose subject is the bones, beside the stepping clause
|
|
296
|
-
* whose subject is the animations.
|
|
297
|
-
* * An assertion with more than one clause SKIPs only when EVERY clause had
|
|
298
|
-
* nothing to measure, which is the shape `A09`, `A33` and `A38` already
|
|
299
|
-
* carry (`polygons.length === 0 && endsChecked === 0`), and `A10` since #902.
|
|
300
|
-
* `A13_MESH_BUDGET` is why the clause is stated: a declared slot budget is
|
|
301
|
-
* measured against a count of mesh slots, and zero is a count, so that half
|
|
302
|
-
* passes on a rig with no mesh — while a rig that declares only a TRIANGLE
|
|
303
|
-
* budget has nothing left to measure and skips.
|
|
304
|
-
*
|
|
305
|
-
* ⚠️ What the criterion is not allowed to become is a table of assertions with
|
|
306
|
-
* their verdicts written beside them. Every row above is decided by reading the
|
|
307
|
-
* name and the sentences the assertion already prints, so a rule added tomorrow
|
|
308
|
-
* is decided by the same reading and nobody re-opens this question.
|
|
309
|
-
*/
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* What a SKIP says when the artifact carries no member of the subject a rule
|
|
313
|
-
* quantifies over (issue #580), by subject.
|
|
314
|
-
*
|
|
315
|
-
* One constant per SUBJECT rather than one per assertion, for the reason the two
|
|
316
|
-
* above are two: what the rule was denied is the whole content of its SKIP, and
|
|
317
|
-
* three rules denied the same thing should say so in the same words. Exported so
|
|
318
|
-
* a control compares against them rather than quoting them.
|
|
319
|
-
*/
|
|
320
|
-
export const SKIP_NO_REGION_ATTACHMENT = 'the skeleton carries no region attachment';
|
|
321
|
-
export const SKIP_NO_MESH_ATTACHMENT = 'the skeleton carries no mesh attachment';
|
|
322
|
-
export const SKIP_NO_ANIMATION = 'the skeleton carries no animation';
|
|
323
|
-
/**
|
|
324
|
-
* A10's (issue #902): its setup clause reads the bones and its stepping clause
|
|
325
|
-
* the animations, so it skips only when the skeleton carries neither — and the
|
|
326
|
-
* sentence is `SKIP_NO_ANIMATION`'s with the second subject added, so the two
|
|
327
|
-
* cannot drift into different words for the same absence.
|
|
328
|
-
*/
|
|
329
|
-
export const SKIP_NO_POSE = `${SKIP_NO_ANIMATION} and no bone, so there is no setup pose to read and nothing to step`;
|
|
330
|
-
export const SKIP_NO_TIMELINE = 'no animation here carries a timeline';
|
|
331
|
-
export const SKIP_NO_PHYSICS_CONSTRAINT = 'the skeleton declares no physics constraint';
|
|
332
|
-
export const SKIP_NO_ATLAS_PAGE = 'the atlas declares no page';
|
|
333
|
-
export const SKIP_NO_ATLAS_REGION = 'the atlas declares no region';
|
|
334
|
-
export const SKIP_NO_ATTACHMENT_REGION_JOIN =
|
|
335
|
-
'no attachment names a region and the atlas declares none, so there is no attachment-to-region join to hold';
|
|
336
|
-
export const SKIP_NO_TWO_COLOR_TINT =
|
|
337
|
-
'no slot declares a "dark" colour and no animation keys an "rgba2" or "rgb2" timeline, so there is no two-colour tint to read back';
|
|
338
|
-
export const SKIP_NO_SEPARABLE_COLOR =
|
|
339
|
-
'no animation keys an "rgb" or "alpha" timeline, so there is no separable slot colour to read back';
|
|
340
|
-
export const SKIP_NO_LINKED_MESH = 'no attachment in this skeleton takes its geometry from another one';
|
|
341
|
-
export const SKIP_NO_SEQUENCE =
|
|
342
|
-
'no attachment carries a "sequence" block and no animation keys a "sequence" timeline, so there is no numbered series to read back';
|
|
343
|
-
/**
|
|
344
|
-
* A09's, which predates this list and joins it rather than being rewritten: it
|
|
345
|
-
* is the same fact about the same subject, and a control that compares against
|
|
346
|
-
* eight constants and quotes the ninth is a control with a hand-kept exception
|
|
347
|
-
* in it.
|
|
348
|
-
*/
|
|
349
|
-
export const SKIP_NO_DECLARED_DURATION =
|
|
350
|
-
'the motion spec declares no animations and the skeleton has none — a static rig has no duration to compare';
|
|
192
|
+
// What a SKIP says, by what the assertion was denied — `./assertions/reasons.ts`
|
|
193
|
+
// since issue #1025, so the assertion bodies that moved out of this file print
|
|
194
|
+
// the same sentences on both sides; every one is re-exported from here.
|
|
195
|
+
export {
|
|
196
|
+
SKIP_NO_ANIMATION,
|
|
197
|
+
SKIP_NO_ATLAS,
|
|
198
|
+
SKIP_NO_ATLAS_PAGE,
|
|
199
|
+
SKIP_NO_ATLAS_REGION,
|
|
200
|
+
SKIP_NO_ATTACHMENT_REGION_JOIN,
|
|
201
|
+
SKIP_NO_DECLARED_DURATION,
|
|
202
|
+
SKIP_NO_LINKED_MESH,
|
|
203
|
+
SKIP_NO_MESH_ATTACHMENT,
|
|
204
|
+
SKIP_NO_PHYSICS_CONSTRAINT,
|
|
205
|
+
SKIP_NO_POSE,
|
|
206
|
+
SKIP_NO_REGION_ATTACHMENT,
|
|
207
|
+
SKIP_NO_SEPARABLE_COLOR,
|
|
208
|
+
SKIP_NO_SEQUENCE,
|
|
209
|
+
SKIP_NO_SKELETON,
|
|
210
|
+
SKIP_NO_TIMELINE,
|
|
211
|
+
SKIP_NO_TWO_COLOR_TINT,
|
|
212
|
+
} from './assertions/reasons.ts';
|
|
351
213
|
|
|
352
214
|
export interface ValidateInput {
|
|
353
215
|
skeletonText: string;
|
|
@@ -711,36 +573,8 @@ export function unnamedPhysicsTimeline(name: string): Timeline | null {
|
|
|
711
573
|
*/
|
|
712
574
|
const SPINE_4_3: SpineGeneration = '4.3';
|
|
713
575
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
function isObj(v: unknown): v is Json {
|
|
717
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* The time to pose a key at so that the runtime is AT it: the later of the
|
|
722
|
-
* file's number and that number as spine-core stores it (issue #771).
|
|
723
|
-
*
|
|
724
|
-
* 🚨 Every timeline keeps its key times in a `Float32Array`
|
|
725
|
-
* (`Utils.newFloatArray`), and a key time the float cannot hold exactly is
|
|
726
|
-
* stored at the nearest one — for `0.2`, `0.20000000298…`, which is LATER than
|
|
727
|
-
* the double `0.2`. Stepped to the file's own number, a timeline is then just
|
|
728
|
-
* BEFORE its key: before a first key it writes the setup value (`time <
|
|
729
|
-
* frames[0]`), and past a stepped key it still holds the one before
|
|
730
|
-
* (`frames[i] > time`). That is a pose one float step from the key, not the
|
|
731
|
-
* key's — measured on the selftest's own `ingest_probe`, whose `alpha` key at
|
|
732
|
-
* 0.2 posed the setup 1.0 and was refused as `the key states value 0.4`.
|
|
733
|
-
*
|
|
734
|
-
* ⚠️ The later of the two rather than `Math.fround` alone: where the float
|
|
735
|
-
* rounds DOWN, the file's number is already past the stored key, and a runtime
|
|
736
|
-
* built without typed arrays stores the double itself — in both, the file's
|
|
737
|
-
* number is the one at or after the key. What a key time rounds to is the
|
|
738
|
-
* runtime's storage and not the file's statement, so a rule judging what a KEY
|
|
739
|
-
* states poses at the key; the rounding itself is nothing an author can repair.
|
|
740
|
-
*/
|
|
741
|
-
function atStoredKey(time: number): number {
|
|
742
|
-
return Math.max(time, Math.fround(time));
|
|
743
|
-
}
|
|
576
|
+
// `Json`, `isObj` and `atStoredKey` are `./assertions/values.ts`'s since issue
|
|
577
|
+
// #1025: the bodies that moved out of this file read values the same way.
|
|
744
578
|
|
|
745
579
|
/**
|
|
746
580
|
* The atlas region names one raw skin entry will make the loader look up — or
|
|
@@ -1156,51 +990,92 @@ function firstDifferingLine(first: string, second: string): string {
|
|
|
1156
990
|
return `${i + 1}: ${cut(a[i])} first, ${cut(b[i])} second`;
|
|
1157
991
|
}
|
|
1158
992
|
|
|
993
|
+
/**
|
|
994
|
+
* The runtime's supply of `SkinEntryFacts` (issue #1025): every skin of the
|
|
995
|
+
* loaded skeleton in the loaded order, and each skin's entries as
|
|
996
|
+
* `getAttachments()` lists them — the walk this file's prelude makes for its
|
|
997
|
+
* own lists, made once more here so the facts the moved bodies read are a
|
|
998
|
+
* value the selftest can compare with the model side's, order included.
|
|
999
|
+
*/
|
|
1000
|
+
export function spineSkinEntries(data: ReturnType<SkeletonJson['readSkeletonData']>): SkinEntryFacts {
|
|
1001
|
+
const regionAttachments: RegionAttachment[] = [];
|
|
1002
|
+
let clippingCount = 0;
|
|
1003
|
+
for (const skin of data.skins) {
|
|
1004
|
+
for (const entry of skin.getAttachments()) {
|
|
1005
|
+
if (entry.attachment instanceof RegionAttachment) regionAttachments.push(entry.attachment);
|
|
1006
|
+
else if (entry.attachment instanceof ClippingAttachment) clippingCount++;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return { regionAttachments, clippingCount };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* The facts the moved bodies read, as `validate()` supplies them from a pair
|
|
1014
|
+
* spine-core loads — or `null` when the load throws, which is A00's failure
|
|
1015
|
+
* and `validate()`'s to name. For the selftest and `tools/verdict_gate.ts`,
|
|
1016
|
+
* which compare these with the model side's fact by fact, where a verdict
|
|
1017
|
+
* line would hide a difference (an order nothing failed on, a name no line
|
|
1018
|
+
* printed); `validate()` itself builds them from its own round trip.
|
|
1019
|
+
*/
|
|
1020
|
+
export function runtimeFacts(skeletonText: string, atlasText: string): { skinEntries: SkinEntryFacts; atlasPages: AtlasPageFacts; slotColour: SlotColourFacts } | null {
|
|
1021
|
+
let atlas: TextureAtlas;
|
|
1022
|
+
let data: ReturnType<SkeletonJson['readSkeletonData']>;
|
|
1023
|
+
try {
|
|
1024
|
+
atlas = new TextureAtlas(atlasText);
|
|
1025
|
+
data = new SkeletonJson(new AtlasAttachmentLoader(atlas)).readSkeletonData(JSON.parse(skeletonText));
|
|
1026
|
+
} catch {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
return { skinEntries: spineSkinEntries(data), atlasPages: { atlas }, slotColour: spineSlotColourFacts(JSON.parse(skeletonText) as Json, data) };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* The runtime's supply of A45's facts (issue #1025): the slot timelines read
|
|
1034
|
+
* off the skeleton JSON in the file's order — every animation the file keys,
|
|
1035
|
+
* every slot an animation keys, skipping what is not an object, as A45's walk
|
|
1036
|
+
* always read them — whether the loaded skeleton holds an animation, and a
|
|
1037
|
+
* slot's colour posed on a fresh, non-looping track by spine-core: setup pose,
|
|
1038
|
+
* `update(0)`, `updateWorldTransform(Physics.reset)`, then the track stepped to
|
|
1039
|
+
* the time and applied. The model side's supply is
|
|
1040
|
+
* `./assertions/model/slot_colour.ts`; the selftest holds the two to the same
|
|
1041
|
+
* lines.
|
|
1042
|
+
*/
|
|
1043
|
+
function spineSlotColourFacts(raw: Json | null, data: ReturnType<SkeletonJson['readSkeletonData']>): SlotColourFacts {
|
|
1044
|
+
const slotTimelines: SlotTimelines[] = [];
|
|
1045
|
+
const rawAnimations = isObj(raw) && isObj(raw.animations) ? raw.animations : {};
|
|
1046
|
+
for (const [animation, anim] of Object.entries(rawAnimations)) {
|
|
1047
|
+
if (!isObj(anim) || !isObj(anim.slots)) continue;
|
|
1048
|
+
for (const [slot, timelines] of Object.entries(anim.slots)) {
|
|
1049
|
+
if (!isObj(timelines)) continue;
|
|
1050
|
+
slotTimelines.push({ animation, slot, timelines });
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
slotTimelines,
|
|
1055
|
+
hasAnimation: (name) => Boolean(data.findAnimation(name)),
|
|
1056
|
+
posedSlot: (animName, slotName, time) => {
|
|
1057
|
+
const skeleton = new Skeleton(data);
|
|
1058
|
+
const state = new AnimationState(new AnimationStateData(data));
|
|
1059
|
+
state.setAnimation(0, animName, false);
|
|
1060
|
+
skeleton.setupPose();
|
|
1061
|
+
skeleton.update(0);
|
|
1062
|
+
skeleton.updateWorldTransform(Physics.reset);
|
|
1063
|
+
state.update(time);
|
|
1064
|
+
state.apply(skeleton);
|
|
1065
|
+
return skeleton.slots.find((s) => s.data.name === slotName)?.appliedPose;
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1159
1070
|
export function validate(input: ValidateInput): ValidateReport {
|
|
1160
|
-
const failures: Failure[] = [];
|
|
1161
|
-
const passed: string[] = [];
|
|
1162
|
-
const skipped: ValidateReport['skipped'] = [];
|
|
1163
|
-
const profileSkipped: ValidateReport['profileSkipped'] = [];
|
|
1164
|
-
const stats: Record<string, number | string> = {};
|
|
1165
1071
|
const profile = input.profile;
|
|
1166
1072
|
/** True when this profile's rulebook includes the policy layer. */
|
|
1167
1073
|
const policy = profile === 'spine-html';
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
* Run one assertion; record it as passed only if it neither failed nor skipped,
|
|
1174
|
-
* and do not run it at all when the profile does not carry that kind of rule.
|
|
1175
|
-
*
|
|
1176
|
-
* The unknown-assertion throw is deliberate: an assertion with no entry in
|
|
1177
|
-
* ASSERTION_KIND would otherwise pick a profile by accident, and picking wrong
|
|
1178
|
-
* means either a rule that never runs or a rule that fires on everybody's data.
|
|
1179
|
-
*
|
|
1180
|
-
* The body's return value is handed back — `undefined` when the assertion did
|
|
1181
|
-
* not run or threw. Only A00 uses it, and it uses it so that the loaded atlas
|
|
1182
|
-
* and skeleton can be `const`: assigning them from inside this callback leaves
|
|
1183
|
-
* the type checker unable to see that they were ever set, and every later read
|
|
1184
|
-
* of `atlas.pages` or `data.bones` becomes a property of `never`.
|
|
1185
|
-
*/
|
|
1186
|
-
const check = <T>(assertion: string, body: () => T): T | undefined => {
|
|
1187
|
-
const kind = ASSERTION_KIND[assertion];
|
|
1188
|
-
if (!kind) throw new Error(`validate: assertion "${assertion}" has no ASSERTION_KIND entry`);
|
|
1189
|
-
if (kind !== 'validity' && !policy) {
|
|
1190
|
-
profileSkipped.push({ assertion, kind });
|
|
1191
|
-
return undefined;
|
|
1192
|
-
}
|
|
1193
|
-
const before = failures.length;
|
|
1194
|
-
const skippedBefore = skipped.length;
|
|
1195
|
-
let result: T | undefined;
|
|
1196
|
-
try {
|
|
1197
|
-
result = body();
|
|
1198
|
-
} catch (err) {
|
|
1199
|
-
fail(assertion, `threw: ${(err as Error).message}`);
|
|
1200
|
-
}
|
|
1201
|
-
if (failures.length === before && skipped.length === skippedBefore) passed.push(assertion);
|
|
1202
|
-
return result;
|
|
1203
|
-
};
|
|
1074
|
+
// The harness — `check`, `fail`, `skip` and the lists they fill — is
|
|
1075
|
+
// `./assertions/harness.ts`'s since issue #1025, and the rule is the one it
|
|
1076
|
+
// always was: the model side runs the bodies that moved there in the same
|
|
1077
|
+
// harness, so a verdict cannot be decided two ways.
|
|
1078
|
+
const { failures, passed, skipped, profileSkipped, stats, fail, skip, check, verdicts } = verdictHarness(profile, ASSERTION_KIND, 'validate');
|
|
1204
1079
|
|
|
1205
1080
|
// -------------------------------------------------------------------------
|
|
1206
1081
|
// Raw text / raw JSON assertions (they must run even if the parser is happy)
|
|
@@ -2030,7 +1905,6 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2030
1905
|
|
|
2031
1906
|
const regionAttachments: RegionAttachment[] = [];
|
|
2032
1907
|
const meshAttachments: MeshAttachment[] = [];
|
|
2033
|
-
let clippingCount = 0;
|
|
2034
1908
|
const meshSlots = new Set<number>();
|
|
2035
1909
|
/**
|
|
2036
1910
|
* The loaded mesh of every attachment the FILE spells as a link, to what the
|
|
@@ -2077,26 +1951,18 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2077
1951
|
linkedMeshes.set(att, link);
|
|
2078
1952
|
loadedLinks.set(join, att);
|
|
2079
1953
|
}
|
|
2080
|
-
}
|
|
1954
|
+
}
|
|
2081
1955
|
}
|
|
2082
1956
|
}
|
|
2083
1957
|
stats.regionAttachments = regionAttachments.length;
|
|
2084
1958
|
stats.meshAttachments = meshAttachments.length;
|
|
1959
|
+
// What the bodies that moved to `./assertions/bodies/` read of the skins —
|
|
1960
|
+
// the runtime's supply of `SkinEntryFacts`, in the loaded skins' own order
|
|
1961
|
+
// (issue #1025).
|
|
1962
|
+
const skinEntries = spineSkinEntries(data);
|
|
2085
1963
|
|
|
2086
1964
|
// --- A03: every region has finite width/height (case 6c) ---------------
|
|
2087
|
-
check('A03_REGION_WIDTH_HEIGHT_FINITE', () =>
|
|
2088
|
-
// ⟨subject⟩_⟨property⟩: with no region there is no size to find non-finite,
|
|
2089
|
-
// and a loop over nothing used to report that as held (#580).
|
|
2090
|
-
if (regionAttachments.length === 0) return skip('A03_REGION_WIDTH_HEIGHT_FINITE', SKIP_NO_REGION_ATTACHMENT);
|
|
2091
|
-
for (const att of regionAttachments) {
|
|
2092
|
-
if (!Number.isFinite(att.width) || !Number.isFinite(att.height)) {
|
|
2093
|
-
fail('A03_REGION_WIDTH_HEIGHT_FINITE', `region "${att.name}" loaded w=${att.width} h=${att.height}`);
|
|
2094
|
-
}
|
|
2095
|
-
if (att.width <= 0 || att.height <= 0) {
|
|
2096
|
-
fail('A03_REGION_WIDTH_HEIGHT_FINITE', `region "${att.name}" has a non-positive size`);
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
});
|
|
1965
|
+
check('A03_REGION_WIDTH_HEIGHT_FINITE', () => a03RegionWidthHeightFinite(verdicts, skinEntries));
|
|
2100
1966
|
|
|
2101
1967
|
// --- A04: mesh triangles + encoding coherence (case 6f) ----------------
|
|
2102
1968
|
check('A04_MESH_TRIANGLES_AND_ENCODING', () => {
|
|
@@ -2291,11 +2157,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2291
2157
|
});
|
|
2292
2158
|
|
|
2293
2159
|
// --- A11 / A13 / A14: renderer + canvas budgets ----
|
|
2294
|
-
check('A11_NO_CLIPPING_ATTACHMENTS', () =>
|
|
2295
|
-
if (clippingCount > 0) {
|
|
2296
|
-
fail('A11_NO_CLIPPING_ATTACHMENTS', `${clippingCount} clipping attachment(s); the renderer skips them silently`);
|
|
2297
|
-
}
|
|
2298
|
-
});
|
|
2160
|
+
check('A11_NO_CLIPPING_ATTACHMENTS', () => a11NoClippingAttachments(verdicts, skinEntries));
|
|
2299
2161
|
// 📐 The two numbers come from the rig spec's `invariants`, never from here.
|
|
2300
2162
|
// A mesh budget is one consumer's frame time written down — the editor's own
|
|
2301
2163
|
// example projects ship meshes many times denser and they are valid — so a
|
|
@@ -4796,174 +4658,10 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
4796
4658
|
|
|
4797
4659
|
// --- A45: the separable colour timelines own their channels ------------
|
|
4798
4660
|
//
|
|
4799
|
-
//
|
|
4800
|
-
//
|
|
4801
|
-
//
|
|
4802
|
-
|
|
4803
|
-
// applied to A43 itself, against a clause on A10). And the SKIP is the
|
|
4804
|
-
// sharper half: a rig with a `dark` and no separable timeline has A43's
|
|
4805
|
-
// subject present, so a clause there could only PASS on it, which is a pass
|
|
4806
|
-
// for an absent subject.
|
|
4807
|
-
//
|
|
4808
|
-
// 🚨 **What it decides is what makes `rgb` + `alpha` not `rgba`**, and both
|
|
4809
|
-
// clauses are about the file against the runtime:
|
|
4810
|
-
//
|
|
4811
|
-
// 1. **One channel, one timeline.** Each colour timeline poses its
|
|
4812
|
-
// channels at EVERY time — before its first key it writes the setup
|
|
4813
|
-
// value — so when two of one slot share a channel, the one applied
|
|
4814
|
-
// later (the one the file states later) overwrites the other
|
|
4815
|
-
// everywhere and the first one's keys on it are read by nothing
|
|
4816
|
-
// (`SLOT_COLOR_CHANNELS`). It is the shape a converter that writes a
|
|
4817
|
-
// separable `rgb` as `rgba` leaves beside the `alpha` it kept, and it
|
|
4818
|
-
// loads without a word.
|
|
4819
|
-
// 2. **Posed as written.** Stepped to each key's own time, the channels
|
|
4820
|
-
// the timeline writes are the key's: a colour that is not six hex
|
|
4821
|
-
// digits loads as NaN, and a key whose time another key repeats is
|
|
4822
|
-
// read by nothing, and both parse in silence.
|
|
4823
|
-
//
|
|
4824
|
-
// ⚠️ An `rgb` timeline alone that a converter wrote as `rgba` with the
|
|
4825
|
-
// setup alpha is NOT this rule's to see, and nothing that reads only the
|
|
4826
|
-
// file can see it: the result is a correct `rgba`, which is also what an
|
|
4827
|
-
// author keying the light colour and holding its alpha would write. What
|
|
4828
|
-
// differs is what happens UNDER another track that moves the alpha, which
|
|
4829
|
-
// is the consumer's composing rather than the object (CLAUDE.md). That
|
|
4830
|
-
// file SKIPs here — it keys no `rgb` or `alpha` — and says so.
|
|
4831
|
-
//
|
|
4832
|
-
// ⚠️ The required values are parsed HERE, as A43's are, and the channel
|
|
4833
|
-
// table is `timelines.ts`'s rather than the loaded timelines' property ids:
|
|
4834
|
-
// a check that asked the parser what a timeline writes would agree with it
|
|
4835
|
-
// whatever it did. A selftest control holds that table to the runtime's ids.
|
|
4836
|
-
check('A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN', () => {
|
|
4837
|
-
const NAME = 'A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN';
|
|
4838
|
-
/** `rrggbb`, or `rrggbbaa` whose alpha an `rgb` key cannot pose. Null for anything else. */
|
|
4839
|
-
const readRgbHex = (hex: unknown): [number, number, number] | null => {
|
|
4840
|
-
if (typeof hex !== 'string') return null;
|
|
4841
|
-
const body = hex.startsWith('#') ? hex.slice(1) : hex;
|
|
4842
|
-
if (!/^[\da-fA-F]{6}([\da-fA-F]{2})?$/.test(body)) return null;
|
|
4843
|
-
return [0, 2, 4].map((i) => Number.parseInt(body.slice(i, i + 2), 16) / 255) as [number, number, number];
|
|
4844
|
-
};
|
|
4845
|
-
/** Half a byte step for a hex channel; a stored `Float32` for an alpha `value`. */
|
|
4846
|
-
const HEX_STEP = 1 / 510;
|
|
4847
|
-
const FLOAT_STEP = 1e-6;
|
|
4848
|
-
const off = (found: number, want: number, step: number): boolean => !Number.isFinite(found) || Math.abs(found - want) > step;
|
|
4849
|
-
const show = (c: readonly number[]): string => c.map((n) => (Number.isFinite(n) ? n.toFixed(4) : 'NaN')).join(', ');
|
|
4850
|
-
|
|
4851
|
-
// -- the subjects, read off the FILE ---------------------------------
|
|
4852
|
-
const subjects: Array<{ anim: string; slot: string; timelines: Record<string, unknown> }> = [];
|
|
4853
|
-
const rawAnimations = isObj(raw) && isObj(raw.animations) ? raw.animations : {};
|
|
4854
|
-
for (const [animName, anim] of Object.entries(rawAnimations)) {
|
|
4855
|
-
if (!isObj(anim) || !isObj(anim.slots)) continue;
|
|
4856
|
-
for (const [slotName, timelines] of Object.entries(anim.slots)) {
|
|
4857
|
-
if (!isObj(timelines)) continue;
|
|
4858
|
-
if (Array.isArray(timelines.rgb) || Array.isArray(timelines.alpha)) {
|
|
4859
|
-
subjects.push({ anim: animName, slot: slotName, timelines });
|
|
4860
|
-
}
|
|
4861
|
-
}
|
|
4862
|
-
}
|
|
4863
|
-
if (subjects.length === 0) return skip(NAME, SKIP_NO_SEPARABLE_COLOR);
|
|
4864
|
-
stats.separableColorTimelines = subjects.reduce(
|
|
4865
|
-
(n, s) => n + (Array.isArray(s.timelines.rgb) ? 1 : 0) + (Array.isArray(s.timelines.alpha) ? 1 : 0),
|
|
4866
|
-
0,
|
|
4867
|
-
);
|
|
4868
|
-
|
|
4869
|
-
for (const { anim: animName, slot: slotName, timelines } of subjects) {
|
|
4870
|
-
const at = `animation "${animName}" slot "${slotName}"`;
|
|
4871
|
-
// In FILE order, which is the order `readAnimation` pushes them and so
|
|
4872
|
-
// the order they apply in.
|
|
4873
|
-
const colour = Object.keys(timelines).filter((name) => name in SLOT_COLOR_CHANNELS && Array.isArray(timelines[name]));
|
|
4874
|
-
|
|
4875
|
-
// -- clause 1: one channel, one timeline ---------------------------
|
|
4876
|
-
let shared = false;
|
|
4877
|
-
for (const channel of ['rgb', 'alpha', 'dark'] as const) {
|
|
4878
|
-
const writers = colour.filter((name) => SLOT_COLOR_CHANNELS[name].includes(channel));
|
|
4879
|
-
if (writers.length < 2 || !writers.some((name) => name === 'rgb' || name === 'alpha')) continue;
|
|
4880
|
-
shared = true;
|
|
4881
|
-
const last = writers[writers.length - 1];
|
|
4882
|
-
fail(
|
|
4883
|
-
NAME,
|
|
4884
|
-
`${at}: ${writers.map((name) => `"${name}"`).join(' and ')} ${writers.length === 2 ? 'both' : 'all'} key the ${channel === 'rgb' ? 'light rgb' : channel === 'alpha' ? 'alpha' : 'dark colour'} ` +
|
|
4885
|
-
`— each poses it at every time, its setup value before its first key included, so "${last}", which the ` +
|
|
4886
|
-
`file states last, overwrites ${writers.length === 2 ? `"${writers[0]}"` : 'the others'} everywhere and ` +
|
|
4887
|
-
'those keys are read by nothing. Key each channel once: "rgb" and "alpha" on their own key times, or one "rgba"',
|
|
4888
|
-
);
|
|
4889
|
-
}
|
|
4890
|
-
if (shared) continue;
|
|
4891
|
-
|
|
4892
|
-
// -- clause 2: posed as written ------------------------------------
|
|
4893
|
-
//
|
|
4894
|
-
// ⚠️ Two things this does NOT do, both measured rather than skipped:
|
|
4895
|
-
// it does not read the loaded timeline's CLASS back, and it does not
|
|
4896
|
-
// hold a channel the timeline leaves alone to the setup pose. Against
|
|
4897
|
-
// the linked parser neither can fail — every `rgb` / `alpha` array that
|
|
4898
|
-
// loads at all loads as an `RGBTimeline` / `AlphaTimeline`, and the
|
|
4899
|
-
// three other outcomes (an empty array, a slot the skeleton lacks, a
|
|
4900
|
-
// name outside the switch) throw at `A00` — so either would be a clause
|
|
4901
|
-
// nobody can see fire. The selftest reads both off spine-core directly
|
|
4902
|
-
// (`S82`–`S84`), where they are measurements of the runtime rather than
|
|
4903
|
-
// checks on a file.
|
|
4904
|
-
if (!data.findAnimation(animName)) {
|
|
4905
|
-
fail(NAME, `${at}: the loaded skeleton has no animation "${animName}"`);
|
|
4906
|
-
continue;
|
|
4907
|
-
}
|
|
4908
|
-
for (const name of ['rgb', 'alpha'] as const) {
|
|
4909
|
-
const keys = timelines[name];
|
|
4910
|
-
if (!Array.isArray(keys)) continue;
|
|
4911
|
-
const where = `${at} ${name}`;
|
|
4912
|
-
for (const rawKey of keys) {
|
|
4913
|
-
if (!isObj(rawKey)) continue;
|
|
4914
|
-
const time = typeof rawKey.time === 'number' ? rawKey.time : 0;
|
|
4915
|
-
const skeleton = new Skeleton(data);
|
|
4916
|
-
const state = new AnimationState(new AnimationStateData(data));
|
|
4917
|
-
state.setAnimation(0, animName, false);
|
|
4918
|
-
skeleton.setupPose();
|
|
4919
|
-
skeleton.update(0);
|
|
4920
|
-
skeleton.updateWorldTransform(Physics.reset);
|
|
4921
|
-
// At the key as the runtime stores it, not one float step before
|
|
4922
|
-
// it — see `atStoredKey` (issue #771).
|
|
4923
|
-
state.update(atStoredKey(time));
|
|
4924
|
-
state.apply(skeleton);
|
|
4925
|
-
const posed = skeleton.slots.find((s) => s.data.name === slotName)?.appliedPose;
|
|
4926
|
-
if (!posed) {
|
|
4927
|
-
fail(NAME, `${where} (t=${time}): the posed skeleton has no slot "${slotName}" to read`);
|
|
4928
|
-
continue;
|
|
4929
|
-
}
|
|
4930
|
-
const light = [posed.color.r, posed.color.g, posed.color.b, posed.color.a];
|
|
4931
|
-
if (name === 'rgb') {
|
|
4932
|
-
const stated = readRgbHex(rawKey.color);
|
|
4933
|
-
if (stated === null) {
|
|
4934
|
-
fail(
|
|
4935
|
-
NAME,
|
|
4936
|
-
`${where} (t=${time}): the key states color ${JSON.stringify(rawKey.color)} — an rgb key is six hex ` +
|
|
4937
|
-
`digits — and the runtime poses (${show(light.slice(0, 3))})`,
|
|
4938
|
-
);
|
|
4939
|
-
} else if (light.slice(0, 3).some((n, i) => off(n, stated[i], HEX_STEP))) {
|
|
4940
|
-
fail(
|
|
4941
|
-
NAME,
|
|
4942
|
-
`${where} (t=${time}): rgb posed (${show(light.slice(0, 3))}), the key states ` +
|
|
4943
|
-
`${JSON.stringify(rawKey.color)} = (${show(stated)})`,
|
|
4944
|
-
);
|
|
4945
|
-
}
|
|
4946
|
-
} else {
|
|
4947
|
-
// `readTimeline1(…, 0, 1)`: an absent `value` IS 0, and an editor
|
|
4948
|
-
// omits it there, so absence is read the parser's way rather than
|
|
4949
|
-
// as a malformed key.
|
|
4950
|
-
const stated = rawKey.value === undefined ? 0 : rawKey.value;
|
|
4951
|
-
if (typeof stated !== 'number' || off(light[3], stated, FLOAT_STEP)) {
|
|
4952
|
-
fail(
|
|
4953
|
-
NAME,
|
|
4954
|
-
`${where} (t=${time}): alpha posed ${show([light[3]])}, the key states value ${JSON.stringify(rawKey.value)}` +
|
|
4955
|
-
(typeof stated !== 'number'
|
|
4956
|
-
? ' — an alpha key\'s value is a number'
|
|
4957
|
-
: stated < 0 || stated > 1
|
|
4958
|
-
? ' — the runtime clamps a posed alpha to 0..1'
|
|
4959
|
-
: ''),
|
|
4960
|
-
);
|
|
4961
|
-
}
|
|
4962
|
-
}
|
|
4963
|
-
}
|
|
4964
|
-
}
|
|
4965
|
-
}
|
|
4966
|
-
});
|
|
4661
|
+
// The body, and the argument for the rule, are `./assertions/bodies/a45.ts`
|
|
4662
|
+
// since issue #1025: the same body runs over the model document. What it
|
|
4663
|
+
// reads off spine-core's loaded skeleton is `spineSlotColourFacts`.
|
|
4664
|
+
check('A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN', () => a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten(verdicts, spineSlotColourFacts(raw, data)));
|
|
4967
4665
|
|
|
4968
4666
|
// --- A44: a linked mesh states no geometry of its own ------------------
|
|
4969
4667
|
//
|
|
@@ -5317,17 +5015,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
5317
5015
|
// green build had ever contained one; making that state legal is exactly what
|
|
5318
5016
|
// #608 does, so the same state has to stop printing four passes about nothing.
|
|
5319
5017
|
// `SKIP_NO_ATLAS_PAGE` is one string for all four because it is one condition.
|
|
5320
|
-
check('A17_ATLAS_PAGE_FILES_EXIST', () => {
|
|
5321
|
-
if (!atlas) return skip('A17_ATLAS_PAGE_FILES_EXIST', SKIP_NO_ATLAS);
|
|
5322
|
-
// The other absence, one ring in (#580): the atlas LOADED and declares no
|
|
5323
|
-
// page, so there is no file to find missing. `!atlas` and "zero pages" print
|
|
5324
|
-
// different reasons because they are different facts about the artifact.
|
|
5325
|
-
if (atlas.pages.length === 0) return skip('A17_ATLAS_PAGE_FILES_EXIST', SKIP_NO_ATLAS_PAGE);
|
|
5326
|
-
for (const page of atlas.pages) {
|
|
5327
|
-
const abs = resolve(input.atlasDir, page.name);
|
|
5328
|
-
if (!existsSync(abs)) fail('A17_ATLAS_PAGE_FILES_EXIST', `page "${page.name}" is not on disk at ${abs}`);
|
|
5329
|
-
}
|
|
5330
|
-
});
|
|
5018
|
+
check('A17_ATLAS_PAGE_FILES_EXIST', () => a17AtlasPageFilesExist(verdicts, { atlas }, input));
|
|
5331
5019
|
check('A06_ATLAS_PAGE_SIZE_MATCHES_PNG', () => {
|
|
5332
5020
|
if (!atlas) return skip('A06_ATLAS_PAGE_SIZE_MATCHES_PNG', SKIP_NO_ATLAS);
|
|
5333
5021
|
if (atlas.pages.length === 0) return skip('A06_ATLAS_PAGE_SIZE_MATCHES_PNG', SKIP_NO_ATLAS_PAGE);
|
|
@@ -6236,7 +5924,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
6236
5924
|
for (const name of ASSERTION_NAMES) {
|
|
6237
5925
|
if (reported.has(name)) continue;
|
|
6238
5926
|
const kind = ASSERTION_KIND[name];
|
|
6239
|
-
if (kind !== 'validity' && !
|
|
5927
|
+
if (kind !== 'validity' && !kindRunsUnder(kind, profile)) profileSkipped.push({ assertion: name, kind });
|
|
6240
5928
|
else if (!roundTrip) skip(name, SKIP_NO_SKELETON);
|
|
6241
5929
|
else {
|
|
6242
5930
|
fail(
|