spine-rigc 2.54.0 → 2.55.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
@@ -601,7 +601,7 @@ commands take it and what its default is.
601
601
  | `repack <dir> --out <dir> --keep-pages` | **`skeleton.model.json` alone, for the pages already in place** — for a player that reads the document beside an atlas and pages that already shipped. The same read and stage rules, then `build --atlas-in` over the input atlas in place of the lift and the pack; it writes the document, and nothing else, only after its `pages` are shown to state the atlas as it stands, the rebuilt skeleton byte-identical to the one on disk (so `spine.sha256` names it — no acceptance flag here) and the gate green. `--out` is usually the build's own directory and must not already hold a `skeleton.model.json`; a page or region line the document has no field for is refused by name ([AUTHORING §0.4](docs/AUTHORING.md)) |
602
602
  | `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all |
603
603
  | `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
604
- | `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene |
604
+ | `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene |
605
605
  | `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version — a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` |
606
606
  | `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
607
607
  | `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
package/docs/AUTHORING.md CHANGED
@@ -246,6 +246,7 @@ What the flags mean:
246
246
  | `--show`, `--dim`, `--at`, `--zoom` | `inspect` only — what it draws, whether the art is faded, which times of `--animation` it poses and the bone, slot or folds it frames to; §13 says each, with the refusals |
247
247
  | `--geometry` | `render` only: also write a `geometry.json` into each frame directory — per frame, every bone's world transform and every slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry and topology, on the frames' own grid and viewport. Every other file is byte for byte what the render writes without it. Refused with `--slot`/`--hide`, because the geometry is the whole pose whatever is drawn — **§8.2** |
248
248
  | `--folds` | `render` only: also read which mesh triangles **fold** — turn over against the setup pose — and write a `folds.json` into each frame directory: per mesh, the frames with a folded triangle, the worst frame's count and which triangles, by index, with their world box and setup-pose area. One printed line per mesh that folds; a triangle degenerate or inverted at the setup pose is said once and never counted. A reading, not a gate. Every other file is byte for byte what the render writes without it, `geometry.json` included. Refused with `--slot`/`--hide`, for `--geometry`'s reason — **§8.3** |
249
+ | `--pass-through` | `render` only: `<cloth>:<leg>[,<cloth>:<leg>…]` — also read where each cloth slot **passes through** its leg slot, and write a `pass-through.json` into each frame directory: the frames where the cloth draws over the leg and its drawn side lies inside the leg's drawn outline by 2 steps or more, each with the depth and the world box. One printed line per pair, and one per frame that passes through. The first name must draw over the second for a frame to be read — the declared order, never both. A reading, not a gate. Every other file is byte for byte what the render writes without it. An unknown slot, a slot that draws on no frame, a malformed list and a pair named twice are refused by name, and so is `--slot`/`--hide` beside it — **§8.5** |
249
250
  | `--environment` | `render` only: the wind and gravity the rig's physics constraints answer, in time — a schedule file `{ "spec": "rigc-environment/1", "steps": [{ "t": <seconds>, "wind"?: [x, y], "gravity"?: [x, y] }, …] }`, x right and **y down**, each step holding from its `t` until the next. Both posers set each change before the step that poses the first frame at or after its `t`, and draw the same frames, `geometry.json` and `frames.json` (`RE01`). `frames.json` and `geometry.json` record it, and the framing reads it. A malformed file, a half no physics constraint answers, two steps on one frame and a step after every set's last frame are refused by name, exit 2, nothing written; `check` refuses a frame set drawn under one as a reference. Without it every file is the bytes it always was. An inspection of the response the rig states, not a scene — **§8.4** |
250
251
  | `--poser` | `render`, `inspect`, `check` and `explain`: `core` or `spine` — on `render`, `inspect` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`, `RC34`). On the entry that links none of spine-core — the installed `rigc` without the runtime beside it, `bun cli_core.ts` in a clone — that refusal ends on the route to the entry that does (``the entry that links it runs them — installed, the same `rigc` once @esotericsoftware/spine-core is installed beside the package; from a source checkout, `bun cli.ts` ``); a runtime that is installed and fails to load is refused without it, since installing it is not what that reader has to do. On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1), ending on the same route where the entry links none of it |
251
252
  | `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
@@ -1115,6 +1116,7 @@ bun cli.ts render --candidate path/to/spine [--animation …] [--skin …] [--
1115
1116
  bun cli.ts render --candidate path/to/spine --slot <name,…> | --hide <name,…> # part of the rig, same grid
1116
1117
  bun cli.ts render --candidate path/to/spine --geometry # + geometry.json per set: the numbers the frames were drawn from
1117
1118
  bun cli.ts render --candidate path/to/spine --folds # + folds.json per set: which mesh triangles turn over, and when
1119
+ bun cli.ts render --candidate path/to/spine --pass-through skirt:leg # + pass-through.json: where a cloth goes through a leg
1118
1120
  bun cli.ts preview --candidate path/to/spine [--candidate path/to/another …] [--animation …] [--out preview.html]
1119
1121
  bun cli.ts vote --candidate path/to/a --candidate path/to/b [--out ballot.html]
1120
1122
  bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl]
@@ -4543,7 +4545,7 @@ a delta from the constraint's own setting.
4543
4545
  infinite inverse mass — every velocity and offset is NaN from the first step.
4544
4546
  - **Four of the seven are bounded, and a key outside its bound is a compile
4545
4547
  error.** `mass` must be
