spine-rigc 2.54.0 → 2.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -246,8 +246,12 @@ 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** |
250
+ | `--angle-range` | `render` only: `<bone>:<relative-to>[,<bone>:<relative-to>…]` — also read, for each pair, the range of the first bone's world direction against the second's over each set's frames, and print one line per pair per set: the lowest and the highest angle in degrees, the span, and the frame each was first reached on. A reading, not a gate: nothing is written, and every file is the bytes the render writes without it. An unknown bone (with the bones there are), a bone against itself, a malformed list and a pair named twice are refused by name, and so is a bone with no direction on a frame — **§8.7** |
251
+ | `--viewport`, `--viewport-from` | `render`: give the box every frame is drawn over instead of fitting one — `--viewport <x,y,w,h>`, a world box, y up, `x,y` its bottom-left corner, at the pixel size `--max` gives; or `--viewport-from <frames.json \| dir>`, an earlier render's grid exactly (its box, scale and pixel size), so two candidates land on one pixel grid. A slot that reaches past the box is named by set and frame, and `frames.json` records the box was given under `pinned`. The two together are refused, and so is `--max` beside `--viewport-from` — **§8.6**. On `check` and `inspect`, `--viewport` pins the box those commands draw over instead |
249
252
  | `--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
253
  | `--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 |
254
+ | `--lever-fold` | `explain` only: a fraction above 0 and at most 1. Also read, on the turn lines' walk, the steps where a physics constraint that drives `scaleX` has its inertia lever divide by less than this fraction of the divisor it takes at the setup pose, and the steps where the scale factor it applies moves by more than the bone's setup scale. **No default**: without it the reading is not taken and one line says so; a value that is not such a fraction is refused by name (exit 2). A reading, not a gate — **§4.6.2** |
251
255
  | `--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` |
252
256
  | `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
253
257
  | `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
@@ -267,7 +271,8 @@ What the flags mean:
267
271
 
268
272
  `render` also takes `--fps <n>` (the rate it samples at, default 12 — the same
269
273
  protocol rate the reference frames use) and `--max <px>` (the long side of a
270
- frame, default 256). A clipping attachment is applied as the runtime applies it —
274
+ frame, default 256). A set drawn at any rate but the default is written to
275
+ `<anim>@<fps>fps/` rather than `<anim>/`, so two rates sit side by side: `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. A clipping attachment is applied as the runtime applies it —
271
276
  spine-core's own `SkeletonClipping`, in spine-webgl's call order, on a Spine export,
272
277
  and the core's clipper, drawing the same pixels, on a rigc build (the `--poser`
273
278
  row) — so a slot from
@@ -1115,6 +1120,9 @@ bun cli.ts render --candidate path/to/spine [--animation …] [--skin …] [--
1115
1120
  bun cli.ts render --candidate path/to/spine --slot <name,…> | --hide <name,…> # part of the rig, same grid
1116
1121
  bun cli.ts render --candidate path/to/spine --geometry # + geometry.json per set: the numbers the frames were drawn from
1117
1122
  bun cli.ts render --candidate path/to/spine --folds # + folds.json per set: which mesh triangles turn over, and when
1123
+ bun cli.ts render --candidate path/to/spine --pass-through skirt:leg # + pass-through.json: where a cloth goes through a leg
1124
+ bun cli.ts render --candidate path/to/spine --angle-range tail:hip # how far a bone turns against another, per animation
1125
+ bun cli.ts render --candidate path/to/spine --viewport-from render/ # an earlier render's grid: two builds, one pixel grid
1118
1126
  bun cli.ts preview --candidate path/to/spine [--candidate path/to/another …] [--animation …] [--out preview.html]
1119
1127
  bun cli.ts vote --candidate path/to/a --candidate path/to/b [--out ballot.html]
1120
1128
  bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl]
@@ -1127,8 +1135,13 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
1127
1135
  key with the curve kind. A rig with a `deform` timeline also gets the **`DEFORM`
1128
1136
  block** — per key, what that key did to the geometry: the area and stretch
1129
1137
  extremes, how far each vertex moved and whether the winding survived
1130
- (**§4.11.2**). It takes no `--profile`, it never gates, and it does not write
1131
- anything — so the figures are readable on a build the gate is refusing.
1138
+ (**§4.11.2**). A rig with a physics constraint also gets the **turn lines** —
1139
+ per constraint and animation, the largest turn its bone makes in one 1/60 s step,
1140
+ walked from the setup pose as a player starts it (**§4.6.1**) — and, given
1141
+ `--lever-fold <fraction>`, the **lever lines**: the steps a `scaleX` drive's
1142
+ inertia lever divides by a collapsed length (**§4.6.2**). It takes no
1143
+ `--profile`, it never gates, and it does not write anything — so the figures are
1144
+ readable on a build the gate is refusing.
1132
1145
 
1133
1146
  ⭐ **It reads what `ingest` writes, given the pack** — `--atlas-in <pack.atlas>`,
1134
1147
  with `build`'s meaning (§0.2). That matters more here than it looks: printing the
@@ -4311,6 +4324,97 @@ physics, no skin set. `A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS` holds it (§5.2)
4311
4324
  declaration the profile did not read would be one nothing reads. The
4312
4325
  declaration is a statement to the gate and changes no emitted Spine byte.
4313
4326
 
4327
+ ### 3.8 `stops` — a swinging chain held off a leg
4328
+
4329
+ Spine has no collider and no angle limit — a physics constraint's `limit` is a
4330
+ velocity cap — so a skirt chain that physics swings carries its hem straight
4331
+ through the thigh it hangs in front of, on a build that passes every assertion.
4332
+ `render --pass-through` (§8.5) names those frames. A **stop** is the stand-in:
4333
+ a point on the leg, a frame that aims at it from the chain's root, the swinging
4334
+ link's rotation copied onto that frame, and the **drawn** link — the bone the
4335
+ mesh is weighted to — held by a transform clamp, so it may swing away from the
4336
+ leg and not past its rest angle toward it. One entry here is that network,
4337
+ written once:
4338
+
4339
+ ```json
4340
+ "stops": [
4341
+ {
4342
+ "chain": "hem",
4343
+ "links": ["hem0", "hem1"],
4344
+ "against": "thigh",
4345
+ "at": [-10, -84],
4346
+ "inward": 0,
4347
+ "outward": 60,
4348
+ "lower": { "inward": 0, "outward": 30 }
4349
+ }
4350
+ ]
4351
+ ```
4352
+
4353
+ | Field | What it is |
4354
+ | --- | --- |
4355
+ | `chain` | the name the point, the frame and the aim are called after |
4356
+ | `links` | the chain's swinging links, root first — each written by a constraint in the rig spec's `constraints` (its physics, or whatever swings it), each one below the first hanging from the drawn link of the one above |
4357
+ | `against` | the bone the point rides on — the leg |
4358
+ | `at` | the point, `[x, y]` in `against`'s local space, as a bone's `x`/`y` are in its parent's. Put it on the leg's outer edge at the hem's height |
4359
+ | `inward`, `outward` | the first drawn link's range, as its two ends, in degrees from its rest angle, counted **positive away from the point**. `inward: 0, outward: 60` is "never past rest toward the leg, up to 60° away"; `inward: -5` lets it turn 5° past rest toward the leg |
4360
+ | `lower` | `{ "inward", "outward" }`: each lower drawn link's range off its own rest bend, the same way round. Required when `links` has more than one, refused when it has one |
4361
+
4362
+ Every field is required and none has a default. **Inward** is the turn from the
4363
+ first link toward the aim line — toward the point, which is on the leg — read off
4364
+ the setup pose, so a stop on the left side of a skirt and one on the right need no
4365
+ sign of their own.
4366
+
4367
+ **What it expands to.** Before anything else reads the rig, `parseRigSpec`
4368
+ replaces the entry with bones and constraints — nothing downstream knows a stop
4369
+ was there, and the build is byte for byte the build of the same network written
4370
+ by hand (`LG01`):
4371
+
4372
+ | Generated | Kind | What it is |
4373
+ | --- | --- | --- |
4374
+ | `<chain>_touch` | bone | the point: parent `against`, `x`/`y` from `at`. Listed after every other bone |
4375
+ | `<chain>_stop` | bone | the frame: the first link's parent, `x`, `y` and `length`, rotated at the point. Listed just before the first link |
4376
+ | `<link>_rel` | bone | a child of the frame, `length` the link's, rotated by the link's rest angle off the aim line. Just after the first link |
4377
+ | `<link>_d` | bone | the first link's **drawn** link — the same as `_rel`, just after it. For each lower link, its drawn link: a sibling with the link's own `length`, `x`, `y`, `rotation`, scale, shear and `inherit`, just after that link |
4378
+ | `<chain>_stop_aim` | ik | aims the frame at the point (`mix` 1). Just before the last constraint that writes the first link |
4379
+ | `<link>_rel_copy` | transform | gives `_rel` the first link's world rotation (`mixRotate` 1). Just after that constraint |
4380
+ | `<link>_clamp` | transform | local to local, `clamp: true`: gives the drawn link its source's turn, held to the range — the first link's from `_rel`, each lower link's from the link. Just after `_rel_copy` for the first, just after the last constraint that writes the link for a lower one |
4381
+
4382
+ So a chain with a stop is authored with each link's parent below the drawn link
4383
+ above it — `hem1`'s control bone under `hem0_d` — and its mesh weighted to the
4384
+ `_d` bones; those names are the expansion's, and the build refuses a name it
4385
+ generates that the rig already declares. The order is the point of the
4386
+ placement: the frame is aimed before the link swings, and each swing is copied
4387
+ and clamped before anything below reads the drawn link.
4388
+
4389
+ The frame's angles are read off the setup pose the rig spec states — its bones as
4390
+ written, no constraint applied, in the exact frame `src/transform.ts` measures in — so at rest the drawn
4391
+ link lies on the link and the clamp's bounds are its rest angle moved by
4392
+ `inward` and `outward`. A bone the angles are read through that takes its place
4393
+ `from` a manifest is refused: its numbers are not known until the compile.
4394
+
4395
+ ⚠️ **The link's swing has to be declared in the rig spec.** The motion spec's
4396
+ `physics` table (§4.6) is emitted after every rig constraint, past the copy and
4397
+ the clamp, which would then read the link before it swings; a link only that
4398
+ table drives is refused as one nothing swings, and a table entry on a stopped link
4399
+ is refused by name.
4400
+
4401
+ Refused, each naming the stop, the field and the value (§5.1): a missing
4402
+ `chain`, `links`, `against`, `at`, `inward`, `outward` or (on a chain of two or
4403
+ more) `lower`; a `lower` on one link; a link or an `against` the rig does not
4404
+ declare; a link stopped twice; the root as a link; `at` that is not two numbers;
4405
+ `inward` above `outward`, at either range; a bound past ±180° once the rest angle
4406
+ is added (the clamp reads a local rotation in (−180, 180]); a generated bone or
4407
+ constraint name already declared, or generated by two stops; a link no rig
4408
+ constraint writes; a lower link not below the drawn link above it; an `against`
4409
+ that hangs below the chain; a point on the link's root or on its own line at
4410
+ rest; a frame parent scaled unevenly, sheared or reflected, or such a bone
4411
+ between two links; a link below a bone a later stop generates; a `from` bone in
4412
+ the angles' path.
4413
+
4414
+ The stop decides nothing about *when* — it is what the object can do. Read it
4415
+ with `render --pass-through <cloth>:<leg>` (§8.5) on the motion that swung
4416
+ through before.
4417
+
4314
4418
  ---
4315
4419
 
4316
4420
  ## 4. The motion spec, field by field
@@ -4543,7 +4647,7 @@ a delta from the constraint's own setting.
4543
4647
  infinite inverse mass — every velocity and offset is NaN from the first step.
4544
4648
  - **Four of the seven are bounded, and a key outside its bound is a compile
4545
4649
  error.** `mass` must be
4546
- `> 0`, `damping` must be inside the closed `[0, 1]`, and `mix` and `strength`
4650
+ `> 0`, `damping` must be inside the half-open `[0, 1)`, and `mix` and `strength`
4547
4651
  must be `0` or more. `A23_PHYSICS_CONSTRAINT_EFFECTIVE` applies the same four to
4548
4652
  a file rigc did not write, naming the animation, the constraint, the key time
4549
4653
  and the value — so the compiler is where a spec you wrote is refused, and the
@@ -4554,14 +4658,14 @@ a delta from the constraint's own setting.
4554
4658
  non-finite at it — and the sentence names the expression and its lines;
4555
4659
  **behavioural** means the runtime runs it, finitely, and the rig runs wrongly,
4556
4660
  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.
4661
+ does. Two of the nine ways out are arithmetic.
4558
4662
 
4559
4663
  | field | at rest | on a key | basis |
4560
4664
  | --- | --- | --- | --- |
4561
4665
  | `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
