spine-rigc 0.16.0 → 0.18.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
@@ -163,7 +163,7 @@ What the flags mean:
163
163
  | `--atlas-in` | `build` only: resolve every part against the **regions of a pre-packed `.atlas`** instead of against loose PNGs. Region geometry is read from the file and sizes are descaled by the page's `scale:`; the atlas is re-emitted into `--out`, re-anchored — **§0.2** |
164
164
  | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory). For `pose` it is the directory of **loose part PNGs to place** — every `.png` in it is a part, in name order. For `chainfit` it is only where each attachment's image name **resolves**: the candidate decides what the parts are, so extra PNGs are unused and a missing name is refused by name (§12.3) |
165
165
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
166
- | `--profile` | `spine` = the 25 validity rules (**the default**) · `spine-html` = all 40, opt-in |
166
+ | `--profile` | `spine` = the 26 validity rules (**the default**) · `spine-html` = all 41, opt-in |
167
167
  | `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` is the one command that takes it more than once** — repeat it 2–4 times, one per pane, labelled A, B, C, D in the order given; everywhere else a repeat is a typo and is refused |
168
168
  | `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview`, and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote`, so is a name that only *some* candidates have |
169
169
  | `--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` |
@@ -788,9 +788,11 @@ silhouette, and an octagon's sides pass `R · cos(π/8)` from its centre, so 5.7
788
788
  the drawing — its whole ink outline, between the spokes — was not going to be
789
789
  drawn, and every assertion passed (issue #277).
790
790
 
791
- The generators are `ring`, `ribbon` and `contour` (see
791
+ The generators are `ring`, `ribbon`, `contour` and `grid` (see
792
792
  [`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
793
- than a table of numbers, which is why they are code invoked by data. A generator
793
+ than a table of numbers, which is why they are code invoked by data. The last two
794
+ are geometry: they pin every vertex to the slot bone and exist to give a
795
+ `deform` timeline (§4.12) somewhere to push. A generator
794
796
  is for a skeleton with **no** manifest; a cut that has one invokes the same
795
797
  builders through the manifest's `mesh` block.
796
798
 
@@ -910,109 +912,281 @@ Self-intersection is refused; **holes are not cut out**; and nothing here does
910
912
  interior/Steiner points, so a contour mesh bends only where its outline has
911
913
  vertices.
912
914
 