4546
- `> 0`, `damping` must be inside the closed `[0, 1]`, and `mix` and `strength`
4548
+ `> 0`, `damping` must be inside the half-open `[0, 1)`, and `mix` and `strength`
4547
4549
  must be `0` or more. `A23_PHYSICS_CONSTRAINT_EFFECTIVE` applies the same four to
4548
4550
  a file rigc did not write, naming the animation, the constraint, the key time
4549
4551
  and the value — so the compiler is where a spec you wrote is refused, and the
@@ -4554,14 +4556,14 @@ a delta from the constraint's own setting.
4554
4556
  non-finite at it — and the sentence names the expression and its lines;
4555
4557
  **behavioural** means the runtime runs it, finitely, and the rig runs wrongly,
4556
4558
  so refusing it is rigc's call and the sentence says so and says what the value
4557
- does. Two of the eight ways out are arithmetic.
4559
+ does. Two of the nine ways out are arithmetic.
4558
4560
 
4559
4561
  | field | at rest | on a key | basis |
4560
4562
  | --- | --- | --- | --- |
4561
4563
  | `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 |
4562
4564
  | `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 |
4563
4565
  | `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 |
4564
- | `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`) |
4566
+ | `damping` | `inside [0, 1)` | `inside [0, 1)` | behavioural: above 1 every velocity grows on every step and the offset diverges; behavioural: at 1 the per-step decay is 1 at every rate, so no velocity ever decays: the step's two eigenvalues lie ON the unit circle (modulus √1 = 1) rather than inside it, so nothing a push puts in (wind, gravity, a key, the bone moving) ever dies out, and two such constraints on one chain stepping alike resonate and grow without limit; 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`) |
4565
4567
 
4566
4568
  ⚠️ The line between the two is **non-finite within the walk that measures
4567
4569
  it**, not "non-finite eventually": [measured] on the generated physics
@@ -4588,14 +4590,25 @@ a delta from the constraint's own setting.
4588
4590
  the key at 45 and at 120. Above `1` every velocity grows on every step and
4589
4591
  diverges at every rate. The refusal names the exponent and the rate for this
4590
4592
  reason.
