spine-rigc 0.36.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -590,8 +590,6 @@ the file are **findings** with codes rather than plausible values: a construct t
590
590
  spec format cannot hold (`point`, a `sequence` block, an unknown field
591
591
  on a bone, slot or constraint) is a blocker, the command exits non-zero, and both
592
592
  specs are still written — a spec plus a list of what is missing from it beats no spec.
593
- One thing it drops on purpose and says so: a path attachment's `lengths`, which is
594
- `PathConstraint`'s own measurement and which rigc re-measures.
595
593
 
596
594
  ⚠️ **Two values are not in a skeleton at all.**
597
595
 
@@ -633,9 +631,12 @@ bun tools/editor_roundtrip.ts --build build/ --editor /Applications/Spine.app/Co
633
631
 
634
632
  It prints the import and export exit codes, the validator's verdict on the
635
633
  export, every `diff` measure that moved, `check`'s mean MAE and worst drift per
636
- animation **for each skin the build declares** — one render-and-check block per
637
- skin, with a per-skin roll-up under them, because a rig's contested art lives in
638
- its named skins and a single un-skinned check draws none of it — and a
634
+ animation **for each skin the build and the export both declare** — one
635
+ render-and-check block per skin, with a per-skin roll-up under them, because a
636
+ rig's contested art lives in its named skins and a single un-skinned check draws
637
+ none of it; a skin only one side declares is a FAIL naming it as **lost** (or
638
+ **added**) **by the export**, with `diff`'s `attachments.skins` beside it, and is
639
+ rendered on neither side ([#801](https://github.com/firejune/rigc/issues/801)) — and a
639
640
  field-by-field list of what the editor rewrote. Every step quotes what its child
640
641
  said when that child did not do what it was for, the renderers included; a skin
641
642
  **neither** side can draw — a hit-box rig, say — is a **SKIP** naming that, not a
@@ -685,7 +686,7 @@ letting `A17` blame the editor for the harness's own doing.
685
686
  | 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
686
687
  | 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 49 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
687
688
  | 📋 [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
688
- | 🗺️ [ROADMAP.md](https://github.com/firejune/rigc/blob/main/ROADMAP.md) | where this is going, and where it has been. What 1.0 has to mean before the number is claimed — conditions rather than a feature list, because direction here comes from what users hit |
689
+ | 🗺️ [ROADMAP.md](https://github.com/firejune/rigc/blob/main/ROADMAP.md) | where this is going, and where it has been. What 1.0 had to mean before the number was claimed, and what it was claimed on — conditions rather than a feature list, because direction here comes from what users hit |
689
690
  | 📐 [CLAUDE.md](https://github.com/firejune/rigc/blob/main/CLAUDE.md) | **the doctrine** — why the validator's messages are the product, why nothing reaches disk before green, why no number is ever invented, and what a change has to keep. [CONTRIBUTING.md](https://github.com/firejune/rigc/blob/main/CONTRIBUTING.md) calls it worth ten minutes before a first patch. Repository material |
690
691
 
691
692
  ## Why you can trust the output
package/cli.ts CHANGED
@@ -2836,6 +2836,17 @@ function cmdExplain(flags: Record<string, string>): void {
2836
2836
  // relying on. Reading it by name means a change to the skins ORDER cannot turn
2837
2837
  // this line into a report about some other skin.
2838
2838
  const defaultSkin = result.skeleton.skins.find((skin) => skin.name === 'default');
2839
+ // ...and a skeleton may declare none (issue #801), in which case every
2840
+ // `attachments=[]` below is true and says nothing — so the line above the
2841
+ // column says where the placeholders are instead of letting it read as
2842
+ // "this rig has no art".
2843
+ if (defaultSkin === undefined) {
2844
+ const names = result.skeleton.skins.map((skin) => JSON.stringify(skin.name));
2845
+ console.log(
2846
+ ` (this skeleton declares no default skin, so the attachments column lists none; its placeholders are in ` +
2847
+ `${names.length === 0 ? 'no skin at all' : `the named skin(s) ${names.join(', ')}`})`,
2848
+ );
2849
+ }
2839
2850
  for (const s of result.skeleton.slots) {
2840
2851
  const atts = Object.keys(defaultSkin?.attachments[s.name] ?? {});
2841
2852
  console.log(
@@ -3039,7 +3050,10 @@ function cmdExplain(flags: Record<string, string>): void {
3039
3050
  const curve = attachments.find((att) => (att as { type?: string }).type === 'path') as
3040
3051
  | { lengths?: number[]; closed?: boolean; constantSpeed?: boolean }
3041
3052
  | undefined;
3042
- const lengths = curve?.lengths ?? [];
3053
+ // `vertexCount / 3` entries on both shapes since issue #804, so an open
3054
+ // path's curves are one fewer than its entries and its length is the last
3055
+ // CURVE's entry — the trailing one is the wrap-around curve nothing reads.
3056
+ const lengths = (curve?.lengths ?? []).slice(0, curve?.closed ? undefined : -1);
3043
3057
  console.log(
3044
3058
  ` ${c.name.padEnd(12)} slot=${slot.padEnd(12)} bones=[${(c.bones as string[]).join(', ')}] ` +
3045
3059
  `position=${c.position ?? 0} ${String(c.positionMode ?? 'percent')}/${String(c.spacingMode ?? 'length')}/${String(c.rotateMode ?? 'tangent')}`,
package/docs/AUTHORING.md CHANGED
@@ -505,6 +505,18 @@ because the text passes through by line; regions the rig does not use stay in th
505
505
  file, because a real pack is shared between cuts and an importer that quietly
506
506
  dropped half of one would make `--out` disagree with the pack it was built from.
507
507
 
508
+ 🔸 **The one thing not passed through is the pack's blank lines**
509
+ ([#803](https://github.com/firejune/rigc/issues/803)). They are written in the
510
+ shape rigc writes its own packs in: none before the first page, exactly one between
511
+ two page blocks, one trailing newline. Every non-blank line keeps its bytes and its
512
+ order, so a pack already in that shape — every editor export in the example corpus,
513
+ and every pack `--pack` writes — is emitted byte for byte as before. What this
514
+ changes is a pack from a 3.8-era packer, which begins with a blank line: the
515
+ runtime reads a run of blank lines before the first page as nothing, so the pack
516
+ loads, and until #803 `build` re-emitted the blank and refused its own emission as
517
+ `A07_ATLAS_TEXT_SHAPE: line 1: consecutive blank lines`. A run of blank lines
518
+ between two page blocks reads as one to the runtime, and is written as one.
519
+
508
520
  Five things are refused rather than warned about, because each of them otherwise
509
521
  **loads clean and draws wrong** — or, the last, cannot be read back at all:
510
522
 
@@ -607,7 +619,6 @@ bun cli.ts ingest hero.json --out specs/ --images parts/
607
619
  # .. art loose
608
620
  # .. images ../parts/ (the rig spec's own, from /abs/path/specs)
609
621
  # JUDGE DURATION: animation "idle" — skeleton JSON carries no duration; the largest key time (2.667) is used …
610
- # LOSS PATH_LENGTHS: skin "default" slot "track" attachment "track" — the source states `lengths`; rigc RE-MEASURES it …
611
622
  # rigc: wrote /abs/path/specs/rig.json
612
623
  # rigc: wrote /abs/path/specs/motion.json
613
624
  # rigc: wrote /abs/path/specs/findings.json
@@ -698,7 +709,7 @@ the first:
698
709
  | --- | --- |
699
710
  | `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read — `point`, a `sequence` block the parser would read as some other series, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
700
711
  | `JUDGE` | the skeleton cannot answer and somebody has to: the stage, and each animation's duration |
701
- | `LOSS` | the skeleton's spelling and rigc's differ, on purpose, and the line says how. A path attachment's `lengths` is the one that matters — it is `PathConstraint`'s own four-sample measurement rather than an arc length (#560), so a transcribed one would freeze whatever produced the source. The header ones are cheaper: `HEADER_BOOKKEEPING` for a field the spec has no home for, `HEADER_REDERIVED` for the version string, `HEADER_ORIGIN` for an origin the source left to the format and the rebuild writes out (#622) |
712
+ | `LOSS` | the skeleton's spelling and rigc's differ, on purpose, and the line says how: `HEADER_BOOKKEEPING` for a field the spec has no home for, `HEADER_REDERIVED` for the version string, `HEADER_ORIGIN` for an origin the source left to the format and the rebuild writes out (#622) |
702
713
 
703
714
  ⛔ **It reads one generation of the format, and a file from another one ends loud.**
704
715
  Spine data is locked to the generation that exported it and a mismatch does not
@@ -1281,8 +1292,8 @@ setup pose as `null`.
1281
1292
 
1282
1293
  ### 3.4 `skins` — placeholder → attachment maps
1283
1294
 
1284
- `skins` is `skinName → slotName → placeholderName → attachment`. Give at least
1285
- `default`; it becomes the skeleton's default skin. (No rung of the benchmark ladder
1295
+ `skins` is `skinName → slotName → placeholderName → attachment`. A skin called
1296
+ `default` becomes the skeleton's default skin. (No rung of the benchmark ladder
1286
1297
  uses a named skin — all twelve official example skeletons have exactly one skin,
1287
1298
  called `default`.)
1288
1299
 
@@ -1290,11 +1301,25 @@ A skin can also say which bones and constraints it **switches on**, and that nee
1290
1301
  one more level, so a skin entry has a second spelling — see §3.4.1. The short one
1291
1302
  above is unchanged and is what almost every rig wants.
1292
1303
 
1293
- 🔸 **`default` is a name, not a requirement.** A rig may put every attachment in
1294
- named skins and declare no `default` at all, which is what an editor export of a
1295
- multi-skin character gives back; rigc still emits a `default` skin, empty, because
1296
- it always does. An animation keying such a slot resolves its attachment names
1297
- against every skin there is — §4.4 states that rule and §5.1 the one refusal left.
1304
+ 🔸 **`default` is a name, not a requirement, and rigc writes one exactly when the
1305
+ spec has one** ([#801](https://github.com/firejune/rigc/issues/801)). A rig may put
1306
+ every attachment in named skins and declare no `default` at all, which is what an
1307
+ editor export of a multi-skin character gives back: the 4.3.26 editor took a build
1308
+ declaring `skins: [default, alt, base]` with `default` **empty** and exported
1309
+ `[alt, base]`. So the emitted file carries a `default` skin when `skins` has the
1310
+ key — `"default": {}` included, which is written back empty — or when a manifest
1311
+ part files its states under it, and **none otherwise**. spine-core loads both
1312
+ spellings alike (`SkeletonData.defaultSkin` is null or an empty skin, and neither
1313
+ fills a slot), and the binary format has no way to hold an empty default skin at
1314
+ all. Until #801 rigc wrote an empty `default` into every build whether the spec
1315
+ had one or not, so a rebuild of such an export read `attachments.skins 1/2`
1316
+ against it. Two things change for a rig with no default skin, and both are
1317
+ named: a linked mesh that states no `skin` looks for its source in the default
1318
+ skin and is refused (the error table in §5.1), and `rigc explain` says the
1319
+ skeleton declares no default skin above its slot list instead of printing
1320
+ `attachments=[]` on every slot. An animation keying such a slot resolves its
1321
+ attachment names against every skin there is — §4.4 states that rule and §5.1 the
1322
+ one refusal left.
1298
1323
 
1299
1324
  🔸 **A skin may fill no slot with anything that needs art, and that build is
1300
1325
  green.** The atlas is built out of what the skins reference, so a rig whose skins
@@ -1644,7 +1669,7 @@ other vertex attachment.
1644
1669
  | `name`, `vertexCount`, `vertices`, `weights`, `boneIndexing`, `color` | as on a bounding box — except that these vertices are knots **and** their handles, which the count rule below is about |
1645
1670
  | `closed` | default **false**. True joins the last knot back to the first |
1646
1671
  | `constantSpeed` | default **true** — note the direction. Leaving it out asks for the expensive-and-correct traversal, in which the runtime re-measures the path every frame and `lengths` is never read. `false` makes the runtime trust the emitted `lengths` instead: cheaper, exact only while the path holds its setup shape, and the reason a deformed path wants the default |
1647
- | `lengths` | 🚫 **refused by name.** rigc measures the setup length of each curve off the geometry and emits it, the way it measures a region's size off its PNG: `"lengths" is not authored — rigc measures the setup arc length of each curve…`. The field is declared only so the refusal can say that rather than report a misspelt key. What the numbers are — and why *arc length* is the wrong name for them — is §10.6 |
1672
+ | `lengths` | **stated or measured** ([#804](https://github.com/firejune/rigc/issues/804)). The cumulative length at the end of each curve, and exactly **`vertexCount / 3`** entries on an open path and a closed one alike — the parser's own allocation, which on an open path is one more than its curves: the last entry is the wrap-around curve's cumulative, which the editor writes and nothing reads. **Stated**, it is emitted as stated (float32, like every number in the file) — which is what `ingest` writes from an export, and the only way to carry the editor's number, because the editor measures the pose the first update gives the path constraint, *constraints applied*, and rigc does not pose. A stated array of the wrong count is refused with both counts, `"lengths" has N entry(ies) where the parser sizes M`, and a non-finite entry or one below its predecessor by index. **Left out**, rigc measures it off the geometry: weighted vertices through their bones' setup world transforms, scale, shear and `inherit` included, on the **unconstrained** setup pose — so a path whose bones a constraint moves at rest gets a different figure from the editor's. Only `constantSpeed: false` reads the field at all. What the numbers are — and why *arc length* is the wrong name for them — is §10.6 |
1648
1673
 
1649
1674
  🚨 **`vertexCount` counts knots AND handles, and it has to be a multiple of 3.**
1650
1675
  The parser hands `vertexCount << 1` to `readVertices` and then walks the result in
@@ -1664,7 +1689,8 @@ and `vertexCount is N and an open path needs at least 6`.
1664
1689
 
1665
1690
  Nine points are two curves. The outer handles at `x = -30` and `x = 210` are
1666
1691
  dropped, so the chain runs from `x = 0` to `x = 180` and rigc emits
1667
- `"lengths": [90, 180]` beside it — measured, not stated. That is the path
1692
+ `"lengths": [90, 180, 360]` beside it — measured, not stated, and the third entry
1693
+ is the wrap-around curve from `x = 180` back to `x = 0`, which nothing reads. That is the path
1668
1694
  §3.5.1's constraint example rides: with `position: 0.25` its `cart` bone poses at
1669
1695
  `worldX = 45.000000`.
1670
1696
 
@@ -2455,7 +2481,10 @@ skin alone and the art you just moved into `base`, `zulu` and `mike` draws
2455
2481
  nothing at all. `check` then compares blank against blank and reports a perfect
2456
2482
  `0.0000` — about the very placeholder this subsection is about. Pass
2457
2483
  `--skin <name>` to both, once per skin (**§9**); `tools/editor_roundtrip.ts`
2458
- loops over every skin the build declares for the same reason.
2484
+ loops over every skin the build and the export both declare for the same reason,
2485
+ and names a skin only one of them declares as **lost** (or **added**) **by the
2486
+ export**, quoting `diff`'s `attachments.skins`, without rendering it on either side
2487
+ ([#801](https://github.com/firejune/rigc/issues/801)).
2459
2488
 
2460
2489
  #### 3.4.3 `sequence` — a numbered image series on one attachment
2461
2490
 
@@ -3561,7 +3590,7 @@ a delta from the constraint's own setting.
3561
3590
  somebody else's file means **0**.
3562
3591
  - ⚠️ `mass` is the one whose keyed number is not what the runtime stores. The
3563
3592
  key states a mass and the pose holds `1 / mass`, so a `mass` key of `0` is an
3564
- infinite inverse mass — the constraint stops moving.
3593
+ infinite inverse mass — every velocity and offset is NaN from the first step.
3565
3594
  - **Four of the seven are bounded, and a key outside its bound is a compile
3566
3595
  error** ([#610](https://github.com/firejune/rigc/issues/610)). `mass` must be
3567
3596
  `> 0`, `damping` must be inside the closed `[0, 1]`, and `mix` and `strength`
@@ -3569,6 +3598,40 @@ a delta from the constraint's own setting.
3569
3598
  a file rigc did not write, naming the animation, the constraint, the key time
3570
3599
  and the value — so the compiler is where a spec you wrote is refused, and the
3571
3600
  assertion is where an import is.
3601
+ - 🔎 **Each way out of a bound has a basis, and the refusal says which**
3602
+ ([#798](https://github.com/firejune/rigc/issues/798)). **arithmetic** means the
3603
+ runtime cannot compute the value — an expression in the integrator is
3604
+ non-finite at it — and the sentence names the expression and its lines;
3605
+ **behavioural** means the runtime runs it, finitely, and the rig runs wrongly,
3606
+ so refusing it is rigc's call and the sentence says so and says what the value
3607
+ does. Two of the eight ways out are arithmetic. The table is the
3608
+ `PHYSICS_POSE_RULES` rows' own `basis`, verbatim, and `CUR100` holds it to them;
3609
+ every basis is re-measured through spine-core by `T113` on every run.
3610
+
3611
+ | field | at rest | on a key | basis |
3612
+ | --- | --- | --- | --- |
3613
+ | `mix` | `> 0` | `>= 0` | behavioural: below 0 the jiggle is applied inverted, on `x` and `y` exactly the offset a positive mix of the same size applies, mirrored (`PhysicsConstraint.js:172,174`), and `PhysicsConstraintPose` documents mix as "a percentage (0+)" where a transform constraint documents its own as "unbounded"; behavioural: at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted |
3614
+ | `mass` | `> 0` | `> 0` | arithmetic: at 0 `massInverse = 1 / mass` is Infinity and `m = t * massInverse` multiplies every velocity update, so the first step takes every velocity and offset to NaN (`SkeletonJson.js:309, Animation.js:2140, PhysicsConstraint.js:149,156,211`); behavioural: below 0 `massInverse` is a finite negative, which flips the sign of every force the velocity update applies, so the restoring force pushes the offset away and it grows with every step |
3615
+ | `strength` | `> 0` | `>= 0` | behavioural: below 0 the restoring term is ADDED to the offset instead of taken out of it, so the offset is pushed away and grows with every step; behavioural: nothing pulls it back |
3616
+ | `damping` | `inside [0, 1]` | `inside [0, 1]` | behavioural: above 1 every velocity grows on every step and the offset diverges; arithmetic: at any fps where `60 / fps` is not whole it is a negative number raised to a fractional power, which is NaN (`PhysicsConstraint.js:148,210`) |
3617
+
3618
+ ⚠️ The line between the two is **non-finite within the walk that measures
3619
+ it**, not "non-finite eventually": [measured] on the generated physics
3620
+ fixture at 60 fps, a setup `damping` of 2 is finite for 1,064 steps before its
3621
+ velocity overflows, and a `mass` of −1 runs the offset to 2.7e6 in 120 steps
3622
+ finite on every one. Every run-away overflows at some horizon, which is why
3623
+ `damping` above 1 is behavioural although it diverges.
3624
+ - ⚖️ **A negative `mix` is refused on a physics constraint and accepted on a
3625
+ transform constraint, and neither is the runtime's arithmetic**
3626
+ ([#798](https://github.com/firejune/rigc/issues/798)). [measured] both are a
3627
+ finite, mirrored scale: a physics constraint resting at `mix` −0.5 moves its
3628
+ bone by exactly the opposite of +0.5 on every step, and a transform constraint
3629
+ at `mixRotate` −0.5 rotates its bone by the negative of +0.5's. What differs is
3630
+ the range the runtime documents for each field — `PhysicsConstraintPose.mix` is
3631
+ "a percentage (0+)", `TransformConstraintPose.mixRotate` "a percentage
3632
+ (unbounded)" — and each rule follows its own. `T114` holds the mirror and both
3633
+ documented ranges against the runtime; `T105` holds the transform side's
3634
+ acceptance.
3572
3635
  - ⚠️ **`damping`'s bound depends on the constraint's `fps`, and a rig played at
3573
3636
  60 fps hides it** ([#748](https://github.com/firejune/rigc/issues/748)). The
3574
3637
  decay is `damping ** (60 * step)` with `step` = `1 / fps`, so the exponent is
@@ -5442,6 +5505,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5442
5505
  | `a linked mesh states "uvs", "triangles", …, and a linked mesh has no geometry of its own` | §3.4 — remove them, or remove `source` and author this as a mesh. The parser returns before `readVertices`, so those keys are read by nothing at all |
5443
5506
  | `"source" is "X", and skin "S" … slot "L" … holds 2: "a", "b"` | §3.4 — `source` is the PLACEHOLDER the source is filed under, not its `name`. A clause after the skin and after the slot says whether each was stated or taken from the parser's default — **the default skin** and **this attachment's own slot**, which is the pair that surprises |
5444
5507
  | `"slot" is "X", which the rig does not declare as a slot` / `"skin" is "X", … the rig declares no such skin` | §3.4 — a link resolves both by name. Left to the round trip these are the runtime's `Source mesh slot not found` and `Skin not found`, which name neither the attachment nor where it looked |
5508
+ | `no "skin" is stated, so the parser looks for the source "X" in the default skin — and this rig declares no default skin` | §3.4 — state the `skin` the source is filed under. A link with no `skin` resolves through `SkeletonData.defaultSkin`, which is null in a file with no default skin, and the runtime reads `getAttachment` off it with a `TypeError` naming nothing. Before [#801](https://github.com/firejune/rigc/issues/801) every build carried an empty `default`, so the same link failed at the source instead |
5445
5509
  | `"source" is "X", which is itself a linked mesh, and a chain of them is refused` | §3.4 — point `source` at the mesh. A chain resolves in file order and loads nothing at all in one of the two orders, silently |
5446
5510
  | `"source" is "X", which is a "region" attachment and not a mesh` | §3.4 — a link takes another MESH's geometry; off any other type the runtime reads `undefined` and says nothing |
5447
5511
  | `… is a boundingbox and states a "sequence". A sequence is a numbered series of atlas regions, and only the 3 kinds that draw a region carry one — region, mesh, linkedmesh …` | §3.4.3 — put the series on a region or a mesh, or remove it |
@@ -5478,7 +5542,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5478
5542
  | `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 |
5479
5543
  | `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 |
5480
5544
  | `animation "A" bone "X" translatex: key value must be an array of 1 number(s)` | the value shape must match the property (§4.4) |
5481
- | `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]`, both ends included ([#794](https://github.com/firejune/rigc/issues/794)), `mix` and `strength` are `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere ([#610](https://github.com/firejune/rigc/issues/610)). ⚠️ Those are the bounds a **key** is held to. A setup `strength` of `0` is refused too, but by `A23` rather than here, and with its own sentence — `physics "C" has strength 0; nothing pulls it back` ([#727](https://github.com/firejune/rigc/issues/727)); a setup `strength` below 0 says the offset is pushed away and grows, the same arm this message quotes for a negative key ([#748](https://github.com/firejune/rigc/issues/748)). A `damping` key's sentence names the exponent `60 * step` and why a negative is NaN at any `fps` where `60 / fps` is not whole (§4.4) |
5545
+ | `animation "A" physics constraint "C" mass key at t=… is 0 (massInverse Infinity); must be > 0 — …` | §4.4 — a keyed physics value outside its row's bound. The message names the bound and then its basis ([#798](https://github.com/firejune/rigc/issues/798)): where the runtime's arithmetic fails, the expression and its lines (`mass` at 0 is the one here, and `damping` below 0); where the value runs and runs wrongly, that refusing it is rigc's call and what the value does. The bounds: `mass` is `> 0`, `damping` is inside `[0, 1]`, both ends included ([#794](https://github.com/firejune/rigc/issues/794)), `mix` and `strength` are `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere ([#610](https://github.com/firejune/rigc/issues/610)). ⚠️ Those are the bounds a **key** is held to. A setup `strength` of `0` is refused too, but by `A23` rather than here, and with its own sentence — `physics "C" has strength 0; nothing pulls it back` ([#727](https://github.com/firejune/rigc/issues/727)); a setup `strength` below 0 says the offset is pushed away and grows, the same arm this message quotes for a negative key ([#748](https://github.com/firejune/rigc/issues/748)). A `damping` key's sentence names the exponent `60 * step` and why a negative is NaN at any `fps` where `60 / fps` is not whole (§4.4) |
5482
5546
  | `a key carries both a named easing and a raw curve; pick one` | R6 |
5483
5547
  | `last key carries an easing but has nothing to ease to` | drop `ease`/`curve` from the final key |
5484
5548
  | `key times must strictly increase (at t=…)` | including after `lag` and `stagger` |
@@ -5522,7 +5586,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5522
5586
  | `deform … (t=…): transform <kind> states …, and every one of this attachment's N vertices evaluates to an offset of 0` | §4.11.1 — the parameters state a deformation and the geometry sampled it to nothing; the message names the measured cause. A key that means the setup pose states the identity in its parameters, or carries no run |
5523
5587
  | `vertexCount is N, which is not a multiple of 3` | §3.4 — a path's vertices are knots and handles read in groups of three: `3(K + 1)` open, `3K` closed |
5524
5588
  | `vertexCount is N and an open path needs at least 6` | §3.4 — an open path drops its first and last point, so it needs six for one curve |
5525
- | `"lengths" is not authored — rigc measures the setup arc length of each curve` | §3.4 — delete the array; it is a measurement of the vertices above it |
5589
+ | `"lengths" has N entry(ies) where the parser sizes M — vertexCount / 3 on …` | §3.4 — `vertexCount / 3` entries on either shape; give the array the source states, or leave the field out and rigc measures it |
5590
+ | `"lengths"[i] is V, below …` / `"lengths"[i] is "…"; every entry is a finite cumulative length` | §3.4 — the array is cumulative, so each entry is at least the one before it |
5526
5591
  | `rig constraint "X": slot "Y" has no path attachment in any skin` | §3.5.1 — give that slot a `"type": "path"` attachment, or aim the constraint at the slot that has one |
5527
5592
  | `rig constraint "X": rotateMode is "CHAINSCALE"; known: Tangent, Chain, ChainScale` | §3.5.1 — only the first letter's case is free; anything else resolves to `undefined` in the parser |
5528
5593
  | `rig constraint "X": applies animation "Y", which the motion spec does not declare (it declares: …)` | §3.5.2 — fix the slider's `animation`, or add it to the motion spec |
@@ -5651,7 +5716,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5651
5716
  | `A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
5652
5717
  | `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel**. **SKIP** when no animation carries a timeline at all ([#580](https://github.com/firejune/rigc/issues/580)). Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one |
5653
5718
  | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk — the message names the ratio it measured on **each** axis and the header that states the same art truthfully, and §0.2 has the whole of it ([#715](https://github.com/firejune/rigc/issues/715)): a runtime maps a region as a fraction of the DECLARED size and never reads the texture's own, so such a page draws, and what it breaks is every reader that addresses the page in texels. A uniform ratio is a `size:`/`scale:` pair away from honest and the message prints it; two ratios are not, because `scale:` carries one number, and the message says so rather than offering a header that would not work. **Or** a region's rectangle is not inside the page it names — rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles ([#694](https://github.com/firejune/rigc/issues/694)): a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (§0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels — a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling** ([#266](https://github.com/firejune/rigc/issues/266)), and what that message names is the pair that shares texels. **Or** the page file is not a PNG at all, and then nothing about its size is measured: the message carries the page's path and the size the atlas declares, and names what the file is by its first bytes — one of WebP, JPEG, GIF, KTX, KTX2 by its signature, or no image format rigc recognises, the bytes in hex either way — or calls it **truncated** when it begins as a PNG and runs out before its `IEND` (§0.2, [#732](https://github.com/firejune/rigc/issues/732)). That clause is validity too: nothing in rigc can read such a page back. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
5654
- | `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` |
5719
+ | `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. Two sentences are about blank lines and name two different repairs ([#803](https://github.com/firejune/rigc/issues/803)): `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: consecutive blank lines` is two blank lines side by side after the first page — delete one. rigc writes neither: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so either sentence means a file rigc did not write, handed to `validate`. ⚠️ 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` |
5655
5720
  | `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)) |
5656
5721
  | `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 |
5657
5722
  | `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 |
@@ -5667,7 +5732,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5667
5732
  | `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not ([#684](https://github.com/firejune/rigc/issues/684)). Fix the rig spec's `controls`/`chain`, or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
5668
5733
  | `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1 |
5669
5734
  | `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
5670
- | `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength` at or below 0 — `0` says `nothing pulls it back`, below 0 says the offset `is pushed away and grows with every step`, both read off the row's `outside` arms, which the key's refusal quotes too ([#748](https://github.com/firejune/rigc/issues/748)) — or has `damping` outside `[0, 1]`, which says `physics "C" has damping 1.5; must be inside [0, 1] — above 1 every velocity grows on every step, and below 0 it is NaN at any fps where 60 / fps is not whole`, the bound read off the row the key's refusal reads; `1` and `0` are inside, since both are finite at every rate ([#794](https://github.com/firejune/rigc/issues/794)) — **at rest, and on every physics timeline key** ([#610](https://github.com/firejune/rigc/issues/610), [#743](https://github.com/firejune/rigc/issues/743)). The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps ([#727](https://github.com/firejune/rigc/issues/727)). As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `physics "C" has mix 0 and none of the 3 animations keys its mix above 0; it is muted — rest it above 0, or key its mix above 0 in an animation`. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, counts every sample of a Bezier between two keys as a value the timeline poses — so two keys of 0 joined by a curve lifted above 0 are a rescue, measured to move the bone — and takes an animation a slider applies like any other. It is the one reading `A36` and `A37` use as well ([#752](https://github.com/firejune/rigc/issues/752)). `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md), [#731](https://github.com/firejune/rigc/issues/731)), so this sentence is met on a file, never on a decompiled rebuild. **SKIP** when the skeleton declares no physics constraint ([#580](https://github.com/firejune/rigc/issues/580)) — the same sentence `A36` and `A37` have always printed for their own constraint types |
5735
+ | `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength` at or below 0 — `0` says `nothing pulls it back`, below 0 says the offset `is pushed away and grows with every step`, both read off the row's `outside` arms, which the key's refusal quotes too ([#748](https://github.com/firejune/rigc/issues/748)) — or has `damping` outside `[0, 1]`, which says `` physics "C" has damping 1.5; must be inside [0, 1] — the per-step decay is `damping ** (60 * step)`, and the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: above 1 every velocity grows on every step and the offset diverges ``, the bound read off the row the key's refusal reads; `1` and `0` are inside, since both are finite at every rate ([#794](https://github.com/firejune/rigc/issues/794)) — **at rest, and on every physics timeline key** ([#610](https://github.com/firejune/rigc/issues/610), [#743](https://github.com/firejune/rigc/issues/743)). The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps ([#727](https://github.com/firejune/rigc/issues/727)). As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `` physics "C" has mix 0 and none of the 3 animations keys its mix above 0; the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted — rest it above 0, or key its mix above 0 in an animation ``. ⚠️ Every reason A23 prints for a setup value is that value's row `basis` arm (§4.4's table, [#798](https://github.com/firejune/rigc/issues/798)) — an arithmetic arm names its expression (`physics "C" has massInverse Infinity; mass must be > 0 — at 0 …`), a behavioural one says it is rigc's call and what the value does. A setup `mix` **below** 0 is told the jiggle is applied inverted, not that it is muted: until #798 it read "it is muted", of a constraint measured moving its bone by exactly the opposite of a positive mix (`T115`). The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, counts every sample of a Bezier between two keys as a value the timeline poses — so two keys of 0 joined by a curve lifted above 0 are a rescue, measured to move the bone — and takes an animation a slider applies like any other. It is the one reading `A36` and `A37` use as well ([#752](https://github.com/firejune/rigc/issues/752)). `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md), [#731](https://github.com/firejune/rigc/issues/731)), so this sentence is met on a file, never on a decompiled rebuild. **SKIP** when the skeleton declares no physics constraint ([#580](https://github.com/firejune/rigc/issues/580)) — the same sentence `A36` and `A37` have always printed for their own constraint types |
5671
5736
  | `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it ([#580](https://github.com/firejune/rigc/issues/580)) |
5672
5737
  | `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
5673
5738
  | `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table — a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (§3.3). **SKIP** when the rig declares no canonical slot order. ⚠️ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass ([#580](https://github.com/firejune/rigc/issues/580)): the completeness clause reads it as every declared slot lost and names them, which is the maximal case of what [#575](https://github.com/firejune/rigc/issues/575) filed |
@@ -5728,7 +5793,6 @@ are `CompileError`s, and they name what the format actually defines
5728
5793
  | attachment `type` of anything else — `sequence`, a typo | `attachment type "X" is not one of the 7 the Spine 4.3 format defines (region, mesh, linkedmesh, boundingbox, path, point, clipping). … the attachment is dropped from the skeleton without a word` — a **`CompileError`**, not a deferral: rigc is not going to implement a name the format does not have. (`sequence` is a key on a region or a mesh, not a type of its own.) |
5729
5794
  | `"type": null` | `"type" is null, which is not a name. … PRESENT-and-null is not absent: getValue(map, "type", "region") takes the default only when the key is missing, so this map matches no case, readAttachment returns null, and the attachment is dropped from the skeleton without a word. Remove the key, or name a type.` Leaving the key **out** is legal and reads as `region`; writing it as `null` is not the same thing ([#577](https://github.com/firejune/rigc/issues/577)) |
5730
5795
  | constraint `type` of anything else | `constraint type "X" is not one Spine 4.3 knows. The five are: ik, transform, path, physics, slider.` — all five are emitted, so this is a typo, and a typo is what the parser drops in silence |
5731
- | a path attachment's `lengths` | `"lengths" is not authored — rigc measures the setup arc length of each curve off the geometry` (§3.4). Not a deferral: a second copy of a number the vertices already fix |
5732
5796
  | any key neither format has, anywhere in either file | `<object> has a key this compiler does not read: "x" (did you mean "y"?) … Known here: …` (§5.1). Not a deferral either: a key nothing reads is a value you wrote and the emitted skeleton does not contain |
5733
5797
 
5734
5798
  Two more limits that are not errors but will shape what you can attempt:
@@ -8053,8 +8117,10 @@ evidence that the format will carry what you write.
8053
8117
  so do its `position`, `spacing` and three-channel `mix` timelines — all three
8054
8118
  channels of every `mix` key, and all twelve curve numbers on each key that
8055
8119
  carries a curve. ⚠️ Its `lengths` did **not**, which is the last bullet — and
8056
- since [#560](https://github.com/firejune/rigc/issues/560) they do, because rigc
8057
- now emits the numbers the editor recomputes rather than numbers near them.
8120
+ since [#560](https://github.com/firejune/rigc/issues/560) they do on that rig,
8121
+ because rigc emits the computation the editor runs rather than one near it.
8122
+ What they are computed ON is a second question, which the round trip could not
8123
+ ask and #804 did: see the end of this section.
8058
8124
  - 🔬 **`physics.mix` and `physics.reset` timelines survive**, the `reset` key
8059
8125
  included — a key that carries a time and no value at all — and so does the
8060
8126
  physics constraint's setup `mix`.
@@ -8107,9 +8173,33 @@ out of the artifact.
8107
8173
 
8108
8174
  🔬 **And the editor always writes `vertexCount / 3` entries, computing the
8109
8175
  wrap-around curve even on an open path** — that open path's fourth entry,
8110
- `2136.228`, is the closed-chain cumulative. ⚠️ Neither array is wrong: the parser
8111
- allocates `vertexCount / 3` and copies whatever is there, and `PathConstraint`
8112
- reads at most `lengths[curveCount]`, so the trailing entry is never read.
8176
+ `2136.228`, is the closed-chain cumulative. The parser allocates `vertexCount / 3`
8177
+ and copies whatever is there, and `PathConstraint` reads at most
8178
+ `lengths[curveCount]`, so the trailing entry is never read — and since
8179
+ [#804](https://github.com/firejune/rigc/issues/804) rigc writes it too, measured
8180
+ over the closed chain, so the `gallery/ride` build ends on the same `2136.228`.
8181
+
8182
+ 🚨 **"Every digit the editor printed" was true of two rigs and false in
8183
+ production** ([#804](https://github.com/firejune/rigc/issues/804)). Both rigs above
8184
+ are unweighted, unscaled and unconstrained, and the computation is only half of
8185
+ the number — the other half is the geometry it is fed, and there the editor and
8186
+ rigc differed twice:
8187
+
8188
+ - **Scale.** rigc blended weighted vertices through setup matrices that ignored
8189
+ bone scale, shear and `inherit`: a 50/50-weighted probe path over a bone at
8190
+ scale 2 measured 0.752× the runtime's own `curves`. The matrices are the
8191
+ runtime's now, and an omitted `lengths` measures within 1e-4 of `curves`
8192
+ (`PS186`).
8193
+ - **Constraints.** The editor measures the pose the first update hands the path
8194
+ constraint, with every constraint ordered before it applied; a transform
8195
+ constraint on the path's slot bone ordered after it moves nothing. rigc does
8196
+ not pose, so it cannot reproduce that — an omitted `lengths` is the
8197
+ **unconstrained** setup figure (`PS190`), and on a path a constraint moves at
8198
+ rest that is not the editor's number.
8199
+
8200
+ ⇒ **A stated `lengths` is carried as stated**, and `ingest` writes the source's
8201
+ array into the spec, so a rebuilt export is its export on this field. What rigc
8202
+ still measures is an array you left out of a spec you wrote.
8113
8203
 
8114
8204
  ⇒ **What this means for you.** `lengths` is the one number in a path rig you
8115
8205
  cannot check by looking: `diff` does not compare it, and `A33` asks only that it
package/docs/INGEST.md CHANGED
@@ -637,7 +637,6 @@ is the one failure a comparison of two sets cannot show you.
637
637
  | `HEADER_REDERIVED` | `LOSS` | 0 | `skeleton.spine`: the rebuild writes the version of the runtime rigc links. The line says whether that is the same string the source states | nothing — but read the line: a 4.2 export rebuilds as 4.3 in that one field, and a source from another generation raises `GENERATION_UNSUPPORTED` beside it, which is the blocker about the DATA rather than about the string |
638
638
  | `IK_KEY_FIELD` | `BLOCK` | 1 | a key field on an `ik` timeline that is not part of its shape | check the spelling; an unknown field is dropped from the rebuilt track |
639
639
  | `NO_STAGE` | `BLOCK` `JUDGE` | 1 | the skeleton declares no stage, and one of two things follows. A **judgement** — exit 0 — when `--stage x,y,w,h` supplied a box, because nothing measured the box you gave it. A **blocker** when the header states **half** a stage — an origin with no extent, or one extent without the other — which the rig spec cannot hold; the detail names the fields it states. A header with **none** of the four is not a finding at all: it is carried as `"width": null, "height": null` and rebuilds byte for byte ([#714](https://github.com/firejune/rigc/issues/714)) | for the judgement, nothing if the box came from the project the file came from. For the blocker, supply the box with `--stage`, or take the stray field(s) out of the source and the absence is carried. It cannot be derived: posing the rig gives the animated extent, which is a different number |
640
- | `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 |
641
640
  | `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 |
642
641
  | `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 |
643
642
  | `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": "*"` |
@@ -628,7 +628,7 @@ with the member's own `skin: true` — either half alone is refused, because `Sk
628
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
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
- | `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) |
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 `lengths` **stated or measured** (#804): a stated array — `vertexCount / 3` entries, which is what `ingest` carries from an export — is emitted as stated; an omitted one is the cumulative setup length at the end of each curve over the closed chain, taken through each influence's own bone on the unconstrained setup pose and measured as `PathConstraint` measures it — its own four-sample forward difference, about 0.5 % below the arc (AUTHORING §10.6). **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
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)) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.36.0",
3
+ "version": "1.0.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
@@ -398,9 +398,10 @@ export function parseAtlasText(text: string): ParsedAtlas {
398
398
  * The same atlas text with every page's name line replaced.
399
399
  *
400
400
  * This is how `--atlas-in` emits: the imported atlas passes through verbatim —
401
- * every field, every region, every page, in its own order and its own whitespace
402
- * — and only the page NAMES move, because they are paths and the file has been
403
- * re-anchored to a new directory. Rewriting by line index rather than by
401
+ * every field, every region, every page, in its own order — and only the page
402
+ * NAMES move, because they are paths and the file has been re-anchored to a new
403
+ * directory. Its blank lines are then put in rigc's own shape by
404
+ * `canonicalAtlasShape` (issue #803); this function leaves them as they were. Rewriting by line index rather than by
404
405
  * re-serialising is the point: a re-serialiser would have to understand every
405
406
  * field it re-emits, and the ones it did not understand would quietly vanish
406
407
  * (`scale:` is the expensive example — [`atlasScales`](render.ts) reports it, and
@@ -421,6 +422,43 @@ export function rewritePageNames(parsed: ParsedAtlas, rename: (name: string, ind
421
422
  return out.join('\n');
422
423
  }
423
424
 
425
+ /**
426
+ * The same atlas text in the whitespace shape `writeAtlasText` writes: no blank
427
+ * line before the first entry, exactly one between two blocks, none after the
428
+ * last, and one trailing newline. Every non-blank line is kept byte for byte and
429
+ * in its own order — only blank lines are dropped or collapsed — so a text
430
+ * already in that shape comes back unchanged.
431
+ *
432
+ * This is what `--atlas-in` re-emits (issue #803). A pack's blank lines are its
433
+ * packer's, not its content: a 3.8-era packer begins every file with one, and
434
+ * `A07_ATLAS_TEXT_SHAPE` — which checks the text rigc writes — refused the
435
+ * re-emission at `line 1`. What makes dropping them safe is what they mean to
436
+ * the reader that owns the format: in `TextureAtlas` a run of blank lines ends a
437
+ * page block exactly as one blank line does, and a run before the first page is
438
+ * read as nothing. Measured through the runtime, `"\n" + text`, `"\n\n" + text`
439
+ * and `text` load to the same pages and regions (`PKR63`, `PKR64`).
440
+ *
441
+ * ⚠️ One reading is NOT the same, and it moves toward rigc's: the runtime's
442
+ * leading-blank loop (`while (line && …)`) stops on an empty string, so a file
443
+ * that opens `"\n"` and then a header entry (`key: value` before the first page
444
+ * name) reads the header as a page name there, while `parseAtlasText` — which the
445
+ * compile took its geometry from — skips the blank and reads it as a header.
446
+ * Emitting without the blank makes the file say to the runtime what rigc measured.
447
+ *
448
+ * A blank line is one the runtime reads as blank: `trim()` is empty. One that
449
+ * carries only spaces is written as the empty line, because that is the blank
450
+ * line's one spelling in `writeAtlasText`.
451
+ */
452
+ export function canonicalAtlasShape(text: string): string {
453
+ const out: string[] = [];
454
+ for (const line of text.split(/\r\n|\r|\n/)) {
455
+ if (line.trim().length > 0) out.push(line);
456
+ else if (out.length > 0 && out[out.length - 1] !== '') out.push('');
457
+ }
458
+ while (out.length > 0 && out[out.length - 1] === '') out.pop();
459
+ return out.length === 0 ? '' : `${out.join('\n')}\n`;
460
+ }
461
+
424
462
  // ---------------------------------------------------------------------------
425
463
  // writing
426
464
  // ---------------------------------------------------------------------------