spine-rigc 0.19.0 → 0.20.1
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 +7 -4
- package/cli.ts +90 -7
- package/docs/AUTHORING.md +173 -41
- package/docs/FACE.md +160 -56
- package/docs/INGEST.md +11 -15
- package/docs/SPEC_COVERAGE.md +1 -8
- package/package.json +1 -1
- package/src/compile.ts +85 -16
- package/src/depth.ts +84 -9
- package/src/diff.ts +14 -4
- package/src/types.ts +30 -9
package/README.md
CHANGED
|
@@ -467,10 +467,12 @@ was verified, and what writing it cost. Repository material: a clone and
|
|
|
467
467
|
|
|
468
468
|
<p align="center"><em>The portrait rig playing its three animations in one take — the turn is
|
|
469
469
|
the shot: both silhouette edges move apart, which a flat slide cannot do, because every
|
|
470
|
-
feature carries its own depth.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
470
|
+
feature carries its own depth. The rig guarantees the seams — <code>idle</code> loops
|
|
471
|
+
while <code>gaze</code> and <code>turn</code> return to rest, so the hand-offs meet at 0
|
|
472
|
+
differing pixels — and the composing is the consumer's. Authorable on plain Spine 4.3, no
|
|
473
|
+
plugin, no runtime patch; the split was authoring cost rather than runtime capability, and
|
|
474
|
+
the cost is now one stated expression per key. Compiled and rendered entirely by the
|
|
475
|
+
published package.</em></p>
|
|
474
476
|
|
|
475
477
|
🎞️ **How the three films on this page were made** is kept with them, one directory per
|
|
476
478
|
film in [`films/`](https://github.com/firejune/rigc/tree/main/films) — a `run.sh` that
|
|
@@ -575,6 +577,7 @@ letting `A17` blame the editor for the harness's own doing.
|
|
|
575
577
|
| 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
|
|
576
578
|
| 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 41 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
|
|
577
579
|
| 📋 [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
|
|
580
|
+
| 🗺️ [ROADMAP.md](https://github.com/firejune/rigc/blob/main/ROADMAP.md) | where this is going, and where it has been. What 1.0 has to mean before the number is claimed — conditions rather than a feature list, because direction here comes from what users hit |
|
|
578
581
|
|
|
579
582
|
## Why you can trust the output
|
|
580
583
|
|
package/cli.ts
CHANGED
|
@@ -492,6 +492,30 @@ function meshDepthNote(m: CompileResult['meshes'][number]): string {
|
|
|
492
492
|
`depth "${m.depth.image}" ${m.depth.digest} near=${m.depth.near} zScale=${m.depth.zScale} ` +
|
|
493
493
|
`z=[${m.depth.range[0]}, ${m.depth.range[1]}]`,
|
|
494
494
|
);
|
|
495
|
+
// Directly under the sheet's own line, because it is the other half of
|
|
496
|
+
// "what did this mesh read": `z=[…]` says how much of the map's range
|
|
497
|
+
// reached the vertices, and this says how many of them read a texel that
|
|
498
|
+
// draws nothing (issue #449). Reported only when there is something to
|
|
499
|
+
// report, so a mesh whose every vertex sits on drawn art gains no line.
|
|
500
|
+
//
|
|
501
|
+
// ⭐ A `contour` gets the OTHER half of the sentence, because rigc built
|
|
502
|
+
// that outline and knows what it is: `buildContourMesh` returns
|
|
503
|
+
// `hullVertices: points.length` over `offsetPolygon(simplified, margin)`,
|
|
504
|
+
// so every vertex of a contour mesh is the traced silhouette pushed out by
|
|
505
|
+
// the margin — there are no interior vertices for the count to be about.
|
|
506
|
+
// Nothing is derived, inferred or thresholded to say so; it is what the
|
|
507
|
+
// generator returns, and `generatedHullAndEdges` already cross-checks that
|
|
508
|
+
// hull against the triangulation's own outline. Without it the line reads
|
|
509
|
+
// as a fault on every correct contour rig, which is a diagnostic authors
|
|
510
|
+
// learn to ignore.
|
|
511
|
+
if (m.depth.undrawn > 0) {
|
|
512
|
+
parts.push(
|
|
513
|
+
`${m.depth.undrawn} of ${m.vertices} vertices sample a texel the part image does not draw — ` +
|
|
514
|
+
(m.kind === 'contour'
|
|
515
|
+
? 'a contour\'s vertices are all traced outline, pushed out by the margin, so this is the topology and not the sheet'
|
|
516
|
+
: 'their z is the sheet\'s reading of somewhere the part is not'),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
495
519
|
const c = m.depth.ceiling;
|
|
496
520
|
parts.push(`turn ceiling yaw ${ceilingPair(c.yaw)} pitch ${ceilingPair(c.pitch)}`);
|
|
497
521
|
const worst = tightestFold(c);
|
|
@@ -503,7 +527,13 @@ function meshDepthNote(m: CompileResult['meshes'][number]): string {
|
|
|
503
527
|
? ` nothing in this sheet folds: ${c.measured} triangle(s) measured, none with a depth gradient across it`
|
|
504
528
|
: ` first to fold: ${worst.kind} ${worst.sign} at ${worst.limit.degrees.toFixed(2)}°, ` +
|
|
505
529
|
`triangle ${worst.limit.triangle} [${worst.limit.ids.join(',')}], the sheet steps ` +
|
|
506
|
-
`${depthStepLevels(worst.limit.depthStep, m.depth.zScale).toFixed(2)} level(s) across it` +
|
|
530
|
+
`${depthStepLevels(worst.limit.depthStep, m.depth.zScale).toFixed(2)} level(s) across it, ` +
|
|
531
|
+
// The same step over the range the mesh sampled (issue #448). A
|
|
532
|
+
// suffix and not a line of its own: it is an apposition on the step
|
|
533
|
+
// beside it, and the reading that matters is the two together — a
|
|
534
|
+
// discontinuity says "plenty of levels" and "nearly all of them" at
|
|
535
|
+
// once, and they have to be read in one breath to say the opposite.
|
|
536
|
+
`which is ${worst.limit.stepShare.toFixed(3)} of the range this mesh sampled` +
|
|
507
537
|
`${c.degenerate ? `; ${c.degenerate} triangle(s) too flat in setup to measure` : ''}`,
|
|
508
538
|
);
|
|
509
539
|
}
|
|
@@ -887,9 +917,37 @@ function deformReportLines(result: CompileResult, exempt: ReadonlySet<string>):
|
|
|
887
917
|
// animation are two frames and two rollups: merging them would average a fold
|
|
888
918
|
// one dial reaches into a run of keys another one is clean over, which is the
|
|
889
919
|
// hiding the two frames exist to prevent.
|
|
920
|
+
// 🔒 ONE derivation of a rollup's identity, read by every filter below.
|
|
921
|
+
//
|
|
922
|
+
// ⚠️ It was spelled three times and one of them drifted. The two key filters
|
|
923
|
+
// separate animation from slider with a NUL; the span filter used a SPACE, so
|
|
924
|
+
// its `=== id` never matched on any rig and `spans` was always empty — the
|
|
925
|
+
// "N span(s) … scanned" line silently stopped printing everywhere, taking with
|
|
926
|
+
// it the one thing issue #403 added it to say: that the scan RAN and found
|
|
927
|
+
// nothing, as opposed to never having run. A dead branch is the same silence
|
|
928
|
+
// this tool exists to convert into a named failure, and it survived because
|
|
929
|
+
// the identity was a literal at each site rather than a derivation (#440).
|
|
930
|
+
const rollupId = (animation: string, slider: string | null): string => `${animation}\u0000${slider ?? ''}`;
|
|
931
|
+
/**
|
|
932
|
+
* The readings A39 puts on its stats line for the three things this rollup
|
|
933
|
+
* reports — each spelled ONCE here and nowhere else in this file.
|
|
934
|
+
*
|
|
935
|
+
* 🔒 Checked rather than derived, and the difference is forced: `src/validate.ts`
|
|
936
|
+
* sets these on a `Record<string, number | string>`, so there is no type to take
|
|
937
|
+
* a name off and no constant to import. So they are spelled here and a control
|
|
938
|
+
* compiles a rig that triggers each one, then asserts the breadcrumb names a
|
|
939
|
+
* reading A39 really printed — two independent derivations compared, which is
|
|
940
|
+
* the shape `CUR07` uses for the same reason.
|
|
941
|
+
*/
|
|
942
|
+
const A39_COUNTS = {
|
|
943
|
+
notDrawn: 'deformKeysNotDrawn',
|
|
944
|
+
unreachable: 'deformKeysUnreachable',
|
|
945
|
+
dialsDisagreed: 'deformDialsDisagreed',
|
|
946
|
+
dialDisagreed: 'deformDialDisagreed',
|
|
947
|
+
} as const;
|
|
890
948
|
const rollups = new Map<string, { animation: string; label: string }>();
|
|
891
949
|
for (const key of survey.keys) {
|
|
892
|
-
rollups.set(
|
|
950
|
+
rollups.set(rollupId(key.animation, key.reach.slider), {
|
|
893
951
|
animation: key.animation,
|
|
894
952
|
label: key.reach.kind === 'slider' ? `${key.animation} via ${key.reach.slider}` : key.animation,
|
|
895
953
|
});
|
|
@@ -899,7 +957,7 @@ function deformReportLines(result: CompileResult, exempt: ReadonlySet<string>):
|
|
|
899
957
|
// the same two counts and A39 reads none of a key that draws nothing, nor of
|
|
900
958
|
// one at a time no dial selects. The ones it left out get their own line
|
|
901
959
|
// rather than a silence (issues #401, #407).
|
|
902
|
-
const mine = survey.keys.filter((k) =>
|
|
960
|
+
const mine = survey.keys.filter((k) => rollupId(k.animation, k.reach.slider) === id);
|
|
903
961
|
const unreachable = mine.filter((k) => k.dial?.unreachable === true);
|
|
904
962
|
const keys = mine.filter((k) => k.dial?.unreachable !== true && k.draw.blank === null);
|
|
905
963
|
const blank = mine.filter((k) => k.dial?.unreachable !== true && k.draw.blank !== null);
|
|
@@ -934,7 +992,7 @@ function deformReportLines(result: CompileResult, exempt: ReadonlySet<string>):
|
|
|
934
992
|
` .. ${keys.length ? ''.padEnd(label.length) : label} ${blank.length} key(s) draw no pixels ` +
|
|
935
993
|
`at their own time and are read for no winding, carrying ` +
|
|
936
994
|
`${blank.reduce((n, k) => n + k.reversed.length, 0)} reversed triangle(s) nothing gates <- A39 counts ` +
|
|
937
|
-
|
|
995
|
+
`them as ${A39_COUNTS.notDrawn}`,
|
|
938
996
|
);
|
|
939
997
|
}
|
|
940
998
|
if (unreachable.length) {
|
|
@@ -942,17 +1000,42 @@ function deformReportLines(result: CompileResult, exempt: ReadonlySet<string>):
|
|
|
942
1000
|
` .. ${keys.length || blank.length ? ''.padEnd(label.length) : label} ${unreachable.length} key(s) ` +
|
|
943
1001
|
'at a time no dial selects, measured in the frame the runtime lands on instead and read for no winding, ' +
|
|
944
1002
|
`carrying ${unreachable.reduce((n, k) => n + k.reversed.length, 0)} reversed triangle(s) nothing gates ` +
|
|
945
|
-
|
|
1003
|
+
` <- A39 counts them as ${A39_COUNTS.unreachable}`,
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
// ⚠️ The dial rigc's two halves disagree about (issues #427, #440). It is a
|
|
1007
|
+
// property of the SLIDER and not of any one key, so it is placed by the
|
|
1008
|
+
// rollup's own identity rather than by a key filter — and it is REPORTED,
|
|
1009
|
+
// never gated: A39 refuses nothing for it, because a disagreement can pose
|
|
1010
|
+
// every frame correctly. Both readings are named because they differ by one
|
|
1011
|
+
// letter, and a breadcrumb that named only one of `deformDialsDisagreed` /
|
|
1012
|
+
// `deformDialDisagreed` would send a reader to grep for the other.
|
|
1013
|
+
const disputes = survey.dialDisputes.filter((dispute) => rollupId(animation, dispute.slider) === id);
|
|
1014
|
+
if (disputes.length) {
|
|
1015
|
+
out.push(
|
|
1016
|
+
` .. ${keys.length || blank.length || unreachable.length ? ''.padEnd(label.length) : label} ` +
|
|
1017
|
+
`${disputes.length} dial(s) the skeleton and the probe disagree about: ` +
|
|
1018
|
+
disputes
|
|
1019
|
+
.map(
|
|
1020
|
+
(dispute) =>
|
|
1021
|
+
`the skeleton reads ${dispute.bone}.${dispute.stated} and the probe drives ` +
|
|
1022
|
+
`${dispute.bone}.${dispute.drive}, ` +
|
|
1023
|
+
(dispute.outside.length === 0
|
|
1024
|
+
? 'and both answers pose the same frames'
|
|
1025
|
+
: `${dispute.outside.length} key time(s) outside what the skeleton's own field reaches`),
|
|
1026
|
+
)
|
|
1027
|
+
.join('; ') +
|
|
1028
|
+
` <- A39 counts them as ${A39_COUNTS.dialsDisagreed} and spells them out as ${A39_COUNTS.dialDisagreed}`,
|
|
946
1029
|
);
|
|
947
1030
|
}
|
|
948
1031
|
// ⚠️ Printed on a clean animation too. "The scan ran and found nothing" and
|
|
949
1032
|
// "the scan never ran" are the two things a gate must never say the same
|
|
950
1033
|
// way, and this line is the only place an author can tell them apart
|
|
951
1034
|
// (issue #403).
|
|
952
|
-
const spans = survey.spans.filter((s) =>
|
|
1035
|
+
const spans = survey.spans.filter((s) => rollupId(s.animation, s.reach.slider) === id);
|
|
953
1036
|
if (spans.length) {
|
|
954
1037
|
out.push(
|
|
955
|
-
` .. ${keys.length || blank.length || unreachable.length ? ''.padEnd(label.length) : label} ` +
|
|
1038
|
+
` .. ${keys.length || blank.length || unreachable.length || disputes.length ? ''.padEnd(label.length) : label} ` +
|
|
956
1039
|
`${spans.length} span(s) between consecutive keys scanned for a fold no key lands on: ` +
|
|
957
1040
|
`${spanTally(spans)} <- A39 reads the same scan`,
|
|
958
1041
|
);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -773,8 +773,13 @@ between two things it has in front of it: the emitted triangles, and the PNG the
|
|
|
773
773
|
attachment names with `image`. So any mesh that names one gets the figure on its
|
|
774
774
|
`MESH` line, authored or generated:
|
|
775
775
|
|
|
776
|
+
```bash
|
|
777
|
+
bun cli.ts build --rig gallery/squash/rig.json \
|
|
778
|
+
--motion gallery/squash/motion.json --out /tmp/squash
|
|
779
|
+
```
|
|
780
|
+
|
|
776
781
|
```
|
|
777
|
-
MESH ball authored 9 vertices / 8 triangles (budget 8) bones=[ball] attachments=[ball] covers
|
|
782
|
+
MESH ball authored 9 vertices / 8 triangles (budget 8) bones=[ball] attachments=[ball] covers 100.00% of the art, reaching 15.00px past it
|
|
778
783
|
```
|
|
779
784
|
|
|
780
785
|
**A number, not a bar.** A `contour` under 99.5% is *refused* because rigc
|
|
@@ -782,11 +787,17 @@ generated that geometry as a claim about the art; an authored mesh that sits ins
|
|
|
782
787
|
its art is a legitimate thing to draw — a soft feather, a trimmed hull, a mesh
|
|
783
788
|
meant to bend a core while its edges stretch — so the figure informs and the
|
|
784
789
|
decision stays with the author. A mesh with no `image` reports nothing, because
|
|
785
|
-
there is nothing to measure it against.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
+
there is nothing to measure it against.
|
|
791
|
+
|
|
792
|
+
**The silence was worth closing, and that example is where it was found.** The
|
|
793
|
+
ball is a centre vertex plus 8 rim vertices, and the first version placed them
|
|
794
|
+
*on* the silhouette — but an octagon's sides pass `R · cos(π/8)` from its centre,
|
|
795
|
+
so its whole ink outline between the spokes was not going to be drawn, and every
|
|
796
|
+
assertion passed (issue #277). That is why the command above prints 100.00%
|
|
797
|
+
rather than the figure it was filed over:
|
|
798
|
+
[`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash)'s
|
|
799
|
+
README carries the inradius arithmetic, both coverage readings, and the rim move
|
|
800
|
+
that settled it.
|
|
790
801
|
|
|
791
802
|
The generators are `ring`, `ribbon`, `contour` and `grid` (see
|
|
792
803
|
[`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
|
|
@@ -1004,7 +1015,7 @@ number that says whether the map covers the part or a corner of it.
|
|
|
1004
1015
|
depth "face_depth.png" f552a2f50d21 near=white zScale=60 z=[0, 60]
|
|
1005
1016
|
turn ceiling yaw +31.41° / -32.01° pitch +32.01° / -31.41°
|
|
1006
1017
|
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
|
|
1018
|
+
first to fold: yaw + at 31.41°, triangle 960 [113,112,593], the sheet steps 12.52 level(s) across it, which is 0.049 of the range this mesh sampled
|
|
1008
1019
|
```
|
|
1009
1020
|
|
|
1010
1021
|
Past that angle a triangle turns inside out and `A39` refuses the build by name.
|
|
@@ -1012,9 +1023,10 @@ The loop this replaces is *pick an angle, build, read the refusal, guess again*.
|
|
|
1012
1023
|
|
|
1013
1024
|
#### Is the ceiling describing the form, or the sheet's grain?
|
|
1014
1025
|
|
|
1015
|
-
The
|
|
1026
|
+
The lines under the ceiling answer that, and they are **reports only** — nothing
|
|
1016
1027
|
in them refuses a build or moves a ceiling
|
|
1017
|
-
([#412](https://github.com/firejune/rigc/issues/412)
|
|
1028
|
+
([#412](https://github.com/firejune/rigc/issues/412),
|
|
1029
|
+
[#448](https://github.com/firejune/rigc/issues/448)).
|
|
1018
1030
|
|
|
1019
1031
|
The ceiling is the **minimum** of the per-triangle fold angles, and a minimum
|
|
1020
1032
|
cannot say whether it is the floor of a band or one bad pixel. Measured: a clean
|
|
@@ -1025,38 +1037,66 @@ triangle does not, and nothing on the first line says so.
|
|
|
1025
1037
|
|
|
1026
1038
|
| the figure | how to read it |
|
|
1027
1039
|
| --- | --- |
|
|
1028
|
-
| `x1.003` — the **1st percentile over the ceiling** | near 1 means a *band* of the mesh reaches the limit together,
|
|
1040
|
+
| `x1.003` — the **1st percentile over the ceiling** | near 1 means a *band* of the mesh reaches the limit together, and 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`. ⚠️ A band is **not** sufficient evidence of a form, which is the third case: **an outline is a band**. A depth sheet estimated over cut-out art has a cliff along the whole silhouette, so its ceiling is a band too and this figure reads 1.02–2.17 — healthy — on a mesh whose angle means nothing. The row below is what tells those two apart |
|
|
1029
1041
|
| `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
1042
|
| `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 |
|
|
1043
|
+
| `which is 0.049 of the range this mesh sampled` — the **same step, over the range this mesh sampled** | how much of everything the sheet said across the whole part it said across that one triangle. A form's slope is bounded, so this **halves every time you double the lattice** while the angle settles. Near 1 it is a **cliff**: a step with no slope in it, whose angle halves with the lattice instead and describes nothing at any density. Measured: `gallery/look` reads 0.112 and 0.468, a synthetic raised cosine 0.394 falling to 0.027 under refinement, the same cosine with one planted cliff a flat 0.50, and estimated depth sheets over cut-out art **0.92–0.99** |
|
|
1031
1044
|
| `+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
1045
|
|
|
1033
|
-
A real one rather than the illustration above — `
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1046
|
+
A real one rather than the illustration above — `gallery/look`, whose two meshes
|
|
1047
|
+
happen to print two of the three spellings:
|
|
1048
|
+
|
|
1049
|
+
```bash
|
|
1050
|
+
bun cli.ts build --rig gallery/look/rig.json \
|
|
1051
|
+
--motion gallery/look/motion.json \
|
|
1052
|
+
--images gallery/look/parts --out /tmp/look
|
|
1053
|
+
```
|
|
1037
1054
|
|
|
1038
1055
|
```
|
|
1039
1056
|
MESH head grid 189 vertices / 320 triangles (budget 320) bones=[head] attachments=[head]
|
|
1040
1057
|
depth "face_depth.png" bf156ea0cfc970a3 near=white zScale=194 z=[0, 194]
|
|
1058
|
+
80 of 189 vertices sample a texel the part image does not draw — their z is the sheet's reading of somewhere the part is not
|
|
1041
1059
|
turn ceiling yaw +19.32° / -19.32° pitch +22.92° / -26.94°
|
|
1042
1060
|
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
|
|
1061
|
+
first to fold: yaw + at 19.32°, triangle 174 [119,138,139], the sheet steps 28.50 level(s) across it, which is 0.112 of the range this mesh sampled
|
|
1044
1062
|
MESH hair_lock_l grid 39 vertices / 48 triangles (budget 320) bones=[lock_l] attachments=[hair_lock_l]
|
|
1045
1063
|
depth "lock_l_depth.png" 0c4eaeb36b7c5cac near=white zScale=64 z=[22.086275, 63.874511]
|
|
1064
|
+
32 of 39 vertices sample a texel the part image does not draw — their z is the sheet's reading of somewhere the part is not
|
|
1046
1065
|
turn ceiling yaw +17.04° / -45.80° pitch +none / -none
|
|
1047
1066
|
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
|
|
1067
|
+
first to fold: yaw + at 17.04°, triangle 2 [1,28,29], the sheet steps 78.00 level(s) across it, which is 0.468 of the range this mesh sampled
|
|
1049
1068
|
```
|
|
1050
1069
|
|
|
1051
1070
|
⭐ Both of those sheets are **form**, and the figures say so from opposite ends:
|
|
1052
1071
|
the head's 320 triangles put the percentile exactly on the ceiling, and the
|
|
1053
1072
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1073
|
+
folding triangle, neither ceiling is anywhere near the encoding, and at 0.112
|
|
1074
|
+
and 0.468 of their own range neither step is a cliff.
|
|
1075
|
+
|
|
1076
|
+
🔸 The line under each `depth "…"` is the other question, and it is not about
|
|
1077
|
+
the ceiling. A `grid` spans the whole part window and a head is not a rectangle, so
|
|
1078
|
+
some of the lattice lands where `head.png` draws nothing and takes its depth
|
|
1079
|
+
from the sheet out there. That is a **count and not a complaint** — read it
|
|
1080
|
+
against the mesh: 80 of 189 is the border of a lattice over a cut-out and the
|
|
1081
|
+
ceiling is still a form, while a count approaching the whole mesh with a step
|
|
1082
|
+
share near 1 beside it is a mesh reading background.
|
|
1083
|
+
|
|
1084
|
+
⇒ **A small ceiling with a ratio near 1, a step well above 3 and a step share
|
|
1085
|
+
well under 1 is a steep surface: flatten the map.** A small ceiling with a large
|
|
1086
|
+
ratio, or with a step near 1, is a *sheet* problem: the grain, the 8-bit
|
|
1087
|
+
rounding, or a stray pixel. A small ceiling with a step share **near 1** is
|
|
1088
|
+
neither — it is a **discontinuity**, and no angle is the right one to quote for
|
|
1089
|
+
it. [`docs/FACE.md` §2.2](FACE.md) has the amplitudes, what each one costs, and
|
|
1090
|
+
why flattening does not apply to the third case.
|
|
1091
|
+
|
|
1092
|
+
🚨 **How to tell a discontinuity from a steep surface in one move: refine the
|
|
1093
|
+
lattice and build again.** A form's ceiling settles and its step share halves; a
|
|
1094
|
+
cliff's ceiling **halves** and its step share does not move. Measured on a
|
|
1095
|
+
synthetic raised cosine against the same cosine with one column of cliff planted
|
|
1096
|
+
in it, over three doublings: the form goes 0.358 → 0.195 → 0.100 while its
|
|
1097
|
+
ceiling moves 41.99° → 39.59° → 38.84°, and the cliff goes 0.460 → 0.489 → 0.497
|
|
1098
|
+
while its ceiling goes 37.07° → 18.59° → 9.24°. The second one is not converging
|
|
1099
|
+
on anything.
|
|
1060
1100
|
|
|
1061
1101
|
⛔ **rigc will not filter the sheet for you, and you should not want it to.** A
|
|
1062
1102
|
smoothed measurement would describe a surface the deform key is not built from:
|
|
@@ -1123,9 +1163,50 @@ refuses it instead:
|
|
|
1123
1163
|
| `gamma` or `contrast` at or below 0 | `collapses the range onto the midpoint … so the map would describe a flat part` |
|
|
1124
1164
|
| a `near` that is neither | `it is "white" or "black"` |
|
|
1125
1165
|
|
|
1126
|
-
A sheet
|
|
1127
|
-
|
|
1128
|
-
|
|
1166
|
+
A sheet that is **opaque everywhere** — a full-frame render with the background
|
|
1167
|
+
in it, which is what monocular depth estimation produces — covers its whole grid
|
|
1168
|
+
by construction, so the coverage refusal has nothing to hold it to and skips.
|
|
1169
|
+
That is right: a full-frame sheet is a legitimate statement and rigc has no
|
|
1170
|
+
authority to guess an input away. But the defect the refusal exists to catch is
|
|
1171
|
+
still reachable in that encoding, so the report counts it instead
|
|
1172
|
+
([#449](https://github.com/firejune/rigc/issues/449)):
|
|
1173
|
+
|
|
1174
|
+
```
|
|
1175
|
+
80 of 189 vertices sample a texel the part image does not draw — their z is the sheet's reading of somewhere the part is not
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
⚠️ **The reported range is NOT where this shows up**, and this guide said it was
|
|
1179
|
+
until it was measured. A background level is a legitimate depth value, so a map
|
|
1180
|
+
half of which is background reports exactly as full a range as one that is all
|
|
1181
|
+
subject: on the measured build, `z=[0, 223.97]` of a stated 224 — healthy — with
|
|
1182
|
+
54 % of the mesh reading background. "Covers its whole grid" is true and about
|
|
1183
|
+
the wrong grid; the question was never coverage of the *sheet*, it was whether
|
|
1184
|
+
the mesh is sampling **art**.
|
|
1185
|
+
|
|
1186
|
+
| The input | What you get |
|
|
1187
|
+
| --- | --- |
|
|
1188
|
+
| a sheet cut to the art's alpha, over a mesh that reaches past it | **refused** — `does not cover N of the mesh's V vertices`, with the fix for your topology named |
|
|
1189
|
+
| the same field stored opaque everywhere, over the same mesh | **compiled**, with `N of V vertices sample a texel the part image does not draw` in the report. The same N |
|
|
1190
|
+
| a mesh every one of whose vertices sits on drawn art | nothing — no line, and no refusal |
|
|
1191
|
+
|
|
1192
|
+
🔸 It is a raw count with **no reach subtracted from it**, so a `contour` mesh
|
|
1193
|
+
reports most or all of its vertices: its outline is pushed `margin` pixels
|
|
1194
|
+
outside the silhouette by design, and out there the part draws nothing.
|
|
1195
|
+
Discounting the margin would mean borrowing a number authored for the trace to
|
|
1196
|
+
mean "close enough" for the sheet, and rigc does not invent tolerances.
|
|
1197
|
+
|
|
1198
|
+
⭐ **So the line says why instead**, because rigc built that outline and knows
|
|
1199
|
+
what it is:
|
|
1200
|
+
|
|
1201
|
+
```
|
|
1202
|
+
15 of 15 vertices sample a texel the part image does not draw — a contour's vertices are all traced outline, pushed out by the margin, so this is the topology and not the sheet
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
The attribution is on a `contour` and **nowhere else**. A `grid`'s border is
|
|
1206
|
+
where your `us`/`vs` put it, not where a margin did, so the same sentence there
|
|
1207
|
+
would be a lie and the count stands alone. What that leaves true either way: the
|
|
1208
|
+
rim's depth really did come from off the art, which is harmless on a sheet
|
|
1209
|
+
dilated past the margin and is the whole failure on a full-frame estimate.
|
|
1129
1210
|
|
|
1130
1211
|
##### `soft` — which part is soft, and which bone carries it
|
|
1131
1212
|
|
|
@@ -2016,6 +2097,7 @@ is not the arrangement the format has. Spine keys one bone per timeline, so the
|
|
|
2016
2097
|
six numbers of a head turn are eighty lines apart in the artifact and nobody can
|
|
2017
2098
|
see a wrong sign in them.
|
|
2018
2099
|
|
|
2100
|
+
**No run reproduces this:** the legend and one group's record lifted out of one `explain` run, which prints the two `bone "faceshift"` records between them
|
|
2019
2101
|
```
|
|
2020
2102
|
group members (the per-member values of one track, side by side — issue #295)
|
|
2021
2103
|
.. a row per member and a block per key, because a wrong sign is visible in a column of six and
|
|
@@ -2595,10 +2677,15 @@ makes against them.
|
|
|
2595
2677
|
|
|
2596
2678
|
**It is auditable.** `explain` prints the model, the scalars the closed form
|
|
2597
2679
|
derived from it, and every offset it produced — the emitted ones, not a second
|
|
2598
|
-
evaluation:
|
|
2680
|
+
evaluation. This is the `t=0.62` key of the spec above, whole:
|
|
2681
|
+
|
|
2682
|
+
```bash
|
|
2683
|
+
bun cli.ts explain --rig gallery/portrait/rig.json \
|
|
2684
|
+
--motion gallery/portrait/motion.json --out /tmp/explain
|
|
2685
|
+
```
|
|
2599
2686
|
|
|
2600
2687
|
```
|
|
2601
|
-
t=0.62 deform[0..50] 25 pair(s)
|
|
2688
|
+
t=0.62 deform[0..50] 25 pair(s) stepped
|
|
2602
2689
|
transform yaw radius=170 degrees=12
|
|
2603
2690
|
dx = (x−about)·(cos t − 1) − z·sin t, z = √(radius² − (x−about)²)
|
|
2604
2691
|
t = 0.20944 rad
|
|
@@ -2607,9 +2694,19 @@ evaluation:
|
|
|
2607
2694
|
centre shift = −radius·sin t = -35.344987
|
|
2608
2695
|
25 vertices, largest offset 35.344987px at vertex 2
|
|
2609
2696
|
v 0 (-7.17493, 0) v 1 (-22.413595, 0) v 2 (-35.344987, 0) v 3 (-27.658171, 0)
|
|
2610
|
-
|
|
2697
|
+
v 4 (-14.255108, 0) v 5 (-14.255108, 0) v 6 (-14.255108, 0) v 7 (-14.255108, 0)
|
|
2698
|
+
v 8 (-14.255108, 0) v 9 (-27.658171, 0) v 10 (-35.344987, 0) v 11 (-22.413595, 0)
|
|
2699
|
+
v 12 (-7.17493, 0) v 13 (-7.17493, 0) v 14 (-7.17493, 0) v 15 (-7.17493, 0)
|
|
2700
|
+
v 16 (-22.413595, 0) v 17 (-35.344987, 0) v 18 (-27.658171, 0) v 19 (-22.413595, 0)
|
|
2701
|
+
v 20 (-35.344987, 0) v 21 (-27.658171, 0) v 22 (-22.413595, 0) v 23 (-35.344987, 0)
|
|
2702
|
+
v 24 (-27.658171, 0)
|
|
2611
2703
|
```
|
|
2612
2704
|
|
|
2705
|
+
The curve reads `stepped` where the spec says `"ease": "swell"`, and that is
|
|
2706
|
+
§4.5's hold rule rather than a discrepancy: the next key emits these same 25
|
|
2707
|
+
offsets, so the segment between them would draw nothing and is written the way
|
|
2708
|
+
the editor writes it.
|
|
2709
|
+
|
|
2613
2710
|
📌 **Float behaviour, stated.** The closed forms are evaluated in float64 and
|
|
2614
2711
|
quantised to six decimals like every other emitted number, so the same spec emits
|
|
2615
2712
|
the same bytes and `A18_DETERMINISTIC_EMIT` proves it on a second compile. The
|
|
@@ -2635,6 +2732,7 @@ triangles. It is a report and it never gates: `explain` takes no `--profile` and
|
|
|
2635
2732
|
exits 0 on a rig `build` would refuse, so the figures are readable on the build
|
|
2636
2733
|
that is failing.
|
|
2637
2734
|
|
|
2735
|
+
**No run reproduces this:** abridged — the `WORST` rollup follows key 1 here, where the run prints `head/head`'s other two keys and all four of `hair_bang/hair_bang` between them
|
|
2638
2736
|
```
|
|
2639
2737
|
deform (what each key does to the geometry — figures with names, never a bar; issue #316)
|
|
2640
2738
|
.. every key measured at its OWN time against the same pose with the deform CLEARED, so the
|
|
@@ -2670,7 +2768,7 @@ deform (what each key does to the geometry — figures with names, never a bar;
|
|
|
2670
2768
|
| `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 |
|
|
2671
2769
|
| `winding` | triangles whose winding survived, and how many the key pinched onto zero area. A fold says so and points at `A39` |
|
|
2672
2770
|
| 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 |
|
|
2673
|
-
| `WORST` | per animation
|
|
2771
|
+
| `WORST` | per animation **and per frame**: 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. Each further line ends with a breadcrumb naming the `A39` reading that counts it — keys that draw no pixels (`deformKeysNotDrawn`, below), keys at a time no dial selects (`deformKeysUnreachable`, §4.11.4), and a dial the skeleton and the probe disagree about (`deformDialsDisagreed`, §4.11.4) |
|
|
2674
2772
|
|
|
2675
2773
|
📌 **The frame is the posed one, and the denominator is 1.000 by definition.**
|
|
2676
2774
|
Both sides of every comparison are taken at the key's own time with the animation
|
|
@@ -2731,8 +2829,12 @@ clothes. The quantity that does move — how much art each drawn pixel now carri
|
|
|
2731
2829
|
📘 **[FACE.md](FACE.md) §9.2** is this block on real art, as three builds of
|
|
2732
2830
|
`gallery/portrait`: the good one, one with a band inverted, and one folded. The
|
|
2733
2831
|
inverted build is the case worth reading — `A39` passes it (correctly: nothing
|
|
2734
|
-
reverses), and the block is what says `x1.362834`
|
|
2735
|
-
|
|
2832
|
+
reverses), and the block is what says `x1.362834` on the band the model's own key
|
|
2833
|
+
reports as `x0.637174`, with no reference render anywhere. ⭐ The comparison is
|
|
2834
|
+
per **triangle** and that is what makes it a reading: the block names one beside
|
|
2835
|
+
every ratio, so the two blocks can be lined up band against band instead of worst
|
|
2836
|
+
against worst — which on this build would have paired the inverted band with an
|
|
2837
|
+
untouched one and called the difference mild.
|
|
2736
2838
|
|
|
2737
2839
|
📘 **[`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod)'s
|
|
2738
2840
|
README is a second reading of the same block** (repository material, hence the
|
|
@@ -2757,15 +2859,23 @@ the frames just before it are drawn, nearly folded, and land on no key at all. O
|
|
|
2757
2859
|
the turn probe that is **8 reversed triangles at alpha 0.20, gating green**.
|
|
2758
2860
|
|
|
2759
2861
|
⇒ `A39` now scans every interval between two consecutive deform keys as well, and
|
|
2760
|
-
refuses one with its own sentence
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2862
|
+
refuses one with its own sentence. No spec this repository ships produces one — the
|
|
2863
|
+
rig it was written from is a probe `selftest.ts` generates and nothing else can
|
|
2864
|
+
invoke — so the sentence is described here rather than transcribed.
|
|
2865
|
+
|
|
2866
|
+
**What it carries**, in the order it says it: `BETWEEN key <i> (t=…s) and key <j>
|
|
2867
|
+
(t=…s)` where a key refusal puts one index; the time the closed form solved for, and
|
|
2868
|
+
how far along the segment that is — or `(a stepped segment)` instead, which
|
|
2869
|
+
interpolates nothing and holds the earlier key's geometry across the span; the
|
|
2870
|
+
reversed count out of the triangle total, with the first four named and each one's
|
|
2871
|
+
vertex ids and its signed area before and after; `NO KEY LANDS THERE` in those words,
|
|
2872
|
+
then whether the runtime interpolates across the span or holds it; the alpha read at
|
|
2873
|
+
that same instant, present only where it is not 1; and the ways out — for an
|
|
2874
|
+
interpolating span the four the table below gives, the fade one among them only
|
|
2875
|
+
where the alpha is not 1, and for a stepped one the key it holds instead, with
|
|
2876
|
+
`invariants.deformMayFold` the last resort either way.
|
|
2877
|
+
[`src/validate.ts`](../src/validate.ts) builds it, beside the key sentence §4.11.2
|
|
2878
|
+
quotes.
|
|
2769
2879
|
|
|
2770
2880
|
**What to change when you see it**, in the order worth trying:
|
|
2771
2881
|
|
|
@@ -2833,7 +2943,13 @@ value = from + (time − to) / scale what A39 sets the dial to
|
|
|
2833
2943
|
|
|
2834
2944
|
🔒 **The `DEFORM` block prints the frame on every key**, because the derivation
|
|
2835
2945
|
changed and a block that went on printing the same figures under a changed meaning
|
|
2836
|
-
would be worse than the red it replaced
|
|
2946
|
+
would be worse than the red it replaced. `gallery/look`'s `turn` is the animation
|
|
2947
|
+
a slider applies, and this is one of its keys:
|
|
2948
|
+
|
|
2949
|
+
```bash
|
|
2950
|
+
bun cli.ts explain --rig gallery/look/rig.json \
|
|
2951
|
+
--motion gallery/look/motion.json --out /tmp/explain-look
|
|
2952
|
+
```
|
|
2837
2953
|
|
|
2838
2954
|
```
|
|
2839
2955
|
DEFORM turn default/head/head key 6 t=1.900000 transform yaw depth=true degrees=19
|
|
@@ -2939,6 +3055,21 @@ deformDialsDisagreed=1 deformDialDisagreed=dial|artifact:knob.x@2.321e-8|reaches
|
|
|
2939
3055
|
- A **tie** never carries `probe:`, `reaches:` or `outside:`, and never counts as a
|
|
2940
3056
|
disagreement. There is one belief there, not two.
|
|
2941
3057
|
|
|
3058
|
+
**And `explain`'s rollup carries it too, with the breadcrumb its neighbours have**
|
|
3059
|
+
([#440](https://github.com/firejune/rigc/issues/440)). The per-key `frame` lines
|
|
3060
|
+
say it once each; the rollup says it once per animation, and ends by naming the
|
|
3061
|
+
two readings a `build` prints it under — because they differ by one letter, and a
|
|
3062
|
+
breadcrumb naming only one would send a reader to grep for the other:
|
|
3063
|
+
|
|
3064
|
+
```
|
|
3065
|
+
.. 1 dial(s) the skeleton and the probe disagree about: the skeleton reads knob.x and the probe drives knob.y, 2 key time(s) outside what the skeleton's own field reaches <- A39 counts them as deformDialsDisagreed and spells them out as deformDialDisagreed
|
|
3066
|
+
```
|
|
3067
|
+
|
|
3068
|
+
The count after `outside` is the length of the stats line's own `outside:` list,
|
|
3069
|
+
and `deformDialsDisagreed` / `deformDialDisagreed` are checked against the keys
|
|
3070
|
+
`A39` really printed rather than restated — `DW34`. An agreed dial adds no line
|
|
3071
|
+
(`DW35`) and a **tie** is never rolled up as a disagreement (`DW36`).
|
|
3072
|
+
|
|
2942
3073
|
⛔ **None of it refuses a build**, and the reason is measured rather than chosen.
|
|
2943
3074
|
The field the survey drives is the largest response the probe found, so its reach
|
|
2944
3075
|
always *contains* the artifact's: a disagreement cannot make `A39` miss a frame the
|
|
@@ -3140,6 +3271,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
3140
3271
|
|
|
3141
3272
|
The report prints one line per assertion:
|
|
3142
3273
|
|
|
3274
|
+
**No run reproduces this:** assembled — one line of each verdict kind; the `PASS` and the `PROF` are verbatim from a `build` this page states, the `SKIP` is cut at the ellipsis and only `--profile spine-html` prints it, and the `FAIL` is invented, because a green build prints none
|
|
3143
3275
|
```
|
|
3144
3276
|
PASS A08_REGION_NAMES_MATCH_ATTACHMENTS
|
|
3145
3277
|
SKIP A21_MESH_RIM_PINNED: the skeleton has no weighted mesh attachment, …
|