4666
  | `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
4667
  | `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`) |
4668
+ | `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
4669
 
4566
4670
  ⚠️ The line between the two is **non-finite within the walk that measures
4567
4671
  it**, not "non-finite eventually": [measured] on the generated physics
@@ -4588,19 +4692,31 @@ a delta from the constraint's own setting.
4588
4692
  the key at 45 and at 120. Above `1` every velocity grows on every step and
4589
4693
  diverges at every rate. The refusal names the exponent and the rate for this
4590
4694
  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`.
4695
+ - ⚖️ **`0` is inside the bound and `1` is not.** `0 ** x` is 0 and `1 ** x` is
4696
+ 1 for every positive exponent, so neither depends on `fps`. At `0` every
4697
+ velocity is zeroed on every step, so the offset follows the bone with no
4698
+ overshoot — a rig. At `1` the velocity never decays, and both are finite, at
4699
+ rest and keyed — [measured] 120 steps from `Physics.reset` at 60, 45 and 30 fps
4700
+ on the generated physics fixture — which is why `1`'s refusal is behavioural.
4701
+ It was inside from issue #794 until #1422, as a jiggle held for as long as it
4702
+ runs. What that missed: a decay of 1 puts the step's two eigenvalues **on** the
4703
+ unit circle (§4.6), so nothing a push puts in ever dies out, and a chain of such
4704
+ links resonates. [measured on #1422, x drive, inertia 1, fps 30, strength 100,
4705
+ the runtime's default world, 600 s] one link held its peak offset at 40.3; two
4706
+ identical links on one chain grew it linearly, 6.03e3 at 60 s to 6.09e4 at
4707
+ 600 s; three passed 10⁶ at 316 s. Detuning the second link's strength by 1 %
4708
+ held it flat — equal maps are the edge, and no authored number says how close
4709
+ is too close, so the value is refused rather than the pair. Lower it to below
4710
+ 1 — `0.999999` is accepted. ⚠️ `1` is also what a 4.2 file means by saying
4711
+ nothing: 4.2's parser reads an omitted `damping` as `1` where 4.3's reads
4712
+ `0.85` (§4.6), so a rig migrated from 4.2 that keys "the default" keys `1`, and
4713
+ is refused.
4599
4714
  - 🚫 **`inertia`, `wind` and `gravity` are bounded nowhere, and neither is the top
4600
4715
  of `mix`.** The runtime documents no range for the first three, and
4601
4716
  `PhysicsConstraintPose` documents `mix` as "a percentage (0+)" — so a negative
4602
4717
  wind is the other direction and a `mix` of `1.5` is an over-mix. Both are real
4603
- and both compile.
4718
+ and both compile. An `inertia` above 1 is kept and posed by the editor too, and
4719
+ what it does to a bone is read rather than refused — §4.6.1.
4604
4720
  - ⚠️ **A `mix` or `strength` key of exactly `0` is legal where a setup value of
4605
4721
  `0` is not**, and the difference is not an inconsistency: a setup pose says what
4606
4722
  the constraint IS and a key says what it is doing for a stretch. For `mix`,
@@ -4609,7 +4725,14 @@ a delta from the constraint's own setting.
4609
4725
  `strength`, 0 takes the restoring term out of the velocity update and leaves
4610
4726
  `damping` and `inertia` applied — the offset is not pulled back *while the key
4611
4727
  holds*, which is "physics released" for that span, and the next key pulls it
4612
- back. At rest the two
4728
+ back. ⚠️ So there has to **be** a next key: an animation whose `strength`
4729
+ poses `0` at every time it plays — every key `0` from `t = 0` and no Bézier
4730
+ lifting it between them — is refused by `A23` (issue #1422). Whether the
4731
+ animation loops is not in the file, and does not matter: looped it never leaves
4732
+ 0, played once its track holds the last frame, which is 0. [measured on #1422,
4733
+ x drive, the runtime's default world] the offset grows exactly linearly while
4734
+ it plays — 1.33e4 at 60 s, 1.33e5 at 600 s. A first key after `t = 0` is a
4735
+ span: before it the timeline poses the setup `strength`, which pulls back. At rest the two
4613
4736
  part ways: a `strength` of 0 is a constraint nothing pulls back, which `A23`
4614
4737
  refuses, and a `mix` of 0 is a constraint muted until an animation keys it
4615
4738
  above 0 — the next bullet. A **negative** setup `strength` is refused too, with
@@ -4981,14 +5104,16 @@ key times. One lag, one place.
4981
5104
  mass?, wind?, gravity?, mix?, fps?, limit? }`. These are emitted into the 4.3