913
- 🖼️ **Worked example: [`gallery/flex`](https://github.com/firejune/rigc/tree/main/gallery/flex)** — four contours over real art, with
914
- the `tolerance`/`margin` sweep that picked their settings and what each one measured.
915
+ #### `grid` — a lattice over the part window
915
916
 
916
- **Bounding box** ([Spine: bounding boxes](http://esotericsoftware.com/spine-bounding-boxes))
917
- and **clipping** ([Spine: clipping](http://esotericsoftware.com/spine-clipping))
918
- attachments — a polygon, and nothing else.
917
+ **When you need one:** the part is a plate whose *surface* moves — a face that
918
+ turns, a cloth that ripples — and a `deform` model (§4.12) needs interior
919
+ vertices to push. A `contour` gives you the silhouette and nothing inside it;
920
+ this gives you columns and rows.
919
921
 
920
- **When you need one:** a *bounding box* is a shape the game hit-tests against — a
921
- hurt box, a pick region, a trigger volume — that follows the skeleton and draws
922
- nothing. A *clipping* attachment is a **mask**: everything drawn from the slot
923
- carrying it up to and including `end` is clipped to the polygon, so a window, a
924
- portal or a wipe is one attachment rather than a second set of art.
922
+ It takes **no geometry and no size**: like a `contour`, the window is the
923
+ attachment's own `image`, and every vertex is pinned to the slot bone at weight
924
+ 1, so an undeformed grid draws exactly what the region drew.
925
925
 
926
926
  ```json
927
- "hitbox_a": { "hitbox_a": { "type": "boundingbox", "vertexCount": 4,
928
- "vertices": [-30, -10, 30, -10, 30, 50, -30, 50] } },
929
- "mask_a": { "mask_a": { "type": "clipping", "end": "box", "vertexCount": 3,
930
- "vertices": [0, 0, 200, 0, 0, 160],
931
- "color": "ff00ffff" } }
927
+ "generator": { "kind": "grid", "us": [0.0235, 0.1471, 0.5, 0.8529, 0.9765],
928
+ "vs": [0.0263, 0.2632, 0.5, 0.7368, 0.9737] }
932
929
  ```
933
930
 
934
- Both polygons above are invented — an axis-aligned rectangle and a right triangle,
935
- in round numbers, so that nothing here can be mistaken for a shape measured off a
936
- reference. A real one is measured off your own art (§8) or drawn to the volume the
937
- game needs.
938
-
939
931
  | Field | Meaning |
940
932
  | --- | --- |
941
- | `vertexCount` | **required.** How many vertices the polygon has, stated outright — see the warning below |
942
- | `vertices` / `weights` | the same two encodings a mesh's geometry uses, with the same by-name default and the same `"boneIndexing": "raw"` opt-in |
943
- | `color` | `rrggbbaa`; the colour the editor draws the outline in |
944
- | `end` | clipping only. The **last** slot the clip applies to, by name |
945
- | `convex`, `inverse` | clipping only, 4.3, both default false |
946
-
947
- 🚨 **`vertexCount` has no parser default and rigc will not infer one.** A mesh gets
948
- its count from `uvs.length`; a polygon has no uvs, and the parser reads
949
- `map.vertexCount << 1` as the number of coordinates to expect. With the field
950
- absent that is `undefined << 1` = **0**, so the coordinate array is decoded as a
951
- *weighted* run — bone counts and weights read out of your x/y pairs — and the
952
- attachment ends up holding nothing. It loads. Neither type draws a pixel, so
953
- nothing downstream notices. rigc requires the count and cross-checks it against
954
- whichever encoding you used; `A33_VERTEX_ATTACHMENT_GEOMETRY` checks it again on
955
- the artifact.
933
+ | `us` / `vs` | column and row positions across the window, `0..1`, ascending, at least two each. **Positions, not a count** |
934
+ | `cols` / `rows` | an even division of the whole window instead. Refused beside `us`/`vs` |
935
+ | `depth` | a depth map, below — this is the generator it was built for |
936
+
937
+ ⭐ **Positions, because [FACE §4.1](FACE.md) places columns where the drawing
938
+ needs them.** The five above are `gallery/portrait`'s own — dense at the
939
+ silhouette, sparse across the middle, and not reaching the window edge. A
940
+ generator that could only divide evenly would be a step backwards from the table
941
+ it replaces, and it does replace it: the selftest builds that exact mesh from
942
+ those five numbers and requires it to come out identical to the 25 vertex pairs
943
+ and 32 triangles the example shipped by hand.
944
+
945
+ 🚨 **The reason to generate this at all is the hull.** Spine's `hull` is a
946
+ **count** — the first `hull` entries of the vertex list are the outline — so the
947
+ perimeter must be listed first and in walk order (top row, right column, bottom
948
+ row, left column), interior after. A hand-numbered grid written a row at a time
949
+ puts interior vertices in the hull, and the mesh loads, draws and deforms wrong
950
+ with nothing anywhere to say so. The generator satisfies that by construction
951
+ rather than being checked afterwards.
952
+
953
+ | The input | What you get |
954
+ | --- | --- |
955
+ | positions **and** a count | `states both positions ("us"/"vs") and a count ("cols"/"rows") … Drop one` |
956
+ | neither | `A lattice is not a default` |
957
+ | one axis only | `a lattice needs both axes` |
958
+ | positions that do not ascend | `"us" is not ascending: [1]=0.6 and [2]=0.6` — equal neighbours put two vertices in one place and collapse a row of triangles to zero area |
959
+ | a position outside `0..1` | `positions are fractions of the part window, 0..1` |
960
+ | `cols` or `rows` under 2 | `it is a whole number of at least 2` |
956
961
 
957
- ⚠️ **A clipping `end` that names nothing is not an error to Spine.**
958
- `skeletonData.findSlot` returns `null` on a miss and the parser assigns that null
959
- without a word, so the clip never ends — it runs to the bottom of the draw order
960
- and takes every slot below it out of the frame. rigc refuses a name the rig does
961
- not declare. Omitting `end` entirely is the format's own way of saying "clip
962
- everything after this one", and is left alone.
962
+ #### `depth` — give every vertex its own z, instead of one cylinder radius
963
963
 
964
- 🚫 Under `--profile spine-html` a clipping attachment is refused by
965
- `A11_NO_CLIPPING_ATTACHMENTS` — that renderer skips them silently, so a mask that
966
- was supposed to hide something would not. It is valid Spine and the default
967
- `--profile spine` accepts it; the refusal is policy, not validity.
964
+ **On a `contour` or a `grid`.** A grid is the one it was built for: a turn
965
+ needs interior vertices to move, and a contour has none of its own.
968
966
 
969
- **Path attachment** ([Spine: paths](http://esotericsoftware.com/spine-paths)) — a
970
- composite cubic Bezier, and the only attachment whose purpose is to be *followed*.
967
+ A `yaw` or `pitch` key (§4.12) turns a part by treating it as painted on a
968
+ cylinder: a vertex `u` off the axis gets `z = √(radius² − u²)`. That is the right
969
+ model for a fringe or a plate that really does bend like a barrel, and the wrong
970
+ one for a face — a nose is not on the skull's cylinder and an ear is behind it,
971
+ and no single radius puts both where they are.
971
972
 
972
- **When you need one:** a path constraint has nowhere to aim without it (§3.5). The
973
- curve is not drawn — no runtime renders a path — and it deforms with its slot's
974
- bone like any other vertex attachment, so an animated path is a moving track.
973
+ Name a **depth map** on the generator and every vertex gets its own `z`, sampled
974
+ off a greyscale sheet in the part's own pixel grid:
975
975
 
976
976
  ```json
977
- "track": { "track": { "type": "path", "vertexCount": 9,
978
- "vertices": [-30, 0, 0, 0, 30, 0, 60, 0, 90, 0,
979
- 120, 0, 150, 0, 180, 0, 210, 0] } }
977
+ "generator": {
978
+ "kind": "contour", "tolerance": 1.5, "margin": 2,
979
+ "depth": { "image": "face_depth.png", "near": "white", "zScale": 40 }
980
+ }
980
981
  ```
981
982
 
982
- That is nine invented points forming a straight line: knots at x = 0, 90 and 180
983
- with their handles evenly spaced between them, which is the simplest path there is
984
- and the one worth reading twice.
985
-
986
983
  | Field | Meaning |
987
984
  | --- | --- |
988
- | `vertexCount` | **required**, and a multiple of 3 — see below |
989
- | `vertices` / `weights` | the same two encodings a mesh's geometry uses, with the same by-name default |
990
- | `closed` | default false. When true the last knot joins the first |
991
- | `constantSpeed` | default **true**: the runtime re-measures the curve every frame so travel along it is even. `false` makes it trust the emitted `lengths` instead — cheaper, and exact only while the path holds its setup shape |
992
- | `color` | `rrggbbaa`; the colour the editor draws the curve in |
993
-
994
- 🚨 **`vertexCount` counts knots AND handles, and the arithmetic is not the one you
995
- would guess.** The vertices are walked in groups of three, and an **open** path
996
- drops the first and last point — those two are the outer control handles of the end
997
- knots, and no curve uses them. So:
998
-
999
- - an open path of K curves has `vertexCount = 3(K + 1)`, minimum **6** (one curve);
1000
- - a closed one has `vertexCount = 3K`, minimum **3**, because the chain wraps.
1001
-
1002
- A count that is not a multiple of 3 does not throw anywhere:
1003
- `Utils.newArray(vertexCount / 3, 0)` takes a fractional size happily, the groups of
1004
- six then straddle the knots, and bones slide along a curve nobody drew. rigc refuses
1005
- it, and `A33_VERTEX_ATTACHMENT_GEOMETRY` refuses it again on the artifact.
1006
-
1007
- ⚠️ **`lengths` is not yours to write.** The emitted file carries one — the
1008
- cumulative arc length at the end of each curve, in world units, in the setup pose —
1009
- and rigc **measures** it off the geometry above, exactly as it measures a region's
1010
- `width`/`height` off its PNG (R5). Writing one is a `CompileError`: it is a second
1011
- copy of a number the vertices already determine, a copy that disagrees is only
1012
- visible under `constantSpeed: false`, and there it silently rescales the whole
1013
- traversal. `explain` prints the measurement, so `position: 0.25` on a curve that
1014
- reports `180 long` is a bone at 45 units along it.
985
+ | `image` | **required.** The sheet, relative to the rig's `images` directory, and the **same pixel size as this attachment's `image`**. It is not packed into the atlas — it is a measurement rigc reads at compile time, not art anything draws |
986
+ | `near` | **required.** `"white"` or `"black"` — which end of the range is closest to the viewer. Stated rather than defaulted: both conventions are in use, and a sheet read with the wrong one turns the part inside out with every gate still green |
987
+ | `zScale` | **required.** How many world units the map's full range spans, in the attachment's own units — the number `radius` used to carry. 8 bits of level say nothing about scale, so this is authored, never measured |
988
+ | `gamma`, `contrast`, `bias` | the tone curve applied to the nearness, defaults `1` / `1` / `0`. State them when a consumer's own renderer curves the same sheet, so the mesh and that renderer describe one surface |
989
+
990
+ The order is fixed and it matters, because every step is a place two
991
+ implementations can silently disagree: **bilinear sample of the raw level** (at
992
+ pixel centres, which is what a GPU's linear filter does), then `near`, then the
993
+ curve clamped back into 0..1, then `zScale`. Naming a map changes no emitted byte
994
+ on its own — a `yaw` or `pitch` key has to ask for it with `"depth": true`.
995
+
996
+ `build` and `explain` report the sheet's digest (over the levels, so a re-encode
997
+ of the same map reads the same) and the `z` range actually sampled, which is the
998
+ number that says whether the map covers the part or a corner of it.
999
+
1000
+ ⭐ **And the TURN CEILING — the angle to write on the key, before you write it.**
1001
+
1002
+ ```
1003
+ MESH face grid 1089 vertices / 2048 triangles (budget 3000) bones=[face]
1004
+ depth "face_depth.png" f552a2f50d21 near=white zScale=60 z=[0, 60]
1005
+ turn ceiling yaw +31.41° / -32.01° pitch +32.01° / -31.41°
1006
+ 1st pct yaw +31.55° x1.004 of 1004 / -32.10° x1.003 of 1044 pitch +32.10° x1.003 of 1044 / -31.55° x1.004 of 1004
1007
+ first to fold: yaw + at 31.41°, triangle 960 [113,112,593], the sheet steps 12.52 level(s) across it
1008
+ ```
1009
+
1010
+ Past that angle a triangle turns inside out and `A39` refuses the build by name.
1011
+ The loop this replaces is *pick an angle, build, read the refusal, guess again*.
1012
+
1013
+ #### Is the ceiling describing the form, or the sheet's grain?
1014
+
1015
+ The second and third lines answer that, and they are **reports only** — nothing
1016
+ in them refuses a build or moves a ceiling
1017
+ ([#412](https://github.com/firejune/rigc/issues/412)).
1018
+
1019
+ The ceiling is the **minimum** of the per-triangle fold angles, and a minimum
1020
+ cannot say whether it is the floor of a band or one bad pixel. Measured: a clean
1021
+ 8-bit sheet at 4,225 vertices reports **64.58°**; move **one texel of 160,000**
1022
+ by 245 levels and the same sheet reports **6.08°** — and `A39` refuses at both,
1023
+ to 0°. The number is not lying. 99 % of that mesh still turns to 64.80° and one
1024
+ triangle does not, and nothing on the first line says so.
1025
+
1026
+ | the figure | how to read it |
1027
+ | --- | --- |
1028
+ | `x1.003` — the **1st percentile over the ceiling** | near 1 means a *band* of the mesh reaches the limit together, which is what a smooth form looks like: its steepest region has area. Near 10 means **one triangle** does, which is what a bad texel looks like. The clean and stray sheets above read `x1.003` and `x10.652` |
1029
+ | `of 1004` — the **population** that percentile came out of | it is the nearest rank, so below **51** folding triangles there is no percentile to take and the line says `unranked of 36` instead of printing the minimum twice. Just over 51 it is the *second*-smallest angle, and a limit two triangles share is not yet a band |
1030
+ | `12.52 level(s)` — the **depth step across the triangle that folds first** | how much of the sheet's 0–255 range that triangle actually read. **Below about 3 the ceiling is quantisation rather than form**, and at exactly 1 it is `atan(255·h / zScale)` for cell size `h` — arithmetic about the encoding, with no form left in it at any density |
1031
+ | `+none` | on the ceiling line, nothing folds on that side at all, at any angle. On the percentile line it is the same statement — there is no population, because there is nothing to take a percentile of |
1032
+
1033
+ A real one rather than the illustration above — `bun cli.ts build --rig
1034
+ gallery/look/rig.json --motion gallery/look/motion.json --images
1035
+ gallery/look/parts --out <dir>`, whose two meshes happen to print two of the
1036
+ three spellings:
1037
+
1038
+ ```
1039
+ MESH head grid 189 vertices / 320 triangles (budget 320) bones=[head] attachments=[head]
1040
+ depth "face_depth.png" bf156ea0cfc970a3 near=white zScale=194 z=[0, 194]
1041
+ turn ceiling yaw +19.32° / -19.32° pitch +22.92° / -26.94°
1042
+ 1st pct yaw +19.32° x1.000 of 80 / -19.32° x1.000 of 80 pitch +22.92° x1.000 of 102 / -26.94° x1.000 of 130
1043
+ first to fold: yaw + at 19.32°, triangle 174 [119,138,139], the sheet steps 28.50 level(s) across it
1044
+ MESH hair_lock_l grid 39 vertices / 48 triangles (budget 320) bones=[lock_l] attachments=[hair_lock_l]
1045
+ depth "lock_l_depth.png" 0c4eaeb36b7c5cac near=white zScale=64 z=[22.086275, 63.874511]
1046
+ turn ceiling yaw +17.04° / -45.80° pitch +none / -none
1047
+ 1st pct yaw +unranked of 12 / -unranked of 36 pitch +none / -none
1048
+ first to fold: yaw + at 17.04°, triangle 2 [1,28,29], the sheet steps 78.00 level(s) across it
1049
+ ```
1050
+
1051
+ ⭐ Both of those sheets are **form**, and the figures say so from opposite ends:
1052
+ the head's 320 triangles put the percentile exactly on the ceiling, and the
1053
+ lock's 48 are too few to rank at all — but at 28.50 and 78.00 levels across the
1054
+ folding triangle, neither ceiling is anywhere near the encoding.
1055
+
1056
+ ⇒ **A small ceiling with a ratio near 1 and a step well above 3 is a steep
1057
+ surface: flatten the map.** A small ceiling with a large ratio, or with a step
1058
+ near 1, is a *sheet* problem: the grain, the 8-bit rounding, or a stray pixel.
1059
+ [`docs/FACE.md` §2.2](FACE.md) has the amplitudes and what each one costs.
1060
+
1061
+ ⛔ **rigc will not filter the sheet for you, and you should not want it to.** A
1062
+ smoothed measurement would describe a surface the deform key is not built from:
1063
+ the mesh still samples the raw sheet, `A39` still refuses at the raw angle, and
1064
+ the report and the gate would part company. The compiler never invents a value
1065
+ that is not in the spec — the fix is an edit to the map.
1066
+
1067
+ ⭐ **Three ways to live with the ceiling**, and the third is the one a face
1068
+ actually uses: flatten the depth map, stay inside the angle, or **take the part
1069
+ off the screen before it gets there** — fade the slot to alpha 0 (`rgba`, §4.4) or
1070
+ swap the attachment away, as the far cheek or ear does while the head turns. That
1071
+ third one is measured rather than declared: a deform key whose slot draws no
1072
+ pixels at that key's own time is passed over by name, with the reason on the
1073
+ stats line and in the `DEFORM` block
1074
+ ([#401](https://github.com/firejune/rigc/issues/401)). **Alpha exactly 0** — a
1075
+ part faded halfway is still refused, and the alpha is in the message.
1076
+
1077
+ 🚨 **Land the alpha-0 key *before* the folding key, not on it.** The gate measures
1078
+ **keys**, and the geometry between two keys is interpolated: on the turn probe,
1079
+ with the fade ending exactly on a 40° key, 8 triangles are already reversed at
1080
+ `t=0.4` where the slot is still drawing at **alpha 0.20** — and no key is
1081
+ measured there, so nothing says so. Fade out over the run *up to* the angle you
1082
+ cannot take, and the keys that fold are the ones that draw nothing.
1083
+
1084
+ The arithmetic is exact and worth knowing, because it tells you what to change.
1085
+ A `yaw` sends each vertex to `x' = u·cos t − z·sin t`, so a triangle's area is
1086
+ `A₀·cos t − A_yaw·sin t` where `A_yaw` is the same area with **z substituted for
1087
+ u** — and it reaches zero at `tan t = A₀/A_yaw`. Three consequences:
1088
+
1089
+ - ⚠️ **The ceiling is a property of the SHEET, not of the mesh.** Over a smooth
1090
+ map the limit approaches `1 / max|dz/du|`, the reciprocal of its steepest
1091
+ slope, with no mesh term in it. Refining the lattice does not lower the angle
1092
+ — it finds slopes that were always there. So a ceiling you cannot live with is
1093
+ fixed by editing the depth map, not by meshing differently:
1094
+ [`docs/FACE.md` §2.2](FACE.md) has the measured ladder and the rule.
1095
+ - **The four numbers are four different answers**, each from its own triangle.
1096
+ A part that turns 30° left and 18° right is ordinary, not an anomaly.
1097
+ - **`none` means no gradient on that axis**, and is a different statement from a
1098
+ large number. A sheet varying linearly along one axis cannot fold the other at
1099
+ *any* angle — `A_pitch` is then identically zero — so a linear ramp reports a
1100
+ yaw ceiling and `pitch +none / -none`.
1101
+
1102
+ ⛔ It is a **report and never a refusal**. `A39` owns the refusal, from the
1103
+ artifact and through the runtime; a second wall here would be the compiler
1104
+ inventing a policy out of a measurement. What holds the two together is a
1105
+ control: `TC01` requires this number to be the angle `A39` actually fires at, on
1106
+ the triangle `A39` actually names, to 0.01°.
1107
+
1108
+ 🚨 **The one that will catch you: the sheet has to cover the mesh, and usually
1109
+ it does not.** A contour mesh puts every vertex *on* the silhouette and pushes it
1110
+ `margin` pixels outside; a grid spans the whole window, corners included. A depth
1111
+ sheet is usually cut to the art's own alpha, so both reach past it — and a naive
1112
+ sample gives those vertices the background depth and folds them away from the
1113
+ turn, with correct arithmetic and plausible numbers all the way down. rigc
1114
+ refuses it instead:
1115
+
1116
+ | The input | What you get |
1117
+ | --- | --- |
1118
+ | a sheet cut to the art's alpha, on a **contour** | `does not cover 12 of the mesh's 12 vertices … A contour mesh puts every vertex ON the silhouette and pushes it out by the margin … Dilate the sheet past the mesh margin, or lower the margin.` |
1119
+ | a sheet cut to the art's alpha, on a **grid** | `does not cover 36 of the mesh's 81 vertices … A grid spans the whole part window, corners included … Dilate the sheet to the window, or state "us"/"vs" that keep the lattice inside the art.` — the two topologies run out of sheet for different reasons, and the message says which |
1120
+ | a sheet that is not the part's size | `the depth map … is 32x32 and the part is 64x64. A depth map is sampled in the part's own pixel grid` |
1121
+ | a colour sheet | `the depth map … is not greyscale — pixel (0, 0) is rgb(10, 200, 10)` |
1122
+ | `zScale` at or below 0 | `it is how many units the map's full range spans, so a positive number. To put the near end at the back, say "near": "black"` |
1123
+ | `gamma` or `contrast` at or below 0 | `collapses the range onto the midpoint … so the map would describe a flat part` |
1124
+ | a `near` that is neither | `it is "white" or "black"` |
1125
+
1126
+ A sheet with **no alpha channel** covers its whole grid by construction and the
1127
+ coverage check has nothing to test; what its background level means is then your
1128
+ statement, and the reported range is where it shows up.
1129
+
1130
+ ##### `soft` — which part is soft, and which bone carries it
1131
+
1132
+ A mesh can say **which vertices are soft**, so a `physics` constraint (§3.5) on
1133
+ their bone answers an impact over exactly that region — a chest, a cheek, a
1134
+ hanging sleeve.
1135
+
1136
+ ```json
1137
+ "generator": {
1138
+ "kind": "grid", "cols": 9, "rows": 9,
1139
+ "depth": { "image": "face_depth.png", "near": "white", "zScale": 40 },
1140
+ "soft": { "bone": "cheek_wobble", "mask": "cheek_soft.png" }
1141
+ }
1142
+ ```
1015
1143
 
1144
+ | Field | Meaning |
1145
+ | --- | --- |
1146
+ | `bone` | **required.** The bone the region is carried by. It has to already exist — a bone a physics constraint targets is part of the skeleton, not a side effect of a mesh |
1147
+ | `mask` | **required.** A greyscale sheet in the part's own pixel grid: the level IS the weight, black still and white fully carried, sampled at each vertex. Alpha is not read — a transparent pixel is black |
1148
+
1149
+ The remainder always stays on the slot bone, so every vertex closes at 1 by
1150
+ construction rather than by `A20` catching it later. `build` and `explain` report
1151
+ the mask's digest, how many vertices were carried outright and how many landed in
1152
+ the painted falloff.
1153
+
1154
+ 🚨 **Why a painted mask and not a depth threshold.** rigc tried the threshold —
1155
+ "the near part wobbles" — for exactly one day. It is wrong, and instructively so:
1156
+ **softness and prominence are different properties of a drawing.** The most
1157
+ prominent thing on a face is the nose, and a nose does not wobble. A threshold
1158
+ produced a region that was plausible, gated green and carried the wrong pixels.
1159
+ It also claimed something untrue — "no mask painted" — while the renderer this
1160
+ was modelled on had a hand-painted spring mask all along. rigc does not get to
1161
+ delete an input by guessing it.
1162
+
1163
+ ⭐ The falloff is painted for the same reason. A `feather` parameter would be
1164
+ rigc guessing the shape of something you can simply draw.
1165
+
1166
+ **`A21_MESH_RIM_PINNED` splits on this rather than being relaxed.** A wobbling
1167
+ silhouette is *supposed* to move; the invariant is that nothing else does. Every
1168
+ vertex must be pinned to the slot bone or shared between it and the one declared
1169
+ bone, must close at 1, and at least one must actually be carried.
1170
+
1171
+ | The input | What you get |
1172
+ | --- | --- |
1173
+ | a bone the rig does not declare | `names bone "x", which this rig does not declare … a bone a physics constraint has to target is part of the skeleton` |
1174
+ | the slot's own bone | `moves nothing — a soft region needs a bone that can move independently` |
1175
+ | a mask that is black everywhere | `carries no vertex of this mesh — every one of its 49 vertices samples black` |
1176
+ | a colour mask | `is not greyscale — pixel (0, 0) is rgb(10, 200, 10)` |
1177
+ | a mask that is not the part's size | `is 48x32 and the part is 96x64` |
1178
+ | a mask that is not on disk | `the soft mask "x.png" is not at …` |
1179
+
1180
+ ⭐ **One depth pass buys both, on one part.** A carried mesh has two bones on the
1181
+ vertices in the mask's falloff, and a `transform` key (§4.12) used to be refused
1182
+ on any attachment that did — so the **angle** a raised surface turns through and
1183
+ the **impact** a soft one answers had to sit on separate slots. Since
1184
+ [#389](https://github.com/firejune/rigc/issues/389) they do not: the model is
1185
+ evaluated at each vertex's setup world position and pushed into every influence
1186
+ through that bone's own inverse. `SF03` is the case that measures the
1187
+ combination. ⚠️ Read §4.11.1 before you add a mask to a part that already turns:
1188
+ past one bone the model's own coordinates are **world** ones, so `radius` and
1189
+ `about` change units.
1016
1190
  #### 3.4.1 A skin that switches bones and constraints on
1017
1191
 
1018
1192
  **When you need one:** a skin that is more than a change of art — a variant with an
@@ -1195,6 +1369,79 @@ artifact side: an animation with no timelines (the slider runs and the skeleton
1195
1369
  never changes), and `loop: true` on a zero-length animation — looping computes
1196
1370
  `duration + (time % duration)`, so the animation is applied at **NaN**.
1197
1371
 
1372
+ 🚨 **`additive` defaults to `false`, and that default is wrong the moment two
1373
+ sliders meet.** A slider applies its animation with `MixFrom.current` and that
1374
+ flag, so at `mix: 1` a non-additive apply **writes the value outright** instead of
1375
+ adding to the pose it found. Two sliders whose animations key the same bone
1376
+ therefore do not compose: the one **later in the `constraints` array** wins and
1377
+ the earlier one contributes nothing at all. [measured] two dials on one bone,
1378
+ contributing 7.50° and 18.75°:
1379
+
1380
+ | the two sliders | the bone poses |
1381
+ | --- | ---: |
1382
+ | both at the default | **18.75°** — the later one alone |
1383
+ | the same two, array order swapped | **7.50°** — the other one alone |
1384
+ | both `"additive": true` | **26.25°** — the sum |
1385
+ | an additive one *under* a non-additive one | 18.75° — the later one still wins |
1386
+
1387
+ So a two- or three-axis face wants `"additive": true` on **every** slider that
1388
+ shares a target. The first entry could keep the default — nothing runs before it —
1389
+ but do not: a non-additive slider also erases the **playing animation** on the
1390
+ bones its own animation keys, even sitting at its neutral where it looks switched
1391
+ off ([measured] an idle keying that bone to 4.00° reads 0.00° under a non-additive
1392
+ slider and 4.00° + the slider's own value under an additive one), and writing the
1393
+ flag on all of them is also what survives a reorder.
1394
+
1395
+ `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` refuses the rest — it names the bone or
1396
+ slot, the property, every slider keying it in array order with its flag, and which
1397
+ one wins today. ⛔ rigc does **not** set the flag for you. The compiler never
1398
+ invents a value that is not in the spec, and a rig whose composition was chosen by
1399
+ the tool is one nobody can reason about.
1400
+
1401
+ ⚠️ **And `"additive": true` is not always available.** Only some timelines support
1402
+ additive application at all: bone, deform, transform-constraint, path `position`,
1403
+ physics `wind`/`gravity`, and a slider's own `mix`. A **slot colour, an attachment
1404
+ swap, a draw order or a sequence ignores the flag entirely**, so two sliders
1405
+ sharing one of those overwrite each other whatever you write. A40 names that case
1406
+ separately, because the fix is different: key such a property from one slider
1407
+ only, or move both edits into the single animation one slider applies.
1408
+
1409
+ 🚨 **A `rotate`-driven slider with `local: false` cannot cross 0°.** `local: false`
1410
+ reads the bone's **world** rotation through `FromRotate.value`, which ends
1411
+ `if (value < 0) value += 360`. A yaw axis authored the natural way — neutral at 0°,
1412
+ range −15°..+15° — therefore never sees a negative value: the bone at −15° arrives
1413
+ as 345°, and `time = to + (value − from) * scale` lands far past the animation.
1414
+ [measured] with `from: 0, to: 0.5, scale: 0.033333` over a 1 s animation the dial
1415
+ at −15° applies time **12.000 s**, so with `loop: false` the entire negative half
1416
+ of the axis holds the last frame — 15° of face jumping between −0.001° and 0°, with
1417
+ nothing anywhere reporting it. The same rig with `"local": true` ramps
1418
+ 0 s → 0.5 s → 1 s as written.
1419
+
1420
+ ⇒ **`local: true` is the form a face axis wants.** It reads `source.rotation`
1421
+ signed and unwrapped, which for a dial bone is the number you authored. rigc
1422
+ refuses the crossing case at compile, with that arithmetic in the message; the
1423
+ alternative it leaves open is to move the range so it does not cross 0° (a neutral
1424
+ at 180°, say), which is the only form `local: false` can express.
1425
+
1426
+ ⚠️ **The refusal is on the LOW end only.** `[0, 360)` is the whole of what
1427
+ `FromRotate.value` returns, so a range running past **360°** is dead in the same
1428
+ way and compiles clean. Nothing refuses that at compile; `A39` reports it from the
1429
+ artifact side as a key at a time no dial selects (§4.11.4).
1430
+
1431
+ 📌 **A slider's animation is measured in the slider's own frame** — `A39` inverts
1432
+ the mapping above and drives the bone to the value it names, rather than playing
1433
+ the animation on a track while the slider sits at its neutral. §4.11.4 is what
1434
+ that changes and why it matters at `mix: 1`.
1435
+
1436
+ 🔸 **`scale` is rounded to six decimals on emit**, like every other number rigc
1437
+ writes, so `1/60` ships as `0.016667` — 2e-5 relative. Invisible in the middle of
1438
+ the range; it shows at the top of it, where a 60° turn then applies at 1.00002 s
1439
+ rather than 1 s. With `loop: false` that is the last frame and harmless, with
1440
+ `loop: true` it wraps to the start. When the range comes from a *measured* ceiling
1441
+ — the turn ceiling `build` reports for a depth mesh (§3.4, `depth`) is the natural
1442
+ one — pick `to`/`scale` so the endpoint lands **inside** the duration rather than
1443
+ exactly on it.
1444
+
1198
1445
  ### 3.6 `events` — names the animation can fire
1199
1446
 
1200
1447
  **When you need one:** something outside the skeleton has to happen on a
@@ -1268,6 +1515,17 @@ and the entry is gone. ⇒ An exemption whose `why` reads *"known defect, see
1268
1515
  against a fix, not a fix — and the thing that made it repayable was A39
1269
1516
  measuring the ceiling the art could actually take.
1270
1517
 
1518
+ 🚫 **Do not reach for it to cover a part you have faded out.** A key whose slot
1519
+ draws no pixels at that key's own time is already passed over — `A39` measures
1520
+ that and says so (§4.11, and the `skipped` line in the `DEFORM` block). Declaring
1521
+ the slot instead would turn the check off at every angle where the part is fully
1522
+ visible too, which is trading a false positive for a blind spot on the same slot
1523
+ ([#401](https://github.com/firejune/rigc/issues/401)). ⚠️ And land that alpha-0
1524
+ key **before** the folding key rather than on it — the frames in between are
1525
+ drawn and are gated (§4.11.3); this used to be a rule you had to follow and is
1526
+ now one the gate keeps
1527
+ ([#403](https://github.com/firejune/rigc/issues/403)).
1528
+
1271
1529
  ---
1272
1530
 
1273
1531
  ## 4. The motion spec, field by field
@@ -1980,6 +2238,10 @@ on a weighted one.** Its length is `vertices.length` in the first case and
1980
2238
  guess. Either key the control bone instead — which is what a rigc-generated ring
1981
2239
  or ribbon mesh is *for* — or write the bind-space pairs yourself and start the
1982
2240
  run with `offset`. The refusal tells you the index that vertex starts at.
2241
+ ⭐ A **`transform` key** (§4.11.1) is the exception, and for a reason that does
2242
+ not extend here: rigc evaluates that model itself, so it can state the
2243
+ displacement in world and push it into every influence through that bone's own
2244
+ inverse. `fromVertex` hands it a number you wrote in a space it cannot know.
1983
2245
 
1984
2246
  **The curve eases the blend.** A deform timeline has exactly one channel and
1985
2247
  `readCurve` builds it between **0 and 1** — the fraction of the way from this key's
@@ -2000,7 +2262,7 @@ animation half of the format:
2000
2262
  | a deform on a region attachment | `a deform timeline keys the vertices of an attachment, and this one is a "region"` |
2001
2263
  | `transform` beside a `vertices` run | `the key carries both a "transform" and a "vertices" run, and they are two answers to one question` |
2002
2264
  | `transform` with `fromVertex` or `offset` | `A transform is a model of the whole attachment and is evaluated over all 25 of its vertices, so it always starts at deform index 0` |
2003
- | a `transform` on a multi-bone or multi-space weighted attachment | `this attachment has no single space to evaluate it in — vertex 2 has 2 bone influences on it` |
2265
+ | a `transform` on an attachment whose weights do not close at 1 | `this one has a vertex the arithmetic cannot place — vertex 1's 2 weights sum to 0.9000 rather than 1` |
2004
2266
 
2005
2267
  The overrun is the one to fear. `readAnimation` copies with
2006
2268
  `Utils.arrayCopy(vertices, 0, deform, offset, vertices.length)` into a
@@ -2082,7 +2344,7 @@ example needed it:
2082
2344
 
2083
2345
  | `kind` | Parameters | What it evaluates | Worked case |
2084
2346
  | --- | --- | --- | --- |
2085
- | `yaw` | `radius`, `degrees`, `about` | `dx = (x−about)·(cos t − 1) − z·sin t`, `z = √(radius² − (x−about)²)` — the 2.5D turn (FACE §1) | `gallery/portrait` |
2347
+ | `yaw` | `radius` **or** `depth`, `degrees`, `about` | `dx = (x−about)·(cos t − 1) − z·sin t`, with `z = √(radius² − (x−about)²)` from a cylinder or `z` read per vertex off a depth map — the 2.5D turn (FACE §1) | `gallery/portrait` |
2086
2348
  | `pitch` | the same | the same expression with `y` for `x` — a nod rather than a turn | `gallery/nod` |
2087
2349
  | `affine` | `scale`, `about` | `dx = (sx−1)·(x−ax)`, `dy = (sy−1)·(y−ay)` — a scale about a fixed point | `gallery/squash` |
2088
2350
  | `wave` | `amplitude`, `wavelength`, `phase`, `along`, `axis` | `d = amplitude · sin(2π·along/wavelength + phase)` | `gallery/nod` |
@@ -2105,18 +2367,62 @@ of a run leaves a **step at the run's edge**, and that is one half of the defect
2105
2367
  [#313](https://github.com/firejune/rigc/issues/313) records. If you want a
2106
2368
  partial run, write it.
2107
2369
 
2370
+ **There is no `parallax` kind, and the reason is worth stating.** A pure depth
2371
+ slide — `d = z · offset`, no angle — is this form with a term dropped: subtract
2372
+ them and the whole remainder is `u·(cos t − 1)`, independent of the depth, so
2373
+ the slide *is* a turn at a small angle (at 16° the gap is 2.32px, at 1°
2374
+ 0.0091px, quartering with each halving). 🚨 And what it is for is not rigc's to
2375
+ state: a depth slide is a **camera** move, driven by a pointer rather than by a
2376
+ clock, and this spec is a timeline. What rigc states about a raised surface is
2377
+ the **angle** it may turn through, and — for a soft one — how it answers an
2378
+ **impact**. The camera belongs to whatever draws the result.
2379
+
2380
+ **A turn projects off one surface, stated one way.** `"depth": true` reads each
2381
+ vertex's `z` off the map its attachment's generator names (§3.4) instead of
2382
+ deriving it from a `radius`; the closed form does not change, only where `z`
2383
+ comes from. Saying both is refused — they are two answers to how far forward a
2384
+ vertex sits, and a key carrying both leaves a reader unable to say which one the
2385
+ output came from. So is `"depth": true` on an attachment that named no map,
2386
+ rather than a silent fall back to a cylinder.
2387
+
2108
2388
  **It is not a `rigc tween`.** MOTION §7 refuses a command that generates
2109
2389
  in-betweens and this is not one: the transform is evaluated **at one key**, from
2110
2390
  parameters that key states, and what happens between two keys is still the
2111
2391
  timeline's own single 0..1 blend channel. Sweeping an angle is editing one number
2112
2392
  per key.
2113
2393
 
2114
- **It needs one coordinate space.** On an unweighted attachment the deform array
2115
- is one `x, y` per vertex in the slot bone's space, so the model has a space to be
2116
- evaluated in. On a weighted one it does only while every vertex has exactly one
2117
- bone **and they all share it** — the same honesty rule `fromVertex` is bounded by
2118
- (§4.11), refused for the same reason: a multi-bone vertex's offset is a weighted
2119
- sum of a pair in each bone's own bind space, and rigc will not guess one.
2394
+ **It works on a weighted attachment, and reads world coordinates there**
2395
+ ([#389](https://github.com/firejune/rigc/issues/389)). On an unweighted
2396
+ attachment the deform array is one `x, y` per vertex in the slot bone's space,
2397
+ and the model is evaluated in that space. The same is true of a weighted
2398
+ attachment while every vertex has exactly one bone and they all share it — the
2399
+ array is that bone's bind space. **Past that, the model is evaluated at each
2400
+ vertex's setup *world* position instead**, and the displacement it produces is
2401
+ written into every influence as `Mᵢ⁻¹ · D`, where `Mᵢ` is that bone's setup world
2402
+ rotation. The runtime then composes `Σ wᵢ · Mᵢ · Mᵢ⁻¹ · D`, which is `D` because
2403
+ the weights close at 1; and once a bone moves, the displacement is carried by the
2404
+ same blend that carries the vertex. Nothing is guessed and nothing is
2405
+ approximated.
2406
+
2407
+ ⚠️ **So `radius`, `about`, `from` and `to` change units the moment a second bone
2408
+ touches any vertex** — they are read in the attachment's own space in the first
2409
+ case and in world coordinates in the second. `explain`'s `DEFORM` block says
2410
+ which, on a `derived` line that only appears on the world path; if you add a
2411
+ `soft` region (§3.4) to a part that already carries a turn key, read that line
2412
+ and check the numbers still mean what they meant. A `radius` smaller than the
2413
+ world coordinates it now spans is refused by name rather than clamped, which is
2414
+ the loud version of the same event.
2415
+
2416
+ The one weighting this cannot cover is a vertex whose weights do **not** sum to
2417
+ 1: `D · Σ wᵢ` is the whole identity, so there is no run that lands such a vertex
2418
+ where the model says. That is refused by name, at
2419
+ `A20_MESH_WEIGHTS_COHERENT`'s own 1e-3.
2420
+
2421
+ `fromVertex` on a multi-bone vertex is still refused (§4.11), and the difference
2422
+ is worth stating because the two look alike: `fromVertex` hands rigc a pair
2423
+ **you** wrote, in a space only you know, so rigc would have to guess. A model is
2424
+ a displacement rigc evaluates itself, so it can put it in world and push it
2425
+ through each bone.
2120
2426
 
2121
2427
  **It is gated like any other key.** `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` and
2122
2428
  `A39_DEFORM_KEEPS_TRIANGLE_WINDING` see the emitted numbers and know nothing
@@ -2188,19 +2494,24 @@ that is failing.
2188
2494
  deform (what each key does to the geometry — figures with names, never a bar; issue #316)
2189
2495
  .. every key measured at its OWN time against the same pose with the deform CLEARED, so the
2190
2496
  .. denominator is 1.000 by definition and a NEGATIVE area ratio IS a reversed triangle
2497
+ .. the FRAME is on each key line: on a track, or the slider that applies the animation with the
2498
+ .. dial value its mapping was inverted to — a slider picks the time, so the key's time IS it
2191
2499
  .. stretch is the two singular values of the map from the cleared triangle to the deformed one —
2192
2500
  .. the worst stretch and the worst squash the drawing takes there; their product is |area ratio|
2193
2501
  .. coverage is NOT here: it is rasterised from the uvs, which no deform moves, so the figure on
2194
2502
  .. the `meshes` line below is already the deformed one
2195
2503
  DEFORM turn default/head/head key 0 t=0.000000 authored table
2504
+ frame played on a track
2196
2505
  moved 0 of 25 vertices — this key IS the setup pose, so every figure is the identity (32 triangles, all kept)
2197
2506
  DEFORM turn default/head/head key 1 t=0.620000 transform yaw radius=170 degrees=12
2507
+ frame played on a track
2198
2508
  moved 25 of 25 vertices, worst 35.3450px at v2
2199
2509
  area min x0.637174 tri 17 max x1.319122 tri 31 (32 triangles, 0 with no area at the cleared pose, band 0.146694px²)
2200
2510
  stretch max x1.319121 tri 22 min x0.637175 tri 8
2201
2511
  winding 32 of 32 kept, 0 collapsed
2202
2512
  WORST turn area x0.637174 (head/head key 1 tri 17) stretch x1.319121 (head/head key 1 tri 22) squash x0.637175 (head/head key 1 tri 8)
2203
2513
  .. reversed 0, collapsed 0, over 8 key(s) and 192 triangle sample(s) <- A39 reads the same two counts
2514
+ .. 6 span(s) between consecutive keys scanned for a fold no key lands on: none folds (the closed form flagged nothing, so no span cost a posed measurement) <- A39 reads the same scan
2204
2515
  ```
2205
2516
 
2206
2517
  **The rows, and what each one is for:**
@@ -2208,11 +2519,13 @@ deform (what each key does to the geometry — figures with names, never a bar;
2208
2519
  | Row | What it is |
2209
2520
  | --- | --- |
2210
2521
  | the `DEFORM` line | `animation`, the `skin/slot/attachment` triple the timeline is keyed on, the key's index — **the same index `A39`'s message names** — its time, and its model: the `transform` kind and parameters the spec stated (§4.11.1), or `authored table` |
2522
+ | `frame` | how the animation was reached, which is the pose everything below was measured in: `played on a track`, or the slider that applies it with the dial value its mapping inverts this time to (§4.11.4). On a key at a time no dial selects, an `unreachable` line follows it and the figures below belong to the frame the runtime landed on instead |
2211
2523
  | `moved` | how many vertices this key moves at all, and the largest **world** displacement with the vertex carrying it. Not the same number as §4.11.1's `largest offset`: that one is the offset the spec stated, this one is where the vertex ended up after the bones |
2212
2524
  | `area` | signed area **after ÷ before**, its smallest and largest over the triangles, each with the triangle. `x0.637` is a band compressed to 64%; **a negative ratio is a triangle turned inside out** |
2213
2525
  | `stretch` | the two singular values of the map from the cleared triangle to the deformed one — the worst stretch and the worst squash the **drawing** takes. `σ₁·σ₂ = \|area ratio\|`, so the two rows are two readings of one map and cannot disagree |
2214
2526
  | `winding` | triangles whose winding survived, and how many the key pinched onto zero area. A fold says so and points at `A39` |
2215
- | `WORST` | per animation: the worst key by each quantity, then the reversal and collapse totals over every key |
2527
+ | the `BETWEEN` line | a fold at a time **no key lands on** (§4.11.3): the two keys it lies between, the time, the segment's curve kind, and how far along the interpolation it is. Present only where one was found |
2528
+ | `WORST` | per animation: the worst key by each quantity, then the reversal and collapse totals over every key, then how many spans between keys were scanned and what the scan found |
2216
2529
 
2217
2530
  📌 **The frame is the posed one, and the denominator is 1.000 by definition.**
2218
2531
  Both sides of every comparison are taken at the key's own time with the animation
@@ -2241,6 +2554,25 @@ ratio would go stale the moment somebody moved a column.
2241
2554
  saying nothing there is gated. An author who has declared a fold is the one
2242
2555
  person with no other way to see how far it goes.
2243
2556
 
2557
+ ⚠️ **A key that draws no pixels says so, on a `skipped` line.** When the slot has
2558
+ faded to alpha exactly 0 at that key's own time, or shows another attachment,
2559
+ `A39` reads no winding off it — and the block prints the reason in the same words
2560
+ the gate counted it under, keeps the key's own figures where there are any, and
2561
+ leaves it out of the `WORST` rollup's counts, because that line ends by claiming
2562
+ `A39` reads the same two. The rollup then carries a second line naming how many
2563
+ keys were passed over and how many reversed triangles nothing gated:
2564
+
2565
+ ```
2566
+ DEFORM turn default/head/head key 1 t=0.500000 transform yaw radius=170 degrees=40
2567
+ skipped A39 reads no winding off this key: the slot's alpha is exactly 0 at this
2568
+ time (slot 0.0000 x attachment 1.0000), so this key draws no pixels — a
2569
+ triangle that draws no pixels cannot draw them backwards
2570
+ ...
2571
+ winding 24 of 32 kept, 0 collapsed <- a fold, and nothing gates it: this key draws no pixels (see above)
2572
+ .. 1 key(s) draw no pixels at their own time and are read for no winding, carrying
2573
+ 8 reversed triangle(s) nothing gates <- A39 counts them as deformKeysNotDrawn
2574
+ ```
2575
+
2244
2576
  🚫 **What it does not print, and why — `coverage`.** A deform **cannot move
2245
2577
  coverage.** That figure is rasterised from the attachment's **uvs** against the
2246
2578
  part's alpha, and a deform moves positions and never uvs, so it is identical at
@@ -2268,6 +2600,143 @@ and displaces `x` over row-major quads preserves every signed area exactly.
2268
2600
 
2269
2601
  ---
2270
2602
 
2603
+ ### 4.11.3 The times no key lands on — `A39` between two keys
2604
+
2605
+ 🚨 **Your keys are not where the runtime is.** It interpolates between them, so a
2606
+ deform that is inside its fold angle at *every* key can be past it in between —
2607
+ and until issue
2608
+ [#403](https://github.com/firejune/rigc/issues/403) nothing looked there. The
2609
+ reachable version of that was the fade above: land the alpha-0 key **on** the
2610
+ folding key and every key is honest — that one really does draw nothing — while
2611
+ the frames just before it are drawn, nearly folded, and land on no key at all. On
2612
+ the turn probe that is **8 reversed triangles at alpha 0.20, gating green**.
2613
+
2614
+ ⇒ `A39` now scans every interval between two consecutive deform keys as well, and
2615
+ refuses one with its own sentence:
2616
+
2617
+ ```
2618
+ FAIL A39_DEFORM_KEEPS_TRIANGLE_WINDING: animation "turn" deform head/head BETWEEN key 0
2619
+ (t=0s) and key 1 (t=0.5s), at t=0.444089s — 88.8% of the way from one to the other:
2620
+ 8 of 32 triangle(s) reverse winding — triangle 0 [0,15,16] 1890.001 -> -272.314px²; …
2621
+ NO KEY LANDS THERE: the runtime interpolates between the two keys, and the mesh is
2622
+ inside out for part of the way, drawing its texture backwards at alpha 0.1118 …
2623
+ ```
2624
+
2625
+ **What to change when you see it**, in the order worth trying:
2626
+
2627
+ | | |
2628
+ | --- | --- |
2629
+ | the fade landed **on** the fold | move the alpha-0 key **before** the folding key, so every frame that folds is a frame that draws nothing. This is the case the message calls out by name |
2630
+ | two keys are simply too far apart | add a key inside the span, so the geometry the runtime passes through is geometry you wrote rather than geometry it inferred |
2631
+ | the two keys' models disagree | move their offsets closer together — a projection past its fold angle is the usual cause, and [FACE.md §4.2](FACE.md) has the closed form |
2632
+ | the fold is the drawing | `invariants.deformMayFold` (§3.7), as for a key |
2633
+
2634
+ **Three things the scan is, and one it is not:**
2635
+
2636
+ - **Solved, not sampled.** A deform interpolated between two keys travels a
2637
+ straight line through offset space, so a triangle's signed area is a
2638
+ **quadratic in the interpolation fraction** and the fold is a root of it. There
2639
+ is no subdivision count and so no sample spacing to argue about.
2640
+ - **Measured before it refuses.** The closed form only decides *where to look*;
2641
+ the time it names is then posed and measured by exactly the code that measures
2642
+ a key. So a `BETWEEN` refusal is the same measurement as a key refusal, taken
2643
+ at a time no key holds.
2644
+ - **Alpha-aware at that same time.** The fade interpolates too, so the alpha is
2645
+ read at the moment the geometry is — otherwise this would refuse the very
2646
+ frames a correct fade is hiding. A fold that lands only where nothing is drawn
2647
+ is reported and not gated, exactly as a key's is.
2648
+ - ⚠️ **Not a claim about the bones.** The closed form holds them still. On an
2649
+ *unweighted* attachment that costs nothing — one bone matrix multiplies every
2650
+ vertex and its determinant cancels out of the comparison — but on a **weighted**
2651
+ mesh whose bones move across the span it is an approximation, and a prediction
2652
+ no measurement reproduced is reported as `deformSpansUnconfirmed` rather than
2653
+ refused. Nothing on this surface can see a fold caused by the bones alone;
2654
+ `rigc check` against a trusted render is what can.
2655
+
2656
+ 📌 **A stepped segment holds rather than interpolates**, so it introduces no
2657
+ geometry the keys do not already have — but it is still scanned, because what it
2658
+ holds that geometry across is a stretch of time whose *alpha* is the next key's
2659
+ business. A key that folds at alpha 0 and is held, stepped, while the slot fades
2660
+ back in is refused, and the message says the segment interpolates nothing.
2661
+
2662
+ 📌 **Silence is not a pass here either.** `deformSpansScanned` is on the stats
2663
+ line of every build `A39` runs on, and the `WORST` rollup names it too, because
2664
+ "the scan ran and found nothing" and "the scan never ran" must not print the
2665
+ same. `deformSpanProbes` beside it is what the scan cost: 0 on a rig nothing was
2666
+ predicted in, one posed measurement per predicted window otherwise.
2667
+
2668
+ ---
2669
+
2670
+ ### 4.11.4 Which frame a deform key is measured in — a slider changes it
2671
+
2672
+ Everything above says *at the key's own time*, and on an ordinary animation that
2673
+ means one thing: played on track 0. **An animation a `slider` applies (§3.5.2) is
2674
+ never played that way** — spine-core says so in `SkeletonData.findSliderAnimations`
2675
+ — and posing it as though it were is a frame no playthrough contains. The dial
2676
+ picks the time, so **the key's time and the applied time are the same number by
2677
+ construction**; measuring them independently was issue
2678
+ [#407](https://github.com/firejune/rigc/issues/407), and it refused a correct rig.
2679
+
2680
+ ⇒ For every deform key of an animation some slider applies, `A39` **inverts that
2681
+ slider's own mapping** and drives its bone until the runtime selects this key's
2682
+ time, then measures there:
2683
+
2684
+ ```
2685
+ time = to + (value − from) × scale the slider's mapping (§3.5.2)
2686
+ value = from + (time − to) / scale what A39 sets the dial to
2687
+ ```
2688
+
2689
+ 🔒 **The `DEFORM` block prints the frame on every key**, because the derivation
2690
+ changed and a block that went on printing the same figures under a changed meaning
2691
+ would be worse than the red it replaced:
2692
+
2693
+ ```
2694
+ DEFORM turn default/head/head key 6 t=1.900000 transform yaw depth=true degrees=19
2695
+ frame applied by slider "yaw" off yaw_dial.rotate (local), dial 19.000000 -> t=1.900000
2696
+ ```
2697
+
2698
+ `A39`'s stats line carries `deformFrames` on any build where a slider chose one,
2699
+ and its refusals name the slider — `animation "turn" (applied by slider "yaw", not
2700
+ played on a track) deform …` — because the same key can be refused through one
2701
+ dial and not reached at all through another.
2702
+
2703
+ **Three consequences worth knowing before you build a rig like this:**
2704
+
2705
+ - ⭐ **A slider at `mix: 1` in setup no longer forces the older idiom.** Muting at
2706
+ setup and keying `slider.<name>.mix` from a playing animation (§4.12) is still
2707
+ legitimate, but it is no longer the only way to keep `A39` honest about a slot
2708
+ the animation itself fades — and it costs `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET`,
2709
+ which excludes a slider below full authority. A slider muted at setup is **not**
2710
+ a way in: `Slider.update` returns before it reads the bone, so its animation
2711
+ keeps the track frame.
2712
+ - **Two sliders applying one animation are two frames**, and both are measured.
2713
+ A fold only one dial can reach is still a fold.
2714
+ - ⚠️ **A key at a time no dial can select is named, not passed.** The cause is
2715
+ [#405](https://github.com/firejune/rigc/issues/405)'s wrap: `FromRotate.value`
2716
+ under `local: false` is an `atan2` ending `if (value < 0) value += 360`, so
2717
+ **`[0, 360)` is the whole of its range** and a mapping needing anything outside
2718
+ it selects nothing. 🚨 The compiler refuses a range that dips **below** 0°
2719
+ (§3.5.2) and says nothing about one running **past 360°** — `from: 300` with
2720
+ `scale: 0.005` over a 1 s animation needs 300°..500° and compiles clean — so
2721
+ this is the surface that sees the second half. `A39` measures the frame the
2722
+ runtime *does* land on, leaves the key out of `deformKeysMeasured`, and names
2723
+ it:
2724
+
2725
+ ```
2726
+ deformKeysUnreachable=2 deformUnreachable=turn/head/head#0@10.800000,turn/head/head#1@11.300000
2727
+ deformSpansNotScanned=2
2728
+ ```
2729
+
2730
+ — the `@` is the time the runtime lands on instead — with the `DEFORM` block
2731
+ giving the whole sentence and the spans those keys bound left unscanned rather
2732
+ than scanned over two poses of some other time.
2733
+
2734
+ ⚠️ **What the artifact cannot say, and rigc therefore does not:** whether a
2735
+ slider's animation is *also* played on a track somewhere. Nothing in skeleton data
2736
+ records that, so a slider-applied animation is measured in its slider frames only.
2737
+
2738
+ ---
2739
+
2271
2740
  ### 4.12 `path` and `slider` timelines — tracks, not their own groups
2272
2741
 
2273
2742
  Unlike `ik` and `transform`, these two are ordinary `tracks` entries: the format
@@ -2298,7 +2767,7 @@ tell a working traversal from a plausible one.
2298
2767
  | `path` | `spacing` | 1 | in the unit `spacingMode` chose |
2299
2768
  | `path` | `mix` | **3** | `[mixRotate, mixX, mixY]` in one key, so a raw `curve` is 12 numbers |
2300
2769
  | `slider` | `time` | 1 | the bone-less slider's own time. A slider WITH a bone takes its time from the bone and this timeline is not what drives it |
2301
- | `slider` | `mix` | 1 | mix 0 makes `update()` return, so this is the on/off switch |
2770
+ | `slider` | `mix` | 1 | mix 0 makes `update()` return, so this is the on/off switch. It is also what decides the frame a deform key of that slider's animation is measured in — §4.11.4 |
2302
2771
 
2303
2772
  ⚠️ **A `path.mix` key states all three mixes.** In the file `mixY` defaults to the
2304
2773
  same key's `mixX`, and every field has a per-key default rather than carrying the
@@ -2424,7 +2893,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
2424
2893
  | `deform …: slot "X" in skin "default" has no attachment "Y" (it has: …)` | §4.11 — fix the placeholder name |
2425
2894
  | `deform … (t=…): the key carries both a "transform" and a "vertices" run` | §4.11.1 — a model and a table are two answers to one question; drop one |
2426
2895
  | `deform … (t=…): the key states a "transform" and a start index` | §4.11.1 — a model covers every vertex, so drop the index or write the partial run by hand |
2427
- | `deform … (t=…): … has no single space to evaluate it in` | §4.11.1 — the attachment's vertices are in several bind spaces; key the control bone, or write the pairs with `offset` |
2896
+ | `deform … (t=…): … has a vertex the arithmetic cannot place — vertex v's N weights sum to …` | §4.11.1 — a model lands a vertex by `D · Σ wᵢ`, so the weights have to close at 1; fix them, or write the pairs with `offset` |
2428
2897
  | `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 |
2429
2898
  | `deform … (t=…): transform affine has scale […], whose determinant is …` | §4.11.1 — at or below zero the map reverses every triangle |
2430
2899
  | `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 |
@@ -2436,6 +2905,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
2436
2905
  | `rig constraint "X": applies animation "Y", which the motion spec does not declare (it declares: …)` | §3.5.2 — fix the slider's `animation`, or add it to the motion spec |
2437
2906
  | `rig constraint "X": declares both a "bone" and "time"` | §3.5.2 — `bone` picks the model and `time` belongs to the other one |
2438
2907
  | `rig constraint "X": declares "property" but no "bone"` | §3.5.2 — name the driving bone, or key `slider.<name>.time` instead |
2908
+ | `rig constraint "X": drives off bone "Y" rotate with "local": false, and the driving values that reach animation "A" (0s..Ds) run from −15.000° to 15.000° …` | §3.5.2 — add `"local": true`, which reads the bone's own rotation signed and unwrapped, or move the range so it does not cross 0°. A world rotation is wrapped into `[0, 360)` before the slider maps it, so the negative half of the range is unreachable and pins to one frame |
2439
2909
  | `skin "S" activates bone "B", but that bone does not declare \`"skin": true\`` | §3.4.1 — the list and the flag are one switch; add the flag or drop the list |
2440
2910
  | `bone "B" declares \`"skin": true\` but no skin activates it` | §3.4.1 — the other half: list it in the skin it belongs to, or drop the flag |
2441
2911
  | `skin "S": uses the long form … and also has a key "X"` | §3.4.1 — move the slot inside `attachments` |
@@ -2506,7 +2976,8 @@ The report prints one line per assertion:
2506
2976
  | `A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying its `mix` (§3.5.1). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. **SKIP** when the skeleton declares no path constraint |
2507
2977
  | `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` (§3.5.2). **SKIP** when the skeleton declares no slider |
2508
2978
  | `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
2509
- | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, or when there is no rig info at all |
2979
+ | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is refused as before. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3, issue #403): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4, issue #407): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
2980
+ | `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` — it writes that property outright at `mix: 1` and every earlier slider on it is dead (§3.5.2). Also fires when the shared timeline **cannot** be additive (a slot colour, an attachment swap, a draw order, a sequence), where `"additive": true` is not the fix and one of the two has to go. The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, and which one wins today. Three shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, and two sliders on different properties. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
2510
2981
 
2511
2982
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
2512
2983
  clauses are gated by profile.