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/README.md +67 -6
- package/cli.ts +368 -19
- package/docs/AUTHORING.md +570 -99
- package/docs/FACE.md +256 -0
- package/docs/MOTION.md +52 -0
- package/package.json +2 -1
- package/skills/motion/SKILL.md +1 -1
- package/src/compile.ts +770 -41
- package/src/deformgen.ts +125 -18
- package/src/deformmeasure.ts +1469 -138
- package/src/depth.ts +610 -0
- package/src/mesh.ts +104 -1
- package/src/rig.ts +124 -0
- package/src/transform.ts +24 -0
- package/src/types.ts +66 -3
- package/src/validate.ts +464 -8
- package/tools/editor_roundtrip.ts +557 -0
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
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
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
|
-
**
|
|
917
|
-
and
|
|
918
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
"
|
|
928
|
-
|
|
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
|
-
| `
|
|
942
|
-
| `
|
|
943
|
-
| `
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
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
|
-
|
|
965
|
-
|
|
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
|
-
|
|
970
|
-
|
|
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
|
-
|
|
973
|
-
|
|
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
|
-
"
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
| `
|
|
989
|
-
| `
|
|
990
|
-
| `
|
|
991
|
-
| `
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
|
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
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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
|
-
| `
|
|
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
|
|
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.
|