spine-rigc 0.30.0 → 0.32.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/docs/AUTHORING.md CHANGED
@@ -169,7 +169,7 @@ What the flags mean:
169
169
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
170
170
  | `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
171
171
  | `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
172
- | `--profile` | `spine` = the 31 validity rules (**the default**) · `spine-html` = all 46, opt-in |
172
+ | `--profile` | `spine` = the 32 validity rules (**the default**) · `spine-html` = all 47, opt-in |
173
173
  | `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` is the one command that takes it more than once** — repeat it 2–4 times, one per pane, labelled A, B, C, D in the order given; everywhere else a repeat is a typo and is refused |
174
174
  | `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview`, and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote`, so is a name that only *some* candidates have |
175
175
  | `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
@@ -360,6 +360,39 @@ it hits instead of leaving it to be discovered:
360
360
  says so rather than offering one, and the repair is to re-export the page at the
361
361
  size the atlas declares, or repack.
362
362
 
363
+ 📐 **`explain` does not refuse such a page — it withholds what it would have
364
+ measured off it** ([#750](https://github.com/firejune/rigc/issues/750)). It never
365
+ gates, a runtime draws the page, and most of its report — bones, slots, timelines
366
+ — reads no texel at all, so the pack still compiles and the report still prints.
367
+ What it no longer prints is a figure taken at the coordinates the atlas states,
368
+ because on such a file those are another part of the picture: measured on a pack
369
+ at half resolution, an authored mesh that covers 100.00% of its art and reaches
370
+ 16.00px past it printed **68.49%** and **76.24px**, and a `grid` whose depth sheet
371
+ reads 10 of its 12 vertices on undrawn texels printed **12 of 12**. The page is
372
+ named once where the report starts, with the ratio `A06` refuses it by, and every
373
+ withheld figure says so where it would have stood:
374
+
375
+ ```bash
376
+ # .. page "hero.png" declares 4096x4096 and its PNG is 2048x2048 — 0.5000 of the declared width and 0.5000
377
+ # of the declared height: every figure below taken off this page's texels is withheld, and says so where
378
+ # it would have stood. …
379
+ # fan authored 9 vertices / 8 triangles (budget 64) bones=[fan] fit not measured: page "hero.png" …
380
+ # the count of vertices on undrawn texels is not measured: page "hero.png" …
381
+ ```
382
+
383
+ `build`'s `MESH` lines withhold the same figures in the same words and print no
384
+ page line of their own, because `A06` is `build`'s statement of that fact and a
385
+ second one would be two refusals of one page. A **`contour`** is the one reading
386
+ that cannot be withheld, because its outline *is* its geometry: it is refused as a
387
+ compile error carrying `A06`'s whole sentence — ratio and repair — on `explain`
388
+ and on `build` alike, where on `build` it arrives before the gate would have said
389
+ it. Carry out the repair and every figure comes back. ⚠️ They come back measured
390
+ on the **coarser** texels the page really has, so they need not equal the figures
391
+ of the pack the page was halved from: on the same fixture the `scale: 0.5`
392
+ restatement reads the authored mesh at 100.00% coverage reaching **8.00px** past
393
+ it (the overshoot is counted in the page's own texels), and traces the contour as
394
+ 11 vertices where the full-resolution page traced 15.
395
+
363
396
  🚨 **A page that is not a PNG is refused by name, before anything is compiled
364
397
  against it** ([#732](https://github.com/firejune/rigc/issues/732)). rigc reads PNG
365
398
  and nothing else — the size `A06` judges, the alpha `A19` judges, the renderer and
@@ -594,7 +627,7 @@ the first:
594
627
 
595
628
  | gutter | meaning |
596
629
  | --- | --- |
597
- | `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read — `point`, an attachment `sequence`, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
630
+ | `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read — `point`, a `sequence` block the parser would read as some other series, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
598
631
  | `JUDGE` | the skeleton cannot answer and somebody has to: the stage, and each animation's duration |
599
632
  | `LOSS` | the skeleton's spelling and rigc's differ, on purpose, and the line says how. A path attachment's `lengths` is the one that matters — it is `PathConstraint`'s own four-sample measurement rather than an arc length (#560), so a transcribed one would freeze whatever produced the source. The header ones are cheaper: `HEADER_BOOKKEEPING` for a field the spec has no home for, `HEADER_REDERIVED` for the version string, `HEADER_ORIGIN` for an origin the source left to the format and the rebuild writes out (#622) |
600
633
 
@@ -660,6 +693,16 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
660
693
  ([#697](https://github.com/firejune/rigc/issues/697), §5.1). ⚠️ `--profile`,
661
694
  `--pack`, `--page-size`, `--padding` and `--copy-images` are `build`'s and are
662
695
  not here: four of them decide what is *written*, and this command writes nothing.
696
+
697
+ 📐 **What it will not measure: the texels of a page that is not its declared
698
+ size** ([#750](https://github.com/firejune/rigc/issues/750)). Under
699
+ `--atlas-in`, a page whose PNG is not the `size:` its atlas states is named once
700
+ at the top of the report, and the figures that would have been read off it — an
701
+ authored mesh's `covers …% of the art, reaching …px past it`, a depth sheet's
702
+ count of vertices on undrawn texels — are replaced by `fit not measured: …` and
703
+ `… is not measured: …` naming the page and the ratio. A `contour` on such a page
704
+ is refused, since its outline is read off those texels. Why, the quoted lines,
705
+ and the repair: §0.2.
663
706
  - **`diff`** compares two skeletons and reports **a ratio per measure** in six
664
707
  sections (bones, slots, attachments, constraints, animations, events). It
665
708
  deliberately does not combine them into a score: a rig with the right skeleton
@@ -1071,7 +1114,7 @@ it would simply be a second root.
1071
1114
  | `rotation` | degrees, counter-clockwise, y **up** | `0` |
1072
1115
  | `scaleX`, `scaleY` | | `1` |
1073
1116
  | `shearX`, `shearY` | | `0` |
1074
- | `inherit` | `normal` · `onlyTranslation` · `noRotationOrReflection` · `noScale` · `noScaleOrReflection` | `normal` |
1117
+ | `inherit` | `normal` · `onlyTranslation` · `noRotationOrReflection` · `noScale` · `noScaleOrReflection`. The first letter's case is free (`NoScale` loads) and the rest is exact: the runtime folds that one letter and nothing else, so `NOSCALE` is refused — it used to compile and load as **no mode at all** ([#733](https://github.com/firejune/rigc/issues/733)). An animation can change it over time: the `inherit` track (§4.4) | `normal` |
1075
1118
  | `skin` | `BoneData.skinRequired` | `false` |
1076
1119
  | `color` | `rrggbbaa`, editor affordance | — |
1077
1120
  | `icon` | the editor's icon for this bone, e.g. `arrowsB`; editor affordance, no rendering effect. Copied through verbatim — no assertion checks the name, because the icon vocabulary is the editor's and an unknown one is not an error | — |
@@ -1180,13 +1223,15 @@ the default `type`:
1180
1223
  | `x`, `y` | offset from the bone, in the bone's local space |
1181
1224
  | `rotation` | degrees; cancels a rotated bone for a plate authored screen-upright |
1182
1225
  | `scaleX`, `scaleY`, `color` | as Spine |
1226
+ | `sequence` | a **numbered image series** instead of one region: `{ "count", "start"?, "digits"?, "setup"? }`, and `path` (or the placeholder) is the series' stem. No `image` beside it — the frames are the images. §3.4.3 |
1183
1227
 
1184
1228
  **Mesh attachment** ([Spine: meshes](http://esotericsoftware.com/spine-meshes)) —
1185
1229
  either authored geometry (`uvs` + `triangles` + geometry) **or** a `generator`,
1186
1230
  never both. `hull`, `edges`, `width` and `height` may be stated; whichever is
1187
1231
  omitted, rigc derives — `hull` and `edges` from the triangles, the size from the
1188
- PNG — and the rules are a few paragraphs down. `type`, `image`, `path` and `color`
1189
- mean exactly what they mean on a region.
1232
+ PNG — and the rules are a few paragraphs down. `type`, `image`, `path`, `color` and
1233
+ `sequence` mean exactly what they mean on a region (a sequence mesh takes authored
1234
+ geometry, never a `generator` — §3.4.3).
1190
1235
 
1191
1236
  🔑 **`path` is one rule for both kinds.** A mesh derives it from `image` the way a
1192
1237
  region does: stated wins, otherwise the PNG's basename when that differs from the
@@ -2288,6 +2333,69 @@ nothing at all. `check` then compares blank against blank and reports a perfect
2288
2333
  `--skin <name>` to both, once per skin (**§9**); `tools/editor_roundtrip.ts`
2289
2334
  loops over every skin the build declares for the same reason.
2290
2335
 
2336
+ #### 3.4.3 `sequence` — a numbered image series on one attachment
2337
+
2338
+ A region, a mesh or a linked mesh can draw one of a **series** of atlas regions
2339
+ instead of one: a flame, a blink, a spinning coin. The attachment says how many
2340
+ frames there are and how they are named, and a `sequence` timeline (§4.13) says
2341
+ which one shows. The format's own block, spelled as the file spells it
2342
+ (`readSequence`, `SkeletonJson.js:641-649`):
2343
+
2344
+ | Field | Meaning |
2345
+ | --- | --- |
2346
+ | `count` | how many frames. **Required** — the parser's default is 0, and a series of no frames loads holding no region and draws nothing, without an error |
2347
+ | `start` | the number the first frame's name carries. Parser default 1 |
2348
+ | `digits` | zero-pad the frame number to at least this many digits. Parser default 0 (no padding) |
2349
+ | `setup` | the frame the setup pose shows, 0-based. Parser default 0. ⚠️ Spelled `setup`, as the file spells it — `setupIndex` is the runtime's field name and is refused as a key this compiler does not read |
2350
+
2351
+ ```json
2352
+ "glint": { "glint": { "path": "glint_", "sequence": { "count": 4, "start": 1, "digits": 4 } } }
2353
+ ```
2354
+
2355
+ Frame `i` is the atlas region **`<stem><start + i>`**, the number left-padded with
2356
+ zeros to `digits` (`Sequence.getPath`) — so the four frames above are `glint_0001`
2357
+ to `glint_0004`. The stem is `path`, or the placeholder when no `path` is stated
2358
+ (`path` defaults to the attachment's name, exactly as for one region). Each frame is
2359
+ resolved **by name**: on the loose route it is the PNG `<images>/<frame>.png`, and
2360
+ under `--atlas-in` it is the pack's region of that name. A region name may carry a
2361
+ folder — an editor's `fx/flame_0001` is `images/fx/flame_0001.png` — and it is the
2362
+ full name that is looked up.
2363
+
2364
+ 🚫 **A missing frame is refused by name, with its number and the name looked for** —
2365
+ the compiler never draws one frame in place of another:
2366
+
2367
+ ```
2368
+ skin "default" slot "glint" attachment "glint": sequence frame 4 of 5 (number 5) is the region "glint_0005", and there is no PNG for it at …/glint_0005.png
2369
+ ```
2370
+
2371
+ The loader's own miss would be `Region not found in atlas: glint_0005 (attachment:
2372
+ glint)`, which says neither that the region was a frame nor of which series.
2373
+
2374
+ ⚠️ **The rest of what is refused**, each a series the parser would load as something
2375
+ other than what was written (measured on spine-core 4.3.13,
2376
+ [#729](https://github.com/firejune/rigc/issues/729)):
2377
+
2378
+ - a `setup` at or past `count` — `Sequence.resolveIndex` clamps it to the last frame
2379
+ (`setup: 7` on four frames showed frame 4);
2380
+ - a fractional `count`, `start`, `digits` or `setup` — `start: 1.5` would ask the
2381
+ atlas for `glint_1.5`;
2382
+ - an `image` beside `sequence` — one file names one region, and the series names
2383
+ `count` of them;
2384
+ - a `generator` beside it on a mesh — a generator traces one plate, and which frame
2385
+ it should trace is not something the spec says. Author the geometry; every frame
2386
+ shares it;
2387
+ - a `sequence` on a `boundingbox`, `clipping` or `path` — the parser reads the key
2388
+ only on the three kinds that draw a region, so it would be dropped in silence. The
2389
+ refusal names region, mesh and linkedmesh.
2390
+
2391
+ `width` and `height` are the attachment's one size, every frame drawn into it. Omit
2392
+ them and rigc takes the frames' size — **only when every frame measures the same**;
2393
+ frames of different sizes are refused until you state the size, because picking one
2394
+ of them would be the compiler choosing a value. Under `--atlas-in` a stated size that
2395
+ disagrees with a packed frame is refused, as it is for one region.
2396
+ `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` (§5.2) holds the block and
2397
+ every frame the timelines show against the file.
2398
+
2291
2399
  ### 3.5 `constraints` — 4.3's single typed array
2292
2400
 
2293
2401
  Spine 4.3 folds every constraint into one `constraints` array with a `type`
@@ -2338,14 +2446,20 @@ carrying here:
2338
2446
  `true` and leaves the rest alone (`Animation.js:2066-2072`). So the flag is one
2339
2447
  constraint's **opt-in to being driven in bulk**, per tuning value, and it does
2340
2448
  nothing on its own. The parser's default for all seven is `false`.
2341
- ⚠️ **rigc emits them and cannot emit the timeline that reads them.** Every
2342
- physics track in a motion spec names its constraint and that name is resolved
2343
- against the rig — the empty name is refused, `animation "A" keys unknown physics
2344
- constraint "" (the rig declares: …)` — so the flags are for a player, or a
2345
- later hand-edit, that supplies one. What rigc does with them is **pass them
2346
- through**, `false` included: measured, a constraint stating none emits none, and
2347
- one stating `"windGlobal": false` emits `"windGlobal": false` rather than
2348
- dropping it the way the motion spec's `physics` table drops a default (§4.6).
2449
+ The motion spec keys that timeline as **`"physics": "*"`** (§4.4,
2450
+ [#726](https://github.com/firejune/rigc/issues/726)), and rigc writes it under
2451
+ the empty name. Until then rigc emitted the flags and could not emit the
2452
+ timeline that reads them: every physics track named its constraint and the
2453
+ empty name was refused as `keys unknown physics constraint ""`. What rigc does
2454
+ with the flags is **pass them through**, `false` included: measured, a
2455
+ constraint stating none emits none, and one stating `"windGlobal": false` emits
2456
+ `"windGlobal": false` rather than dropping it the way the motion spec's
2457
+ `physics` table drops a default (§4.6). ⚠️ That table has **no** `…Global`
2458
+ field, so a constraint a `"*"` track is meant to reach is declared here, in the
2459
+ rig spec's `constraints`.
2460
+ - `"*"` is **reserved** as a physics constraint's name, in the rig spec and in
2461
+ §4.6's table alike: a track naming it could mean either. `compile` refuses one
2462
+ by name.
2349
2463
  - ⚠️ `src/rig.ts` called the physics `ScaleYMode` key **`scaleYMode`** until
2350
2464
  issue #545 — the runtime's field name rather than the format's key — and nothing
2351
2465
  read it, so a spec that wrote `scaleYMode` set no mode and said nothing. A rig
@@ -2601,8 +2715,8 @@ driven constraint and its kind, the property, both `constraints` indices, the
2601
2715
  runtime class whose `update` does the reading and the animation the key sits in,
2602
2716
  and its repair is the reorder. It is a rule of its own rather than a clause of
2603
2717
  `A40` because `A40`'s population is the sliders whose `mix` nothing keys — the
2604
- exclusion *is* the shape of the hole — and not a clause of `A37`, whose `keyedBy`
2605
- asks whether some animation keys the `mix` and never which one.
2718
+ exclusion *is* the shape of the hole — and not a clause of `A37`, which asks
2719
+ whether some animation keys the `mix` above 0 and never which one.
2606
2720
  ⭐ **The runtime repairs this for bones and not for constraints**, which is why an
2607
2721
  author cannot reason it out from the bone case: `Slider.sort` clears `sorted` on
2608
2722
  every bone its animation keys so those bones re-sort *after* the slider, while
@@ -3057,6 +3171,7 @@ time puts it here.
3057
3171
  | `ik` | IK constraint timelines — §4.9. Not a track: its keys carry named fields, not one `v` |
3058
3172
  | `transform` | transform constraint timelines — §4.10. Same reason |
3059
3173
  | `deform` | deform timelines — §4.11. Same reason |
3174
+ | `sequence` | which frame of an attachment's numbered series shows — §4.13. Same reason |
3060
3175
 
3061
3176
  `groups` (`name → [member, …]`) lets one track target several bones, slots or
3062
3177
  physics constraints at once; `lag` shifts every key of a track, and `stagger`
@@ -3067,7 +3182,7 @@ group that names a member twice is a compile error, and so is one that names non
3067
3182
  ⭐ **Which of the three a group's members are is decided by the `property`, not
3068
3183
  by the group.** A group declares names and nothing else; the compiler reads the
3069
3184
  property first — a physics timeline makes the members physics constraints, one of
3070
- a bone's ten makes them bones, and `attachment`/`rgba` makes them slots — and then
3185
+ a bone's eleven makes them bones, and `attachment`/`rgba` makes them slots — and then
3071
3186
  resolves every member against the rig as that. So a `group` is the one target
3072
3187
  where the property picks the family rather than the other way round (§4.4's ⭐ is
3073
3188
  about the three **constraint** families, which are picked by the field), and a
@@ -3109,6 +3224,7 @@ a deform). Folding them in would make `v` mean four different things depending o
3109
3224
  | --- | --- | --- |
3110
3225
  | `bone` | `translate`, `scale`, `shear` | `[x, y]` |
3111
3226
  | `bone` | `translatex`, `translatey`, `scalex`, `scaley`, `shearx`, `sheary`, `rotate` | `[value]` |
3227
+ | `bone` | `inherit` | a mode name, not an array: `normal` · `onlyTranslation` · `noRotationOrReflection` · `noScale` · `noScaleOrReflection` — the five a bone's setup `inherit` takes (§3.2), read by the same rule. **Stepped by the format**: the mode changes AT the key and holds until the next one, and before the first key the bone is in its setup mode. So a key carries no `ease` and no `curve`. What it changes is how much of the parent's world transform the bone takes: `normal` all of it, `onlyTranslation` the position alone, `noRotationOrReflection` everything but the rotation, `noScale` / `noScaleOrReflection` everything but the scale |
3112
3228
  | `slot` | `rgba` | `[r, g, b, a]` in 0..1 |
3113
3229
  | `slot` | `rgb` | `[r, g, b]` in 0..1 — the light colour **without** its alpha, which stays wherever the setup pose or an `alpha` track puts it. Emitted as `{ time, color: "rrggbb" }` |
3114
3230
  | `slot` | `alpha` | `[a]`, **0 to 1** — the light colour's alpha alone; the rgb is left where it is. Emitted as `{ time, value }`, a number rather than a byte |
@@ -3118,6 +3234,7 @@ a deform). Folding them in would make `v` mean four different things depending o
3118
3234
  | `physics` | `inertia`, `strength`, `damping`, `mass`, `wind`, `gravity` | `[value]` — the constraint's own tuning, keyed over time |
3119
3235
  | `physics` | `mix` | `[mix]`, **0 or more** — the constraint's authority |
3120
3236
  | `physics` | `reset` | `null` — the key *is* the event |
3237
+ | `physics: "*"` | any of the eight above | as above — the timeline that names **no** constraint. The value timelines write every physics constraint whose rig-spec entry declares that property global (`"strengthGlobal": true` for `strength`; §3.5) and leave every other one alone; `reset` resets every physics constraint and asks no flag |
3121
3238
  | `path` | `position`, `spacing` | `[value]` — see §4.12 |
3122
3239
  | `path` | `mix` | `[mixRotate, mixX, mixY]` — one timeline, three channels |
3123
3240
  | `slider` | `time` | `[seconds]` — where in its animation the slider sits |
@@ -3126,10 +3243,10 @@ a deform). Folding them in would make `v` mean four different things depending o
3126
3243
  Translate values are **relative to the bone's setup position**; scale values are
3127
3244
  multipliers where `1` is setup; rotation is in degrees.
3128
3245
 
3129
- ⚠️ **A `bone` track's `property` is one of the ten above, and anything else is a
3246
+ ⚠️ **A `bone` track's `property` is one of the eleven above, and anything else is a
3130
3247
  compile error** — `animation "A" bone "B" has no timeline "P" (it has: translate,
3131
- translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate)`,
3132
- §5.1's row. The ten are the two rows above read as one list, in the order the
3248
+ translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate,
3249
+ inherit)`, §5.1's row. The eleven are the three rows above read as one list, in the order the
3133
3250
  message prints them, and they are the emitter's own dispatch table (`BONE_TRACKS`
3134
3251
  in `src/compile.ts`): `resolveTargets` asks that table which family a track
3135
3252
  belongs to, `compileValueTrack` writes a key out of the shape it finds there, and
@@ -3149,13 +3266,15 @@ accepts is what it accepts.
3149
3266
  **constraint** property written on a bone track (`mix`, `inertia`, `position`,
3150
3267
  …) never reaches this refusal at all — it is refused first, by the row that
3151
3268
  names the field its constraint's name belongs in (§4.12).
3152
- - **Nothing derives this page's copy of the ten from the table.** What keeps the
3269
+ - **Nothing derives this page's copy of the eleven from the table.** What keeps the
3153
3270
  two in step is the control that quotes the message — `RF26` in `selftest.ts` —
3154
3271
  which goes red if the list ever widens without this page moving with it, and
3155
3272
  `RF27` holds the slot clause the same way. The selftest's spelling census
3156
- (`PS144`) reads the ten off the same message and compares them against the ten
3157
- it actually poses, both ways, which is what makes the list checkable at all: it
3158
- was stated there too until the refusal had something to state.
3273
+ (`PS144`) reads the eleven off the same message and compares them against the
3274
+ eleven it actually poses, both ways, which is what makes the list checkable at
3275
+ all: it was stated there too until the refusal had something to state. It was
3276
+ ten until [#733](https://github.com/firejune/rigc/issues/733) added `inherit`,
3277
+ and that landing is what moved `RF26`, this page and the census together.
3159
3278
 
3160
3279
  ⚠️ **A `slot` track's `property` is one of the six above, and anything else is a
3161
3280
  compile error** — `animation "A" slot "X" has no timeline "P" (it has:
@@ -3228,8 +3347,8 @@ needs 4 channels, got 1`, a message about a key you had not written.
3228
3347
  here, and would be refused by the runtime's own reader too (`Invalid timeline
3229
3348
  type for a slot`). `A12_NO_DARK_COLOR` refuses `rgb2` — and `rgba2`, and the
3230
3349
  slot field — in a file under the `spine-html` profile (SPEC_COVERAGE §2.1).
3231
- `sequence` is a timeline on an **attachment**, not on a slot, and rigc does
3232
- not emit that one.
3350
+ `sequence` is a timeline on an **attachment**, not on a slot — it is the
3351
+ family beside `deform`, §4.13.
3233
3352
 
3234
3353
  ⚠️ **A `group` track's `property` is one of those two lists or the physics one,
3235
3354
  and anything else is a compile error** — `animation "A" group "G" has no timeline
@@ -3283,6 +3402,17 @@ a delta from the constraint's own setting.
3283
3402
  a file rigc did not write, naming the animation, the constraint, the key time
3284
3403
  and the value — so the compiler is where a spec you wrote is refused, and the
3285
3404
  assertion is where an import is.
3405
+ - ⚠️ **`damping`'s bound depends on the constraint's `fps`, and a rig played at
3406
+ 60 fps hides it** ([#748](https://github.com/firejune/rigc/issues/748)). The
3407
+ decay is `damping ** (60 * step)` with `step` = `1 / fps`, so the exponent is
3408
+ exactly 1 at 60 fps, where a negative damping only flips the velocity's sign
3409
+ each step and can look like a jiggle settling. Wherever `60 / fps` is not a
3410
+ whole number the same negative is raised to a fractional power, which is NaN.
3411
+ [measured] one planted `damping` key of −0.5 on the generated physics fixture
3412
+ stays finite at 60 fps and at 30 (exponent 2), and is NaN within three steps of
3413
+ the key at 45 and at 120. `1` never decays and above `1` diverges at every
3414
+ rate. The refusal names the exponent and the rate for this reason, and `T101`
3415
+ holds it to the two-rate measurement.
3286
3416
  - 🚫 **`inertia`, `wind` and `gravity` are bounded nowhere, and neither is the top
3287
3417
  of `mix`.** The runtime documents no range for the first three, and
3288
3418
  `PhysicsConstraintPose` documents `mix` as "a percentage (0+)" — so a negative
@@ -3299,7 +3429,15 @@ a delta from the constraint's own setting.
3299
3429
  back ([#727](https://github.com/firejune/rigc/issues/727)). At rest the two
3300
3430
  part ways: a `strength` of 0 is a constraint nothing pulls back, which `A23`
3301
3431
  refuses, and a `mix` of 0 is a constraint muted until an animation keys it
3302
- above 0 — the next bullet.
3432
+ above 0 — the next bullet. A **negative** setup `strength` is refused too, with
3433
+ a different sentence, because it is a different rig: the restoring term is
3434
+ added to the offset instead of taken out, so the offset is **pushed away** and
3435
+ grows — [measured] resting at −100 on the generated physics fixture it grew
3436
+ 28.35× over 0.5 s with no sign change, where resting at 100 it swung back
3437
+ through 0. `A23` reads which sentence to print off the `strength` row of
3438
+ `PHYSICS_POSE_RULES` (its `outside` arms), and the key's refusal quotes the
3439
+ negative arm from the same row, so the two cannot say different things about
3440
+ one number ([#748](https://github.com/firejune/rigc/issues/748)).
3303
3441
  - ⚠️ **A setup `mix` of `0` is legal when some animation keys it above 0** ([#743](https://github.com/firejune/rigc/issues/743)).
3304
3442
  A constraint muted *at rest* is a rig whose physics is off until an animation
3305
3443
  switches it on, which is a design rather than the silence `A23` was built for.
@@ -3309,6 +3447,11 @@ a delta from the constraint's own setting.
3309
3447
  at 1 does, and keyed-to-0 poses it exactly where one with no timeline at all
3310
3448
  does. The unnamed global timeline counts as a key for every constraint whose
3311
3449
  own `mixGlobal` is set (§3.5), and so does an animation only a slider applies.
3450
+ "Keyed above 0" means **any value the timeline poses**, which includes a
3451
+ Bezier between two keys of 0 whose handles lie above 0: the runtime
3452
+ interpolates through the curve's samples, not between the keys, and such a
3453
+ pair moves the bone ([#752](https://github.com/firejune/rigc/issues/752)). The
3454
+ same reading is `A36`'s and `A37`'s (§4.12).
3312
3455
  - 📏 **What a `strength` key of `0` costs, measured through spine-core** on the
3313
3456
  generated overlay fixture, stepping at 60 fps from `Physics.reset` (#727). With
3314
3457
  no wind or gravity the offset coasts to a limit rather than running away — a
@@ -3322,6 +3465,28 @@ a delta from the constraint's own setting.
3322
3465
  the first sub-step and **still NaN after the restoring key**, and a keyed
3323
3466
  `damping` of `2` was still 6.9e4 two seconds later.
3324
3467
 
3468
+ **`"physics": "*"` is the physics timeline that names no constraint**
3469
+ ([#726](https://github.com/firejune/rigc/issues/726)). The skeleton file writes it
3470
+ under the empty name — `animations.<a>.physics[""]`, which `SkeletonJson` loads as
3471
+ constraint index `-1` without looking anything up — and the runtime then applies it
3472
+ to every active physics constraint whose own data declares the keyed property
3473
+ global (`Animation.js:2067-2075`). So what it drives is decided in the **rig spec**,
3474
+ by the seven `…Global` flags of §3.5, one per value timeline; `reset` is the
3475
+ exception and resets every physics constraint. [measured] over three constraints of
3476
+ which two declare `strengthGlobal`, a `"*"` `strength` track keyed to 35 poses those
3477
+ two at 35 and leaves the third at its setup 100.
3478
+
3479
+ - ⚠️ **A `"*"` track that reaches no constraint is a compile error**, naming the flag
3480
+ and the constraints that do not declare it: the runtime would walk every
3481
+ constraint, skip each, and the file would parse and do nothing. Set the flag on
3482
+ the constraints the track is for, or key one by name.
3483
+ - 🚫 **`"physics": ""` is refused**, and deliberately: the empty string is the
3484
+ likeliest shape of a target somebody forgot to fill in, and a forgotten target
3485
+ that quietly meant "every global constraint" is the silence this format exists to
3486
+ name. The refusal points at `"*"`.
3487
+ - ⚠️ `"*"` is a track's `physics` field and nothing else — a `group` that lists it
3488
+ is refused, and so is a constraint called `"*"` (§3.5).
3489
+
3325
3490
  On a track that names a `group`, one key's `v` may instead be a **map keyed by
3326
3491
  member name**, whose entries are each exactly the `v` above — or a `derive`
3327
3492
  model the compiler evaluates per member. §4.5.1.
@@ -3641,10 +3806,14 @@ mass?, wind?, gravity?, mix?, fps?, limit? }`. These are emitted into the 4.3
3641
3806
  be **keyed over time** as `tracks` entries naming this constraint (§4.4); this
3642
3807
  table is the value at rest, and a timeline overrides it while it plays. `mass: 0` becomes an infinite inverse mass and `damping ≥ 1`
3643
3808
  never settles — both are `A23`, here and on every timeline key that states them
3644
- ([#610](https://github.com/firejune/rigc/issues/610)). ⚠️ `strength: 0` is `A23` **here and not on a key**:
3809
+ ([#610](https://github.com/firejune/rigc/issues/610)); a `damping` at or below 0
3810
+ is refused as well, and below 0 whether it is NaN depends on `fps` — §4.4
3811
+ ([#748](https://github.com/firejune/rigc/issues/748)). ⚠️ `strength: 0` is `A23` **here and not on a key**:
3645
3812
  at rest it is a constraint nothing pulls back, and on a key it is a release somebody
3646
3813
  asked for, which §4.4 states with the measurement behind it
3647
- ([#727](https://github.com/firejune/rigc/issues/727)). `mix: 0` is the one value
3814
+ ([#727](https://github.com/firejune/rigc/issues/727)). A negative `strength` is
3815
+ `A23` here and on a key, and here its sentence is its own: the offset is pushed
3816
+ away and grows ([#748](https://github.com/firejune/rigc/issues/748)). `mix: 0` is the one value
3648
3817
  here an animation can answer for: it rests the constraint **muted**, which is
3649
3818
  legal, and `A23` names it only when no timeline in any animation keys that `mix`
3650
3819
  above 0 — on a key it is the mute for a span (§4.4, [#743](https://github.com/firejune/rigc/issues/743)). None of the
@@ -4758,12 +4927,80 @@ so `v` is three numbers and not one.
4758
4927
 
4759
4928
  ⚠️ **A muted constraint with no timeline is a finding, not an idiom.** Turning a
4760
4929
  constraint on from an animation is the idiom (§4.10), so `A36`/`A37` only object to
4761
- all-zero mixes when **no** animation keys that constraint's `mix`. If you mute one
4762
- at setup, key it somewhere. `A23` asks the same question of a physics constraint
4763
- ([#743](https://github.com/firejune/rigc/issues/743)) and asks it more sharply:
4764
- it reads the key **values**, so a `mix` timeline keying 0 only is not a rescue,
4765
- and it counts the unnamed global timeline for every constraint declaring
4766
- `mixGlobal`. `A36`/`A37` take any non-empty key array.
4930
+ all-zero mixes when **no** animation keys that constraint's `mix` **above 0**. If
4931
+ you mute one at setup, key it up somewhere. `A23` asks the same question of a
4932
+ physics constraint ([#743](https://github.com/firejune/rigc/issues/743)), and the
4933
+ three share one reading ([#752](https://github.com/firejune/rigc/issues/752)): the
4934
+ key **values** the loaded timeline poses, so a `mix` timeline keying 0 only is not a
4935
+ rescue, and a Bezier between two keys of 0 whose handles lie above 0 is. A path
4936
+ constraint is switched on by a key posing **any one** of its three mixes above 0,
4937
+ because `PathConstraint.update` returns only when all three are 0. [measured] on
4938
+ generated fixtures, a path constraint and a slider muted at rest and keyed to 0
4939
+ only pose every bone exactly where the same rig with no mix timeline does, and until
4940
+ #752 both passed. `A23` alone also counts the unnamed global timeline for every
4941
+ constraint declaring `mixGlobal`, since only the physics family has one. The refusal
4942
+ says both halves — `path constraint "P" has mixRotate 0, mixX 0 and mixY 0 at setup
4943
+ and none of the 2 animations keys its mix above 0; …` — and names both repairs.
4944
+
4945
+ ### 4.13 `sequence` — which frame of a numbered series shows
4946
+
4947
+ The other attachment timeline, beside `deform` and for the same reason: its key is
4948
+ three named fields rather than one `v`, and it is aimed at an attachment rather than
4949
+ a slot, so it is a family of its own
4950
+ (`animations.<a>.attachments.<skin>.<slot>.<attachment>.sequence` in the file):
4951
+
4952
+ ```json
4953
+ "sequence": [
4954
+ { "slot": "glint", "attachment": "glint", "keys": [
4955
+ { "t": 0, "mode": "loop", "delay": 0.1 },
4956
+ { "t": 0.5, "mode": "pingpong", "index": 1, "delay": 0.1 } ] }
4957
+ ]
4958
+ ```
4959
+
4960
+ | Field | Meaning |
4961
+ | --- | --- |
4962
+ | `skin` | the skin the attachment lives in; absent means `"default"` |
4963
+ | `slot`, `attachment` | the slot and the attachment's placeholder — the attachment must carry a `sequence` block (§3.4.3) |
4964
+ | `keys[].t` | seconds |
4965
+ | `keys[].mode` | one of `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse`. Parser default `hold` — **per key**, not carried |
4966
+ | `keys[].index` | the frame the key starts on, 0-based. Parser default 0 |
4967
+ | `keys[].delay` | seconds per frame. Parser default: **the previous key's** `delay`, 0 on the first |
4968
+
4969
+ From the key's time on, the frame shown is `index` advanced by one every `delay`
4970
+ seconds and folded back into the series by `mode` — `SequenceTimeline.applyToSlot`,
4971
+ with `i = index + floor((time - keyTime) / delay + 0.00001)` and `n = 2 * count - 2`:
4972
+
4973
+ | `mode` | frame shown |
4974
+ | --- | --- |
4975
+ | `hold` | `index`, never advancing |
4976
+ | `once` | `min(count - 1, i)` — stops on the last frame |
4977
+ | `loop` | `i % count` |
4978
+ | `pingpong` | `i % n`, then `n - that` once it reaches `count` — bounces off both ends |
4979
+ | `onceReverse` | `max(count - 1 - i, 0)` |
4980
+ | `loopReverse` | `count - 1 - (i % count)` |
4981
+ | `pingpongReverse` | `(i + count - 1) % n`, then folded as `pingpong` |
4982
+
4983
+ Measured on four frames at `delay: 0.1` from t = 0 (frames numbered 0–3):
4984
+ `loop` shows 0, 1, 2, 3, 0, 1 at t = 0.05, 0.15 … 0.55; `pingpong` 0, 1, 2, 3, 2, 1, 0;
4985
+ `onceReverse` 3, 2, 1, 0, 0. Before the first key the frame is the block's `setup`.
4986
+
4987
+ ⚠️ **A sequence timeline does not lengthen an animation.** A loop keyed once at t=0
4988
+ in an animation nothing else extends has a duration of 0, and a player that does
4989
+ not loop the animation shows frame `index` forever (measured). The animation's
4990
+ `duration` is its last key, as everywhere (R7), so key something to the length you
4991
+ mean.
4992
+
4993
+ 🚫 Refused by name, each measured to load without a word: a `mode` outside the seven
4994
+ (it loads as `hold`); an `index` that is fractional, negative, or at or past the
4995
+ series' `count` (truncated, clamped); an advancing mode at an **effective** delay of
4996
+ 0 — stated, or carried from the key before — where `(time - keyTime) / 0` is
4997
+ Infinity, `Infinity | 0` is 0, and the key shows its first frame throughout; a track
4998
+ on an attachment with no `sequence` block (the parser gives every region a series of
4999
+ one, so every mode shows it); and a track on a **linked mesh that plays its source's
5000
+ timelines** (`timelines` absent or true), whose `timelineAttachment` is the source,
5001
+ so a key aimed at the link is applied to nothing — key the source instead, and the
5002
+ link steps its own series by it, or set `"timelines": false`. Only the fields the
5003
+ key states are emitted: the compiler writes no `mode`, `index` or `delay` for you.
4767
5004
 
4768
5005
  ---
4769
5006
 
@@ -4899,6 +5136,11 @@ same hole issue #307 closed for the motion spec.)
4899
5136
  | `…ik[i]`, `…transform[i]`, `…deform[i]` | an object | `null` in one of these lists crashed with a raw `TypeError` on `track.constraint` / `track.skin` |
4900
5137
  | `…ik[i].constraint`, `…transform[i].constraint` | a non-empty string | 4.3 writes the group as `ik.<constraint>`, so the name is the only target there is |
4901
5138
  | `…deform[i].slot`, `.attachment`, `.skin` | a string (`skin` optional) | — |
5139
+ | `…sequence` | an array | §4.13 — one entry per skin/slot/attachment triple |
5140
+ | `…sequence[i].slot`, `.attachment`, `.skin` | a string (`skin` optional) | — |
5141
+ | `…sequence[i].keys[j].mode` | one of the seven — `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` | `SequenceMode[mode]` is `undefined` for anything else and the mode bits store 0: the key loads without a word and plays as `hold` |
5142
+ | `…sequence[i].keys[j].index` | a whole number ≥ 0 | stored as `index << 4`, which truncates a fraction (1.5 showed frame 1). Whether it is inside the series' `count` is the compile-time row below |
5143
+ | `…sequence[i].keys[j].delay` | a finite number ≥ 0 — and **above 0 wherever the mode advances**, counting a delay carried from the key before | `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so a `loop` at delay 0 shows its first frame throughout. The message reads `` `…keys[j]` plays "loop" at a delay of 0 … which is "hold" spelt as "loop" `` |
4902
5144
 
4903
5145
  The second wave is everything that needed the **other** file, the property table
4904
5146
  or the key's position in its own track. These are the frequent ones, verbatim:
@@ -4928,6 +5170,14 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4928
5170
  | `"slot" is "X", which the rig does not declare as a slot` / `"skin" is "X", … the rig declares no such skin` | §3.4 — a link resolves both by name. Left to the round trip these are the runtime's `Source mesh slot not found` and `Skin not found`, which name neither the attachment nor where it looked |
4929
5171
  | `"source" is "X", which is itself a linked mesh, and a chain of them is refused` | §3.4 — point `source` at the mesh. A chain resolves in file order and loads nothing at all in one of the two orders, silently |
4930
5172
  | `"source" is "X", which is a "region" attachment and not a mesh` | §3.4 — a link takes another MESH's geometry; off any other type the runtime reads `undefined` and says nothing |
5173
+ | `… is a boundingbox and states a "sequence". A sequence is a numbered series of atlas regions, and only the 3 kinds that draw a region carry one — region, mesh, linkedmesh …` | §3.4.3 — put the series on a region or a mesh, or remove it |
5174
+ | `… "sequence" states no "count" …` / `… "sequence".setup is N, and a C-frame series has frames 0 to C-1 …` / `… "sequence".F is V; it is a whole number …` | §3.4.3 — the parser reads an omitted count as 0 frames and clamps a setup past the end; state the count, and a 0-based `setup` inside it |
5175
+ | `… states "image" beside "sequence" …` / `… states "generator" beside "sequence" …` | §3.4.3 — the frames are the images; remove `image`. A sequence mesh takes authored geometry |
5176
+ | `… sequence frame I of C (number N) is the region "R", and there is no PNG for it at P …` (or `… which the atlas at A does not have …` under `--atlas-in`) | §3.4.3 — add the frame, or state the `count` the series really has. The compiler draws no frame in place of another |
5177
+ | `… the N frames of this sequence measure W1, W2 in width … State "width"` | §3.4.3 — frames of different sizes; state the attachment's size |
5178
+ | `animation "A" sequence S/X/P: attachment "P" carries no "sequence" block, so there is no series to step …` | §4.13 — give the attachment a `sequence`, or remove the track |
5179
+ | `animation "A" sequence S/X/P (t=T): index I is past the end of a C-frame series …` | §4.13 — `index` is 0-based |
5180
+ | `animation "A" sequence S/X/P: attachment "P" is a linked mesh that plays its source's timelines …` | §4.13 — key the source (the link steps its own series by it), or set `"timelines": false` on the link |
4931
5181
  | `a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG` | §3.4 — the mesh rule, on a link. Its art is its own |
4932
5182
  | `hull N disagrees with the triangles, whose outline has K vertices (0 → …)` | §3.4 — delete `hull`, or state K |
4933
5183
  | `hull vertices must come first; vertex i is on the boundary and vertex j is not. The triangles' outline runs …: list those K vertices first, in that order, then the M interior vertices` | §3.4 — renumber the vertices: the printed walk first, then the interior |
@@ -4936,6 +5186,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4936
5186
  | `vertex N binds bone "X", which the rig does not declare as a bone` | §3.4 — an authored mesh's `weights` bind by NAME, like everything else in a rig spec. Fix the spelling, or declare the bone. The message names the skin, the slot, the placeholder and the vertex, because an index would name none of them |
4937
5187
  | `image "X.png" is not on disk at …` | fix the name, or point `--images` at the right directory |
4938
5188
  | `image "X.png": /…/X.png is a WebP image (…), not a PNG: its first 12 byte(s) are …` | the file is there and is not a PNG — the name ends in `.png` and the bytes decide. Re-export it as PNG; the same sentence says **truncated** for a PNG that runs out before its `IEND`, and then the repair is a whole copy (§0.2, [#732](https://github.com/firejune/rigc/issues/732)) |
5189
+ | `a "contour" generator traces the part's own alpha, and "X.png" is lifted off a packed page at the coordinates the atlas states, which on this file are not where its texels are — so there is no silhouette here to trace, only another part of the page. page "p.png" declares …` | the page's PNG is not the size its atlas declares, and the rest of the message is `A06`'s sentence for it: re-declare the page with the `scale:` header it names, or re-export the page at its declared size (§0.2, [#750](https://github.com/firejune/rigc/issues/750)) |
4939
5190
  | `--atlas-in <pack>.atlas: N of its M page(s) cannot be read as PNG, and nothing was compiled against the pack — page "p.png": …` | the same, for every page of the pack at once, before the compile: re-export each named page as PNG under the name the atlas gives it (§0.2) |
4940
5191
  | `parts/iris_open.png is 96x64 but slot "iris" declares 96x60` | R5 — a manifest `states:` entry whose art is not the window the part declares. Re-export the PNG, or fix the part's `size`; a quad sized against art of another size is the silence `A06` exists for, and the window is what the quad is built from |
4941
5192
  | `plates/00_stage.png is 256x256 but the manifest window for "stage" is 250x256` | R5 — the same check on the part's unconditional `image`, against the window the crop gives it |
@@ -4948,8 +5199,12 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4948
5199
  | `animation "A" slot "X" attachment: key at Ns is Ms past the declared duration Ds` | §4.5 — the key is past the end of the animation and nothing will sample it. Move the key onto `duration`, or raise `duration` |
4949
5200
  | `animation "A" slot "X" attachment: attachment "N" is not in slot "X" under any skin (searched: default, alt) — the slot has: plain, trim` | §4.4 — the keyed name is in **no** skin, and the two clauses say where the compiler looked and what it would have taken. Fix the spelling, or give some skin a placeholder called `N`. A name only a NAMED skin fills is not this error and never was one to fix — it compiles, and the slot shows nothing under the skins that lack it. Before [#695](https://github.com/firejune/rigc/issues/695) the message read `attachment "N" is not in slot "X"` and was raised against the **default skin alone**, so it fired on correct rigs: any key into named-skin art, and every key in a rig with no default skin. `the slot has no attachments at all` is the same message where nothing fills the slot |
4950
5201
  | `animation "A" keys unknown bone "X"` | the track's `bone` is not in the rig |
5202
+ | `animation "A" keys physics "*" P, the timeline that names no constraint and drives every physics constraint declaring "PGlobal": true, and none of "C", … does — it would parse and move nothing` | §4.4 — set `"PGlobal": true` on the constraints the track is for (§3.5), or key one by name. For `reset` it reads *every physics constraint the rig has*, and fires only on a rig with none |
5203
+ | `` `animations."A".tracks[i].physics` is the string ""; the empty name is how a skeleton file spells a physics timeline that names no constraint, and a motion spec spells that "*" … `` | §4.4 — name one constraint, or write `"*"` |
5204
+ | `physics constraint "*": the name is reserved — …` / `` `physics."*"` names a physics constraint "*", and that name is reserved … `` | §3.5 — `"*"` is the target of the timeline that names no constraint; rename the constraint |
5205
+ | `animation "A": group "G" lists "*", which is not a constraint but the target that names none …` | §4.4 — write `"*"` as the track's `physics` field |
4951
5206
  | `animation "A" bone "X" translatex: key value must be an array of 1 number(s)` | the value shape must match the property (§4.4) |
4952
- | `animation "A" physics constraint "C" mass key at t=… is 0 (massInverse Infinity); must be > 0 — …` | §4.4 — a keyed physics value the runtime cannot use. The message names the bound and the `PhysicsConstraint.js` lines that make it one: `mass` is `> 0`, `damping` is inside `(0, 1)`, `mix` and `strength` are `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere ([#610](https://github.com/firejune/rigc/issues/610)). ⚠️ Those are the bounds a **key** is held to. A setup `strength` of `0` is refused too, but by `A23` rather than here, and with its own sentence — `physics "C" has strength 0; nothing pulls it back` ([#727](https://github.com/firejune/rigc/issues/727)) |
5207
+ | `animation "A" physics constraint "C" mass key at t=… is 0 (massInverse Infinity); must be > 0 — …` | §4.4 — a keyed physics value the runtime cannot use. The message names the bound and the `PhysicsConstraint.js` lines that make it one: `mass` is `> 0`, `damping` is inside `(0, 1)`, `mix` and `strength` are `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere ([#610](https://github.com/firejune/rigc/issues/610)). ⚠️ Those are the bounds a **key** is held to. A setup `strength` of `0` is refused too, but by `A23` rather than here, and with its own sentence — `physics "C" has strength 0; nothing pulls it back` ([#727](https://github.com/firejune/rigc/issues/727)); a setup `strength` below 0 says the offset is pushed away and grows, the same arm this message quotes for a negative key ([#748](https://github.com/firejune/rigc/issues/748)). A `damping` key's sentence names the exponent `60 * step` and why a negative is NaN at any `fps` where `60 / fps` is not whole (§4.4) |
4953
5208
  | `a key carries both a named easing and a raw curve; pick one` | R6 |
4954
5209
  | `last key carries an easing but has nothing to ease to` | drop `ease`/`curve` from the final key |
4955
5210
  | `key times must strictly increase (at t=…)` | including after `lag` and `stagger` |
@@ -5014,8 +5269,11 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5014
5269
  | `animation "A" slot "X" alpha: alpha key value must be [a]` | §4.4 — an alpha key's `v` is a one-element array like every other one-channel track, even though the file writes it bare (`{ time, value }`). The same row exists for each colour shape with its own spelling (`rgb key value must be [r,g,b]`, …) |
5015
5270
  | `animation "A" slot "X" alpha: key at t=T is V; an alpha is a number from 0 to 1 …` | §4.4 — the one colour key stored as a number, so the one nothing clamps on the way out. The runtime clamps the posed alpha only after interpolating, so a key outside 0..1 bends the curve toward a value no pose holds; state the value you mean. 0 and 1 themselves are taken |
5016
5271
  | `animation "A" slot "X": tracks "P" and "Q" both key the slot's alpha — a colour timeline poses its channels at every time …` | §4.4 — two colour tracks of one slot that pose a shared channel (`rgba` + `alpha`, `rgba` + `rgb`, `rgba2` + `rgb2`, …). Each poses its channels at every time, its setup value before its first key included, so the later in the file overwrites the other everywhere and one of them is read by nothing. Key each channel once: `rgb` and `alpha` for two halves on their own key times, `rgba` for both together. The channel named is whichever the two share — `light rgb`, `alpha`, `dark colour` |
5017
- | `animation "A" bone "B" has no timeline "P" (it has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate)` | §4.4 — a bone has exactly ten timelines and `P` is none of them. Fix the spelling — the single-axis ones are lower-case (`translatex`, not `translateX`). A **constraint** property is refused first, by its own row, naming the field its constraint's name goes in. When `P` is a slot timeline the message says so and where to put the name: `. "rgba" is a slot timeline — put the name in "slot"`. Before [#656](https://github.com/firejune/rigc/issues/656) all of them read `bone "B" cannot take slot property "P"`, which named the slot family whatever you had written and listed nothing |
5018
- | `animation "A" group "G" has no timeline "P" (a bone group has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate; a slot group has: attachment, rgba, rgb, alpha, rgba2, rgb2; a physics constraint group has: inertia, strength, damping, mass, wind, gravity, mix, reset)` | §4.3, §4.4 — a group's family is decided by the property, and `P` is in none of the three tables, so there is no family to resolve the members as. Fix the spelling and the group becomes whichever family the property names. The group is refused before its members are looked up, so a member the rig does not declare is a **later** message; an unknown group NAME is an earlier one. Before [#661](https://github.com/firejune/rigc/issues/661) a group of bones read `animation "A" targets unknown slot "M"` and a group of slots got the slot row below, naming one family out of three |
5272
+ | `animation "A" bone "B" has no timeline "P" (it has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate, inherit)` | §4.4 — a bone has exactly eleven timelines and `P` is none of them. Fix the spelling — the single-axis ones are lower-case (`translatex`, not `translateX`). A **constraint** property is refused first, by its own row, naming the field its constraint's name goes in. When `P` is a slot timeline the message says so and where to put the name: `. "rgba" is a slot timeline — put the name in "slot"`. Before [#656](https://github.com/firejune/rigc/issues/656) all of them read `bone "B" cannot take slot property "P"`, which named the slot family whatever you had written and listed nothing |
5273
+ | `animation "A" bone "B" inherit key at t=T carries an easing ("E"), and this timeline is stepped by the format — its reader builds no curve, so the mode changes AT the key and holds until the next one. Remove it` (and `… carries a curve, …` for a raw `curve`) | §4.4 — an `inherit` key is a mode, not a value between two others: the parser's `inherit` branch reads `time` and `inherit` and nothing else, so an easing would be written into a key nobody reads (`A05_CURVE_ARRAY_LENGTH` refuses the same `curve` on a file rigc did not write). `"stepped"` is refused too, for the same reason — the timeline already is. Delete the `ease` or `curve` ([#733](https://github.com/firejune/rigc/issues/733)) |
5274
+ | `animation "A" bone "B" inherit key at t=T names mode "M"; known: normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection — the runtime folds the case of the first letter and of nothing else` | §4.4 — `M` is not one of the five. The list and the rule are the setup field's (§3.2) — one resolver reads both — so `NoScale` compiles (and is written `noScale`, the editor's spelling) while `NOSCALE` and `noscale` do not: the runtime would load either as **no mode**, and the bone would keep whatever world transform it had ([#733](https://github.com/firejune/rigc/issues/733)) |
5275
+ | `bone "B" has inherit "M"; known: normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection — the runtime folds the case of the first letter and of nothing else` | §3.2 — the setup half of the row above, and the same list. Before [#733](https://github.com/firejune/rigc/issues/733) this check was case-insensitive, which is wider than the runtime: `"NOSCALE"` compiled, gated green, and loaded as no mode, so the bone's world rotation, scale and shear were never computed — every attachment on it collapsed to a point. Fix the spelling |
5276
+ | `animation "A" group "G" has no timeline "P" (a bone group has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate, inherit; a slot group has: attachment, rgba, rgb, alpha, rgba2, rgb2; a physics constraint group has: inertia, strength, damping, mass, wind, gravity, mix, reset)` | §4.3, §4.4 — a group's family is decided by the property, and `P` is in none of the three tables, so there is no family to resolve the members as. Fix the spelling and the group becomes whichever family the property names. The group is refused before its members are looked up, so a member the rig does not declare is a **later** message; an unknown group NAME is an earlier one. Before [#661](https://github.com/firejune/rigc/issues/661) a group of bones read `animation "A" targets unknown slot "M"` and a group of slots got the slot row below, naming one family out of three |
5019
5277
  | `animation "A" slot "X" has no timeline "P" (it has: attachment, rgba, rgb, alpha, rgba2, rgb2)` | §4.4 — a slot has exactly six timelines — every one the format has — and `P` is none of them. Fix the spelling; a bone or constraint property written on a slot track is refused by its own row instead. Before [#650](https://github.com/firejune/rigc/issues/650) every other name compiled as an **rgba** timeline called `P`, and what you saw was `A00_ROUNDTRIP_PARSE` on the emitted file — or, for the one-channel spelling, `rgba value needs 4 channels, got 1` |
5020
5278
  | `animation "A" slot "X" rgba2: slot "X" declares no setup "dark", and an "rgba2" timeline poses a slot's dark colour …` — and the same with `rgb2` | §3.3, §4.4 — the two-colour tint has a setup half and a keyed half, and the keyed half cannot exist without the other. `Slot`'s constructor allocates a dark colour only for a slot whose setup pose declares one, and `RGBA2Timeline` writes it unconditionally — so without the `dark` the file loads, and the first `state.apply` throws `TypeError: null is not an object` in the consumer's process. Give the slot the `dark` it holds at rest, or key `rgba` (for `rgb2`, `rgb`) if only the light colour moves. `RGB2Timeline` was measured to throw the same way before `rgb2` joined the refusal ([#730](https://github.com/firejune/rigc/issues/730)). Raised before the keys are read, with the slot named, for the same reason the row above is |
5021
5279
  | `N pair(s) of animation names have no one order: … "Fx/a" / "fx/b" (folder) — "Fx/a" and "fx/b" sit in the sibling folders "Fx" and "fx", which the comparator leaves in one place …; rename one of the two folders so they differ by more than letter case, spacing or a leading zero` | **R10** — rename until no pair is left. The kind in brackets says which of the three things the five stored round trips leave open decides the pair: `number` (two digit runs that are each one number written twice, pointing opposite ways), `separator` (a whitespace character that is not a space) or `folder` (two sibling folders the comparator cannot separate). rigc keys `animations` in the editor's own comparator, read off `fixtures/editor-order/probe{1..5}.{in,out}.json` ([#728](https://github.com/firejune/rigc/issues/728)) — so a pair those files settle is emitted rather than refused, **including a pair that differs only in case**, whose order is then the one your spec declared. On the three that are left, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
@@ -5122,7 +5380,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5122
5380
  | `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` |
5123
5381
  | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. Until [#589](https://github.com/firejune/rigc/issues/589) they were unreachable: the loader threw first and the miss arrived as `A00_ROUNDTRIP_PARSE`. There is no `spine-html` clause here any more — a placeholder is free to differ from the region its `path` names ([#574](https://github.com/firejune/rigc/issues/574)) **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once ([#580](https://github.com/firejune/rigc/issues/580)) |
5124
5382
  | `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
5125
- | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale. **SKIP** when the skeleton carries no animation ([#580](https://github.com/firejune/rigc/issues/580)): the NaN is produced by stepping, and a static rig is never stepped — the same subject `A09` skips on |
5383
+ | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time** ([#733](https://github.com/firejune/rigc/issues/733)): the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked **SKIP** when the skeleton carries no animation ([#580](https://github.com/firejune/rigc/issues/580)): the NaN is produced by stepping, and a static rig is never stepped — the same subject `A09` skips on |
5126
5384
  | `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
5127
5385
  | `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
5128
5386
  | `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7, issue #274) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh ([#580](https://github.com/firejune/rigc/issues/580)). A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
@@ -5135,7 +5393,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5135
5393
  | `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not ([#684](https://github.com/firejune/rigc/issues/684)). Fix the rig spec's `controls`/`chain`, or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
5136
5394
  | `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1 |
5137
5395
  | `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
5138
- | `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength: 0`, or has `damping` outside `(0, 1)` so it never settles — **at rest, and on every physics timeline key** ([#610](https://github.com/firejune/rigc/issues/610), [#743](https://github.com/firejune/rigc/issues/743)). The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps ([#727](https://github.com/firejune/rigc/issues/727)). As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `physics "C" has mix 0 and none of the 3 animations keys its mix above 0; it is muted — rest it above 0, or key its mix above 0 in an animation`. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, and takes an animation a slider applies like any other. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md), [#731](https://github.com/firejune/rigc/issues/731)), so this sentence is met on a file, never on a decompiled rebuild. **SKIP** when the skeleton declares no physics constraint ([#580](https://github.com/firejune/rigc/issues/580)) — the same sentence `A36` and `A37` have always printed for their own constraint types |
5396
+ | `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength` at or below 0 — `0` says `nothing pulls it back`, below 0 says the offset `is pushed away and grows with every step`, both read off the row's `outside` arms, which the key's refusal quotes too ([#748](https://github.com/firejune/rigc/issues/748)) — or has `damping` outside `(0, 1)` so it never settles — **at rest, and on every physics timeline key** ([#610](https://github.com/firejune/rigc/issues/610), [#743](https://github.com/firejune/rigc/issues/743)). The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps ([#727](https://github.com/firejune/rigc/issues/727)). As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `physics "C" has mix 0 and none of the 3 animations keys its mix above 0; it is muted — rest it above 0, or key its mix above 0 in an animation`. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, counts every sample of a Bezier between two keys as a value the timeline poses — so two keys of 0 joined by a curve lifted above 0 are a rescue, measured to move the bone — and takes an animation a slider applies like any other. It is the one reading `A36` and `A37` use as well ([#752](https://github.com/firejune/rigc/issues/752)). `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md), [#731](https://github.com/firejune/rigc/issues/731)), so this sentence is met on a file, never on a decompiled rebuild. **SKIP** when the skeleton declares no physics constraint ([#580](https://github.com/firejune/rigc/issues/580)) — the same sentence `A36` and `A37` have always printed for their own constraint types |
5139
5397
  | `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it ([#580](https://github.com/firejune/rigc/issues/580)) |
5140
5398
  | `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
5141
5399
  | `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table — a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (§3.3). **SKIP** when the rig declares no canonical slot order. ⚠️ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass ([#580](https://github.com/firejune/rigc/issues/580)): the completeness clause reads it as every declared slot lost and names them, which is the maximal case of what [#575](https://github.com/firejune/rigc/issues/575) filed |
@@ -5146,10 +5404,10 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5146
5404
  | `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted |
5147
5405
  | `A32_EVENT_KEYS_RESOLVE` | both | an event key fires a name the skeleton's `events` block does not declare, sits earlier in time than the key before it, or sets `volume`/`balance` on an event with no `audio` (§4.8). **SKIP** when no animation carries an event timeline |
5148
5406
  | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box, clipping polygon or path whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index, a clipping `end` naming a slot the skeleton does not have, a path whose vertex count is not a multiple of 3, or a path `lengths` array that does not strictly increase (§3.4). **SKIP** when the skeleton carries none of the three |
5149
- | `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path`, `physics` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.4, §4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. **SKIP** when no animation carries one |
5407
+ | `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path`, `physics` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.4, §4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. The **empty** name under `physics` is not a miss — it is the timeline that names no constraint (§4.4's `"*"`), and it is refused only when it reaches none: `animation "A" physics constraint "" timeline "P": a physics group that names no constraint writes every physics constraint declaring "PGlobal": true, and none of "C", … does — the parser loads it, the runtime walks every constraint, and no constraint takes the key` ([#726](https://github.com/firejune/rigc/issues/726)). Who it reaches is asked of the runtime's own `PhysicsConstraintTimeline.global` on the file's constraints — the reading `A23` and `A42` share. **SKIP** when no animation carries one |
5150
5408
  | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, holds a non-finite number, has an empty key array, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. ⛔ It does **not** require pair alignment: the runtime has no such rule and a trimmed editor run legitimately starts and ends mid-pair (§4.11, issue #262). **SKIP** when no animation carries a deform timeline |
5151
- | `A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying its `mix` (§3.5.1). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. **SKIP** when the skeleton declares no path constraint |
5152
- | `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` (§3.5.2). **SKIP** when the skeleton declares no slider |
5409
+ | `A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying any of them **above 0** (§3.5.1, §4.12). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. The third reads the values the loaded timeline poses — keys and Bezier samples alike — the way `A23` reads a physics `mix`, so a `mix` timeline keying 0 only is not a rescue: [measured] it poses every bone exactly where no timeline does, and before [#752](https://github.com/firejune/rigc/issues/752) it passed. The detail says both halves: `path constraint "P" has mixRotate 0, mixX 0 and mixY 0 at setup and none of the 2 animations keys its mix above 0; update() returns on all-zero mixes, so nothing ever puts a bone on the path — rest one of the three above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no path constraint |
5410
+ | `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` **above 0** (§3.5.2, §4.12) — the same reading as `A36`'s, so a `mix` timeline keying 0 only is no rescue ([#752](https://github.com/firejune/rigc/issues/752)): `slider "S" has mix 0 at setup and none of the 2 animations keys its mix above 0; update() returns on mix 0 — rest it above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no slider |
5153
5411
  | `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
5154
5412
  | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is refused as before. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3, issue #403): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4, issue #407): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. ⚠️ And the **skin** it poses in is the one the timeline is keyed on (§4.11.5, issue #583), since a deform's address is a `skin / slot / attachment` triple: the pose wears that skin, which also switches on any `skin: true` bone or constraint it activates, and the "nothing is drawn" sentence names the skin it was read under. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
5155
5413
  | `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` — it writes that property outright at `mix: 1` and every earlier slider on it is dead (§3.5.2). Also fires when the shared timeline **cannot** be applied additively (a slot colour, an attachment swap, a draw order, an ik mix, a path's `spacing`, most physics properties), where `"additive": true` is not the fix and one of the two has to go. ⭐ Which of the two it is, is **posed rather than read off `Timeline.additive`**: the shared timeline is applied twice with `add` set and the detail says what it did ([#655](https://github.com/firejune/rigc/issues/655) — two classes declare that flag falsely about themselves, so a path constraint's `mix` and a slider's `time` were refused although they compose). The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, which one wins today, and the class that was posed. Four shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, two sliders on different properties, and a shared timeline that writes **nothing a pose holds** — an `events` timeline fires no event under a slider (`firedEvents` is null), so neither slider has anything there for the other to erase. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
@@ -5158,6 +5416,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5158
5416
  | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | both | a slot's `dark` (§3.3) or an `rgba2` / `rgb2` timeline (§4.4) that the runtime does not hold as the file states it. Three shapes, all of which parse in silence: a `dark` the slot reader **drops** — it takes the field through a truthiness test, so `""` is discarded without a word and the slot renders with one colour; a `dark` that is **not six hex digits** — `Color.setFromString` slices fixed offsets and stores whatever `parseInt` gives back, so `"4020"` loads a channel of `NaN`; and an `rgba2` or `rgb2` timeline on a slot with **no `dark` at all**, where the runtime allocates no dark colour and the first `state.apply` throws in the consumer's process. The keyed half is read by posing: the animation is stepped to each key's own time and the posed `color` and `darkColor` are compared against the hex the key states, to half a quantisation step (`1/510`). The detail names the slot, the value found and the value required. ⚠️ The required value is parsed **here** and not through `Color.fromString`, because a check that read it out of the parser it is checking would agree with that parser whatever it did. `compile.ts` refuses the third shape outright in a rig rigc builds; this is the same fact held against a skeleton it did not write. An `rgb2` key's light colour is compared over its three channels only: the light alpha is not its to state, and that it is left where it was is measured in the selftest (`S85`). **SKIP** when no slot declares a `dark` and no animation keys an `rgba2` or `rgb2` — there is then no two-colour tint to read back |
5159
5417
  | `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` | both | a **linked mesh** (§3.4) — `type: "linkedmesh"`, or a `type: "mesh"` carrying `source` — that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by **nothing at all** and `setSourceMesh` fills the attachment with the source's arrays instead: the file says one mesh and every runtime draws another, in silence. The detail names the attachment by skin, slot and placeholder, every key it states, the `source` and where the parser looks for it — the two defaults spelled out, because an omitted `skin` is the **default** skin rather than the one the link is written in — and the shape the keys describe beside the shape the attachment loaded. ⚠️ **`width`/`height` are not part of this.** `setSourceMesh` overwrites both with the source's, so they are as dead at runtime — but the parser reads them (`:569-570`), the format carries them on a link and rigc emits them, so refusing them would refuse every link rigc writes (§3.4). `compile.ts` refuses the same shape outright in a rig rigc builds (§5.1); this is that fact held against a skeleton it did not write, and `ingest` reports it as `ATTACHMENT_LINK_GEOMETRY` ([INGEST §2.0](INGEST.md)). **SKIP** when no attachment in the skeleton takes its geometry from another — which is almost every skeleton, so a pass here means a link was read ([#710](https://github.com/firejune/rigc/issues/710)) |
5160
5418
  | `A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | both | an `rgb` or `alpha` timeline (§4.4) the runtime does not hold as the file states it, in one of two shapes that both parse in silence. **A channel keyed twice**: another colour timeline of the same slot in the same animation poses a channel this one poses — `rgba` beside `alpha` is the shape a converter leaves when it writes a separable `rgb` back as `rgba` next to the `alpha` it kept. Every colour timeline poses its channels at every time, the setup value before its first key included, so the one the file states later overwrites the other everywhere; the detail names both timelines, the channel, and which one survives. **A key not posed as written**: the animation is stepped to each key's own time and the posed r g b (for `rgb`, against the hex, to half a quantisation step) or alpha (for `alpha`, against `value`, whose absence the parser reads as 0) is compared — a colour that is not six hex digits loads as NaN, and a key whose time another key repeats is read by nothing. ⚠️ An `rgb` alone written as an `rgba` holding the setup alpha is **not** caught and cannot be from the file: it is a correct `rgba`, and the difference shows only under another track that moves the alpha. The loaded timeline class and the channels a separable timeline leaves alone are measured in the selftest (`S83`–`S85`) rather than here, because against the linked parser neither can come out wrong. The channel table is `SLOT_COLOR_CHANNELS` in `src/timelines.ts`, shared with the compiler's refusal and held to the runtime's own property ids (`S89`). **SKIP** when no animation keys an `rgb` or `alpha` — there is then no separable slot colour to read back |
5419
+ | `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | both | a **numbered series** (§3.4.3, §4.13) that the runtime does not show as the file states it. Every shape below loads without a word, measured on spine-core 4.3.13 ([#729](https://github.com/firejune/rigc/issues/729)). **The block**: a `sequence` with no `count` (`readSequence` reads 0, and the attachment holds no region) or a `setup` at or past `count` (`Sequence.resolveIndex` clamps it to the last frame). **The keys**: a `mode` outside the seven — `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` — loads as `hold`; an `index` that is fractional (`index << 4` truncates it) or past the end (clamped); an advancing mode at an effective delay of 0 (the parser carries a key's `delay` from the key before; `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so it never advances); a timeline on an attachment that carries no block (the parser gives every region a one-region series, so every mode shows it). **The pose**: every key is stepped to mid-frame sample times — enough to wrap every mode — and the region the slot shows is held to the frame the file's own statement gives, the arithmetic of `SequenceTimeline.applyToSlot` and the names of `Sequence.getPath` transcribed rather than read off the loaded timeline, so the check is not the runtime agreeing with itself. Before the first key the frame is `setup`. ⚠️ A sample where the slot shows another attachment is not compared, because the runtime writes nothing there; a timeline with no comparable sample is counted in `stats.sequenceSamplesUnshown`. `compile.ts` refuses every one of these shapes in a spec (§5.1); this is them held against a skeleton it did not write. **SKIP** when no attachment carries a `sequence` block and no animation keys a `sequence` timeline |
5161
5420
 
5162
5421
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
5163
5422
  clauses are gated by profile.
@@ -5205,12 +5464,14 @@ Two more limits that are not errors but will shape what you can attempt:
5205
5464
  `--profile spine-html` over a packed atlas. The default is still **one part per
5206
5465
  page** with `pma: false` and every region covering its whole page, and nothing
5207
5466
  about a build changes unless one of those flags is given.
5208
- - **`sequence` timelines and `drawOrderFolder`** are walked by the validator (so
5209
- `A05` checks their curves and `diff` counts their keys) and cannot be *written*:
5210
- there is no motion-spec property for either. Everything a motion spec **can** key
5211
- is §4.4's track table — which now includes the `path` and `slider` groups (§4.12)
5212
- — plus the five families that sit beside `tracks`: `drawOrder` (§4.7), `events`
5213
- (§4.8), `ik` (§4.9), `transform` (§4.10) and `deform` (§4.11).
5467
+ - **`drawOrderFolder`** is walked by the validator (so `A05` checks its curves and
5468
+ `diff` counts its keys) and cannot be *written*: there is no motion-spec property
5469
+ for it. Everything a motion spec **can** key is §4.4's track table — which now
5470
+ includes the `path` and `slider` groups (§4.12) — plus the six families that sit
5471
+ beside `tracks`: `drawOrder` (§4.7), `events` (§4.8), `ik` (§4.9), `transform`
5472
+ (§4.10), `deform` (§4.11) and `sequence` (§4.13). `sequence` stood beside
5473
+ `drawOrderFolder` in this sentence until
5474
+ [#729](https://github.com/firejune/rigc/issues/729) made it writable.
5214
5475
 
5215
5476
  ---
5216
5477