4982
5105
  `constraints` array. Seven of them — the six tuning numbers and `mix` — can also
4983
5106
  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
5107
+ 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
5108
  never settles — both are `A23`, here and on every timeline key that states them;
4986
5109
  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.
5110
+ §4.4. `damping` is `[0, 1)`: `0` zeroes the velocity on every step at every rate
5111
+ and is in, and `1` never decays — its step sits on the unit circle, below — and is
5112
+ out since issue #1422 (it was in from #794).
4989
5113
  ⚠️ An omitted `damping` is `0.85` to 4.3's parser (`SkeletonJson.js:308` in
4990
5114
  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**:
5115
+ migrated from 4.2 that stated nothing may arrive stating `1` — which `A23` refuses;
5116
+ state the damping you mean, below 1. ⚠️ `strength: 0` is `A23` **here and not on a key**:
4992
5117
  at rest it is a constraint nothing pulls back, and on a key it is a release somebody
4993
5118
  asked for, which §4.4 states with the measurement behind it. A negative `strength` is
4994
5119
  `A23` here and on a key, and here its sentence is its own: the offset is pushed
@@ -5001,6 +5126,21 @@ file can be read; it is a **key** outside its bound that `build` refuses (§4.4)
5001
5126
  number: a non-number would reach the file as `null`, which the runtime reads as
5002
5127
  **zero**, so `"mass": "heavy"` would ship a constraint that never settles.
5003
5128
 
