spine-rigc 0.16.0 → 0.17.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/cli.ts CHANGED
@@ -114,6 +114,7 @@ import {
114
114
  } from './src/render.ts';
115
115
  import { CLI_DEFAULT_PROFILE, reportLines, validate, VALIDATE_PROFILES, type ValidateProfile } from './src/validate.ts';
116
116
  import { parseMotionSpec } from './src/motion.ts';
117
+ import type { FoldLimit, TurnCeiling } from './src/depth.ts';
117
118
  import type { CompileResult } from './src/types.ts';
118
119
 
119
120
  /**
@@ -407,10 +408,72 @@ function runGate(
407
408
  * unconditionally, so a build whose only mesh was a ribbon or a contour got a
408
409
  * sentence about a rim ring and a seam it does not have.
409
410
  */
411
+ /**
412
+ * What a depth map and a soft region put on a mesh, when it named either.
413
+ *
414
+ * The digests are the reason this prints at all: a claim about a rig can name
415
+ * WHICH sheet produced it, and two runs a reader believes differ can be shown to
416
+ * have read the same pixels. The ranges and counts are what say the input
417
+ * reached the geometry rather than merely being resolved — a `carried 0` never
418
+ * gets here (it is refused) and a `ramped 0` is a hard-edged mask, which is
419
+ * legal and usually not what somebody meant.
420
+ */
421
+ /**
422
+ * One axis's two ceilings, as `+31.41 / -18.03`, or what is unbounded on it.
423
+ *
424
+ * ⚠️ `none` and a number are different claims and are printed differently. A
425
+ * sheet with no gradient along an axis cannot fold anything on it AT ANY ANGLE,
426
+ * which is a fact about the sheet worth reading; printing `90` for it would be
427
+ * a limit nothing measured.
428
+ */
429
+ function ceilingPair(axis: { positive: FoldLimit | null; negative: FoldLimit | null }): string {
430
+ const one = (l: FoldLimit | null, sign: string) => (l === null ? `${sign}none` : `${sign}${l.degrees.toFixed(2)}°`);
431
+ return `${one(axis.positive, '+')} / ${one(axis.negative, '-')}`;
432
+ }
433
+
434
+ /** The tightest of the four, so the line that names a triangle names the right one. */
435
+ function tightestFold(c: TurnCeiling): { kind: string; sign: string; limit: FoldLimit } | null {
436
+ const all = [
437
+ { kind: 'yaw', sign: '+', limit: c.yaw.positive },
438
+ { kind: 'yaw', sign: '-', limit: c.yaw.negative },
439
+ { kind: 'pitch', sign: '+', limit: c.pitch.positive },
440
+ { kind: 'pitch', sign: '-', limit: c.pitch.negative },
441
+ ].filter((e): e is { kind: string; sign: string; limit: FoldLimit } => e.limit !== null);
442
+ if (all.length === 0) return null;
443
+ return all.reduce((best, e) => (e.limit.degrees < best.limit.degrees ? e : best));
444
+ }
445
+
446
+ function meshDepthNote(m: CompileResult['meshes'][number]): string {
447
+ const parts: string[] = [];
448
+ if (m.depth) {
449
+ parts.push(
450
+ `depth "${m.depth.image}" ${m.depth.digest} near=${m.depth.near} zScale=${m.depth.zScale} ` +
451
+ `z=[${m.depth.range[0]}, ${m.depth.range[1]}]`,
452
+ );
453
+ const c = m.depth.ceiling;
454
+ parts.push(`turn ceiling yaw ${ceilingPair(c.yaw)} pitch ${ceilingPair(c.pitch)}`);
455
+ const worst = tightestFold(c);
456
+ parts.push(
457
+ worst === null
458
+ ? ` nothing in this sheet folds: ${c.measured} triangle(s) measured, none with a depth gradient across it`
459
+ : ` first to fold: ${worst.kind} ${worst.sign} at ${worst.limit.degrees.toFixed(2)}°, ` +
460
+ `triangle ${worst.limit.triangle} [${worst.limit.ids.join(',')}]` +
461
+ `${c.degenerate ? `; ${c.degenerate} triangle(s) too flat in setup to measure` : ''}`,
462
+ );
463
+ }
464
+ if (m.soft) {
465
+ parts.push(
466
+ `soft "${m.soft.mask}" ${m.soft.digest} -> ${m.soft.bone}, ${m.soft.carried} carried / ${m.soft.ramped} in the falloff`,
467
+ );
468
+ }
469
+ return parts.length === 0 ? '' : `\n ${parts.join('\n ')}`;
470
+ }
471
+
410
472
  const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> = {
411
473
  ring: 'ring rim ring pinned on the window edge, seam ring pinned on the mask contour, aperture moves',
412
474
  ribbon: 'ribbon entry row pinned, rows share their weights so the strip lengthens without widening',
413
475
  contour: 'contour the art\'s own silhouette, every vertex pinned to the slot bone (geometry, not a deformation)',
476
+ grid: 'grid a lattice over the part window at stated column and row positions, every vertex pinned to the slot bone',
414
477
  authored: 'authored geometry rigc did not build; it assumes nothing about the topology',
415
478
  };