4591
- - ✅ **Both ends of `damping` are inside the bound.** `1 ** x` is 1 and
4592
- `0 ** x` is 0 for every positive exponent, so neither depends on `fps`: at `1`
4593
- the velocity never decays and the jiggle holds for as long as it runs, and at
4594
- `0` every velocity is zeroed on every step, so the offset follows the bone with
4595
- no overshoot. Both are finite, at rest and keyed — [measured] 120 steps from
4596
- `Physics.reset` at 60, 45 and 30 fps on the generated physics fixture. ⚠️ `1` is also what a 4.2 file means by saying nothing:
4597
- 4.2's parser reads an omitted `damping` as `1` where 4.3's reads `0.85`
4598
- (§4.6), so a rig migrated from 4.2 that keys "the default" keys `1`.
4593
+ - ⚖️ **`0` is inside the bound and `1` is not.** `0 ** x` is 0 and `1 ** x` is
4594
+ 1 for every positive exponent, so neither depends on `fps`. At `0` every
4595
+ velocity is zeroed on every step, so the offset follows the bone with no
4596
+ overshoot — a rig. At `1` the velocity never decays, and both are finite, at
4597
+ rest and keyed — [measured] 120 steps from `Physics.reset` at 60, 45 and 30 fps
4598
+ on the generated physics fixture — which is why `1`'s refusal is behavioural.
4599
+ It was inside from issue #794 until #1422, as a jiggle held for as long as it
4600
+ runs. What that missed: a decay of 1 puts the step's two eigenvalues **on** the
4601
+ unit circle (§4.6), so nothing a push puts in ever dies out, and a chain of such
4602
+ links resonates. [measured on #1422, x drive, inertia 1, fps 30, strength 100,
4603
+ the runtime's default world, 600 s] one link held its peak offset at 40.3; two
4604
+ identical links on one chain grew it linearly, 6.03e3 at 60 s to 6.09e4 at
4605
+ 600 s; three passed 10⁶ at 316 s. Detuning the second link's strength by 1 %
4606
+ held it flat — equal maps are the edge, and no authored number says how close
4607
+ is too close, so the value is refused rather than the pair. Lower it to below
4608
+ 1 — `0.999999` is accepted. ⚠️ `1` is also what a 4.2 file means by saying
4609
+ nothing: 4.2's parser reads an omitted `damping` as `1` where 4.3's reads
4610
+ `0.85` (§4.6), so a rig migrated from 4.2 that keys "the default" keys `1`, and
4611
+ is refused.
4599
4612
  - 🚫 **`inertia`, `wind` and `gravity` are bounded nowhere, and neither is the top
4600
4613
  of `mix`.** The runtime documents no range for the first three, and
4601
4614
  `PhysicsConstraintPose` documents `mix` as "a percentage (0+)" — so a negative
@@ -4609,7 +4622,14 @@ a delta from the constraint's own setting.
4609
4622
  `strength`, 0 takes the restoring term out of the velocity update and leaves
4610
4623
  `damping` and `inertia` applied — the offset is not pulled back *while the key
4611
4624
  holds*, which is "physics released" for that span, and the next key pulls it
4612
- back. At rest the two
4625
+ back. ⚠️ So there has to **be** a next key: an animation whose `strength`
4626
+ poses `0` at every time it plays — every key `0` from `t = 0` and no Bézier
4627
+ lifting it between them — is refused by `A23` (issue #1422). Whether the
4628
+ animation loops is not in the file, and does not matter: looped it never leaves
4629
+ 0, played once its track holds the last frame, which is 0. [measured on #1422,
4630
+ x drive, the runtime's default world] the offset grows exactly linearly while
4631
+ it plays — 1.33e4 at 60 s, 1.33e5 at 600 s. A first key after `t = 0` is a
4632
+ span: before it the timeline poses the setup `strength`, which pulls back. At rest the two
4613
4633
  part ways: a `strength` of 0 is a constraint nothing pulls back, which `A23`
4614
4634
  refuses, and a `mix` of 0 is a constraint muted until an animation keys it
4615
4635
  above 0 — the next bullet. A **negative** setup `strength` is refused too, with
@@ -4981,14 +5001,16 @@ key times. One lag, one place.
4981
5001
  mass?, wind?, gravity?, mix?, fps?, limit? }`. These are emitted into the 4.3
4982
5002
  `constraints` array. Seven of them — the six tuning numbers and `mix` — can also
4983
5003
  be **keyed over time** as `tracks` entries naming this constraint (§4.4); this
4984
- table is the value at rest, and a timeline overrides it while it plays. `mass: 0` becomes an infinite inverse mass and a `damping` above 1
5004
+ table is the value at rest, and a timeline overrides it while it plays. `mass: 0` becomes an infinite inverse mass and a `damping` at or above 1
4985
5005
  never settles — both are `A23`, here and on every timeline key that states them;
4986
5006
  a `damping` below 0 is refused as well, and whether it is NaN depends on `fps` —
4987
- §4.4. `damping` is `[0, 1]` with both ends in: `1` never decays and `0` zeroes the
4988
- velocity on every step, both finite at every rate.
5007
+ §4.4. `damping` is `[0, 1)`: `0` zeroes the velocity on every step at every rate
5008
+ and is in, and `1` never decays — its step sits on the unit circle, below — and is
5009
+ out since issue #1422 (it was in from #794).
4989
5010
  ⚠️ An omitted `damping` is `0.85` to 4.3's parser (`SkeletonJson.js:308` in
4990
5011
  4.3.13) and `1` to 4.2's (`SkeletonJson.js:242` in 4.2.120), so a constraint
4991
- migrated from 4.2 that stated nothing may arrive stating `1` — which builds. ⚠️ `strength: 0` is `A23` **here and not on a key**:
5012
+ migrated from 4.2 that stated nothing may arrive stating `1` — which `A23` refuses;
5013
+ state the damping you mean, below 1. ⚠️ `strength: 0` is `A23` **here and not on a key**:
4992
5014
  at rest it is a constraint nothing pulls back, and on a key it is a release somebody
4993
5015
  asked for, which §4.4 states with the measurement behind it. A negative `strength` is
4994
5016
  `A23` here and on a key, and here its sentence is its own: the offset is pushed
@@ -5008,9 +5030,16 @@ satisfy
5008
5030
  strength · massInverse · step² < 2 · (1 + damping^(60 · step))
5009
5031
 
5010
5032
  strictly, where `massInverse` is `1 / mass`. Each substep of the integrator is a
5011
- linear map whose two eigenvalues lie inside the unit circle exactly then; at or
5012
- past it the offset never dies out, and past it any push — wind, gravity, a key,
5013
- the bone moving — grows without limit. [measured] on 270 generated rigs, all 115
5033
+ linear map whose determinant is `damping^(60 · step)`, and for that decay below 1
5034
+ its two eigenvalues lie inside the unit circle exactly then; at or past it the
5035
+ offset never dies out, and past it any push — wind, gravity, a key, the bone
5036
+ moving — grows without limit. ⚠️ At a decay of exactly 1 no `strength` settles:
5037
+ the determinant is 1, so the pair has modulus 1 and sits **on** the circle — the
5038
+ reason `damping` 1 is refused (§4.4), and this guide said otherwise until issue
5039
+ #1422. The decay is the runtime's own double, so a `damping` a hair below 1 can
5040
+ reach it at a high rate — [measured] the largest double below 1 does from
5041
+ `fps: 121` on — and `A23` names that case by the decay:
5042
+ `` physics "C" has damping 0.9999999999999999 at fps 121 — damping^(60·step) = 1 as the runtime forms it, so although the damping is below 1 no velocity ever decays: … ``. [measured] on 270 generated rigs, all 115
5014
5043
  past the line ran past 10⁶ units under a unit gust (most with no gust at all),
5015
5044
  all 145 under it stayed bounded, and the 10 exactly on it were still growing or
5016
5045
  swinging their tip the bone's full length when a 6 s walk ended, spine-core
@@ -5026,9 +5055,9 @@ joint is an upper bound: an animation that raises strength and mass together is
5026
5055
  judged at its stiffest strength against its lightest mass. **To fix it, lower
5027
5056
  `strength`, raise `mass`, raise `fps` or raise `damping`** — the first three move
5028
5057
  the left side down, the last moves the right side up (a higher `damping` number
5029
- keeps more velocity per step, yet it is what widens the bound: from 2 at `0` to
5030
- 4 at `1`); the refusal prints both sides so you can see how far each lever has
5031
- to go.
5058
+ keeps more velocity per step, yet it is what widens the bound: from 2 at `0`
5059
+ toward 4 as it nears `1`, which is itself out); the refusal prints both sides so
5060
+ you can see how far each lever has to go.
5032
5061
 
5033
5062
  ⚠️ **What `rigc render` draws a spring with is not a player's clock to the bit.** A
5034
5063
  constraint integrates whole steps of `1 / fps` and shows the rest as a lag weight, so
@@ -6533,7 +6562,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6533
6562
  | `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 |
6534
6563
  | `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 |
6535
6564
  | `animation "A" bone "X" translatex: key value must be an array of 1 number(s)` | the value shape must match the property (§4.4) |
6536
- | `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: 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, `mix` and `strength` are `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere. ⚠️ 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`; a setup `strength` below 0 says the offset is pushed away and grows, the same arm this message quotes for a negative key. 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) |
6565
+ | `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: 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)` — `0` included, `1` not, since issue #1422 —, `mix` and `strength` are `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere. ⚠️ 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`; a setup `strength` below 0 says the offset is pushed away and grows, the same arm this message quotes for a negative key. 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) |
6537
6566
  | `a key carries both a named easing and a raw curve; pick one` | R6 |
6538
6567
  | `last key carries an easing but has nothing to ease to` | drop `ease`/`curve` from the final key |
6539
6568
  | `key times must strictly increase (at t=…)` | including after `lag` and `stagger` |
@@ -6735,7 +6764,7 @@ Fix A00 and run it again.
6735
6764
  | <a id="A20_MESH_WEIGHTS_COHERENT"></a>`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. On a rig spec's `"boneIndexing": "raw"` mesh an index out of range or a run that does not decode is refused at compile first (§5.1, issue #917), so here it names a skeleton rigc did not compile. Under `rig-play` 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. Fix the rig spec's `controls`/`chain`, a `segments` generator's `bones` (its `influence` line names a bone no vertex binds), or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
6736
6765
  | <a id="A21_MESH_RIM_PINNED"></a>`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. **SKIPs** by name on authored or linked geometry and on a `segments` mesh, whose outline is weighted by distance like the rest of it — none of them claims a pinned rim |
6737
6766
  | <a id="A22_MESH_UVS_IN_UNIT_RANGE"></a>`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 |
6738
- | <a id="A23_PHYSICS_CONSTRAINT_EFFECTIVE"></a>`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 — 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 — **at rest, and on every physics timeline key**. 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. 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) — 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: such a constraint is measured moving its bone by exactly the opposite of a positive mix. 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. `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)), so this sentence is met on a file, never on a decompiled rebuild. 🦴 **A lever drive on a bone with no lever** (issue #1195): a constraint driving `rotate`, `shearX` or `scaleX` on a bone whose `length` is 0 (or states none) is refused, at rest, with one sentence per reading. `rotate` and `shearX` chase the bone's tip, so with no tip the solver aims the bone at a **world** direction — measured through rigc's poser on a chain held still for 3 s, a length-0 bone turns 53.204° from a 90° rest and holds there, where length 0.01 moves 0.0000: `` physics "C" drives rotate on bone "B" whose length is 0 — with no tip the solver aims the bone at world angle 0 and holds it there; give "B" a length or drive x/y instead ``. The rest angle is not read: a bone resting at world 0° does not move, but only until a key or a parent turns it. `scaleX` divides the motion along the bone by its length and adds nothing at 0, so the drive does nothing: `` physics "C" drives scaleX on bone "B" whose length is 0, which the solver divides the motion by, so it drives nothing; give "B" a length ``. Fix in the **rig spec**: give the bone its `length` (§3.2), measured off the art it carries, or, for the first sentence, drive the constraint in `x`/`y` in the **motion spec** instead. `x` and `y` read the bone's origin and never its tip, so a constraint driving only those on a length-0 bone is not refused; the editor's own example exports carry them there. The Spine editor's export drops `rotate`, `scaleX` and `shearX` from a physics constraint on a zero-length bone and keeps them on any bone with a length — measured on 4.3.23 and 4.3.26, issue #1196 — so what this clause refuses is also what the editor would silently lose. 🧮 **A step that does not settle** (issue #1416): a constraint that drives anything, every value inside its own row, is refused when `strength·massInverse·step² ≥ 2·(1 + damping^(60·step))` with `step = 1 / fps` — the integrator's step map then has an eigenvalue on or outside the unit circle, and [measured] of 270 generated rigs, all 115 past the bound ran past 10⁶ units under a unit gust, all 145 under it stayed bounded, and the 10 exactly on it were still growing or swinging their tip the bone's full length when a 6 s walk ended. It reads the setup values, and for each animation keying `strength`, `mass` or `damping` on the constraint, the stiffest strength, the lightest mass and the smallest damping that animation poses (each key, each Bézier sample, and the setup value where the animation keys that value first after t = 0), judged together — an upper bound on any pose the animation reaches. Both sides are formed in the runtime's own order (`m = step · massInverse`, `damping ** (60 · step)`) and printed apart: `` physics "C" has strength 100, mass 1 (massInverse 1), fps 5, damping 0.85 — strength·massInverse·step² = 4.0000 must be < 2·(1 + damping^(60·step)) = 2.2845: at or past that bound the step stops settling — one step can multiply the offset by 2.807 every 1/5 s — so a push (wind, gravity, a key, the bone moving) never dies out, and past the bound it grows without limit; lower strength, raise mass, raise fps or raise damping ``; a keyed combination opens `` animation "A" can pose physics "C" at strength … `` and names the key each value came from. Fix in the **motion spec**'s physics table or its keys (§4.6). **SKIP** when the skeleton declares no physics constraint — the same sentence `A36` and `A37` print for their own constraint types |
6767
+ | <a id="A23_PHYSICS_CONSTRAINT_EFFECTIVE"></a>`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 — 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; `0` is inside, and `1` is not since issue #1422 (it was from #794): finite at every rate, but a decay of 1 puts the step's eigenvalues on the unit circle, so nothing a push puts in dies out and two such links on one chain were measured resonating without limit — `` physics "C" has damping 1; must be inside [0, 1) — … at 1 the per-step decay is 1 at every rate, so no velocity ever decays: the step's two eigenvalues lie ON the unit circle … ``, one line per constraint that states it — **at rest, and on every physics timeline key**. 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. 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) — 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: such a constraint is measured moving its bone by exactly the opposite of a positive mix. 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. `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)), so this sentence is met on a file, never on a decompiled rebuild. 🦴 **A lever drive on a bone with no lever** (issue #1195): a constraint driving `rotate`, `shearX` or `scaleX` on a bone whose `length` is 0 (or states none) is refused, at rest, with one sentence per reading. `rotate` and `shearX` chase the bone's tip, so with no tip the solver aims the bone at a **world** direction — measured through rigc's poser on a chain held still for 3 s, a length-0 bone turns 53.204° from a 90° rest and holds there, where length 0.01 moves 0.0000: `` physics "C" drives rotate on bone "B" whose length is 0 — with no tip the solver aims the bone at world angle 0 and holds it there; give "B" a length or drive x/y instead ``. The rest angle is not read: a bone resting at world 0° does not move, but only until a key or a parent turns it. `scaleX` divides the motion along the bone by its length and adds nothing at 0, so the drive does nothing: `` physics "C" drives scaleX on bone "B" whose length is 0, which the solver divides the motion by, so it drives nothing; give "B" a length ``. Fix in the **rig spec**: give the bone its `length` (§3.2), measured off the art it carries, or, for the first sentence, drive the constraint in `x`/`y` in the **motion spec** instead. `x` and `y` read the bone's origin and never its tip, so a constraint driving only those on a length-0 bone is not refused; the editor's own example exports carry them there. The Spine editor's export drops `rotate`, `scaleX` and `shearX` from a physics constraint on a zero-length bone and keeps them on any bone with a length — measured on 4.3.23 and 4.3.26, issue #1196 — so what this clause refuses is also what the editor would silently lose. 🧮 **A step that does not settle** (issue #1416): a constraint that drives anything, every value inside its own row, is refused when `strength·massInverse·step² ≥ 2·(1 + damping^(60·step))` with `step = 1 / fps` — the integrator's step map then has an eigenvalue on or outside the unit circle, and [measured] of 270 generated rigs, all 115 past the bound ran past 10⁶ units under a unit gust, all 145 under it stayed bounded, and the 10 exactly on it were still growing or swinging their tip the bone's full length when a 6 s walk ended. It reads the setup values, and for each animation keying `strength`, `mass` or `damping` on the constraint, the stiffest strength, the lightest mass and the smallest damping that animation poses (each key, each Bézier sample, and the setup value where the animation keys that value first after t = 0), judged together — an upper bound on any pose the animation reaches. Both sides are formed in the runtime's own order (`m = step · massInverse`, `damping ** (60 · step)`) and printed apart: `` physics "C" has strength 100, mass 1 (massInverse 1), fps 5, damping 0.85 — strength·massInverse·step² = 4.0000 must be < 2·(1 + damping^(60·step)) = 2.2845: at or past that bound the step stops settling — one step can multiply the offset by 2.807 every 1/5 s — so a push (wind, gravity, a key, the bone moving) never dies out, and past the bound it grows without limit; lower strength, raise mass, raise fps or raise damping ``; a keyed combination opens `` animation "A" can pose physics "C" at strength … `` and names the key each value came from. A `damping` the row accepts whose decay the runtime's doubles round to exactly 1 — the largest double below 1 at `fps: 121` or more, [measured] — is the same fault and is named by the decay: `` physics "C" has damping 0.9999999999999999 at fps 121 — damping^(60·step) = 1 as the runtime forms it, … reached here by rounding; lower damping or lower fps ``. Fix in the **motion spec**'s physics table or its keys (§4.6). ⏸️ **Strength held at 0 for as long as an animation plays** (issue #1422): an animation whose `strength` timelines on a constraint that drives anything pose `0` at every time — every key `0` from `t = 0`, and no Bézier sample between them above it — is refused, because a key of 0 is accepted for a span only on the strength of the later key that pulls the offset back, and this animation has none: `` animation "A" poses physics "C" strength 0 at every time it plays — 2 key(s) from t=0, every one 0 and no curve between them lifting it, so nothing pulls it back: a strength key of 0 is accepted for a span because a later key pulls the offset back, and this animation has none, so for as long as it plays, looped or held on its last frame, a push (wind, gravity, the bone moving) carries the offset away and it grows without limit; key strength above 0 somewhere in the animation, or remove the timeline ``. Whether the animation loops is not read — skeleton JSON has no loop flag and the motion spec's `loop` is a player hint — and is not needed: looped, it never leaves 0; played once, its track holds the last frame, which is 0. [measured on #1422] the offset grows exactly linearly while it plays, 1.33e4 at 60 s and 1.33e5 at 600 s, which A10's walk of the animation's own length passes as a finite number. A first key after `t = 0` is not held — before it the timeline poses the setup `strength` — and a constraint resting at `strength` 0 is named by its row instead. Fix in the **motion spec**: key `strength` above 0 somewhere in the animation (§4.4). **SKIP** when the skeleton declares no physics constraint — the same sentence `A36` and `A37` print for their own constraint types |
6739
6768
  | <a id="A24_AXIS_SPACE_STROKE"></a>`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 |
6740
6769
  | <a id="A25_DETACHED_BONE_PARENTAGE"></a>`A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
