spine-rigc 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.ts +39 -1
- package/docs/AUTHORING.md +104 -21
- package/docs/INGEST.md +4 -3
- package/package.json +1 -1
- package/src/atlas.ts +180 -0
- package/src/compile.ts +158 -11
- package/src/ingest.ts +100 -4
- package/src/motion.ts +38 -1
- package/src/rig.ts +34 -2
- package/src/types.ts +43 -3
- package/src/validate.ts +187 -158
package/cli.ts
CHANGED
|
@@ -536,7 +536,12 @@ function meshDepthNote(m: CompileResult['meshes'][number]): string {
|
|
|
536
536
|
// hull against the triangulation's own outline. Without it the line reads
|
|
537
537
|
// as a fault on every correct contour rig, which is a diagnostic authors
|
|
538
538
|
// learn to ignore.
|
|
539
|
-
|
|
539
|
+
// Withheld, and said where the count would have stood (issue #750): the
|
|
540
|
+
// part's texels could not be located on its page, so there is no count
|
|
541
|
+
// that is about this part.
|
|
542
|
+
if (m.depth.unlocated !== undefined) {
|
|
543
|
+
parts.push(`the count of vertices on undrawn texels is not measured: ${m.depth.unlocated}. ${PAGE_GRID_UNLOCATED}`);
|
|
544
|
+
} else if (m.depth.undrawn !== null && m.depth.undrawn > 0) {
|
|
540
545
|
parts.push(
|
|
541
546
|
`${m.depth.undrawn} of ${m.vertices} vertices sample a texel the part image does not draw — ` +
|
|
542
547
|
(m.kind === 'contour'
|
|
@@ -581,6 +586,17 @@ const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> =
|
|
|
581
586
|
authored: 'authored geometry rigc did not build; it assumes nothing about the topology',
|
|
582
587
|
};
|
|
583
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Why a figure taken off a part's texels is withheld on a page whose file is
|
|
591
|
+
* not the size its atlas declares — the tail of every line that withholds one
|
|
592
|
+
* (issue #750). The page and its ratios come first, from `pageGridSaid`; this
|
|
593
|
+
* says what that does to the reading and where the repair is named.
|
|
594
|
+
*/
|
|
595
|
+
const PAGE_GRID_UNLOCATED =
|
|
596
|
+
'rigc lifts a part off its page at the coordinates the atlas states, and on this file those are not where the ' +
|
|
597
|
+
"part's texels are, so a figure taken there would describe another part of the page. " +
|
|
598
|
+
'`A06_ATLAS_PAGE_SIZE_MATCHES_PNG` refuses the page by the same ratio and names the `scale:` header that states it';
|
|
599
|
+
|
|
584
600
|
/**
|
|
585
601
|
* What a mesh measured about its own fit against the art it names, or nothing
|
|
586
602
|
* for a mesh with no art to measure against.
|
|
@@ -599,6 +615,13 @@ const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> =
|
|
|
599
615
|
* fill over transparent pixels with nothing anywhere saying so (issue #275).
|
|
600
616
|
*/
|
|
601
617
|
function meshFit(m: CompileResult['meshes'][number]): string {
|
|
618
|
+
// The fit is a measurement against the part's texels, and on a page whose
|
|
619
|
+
// file is not its declared size the region lift does not have them (issue
|
|
620
|
+
// #750). It printed 68.49% / 76.24px there for a mesh that measures 100.00% /
|
|
621
|
+
// 16.00px on the page it was packed from — two plausible numbers about
|
|
622
|
+
// another part of the picture. So the line says what was not measured and
|
|
623
|
+
// why, in the place the figures stood, and prints no figure.
|
|
624
|
+
if (m.fitWithheld !== undefined) return ` fit not measured: ${m.fitWithheld}. ${PAGE_GRID_UNLOCATED}`;
|
|
602
625
|
if (m.coverage === undefined) return '';
|
|
603
626
|
const hole = m.holePixels ? `, enclosing ${m.holePixels}px of hole` : '';
|
|
604
627
|
return ` covers ${(m.coverage * 100).toFixed(2)}% of the art, reaching ${m.overshoot?.toFixed(2) ?? '?'}px past it${hole}`;
|
|
@@ -2672,6 +2695,21 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
2672
2695
|
// the sentence saying so scrolled off the top. It is the invocation that has
|
|
2673
2696
|
// to change, so it is refused before the report it cannot finish (issue #697).
|
|
2674
2697
|
refuseUnposableArt(result, opts);
|
|
2698
|
+
// 📐 **A page whose file is not its declared size is said where the report
|
|
2699
|
+
// starts, and nothing is refused for it** (issue #750). `explain` never
|
|
2700
|
+
// gates, so on such a pack nothing stood between the region lift and the
|
|
2701
|
+
// figures it fed: the mesh block printed a fit taken off another part of the
|
|
2702
|
+
// page. The pack still compiles — a runtime draws it, and most of this report
|
|
2703
|
+
// (bones, slots, timelines) reads no texel at all — so the page is named
|
|
2704
|
+
// here, once, with the ratio `A06` refuses it by, and every figure that WOULD
|
|
2705
|
+
// have been taken off its texels is withheld on its own line and says so.
|
|
2706
|
+
// `build` prints no such line: `A06` is its statement of the same fact.
|
|
2707
|
+
for (const grid of result.pageGrids) {
|
|
2708
|
+
console.log(
|
|
2709
|
+
` .. ${grid.said}: every figure below taken off this page's texels is withheld, and says so where it ` +
|
|
2710
|
+
`would have stood. ${PAGE_GRID_UNLOCATED}`,
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2675
2713
|
// `compile` has already parsed this file, so the read below cannot fail — but
|
|
2676
2714
|
// it goes through the same parser rather than a cast, because the cast was the
|
|
2677
2715
|
// last one in the repository and issue #307 was about exactly that.
|
package/docs/AUTHORING.md
CHANGED
|
@@ -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
|
|
@@ -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 | — |
|
|
@@ -2338,14 +2381,20 @@ carrying here:
|
|
|
2338
2381
|
`true` and leaves the rest alone (`Animation.js:2066-2072`). So the flag is one
|
|
2339
2382
|
constraint's **opt-in to being driven in bulk**, per tuning value, and it does
|
|
2340
2383
|
nothing on its own. The parser's default for all seven is `false`.
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
through**, `false` included: measured, a
|
|
2347
|
-
|
|
2348
|
-
dropping it the way the motion spec's
|
|
2384
|
+
The motion spec keys that timeline as **`"physics": "*"`** (§4.4,
|
|
2385
|
+
[#726](https://github.com/firejune/rigc/issues/726)), and rigc writes it under
|
|
2386
|
+
the empty name. Until then rigc emitted the flags and could not emit the
|
|
2387
|
+
timeline that reads them: every physics track named its constraint and the
|
|
2388
|
+
empty name was refused as `keys unknown physics constraint ""`. What rigc does
|
|
2389
|
+
with the flags is **pass them through**, `false` included: measured, a
|
|
2390
|
+
constraint stating none emits none, and one stating `"windGlobal": false` emits
|
|
2391
|
+
`"windGlobal": false` rather than dropping it the way the motion spec's
|
|
2392
|
+
`physics` table drops a default (§4.6). ⚠️ That table has **no** `…Global`
|
|
2393
|
+
field, so a constraint a `"*"` track is meant to reach is declared here, in the
|
|
2394
|
+
rig spec's `constraints`.
|
|
2395
|
+
- `"*"` is **reserved** as a physics constraint's name, in the rig spec and in
|
|
2396
|
+
§4.6's table alike: a track naming it could mean either. `compile` refuses one
|
|
2397
|
+
by name.
|
|
2349
2398
|
- ⚠️ `src/rig.ts` called the physics `ScaleYMode` key **`scaleYMode`** until
|
|
2350
2399
|
issue #545 — the runtime's field name rather than the format's key — and nothing
|
|
2351
2400
|
read it, so a spec that wrote `scaleYMode` set no mode and said nothing. A rig
|
|
@@ -3067,7 +3116,7 @@ group that names a member twice is a compile error, and so is one that names non
|
|
|
3067
3116
|
⭐ **Which of the three a group's members are is decided by the `property`, not
|
|
3068
3117
|
by the group.** A group declares names and nothing else; the compiler reads the
|
|
3069
3118
|
property first — a physics timeline makes the members physics constraints, one of
|
|
3070
|
-
a bone's
|
|
3119
|
+
a bone's eleven makes them bones, and `attachment`/`rgba` makes them slots — and then
|
|
3071
3120
|
resolves every member against the rig as that. So a `group` is the one target
|
|
3072
3121
|
where the property picks the family rather than the other way round (§4.4's ⭐ is
|
|
3073
3122
|
about the three **constraint** families, which are picked by the field), and a
|
|
@@ -3109,6 +3158,7 @@ a deform). Folding them in would make `v` mean four different things depending o
|
|
|
3109
3158
|
| --- | --- | --- |
|
|
3110
3159
|
| `bone` | `translate`, `scale`, `shear` | `[x, y]` |
|
|
3111
3160
|
| `bone` | `translatex`, `translatey`, `scalex`, `scaley`, `shearx`, `sheary`, `rotate` | `[value]` |
|
|
3161
|
+
| `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
3162
|
| `slot` | `rgba` | `[r, g, b, a]` in 0..1 |
|
|
3113
3163
|
| `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
3164
|
| `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 +3168,7 @@ a deform). Folding them in would make `v` mean four different things depending o
|
|
|
3118
3168
|
| `physics` | `inertia`, `strength`, `damping`, `mass`, `wind`, `gravity` | `[value]` — the constraint's own tuning, keyed over time |
|
|
3119
3169
|
| `physics` | `mix` | `[mix]`, **0 or more** — the constraint's authority |
|
|
3120
3170
|
| `physics` | `reset` | `null` — the key *is* the event |
|
|
3171
|
+
| `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
3172
|
| `path` | `position`, `spacing` | `[value]` — see §4.12 |
|
|
3122
3173
|
| `path` | `mix` | `[mixRotate, mixX, mixY]` — one timeline, three channels |
|
|
3123
3174
|
| `slider` | `time` | `[seconds]` — where in its animation the slider sits |
|
|
@@ -3126,10 +3177,10 @@ a deform). Folding them in would make `v` mean four different things depending o
|
|
|
3126
3177
|
Translate values are **relative to the bone's setup position**; scale values are
|
|
3127
3178
|
multipliers where `1` is setup; rotation is in degrees.
|
|
3128
3179
|
|
|
3129
|
-
⚠️ **A `bone` track's `property` is one of the
|
|
3180
|
+
⚠️ **A `bone` track's `property` is one of the eleven above, and anything else is a
|
|
3130
3181
|
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
|
|
3182
|
+
translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate,
|
|
3183
|
+
inherit)`, §5.1's row. The eleven are the three rows above read as one list, in the order the
|
|
3133
3184
|
message prints them, and they are the emitter's own dispatch table (`BONE_TRACKS`
|
|
3134
3185
|
in `src/compile.ts`): `resolveTargets` asks that table which family a track
|
|
3135
3186
|
belongs to, `compileValueTrack` writes a key out of the shape it finds there, and
|
|
@@ -3149,13 +3200,15 @@ accepts is what it accepts.
|
|
|
3149
3200
|
**constraint** property written on a bone track (`mix`, `inertia`, `position`,
|
|
3150
3201
|
…) never reaches this refusal at all — it is refused first, by the row that
|
|
3151
3202
|
names the field its constraint's name belongs in (§4.12).
|
|
3152
|
-
- **Nothing derives this page's copy of the
|
|
3203
|
+
- **Nothing derives this page's copy of the eleven from the table.** What keeps the
|
|
3153
3204
|
two in step is the control that quotes the message — `RF26` in `selftest.ts` —
|
|
3154
3205
|
which goes red if the list ever widens without this page moving with it, and
|
|
3155
3206
|
`RF27` holds the slot clause the same way. The selftest's spelling census
|
|
3156
|
-
(`PS144`) reads the
|
|
3157
|
-
it actually poses, both ways, which is what makes the list checkable at
|
|
3158
|
-
was stated there too until the refusal had something to state.
|
|
3207
|
+
(`PS144`) reads the eleven off the same message and compares them against the
|
|
3208
|
+
eleven it actually poses, both ways, which is what makes the list checkable at
|
|
3209
|
+
all: it was stated there too until the refusal had something to state. It was
|
|
3210
|
+
ten until [#733](https://github.com/firejune/rigc/issues/733) added `inherit`,
|
|
3211
|
+
and that landing is what moved `RF26`, this page and the census together.
|
|
3159
3212
|
|
|
3160
3213
|
⚠️ **A `slot` track's `property` is one of the six above, and anything else is a
|
|
3161
3214
|
compile error** — `animation "A" slot "X" has no timeline "P" (it has:
|
|
@@ -3322,6 +3375,28 @@ a delta from the constraint's own setting.
|
|
|
3322
3375
|
the first sub-step and **still NaN after the restoring key**, and a keyed
|
|
3323
3376
|
`damping` of `2` was still 6.9e4 two seconds later.
|
|
3324
3377
|
|
|
3378
|
+
**`"physics": "*"` is the physics timeline that names no constraint**
|
|
3379
|
+
([#726](https://github.com/firejune/rigc/issues/726)). The skeleton file writes it
|
|
3380
|
+
under the empty name — `animations.<a>.physics[""]`, which `SkeletonJson` loads as
|
|
3381
|
+
constraint index `-1` without looking anything up — and the runtime then applies it
|
|
3382
|
+
to every active physics constraint whose own data declares the keyed property
|
|
3383
|
+
global (`Animation.js:2067-2075`). So what it drives is decided in the **rig spec**,
|
|
3384
|
+
by the seven `…Global` flags of §3.5, one per value timeline; `reset` is the
|
|
3385
|
+
exception and resets every physics constraint. [measured] over three constraints of
|
|
3386
|
+
which two declare `strengthGlobal`, a `"*"` `strength` track keyed to 35 poses those
|
|
3387
|
+
two at 35 and leaves the third at its setup 100.
|
|
3388
|
+
|
|
3389
|
+
- ⚠️ **A `"*"` track that reaches no constraint is a compile error**, naming the flag
|
|
3390
|
+
and the constraints that do not declare it: the runtime would walk every
|
|
3391
|
+
constraint, skip each, and the file would parse and do nothing. Set the flag on
|
|
3392
|
+
the constraints the track is for, or key one by name.
|
|
3393
|
+
- 🚫 **`"physics": ""` is refused**, and deliberately: the empty string is the
|
|
3394
|
+
likeliest shape of a target somebody forgot to fill in, and a forgotten target
|
|
3395
|
+
that quietly meant "every global constraint" is the silence this format exists to
|
|
3396
|
+
name. The refusal points at `"*"`.
|
|
3397
|
+
- ⚠️ `"*"` is a track's `physics` field and nothing else — a `group` that lists it
|
|
3398
|
+
is refused, and so is a constraint called `"*"` (§3.5).
|
|
3399
|
+
|
|
3325
3400
|
On a track that names a `group`, one key's `v` may instead be a **map keyed by
|
|
3326
3401
|
member name**, whose entries are each exactly the `v` above — or a `derive`
|
|
3327
3402
|
model the compiler evaluates per member. §4.5.1.
|
|
@@ -4936,6 +5011,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
4936
5011
|
| `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
5012
|
| `image "X.png" is not on disk at …` | fix the name, or point `--images` at the right directory |
|
|
4938
5013
|
| `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)) |
|
|
5014
|
+
| `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
5015
|
| `--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
5016
|
| `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
5017
|
| `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,6 +5024,10 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
4948
5024
|
| `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
5025
|
| `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
5026
|
| `animation "A" keys unknown bone "X"` | the track's `bone` is not in the rig |
|
|
5027
|
+
| `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 |
|
|
5028
|
+
| `` `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 `"*"` |
|
|
5029
|
+
| `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
|
+
| `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
5031
|
| `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
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)) |
|
|
4953
5033
|
| `a key carries both a named easing and a raw curve; pick one` | R6 |
|
|
@@ -5014,8 +5094,11 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
5014
5094
|
| `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
5095
|
| `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
5096
|
| `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
|
|
5018
|
-
| `animation "A"
|
|
5097
|
+
| `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 |
|
|
5098
|
+
| `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)) |
|
|
5099
|
+
| `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)) |
|
|
5100
|
+
| `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 |
|
|
5101
|
+
| `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
5102
|
| `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
5103
|
| `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
5104
|
| `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 +5205,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
|
|
|
5122
5205
|
| `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
5206
|
| `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
5207
|
| `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 |
|
|
5208
|
+
| `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
5209
|
| `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
|
|
5127
5210
|
| `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
|
|
5128
5211
|
| `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 |
|
|
@@ -5146,7 +5229,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
|
|
|
5146
5229
|
| `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
5230
|
| `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
5231
|
| `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 |
|
|
5232
|
+
| `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
5233
|
| `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
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 |
|
|
5152
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 |
|
package/docs/INGEST.md
CHANGED
|
@@ -598,7 +598,7 @@ is the one failure a comparison of two sets cannot show you.
|
|
|
598
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
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 |
|
|
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
|
-
| `BONE_TIMELINE` | `BLOCK` | 1 | a bone timeline the motion spec has no track for. The detail names the
|
|
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` |
|
|
603
603
|
| `CONSTRAINT_KEY_RESTATED` | `LOSS` | 0 | an `ik` or `transform` track whose keys do not all state the same fields. The motion spec takes one field set per track, so a field **any** key states is written on **every** key at the value the parser would have read there | nothing. Same values, larger file — the rebuild plays what the source plays |
|
|
604
604
|
| `CONSTRAINT_TYPE` | `BLOCK` | 1 | a constraint whose `type` is none rigc knows, so the whole constraint is dropped rather than approximated | the rebuild has no such constraint; check the spelling before assuming the type is unsupported |
|
|
@@ -613,13 +613,14 @@ is the one failure a comparison of two sets cannot show you.
|
|
|
613
613
|
| `PATH_LENGTHS` | `LOSS` | 0 | the source states a path attachment's `lengths` and rigc re-measures it as `PathConstraint` does | nothing. Dropping it is the correct reading: the field is the runtime's own four-sample forward difference, not an arc length |
|
|
614
614
|
| `PATH_TIMELINE` | `BLOCK` | 1 | a path-constraint timeline the motion spec has no track for — it carries position, spacing and mix | transcribe it, or accept that the rebuild plays nothing there |
|
|
615
615
|
| `PHYSICS_DRIVES_NOTHING` | `LOSS` | 0 | a physics constraint none of whose `x`, `y`, `rotate`, `scaleX`, `shearX` is above 0 — absent, or stated at 0 or below. `PhysicsConstraint.update` applies a component only above 0 (`PhysicsConstraint.js:112`), so it moves no bone, and `build` refuses exactly that shape by name at `A23_PHYSICS_CONSTRAINT_EFFECTIVE` — which, until [#731](https://github.com/firejune/rigc/issues/731), meant the whole rebuild of a file an editor exports was refused over a constraint that did nothing in it. The rig spec **omits** it, together with every timeline keyed to it (a track naming it would be an unknown constraint to the rebuild, refused at compile) and its place on any skin's `physics` list; the detail names each, and the values it did state. Measured on a generated rig through spine-core, posing the source with and without such a constraint differs by **0** on every bone world value — and by at most 9e-8 when it sits on the root, which is the runtime's `modifyWorld` recomputing a local transform it had no reason to, not a component. ⚠️ **One thing does move:** a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation, and the detail says which animation and both lengths | nothing, if it was meant to do nothing. If it was meant to jiggle, the file never said so: give it the component it should drive and it is carried like any other. Where the detail names a shortened animation and the length matters to whatever loops it, key something at the length it had |
|
|
616
|
+
| `PHYSICS_GLOBAL_REACHES_NOTHING` | `LOSS` | 0 | a physics timeline keyed under the **empty** name — the one that names no constraint, which the runtime applies to every physics constraint declaring that property global (`"strengthGlobal": true` for `strength`; `reset` resets every physics constraint and asks no flag) — in a file where no physics constraint the rebuild carries declares it. The motion spec spells that timeline `"physics": "*"` ([#726](https://github.com/firejune/rigc/issues/726)) and `build` refuses one that reaches nobody by name, so the rig spec **omits** it: in the source it walked every constraint and wrote into none. A constraint `PHYSICS_DRIVES_NOTHING` omitted counts as not carried — it was the only thing such a timeline could reach, and it moved no bone. ⚠️ As with that row, a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation and the detail says both lengths. An unnamed timeline that **does** reach a constraint is not a finding at all: it is carried as `"*"` and rebuilt under the empty name byte for byte | nothing, if it was meant to do nothing. If it was meant to drive the constraints, the file never said which: set `"<property>Global": true` on them in the rig spec and key it as `"physics": "*"` |
|
|
616
617
|
| `PHYSICS_TIMELINE` | `BLOCK` | 1 | the same for a physics constraint, whose eight the motion spec carries in full — so this is reachable only for a name the **parser** falls through too | as `PATH_TIMELINE` |
|
|
617
618
|
| `SLIDER_TIMELINE` | `BLOCK` | 1 | the same for a slider, which carries time and mix | as `PATH_TIMELINE` |
|
|
618
619
|
| `SLOT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a slot | as `BONE_FIELD` |
|
|
619
620
|
| `SLOT_TIMELINE` | `BLOCK` | 1 | a slot timeline the motion spec has no track for. Since [#730](https://github.com/firejune/rigc/issues/730) carried `rgb`, `alpha` and `rgb2` the spec has a track for all six the format has, so what still reaches this line is a name **outside** the format — `sequence` written on a slot rather than an attachment is the likeliest — and the detail says so: the runtime's own reader throws `Invalid timeline type for a slot` on it, so no player loads that file either. The detail names the tracks the spec does have and, when the format has any it lacks, those too, **both read off the tables** rather than listed here: this cell named `rgba2` among the timelines nobody carries until [#690](https://github.com/firejune/rigc/issues/690) made that false, which is what a hand-kept list beside a derived one always comes to. `rgb` and `alpha` are carried under their own names and on their own key times, never folded into one `rgba` — that would state each channel at the other's key times, a value nobody keyed | fix the timeline's name, or accept that the rebuild plays nothing there |
|
|
620
621
|
| `SPEC_REFUSED` | `BLOCK` | 1 | the specs were written and **rigc's own parser refuses one of them** — the detail carries that refusal word for word, after the file and the spec it is about. It is the one finding that is not about a single construct: it is whatever `parseRigSpec` or `parseMotionSpec` names, from a shape the format holds and the spec cannot say (a constraint that is `skinRequired` under no skin) to a defect in this decompiler. Until [#692](https://github.com/firejune/rigc/issues/692) the refusal left through `ingest` itself, so the run exited 1 with no line, no code and no `findings.json` at all | read the quoted sentence against the skeleton: it names the object. Both specs are on disk for exactly that, and `build` will refuse them until the shape has a spelling — [AUTHORING §5.1](AUTHORING.md) is the list of what a parser says |
|
|
621
|
-
| `TIMELINE_FIELD` | `BLOCK` | 1 | a key field on a bone, path, physics or slider timeline that is not part of that timeline's shape | check the spelling; the field is dropped from the rebuilt key |
|
|
622
|
-
| `TIMELINE_KEY_RESTATED` | `LOSS` | 0 | **the commonest line in a real run.** An editor omits a channel that equals the parser's default; the motion spec's `v` is positional, so the omission is written out at that default | nothing. The same values the runtime reads, spelled out — a larger file and the same animation |
|
|
622
|
+
| `TIMELINE_FIELD` | `BLOCK` | 1 | a key field on a bone, path, physics or slider timeline that is not part of that timeline's shape. On an `inherit` key that includes a `curve`: the parser reads `time` and `inherit` there and nothing else, and `build` refuses a curve on that track by name | check the spelling; the field is dropped from the rebuilt key |
|
|
623
|
+
| `TIMELINE_KEY_RESTATED` | `LOSS` | 0 | **the commonest line in a real run.** An editor omits a channel that equals the parser's default; the motion spec's `v` is positional, so the omission is written out at that default. On an `inherit` key it is the mode: one that omits it is written as `normal` — the parser's default — and one spelled with a capital first letter (`NoScale`) as the editor's `noScale`, the same mode either way | nothing. The same values the runtime reads, spelled out — a larger file and the same animation |
|
|
623
624
|
| `TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
|
|
624
625
|
|
|
625
626
|
⚠️ **One thing the table cannot carry: the region key is kept where a placeholder is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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": {
|
package/src/atlas.ts
CHANGED
|
@@ -1070,3 +1070,183 @@ export function extractRegion(page: Plate, region: AtlasRegion): Plate {
|
|
|
1070
1070
|
}
|
|
1071
1071
|
return out;
|
|
1072
1072
|
}
|
|
1073
|
+
|
|
1074
|
+
// ---------------------------------------------------------------------------
|
|
1075
|
+
// a page against the file it names
|
|
1076
|
+
// ---------------------------------------------------------------------------
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* The one page rectangle every region on a page shares: the size the atlas
|
|
1080
|
+
* declares for it against the size of the file it names.
|
|
1081
|
+
*
|
|
1082
|
+
* ⭐ **What a size that disagrees with the file is and is not**, measured rather
|
|
1083
|
+
* than assumed (issue #715). `TextureAtlas` computes every region's UVs as a
|
|
1084
|
+
* fraction of the DECLARED size — `region.u = region.x / page.width`, spine-core
|
|
1085
|
+
* 4.3.13 `dist/TextureAtlas.js:162-171` — `MeshAttachment.computeUVs` takes its
|
|
1086
|
+
* `textureWidth` from the same field (`dist/attachments/MeshAttachment.js:125`),
|
|
1087
|
+
* `RegionAttachment.computeUVs` reads nothing but `u/v/u2/v2`
|
|
1088
|
+
* (`dist/attachments/RegionAttachment.js:152-167`), and `TextureAtlasPage.setTexture`
|
|
1089
|
+
* never writes `width`/`height`. So **nothing in the region mapping reads the
|
|
1090
|
+
* texture's own size**, and a page whose PNG is the declared page RESCALED is
|
|
1091
|
+
* addressed at the same fraction of the picture whatever size the file is:
|
|
1092
|
+
* measured on a coordinate-ramp page where every texel names its own position,
|
|
1093
|
+
* rigc's own rasteriser drew 116,480 pixels in both and 0 in exactly one at a
|
|
1094
|
+
* uniform 0.5.
|
|
1095
|
+
*
|
|
1096
|
+
* ⇒ That is why this is a clause about **texel** readers rather than about
|
|
1097
|
+
* drawing, and why it is nevertheless not renderer policy. Three readers address
|
|
1098
|
+
* the page at the coordinates the atlas states, and two of them are rigc's own:
|
|
1099
|
+
* `A19`'s alpha scan in [`src/validate.ts`](validate.ts), the region lift
|
|
1100
|
+
* `partPlate` traces a mesh generator over in [`src/compile.ts`](compile.ts), and `spine-html`'s region tier, which
|
|
1101
|
+
* cuts each part with `drawImage(image, x, y, w, h, …)` and says in its own
|
|
1102
|
+
* comment that it tests against the image rather than the `size:` line. Measured
|
|
1103
|
+
* on the same page: the lift returned 768 of 768 texels from somewhere else.
|
|
1104
|
+
*
|
|
1105
|
+
* 🔑 And the format already states coarser texels honestly — `scale:`, which
|
|
1106
|
+
* rigc reads (`AtlasPage.scale`) and `--atlas-in` divides by. The same art
|
|
1107
|
+
* declared that way builds green and renders identically, so this refusal names
|
|
1108
|
+
* a repair the format provides rather than one rigc invented.
|
|
1109
|
+
*/
|
|
1110
|
+
export interface PageGridReading {
|
|
1111
|
+
/** file width / declared width, and the same for height. Both 1 when they agree. */
|
|
1112
|
+
readonly x: number;
|
|
1113
|
+
readonly y: number;
|
|
1114
|
+
/** One ratio for both axes — the only relation a `scale:` line can state. */
|
|
1115
|
+
readonly uniform: boolean;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** `null` when the page declares no positive size for the ratios to divide by. */
|
|
1119
|
+
export function pageGridReading(
|
|
1120
|
+
declared: { width: number; height: number },
|
|
1121
|
+
file: { width: number; height: number },
|
|
1122
|
+
): PageGridReading | null {
|
|
1123
|
+
if (declared.width <= 0 || declared.height <= 0) return null;
|
|
1124
|
+
return {
|
|
1125
|
+
x: file.width / declared.width,
|
|
1126
|
+
y: file.height / declared.height,
|
|
1127
|
+
// Cross-multiplied rather than compared as two divisions: the question is
|
|
1128
|
+
// whether one rational number describes both axes, and two floats that
|
|
1129
|
+
// round to the same digits are not that.
|
|
1130
|
+
uniform: file.width * declared.height === file.height * declared.width,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** A ratio, printed the one way every message here prints one. */
|
|
1135
|
+
function gridRatio(n: number): string {
|
|
1136
|
+
return n.toFixed(4);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* The first number on this page that a `scale:` re-declaration could not carry,
|
|
1141
|
+
* or `null` when every one of them lands on a whole texel of the file.
|
|
1142
|
+
*
|
|
1143
|
+
* Every number in a region block is in the page's own texel units — `bounds` and
|
|
1144
|
+
* `offsets` alike — so re-declaring the page at the file's size means scaling all
|
|
1145
|
+
* eight by the same ratio, and a region that then lands between texels is one no
|
|
1146
|
+
* reader could cut out. Stated as integer arithmetic (`n * file % declared`)
|
|
1147
|
+
* rather than as a float test, so the answer does not depend on how the ratio
|
|
1148
|
+
* rounded. ⚠️ One ratio for both axes, so this is the UNIFORM case's question
|
|
1149
|
+
* and is only ever asked there — a page whose two axes differ has no `scale:`
|
|
1150
|
+
* line to be re-declared with at all.
|
|
1151
|
+
*/
|
|
1152
|
+
function firstNumberOffTheCoarseGrid(
|
|
1153
|
+
regions: ReadonlyArray<{
|
|
1154
|
+
name: string;
|
|
1155
|
+
x: number;
|
|
1156
|
+
y: number;
|
|
1157
|
+
width: number;
|
|
1158
|
+
height: number;
|
|
1159
|
+
offsetX: number;
|
|
1160
|
+
offsetY: number;
|
|
1161
|
+
originalWidth: number;
|
|
1162
|
+
originalHeight: number;
|
|
1163
|
+
}>,
|
|
1164
|
+
declared: number,
|
|
1165
|
+
file: number,
|
|
1166
|
+
): string | null {
|
|
1167
|
+
for (const region of regions) {
|
|
1168
|
+
const numbers: Array<[string, number]> = [
|
|
1169
|
+
['bounds x', region.x],
|
|
1170
|
+
['bounds y', region.y],
|
|
1171
|
+
['bounds width', region.width],
|
|
1172
|
+
['bounds height', region.height],
|
|
1173
|
+
['offsets offsetX', region.offsetX],
|
|
1174
|
+
['offsets offsetY', region.offsetY],
|
|
1175
|
+
['offsets originalWidth', region.originalWidth],
|
|
1176
|
+
['offsets originalHeight', region.originalHeight],
|
|
1177
|
+
];
|
|
1178
|
+
for (const [field, value] of numbers) {
|
|
1179
|
+
if ((value * file) % declared === 0) continue;
|
|
1180
|
+
return (
|
|
1181
|
+
`region ${JSON.stringify(region.name.trim())}'s \`${field}\` of ${value} becomes ` +
|
|
1182
|
+
`${((value * file) / declared).toFixed(4)} on the file's own grid, which is not a whole texel`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/** What every size-mismatch sentence says before it says what to do about it. */
|
|
1190
|
+
const PAGE_GRID_PREAMBLE =
|
|
1191
|
+
'A runtime does not read the file\'s own size anywhere in the region mapping — `TextureAtlas` computes every ' +
|
|
1192
|
+
"region's UVs as a fraction of the DECLARED size (`region.u = region.x / page.width`, spine-core " +
|
|
1193
|
+
'`dist/TextureAtlas.js:162-171`) — so a page whose PNG is the declared page RESCALED draws the same picture at ' +
|
|
1194
|
+
"the file's resolution, and one whose PNG is anything else draws whatever sits at those fractions. What a " +
|
|
1195
|
+
'declared size that disagrees with the file breaks is every reader that addresses the page in TEXELS: this ' +
|
|
1196
|
+
"validator's own `A19` alpha scan, the region lift rigc's mesh generators trace, and a canvas renderer that " +
|
|
1197
|
+
'cuts each part out of the page by source rectangle.';
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* What a page that is not its declared size is, stated as the page, both sizes
|
|
1201
|
+
* and the two ratios — the clause every message about it opens with.
|
|
1202
|
+
*
|
|
1203
|
+
* `A06`'s refusal starts with it, and so does every line in which a reader that
|
|
1204
|
+
* addresses the page in texels declines to (issue #750): `explain` and `build`
|
|
1205
|
+
* print it where they withhold a mesh's fit, and the contour generator's refusal
|
|
1206
|
+
* carries the whole of `pageGridSentence` below. One derivation, so a page is
|
|
1207
|
+
* never described two ways by the two halves of one run.
|
|
1208
|
+
*/
|
|
1209
|
+
export function pageGridSaid(
|
|
1210
|
+
page: { name: string; width: number; height: number },
|
|
1211
|
+
file: { width: number; height: number },
|
|
1212
|
+
): string {
|
|
1213
|
+
const said = `page "${page.name}" declares ${page.width}x${page.height} and its PNG is ${file.width}x${file.height}`;
|
|
1214
|
+
const grid = pageGridReading(page, file);
|
|
1215
|
+
return grid === null
|
|
1216
|
+
? said
|
|
1217
|
+
: `${said} — ${gridRatio(grid.x)} of the declared width and ${gridRatio(grid.y)} of the declared height`;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* `A06`'s whole sentence for a page whose file is not its declared size, or
|
|
1222
|
+
* `null` when the two agree: the page and its ratios (`pageGridSaid`), why a
|
|
1223
|
+
* runtime still draws it and which readers it breaks, and the repair the format
|
|
1224
|
+
* offers — or why it offers none.
|
|
1225
|
+
*
|
|
1226
|
+
* It lives here rather than in [`src/validate.ts`](validate.ts) because the
|
|
1227
|
+
* compiler states it too, and `src/compile.ts` must not link the runtime that
|
|
1228
|
+
* file links. `regions` is the page's own regions; only the uniform case reads
|
|
1229
|
+
* them, to find a number a `scale:` re-declaration could not carry.
|
|
1230
|
+
*/
|
|
1231
|
+
export function pageGridSentence(
|
|
1232
|
+
page: { name: string; width: number; height: number },
|
|
1233
|
+
file: { width: number; height: number },
|
|
1234
|
+
regions: Parameters<typeof firstNumberOffTheCoarseGrid>[0],
|
|
1235
|
+
): string | null {
|
|
1236
|
+
if (page.width === file.width && page.height === file.height) return null;
|
|
1237
|
+
const said = pageGridSaid(page, file);
|
|
1238
|
+
const grid = pageGridReading(page, file);
|
|
1239
|
+
if (grid === null) return `${said}. ${PAGE_GRID_PREAMBLE}`;
|
|
1240
|
+
const off = grid.uniform ? firstNumberOffTheCoarseGrid(regions, page.width, file.width) : null;
|
|
1241
|
+
const repair = !grid.uniform
|
|
1242
|
+
? 'The `scale:` header states one ratio for both axes, so a page whose axes differ cannot be ' +
|
|
1243
|
+
`declared honestly at all: re-export the page at ${page.width}x${page.height}, or repack it.`
|
|
1244
|
+
: off !== null
|
|
1245
|
+
? 'The format states coarser texels with the `scale:` header, but this page cannot be re-declared ' +
|
|
1246
|
+
`that way: ${off}. Re-export the page at ${page.width}x${page.height}, or repack it.`
|
|
1247
|
+
: 'The format states coarser texels with the `scale:` header and rigc builds that: declare ' +
|
|
1248
|
+
`\`size: ${file.width}, ${file.height}\` with \`scale: ${gridRatio(grid.x)}\` and multiply every ` +
|
|
1249
|
+
`\`bounds\`/\`offsets\` on this page by ${gridRatio(grid.x)}, and every part keeps the size it ` +
|
|
1250
|
+
'has now.';
|
|
1251
|
+
return `${said}. ${PAGE_GRID_PREAMBLE} ${repair}`;
|
|
1252
|
+
}
|