416
479
 
@@ -796,7 +859,8 @@ function cmdBuild(flags: Record<string, string>): void {
796
859
  for (const m of result.meshes) {
797
860
  console.log(
798
861
  ` MESH ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
799
- `${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}`,
862
+ `${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}` +
863
+ meshDepthNote(m),
800
864
  );
801
865
  }
802
866
  for (const ph of result.physics) {
@@ -2223,9 +2287,14 @@ function cmdExplain(flags: Record<string, string>): void {
2223
2287
  console.log('\nmeshes');
2224
2288
  for (const kind of new Set(result.meshes.map((m) => m.kind))) console.log(` ${MESH_KIND_NOTES[kind]}`);
2225
2289
  for (const m of result.meshes) {
2290
+ // The depth block belongs here more than it belongs in `build`: `explain`
2291
+ // is the command that says what a spec MEANS, and the turn ceiling is the
2292
+ // number an author needs before writing a key rather than after a refusal.
2293
+ // It was absent, while `docs/AUTHORING.md` said both commands printed it.
2226
2294
  console.log(
2227
2295
  ` ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
2228
- `${meshBudget(result.rig)} bones=[${m.bones.join(', ')}]${meshFit(m)}`,
2296
+ `${meshBudget(result.rig)} bones=[${m.bones.join(', ')}]${meshFit(m)}` +
2297
+ meshDepthNote(m),
2229
2298
  );
2230
2299
  }
2231
2300
  }