6741
6770
  | <a id="A26_SLOT_DRAW_ORDER"></a>`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: the completeness clause reads it as every declared slot lost and names them |
@@ -7684,6 +7713,97 @@ not have — and a render draws the rig where it stands, so the picture would sh
7684
7713
  a coat trailing a rig that never moves. The live track holds both to spine-core
7685
7714
  (`CO56`), and a player forwards them as §4.6 says.
7686
7715
 
7716
+ ### 8.5 Where a cloth passes through a leg — `render --pass-through`
7717
+
7718
+ A skirt drawn over a thigh can swing its side straight through the thigh on a build
7719
+ that passes every assertion and every check. A real skirt cannot do that while it is
7720
+ in front of the leg: the leg is inside it and leaves through the hem. `--pass-through`
7721
+ reads it off the frames `render` draws, for the pairs you name. On the selftest's
7722
+ probe (`pass_through_probe`, `fixtures/public.ts`: a skirt whose hip swings 30° and
7723
+ back over a leg it hangs in front of, and a second copy of the leg drawn over it):
7724
+
7725
+ ```bash
7726
+ bun cli.ts render --candidate path/to/spine --animation swing --pass-through skirt:leg,skirt:leg_front [--skin …] [--fps 12]
7727
+ # rigc render
7728
+ # .. swing 25 frame(s), 2.000s + contact.png + pass-through.json (1 of 2 pair(s) pass through) -> …/swing
7729
+ # .. pass "swing": cloth "skirt" over leg "leg" passes through on 21 of 25 frame(s) read [2-22] at 12 fps; deepest frame 11 (t=0.9167s): 23.00 units (23 step(s) of 1.0000) on its left side
7730
+ # .. pass "swing" frame 2 (t=0.1667s): cloth "skirt" over leg "leg" by 5.00 units (5 step(s) of 1.0000) on its left side, 44 row(s), in world box x [-27.48, -21.73] y [103.80, 147.08] (5.74 × 43.27)
7731
+ # … one line for each of frames 3-22
7732
+ # .. pass "swing": cloth "skirt" over leg "leg_front": the cloth draws over the leg on 0 of 25 frame(s) both draw ("leg_front" draws over "skirt" wherever both draw) — nothing read
7733
+ ```
7734
+
7735
+ **Which frames are read.** A frame where both slots draw and the **cloth draws over
7736
+ the leg**, in that frame's own draw order — a draw-order key that puts the leg in
7737
+ front takes its frames out of the reading. The order in the flag is the
7738
+ declaration: `skirt:leg` asks whether the skirt passes through the leg, and
7739
+ `leg:skirt` is a different question, never the same one read both ways. A slot
7740
+ paired with itself is read on no frame, and its line says why.
7741
+
7742
+ **What passing through means.** Four steps, every number in them derived:
7743
+
7744
+ 1. **Rows, in the cloth's setup pose.** The cloth is drawn over its setup-pose
7745
+ geometry — `rest`'s vertices (§8.2), the setup pose's bones with no deform — at
7746
+ the frame's own UVs and tint, onto cells of **one texel of its drawing** as the
7747
+ setup pose lays it out. A cell is drawn when the renderer paints its centre at
7748
+ half opacity or more (128 of 255) — the line an antialiased edge sits on, rather
7749
+ than the faintest fringe the renderer would still paint. The rows are the setup
7750
+ pose's, not the world's, so a skirt the pose swings or bends still has its side
7751
+ where its side is: read in world rows, a waist the pose tilts becomes a "side"
7752
+ on the rows it spans, and a leg under it would read as passing through.
7753
+ 2. **The hem's corner.** On each side, the corner is the outermost drawn cell in the
7754
+ lower half of the drawn rows — the lowest on a tie. The rows from the corner up
7755
+ to the cloth's top are read; the rows below it are the hem's own curve, under
7756
+ which a leg is meant to show.
7757
+ 3. **The run.** On each read row, from the centre of the outermost drawn cell as the
7758
+ frame poses it, step outward — along the setup pose's outward direction, carried
7759
+ to the frame by the cloth triangle holding that centre — in **world units**, one
7760
+ texel of the coarser of the two drawings at a time, and count the steps whose
7761
+ point the leg draws at half opacity, up to the first it does not. That is how far
7762
+ the leg's outline passes the cloth's, in the units the picture shows.
7763
+ 4. **The line.** A frame passes through when some read row's run is **2 steps or
7764
+ more** — more than the one step an outline is resolved to.
7765
+
7766
+ ⚠️ The run is in world steps, not in the cloth's setup-pose cells, because on a
7767
+ cloth the pose squashes the two disagree: counted in setup cells, a run reads longer
7768
+ than the picture holds, and the line printed a depth in units over a box narrower
7769
+ than that depth. The rows are the setup pose's
7770
+ because they say *which* part of the cloth is its side; the run is the world's
7771
+ because it says *how far* the leg shows (`PX04`, and `PX12` plants the other).
7772
+
7773
+ **Blind spots, stated.** A cloth that widens all the way up to its waist has its
7774
+ corner at mid-height, so the rows below are not read. A leg wholly beside the cloth,
7775
+ entering above its top, meets no read row's side. And the rows are the setup pose's,
7776
+ so a cloth authored lying on its side reads its side as a hem.
7777
+
7778
+ **The file.** `pass-through.json`, `rigc-pass-through/1`, beside each set's frames:
7779
+ `animation`, `skin` (absent when none was set), `fps`, `reading` (these definitions,
7780
+ stated in the file), and `pairs[]` in the flag's order, each with `cloth`, `leg`,
7781
+ `drawn` (frames where both slots draw), `over` (of those, the frames the cloth draws
7782
+ over the leg — the frames read), `frames[]` (only the frames that pass through:
7783
+ `index`, `time`, `cells` — the longest run, in steps — `depth` — that in world
7784
+ units — `cell` — the step — `side` — `left` or `right` of the cloth as it lies at its
7785
+ setup pose — `attachment` — the cloth's at that frame — `rows` — read rows whose run
7786
+ reaches the line — and `box` — `[minX, minY, maxX, maxY]`, world units, y up, over
7787
+ both ends of every such run, the cloth's outline to the run's far end) and `deepest`
7788
+ (the read frame with the longest run, the earliest on a tie, with `index`, `time`,
7789
+ `cells`, `depth`, `cell` and `side`; `null` when no frame is read). A pair that passes
7790
+ through on no frame is in the file, and its line says how many frames were read.
7791
+
7792
+ **Refused, nothing written:** a slot the skeleton does not have (with the slots it
7793
+ does), an entry without exactly one `:`, an empty entry or name, a pair named twice,
7794
+ a slot that draws on no frame of the sets drawn, a cloth or leg a clip cuts (its
7795
+ drawing's outline is no longer its attachment's), and `--slot`/`--hide` beside the
7796
+ flag, for `--folds`' reason.
7797
+
7798
+ **Nothing else moves.** The PNGs, `contact.png`, `frames.json` and `geometry.json`
7799
+ are byte for byte what the render writes without the flag (`PX06`), and both posers
7800
+ write the same file (`PX05`).
7801
+
7802
+ **A reading, not a gate.** It names frames and decides nothing: whether a frame that
7803
+ passes through is a defect, and what to do about it — a leg stop, a wider hem, a
7804
+ different swing — is yours. To look at one, draw it:
7805
+ `inspect --show art --animation <name> --at <time> --zoom slot:<cloth>`.
7806
+
7687
7807
  ---
7688
7808
 
7689
7809
  ## 9. Checking against the frames — `rigc check`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "2.54.0",
3
+ "version": "2.55.0",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,7 +74,8 @@ const SETUP_POSE_SAYS: Record<string, (pose: PhysicsSetup, animations: number, r
74
74
  // on the arm's own words, which `T100` holds.
75
75
  strength: (pose, _animations, rule) => `has strength ${pose.strength}; ${setupBasisSays(rule, pose.strength)}`,
76
76
  // The bound is read off the row, so the setup pose and a key cannot state two
77
- // intervals; the reason is the arm the value took, since the ends are inside (#794).
77
+ // intervals; the reason is the arm the value took — above 1, at 1 (issue
78
+ // #1422, inside from #794 until then) or below 0.
78
79
  damping: (pose, _animations, rule) =>
79
80
  `has damping ${pose.damping}; must be ${rule.states} — the per-step decay is \`damping ** (60 * step)\`, and ` +
