spine-rigc 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/INGEST.md CHANGED
@@ -594,11 +594,11 @@ is the one failure a comparison of two sets cannot show you.
594
594
  | `ANIMATION_GROUP` | `BLOCK` | 1 | the animation carries a group the motion spec has no home for. The detail names the ten it does carry. `drawOrderFolder` is the group to know about: the runtime reads it and builds a timeline from it, and no export in this corpus carries one | transcribe that group by hand (§2), or accept that the rebuild does not carry it |
595
595
  | `ATTACHMENT_<TYPE>` | `BLOCK` | 1 | an attachment of a type rigc does not emit; the code is composed from the type, so on the one type left it reads `ATTACHMENT_POINT`. rigc emits region, mesh, linkedmesh, boundingbox, clipping and path — `linkedmesh` since [#691](https://github.com/firejune/rigc/issues/691), and `point` is the remaining deferred type | the rebuild will not have that attachment at all. `docs/SPEC_COVERAGE.md` part 1-6 says what a deferred type would carry |
596
596
  | `ATTACHMENT_LINK_GEOMETRY` | `LOSS` | 0 | a **linked mesh** that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by nothing at all and the attachment draws the geometry its `source` names; the rig spec has no home for them either, because `build` refuses geometry on a link by name. The detail lists the keys and the source. Until [#710](https://github.com/firejune/rigc/issues/710) the rebuild dropped them with no line at all, so an `ingest` that normalised somebody's file said nothing about it | nothing. The rebuild is the mesh the runtime was already drawing — and if those keys were the geometry you meant, take `source` off and author it as a mesh of its own. `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` is the same fact at the gate |
597
- | `ATTACHMENT_NAME` | `LOSS` | 0 | the attachment states a `name` and only **one** skin fills the placeholder, so rigc writes none — it composes `<skin>/<placeholder>` exactly where a placeholder is contested. 🚨 **The name is also the ATLAS REGION KEY**, and the detail says what became of it: `readAttachment` reads `name = getValue(map, "name", placeholder)` and then `path = getValue(map, "path", name)`, so `path` defaults to the **name** and not to the placeholder. A name that differs from the placeholder is therefore **kept as `path`** on an attachment that resolves a region — region, mesh, linked mesh — and the detail names the region. The three shapes that keep nothing say which they are: the source stated its own `path` (carried unchanged), the name **is** the placeholder (same region either way), or the type resolves no region at all (`boundingbox`, `clipping`, `path`). Until [#742](https://github.com/firejune/rigc/issues/742) nothing was written, so the rebuild asked the atlas for the placeholder and `A08_REGION_NAMES_MATCH_ATTACHMENTS` refused it | nothing about the art, which is carried. The **name** is what is gone, so this matters where something downstream looks that attachment up by the name the source gave it |
598
- | `ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | the attachment carries a `sequence` block — a numbered image series — which the rig spec cannot say | the rebuild draws the single region the attachment names; the frames have to be driven some other way |
599
- | `ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline other than `deform`, which is the only one the motion spec carries | transcribe it, or accept that the rebuild does not play it |
597
+ | `ATTACHMENT_NAME` | `LOSS` | 0 | the rebuilt attachment answers to a different name than the source's. **Two shapes, one comparison.** Where only **one** skin fills the placeholder, any stated `name` is lost: rigc writes none — it composes `<skin>/<placeholder>` exactly where a placeholder is contested. Where the placeholder **is** contested, rigc composes that name, and the source's name — what it states, or its placeholder where it states none (`SkeletonJson.js:526`) — is compared with it: equal is rigc's own emit and prints nothing, different is a rename and the detail states **both strings**. Until [#746](https://github.com/firejune/rigc/issues/746) the contested half printed nothing at all. A contested placeholder the **default** skin fills gets no line — the compiler composes nothing there and refuses the rebuild by name. 🚨 **The name is also the ATLAS REGION KEY**, and the detail says what became of it: `readAttachment` reads `name = getValue(map, "name", placeholder)` and then `path = getValue(map, "path", name)`, so `path` defaults to the **name** and not to the placeholder. A name that differs from the placeholder is therefore **kept as `path`** on an attachment that resolves a region — region, mesh, linked mesh — and the detail names the region. The three shapes that keep nothing say which they are: the source stated its own `path` (carried unchanged), the name **is** the placeholder (same region either way), or the type resolves no region at all (`boundingbox`, `clipping`, `path`). Until [#742](https://github.com/firejune/rigc/issues/742) nothing was written, so the rebuild asked the atlas for the placeholder and `A08_REGION_NAMES_MATCH_ATTACHMENTS` refused it | nothing about the art, which is carried. The **name** is what is gone, so this matters where something downstream looks that attachment up by the name the source gave it |
598
+ | `ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | a `sequence` block — a numbered image series — that the rig spec cannot say **as written**. Since [#729](https://github.com/firejune/rigc/issues/729) a well-formed block on a region, mesh or linked mesh is carried field for field, with no `image` on the loose route (the frames `<path><number>` are the art), and a `sequence` timeline with it; what is left here is a block the parser reads into a series other than the one written — no `count` (0 regions), a `setup` past the end (clamped), a fraction — or one on a `boundingbox`, `clipping` or `path`, where the parser never reads it. The detail quotes the block and says which | the rebuild draws the single region the attachment names. Fix the block in the source — a `count` is the usual one — and ingest again |
599
+ | `ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline that is neither `deform` nor `sequence`. Both are carried since [#729](https://github.com/firejune/rigc/issues/729), and `readAnimation` tests an attachment timeline for exactly those two names and ignores anything else (`SkeletonJson.js:1147-1201`) — so what reaches this line is a name outside the format, which no player plays either | fix the timeline's name in the source, or accept that the rebuild does not carry it |
600
600
  | `BONE_FIELD` | `BLOCK` | 1 | a bone field with no rig-spec field, so it is dropped. A 4.0/4.1 export spelling `transform` where 4.3 spells `inherit` lands here; so does a misspelling | check the name against AUTHORING §3 first — a typo and an unsupported field read exactly the same |
601
- | `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,20 +613,22 @@ 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
626
- CONTESTED too, and there is no finding there at all.** `ATTACHMENT_NAME` is silent
627
- exactly where the name is re-derived — rigc composes `<skin>/<placeholder>` for a
628
- contested placeholder, so nothing is lost about the name — and until
629
- [#742](https://github.com/firejune/rigc/issues/742) the region went with it anyway:
627
+ CONTESTED too, whether or not a line is printed.** On a contested placeholder rigc
628
+ composes `<skin>/<placeholder>`, so `ATTACHMENT_NAME` fires there only where the
629
+ source's name differs from that composed one ([#746](https://github.com/firejune/rigc/issues/746);
630
+ `IG68`–`IG70` hold both directions) — and until
631
+ [#742](https://github.com/firejune/rigc/issues/742) the region went with the name anyway:
630
632
  `compile` pins a composed name's `path` at the **placeholder**, so two skins naming two
631
633
  regions rebuilt onto **one**, neither of them the art the source drew, with no line
632
634
  printed. `ingest` now writes the source's region as `path` on both, and `IG53` in
@@ -624,14 +624,14 @@ with the member's own `skin: true` — either half alone is refused, because `Sk
624
624
 
625
625
  | Part-1 type | rigc | Detail |
626
626
  | --- | --- | --- |
627
- | `region` | 🟡 | emits `width`, `height` (always, from PNG measurement — the fix for case 6c), `x`, `y` (only when non-zero), `rotation` (only when non-zero, cancelling the bone's world rotation), `scaleX`/`scaleY` (`compile.ts:1238-1239`), `color`, and `path` — taken from the rig spec, else derived from the image basename and omitted when that basename *is* the attachment name (the region name == attachment name == PNG basename convention **A08** + **A27** enforce). ❌ `sequence` |
628
- | `mesh` | 🟡 | emits `type`, `uvs`, `triangles`, `vertices` (**weighted encoding only**), `hull`, `width`, `height`, `edges` (`compile.ts:1343`, and part 4's rung-6 entry measures it byte-identical to the reference), `path`, `color`. ❌ `sequence`. Unweighted meshes are 🚫 **A20_MESH_WEIGHTS_COHERENT** (`validate.ts:430-433`) |
629
- | `linkedmesh` | 🟡 | emits `type`, `source`, its own `path`, `width`, `height` and `color`, and `slot`/`skin`/`timelines` **only where they differ from the parser's defaults** (this attachment's slot, the default skin, true). ❌ `sequence`. Geometry keys on a link are refused by name — the parser returns before `readVertices`, so it reads none of them. A chain (a `source` that is itself a link) and a link to itself are refused: they resolve in file order and load `worldVerticesLength` 0 in one of the two orders, silently. `A21`/`A28` leave a link out (its rim and rows are its source's), `A04`/`A20`/`A22` read it as the mesh it resolves to, `A13` counts it as a mesh slot of its own ([#691](https://github.com/firejune/rigc/issues/691)) |
627
+ | `region` | 🟡 | emits `width`, `height` (always, from PNG measurement — the fix for case 6c), `x`, `y` (only when non-zero), `rotation` (only when non-zero, cancelling the bone's world rotation), `scaleX`/`scaleY` (`compile.ts:1238-1239`), `color`, and `path` — taken from the rig spec, else derived from the image basename and omitted when that basename *is* the attachment name (the region name == attachment name == PNG basename convention **A08** + **A27** enforce). ✅ `sequence` since [#729](https://github.com/firejune/rigc/issues/729) — see the `sequence` block row |
628
+ | `mesh` | 🟡 | emits `type`, `uvs`, `triangles`, `vertices` (**weighted encoding only**), `hull`, `width`, `height`, `edges` (`compile.ts:1343`, and part 4's rung-6 entry measures it byte-identical to the reference), `path`, `color`. ✅ `sequence` since [#729](https://github.com/firejune/rigc/issues/729) — see the `sequence` block row. Unweighted meshes are 🚫 **A20_MESH_WEIGHTS_COHERENT** (`validate.ts:430-433`) |
629
+ | `linkedmesh` | 🟡 | emits `type`, `source`, its own `path`, `width`, `height` and `color`, and `slot`/`skin`/`timelines` **only where they differ from the parser's defaults** (this attachment's slot, the default skin, true). ✅ `sequence` since [#729](https://github.com/firejune/rigc/issues/729) — see the `sequence` block row. Geometry keys on a link are refused by name — the parser returns before `readVertices`, so it reads none of them. A chain (a `source` that is itself a link) and a link to itself are refused: they resolve in file order and load `worldVerticesLength` 0 in one of the two orders, silently. `A21`/`A28` leave a link out (its rim and rows are its source's), `A04`/`A20`/`A22` read it as the mesh it resolves to, `A13` counts it as a mesh slot of its own ([#691](https://github.com/firejune/rigc/issues/691)) |
630
630
  | `boundingbox` | ✅ | `vertexCount` (required and cross-checked), `vertices` **or** by-name `weights`, `color`. **A33_VERTEX_ATTACHMENT_GEOMETRY** |
631
631
  | `path` | ✅ | `vertexCount` (required, and checked as a multiple of 3 — the parser's own `vertexCount / 3` takes a fractional size in silence), `vertices` **or** by-name `weights`, `closed`, `constantSpeed`, `color`, and a **measured** `lengths`: the cumulative setup length at the end of each curve, taken through each influence's own bone and measured as `PathConstraint` measures it — its own four-sample forward difference, which is what the editor writes too and is about 0.5 % below the arc (AUTHORING §10.6) — refused if authored. **A33_VERTEX_ATTACHMENT_GEOMETRY** re-checks the structure and the array's monotonicity. A `deform` timeline may key one: the array is the control points, `A39` SKIPs on it (no triangles), and `lengths` stays the setup measurement because the format has nowhere to put a per-key one — which only a `constantSpeed: false` traversal reads (AUTHORING §4.11) |
632
632
  | `point` | ❌ | deliberately deferred — never appears in the corpus (part 3-1) |
633
633
  | `clipping` | ✅ under `--profile spine` · 🚫 under `spine-html` | `end` (refused when it names no slot), `convex`, `inverse`, `vertexCount`, geometry, `color`. **A33**, and **A11_NO_CLIPPING_ATTACHMENTS** is the renderer-profile refusal |
634
- | `sequence` block | ❌ | |
634
+ | `sequence` block | ✅ | on a region, a mesh or a linked mesh: `count` (required — the parser's 0 loads no region), `start`, `digits`, `setup`, emitted as stated. The frames are the regions `<path><start + i>` zero-padded to `digits`, each atlased by name — the loose route's PNG of that name, or `--atlas-in`'s region — and a missing frame is refused with its number and the name looked for. A `setup` past the end (clamped), a fraction, an `image` or a `generator` beside it, and a `sequence` on any other kind are refused by name. **A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES** ([#729](https://github.com/firejune/rigc/issues/729)) |
635
635
 
636
636
  Across all five types, rigc emits the attachment's own **`name`** in exactly one case: a placeholder
637
637
  that more than one skin fills (`compile.ts`'s `composeSkinAttachmentName` and `nameSkinAttachment`).
@@ -682,7 +682,7 @@ editor-made meshes — those arrive as authored `uvs`/`triangles`/`weights`.
682
682
  | `ik`, `transform` | ✅ — one unnamed timeline per constraint, keyed by the motion spec's `ik` / `transform` arrays |
683
683
  | `path.position/spacing/mix`, `slider.time/mix` | ✅ (`PATH_TRACKS` / `SLIDER_TRACKS`) — authored as `tracks` entries naming `path` or `slider`, the same shape as `physics`, since both groups put named timelines under a constraint name. `path.mix` is one timeline of three channels |
684
684
  | `attachments.<skin>.…deform` | ❌ (validator knows it: 1 channel, `validate.ts:104-107`) |
685
- | `attachments.…sequence` | ❌ |
685
+ | `attachments.…sequence` | ✅ — the motion spec's per-animation `sequence` array of `{ skin?, slot, attachment, keys: [{ t, mode?, index?, delay? }] }`, each field emitted only where stated. A mode outside the seven (read as `hold`), a fractional or out-of-range `index` (truncated, clamped), an advancing mode at an effective delay of 0 (never advances), an attachment with no `sequence` block (every mode shows its one region) and a linked mesh that plays its source's timelines (the key is applied to nothing) are refused by name. **A46** poses every key at mid-frame samples against the file's own statement ([#729](https://github.com/firejune/rigc/issues/729)) |
686
686
  | `drawOrder` | ✅ — the motion spec's per-animation `drawOrder` array of `{ t, offsets: [{ slot, offset }] }` (`compile.ts:823`, `:850`, `compileDrawOrder` `:1918-1970`). A key with no `offsets`, and a key with an empty one, both emit the parser's reset-to-setup encoding, so two spellings cannot make two files. An unknown slot, a slot offset twice in one key, a non-whole offset and a landing outside the emitted slots are each refused by name, and **A31_DRAW_ORDER_OFFSETS_RESOLVE** re-checks the emitted file |
687
687
  | `drawOrderFolder` | ❌ — editor bookkeeping, 4.3-only |
688
688
  | `events` (+ the `root.events` block) | ✅ — the rig spec declares the names (`rig.ts` `RigEvent`), the motion spec's per-animation `events` array fires them, and **A32_EVENT_KEYS_RESOLVE** checks the three ways the timeline goes wrong quietly. `int`/`float`/`string` overrides and `audio`/`volume`/`balance` all round-trip |
@@ -738,6 +738,7 @@ This is the split Part 4(c) needs. **Spine-validity** = the file is wrong for an
738
738
  | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | validity | a slot `dark` the parser drops or reads as NaN, an `rgba2` or `rgb2` timeline on a slot with no dark colour to pose, or a key whose posed light/dark is not what it states |
739
739
  | `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` | validity | a linked mesh, in either spelling, that also states `uvs`, `triangles`, `vertices`, `hull` or `edges` — keys the `source` branch returns before reading, so the file says one mesh and every runtime draws its source's |
740
740
  | `A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | validity | an `rgb` or `alpha` timeline beside another colour timeline of the slot that poses the same channel — the later in the file overwrites the other at every time — or a key of one the pose does not reproduce |
741
+ | `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | validity | a numbered series that loads as something other than what the file states — a `sequence` block with no `count` (0 regions) or a `setup` past the end (clamped), or a `sequence` key whose `mode` is outside the seven (read as `hold`), whose `index` is fractional or past the end, whose advancing mode runs at an effective delay of 0, or that steps an attachment with no block — and then the pose: every key sampled mid-frame, the region shown held to the frame the file's statement gives ([#729](https://github.com/firejune/rigc/issues/729)) |
741
742
  | `A13_MESH_BUDGET` | **renderer-profile** | >4 mesh slots, >80 triangles per mesh |
742
743
  | `A14_NO_FULL_FRAME_MESH` | **renderer-profile** | a mesh spanning the whole stage |
743
744
  | `A19_OVERLAY_PNGS_HAVE_ALPHA` | **renderer-profile** | an overlay page that can never be transparent — no alpha channel and no `tRNS` chunk |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
5
5
  "type": "module",
6
6
  "bin": {
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
+ }