package/docs/AUTHORING.md CHANGED
@@ -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,206 @@ 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 3see 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
+ first to fold: yaw + at 31.41°, triangle 960 [113,112,593]
1007
+ ```
1008
+
1009
+ Past that angle a triangle turns inside out and `A39` refuses the build by name.
1010
+ The loop this replaces is *pick an angle, build, read the refusal, guess again*.
1011
+
1012
+ The arithmetic is exact and worth knowing, because it tells you what to change.
1013
+ A `yaw` sends each vertex to `x' = u·cos t − z·sin t`, so a triangle's area is
1014
+ `A₀·cos t − A_yaw·sin t` where `A_yaw` is the same area with **z substituted for
1015
+ u** — and it reaches zero at `tan t = A₀/A_yaw`. Three consequences:
1016
+
1017
+ - ⚠️ **The ceiling is a property of the SHEET, not of the mesh.** Over a smooth
1018
+ map the limit approaches `1 / max|dz/du|`, the reciprocal of its steepest
1019
+ slope, with no mesh term in it. Refining the lattice does not lower the angle
1020
+ — it finds slopes that were always there. So a ceiling you cannot live with is
1021
+ fixed by editing the depth map, not by meshing differently:
1022
+ [`docs/FACE.md` §2.2](FACE.md) has the measured ladder and the rule.
1023
+ - **The four numbers are four different answers**, each from its own triangle.
1024
+ A part that turns 30° left and 18° right is ordinary, not an anomaly.
1025
+ - **`none` means no gradient on that axis**, and is a different statement from a
1026
+ large number. A sheet varying linearly along one axis cannot fold the other at
1027
+ *any* angle — `A_pitch` is then identically zero — so a linear ramp reports a
1028
+ yaw ceiling and `pitch +none / -none`.
1029
+
1030
+ ⛔ It is a **report and never a refusal**. `A39` owns the refusal, from the
1031
+ artifact and through the runtime; a second wall here would be the compiler
1032
+ inventing a policy out of a measurement. What holds the two together is a
1033
+ control: `TC01` requires this number to be the angle `A39` actually fires at, on
1034
+ the triangle `A39` actually names, to 0.01°.
1035
+
1036
+ 🚨 **The one that will catch you: the sheet has to cover the mesh, and usually
1037
+ it does not.** A contour mesh puts every vertex *on* the silhouette and pushes it
1038
+ `margin` pixels outside; a grid spans the whole window, corners included. A depth
1039
+ sheet is usually cut to the art's own alpha, so both reach past it — and a naive
1040
+ sample gives those vertices the background depth and folds them away from the
1041
+ turn, with correct arithmetic and plausible numbers all the way down. rigc
1042
+ refuses it instead:
1043
+
1044
+ | The input | What you get |
1045
+ | --- | --- |
1046
+ | 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.` |
1047
+ | 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 |
1048
+ | 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` |
1049
+ | a colour sheet | `the depth map … is not greyscale — pixel (0, 0) is rgb(10, 200, 10)` |
1050
+ | `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"` |
1051
+ | `gamma` or `contrast` at or below 0 | `collapses the range onto the midpoint … so the map would describe a flat part` |
1052
+ | a `near` that is neither | `it is "white" or "black"` |
1053
+
1054
+ A sheet with **no alpha channel** covers its whole grid by construction and the
1055
+ coverage check has nothing to test; what its background level means is then your
1056
+ statement, and the reported range is where it shows up.
1057
+
1058
+ ##### `soft` — which part is soft, and which bone carries it
1059
+
1060
+ A mesh can say **which vertices are soft**, so a `physics` constraint (§3.5) on
1061
+ their bone answers an impact over exactly that region — a chest, a cheek, a
1062
+ hanging sleeve.
1063
+
1064
+ ```json
1065
+ "generator": {
1066
+ "kind": "grid", "cols": 9, "rows": 9,
1067
+ "depth": { "image": "face_depth.png", "near": "white", "zScale": 40 },
1068
+ "soft": { "bone": "cheek_wobble", "mask": "cheek_soft.png" }
1069
+ }
1070
+ ```
1015
1071
 