80
81
  setupBasisSays(rule, pose.damping),
@@ -92,10 +93,11 @@ function setupBasisSays(rule: PhysicsPoseRule, poseValue: number): string {
92
93
 
93
94
  // Every failure mode here is silent. The five component fields default to
94
95
  // 0, so a constraint can drive nothing at all; `mix` 0 mutes it; `mass` 0
95
- // becomes an infinite massInverse; and `damping` above 1 never settles, which
96
- // on a mesh-driving bone means the canvas re-rasterises forever. 1 itself is
97
- // inside the bound since issue #794: it never decays, which is finite and a
98
- // choice, and what it costs a consumer's frame is the consumer's to judge.
96
+ // becomes an infinite massInverse; and `damping` at or above 1 never settles,
97
+ // which on a mesh-driving bone means the canvas re-rasterises forever. 1 itself
98
+ // was inside the bound from issue #794 to #1422, as a jiggle held rather than a
99
+ // fault; it is out because a decay of 1 puts the step's eigenvalues on the unit
100
+ // circle, and a chain of such links was measured resonating without limit.
99
101
  //
100
102
  // 🔑 **Two arms, one criterion.** The second arm below reads every physics
101
103
  // TIMELINE key, and it is written against `PHYSICS_POSE_RULES` — the table
@@ -410,12 +412,72 @@ export function a23PhysicsConstraintEffective({ fail, skip, stats }: Verdicts, f
410
412
  if (combo.strength.value === setup.strength.value && combo.massInverse.value === setup.massInverse.value && combo.damping.value === setup.damping.value) continue;
411
413
  const at = physicsStepStability(combo.strength.value, combo.massInverse.value, combo.damping.value, physics.step);
412
414
  if (at.stable) continue;
415
+ // A decay rounded to 1 by the setup damping itself is the setup line's, whatever else this animation keys.
416
+ if (!(at.d < 1) && at.k < at.bound && combo.damping.from === 'setup') continue;
413
417
  fail(
414
418
  'A23_PHYSICS_CONSTRAINT_EFFECTIVE',
415
419
  `animation "${animation}" can pose physics "${facts.constraints[index]?.name ?? `#${index}`}" at ${runawaySays(combo, physics.step, at)}`,
416
420
  );
417
421
  }
418
422
  }
