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 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
- if (m.depth.undrawn > 0) {
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
- ⚠️ **rigc emits them and cannot emit the timeline that reads them.** Every
2342
- physics track in a motion spec names its constraint and that name is resolved
2343
- against the rig — the empty name is refused, `animation "A" keys unknown physics
2344
- constraint "" (the rig declares: …)` — so the flags are for a player, or a
2345
- later hand-edit, that supplies one. What rigc does with them is **pass them
2346
- through**, `false` included: measured, a constraint stating none emits none, and
2347
- one stating `"windGlobal": false` emits `"windGlobal": false` rather than
2348
- dropping it the way the motion spec's `physics` table drops a default (§4.6).
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 ten makes them bones, and `attachment`/`rgba` makes them slots — and then
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 ten above, and anything else is a
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 ten are the two rows above read as one list, in the order 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 ten from the table.** What keeps 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 ten off the same message and compares them against the ten
3157
- it actually poses, both ways, which is what makes the list checkable at all: it
3158
- was stated there too until the refusal had something to state.
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 ten timelines and `P` is none of them. Fix the spelling — the single-axis ones are lower-case (`translatex`, not `translateX`). A **constraint** property is refused first, by its own row, naming the field its constraint's name goes in. When `P` is a slot timeline the message says so and where to put the name: `. "rgba" is a slot timeline — put the name in "slot"`. Before [#656](https://github.com/firejune/rigc/issues/656) all of them read `bone "B" cannot take slot property "P"`, which named the slot family whatever you had written and listed nothing |
5018
- | `animation "A" group "G" has no timeline "P" (a bone group has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate; a slot group has: attachment, rgba, rgb, alpha, rgba2, rgb2; a physics constraint group has: inertia, strength, damping, mass, wind, gravity, mix, reset)` | §4.3, §4.4 — a group's family is decided by the property, and `P` is in none of the three tables, so there is no family to resolve the members as. Fix the spelling and the group becomes whichever family the property names. The group is refused before its members are looked up, so a member the rig does not declare is a **later** message; an unknown group NAME is an earlier one. Before [#661](https://github.com/firejune/rigc/issues/661) a group of bones read `animation "A" targets unknown slot "M"` and a group of slots got the slot row below, naming one family out of three |
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 ten it has. `inherit` is the eleventh case of the runtime's own bone switch and the one this corpus has no example of | transcribe it, or accept that the rebuild plays nothing there |
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.30.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
+ }