1072
+ | Field | Meaning |
1073
+ | --- | --- |
1074
+ | `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 |
1075
+ | `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 |
1076
+
1077
+ The remainder always stays on the slot bone, so every vertex closes at 1 by
1078
+ construction rather than by `A20` catching it later. `build` and `explain` report
1079
+ the mask's digest, how many vertices were carried outright and how many landed in
1080
+ the painted falloff.
1081
+
1082
+ 🚨 **Why a painted mask and not a depth threshold.** rigc tried the threshold —
1083
+ "the near part wobbles" — for exactly one day. It is wrong, and instructively so:
1084
+ **softness and prominence are different properties of a drawing.** The most
1085
+ prominent thing on a face is the nose, and a nose does not wobble. A threshold
1086
+ produced a region that was plausible, gated green and carried the wrong pixels.
1087
+ It also claimed something untrue — "no mask painted" — while the renderer this
1088
+ was modelled on had a hand-painted spring mask all along. rigc does not get to
1089
+ delete an input by guessing it.
1090
+
1091
+ ⭐ The falloff is painted for the same reason. A `feather` parameter would be
1092
+ rigc guessing the shape of something you can simply draw.
1093
+
1094
+ **`A21_MESH_RIM_PINNED` splits on this rather than being relaxed.** A wobbling
1095
+ silhouette is *supposed* to move; the invariant is that nothing else does. Every
1096
+ vertex must be pinned to the slot bone or shared between it and the one declared
1097
+ bone, must close at 1, and at least one must actually be carried.
1098
+
1099
+ | The input | What you get |
1100
+ | --- | --- |
1101
+ | 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` |
1102
+ | the slot's own bone | `moves nothing — a soft region needs a bone that can move independently` |
1103
+ | a mask that is black everywhere | `carries no vertex of this mesh — every one of its 49 vertices samples black` |
1104
+ | a colour mask | `is not greyscale — pixel (0, 0) is rgb(10, 200, 10)` |
1105
+ | a mask that is not the part's size | `is 48x32 and the part is 96x64` |
1106
+ | a mask that is not on disk | `the soft mask "x.png" is not at …` |
1107
+
1108
+ 🚨 **What it cannot do yet.** A carried mesh has two bones on some vertices, and
1109
+ a `transform` key (§4.12) needs one coordinate space to be evaluated in — so the
1110
+ **angle** a raised surface turns through and the **impact** a soft one answers
1111
+ cannot ride the *same* attachment today. `SF03` holds that refusal, so the day it
1112
+ changes something fails and says so;
1113
+ [#389](https://github.com/firejune/rigc/issues/389) has the arithmetic showing it
1114
+ need not be true. Until then, put the two on separate slots.
1016
1115
  #### 3.4.1 A skin that switches bones and constraints on
1017
1116
 
1018
1117
  **When you need one:** a skin that is more than a change of art — a variant with an
@@ -2082,7 +2181,7 @@ example needed it:
2082
2181
 
2083
2182
  | `kind` | Parameters | What it evaluates | Worked case |
2084
2183
  | --- | --- | --- | --- |
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` |
2184
+ | `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
2185
  | `pitch` | the same | the same expression with `y` for `x` — a nod rather than a turn | `gallery/nod` |
2087
2186
  | `affine` | `scale`, `about` | `dx = (sx−1)·(x−ax)`, `dy = (sy−1)·(y−ay)` — a scale about a fixed point | `gallery/squash` |
2088
2187
  | `wave` | `amplitude`, `wavelength`, `phase`, `along`, `axis` | `d = amplitude · sin(2π·along/wavelength + phase)` | `gallery/nod` |
@@ -2105,6 +2204,24 @@ of a run leaves a **step at the run's edge**, and that is one half of the defect
2105
2204
  [#313](https://github.com/firejune/rigc/issues/313) records. If you want a
2106
2205
  partial run, write it.
2107
2206
 
2207
+ **There is no `parallax` kind, and the reason is worth stating.** A pure depth
2208
+ slide — `d = z · offset`, no angle — is this form with a term dropped: subtract
2209
+ them and the whole remainder is `u·(cos t − 1)`, independent of the depth, so
2210
+ the slide *is* a turn at a small angle (at 16° the gap is 2.32px, at 1°
2211
+ 0.0091px, quartering with each halving). 🚨 And what it is for is not rigc's to
2212
+ state: a depth slide is a **camera** move, driven by a pointer rather than by a
2213
+ clock, and this spec is a timeline. What rigc states about a raised surface is
2214
+ the **angle** it may turn through, and — for a soft one — how it answers an
2215
+ **impact**. The camera belongs to whatever draws the result.
2216
+
2217
+ **A turn projects off one surface, stated one way.** `"depth": true` reads each
2218
+ vertex's `z` off the map its attachment's generator names (§3.4) instead of
2219
+ deriving it from a `radius`; the closed form does not change, only where `z`
2220
+ comes from. Saying both is refused — they are two answers to how far forward a
2221
+ vertex sits, and a key carrying both leaves a reader unable to say which one the
2222
+ output came from. So is `"depth": true` on an attachment that named no map,
2223
+ rather than a silent fall back to a cylinder.
2224
+
2108
2225
  **It is not a `rigc tween`.** MOTION §7 refuses a command that generates
2109
2226
  in-betweens and this is not one: the transform is evaluated **at one key**, from
2110
2227
  parameters that key states, and what happens between two keys is still the
package/docs/FACE.md CHANGED
@@ -245,6 +245,139 @@ shape the drawing only implies. Two readings that help:
245
245
  measured 5.406 on the artifact. If the fringe does not read as separate,
246
246
  this is the one number to move, and moving it moves nothing else.
247
247
 
248
+ ### 2.1 One depth per part, and the limit of that
249
+
250
+ Everything above states **one depth per part** — a stand-off the whole plate
251
+ shares. That is the right resolution for a part list: the fringe is 26 in front
252
+ of the skull, and arguing about which *pixel* of the fringe is 26 would be
253
+ arguing past what the drawing says.
254
+
255
+ It stops being the right resolution the moment a part's own surface is the
256
+ subject. §4 meshes the face plate into columns precisely because the plate is not
257
+ flat, and §4.2 then finds that refining those columns makes the fold **worse**,
258
+ not better — the columns are sampling a cylinder that was never the shape of a
259
+ face. A cylinder is one number pretending to be a surface, and past a certain
260
+ density the pretence is what fails.
261
+
262
+ ⭐ **Two things had to arrive together, and both are now generated rather than
263
+ written.** The lattice itself was hand-numbered — this example's 25 vertex pairs,
264
+ 32 triangles and hull walk were a person's arithmetic — and
265
+ [AUTHORING §3.4](AUTHORING.md#grid--a-lattice-over-the-part-window)'s `grid`
266
+ generator now builds it from the column positions alone, reproducing this one
267
+ exactly. That is the prerequisite: a turn needs interior vertices to move, and
268
+ §4.3's contour has none.
269
+
270
+ ⭐ **And a depth map is the number above becoming a surface.** A greyscale sheet in the
271
+ part's own pixel grid gives every mesh vertex its own `z`, sampled where the
272
+ vertex actually is, and `yaw`/`pitch` project off it with the same closed form as
273
+ §1 — only the source of `z` changes. What it buys is stated where it is authored:
274
+ [AUTHORING §3.4](AUTHORING.md#depth--give-every-vertex-its-own-z-instead-of-one-cylinder-radius),
275
+ with the sampling order, the refusals and the one that matters most (a sheet cut
276
+ to the art covers **none** of a contour mesh's vertices, because they all sit on
277
+ the silhouette and outside it).
278
+
279
+ ⭐ **And the mesh really is evaluating the pixels' model — measured, not
280
+ asserted.** A consumer that renders the same sheet in a shader displaces every
281
+ PIXEL by its own depth; a mesh displaces vertices and interpolates across
282
+ triangles. On a dome (a ramp would prove nothing — linear interpolation is exact
283
+ on a linear field) at 18°, the two evaluations converge as the lattice refines:
284
+
285
+ | lattice | 3×3 | 5×5 | 9×9 | 17×17 | 33×33 |
286
+ | --- | --- | --- | --- | --- | --- |
287
+ | mean disagreement, px | 6.4141 | 1.8452 | 0.5887 | 0.2276 | **0.0926** |
288
+ | the same lattice reading a **cylinder** instead | 3.8972 | 3.1628 | 3.2558 | 3.3429 | **3.3777** |
289
+
290
+ 🚨 **Read the second row before the first.** A mesh evaluating the wrong surface
291
+ does not converge — it settles at ~3.4px however dense it gets — and at 3×3 it
292
+ reads *better* than the right one. So one measurement at one density cannot tell
293
+ the two models apart, and would have picked the wrong one. The claim is the
294
+ convergence, never a single number. (`CS01`–`CS03` in `selftest.ts`; the worst
295
+ case falls more slowly than the mean and is expected to, because the dome's rim
296
+ has an unbounded depth gradient.)
297
+
298
+ ⚠️ **This is not the same quantity as §4.2's fold angle, which refining makes
299
+ WORSE.** Both are true: a finer lattice buys fidelity to the model and costs the
300
+ angle at which a column pair inverts. This measures the first; `A39` refuses the
301
+ second.
302
+
303
+ ⚠️ **It does not make depth measurable.** The map is relative — 8 bits of level
304
+ say nothing about world units — so `zScale` is authored exactly as the fringe's
305
+ 26 was, and the two warnings above survive intact: get it right and the parallax
306
+ is free, get it wrong and nothing complains. What the map removes is the
307
+ *resolution* limit, not the judgement.
308
+
309
+ ### 2.2 The angle belongs to the map, not to the mesh
310
+
311
+ §2.1 leaves an open question and it has now been measured. §4.2 finds that
312
+ refining the lattice makes the fold **worse**, and read that as the cylinder's
313
+ error surfacing — so per-vertex depth ought to have bought the angle back. ⛔ **It
314
+ does not, and it never could have.**
315
+
316
+ Two neighbouring vertices swap places when the turn tips one past the other,
317
+ which is `tan t ≥ Δu/Δz` — so the largest turn a part supports is
318
+
319
+ tan t_max = 1 / max |dz/du|
320
+
321
+ **the reciprocal of the steepest slope anywhere in its depth map**, and there is
322
+ no mesh in that formula at all. Refining the lattice does not change the angle;
323
+ it changes which slopes the lattice is close enough to *find*. A map with a
324
+ vertical edge has an infinite slope there, so a fine enough mesh folds at any
325
+ angle you name.
326
+
327
+ ⭐ **Which is exactly what a dome is.** `z = Z√(1 − r²)` is vertical at its rim,
328
+ and an even lattice over it folds at `tan t = √h·√(R/2)/Z` for spacing
329
+ `h = W/(side−1)` — a **√h** that goes to zero. Measured against that closed form
330
+ over a 1,300× range of vertex counts, agreeing to ≤ 1.2°:
331
+
332
+ | lattice | 5×5 | 17×17 | 33×33 | 65×65 | 129×129 | 181×181 |
333
+ | --- | --- | --- | --- | --- | --- | --- |
334
+ | **dome**, largest turn admitted | 62° | 41° | 31° | 23° | 17° | **14°** |
335
+ | the closed form above | 59.0° | 39.8° | 30.5° | 22.6° | 16.4° | **14.0°** |
336
+ | **raised cosine**, slope bounded | 73° | 65° | 64° | 64° | 64° | **63°** |
337
+ | its closed form, `atan(2R/Zπ)` | 64.8° | 64.8° | 64.8° | 64.8° | 64.8° | **64.8°** |
338
+
339
+ ⇒ **Author the map so its slope is bounded, and the angle stops depending on the
340
+ mesh.** A raised cosine — flat at the centre, flat again at the rim — holds
341
+ 63–64° from 289 vertices to 32,761. The dome loses three quarters of its angle
342
+ over the same refinement. Both are "correct" depth; only one of them is a
343
+ *surface a turn can be built on*, and the difference is entirely in the input.
344
+
345
+ 🚨 **So a map traced straight off a rendered normal or a photogrammetry pass is
346
+ the dome case, not the cosine case.** Where the part curves away to its
347
+ silhouette is where every such map goes vertical. Flattening it there — letting
348
+ z reach its floor *before* the outline rather than at it — is the edit that buys
349
+ the angle, and it is an edit to the sheet. rigc will not do it for you: the
350
+ compiler never invents a value that is not in the spec, and a depth map is a
351
+ measurement.
352
+
353
+ ⚠️ **This does not touch §2.1's convergence.** A finer lattice still evaluates
354
+ the map's own surface more faithfully; it also finds steeper slopes in it. Those
355
+ are two different quantities and both are true — §2.1 measures the first, `A39`
356
+ refuses the second. What is new here is that the second is a fact about the
357
+ sheet, and can be fixed there.
358
+
359
+ ⭐ **And you no longer have to find the ceiling by building into it.** `build`
360
+ and `explain` print it for any mesh with a depth map — per axis, per direction,
361
+ naming the triangle that goes first — from `tan t = A₀/A_axis` on the mesh's own
362
+ geometry ([AUTHORING §3.4](AUTHORING.md)):
363
+
364
+ ```
365
+ turn ceiling yaw +31.41° / -32.01° pitch +32.01° / -31.41°
366
+ first to fold: yaw + at 31.41°, triangle 960 [113,112,593]
367
+ ```
368
+
369
+ Read it as a fact about **the sheet**. If the number is too small, the fix is in
370
+ the map — flatten it where the part curves away — and not in the lattice.
371
+
372
+ 📐 Method, harness and the full ladders live in the repository rather than in
373
+ this package, as
374
+ [`bench/studies/2026-09-05-density`](https://github.com/firejune/rigc/tree/main/bench/studies/2026-09-05-density) —
375
+ the same study also measures why `contour` is the wrong generator for a turn: it
376
+ saturates at 868 vertices however fine the tolerance, its vertices all sit on the
377
+ silhouette so it samples **2 %** of the depth range, and its ear-clipped interior
378
+ holds triangles three orders of magnitude apart in area, the smallest of which
379
+ reverse under a fraction of a pixel.
380
+
248
381
  ---
249
382
 
250
383
  ## 3. One shared shift, then residuals
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
5
5
  "type": "module",
6
6
  "bin": {