spine-rigc 0.31.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/README.md CHANGED
@@ -533,9 +533,9 @@ first three work on any reference you have, and `bench` is a repository workflow
533
533
  and `bun run fetch-examples`. The reasoning behind them is in
534
534
  [the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
535
535
 
536
- `build` and `validate` both default to `--profile spine` — the 31 validity rules, which
536
+ `build` and `validate` both default to `--profile spine` — the 32 validity rules, which
537
537
  ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
538
- adds all 46: the other 15 are one renderer's policy and one canvas budget's, and they
538
+ adds all 47: the other 15 are one renderer's policy and one canvas budget's, and they
539
539
  fire on perfectly correct editor-produced Spine data, which is why they are opt-in.
540
540
  ⇒ **That reason is about foreign data and does not carry to a rig you are authoring
541
541
  yourself: author under `--profile spine-html` and read the extra 15 as findings, and
@@ -681,7 +681,7 @@ letting `A17` blame the editor for the harness's own doing.
681
681
  | 📥 **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
682
682
  | 🤖 **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** — the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
683
683
  | 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
684
- | 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 46 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
684
+ | 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 47 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
685
685
  | 📋 [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
686
686
  | 🗺️ [ROADMAP.md](https://github.com/firejune/rigc/blob/main/ROADMAP.md) | where this is going, and where it has been. What 1.0 has to mean before the number is claimed — conditions rather than a feature list, because direction here comes from what users hit |
687
687
  | 📐 [CLAUDE.md](https://github.com/firejune/rigc/blob/main/CLAUDE.md) | **the doctrine** — why the validator's messages are the product, why nothing reaches disk before green, why no number is ever invented, and what a change has to keep. [CONTRIBUTING.md](https://github.com/firejune/rigc/blob/main/CONTRIBUTING.md) calls it worth ten minutes before a first patch. Repository material |
@@ -738,7 +738,7 @@ quality."* All six, with their verdicts, are in
738
738
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
739
739
 
740
740
  The whole dossier — the yardstick, `diff` and `check` and what neither of them can
741
- see, every rung, the run viewer, the 46 assertions and the selftest behind them — is
741
+ see, every rung, the run viewer, the 47 assertions and the selftest behind them — is
742
742
  [docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
743
743
  Live rung status is
744
744
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
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` |
@@ -627,7 +627,7 @@ the first:
627
627
 
628
628
  | gutter | meaning |
629
629
  | --- | --- |
630
- | `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 |
631
631
  | `JUDGE` | the skeleton cannot answer and somebody has to: the stage, and each animation's duration |
632
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) |
633
633
 
@@ -1223,13 +1223,15 @@ the default `type`:
1223
1223
  | `x`, `y` | offset from the bone, in the bone's local space |
1224
1224
  | `rotation` | degrees; cancels a rotated bone for a plate authored screen-upright |
1225
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 |
1226
1227
 
1227
1228
  **Mesh attachment** ([Spine: meshes](http://esotericsoftware.com/spine-meshes)) —
1228
1229
  either authored geometry (`uvs` + `triangles` + geometry) **or** a `generator`,
1229
1230
  never both. `hull`, `edges`, `width` and `height` may be stated; whichever is
1230
1231
  omitted, rigc derives — `hull` and `edges` from the triangles, the size from the
1231
- PNG — and the rules are a few paragraphs down. `type`, `image`, `path` and `color`
1232
- 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).
1233
1235
 
1234
1236
  🔑 **`path` is one rule for both kinds.** A mesh derives it from `image` the way a
1235
1237
  region does: stated wins, otherwise the PNG's basename when that differs from the
@@ -2331,6 +2333,69 @@ nothing at all. `check` then compares blank against blank and reports a perfect
2331
2333
  `--skin <name>` to both, once per skin (**§9**); `tools/editor_roundtrip.ts`
2332
2334
  loops over every skin the build declares for the same reason.
2333
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
+
2334
2399
  ### 3.5 `constraints` — 4.3's single typed array
2335
2400
 
2336
2401
  Spine 4.3 folds every constraint into one `constraints` array with a `type`
@@ -2650,8 +2715,8 @@ driven constraint and its kind, the property, both `constraints` indices, the
2650
2715
  runtime class whose `update` does the reading and the animation the key sits in,
2651
2716
  and its repair is the reorder. It is a rule of its own rather than a clause of
2652
2717
  `A40` because `A40`'s population is the sliders whose `mix` nothing keys — the
2653
- exclusion *is* the shape of the hole — and not a clause of `A37`, whose `keyedBy`
2654
- 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.
2655
2720
  ⭐ **The runtime repairs this for bones and not for constraints**, which is why an
2656
2721
  author cannot reason it out from the bone case: `Slider.sort` clears `sorted` on
2657
2722
  every bone its animation keys so those bones re-sort *after* the slider, while
@@ -3106,6 +3171,7 @@ time puts it here.
3106
3171
  | `ik` | IK constraint timelines — §4.9. Not a track: its keys carry named fields, not one `v` |
3107
3172
  | `transform` | transform constraint timelines — §4.10. Same reason |
3108
3173
  | `deform` | deform timelines — §4.11. Same reason |
3174
+ | `sequence` | which frame of an attachment's numbered series shows — §4.13. Same reason |
3109
3175
 
3110
3176
  `groups` (`name → [member, …]`) lets one track target several bones, slots or
3111
3177
  physics constraints at once; `lag` shifts every key of a track, and `stagger`
@@ -3281,8 +3347,8 @@ needs 4 channels, got 1`, a message about a key you had not written.
3281
3347
  here, and would be refused by the runtime's own reader too (`Invalid timeline
3282
3348
  type for a slot`). `A12_NO_DARK_COLOR` refuses `rgb2` — and `rgba2`, and the
3283
3349
  slot field — in a file under the `spine-html` profile (SPEC_COVERAGE §2.1).
3284
- `sequence` is a timeline on an **attachment**, not on a slot, and rigc does
3285
- 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.
3286
3352
 
3287
3353
  ⚠️ **A `group` track's `property` is one of those two lists or the physics one,
3288
3354
  and anything else is a compile error** — `animation "A" group "G" has no timeline
@@ -3336,6 +3402,17 @@ a delta from the constraint's own setting.
3336
3402
  a file rigc did not write, naming the animation, the constraint, the key time
3337
3403
  and the value — so the compiler is where a spec you wrote is refused, and the
3338
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.
3339
3416
  - 🚫 **`inertia`, `wind` and `gravity` are bounded nowhere, and neither is the top
3340
3417
  of `mix`.** The runtime documents no range for the first three, and
3341
3418
  `PhysicsConstraintPose` documents `mix` as "a percentage (0+)" — so a negative
@@ -3352,7 +3429,15 @@ a delta from the constraint's own setting.
3352
3429
  back ([#727](https://github.com/firejune/rigc/issues/727)). At rest the two
3353
3430
  part ways: a `strength` of 0 is a constraint nothing pulls back, which `A23`
3354
3431
  refuses, and a `mix` of 0 is a constraint muted until an animation keys it
3355
- 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)).
3356
3441
  - ⚠️ **A setup `mix` of `0` is legal when some animation keys it above 0** ([#743](https://github.com/firejune/rigc/issues/743)).
3357
3442
  A constraint muted *at rest* is a rig whose physics is off until an animation
3358
3443
  switches it on, which is a design rather than the silence `A23` was built for.
@@ -3362,6 +3447,11 @@ a delta from the constraint's own setting.
3362
3447
  at 1 does, and keyed-to-0 poses it exactly where one with no timeline at all
3363
3448
  does. The unnamed global timeline counts as a key for every constraint whose
3364
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).
3365
3455
  - 📏 **What a `strength` key of `0` costs, measured through spine-core** on the
3366
3456
  generated overlay fixture, stepping at 60 fps from `Physics.reset` (#727). With
3367
3457
  no wind or gravity the offset coasts to a limit rather than running away — a
@@ -3716,10 +3806,14 @@ mass?, wind?, gravity?, mix?, fps?, limit? }`. These are emitted into the 4.3
3716
3806
  be **keyed over time** as `tracks` entries naming this constraint (§4.4); this
3717
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`
3718
3808
  never settles — both are `A23`, here and on every timeline key that states them
3719
- ([#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**:
3720
3812
  at rest it is a constraint nothing pulls back, and on a key it is a release somebody
3721
3813
  asked for, which §4.4 states with the measurement behind it
3722
- ([#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
3723
3817
  here an animation can answer for: it rests the constraint **muted**, which is
3724
3818
  legal, and `A23` names it only when no timeline in any animation keys that `mix`
3725
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
@@ -4833,12 +4927,80 @@ so `v` is three numbers and not one.
4833
4927
 
4834
4928
  ⚠️ **A muted constraint with no timeline is a finding, not an idiom.** Turning a
4835
4929
  constraint on from an animation is the idiom (§4.10), so `A36`/`A37` only object to
4836
- all-zero mixes when **no** animation keys that constraint's `mix`. If you mute one
4837
- at setup, key it somewhere. `A23` asks the same question of a physics constraint
4838
- ([#743](https://github.com/firejune/rigc/issues/743)) and asks it more sharply:
4839
- it reads the key **values**, so a `mix` timeline keying 0 only is not a rescue,
4840
- and it counts the unnamed global timeline for every constraint declaring
4841
- `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.
4842
5004
 
4843
5005
  ---
4844
5006
 
@@ -4974,6 +5136,11 @@ same hole issue #307 closed for the motion spec.)
4974
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` |
4975
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 |
4976
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" `` |
4977
5144
 
4978
5145
  The second wave is everything that needed the **other** file, the property table
4979
5146
  or the key's position in its own track. These are the frequent ones, verbatim:
@@ -5003,6 +5170,14 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5003
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 |
5004
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 |
5005
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 |
5006
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 |
5007
5182
  | `hull N disagrees with the triangles, whose outline has K vertices (0 → …)` | §3.4 — delete `hull`, or state K |
5008
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 |
@@ -5029,7 +5204,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5029
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 |
5030
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 |
5031
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) |
5032
- | `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) |
5033
5208
  | `a key carries both a named easing and a raw curve; pick one` | R6 |
5034
5209
  | `last key carries an easing but has nothing to ease to` | drop `ease`/`curve` from the final key |
5035
5210
  | `key times must strictly increase (at t=…)` | including after `lag` and `stagger` |
@@ -5218,7 +5393,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5218
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)) |
5219
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 |
5220
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)) |
5221
- | `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 |
5222
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)) |
5223
5398
  | `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
5224
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 |
@@ -5231,8 +5406,8 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5231
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 |
5232
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 |
5233
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 |
5234
- | `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 |
5235
- | `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 |
5236
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` |
5237
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 |
5238
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 |
@@ -5241,6 +5416,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5241
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 |
5242
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)) |
5243
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 |
5244
5420
 
5245
5421
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
5246
5422
  clauses are gated by profile.
@@ -5288,12 +5464,14 @@ Two more limits that are not errors but will shape what you can attempt:
5288
5464
  `--profile spine-html` over a packed atlas. The default is still **one part per
5289
5465
  page** with `pma: false` and every region covering its whole page, and nothing
5290
5466
  about a build changes unless one of those flags is given.
5291
- - **`sequence` timelines and `drawOrderFolder`** are walked by the validator (so
5292
- `A05` checks their curves and `diff` counts their keys) and cannot be *written*:
5293
- there is no motion-spec property for either. Everything a motion spec **can** key
5294
- is §4.4's track table — which now includes the `path` and `slider` groups (§4.12)
5295
- — plus the five families that sit beside `tracks`: `drawOrder` (§4.7), `events`
5296
- (§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.
5297
5475
 
5298
5476
  ---
5299
5477
 
package/docs/INGEST.md CHANGED
@@ -594,9 +594,9 @@ is the one failure a comparison of two sets cannot show you.
594
594
  | `ANIMATION_GROUP` | `BLOCK` | 1 | the animation carries a group the motion spec has no home for. The detail names the ten it does carry. `drawOrderFolder` is the group to know about: the runtime reads it and builds a timeline from it, and no export in this corpus carries one | transcribe that group by hand (§2), or accept that the rebuild does not carry it |
595
595
  | `ATTACHMENT_<TYPE>` | `BLOCK` | 1 | an attachment of a type rigc does not emit; the code is composed from the type, so on the one type left it reads `ATTACHMENT_POINT`. rigc emits region, mesh, linkedmesh, boundingbox, clipping and path — `linkedmesh` since [#691](https://github.com/firejune/rigc/issues/691), and `point` is the remaining deferred type | the rebuild will not have that attachment at all. `docs/SPEC_COVERAGE.md` part 1-6 says what a deferred type would carry |
596
596
  | `ATTACHMENT_LINK_GEOMETRY` | `LOSS` | 0 | a **linked mesh** 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 the attachment draws the geometry its `source` names; the rig spec has no home for them either, because `build` refuses geometry on a link by name. The detail lists the keys and the source. Until [#710](https://github.com/firejune/rigc/issues/710) the rebuild dropped them with no line at all, so an `ingest` that normalised somebody's file said nothing about it | nothing. The rebuild is the mesh the runtime was already drawing — and if those keys were the geometry you meant, take `source` off and author it as a mesh of its own. `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` is the same fact at the gate |
597
- | `ATTACHMENT_NAME` | `LOSS` | 0 | the attachment states a `name` and only **one** skin fills the placeholder, so rigc writes none — it composes `<skin>/<placeholder>` exactly where a placeholder is contested. 🚨 **The name is also the ATLAS REGION KEY**, and the detail says what became of it: `readAttachment` reads `name = getValue(map, "name", placeholder)` and then `path = getValue(map, "path", name)`, so `path` defaults to the **name** and not to the placeholder. A name that differs from the placeholder is therefore **kept as `path`** on an attachment that resolves a region — region, mesh, linked mesh — and the detail names the region. The three shapes that keep nothing say which they are: the source stated its own `path` (carried unchanged), the name **is** the placeholder (same region either way), or the type resolves no region at all (`boundingbox`, `clipping`, `path`). Until [#742](https://github.com/firejune/rigc/issues/742) nothing was written, so the rebuild asked the atlas for the placeholder and `A08_REGION_NAMES_MATCH_ATTACHMENTS` refused it | nothing about the art, which is carried. The **name** is what is gone, so this matters where something downstream looks that attachment up by the name the source gave it |
598
- | `ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | the attachment carries a `sequence` block — a numbered image series — which the rig spec cannot say | the rebuild draws the single region the attachment names; the frames have to be driven some other way |
599
- | `ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline other than `deform`, which is the only one the motion spec carries | transcribe it, or accept that the rebuild does not play it |
597
+ | `ATTACHMENT_NAME` | `LOSS` | 0 | the rebuilt attachment answers to a different name than the source's. **Two shapes, one comparison.** Where only **one** skin fills the placeholder, any stated `name` is lost: rigc writes none — it composes `<skin>/<placeholder>` exactly where a placeholder is contested. Where the placeholder **is** contested, rigc composes that name, and the source's name — what it states, or its placeholder where it states none (`SkeletonJson.js:526`) — is compared with it: equal is rigc's own emit and prints nothing, different is a rename and the detail states **both strings**. Until [#746](https://github.com/firejune/rigc/issues/746) the contested half printed nothing at all. A contested placeholder the **default** skin fills gets no line — the compiler composes nothing there and refuses the rebuild by name. 🚨 **The name is also the ATLAS REGION KEY**, and the detail says what became of it: `readAttachment` reads `name = getValue(map, "name", placeholder)` and then `path = getValue(map, "path", name)`, so `path` defaults to the **name** and not to the placeholder. A name that differs from the placeholder is therefore **kept as `path`** on an attachment that resolves a region — region, mesh, linked mesh — and the detail names the region. The three shapes that keep nothing say which they are: the source stated its own `path` (carried unchanged), the name **is** the placeholder (same region either way), or the type resolves no region at all (`boundingbox`, `clipping`, `path`). Until [#742](https://github.com/firejune/rigc/issues/742) nothing was written, so the rebuild asked the atlas for the placeholder and `A08_REGION_NAMES_MATCH_ATTACHMENTS` refused it | nothing about the art, which is carried. The **name** is what is gone, so this matters where something downstream looks that attachment up by the name the source gave it |
598
+ | `ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | a `sequence` block — a numbered image series — that the rig spec cannot say **as written**. Since [#729](https://github.com/firejune/rigc/issues/729) a well-formed block on a region, mesh or linked mesh is carried field for field, with no `image` on the loose route (the frames `<path><number>` are the art), and a `sequence` timeline with it; what is left here is a block the parser reads into a series other than the one written — no `count` (0 regions), a `setup` past the end (clamped), a fraction — or one on a `boundingbox`, `clipping` or `path`, where the parser never reads it. The detail quotes the block and says which | the rebuild draws the single region the attachment names. Fix the block in the source — a `count` is the usual one — and ingest again |
599
+ | `ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline that is neither `deform` nor `sequence`. Both are carried since [#729](https://github.com/firejune/rigc/issues/729), and `readAnimation` tests an attachment timeline for exactly those two names and ignores anything else (`SkeletonJson.js:1147-1201`) — so what reaches this line is a name outside the format, which no player plays either | fix the timeline's name in the source, or accept that the rebuild does not carry it |
600
600
  | `BONE_FIELD` | `BLOCK` | 1 | a bone field with no rig-spec field, so it is dropped. A 4.0/4.1 export spelling `transform` where 4.3 spells `inherit` lands here; so does a misspelling | check the name against AUTHORING §3 first — a typo and an unsupported field read exactly the same |
601
601
  | `BONE_TIMELINE` | `BLOCK` | 1 | a bone timeline the motion spec has no track for. The detail names the eleven it has, read off the table. Since [#733](https://github.com/firejune/rigc/issues/733) carried `inherit` — the eleventh case of the runtime's own bone switch, a stepped mode per key — every bone timeline the runtime plays has a track, so this is reachable only for a name the **parser** throws on too (`Invalid timeline type for a bone`), the position `PHYSICS_TIMELINE` is in | check the spelling; there is no bone timeline left for the rebuild to be missing |
602
602
  | `CONSTRAINT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a constraint, with its type named beside it | as `BONE_FIELD` |
@@ -624,10 +624,11 @@ is the one failure a comparison of two sets cannot show you.
624
624
  | `TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
625
625
 
626
626
  ⚠️ **One thing the table cannot carry: the region key is kept where a placeholder is
627
- CONTESTED too, and there is no finding there at all.** `ATTACHMENT_NAME` is silent
628
- exactly where the name is re-derived — rigc composes `<skin>/<placeholder>` for a
629
- contested placeholder, so nothing is lost about the name — and until
630
- [#742](https://github.com/firejune/rigc/issues/742) the region went with it anyway:
627
+ CONTESTED too, whether or not a line is printed.** On a contested placeholder rigc
628
+ composes `<skin>/<placeholder>`, so `ATTACHMENT_NAME` fires there only where the
629
+ source's name differs from that composed one ([#746](https://github.com/firejune/rigc/issues/746);
630
+ `IG68`–`IG70` hold both directions) — and until
631
+ [#742](https://github.com/firejune/rigc/issues/742) the region went with the name anyway:
631
632
  `compile` pins a composed name's `path` at the **placeholder**, so two skins naming two
632
633
  regions rebuilt onto **one**, neither of them the art the source drew, with no line
633
634
  printed. `ingest` now writes the source's region as `path` on both, and `IG53` in
@@ -624,14 +624,14 @@ with the member's own `skin: true` — either half alone is refused, because `Sk
624
624
 
625
625
  | Part-1 type | rigc | Detail |
626
626
  | --- | --- | --- |
627
- | `region` | 🟡 | emits `width`, `height` (always, from PNG measurement — the fix for case 6c), `x`, `y` (only when non-zero), `rotation` (only when non-zero, cancelling the bone's world rotation), `scaleX`/`scaleY` (`compile.ts:1238-1239`), `color`, and `path` — taken from the rig spec, else derived from the image basename and omitted when that basename *is* the attachment name (the region name == attachment name == PNG basename convention **A08** + **A27** enforce). ❌ `sequence` |
628
- | `mesh` | 🟡 | emits `type`, `uvs`, `triangles`, `vertices` (**weighted encoding only**), `hull`, `width`, `height`, `edges` (`compile.ts:1343`, and part 4's rung-6 entry measures it byte-identical to the reference), `path`, `color`. ❌ `sequence`. Unweighted meshes are 🚫 **A20_MESH_WEIGHTS_COHERENT** (`validate.ts:430-433`) |
629
- | `linkedmesh` | 🟡 | emits `type`, `source`, its own `path`, `width`, `height` and `color`, and `slot`/`skin`/`timelines` **only where they differ from the parser's defaults** (this attachment's slot, the default skin, true). ❌ `sequence`. Geometry keys on a link are refused by name — the parser returns before `readVertices`, so it reads none of them. A chain (a `source` that is itself a link) and a link to itself are refused: they resolve in file order and load `worldVerticesLength` 0 in one of the two orders, silently. `A21`/`A28` leave a link out (its rim and rows are its source's), `A04`/`A20`/`A22` read it as the mesh it resolves to, `A13` counts it as a mesh slot of its own ([#691](https://github.com/firejune/rigc/issues/691)) |
627
+ | `region` | 🟡 | emits `width`, `height` (always, from PNG measurement — the fix for case 6c), `x`, `y` (only when non-zero), `rotation` (only when non-zero, cancelling the bone's world rotation), `scaleX`/`scaleY` (`compile.ts:1238-1239`), `color`, and `path` — taken from the rig spec, else derived from the image basename and omitted when that basename *is* the attachment name (the region name == attachment name == PNG basename convention **A08** + **A27** enforce). ✅ `sequence` since [#729](https://github.com/firejune/rigc/issues/729) — see the `sequence` block row |
628
+ | `mesh` | 🟡 | emits `type`, `uvs`, `triangles`, `vertices` (**weighted encoding only**), `hull`, `width`, `height`, `edges` (`compile.ts:1343`, and part 4's rung-6 entry measures it byte-identical to the reference), `path`, `color`. ✅ `sequence` since [#729](https://github.com/firejune/rigc/issues/729) — see the `sequence` block row. Unweighted meshes are 🚫 **A20_MESH_WEIGHTS_COHERENT** (`validate.ts:430-433`) |
629
+ | `linkedmesh` | 🟡 | emits `type`, `source`, its own `path`, `width`, `height` and `color`, and `slot`/`skin`/`timelines` **only where they differ from the parser's defaults** (this attachment's slot, the default skin, true). ✅ `sequence` since [#729](https://github.com/firejune/rigc/issues/729) — see the `sequence` block row. Geometry keys on a link are refused by name — the parser returns before `readVertices`, so it reads none of them. A chain (a `source` that is itself a link) and a link to itself are refused: they resolve in file order and load `worldVerticesLength` 0 in one of the two orders, silently. `A21`/`A28` leave a link out (its rim and rows are its source's), `A04`/`A20`/`A22` read it as the mesh it resolves to, `A13` counts it as a mesh slot of its own ([#691](https://github.com/firejune/rigc/issues/691)) |
630
630
  | `boundingbox` | ✅ | `vertexCount` (required and cross-checked), `vertices` **or** by-name `weights`, `color`. **A33_VERTEX_ATTACHMENT_GEOMETRY** |
631
631
  | `path` | ✅ | `vertexCount` (required, and checked as a multiple of 3 — the parser's own `vertexCount / 3` takes a fractional size in silence), `vertices` **or** by-name `weights`, `closed`, `constantSpeed`, `color`, and a **measured** `lengths`: the cumulative setup length at the end of each curve, taken through each influence's own bone and measured as `PathConstraint` measures it — its own four-sample forward difference, which is what the editor writes too and is about 0.5 % below the arc (AUTHORING §10.6) — refused if authored. **A33_VERTEX_ATTACHMENT_GEOMETRY** re-checks the structure and the array's monotonicity. A `deform` timeline may key one: the array is the control points, `A39` SKIPs on it (no triangles), and `lengths` stays the setup measurement because the format has nowhere to put a per-key one — which only a `constantSpeed: false` traversal reads (AUTHORING §4.11) |
632
632
  | `point` | ❌ | deliberately deferred — never appears in the corpus (part 3-1) |
633
633
  | `clipping` | ✅ under `--profile spine` · 🚫 under `spine-html` | `end` (refused when it names no slot), `convex`, `inverse`, `vertexCount`, geometry, `color`. **A33**, and **A11_NO_CLIPPING_ATTACHMENTS** is the renderer-profile refusal |
634
- | `sequence` block | ❌ | |
634
+ | `sequence` block | ✅ | on a region, a mesh or a linked mesh: `count` (required — the parser's 0 loads no region), `start`, `digits`, `setup`, emitted as stated. The frames are the regions `<path><start + i>` zero-padded to `digits`, each atlased by name — the loose route's PNG of that name, or `--atlas-in`'s region — and a missing frame is refused with its number and the name looked for. A `setup` past the end (clamped), a fraction, an `image` or a `generator` beside it, and a `sequence` on any other kind are refused by name. **A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES** ([#729](https://github.com/firejune/rigc/issues/729)) |
635
635
 
636
636
  Across all five types, rigc emits the attachment's own **`name`** in exactly one case: a placeholder
637
637
  that more than one skin fills (`compile.ts`'s `composeSkinAttachmentName` and `nameSkinAttachment`).
@@ -682,7 +682,7 @@ editor-made meshes — those arrive as authored `uvs`/`triangles`/`weights`.
682
682
  | `ik`, `transform` | ✅ — one unnamed timeline per constraint, keyed by the motion spec's `ik` / `transform` arrays |
683
683
  | `path.position/spacing/mix`, `slider.time/mix` | ✅ (`PATH_TRACKS` / `SLIDER_TRACKS`) — authored as `tracks` entries naming `path` or `slider`, the same shape as `physics`, since both groups put named timelines under a constraint name. `path.mix` is one timeline of three channels |
684
684
  | `attachments.<skin>.…deform` | ❌ (validator knows it: 1 channel, `validate.ts:104-107`) |
685
- | `attachments.…sequence` | ❌ |
685
+ | `attachments.…sequence` | ✅ — the motion spec's per-animation `sequence` array of `{ skin?, slot, attachment, keys: [{ t, mode?, index?, delay? }] }`, each field emitted only where stated. A mode outside the seven (read as `hold`), a fractional or out-of-range `index` (truncated, clamped), an advancing mode at an effective delay of 0 (never advances), an attachment with no `sequence` block (every mode shows its one region) and a linked mesh that plays its source's timelines (the key is applied to nothing) are refused by name. **A46** poses every key at mid-frame samples against the file's own statement ([#729](https://github.com/firejune/rigc/issues/729)) |
686
686
  | `drawOrder` | ✅ — the motion spec's per-animation `drawOrder` array of `{ t, offsets: [{ slot, offset }] }` (`compile.ts:823`, `:850`, `compileDrawOrder` `:1918-1970`). A key with no `offsets`, and a key with an empty one, both emit the parser's reset-to-setup encoding, so two spellings cannot make two files. An unknown slot, a slot offset twice in one key, a non-whole offset and a landing outside the emitted slots are each refused by name, and **A31_DRAW_ORDER_OFFSETS_RESOLVE** re-checks the emitted file |
687
687
  | `drawOrderFolder` | ❌ — editor bookkeeping, 4.3-only |
688
688
  | `events` (+ the `root.events` block) | ✅ — the rig spec declares the names (`rig.ts` `RigEvent`), the motion spec's per-animation `events` array fires them, and **A32_EVENT_KEYS_RESOLVE** checks the three ways the timeline goes wrong quietly. `int`/`float`/`string` overrides and `audio`/`volume`/`balance` all round-trip |
@@ -738,6 +738,7 @@ This is the split Part 4(c) needs. **Spine-validity** = the file is wrong for an
738
738
  | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | validity | a slot `dark` the parser drops or reads as NaN, an `rgba2` or `rgb2` timeline on a slot with no dark colour to pose, or a key whose posed light/dark is not what it states |
739
739
  | `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` | validity | a linked mesh, in either spelling, that also states `uvs`, `triangles`, `vertices`, `hull` or `edges` — keys the `source` branch returns before reading, so the file says one mesh and every runtime draws its source's |
740
740
  | `A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | validity | an `rgb` or `alpha` timeline beside another colour timeline of the slot that poses the same channel — the later in the file overwrites the other at every time — or a key of one the pose does not reproduce |
741
+ | `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | validity | a numbered series that loads as something other than what the file states — a `sequence` block with no `count` (0 regions) or a `setup` past the end (clamped), or a `sequence` key whose `mode` is outside the seven (read as `hold`), whose `index` is fractional or past the end, whose advancing mode runs at an effective delay of 0, or that steps an attachment with no block — and then the pose: every key sampled mid-frame, the region shown held to the frame the file's statement gives ([#729](https://github.com/firejune/rigc/issues/729)) |
741
742
  | `A13_MESH_BUDGET` | **renderer-profile** | >4 mesh slots, >80 triangles per mesh |
742
743
  | `A14_NO_FULL_FRAME_MESH` | **renderer-profile** | a mesh spanning the whole stage |
743
744
  | `A19_OVERLAY_PNGS_HAVE_ALPHA` | **renderer-profile** | an overlay page that can never be transparent — no alpha channel and no `tRNS` chunk |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
5
5
  "type": "module",
6
6
  "bin": {