5129
+ 🌀 **`inertia` has no upper bound, and above 1 a link can whirl** (issue #1423).
5130
+ Values above 1 are kept by the editor and posed: the Spine 4.3.26 editor imports
5131
+ 2, 3 and 10 and exports them unchanged, the export posing as the build does, and
5132
+ the user guide describes the field as 0–100% with no cap stated. So `build`
5133
+ refuses none of them. What one does is a turn: a bone whose parent moves is kicked
5134
+ by `inertia` times that motion, and a 1-link rotate drive under a parent keyed
5135
+ ±15° turns its bone at most 2.29–4.10° (by its `fps`), 18.78°, 35.61° and 153.38°
5136
+ in a single 1/60 s step at `inertia` 1, 2, 3 and 10 — the last half a turn between
5137
+ two frames. **`explain`
5138
+ prints that turn for every physics constraint and every animation** (§4.6.1), so a
5139
+ whirl is a figure you read rather than a frame you happen to watch. On a `scaleX`
5140
+ drive, a high `inertia` down a chain can also make a link's lever divide by a
5141
+ length near zero and throw its scale offset by hundreds in one step — `explain
5142
+ --lever-fold <fraction>` names those steps (§4.6.2).
5143
+
5004
5144
  🧮 **Each value inside its bound is not enough: the step has to settle**
5005
5145
  (issue #1416). With `step = 1 / fps`, a constraint that drives anything must
5006
5146
  satisfy
@@ -5008,9 +5148,16 @@ satisfy
5008
5148
  strength · massInverse · step² < 2 · (1 + damping^(60 · step))
5009
5149
 
5010
5150
  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
5151
+ linear map whose determinant is `damping^(60 · step)`, and for that decay below 1
5152
+ its two eigenvalues lie inside the unit circle exactly then; at or past it the
5153
+ offset never dies out, and past it any push — wind, gravity, a key, the bone
5154
+ moving — grows without limit. ⚠️ At a decay of exactly 1 no `strength` settles:
5155
+ the determinant is 1, so the pair has modulus 1 and sits **on** the circle — the
5156
+ reason `damping` 1 is refused (§4.4), and this guide said otherwise until issue
5157
+ #1422. The decay is the runtime's own double, so a `damping` a hair below 1 can
5158
+ reach it at a high rate — [measured] the largest double below 1 does from
5159
+ `fps: 121` on — and `A23` names that case by the decay:
5160
+ `` 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
5161
  past the line ran past 10⁶ units under a unit gust (most with no gust at all),
5015
5162
  all 145 under it stayed bounded, and the 10 exactly on it were still growing or
5016
5163
  swinging their tip the bone's full length when a 6 s walk ended, spine-core
@@ -5026,9 +5173,19 @@ joint is an upper bound: an animation that raises strength and mass together is
5026
5173
  judged at its stiffest strength against its lightest mass. **To fix it, lower
5027
5174
  `strength`, raise `mass`, raise `fps` or raise `damping`** — the first three move
5028
5175
  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.
5176
+ keeps more velocity per step, yet it is what widens the bound: from 2 at `0`
5177
+ toward 4 as it nears `1`, which is itself out); the refusal prints both sides so
5178
+ you can see how far each lever has to go.
5179
+
5180
+ 📏 **What a value does to a link is read off a render, not estimated.** The bound
5181
+ above says whether a spring settles; it does not say how far it swings, which
5182
+ depends on the link's length, its parent's motion and every value together. Render
5183
+ the animation with `--angle-range <link>:<parent>` (§8.7) and the line prints how
5184
+ far the link turns against the bone it hangs from — the lowest and the highest
5185
+ angle and the frames they fall on — so a change of `inertia` or `strength` is read
5186
+ as the change of that range, before and after. On the selftest's angle probe a
5187
+ link's span widens when its `inertia` is raised from 0.2 to 0.8, and `AR02` prints
5188
+ both spans.
5032
5189
 
5033
5190
  ⚠️ **What `rigc render` draws a spring with is not a player's clock to the bit.** A
5034
5191
  constraint integrates whole steps of `1 / fps` and shows the rest as a lag weight, so
@@ -5097,6 +5254,134 @@ skeleton JSON, so the parser is the only check it gets: it is
5097
5254
  `default` that is not a number would pass the gate and the round trip before
5098
5255
  becoming a `NaN` mix duration in the player.
5099
5256
 
5257
+ #### 4.6.1 How far a physics bone turns in one step — `explain`'s turn lines
5258
+
5259
+ Under its `physics constraints` block, `explain` prints one line per physics
5260
+ constraint per animation: the largest turn the constraint's bone makes in one
5261
+ step of the animation's walk, the step it happens at and its time.
5262
+
5263
+ ```
5264
+ turn per step (a reading, never a gate: each animation walked by rigc's core as a player starts it — …)
5265
+ .. turn "swing": physics "p_i10_f60" (bone "l_i10_f60") turns at most 153.38° in one step — step 1 of 240 (t=0.0167s)
5266
+ .. turn "sweep": physics "whip" (bone "ahoge_whip") turns at most 0.29° in one step — step 90 of 193 (t=1.5000s)
5267
+ ```
5268
+
5269
+ **The walk.** Each animation is walked as a player that starts it plays it: a
5270
+ skeleton with no skin set, at the setup pose with every physics state reset there,
5271
+ then the animation in steps of 1/60 s until its duration (one step at least) — the
5272
+ physics stepped by the step itself, as spine-core's `update(1/60)` steps it. It is
5273
+ posed by rigc's core; the repository's selftest walks the same probe through
5274
+ spine-core and holds the two to the same figure and step, to the bit (`WH01`).
5275
+
5276
+ ⚠️ **Not `render`'s walk, on purpose.** `render` resets the physics at the
5277
+ animation's first pose, so a bone the first key moves away from the setup pose is
5278
+ already there when the physics starts. A player starts from the setup pose, and the
5279
+ first step moves every keyed bone at once — which is where a high `inertia` answers
5280
+ hardest: on the probe above, inertia 10 turns 153.38° in step 1 from the setup pose,
5281
+ and walked from the animation's first pose at most 4.10°, 3.82° and 13.68° at `fps`
5282
+ 15, 30 and 60. The whirl is a property of
5283
+ starting the animation, so it is read where a player starts it.
5284
+
5285
+ **The turn** is the signed angle between the bone's world x axis at two consecutive
5286
+ steps, in (−180°, 180°], printed as its magnitude to two decimals; the largest over
5287
+ the walk, the earliest step on a tie. It is the change of the direction the bone is
5288
+ drawn along, whatever moved it — its physics, its parent, a key, a mirrored scale —
5289
+ so every physics constraint has a line, whichever components it drives. A turn near
5290
+ 180° in one step is a direction a frame cannot tell from its opposite. A bone that
5291
+ never turns reads `does not turn in any of N step(s)`.
5292
+
5293
+ **Not read, and said so:** a skin-required constraint (`not read — skin-required,
5294
+ and a skeleton with no skin set does not step it`); an animation a slider applies —
5295
+ a table posed at a value the consumer holds, not a span of time (`A39`'s reading) —
5296
+ whose line names the slider, while the animation that moves the slider's value is
5297
+ walked; a walk the core refuses to pose (`not read — the core refused to pose the
5298
+ walk — …`); a bone whose world direction stops being a number (`not read — its
5299
+ bone's world direction is not a number from step k (t=…s)`), rather than the finite
5300
+ figure it reached before (`WH04`); and a skeleton with no animation.
5301
+
5302
+ **A reading, not a gate.** Nothing in it is refused, and `explain`'s exit is the
5303
+ same whatever it prints — `src/whirl.ts`, which computes it, is imported by
5304
+ `explain` alone (`WH02`). Whether half a turn per frame is the motion you meant is
5305
+ yours: a link that should trail its parent wants an `inertia` at or under 1, and
5306
+ the figure is how you see the difference without watching the right frame. A walk
5307
+ that ignores the parent's motion — the turn read in the parent's frame, or
5308
+ `render`'s walk — misses the figures above, and the selftest plants both to show it
5309
+ (`WH03`).
5310
+
5311
+ #### 4.6.2 Where a `scaleX` lever divides by a collapsed length — `explain --lever-fold`
5312
+
5313
+ A physics constraint that drives `scaleX` kicks its scale offset by its bone's
5314
+ motion **divided by a length**: `scaleOffset += (dx·cos + dy·sin) · inertia / rr`.
5315
+ `limit` caps `dx` and `dy`; nothing caps the division, and the kick is skipped
5316
+ only when `rr` is not above 0. `rr` is the bone's `length` times its world x-axis
5317
+ length (`√(a² + c²)` of the world matrix the step is handed) — and, on a drive
5318
+ that does not also rotate (`rotate` and `shearX` both 0), **less `scaleLag ·
5319
+ shown`**, the last integration's move of the offset weighted by how much of it was
5320
+ shown. When that divisor collapses, the next kick is enormous, children inherit
5321
+ the scale, and the lengths multiply down a chain: [measured] on a 5-link chain
5322
+ driven on `scaleX` alone at `inertia` 10, `fps` 15 and 0.95 of the step bound,
5323
+ link 3's offset jumps 24.3 → 349 in one step and the tip passes 10⁶ units with
5324
+ every assertion green (issue #1422). Whether a chain gets there depends on how far
5325
+ an offset swings, which no number in the spec states — so `build` refuses nothing
5326
+ for it, and `explain` reads it when you ask:
5327
+
5328
+ ```
5329
+ lever under 0.5× setup (a reading, never a gate: the turn lines' walk; …)
5330
+ .. lever "quiet": physics "p_v_1" (bone "v_1") reads nothing in 240 step(s): no lever divides by less than 0.5× its setup divisor 50.00, and no step moves its scale factor by its setup scale
5331
+ .. lever "quiet": physics "p_v_3" (bone "v_3") lever divides by less than 0.5× its setup divisor 50.00 on 8 of 240 step(s) [8, 17, 28, 56, 83-84, 92, 99] — least 2.940 (0.05880× setup; world x axis 0.5636× setup) at step 17 (t=0.2833s)
5332
+ .. lever "quiet": physics "p_v_3" (bone "v_3") scale factor moves by more than its setup scale in one step on 73 of 240 step(s) [5-13, …] — largest 1268, scale offset -616.8 → 651.4 at step 25 (t=0.4167s)
5333
+ ```
5334
+
5335
+ **The walk** is the turn lines' (§4.6.1): from the setup pose with every physics
5336
+ state reset, in steps of 1/60 s to the duration, posed by rigc's core. The
5337
+ repository's selftest walks the same probe through spine-core and holds every
5338
+ step and figure to the core's, to the bit (`LV01`).
5339
+
5340
+ **What it names**, per animation, for every constraint that drives `scaleX`:
5341
+
5342
+ - **a collapsed division** — a step where the lever divides (`rr` above 0) by less
5343
+ than `--lever-fold` × the divisor it takes at the setup pose (`length` × the
5344
+ world x-axis length there). A lever divides on a step where the constraint is
5345
+ not pending (the first update after a reset integrates nothing), its `mix` is
5346
+ not 0 and its `inertia` is not 0. The line gives the steps as runs and the
5347
+ least divisor, as that fraction, with the world x-axis length the lever read on
5348
+ that step beside it.
5349
+ - **a jump** — a step where the scale factor the constraint applies, `1 +
5350
+ scaleOffset · mix · scaleX`, moves by more than 1: by more than the bone's own
5351
+ setup scale, since the factor is 1 at the setup pose and multiplies whatever
5352
+ scale the bone already has. The line gives the steps and the largest move,
5353
+ with the offset on either side of it.
5354
+
5355
+ ⚠️ **The divisor, not the world length alone.** On the step above, link 3's lever
5356
+ reads its world x axis at 0.5636 of its setup length — not near zero — and divides
5357
+ by 0.0588 of its setup divisor, because `scaleLag · shown` (25.24) takes almost all
5358
+ of `length · |world x axis|` (28.18) away. A reading of the world length alone misses
5359
+ that step at every fraction below 0.5636, and names steps where the kick is skipped
5360
+ altogether: the same link reads 0.481 of its setup length three steps earlier while
5361
+ its divisor is −8.47, so nothing is divided (`LV04`). On a drive that rotates too
5362
+ the two are one number. A reading of the parent's **local** scale sees none of it —
5363
+ physics moves the world matrix, never a local transform (`LV03`).
5364
+
5365
+ **The fraction is yours, stated.** `--lever-fold` has no default: what counts as
5366
+ collapsed is not a number the spec carries. Without the flag `explain` prints one
5367
+ line saying the reading was not taken (`` not read — `--lever-fold <fraction>` … ``);
5368
+ a value that is not a number above 0 and at most 1 is refused by name, exit 2
5369
+ (`LV05`).
5370
+
5371
+ **Not read, and said so:** constraints that drive no `scaleX`, on one line —
5372
+ no lever of theirs divides by a length, which is what the gallery's and the
5373
+ public fixtures' physics read (`LV02`); and, per constraint, the cases §4.6.1
5374
+ lists — skin-required, an animation a slider applies, a walk the core refuses — plus
5375
+ a divisor, world length or offset that stops being a number (`not read — … is not
5376
+ a number from step k`), rather than the steps it named before.
5377
+
5378
+ **A reading, not a gate.** Nothing in it is refused, `explain`'s exit is the same
5379
+ whatever it prints, and the module that computes it is the turn lines' own
5380
+ (`src/whirl.ts`, imported by `explain` alone — `WH02`). A chain that collapses is
5381
+ yours to judge: an `inertia` at or under 1, a stiffness further from the bound, or
5382
+ `rotate` driven alongside (which drops the lag term from the divisor) are the
5383
+ levers you hold.
5384
+
5100
5385
  ### 4.7 `drawOrder` — reordering the slots over time
5101
5386
 
5102
5387
  The one timeline that names no target, so it sits on the animation rather than in
@@ -5598,7 +5883,9 @@ convention:
5598
5883
  an edit of part of it, so it starts at deform index 0 and runs to the end — which
5599
5884
  is why `fromVertex` and `offset` are refused beside it. A model applied to part
5600
5885
  of a run leaves a **step at the run's edge**. If you want a
5601
- partial run, write it.
5886
+ partial run, write it — or, to move part of the attachment by the model, give the
5887
+ key an `envelope` (below), which weighs every vertex rather than cutting the run,
5888
+ so there is no edge to step at.
5602
5889
 
5603
5890
  **There is no `parallax` kind, and the reason is worth stating.** A pure depth
5604
5891
  slide — `d = z · offset`, no angle — is this form with a term dropped: subtract
@@ -5662,7 +5949,10 @@ about where they came from. A `yaw` past its fold angle folds the mesh and A39
5662
5949
  says so — the construct removes the transcription, not the arithmetic's
5663
5950
  consequences. One kind is the exception and it is an exception with a proof:
5664
5951
  `affine` refuses a determinant at or below zero, and above zero a positive
5665
- determinant means no triangle **can** reverse.
5952
+ determinant means no triangle **can** reverse. ⚠️ The proof is about the
5953
+ un-enveloped key: under an `envelope` each vertex takes its own share of the map,
5954
+ so the keyed map is no longer affine, the `derived` line says so, and A39
5955
+ measures it like any other.
5666
5956
 
5667
5957
  **A model that evaluates to nothing is refused too.** Every parameter can be
5668
5958
  individually legal and the model still come out as a **run of zeros** — a `wave`
@@ -5729,6 +6019,83 @@ the same turn, where the missing number is a depth rather than a vertex, take a
5729
6019
  per-key `DEFORM` report block quotes the model above rather than re-evaluating it
5730
6020
  — **§4.11.2**.
5731
6021
 
6022
+ ##### `envelope` — which vertices the model moves, and how much
6023
+
6024
+ A transform moves **every** vertex of the attachment, so a `wave` on a skirt
6025
+ ripples the waistband too and "only the hem ripples" cannot be written as the
6026
+ model alone. An `envelope` on the transform gives each vertex a **weight in
6027
+ [0, 1]**, and each vertex's offset is multiplied by its weight before it is
6028
+ written. Every kind takes one:
6029
+
6030
+ ```json
6031
+ { "t": 1, "transform": {
6032
+ "kind": "wave", "amplitude": 6, "wavelength": 64, "phase": 90, "along": "x", "axis": "y",
6033
+ "envelope": { "axis": [0, 1], "from": 224, "to": 120, "shape": "smooth" } } }
6034
+ ```
6035
+
6036
+ ⚠️ **These numbers are the public skirt probe's** (`fixtures/public.ts`,
6037
+ `skirtFixture`): its waist row sits at world `y` 224 and its hem rows at 120
6038
+ and below. On your part, `from` and `to` are positions you read off the part —
6039
+ `explain` prints where its vertices sit — and nothing here is a default.
6040
+
6041
+ It comes in two forms, and a key states exactly one:
6042
+
6043
+ | Field | Meaning |
6044
+ | --- | --- |
6045
+ | `axis` | **ramp, required.** The unit direction a vertex's position is read along, `[x, y]` — `[0, 1]` reads `y`, `[1, 0]` reads `x`, a diagonal reads across both. Its length is 1 on the model's 1e-6 grid |
6046
+ | `from` | **ramp, required.** The position along `axis` where the weight is **0** — and before which it stays 0 |
6047
+ | `to` | **ramp, required.** The position along `axis` where the weight is **1** — and past which it stays 1. Differs from `from`; either may be the larger |
6048
+ | `shape` | **ramp, required.** How the weight rises between them: `"linear"` is `u`, `"smooth"` is `u²(3 − 2u)`, flat at both ends — the two curves `influence.shape` names (§3.4.7), read rising rather than falling |
6049
+ | `mask` | **mask, required, and alone.** The file name of this attachment's own `soft` mask (§3.4.6). The weight is the level that mask was sampled to at the vertex when the mesh was built, black 0 and white 1 — the same number the soft region's binding carries |
6050
+
6051
+ For a ramp, `u = clamp((axis · position − from) / (to − from), 0, 1)` per
6052
+ vertex, and the weight is the shape at `u`. Weights are quantised onto the
6053
+ model's 1e-6 grid and each emitted offset is `round(w × the un-enveloped
6054
+ offset)` on the same grid — so a weight of 0 writes exactly 0, a weight of 1
6055
+ writes exactly the offset the key writes with no envelope, and a reader holding
6056
+ the weights `explain` prints reproduces every number.
6057
+
6058
+ ⭐ **The ramp is read in the space the model is.** The weight multiplies the
6059
+ offset the model produced at a vertex, so the position that picks it is the
6060
+ position the model read: the attachment's own space while every vertex is one
6061
+ pair of one bone, and setup **world** past that (above). `from` and `to`
6062
+ therefore change units exactly when `radius`, `about` and a bend's `from` do,
6063
+ and the `derived` line that says so applies to them too. A skirt weighted to
6064
+ its chains is on the world path, which is why the example's positions are world
6065
+ `y`.
6066
+
6067
+ 🔒 **It is baked, not played.** The weighted offsets are the deform table the
6068
+ file carries, so the runtime is asked to do nothing it did not already do:
6069
+ rigc's core and spine-core pose an enveloped key identically, and the gate
6070
+ judges it like any table — A35 the run, A39 the winding, `render --folds` the
6071
+ posed triangles. An envelope that rises over too short a distance can fold a
6072
+ triangle the model alone could not (a wave that reads `x` and displaces `y` is
6073
+ a shear and folds nothing), and A39 names it.
6074
+
6075
+ What is refused, each by the field and the value:
6076
+
6077
+ | You wrote | You get |
6078
+ | --- | --- |
6079
+ | `from` equal to `to` | `transform fields "envelope.from" and "envelope.to" are both 224; they are where the weight is 0 and where it is 1, so they differ` |
6080
+ | an `axis` that is not a unit direction | `transform field "envelope.axis" is [0, 2], whose length is 2; it is a unit direction, length 1 on the model's 1e-6 grid` |
6081
+ | a `shape` outside the two | `transform field "envelope.shape" is "cubic"; it is one of "linear", "smooth"` |
6082
+ | a ramp field missing | `transform field "envelope.from" is undefined; it is a finite number the spec has to state` |
6083
+ | a `mask` on an attachment with no `soft` block | `transform field "envelope.mask" is "skirt_soft.png", and this attachment has no "soft" block, so the mask names nothing` |
6084
+ | a `mask` naming another file than the attachment's soft mask | `… and this attachment's soft mask is "blob_soft.png", so the mask names nothing it was built with` |
6085
+ | a `mask` beside a ramp field | `transform field "envelope" states "mask" … beside the ramp's "axis", "from", "to", "shape". They are two answers to one question` |
6086
+ | an empty `envelope` | `transform field "envelope" is {}, which states no weight at all` |
6087
+ | a weight outside [0, 1] | `transform field "envelope.shape" weighs vertex 2 at 1.2; an envelope's weight is in [0, 1]` — unreachable from a spec (the ramp clamps, both shapes map [0, 1] onto itself, a mask is a sampled level), and kept because a weight past 1 would push a vertex past the model in silence |
6088
+ | a ramp that weighs every moving vertex 0 | `… and envelope axis=[0, 1] from=-16 to=-152 shape=smooth weighs every one of them 0 — so the key states a deformation and emits the identity`, with where the vertices sit along the axis |
6089
+ | a ramp the part never enters, so nothing changes | `… it changes no emitted number: every vertex the model moves takes weight 1` — a field a reader would credit for an output it did not shape |
6090
+
6091
+ The last two are the "evaluates to nothing" rule above, one step on, and a key
6092
+ whose model states the identity (`amplitude: 0`, …) is exempt from both — the
6093
+ envelope multiplies the zeros it already said.
6094
+
6095
+ `explain` prints the envelope under the model — its fields, its formula, how
6096
+ many vertices took 0 and 1, and every vertex's weight — and the `DEFORM` block's
6097
+ model line carries it as `envelope(…)` after the model's parameters.
6098
+
5732
6099
  ---
5733
6100
 
5734
6101
  #### 4.11.2 The `DEFORM` block — what a key does to the geometry
@@ -6480,6 +6847,22 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6480
6847
  | `ik constraint "X" names 3 bones ("a", "b", "c"); the solver applies one or two, so a constraint over 3 moves nothing — name one bone, or two where the second is the first's child` | §3.5 — the constraint's `bones` is what changes: one bone, or a parent and its child. The runtime's ik update has a case for one bone and one for two, so a longer list loads, sits in the update cache and applies nothing — [measured] every bone posed exactly where the same rig with no constraint has it. A longer chain is several constraints, each over one bone or a pair |
6481
6848
  | `ik constraint "X": "B" is not a child of "A" ("C" stands between), so the two-bone solve is not of the chain drawn — name "B"'s own parent as the first bone, or make "B" a child of "A"` (also `("A" is not above it; its parent is "P")`, `("B" has no parent)`) | §3.5 — the constraint's `bones` is what changes, or `B`'s `parent`. The two-bone solve places `B` through `A`'s matrix from `B`'s own local offset, so what stands between them is left out of the triangle it solves — [measured] a bone between them at `B`'s origin left the tip up to 2.60 off the target, and one at `A`'s origin landed only while it held its identity |
6482
6849
  | `physics constraint "X" is declared in both the rig spec and the motion spec's physics table` | §4.6 — the rig spec declares a physics constraint's structure and the motion spec's `physics` table declares one outright; pick the file it belongs in. Per kind, like every other constraint name: an `ik` "X" in the rig spec beside a `physics` "X" here is two constraints and is not this error |
6850
+ | `stops[i] needs a "chain" …` / `stop "C" states "links" …; it is the chain's swinging links, root first …` / `stop "C" needs "against" …` / `stop "C" states no "at" …` / `stop "C" states no "inward"` (or `"outward"`, `"lower.inward"`, `"lower.outward"`) / `stop "C" states no "lower" …` | §3.8 — state the field; a stop has no defaults. `at` is `[x, y]` in `against`'s local space, and `lower` is required on a chain of two or more links |
6851
+ | `stop "C" states "at" [V]; it is [x, y], two numbers in "A"'s local space` | §3.8 — two numbers |
6852
+ | `stop "C" states "lower" and its chain has one link …` | §3.8 — remove `lower`, or list the links below the first |
6853
+ | `stop "C": links[k] names bone "X", which this rig does not declare` / `stop "C": "against" names bone "X", which this rig does not declare` | §3.8 — fix the name; both resolve against the rig spec's own `bones` |
6854
+ | `stop "C": links[k] "X" is already stopped by stop "D" …` | §3.8 — a link has one drawn link and one clamp; stop it once |
6855
+ | `stop "C": links[k] "X" is the skeleton's root …` / `… takes its place from the manifest ("from") …` | §3.8 — a link hangs from a parent, and its drawn link copies its stated `x`, `y` and `rotation`; state them. The same `from` refusal names a bone the stop's angles are read through |
6856
+ | `stop "C": "inward" N is greater than "outward" M …` (also `"lower.inward"`) | §3.8 — the range is `[inward, outward]`, counted positive away from the point; `inward` is the lower end |
6857
+ | `stop "C": "outward" N puts "L_d"'s bound at B°, past ±180° …` (also `"inward"`, `"lower.…"`) | §3.8 — the clamp reads a local rotation in (−180, 180]; the bound is the drawn link's rest angle moved by the range, so narrow the range |
6858
+ | `stop "C" generates bone "X", which this rig already declares …` / `… generates ik constraint "X" …` / `… generates transform constraint "X" …` / `… which stop "D" generates too …` | §3.8 — the names are the expansion's (`<chain>_touch`, `<chain>_stop`, `<chain>_stop_aim`, `<link>_rel`, `<link>_rel_copy`, `<link>_d`, `<link>_clamp`); rename the bone or constraint, or the `chain` |
6859
+ | `stop "C": links[k] "X" is written by no constraint of this rig spec …` | §3.8 — declare what swings the link (a physics constraint, usually) in the rig spec's `constraints`; the motion spec's `physics` table is emitted after the clamp |
6860
+ | `stop "C": links[k] "X" does not hang below "P_d", the drawn link of links[k-1] …` | §3.8 — parent the link (or its control bone) below the drawn link above it, so the clamp that holds that one carries it |
6861
+ | `stop "C": "against" "A" hangs below "B", which the stop moves …` | §3.8 — name the leg; a point on the chain would swing with it |
6862
+ | `stop "C": "at" [x, y] on "A" sits on the root of "L" …` / `… lies on "L"'s own line at the setup pose …` | §3.8 — move the point onto the leg's edge beside the hem; on the link's line it has no side to call inward |
6863
+ | `stop "C": "P", where the frame hangs, is scaled unevenly, sheared or reflected …` / `stop "C": "B", between links[k] "L" and the drawn link above it, is scaled unevenly, sheared, reflected or inherits other than normally …` | §3.8 — the stop's angles add only through a rotation and an even scale; take the scale off that bone |
6864
+ | `stop "C": "L" hangs below "B", which a later stop generates …` | §3.8 — list that stop first |
6865
+ | `physics constraint "X" drives "L", a link stop "C" holds, from the physics table …` | §3.8, §4.6 — declare the constraint in the rig spec's `constraints`, where the stop's copy and clamp are placed after it |
6483
6866
  | `skin "S" lists "X" under "transform", but the rig declares it as a "ik" constraint — a skin looks its constraints up by name AND type, so this one is a miss and the loader throws` | §3.4.8 — move the name to the list named after the constraint's own kind. A name the rig declares under **no** kind is the other miss and says so (`skin "S" activates ik constraint "X", which this rig does not declare`). ⚠️ A name listed by **several skins** is not an error: the lists are per-skin sets and a bone or constraint in two of them is active under either |
6484
6867
  | `slot "X" names bone "Y", which this rig does not declare` | add the bone, or fix the slot's `bone` |
6485
6868
  | `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 — pick one file and declare it there. A slot **nothing** fills is exempt: its setup pose can only be "show nothing" and is not asked for |
@@ -6533,7 +6916,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6533
6916
  | `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
6917
  | `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
6918
  | `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) |