423
+
424
+ // --- strength held at 0 for as long as an animation plays (issue #1422) ---
425
+ //
426
+ // A strength KEY of 0 is accepted (issue #727) because it releases the
427
+ // constraint for a span and a later key pulls the offset back. An animation
428
+ // whose strength timelines pose 0 at EVERY time has no later key: [measured on
429
+ // #1422, x drive, the runtime's default world, spine-core and rigc's core
430
+ // identical bit for bit] the offset grows exactly linearly while it plays —
431
+ // 1.33e4 at 60 s, 5.33e4 at 240 s, 1.33e5 at 600 s — and A10's walk of
432
+ // max(duration, 1) s sees a finite number and passes it.
433
+ //
434
+ // 🔑 **"Every time it plays", not "looped".** Whether an animation loops is
435
+ // not in the artifact: skeleton JSON has no loop flag, the motion spec's
436
+ // `loop` is a player hint the compiler never reads (`MotionAnimation.loop`),
437
+ // and an editor export carries neither — and when it plays, and for how long,
438
+ // is the consumer's. The criterion that needs none of that is the one the
439
+ // runtime makes true either way: looped, the animation never leaves its
440
+ // values; played once, a track holds its last frame, which is 0. So the
441
+ // clause reads the timeline alone — every key and every Bézier sample, as
442
+ // the pose field it lands in, and the first key at t = 0, since before a
443
+ // later first key the timeline poses the setup value, which the strength row
444
+ // holds above 0.
445
+ //
446
+ // Per animation and constraint: when two strength timelines reach one
447
+ // constraint in one animation (a named one and the global one), it is held
448
+ // only if both are — a timeline that lifts it is a later key for the other.
449
+ // A constraint driving nothing is named by the first clause, and one resting
450
+ // at strength 0 or below by its row, so neither is read here.
451
+ const strengthRule = physicsRuleFor('strength');
452
+ const heldAtZero = new Map<string, Map<number, { keys: number; held: boolean }>>();
453
+ for (const timeline of facts.timelines) {
454
+ if (timeline.kind !== 'physics' || timeline.word !== 'strength') continue;
455
+ const first = timeline.frames[0];
456
+ const posed = (value: number): number => (timeline.posed === undefined ? value : timeline.posed(value));
457
+ const held = first !== undefined && first.time <= 0 && timeline.channelValues(0).every((value) => posed(value) === 0);
458
+ const byConstraint = heldAtZero.get(timeline.animation) ?? new Map<number, { keys: number; held: boolean }>();
459
+ heldAtZero.set(timeline.animation, byConstraint);
460
+ for (const index of timeline.reach) {
461
+ const was = byConstraint.get(index);
462
+ byConstraint.set(index, { keys: (was?.keys ?? 0) + timeline.frames.length, held: (was?.held ?? true) && held });
463
+ }
464
+ }
465
+ for (const [animation, byConstraint] of heldAtZero) {
466
+ for (const [index, { keys, held }] of byConstraint) {
467
+ const constraint = facts.constraints[index];
468
+ const physics = constraint?.physics;
469
+ if (!held || constraint === undefined || physics === undefined || strengthRule === undefined) continue;
470
+ if (!PHYSICS_COMPONENTS.some((k) => physics.components[k] > 0) || !strengthRule.poseOk(physics.setup.strength)) continue;
471
+ fail(
472
+ 'A23_PHYSICS_CONSTRAINT_EFFECTIVE',
473
+ `animation "${animation}" poses physics "${constraint.name}" strength 0 at every time it plays — ${keys} key(s) from t=0, ` +
474
+ `every one 0 and no curve between them lifting it, so ${physicsOutsideSays(strengthRule, 0)}: a strength key of 0 is ` +
475
+ 'accepted for a span because a later key pulls the offset back, and this animation has none, so for as long as it ' +
476
+ 'plays, looped or held on its last frame, a push (wind, gravity, the bone moving) carries the offset away and it ' +
477
+ 'grows without limit; key strength above 0 somewhere in the animation, or remove the timeline',
478
+ );
479
+ }
480
+ }
419
481
  }
420
482
 
421
483
  /** The three timelines whose values the stability clause combines. */
@@ -453,10 +515,23 @@ function plain(value: number): string {
453
515
  * The stability clause's sentence: the four authored numbers, `k` and the bound
454
516
  * as the runtime forms them, what the step does at that pose, and the four
455
517
  * levers that move it back under the bound.
518
+ *
519
+ * ⚠️ One pose fails with `k` under the bound: a damping the row accepts — below
520
+ * 1 — whose decay `damping ** (60 * step)` the runtime's doubles round to exactly
521
+ * 1 (issue #1422: the largest double below 1 does from fps 121 on). That is the
522
+ * damping row's own fault, a velocity that never decays, reached through the
523
+ * rate, so it says so rather than printing a `k` that is under its bound.
456
524
  */
457
525
  function runawaySays(combo: StabilityCombo, step: number, at: PhysicsStepStability): string {
458
526
  const src = (c: Candidate): string => (c.from === 'setup' ? '' : ` (${c.from})`);
459
527
  const fps = plain(1 / step);
528
+ if (!(at.d < 1)) {
529
+ return (
530
+ `damping ${combo.damping.value}${src(combo.damping)} at fps ${fps} — damping^(60·step) = ${at.d} as the runtime forms it, ` +
531
+ 'so although the damping is below 1 no velocity ever decays: the step\'s eigenvalues lie on the unit circle, the fault ' +
532
+ 'the damping row refuses 1 for, reached here by rounding; lower damping or lower fps'
533
+ );
534
+ }
460
535
  const places = apart(at.k, at.bound, 4);
461
536
  return (
462
537
  `strength ${combo.strength.value}${src(combo.strength)}, mass ${plain(1 / combo.massInverse.value)} (massInverse ${combo.massInverse.value})${src(combo.massInverse)}, ` +