6919
+ | `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
6920
  | `a key carries both a named easing and a raw curve; pick one` | R6 |
6538
6921
  | `last key carries an easing but has nothing to ease to` | drop `ease`/`curve` from the final key |
6539
6922
  | `key times must strictly increase (at t=…)` | including after `lag` and `stagger` |
@@ -6575,6 +6958,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6575
6958
  | `deform … (t=…): transform yaw has radius R, and vertex v sits at x=… past it` | §4.11.1 — the cylinder has no surface there; raise the radius to where the part sits |
6576
6959
  | `deform … (t=…): transform affine has scale […], whose determinant is …` | §4.11.1 — at or below zero the map reverses every triangle |
6577
6960
  | `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 |
6961
+ | `deform … (t=…): transform field "envelope.<field>" is …` | §4.11.1 `envelope` — the field the message names: `from` and `to` differ, `axis` is a unit direction, `shape` is `"linear"` or `"smooth"`, a `mask` is this attachment's own soft mask, and a weight is in [0, 1] |
6962
+ | `deform … (t=…): transform <kind> states … and envelope … weighs every one of them 0` / `… it changes no emitted number` | §4.11.1 `envelope` — the ramp does not cross the part; the message says where its vertices sit along the axis |
6578
6963
  | `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 |
6579
6964
  | `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 |
6580
6965
  | `"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 |
@@ -6735,7 +7120,7 @@ Fix A00 and run it again.
6735
7120
  | <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
7121
  | <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
7122
  | <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 |
7123
+ | <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
7124
  | <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
7125
  | <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
7126
  | <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 |
@@ -7430,6 +7815,8 @@ exports names the frame that moved. Two exports of one skeleton are byte-identic
7430
7815
  | `setup` | the setup pose, sampled the way a skeleton with no animation is rendered: `{ bones, attachments }` |
7431
7816
  | `frames` | one per PNG, in index order: `{ index, time, bones, attachments }`. `index` is the number in `f####.png`, `time` is `index / fps` in seconds |
7432
7817
 
7818
+ **The keys, as the file writes them.** Top level: `spec`, `coordinates`, `animation`, `skin`?, `environment`?, `fps`, `viewport`, `bones`, `slots`, `rest`, `setup`, `frames` — a `?` marks a key written only when it is set. Each frame: `index`, `time`, `bones`, `attachments`; and `setup`: `bones`, `attachments`. What a frame draws is under `attachments`: the top-level `slots` is the table of the bone each slot hangs from, not a frame's pieces.
7819
+
7433
7820
  Inside `setup` and each frame:
7434
7821
 
7435
7822
  - `bones` — every bone in declaration order: `{ name, a, b, c, d, worldX, worldY }`,
@@ -7684,6 +8071,179 @@ not have — and a render draws the rig where it stands, so the picture would sh
7684
8071
  a coat trailing a rig that never moves. The live track holds both to spine-core
7685
8072
  (`CO56`), and a player forwards them as §4.6 says.
7686
8073
 
8074
+ ### 8.5 Where a cloth passes through a leg — `render --pass-through`
8075
+
8076
+ A skirt drawn over a thigh can swing its side straight through the thigh on a build
8077
+ that passes every assertion and every check. A real skirt cannot do that while it is
8078
+ in front of the leg: the leg is inside it and leaves through the hem. `--pass-through`
8079
+ reads it off the frames `render` draws, for the pairs you name. On the selftest's
8080
+ probe (`pass_through_probe`, `fixtures/public.ts`: a skirt whose hip swings 30° and
8081
+ back over a leg it hangs in front of, and a second copy of the leg drawn over it):
8082
+
8083
+ ```bash
8084
+ bun cli.ts render --candidate path/to/spine --animation swing --pass-through skirt:leg,skirt:leg_front [--skin …] [--fps 12]
8085
+ # rigc render
8086
+ # .. swing 25 frame(s), 2.000s + contact.png + pass-through.json (1 of 2 pair(s) pass through) -> …/swing
8087
+ # .. 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
8088
+ # .. 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)
8089
+ # … one line for each of frames 3-22
8090
+ # .. 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
8091
+ ```
8092
+
8093
+ **Which frames are read.** A frame where both slots draw and the **cloth draws over
8094
+ the leg**, in that frame's own draw order — a draw-order key that puts the leg in
8095
+ front takes its frames out of the reading. The order in the flag is the
8096
+ declaration: `skirt:leg` asks whether the skirt passes through the leg, and
8097
+ `leg:skirt` is a different question, never the same one read both ways. A slot
8098
+ paired with itself is read on no frame, and its line says why.
8099
+
8100
+ **What passing through means.** Four steps, every number in them derived:
8101
+
8102
+ 1. **Rows, in the cloth's setup pose.** The cloth is drawn over its setup-pose
8103
+ geometry — `rest`'s vertices (§8.2), the setup pose's bones with no deform — at
8104
+ the frame's own UVs and tint, onto cells of **one texel of its drawing** as the
8105
+ setup pose lays it out. A cell is drawn when the renderer paints its centre at
8106
+ half opacity or more (128 of 255) — the line an antialiased edge sits on, rather
8107
+ than the faintest fringe the renderer would still paint. The rows are the setup
8108
+ pose's, not the world's, so a skirt the pose swings or bends still has its side
8109
+ where its side is: read in world rows, a waist the pose tilts becomes a "side"
8110
+ on the rows it spans, and a leg under it would read as passing through.
8111
+ 2. **The hem's corner.** On each side, the corner is the outermost drawn cell in the
8112
+ lower half of the drawn rows — the lowest on a tie. The rows from the corner up
8113
+ to the cloth's top are read; the rows below it are the hem's own curve, under
8114
+ which a leg is meant to show.
8115
+ 3. **The run.** On each read row, from the centre of the outermost drawn cell as the
8116
+ frame poses it, step outward — along the setup pose's outward direction, carried
8117
+ to the frame by the cloth triangle holding that centre — in **world units**, one
8118
+ texel of the coarser of the two drawings at a time, and count the steps whose
8119
+ point the leg draws at half opacity, up to the first it does not. That is how far
8120
+ the leg's outline passes the cloth's, in the units the picture shows.
8121
+ 4. **The line.** A frame passes through when some read row's run is **2 steps or
8122
+ more** — more than the one step an outline is resolved to.
8123
+
8124
+ ⚠️ The run is in world steps, not in the cloth's setup-pose cells, because on a
8125
+ cloth the pose squashes the two disagree: counted in setup cells, a run reads longer
8126
+ than the picture holds, and the line printed a depth in units over a box narrower
8127
+ than that depth. The rows are the setup pose's
8128
+ because they say *which* part of the cloth is its side; the run is the world's
8129
+ because it says *how far* the leg shows (`PX04`, and `PX12` plants the other).
8130
+
8131
+ **Blind spots, stated.** A cloth that widens all the way up to its waist has its
8132
+ corner at mid-height, so the rows below are not read. A leg wholly beside the cloth,
8133
+ entering above its top, meets no read row's side. And the rows are the setup pose's,
8134
+ so a cloth authored lying on its side reads its side as a hem.
8135
+
8136
+ **The file.** `pass-through.json`, `rigc-pass-through/1`, beside each set's frames:
8137
+ `animation`, `skin` (absent when none was set), `fps`, `reading` (these definitions,
8138
+ stated in the file), and `pairs[]` in the flag's order, each with `cloth`, `leg`,
8139
+ `drawn` (frames where both slots draw), `over` (of those, the frames the cloth draws
8140
+ over the leg — the frames read), `frames[]` (only the frames that pass through:
8141
+ `index`, `time`, `cells` — the longest run, in steps — `depth` — that in world
8142
+ units — `cell` — the step — `side` — `left` or `right` of the cloth as it lies at its
8143
+ setup pose — `attachment` — the cloth's at that frame — `rows` — read rows whose run
8144
+ reaches the line — and `box` — `[minX, minY, maxX, maxY]`, world units, y up, over
8145
+ both ends of every such run, the cloth's outline to the run's far end) and `deepest`
8146
+ (the read frame with the longest run, the earliest on a tie, with `index`, `time`,
8147
+ `cells`, `depth`, `cell` and `side`; `null` when no frame is read). A pair that passes
8148
+ through on no frame is in the file, and its line says how many frames were read.
8149
+
8150
+ **Refused, nothing written:** a slot the skeleton does not have (with the slots it
8151
+ does), an entry without exactly one `:`, an empty entry or name, a pair named twice,
8152
+ a slot that draws on no frame of the sets drawn, a cloth or leg a clip cuts (its
8153
+ drawing's outline is no longer its attachment's), and `--slot`/`--hide` beside the
8154
+ flag, for `--folds`' reason.
8155
+
8156
+ **Nothing else moves.** The PNGs, `contact.png`, `frames.json` and `geometry.json`
8157
+ are byte for byte what the render writes without the flag (`PX06`), and both posers
8158
+ write the same file (`PX05`).
8159
+
8160
+ **A reading, not a gate.** It names frames and decides nothing: whether a frame that
8161
+ passes through is a defect, and what to do about it — a leg stop, a wider hem, a
8162
+ different swing — is yours. To look at one, draw it:
8163
+ `inspect --show art --animation <name> --at <time> --zoom slot:<cloth>`.
8164
+
8165
+ ### 8.6 One box for two renders — `render --viewport`, `render --viewport-from`
8166
+
8167
+ `render` fits its box to the rig it draws, so two builds of one rig — before and
8168
+ after a change — usually land on two grids of different sizes, and their pixels
8169
+ cannot be compared without a transform written by hand. Give the box instead:
8170
+
8171
+ ```bash
8172
+ bun cli.ts render --candidate before/ --out render-before
8173
+ bun cli.ts render --candidate after/ --viewport-from render-before --out render-after
8174
+ bun cli.ts render --candidate after/ --viewport -40,70,90,140 --max 256 --out render-box
8175
+ ```
8176
+
8177
+ - `--viewport-from <frames.json | dir>` reads an earlier render's `frames.json` (the
8178
+ file, or the directory `render --out` wrote it into) and draws on **its grid
8179
+ exactly**: the same world box, the same scale and the same pixel size, so a world
8180
+ point lands on the same pixel in both renders and the two frame sets overlay.
8181
+ `--max` is refused beside it, because the file already fixes the pixel size.
8182
+ - `--viewport <x,y,w,h>` draws over a world box you state, y up, `x,y` its
8183
+ bottom-left corner — the numbers `frames.json`'s `viewport` records, and the
8184
+ spelling `check --viewport` and `inspect --viewport` take. Its pixel size follows
8185
+ from `--max` as a fitted box's does.
8186
+
8187
+ The render prints a `viewport given by …` line with the box and the box it would
8188
+ have fitted, so you can see what the given one leaves out.
8189
+
8190
+ ⚠️ **Nothing is cropped in silence.** Each frame is the box: what lies past it is
8191
+ not drawn. A fitted box is measured over every pose the rig strikes at 60 fps and padded; a given one is not,
8192
+ so every slot whose drawn geometry reaches past it — a piece's posed vertex outside
8193
+ the box — is named, once per set: the frames, the farthest it reaches in world
8194
+ units, and the sides (`left`, `right`, `bottom`, `top`, y up). `frames.json` records
8195
+ the box was given and those slots under `pinned`: `{ "by": "--viewport" |
8196
+ "--viewport-from", "outside": [{ "set", "slot", "frames" }] }` — written only when a
8197
+ box was given, so a fitted render is the bytes it always was.
8198
+
8199
+ **Refused, nothing written:** the two flags together; `--max` beside
8200
+ `--viewport-from`; a `--viewport` that is not four numbers or whose width or height
8201
+ is not above 0; a `--viewport-from` path with no `frames.json`, a file whose `spec`
8202
+ is not `rigc-frames/1`, and a `viewport` field that is not a finite number (`x`,
8203
+ `y`), a number above 0 (`width`, `height`, `scale`) or a whole number of pixels above
8204
+ 0 (`pixelWidth`, `pixelHeight`) — each named with the value found. The framing still
8205
+ runs under a given box, so a pose with nothing to draw, one that is not finite and
8206
+ one with no extent are refused as they are without one.
8207
+
8208
+ ### 8.7 How far a bone turns against another — `render --angle-range`
8209
+
8210
+ A physics value has no scale you can see from the spec: what `inertia` 0.5 does to
8211
+ a link depends on its length, on how its parent moves and on every other value
8212
+ together. The answer is a number the pose already holds — how far the link turned
8213
+ against the bone it hangs from — and `--angle-range` reads it off the frames
8214
+ `render` draws:
8215
+
8216
+ ```bash
8217
+ bun cli.ts render --candidate path/to/spine --angle-range tail:hip,sleeve:arm [--animation …] [--fps 12]
8218
+ ```
8219
+
8220
+ One line per pair per set, beside the set's line: the lowest and the highest angle
8221
+ in degrees, the span, the frames read and the rate, and the frame each extreme was
8222
+ first reached on. Nothing is written, and every file is the bytes the render writes
8223
+ without the flag.
8224
+
8225
+ **What the angle is.** At each frame, the first bone's world direction less the
8226
+ second's — each the direction its x axis points, degrees counter-clockwise, y up,
8227
+ as the poser that drew the frame poses it (`rotationX`). The first frame's
8228
+ difference is taken into (−180°, 180°], and each later frame's is moved by whole
8229
+ turns to within half a turn of the frame before, so a link that sweeps 200° reads
8230
+ 200°, not −160°. ⚠️ It is the angle **drawn**, not the bone's local `rotation`: the
8231
+ two agree for a child of the other bone when the parent carries no non-uniform scale
8232
+ or shear.
8233
+
8234
+ **Where it is read.** On the frames' own grid — `--fps`, from t = 0 to each set's
8235
+ last frame — so a peak between two frames is not read; raise `--fps` to read finer.
8236
+ It reads bones, which `--slot`/`--hide` do not change, so it takes them.
8237
+
8238
+ **Refused, nothing written:** a bone the skeleton does not have (with the bones it
8239
+ does), an entry without exactly one `:`, an empty entry or name, a bone against
8240
+ itself (0° on every frame), a pair named twice, and a bone with no direction on a
8241
+ frame — a world matrix of zero, which a bone the posed skin leaves unposed has, or
8242
+ one scaled to nothing — named with the frame.
8243
+
8244
+ **A reading, not a gate.** Whether a range is too wide is yours; §4.6 says which
8245
+ values move it.
8246
+
7687
8247
  ---
7688
8248
 
7689
8249
  ## 9. Checking against the frames — `rigc check`