spine-rigc 0.19.0 → 0.20.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 +7 -4
- package/cli.ts +90 -7
- package/docs/AUTHORING.md +97 -16
- package/docs/FACE.md +61 -14
- 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/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
|
@@ -1004,7 +1004,7 @@ number that says whether the map covers the part or a corner of it.
|
|
|
1004
1004
|
depth "face_depth.png" f552a2f50d21 near=white zScale=60 z=[0, 60]
|
|
1005
1005
|
turn ceiling yaw +31.41° / -32.01° pitch +32.01° / -31.41°
|
|
1006
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
|
|
1007
|
+
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
1008
|
```
|
|
1009
1009
|
|
|
1010
1010
|
Past that angle a triangle turns inside out and `A39` refuses the build by name.
|
|
@@ -1012,9 +1012,10 @@ The loop this replaces is *pick an angle, build, read the refusal, guess again*.
|
|
|
1012
1012
|
|
|
1013
1013
|
#### Is the ceiling describing the form, or the sheet's grain?
|
|
1014
1014
|
|
|
1015
|
-
The
|
|
1015
|
+
The lines under the ceiling answer that, and they are **reports only** — nothing
|
|
1016
1016
|
in them refuses a build or moves a ceiling
|
|
1017
|
-
([#412](https://github.com/firejune/rigc/issues/412)
|
|
1017
|
+
([#412](https://github.com/firejune/rigc/issues/412),
|
|
1018
|
+
[#448](https://github.com/firejune/rigc/issues/448)).
|
|
1018
1019
|
|
|
1019
1020
|
The ceiling is the **minimum** of the per-triangle fold angles, and a minimum
|
|
1020
1021
|
cannot say whether it is the floor of a band or one bad pixel. Measured: a clean
|
|
@@ -1025,9 +1026,10 @@ triangle does not, and nothing on the first line says so.
|
|
|
1025
1026
|
|
|
1026
1027
|
| the figure | how to read it |
|
|
1027
1028
|
| --- | --- |
|
|
1028
|
-
| `x1.003` — the **1st percentile over the ceiling** | near 1 means a *band* of the mesh reaches the limit together,
|
|
1029
|
+
| `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
1030
|
| `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
1031
|
| `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 |
|
|
1032
|
+
| `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
1033
|
| `+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
1034
|
|
|
1033
1035
|
A real one rather than the illustration above — `bun cli.ts build --rig
|
|
@@ -1038,25 +1040,48 @@ three spellings:
|
|
|
1038
1040
|
```
|
|
1039
1041
|
MESH head grid 189 vertices / 320 triangles (budget 320) bones=[head] attachments=[head]
|
|
1040
1042
|
depth "face_depth.png" bf156ea0cfc970a3 near=white zScale=194 z=[0, 194]
|
|
1043
|
+
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
1044
|
turn ceiling yaw +19.32° / -19.32° pitch +22.92° / -26.94°
|
|
1042
1045
|
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
|
|
1046
|
+
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
1047
|
MESH hair_lock_l grid 39 vertices / 48 triangles (budget 320) bones=[lock_l] attachments=[hair_lock_l]
|
|
1045
1048
|
depth "lock_l_depth.png" 0c4eaeb36b7c5cac near=white zScale=64 z=[22.086275, 63.874511]
|
|
1049
|
+
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
1050
|
turn ceiling yaw +17.04° / -45.80° pitch +none / -none
|
|
1047
1051
|
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
|
|
1052
|
+
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
1053
|
```
|
|
1050
1054
|
|
|
1051
1055
|
⭐ Both of those sheets are **form**, and the figures say so from opposite ends:
|
|
1052
1056
|
the head's 320 triangles put the percentile exactly on the ceiling, and the
|
|
1053
1057
|
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
|
-
|
|
1058
|
+
folding triangle, neither ceiling is anywhere near the encoding, and at 0.112
|
|
1059
|
+
and 0.468 of their own range neither step is a cliff.
|
|
1060
|
+
|
|
1061
|
+
🔸 The line under each `depth "…"` is the other question, and it is not about
|
|
1062
|
+
the ceiling. A `grid` spans the whole part window and a head is not a rectangle, so
|
|
1063
|
+
some of the lattice lands where `head.png` draws nothing and takes its depth
|
|
1064
|
+
from the sheet out there. That is a **count and not a complaint** — read it
|
|
1065
|
+
against the mesh: 80 of 189 is the border of a lattice over a cut-out and the
|
|
1066
|
+
ceiling is still a form, while a count approaching the whole mesh with a step
|
|
1067
|
+
share near 1 beside it is a mesh reading background.
|
|
1068
|
+
|
|
1069
|
+
⇒ **A small ceiling with a ratio near 1, a step well above 3 and a step share
|
|
1070
|
+
well under 1 is a steep surface: flatten the map.** A small ceiling with a large
|
|
1071
|
+
ratio, or with a step near 1, is a *sheet* problem: the grain, the 8-bit
|
|
1072
|
+
rounding, or a stray pixel. A small ceiling with a step share **near 1** is
|
|
1073
|
+
neither — it is a **discontinuity**, and no angle is the right one to quote for
|
|
1074
|
+
it. [`docs/FACE.md` §2.2](FACE.md) has the amplitudes, what each one costs, and
|
|
1075
|
+
why flattening does not apply to the third case.
|
|
1076
|
+
|
|
1077
|
+
🚨 **How to tell a discontinuity from a steep surface in one move: refine the
|
|
1078
|
+
lattice and build again.** A form's ceiling settles and its step share halves; a
|
|
1079
|
+
cliff's ceiling **halves** and its step share does not move. Measured on a
|
|
1080
|
+
synthetic raised cosine against the same cosine with one column of cliff planted
|
|
1081
|
+
in it, over three doublings: the form goes 0.358 → 0.195 → 0.100 while its
|
|
1082
|
+
ceiling moves 41.99° → 39.59° → 38.84°, and the cliff goes 0.460 → 0.489 → 0.497
|
|
1083
|
+
while its ceiling goes 37.07° → 18.59° → 9.24°. The second one is not converging
|
|
1084
|
+
on anything.
|
|
1060
1085
|
|
|
1061
1086
|
⛔ **rigc will not filter the sheet for you, and you should not want it to.** A
|
|
1062
1087
|
smoothed measurement would describe a surface the deform key is not built from:
|
|
@@ -1123,9 +1148,50 @@ refuses it instead:
|
|
|
1123
1148
|
| `gamma` or `contrast` at or below 0 | `collapses the range onto the midpoint … so the map would describe a flat part` |
|
|
1124
1149
|
| a `near` that is neither | `it is "white" or "black"` |
|
|
1125
1150
|
|
|
1126
|
-
A sheet
|
|
1127
|
-
|
|
1128
|
-
|
|
1151
|
+
A sheet that is **opaque everywhere** — a full-frame render with the background
|
|
1152
|
+
in it, which is what monocular depth estimation produces — covers its whole grid
|
|
1153
|
+
by construction, so the coverage refusal has nothing to hold it to and skips.
|
|
1154
|
+
That is right: a full-frame sheet is a legitimate statement and rigc has no
|
|
1155
|
+
authority to guess an input away. But the defect the refusal exists to catch is
|
|
1156
|
+
still reachable in that encoding, so the report counts it instead
|
|
1157
|
+
([#449](https://github.com/firejune/rigc/issues/449)):
|
|
1158
|
+
|
|
1159
|
+
```
|
|
1160
|
+
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
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
⚠️ **The reported range is NOT where this shows up**, and this guide said it was
|
|
1164
|
+
until it was measured. A background level is a legitimate depth value, so a map
|
|
1165
|
+
half of which is background reports exactly as full a range as one that is all
|
|
1166
|
+
subject: on the measured build, `z=[0, 223.97]` of a stated 224 — healthy — with
|
|
1167
|
+
54 % of the mesh reading background. "Covers its whole grid" is true and about
|
|
1168
|
+
the wrong grid; the question was never coverage of the *sheet*, it was whether
|
|
1169
|
+
the mesh is sampling **art**.
|
|
1170
|
+
|
|
1171
|
+
| The input | What you get |
|
|
1172
|
+
| --- | --- |
|
|
1173
|
+
| 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 |
|
|
1174
|
+
| 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 |
|
|
1175
|
+
| a mesh every one of whose vertices sits on drawn art | nothing — no line, and no refusal |
|
|
1176
|
+
|
|
1177
|
+
🔸 It is a raw count with **no reach subtracted from it**, so a `contour` mesh
|
|
1178
|
+
reports most or all of its vertices: its outline is pushed `margin` pixels
|
|
1179
|
+
outside the silhouette by design, and out there the part draws nothing.
|
|
1180
|
+
Discounting the margin would mean borrowing a number authored for the trace to
|
|
1181
|
+
mean "close enough" for the sheet, and rigc does not invent tolerances.
|
|
1182
|
+
|
|
1183
|
+
⭐ **So the line says why instead**, because rigc built that outline and knows
|
|
1184
|
+
what it is:
|
|
1185
|
+
|
|
1186
|
+
```
|
|
1187
|
+
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
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
The attribution is on a `contour` and **nowhere else**. A `grid`'s border is
|
|
1191
|
+
where your `us`/`vs` put it, not where a margin did, so the same sentence there
|
|
1192
|
+
would be a lie and the count stands alone. What that leaves true either way: the
|
|
1193
|
+
rim's depth really did come from off the art, which is harmless on a sheet
|
|
1194
|
+
dilated past the margin and is the whole failure on a full-frame estimate.
|
|
1129
1195
|
|
|
1130
1196
|
##### `soft` — which part is soft, and which bone carries it
|
|
1131
1197
|
|
|
@@ -2670,7 +2736,7 @@ deform (what each key does to the geometry — figures with names, never a bar;
|
|
|
2670
2736
|
| `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
2737
|
| `winding` | triangles whose winding survived, and how many the key pinched onto zero area. A fold says so and points at `A39` |
|
|
2672
2738
|
| 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
|
|
2739
|
+
| `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
2740
|
|
|
2675
2741
|
📌 **The frame is the posed one, and the denominator is 1.000 by definition.**
|
|
2676
2742
|
Both sides of every comparison are taken at the key's own time with the animation
|
|
@@ -2939,6 +3005,21 @@ deformDialsDisagreed=1 deformDialDisagreed=dial|artifact:knob.x@2.321e-8|reaches
|
|
|
2939
3005
|
- A **tie** never carries `probe:`, `reaches:` or `outside:`, and never counts as a
|
|
2940
3006
|
disagreement. There is one belief there, not two.
|
|
2941
3007
|
|
|
3008
|
+
**And `explain`'s rollup carries it too, with the breadcrumb its neighbours have**
|
|
3009
|
+
([#440](https://github.com/firejune/rigc/issues/440)). The per-key `frame` lines
|
|
3010
|
+
say it once each; the rollup says it once per animation, and ends by naming the
|
|
3011
|
+
two readings a `build` prints it under — because they differ by one letter, and a
|
|
3012
|
+
breadcrumb naming only one would send a reader to grep for the other:
|
|
3013
|
+
|
|
3014
|
+
```
|
|
3015
|
+
.. 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
|
|
3016
|
+
```
|
|
3017
|
+
|
|
3018
|
+
The count after `outside` is the length of the stats line's own `outside:` list,
|
|
3019
|
+
and `deformDialsDisagreed` / `deformDialDisagreed` are checked against the keys
|
|
3020
|
+
`A39` really printed rather than restated — `DW34`. An agreed dial adds no line
|
|
3021
|
+
(`DW35`) and a **tie** is never rolled up as a disagreement (`DW36`).
|
|
3022
|
+
|
|
2942
3023
|
⛔ **None of it refuses a build**, and the reason is measured rather than chosen.
|
|
2943
3024
|
The field the survey drives is the largest response the probe found, so its reach
|
|
2944
3025
|
always *contains* the artifact's: a disagreement cannot make `A39` miss a frame the
|
package/docs/FACE.md
CHANGED
|
@@ -364,12 +364,54 @@ geometry ([AUTHORING §3.4](AUTHORING.md)):
|
|
|
364
364
|
```
|
|
365
365
|
turn ceiling yaw +31.41° / -32.01° pitch +32.01° / -31.41°
|
|
366
366
|
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
|
|
367
|
-
first to fold: yaw + at 31.41°, triangle 960 [113,112,593], the sheet steps 12.52 level(s) across it
|
|
367
|
+
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
|
|
368
368
|
```
|
|
369
369
|
|
|
370
370
|
Read it as a fact about **the sheet**. If the number is too small, the fix is in
|
|
371
371
|
the map — flatten it where the part curves away — and not in the lattice.
|
|
372
372
|
|
|
373
|
+
#### ⚠️ That rule presumes the map is continuous
|
|
374
|
+
|
|
375
|
+
"Flatten it where the part curves away" is an edit to a **surface**, and it
|
|
376
|
+
assumes there is one under the whole mesh. A depth sheet estimated from a
|
|
377
|
+
picture of cut-out art is not a surface: it is piecewise, with a **cliff at every
|
|
378
|
+
occlusion boundary** — figure against background at the silhouette, and one part
|
|
379
|
+
of the figure over another wherever they overlap. There is nothing to flatten
|
|
380
|
+
across a cliff, because the two sides are not two ends of a slope. They are two
|
|
381
|
+
different things at two different depths, and a 2.5D turn does not model
|
|
382
|
+
occlusion at all.
|
|
383
|
+
|
|
384
|
+
The ceiling reads the cliff, correctly, and the number it reports is real:
|
|
385
|
+
walked one degree at a time through the survey `A39` refuses from, a reported
|
|
386
|
+
1.936° admits +1° and reverses 8 triangles at +2°. **The rig genuinely folds at
|
|
387
|
+
two degrees.** What is wrong is not the instrument and not the mesh — it is that
|
|
388
|
+
the question "how far can this turn" has no answer for an input with a
|
|
389
|
+
discontinuity in it, and the ceiling proves it by halving with every doubling of
|
|
390
|
+
the lattice: measured tangent ratios 2.02 / 1.95 / 2.02, `tan t ∝ h` exactly,
|
|
391
|
+
with no limit to converge to.
|
|
392
|
+
|
|
393
|
+
🚨 **And the two figures beside the ceiling both call it healthy.** The 1st
|
|
394
|
+
percentile reads 1.02–2.17, which is a band reaching the limit together — and it
|
|
395
|
+
*is* a band, because an outline is long. The depth step reads 148–252 levels of
|
|
396
|
+
255, far above the quantisation floor — and the sheet really did say that much,
|
|
397
|
+
in one step. Divided by the range, the same number says the opposite: the step
|
|
398
|
+
share pins at **0.92–0.99** where rigc's own gallery reads 0.112 and 0.468.
|
|
399
|
+
|
|
400
|
+
⇒ Two ways out, and **neither of them is flattening**:
|
|
401
|
+
|
|
402
|
+
- **Mesh only what is continuous.** One face, one lock, one sleeve — a region
|
|
403
|
+
the sheet describes without a jump in it — rather than a lattice over a whole
|
|
404
|
+
figure. Whether a mask that tight gives a usable angle is not yet measured;
|
|
405
|
+
the mask has to be painted rather than thresholded, for the reason `soft`
|
|
406
|
+
is painted (§3.4's `soft` block).
|
|
407
|
+
- **State a sheet that was authored rather than estimated.** §2.2's raised
|
|
408
|
+
cosine holds 63–64° from 289 vertices to 32,761 because somebody drew its
|
|
409
|
+
slope. That is the input this whole section is about.
|
|
410
|
+
|
|
411
|
+
⛔ rigc will not decide that your sheet is the wrong kind of thing. It has every
|
|
412
|
+
authority to say what it measured, and the step share is that
|
|
413
|
+
([#448](https://github.com/firejune/rigc/issues/448)).
|
|
414
|
+
|
|
373
415
|
📐 Method, harness and the full ladders live in the repository rather than in
|
|
374
416
|
this package, as
|
|
375
417
|
[`bench/studies/2026-09-05-density`](https://github.com/firejune/rigc/tree/main/bench/studies/2026-09-05-density) —
|
|
@@ -394,17 +436,22 @@ the range describes the same surface and reports 5.8° less of it. Method and
|
|
|
394
436
|
ladders:
|
|
395
437
|
[`bench/studies/2026-09-05-noise`](https://github.com/firejune/rigc/tree/main/bench/studies/2026-09-05-noise).
|
|
396
438
|
|
|
397
|
-
⭐ **And you do not have to guess which of the
|
|
398
|
-
lines under the ceiling say it
|
|
399
|
-
|
|
400
|
-
the
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
439
|
+
⭐ **And you do not have to guess which of the three you are looking at.** The
|
|
440
|
+
lines under the ceiling say it
|
|
441
|
+
([#412](https://github.com/firejune/rigc/issues/412),
|
|
442
|
+
[#448](https://github.com/firejune/rigc/issues/448)): the **1st percentile over
|
|
443
|
+
the ceiling** is near 1 when a band of the mesh reaches the limit together and
|
|
444
|
+
near 10 when one triangle does, which is a texel — 1.003 against 10.652 for the
|
|
445
|
+
two sheets above. The **depth step across the triangle that folds first**, in
|
|
446
|
+
levels, is the second: below about 3 the ceiling is quantisation, and at 1 it is
|
|
447
|
+
`atan(255·h / zScale)` and carries nothing about the form at all. The **same step
|
|
448
|
+
over the range the mesh sampled** is the third, and it is the one that separates
|
|
449
|
+
a steep surface from a cliff — a form's halves with every doubling of the lattice
|
|
450
|
+
while its angle settles, a discontinuity's does not move while its angle halves.
|
|
451
|
+
All three are reports and none of them moves the ceiling — ⛔ rigc will not filter
|
|
452
|
+
a depth map, because a smoothed measurement would describe a surface the deform
|
|
453
|
+
key is not built from and `A39` would go on refusing at the raw angle.
|
|
454
|
+
[AUTHORING §3.4](AUTHORING.md) has the reading table.
|
|
408
455
|
|
|
409
456
|
---
|
|
410
457
|
|
|
@@ -872,7 +919,7 @@ anything:
|
|
|
872
919
|
Three collisions survive there: `headroll` rotate in all three, `brows`
|
|
873
920
|
translatey in two, and the locks' rotate in two. ⚠️ **On plain Spine the fixes
|
|
874
921
|
are ordinary — `MixBlend.add` on the layered track, or splitting a bone into a
|
|
875
|
-
stack (`headroll_idle` under `
|
|
922
|
+
stack (`headroll_idle` under `headroll_layer`) — but both are runtime or rig
|
|
876
923
|
decisions the motion spec cannot express, so nothing warns an author that two of
|
|
877
924
|
their animations will fight.**
|
|
878
925
|
|
|
@@ -1195,7 +1242,7 @@ by name, on both its keys, with the triangles listed:
|
|
|
1195
1242
|
```
|
|
1196
1243
|
FAIL A39_DEFORM_KEEPS_TRIANGLE_WINDING: animation "turn" deform head/head key 1
|
|
1197
1244
|
(t=0.6200000047683716s): 8 of 32 triangle(s) reverse winding — triangle 0
|
|
1198
|
-
[0,
|
|
1245
|
+
[0,15,16] 1890.000 -> -544.548px²; …
|
|
1199
1246
|
```
|
|
1200
1247
|
|
|
1201
1248
|
and builds (a) and the good one both still PASS it, because **an inverted band is
|
package/docs/SPEC_COVERAGE.md
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
# Spine 4.3 export-format surface vs. rigc coverage vs. the example ladder
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
The marker below is the machine half of the 🔼 note further down: this file's
|
|
5
|
-
statements are a dated snapshot, not current state, so the selftest's currency
|
|
6
|
-
gate (CUR01–CUR06, issue #360) leaves its numbers alone. Do NOT copy it into a
|
|
7
|
-
document that describes how rigc behaves today — that is the whole class the
|
|
8
|
-
gate exists to catch.
|
|
9
|
-
-->
|
|
10
|
-
<!-- currency: dated-record -->
|
|
3
|
+
**A dated record:** what this page states was measured on the date it carries and is not kept current, so the selftest's currency gate leaves its figures alone.
|
|
11
4
|
|
|
12
5
|
Research note, 2026-08-22. A survey, not a plan: it establishes what the 4.3 export format can hold,
|
|
13
6
|
what rigc emits today, and what the official example projects actually use — so the gap list is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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": {
|
package/src/compile.ts
CHANGED
|
@@ -910,6 +910,21 @@ function partPlate(img: CompiledImage): Plate {
|
|
|
910
910
|
return img.atlas === undefined ? page : extractRegion(page, img.atlas);
|
|
911
911
|
}
|
|
912
912
|
|
|
913
|
+
/**
|
|
914
|
+
* A part's alpha channel on its own grid — where the drawing is, and where it
|
|
915
|
+
* is not.
|
|
916
|
+
*
|
|
917
|
+
* Two readers want exactly this and they used to be one inline loop and one
|
|
918
|
+
* absence: the contour generator traces it, and `sampleMeshDepth` counts the
|
|
919
|
+
* vertices whose depth came from outside it (issue #449). One function so the
|
|
920
|
+
* two cannot come to mean different things by "the part draws here".
|
|
921
|
+
*/
|
|
922
|
+
function plateAlpha(plate: Plate): Uint8Array {
|
|
923
|
+
const alpha = new Uint8Array(plate.width * plate.height);
|
|
924
|
+
for (let i = 0; i < alpha.length; i++) alpha[i] = plate.data[i * 4 + 3];
|
|
925
|
+
return alpha;
|
|
926
|
+
}
|
|
927
|
+
|
|
913
928
|
export function compile(opts: CompileOptions): CompileResult {
|
|
914
929
|
const rigPath = resolve(opts.rigPath);
|
|
915
930
|
const motionPath = resolve(opts.motionPath);
|
|
@@ -2711,9 +2726,40 @@ function buildGeneratedMesh(
|
|
|
2711
2726
|
* sheet that carries an alpha channel is held to it, and the fix — dilate the
|
|
2712
2727
|
* sheet past the mesh margin — is named in the message.
|
|
2713
2728
|
*
|
|
2714
|
-
*
|
|
2715
|
-
*
|
|
2716
|
-
*
|
|
2729
|
+
* ## The count beside the third refusal, and why it is a count (issue #449)
|
|
2730
|
+
*
|
|
2731
|
+
* A sheet with no transparent texel anywhere covers its whole grid by
|
|
2732
|
+
* construction, so the third refusal has nothing to hold it to and skips. That
|
|
2733
|
+
* is the encoding a monocular depth estimator produces — a full-frame opaque
|
|
2734
|
+
* render with the background in it — and it is a legitimate statement, so
|
|
2735
|
+
* skipping the refusal is right. What was wrong is that nothing took its place:
|
|
2736
|
+
* measured on one cell and one depth field, the same 54 %-background mesh is a
|
|
2737
|
+
* named refusal when the sheet's alpha is cut to the art and a green build when
|
|
2738
|
+
* it is 255 everywhere.
|
|
2739
|
+
*
|
|
2740
|
+
* ⚠️ **`range` is not where that shows up**, and this header said it was until
|
|
2741
|
+
* #449 measured it. On the build above `range` reads `[0, 223.97]` of a stated
|
|
2742
|
+
* 224 — full, healthy — because the background is a legitimate depth value and
|
|
2743
|
+
* a map that is half background has exactly as full a range as one that is all
|
|
2744
|
+
* subject. "Covers its whole grid" is true and about the wrong grid: the
|
|
2745
|
+
* question was never coverage of the *sheet*, it was whether the mesh is
|
|
2746
|
+
* sampling **art**.
|
|
2747
|
+
*
|
|
2748
|
+
* ⇒ what shows it is `undrawn`: how many of the mesh's vertices take their
|
|
2749
|
+
* depth from a texel **the part image does not draw**, over the same four
|
|
2750
|
+
* bilinear taps the refusal walks. That is a measurement, not a guess, so it is
|
|
2751
|
+
* reported and never refused — a full-frame sheet is a statement rigc has no
|
|
2752
|
+
* authority to guess away.
|
|
2753
|
+
*
|
|
2754
|
+
* 🔸 It is a raw count with no reach subtracted from it, which is why a
|
|
2755
|
+
* `contour` mesh reports most or all of its vertices: its outline is pushed
|
|
2756
|
+
* `margin` pixels outside the silhouette by design, and out there the part
|
|
2757
|
+
* draws nothing. That is the true reading of that geometry rather than a defect
|
|
2758
|
+
* in the count — the rim's depth really did come from off the art, which is
|
|
2759
|
+
* benign on a sheet dilated past the margin and is the whole failure on a
|
|
2760
|
+
* full-frame estimate. Discounting the margin would be borrowing a number
|
|
2761
|
+
* authored for the trace to mean "close enough" for the sheet, and rigc does
|
|
2762
|
+
* not invent tolerances.
|
|
2717
2763
|
*/
|
|
2718
2764
|
function sampleMeshDepth(
|
|
2719
2765
|
spec: NonNullable<Extract<NonNullable<RigMeshAttachment['generator']>, { kind: 'contour' }>['depth']>,
|
|
@@ -2734,6 +2780,14 @@ function sampleMeshDepth(
|
|
|
2734
2780
|
* units `z` is already in before it is taken.
|
|
2735
2781
|
*/
|
|
2736
2782
|
toBind: (px: number, py: number) => readonly [number, number],
|
|
2783
|
+
/**
|
|
2784
|
+
* The PART's own alpha channel, in the same grid the sheet is sampled in —
|
|
2785
|
+
* what the drawing actually covers, as against what the sheet covers.
|
|
2786
|
+
*
|
|
2787
|
+
* The two are different questions and the header says why. This one has no
|
|
2788
|
+
* refusal behind it: it is counted, reported, and left to the author.
|
|
2789
|
+
*/
|
|
2790
|
+
partAlpha: Uint8Array,
|
|
2737
2791
|
partWidth: number,
|
|
2738
2792
|
partHeight: number,
|
|
2739
2793
|
where: string,
|
|
@@ -2771,19 +2825,32 @@ function sampleMeshDepth(
|
|
|
2771
2825
|
// Every texel a bilinear tap touches has to be covered, not just the nearest
|
|
2772
2826
|
// one: a vertex half a pixel outside the sheet blends real depth with the
|
|
2773
2827
|
// background and lands somewhere neither states.
|
|
2828
|
+
//
|
|
2829
|
+
// 🔒 One walk, one footprint, two readings. `cover` is the SHEET's alpha and
|
|
2830
|
+
// decides a refusal; `partAlpha` is the PART's and decides a count. They are
|
|
2831
|
+
// deliberately the same four taps and the same clamp — the second is the
|
|
2832
|
+
// first's instinct applied to the other input (issue #449), and writing it as
|
|
2833
|
+
// a second loop is how the two would come to disagree about which texels a
|
|
2834
|
+
// vertex reads.
|
|
2835
|
+
const cx = (i: number): number => (i < 0 ? 0 : i > partWidth - 1 ? partWidth - 1 : i);
|
|
2836
|
+
const cy = (j: number): number => (j < 0 ? 0 : j > partHeight - 1 ? partHeight - 1 : j);
|
|
2774
2837
|
const uncovered: number[] = [];
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2838
|
+
let undrawn = 0;
|
|
2839
|
+
for (let v = 0; v < points.length; v++) {
|
|
2840
|
+
const x0 = Math.floor(points[v][0] - 0.5);
|
|
2841
|
+
const y0 = Math.floor(points[v][1] - 0.5);
|
|
2842
|
+
let covered = true;
|
|
2843
|
+
let drawn = true;
|
|
2844
|
+
for (const [dx, dy] of [[0, 0], [1, 0], [0, 1], [1, 1]] as const) {
|
|
2845
|
+
const at = cy(y0 + dy) * partWidth + cx(x0 + dx);
|
|
2846
|
+
if (cover[at] !== 255) covered = false;
|
|
2847
|
+
// Zero, not a threshold: "the part image draws nothing here" is a fact
|
|
2848
|
+
// about the file, and any other cut-off would be rigc deciding how faint
|
|
2849
|
+
// a texel has to be before it stops counting as art.
|
|
2850
|
+
if (partAlpha[at] === 0) drawn = false;
|
|
2786
2851
|
}
|
|
2852
|
+
if (!opaqueEverywhere && !covered) uncovered.push(v);
|
|
2853
|
+
if (!drawn) undrawn++;
|
|
2787
2854
|
}
|
|
2788
2855
|
if (uncovered.length > 0) {
|
|
2789
2856
|
const first = uncovered[0];
|
|
@@ -2825,6 +2892,7 @@ function sampleMeshDepth(
|
|
|
2825
2892
|
zScale: spec.zScale,
|
|
2826
2893
|
tone,
|
|
2827
2894
|
range: [r6(lo), r6(hi)],
|
|
2895
|
+
undrawn,
|
|
2828
2896
|
ceiling: turnCeiling(
|
|
2829
2897
|
points.map(([px, py]) => toBind(px, py)),
|
|
2830
2898
|
z,
|
|
@@ -3064,6 +3132,7 @@ function buildGridAttachment(
|
|
|
3064
3132
|
geometry.points,
|
|
3065
3133
|
geometry.triangles,
|
|
3066
3134
|
(px, py) => toBoneLocal(anchor, anchor.worldX + px * toArt - w / 2, anchor.worldY + h / 2 - py * toArt),
|
|
3135
|
+
plateAlpha(plate),
|
|
3067
3136
|
plate.width,
|
|
3068
3137
|
plate.height,
|
|
3069
3138
|
where,
|
|
@@ -3160,8 +3229,7 @@ function buildContourAttachment(
|
|
|
3160
3229
|
// none — so "this part has no silhouette to trace" is a question about pixels,
|
|
3161
3230
|
// and `buildContourMesh` refuses it by counting them.
|
|
3162
3231
|
const plate = partPlate(img);
|
|
3163
|
-
const alpha =
|
|
3164
|
-
for (let i = 0; i < alpha.length; i++) alpha[i] = plate.data[i * 4 + 3];
|
|
3232
|
+
const alpha = plateAlpha(plate);
|
|
3165
3233
|
|
|
3166
3234
|
const margin = generator.margin ?? CONTOUR_DEFAULTS.margin;
|
|
3167
3235
|
const maxVertices = generator.maxVertices ?? CONTOUR_DEFAULTS.maxVertices;
|
|
@@ -3219,6 +3287,7 @@ function buildContourAttachment(
|
|
|
3219
3287
|
geometry.points,
|
|
3220
3288
|
geometry.triangles,
|
|
3221
3289
|
(px, py) => toBoneLocal(anchor, anchor.worldX + px * toArt - w / 2, anchor.worldY + h / 2 - py * toArt),
|
|
3290
|
+
alpha,
|
|
3222
3291
|
plate.width,
|
|
3223
3292
|
plate.height,
|
|
3224
3293
|
where,
|
package/src/depth.ts
CHANGED
|
@@ -374,14 +374,20 @@ const CEILING_AREA_FLOOR = 1e-6;
|
|
|
374
374
|
* Where one triangle turns inside out, which triangle that is — and, beside it,
|
|
375
375
|
* what the REST of this axis and side's triangles do.
|
|
376
376
|
*
|
|
377
|
-
*
|
|
378
|
-
* population it is the minimum of, and they are here because the
|
|
379
|
-
* cannot answer the question an author actually has: `degrees` is
|
|
380
|
-
* number whether a whole band of the mesh reaches the limit together
|
|
381
|
-
* triangle does, and those are a form and a bad texel respectively
|
|
377
|
+
* Everything down to `stepShare` is about one triangle. `count` and `p1` are
|
|
378
|
+
* about the population it is the minimum of, and they are here because the
|
|
379
|
+
* minimum alone cannot answer the question an author actually has: `degrees` is
|
|
380
|
+
* the same number whether a whole band of the mesh reaches the limit together
|
|
381
|
+
* or one triangle does, and those are a form and a bad texel respectively
|
|
382
382
|
* ([#412](https://github.com/firejune/rigc/issues/412),
|
|
383
383
|
* `bench/studies/2026-09-05-noise` §6).
|
|
384
384
|
*
|
|
385
|
+
* ⚠️ And a band is not sufficient evidence of a form either, which is what
|
|
386
|
+
* `stepShare` is here for: an OUTLINE is a band, so a mesh whose ceiling is set
|
|
387
|
+
* by the occlusion edge of a cut-out reads `p1/degrees` near 1 and a `depthStep`
|
|
388
|
+
* of most of the sheet's range — both of the older figures reading *healthy* on
|
|
389
|
+
* the same measurement ([#448](https://github.com/firejune/rigc/issues/448)).
|
|
390
|
+
*
|
|
385
391
|
* ⛔ Neither figure changes `degrees`, and neither is a threshold. rigc does not
|
|
386
392
|
* have the authority to guess its input away, so nothing here filters,
|
|
387
393
|
* smooths or rejects a sample — the ceiling stays the raw sheet read through
|
|
@@ -407,6 +413,42 @@ export interface FoldLimit {
|
|
|
407
413
|
* with no form left in it (`bench/studies/2026-09-05-noise` §3).
|
|
408
414
|
*/
|
|
409
415
|
depthStep: number;
|
|
416
|
+
/**
|
|
417
|
+
* `depthStep` over the depth range this mesh actually sampled — what fraction
|
|
418
|
+
* of everything the sheet said across the whole part it said across the one
|
|
419
|
+
* triangle that folds first.
|
|
420
|
+
*
|
|
421
|
+
* ⭐ The figure that tells a form from a cliff, and it is the one reading the
|
|
422
|
+
* other two cannot give ([#448](https://github.com/firejune/rigc/issues/448)).
|
|
423
|
+
* A form has a slope, so refining the lattice halves the step and halves this
|
|
424
|
+
* with it while the angle converges. A **discontinuity has no slope**: the
|
|
425
|
+
* step stays the whole range however fine the lattice gets, this figure pins
|
|
426
|
+
* near 1, and the ceiling halves with every doubling instead of converging —
|
|
427
|
+
* `tan t ∝ h`, an angle that describes nothing at any density.
|
|
428
|
+
*
|
|
429
|
+
* ⚠️ **The ceiling is not wrong when this reads high; the input is not a
|
|
430
|
+
* surface.** Measured on estimated sheets: reported 1.936°, and the runtime
|
|
431
|
+
* admits +1° and reverses 8 triangles at +2°. The rig genuinely folds at two
|
|
432
|
+
* degrees. What a `depthStep` near the whole range means is an occlusion
|
|
433
|
+
* boundary — figure against background, or one part of a figure over
|
|
434
|
+
* another — and a 2.5D turn does not model occlusion at all, so **no angle is
|
|
435
|
+
* the right one to quote for it**. The fix is upstream of the ceiling: mesh
|
|
436
|
+
* only what is continuous, or state a sheet that was authored rather than
|
|
437
|
+
* estimated.
|
|
438
|
+
*
|
|
439
|
+
* 🔒 In (0, 1] by construction, never a division by zero. `depthStep` is a
|
|
440
|
+
* difference between two of this mesh's own `z` values, so the span over all
|
|
441
|
+
* of them is at least as large; and a fold only exists where the axis area
|
|
442
|
+
* with `z` substituted in is non-zero, which needs two vertices of the
|
|
443
|
+
* triangle at different depths — so a mesh with no span reports no fold and
|
|
444
|
+
* never reaches the divide.
|
|
445
|
+
*
|
|
446
|
+
* ⛔ A report and never a threshold. rigc does not decide that an author's
|
|
447
|
+
* sheet is the wrong kind of thing; nothing here filters, and nothing here
|
|
448
|
+
* moves a ceiling. What to read off the number is stated in
|
|
449
|
+
* `docs/AUTHORING.md` §3.4.
|
|
450
|
+
*/
|
|
451
|
+
stepShare: number;
|
|
410
452
|
/** How many triangles fold on this axis and side — the population below. */
|
|
411
453
|
count: number;
|
|
412
454
|
/**
|
|
@@ -509,9 +551,26 @@ export interface TurnCeiling {
|
|
|
509
551
|
* triangle, which is a texel. A `depthStep` of one level is `atan(255·h/zScale)`
|
|
510
552
|
* and says nothing about the form at all.
|
|
511
553
|
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
554
|
+
* ## What a band cannot say either (issue #448)
|
|
555
|
+
*
|
|
556
|
+
* Both of those figures read *healthy* on an estimated depth sheet over cut-out
|
|
557
|
+
* art, and they do not merely stay silent — they affirm it. `p1/degrees` comes
|
|
558
|
+
* back at 1.02–2.17, which reads as a band; `depthStep` at 148–252 levels of
|
|
559
|
+
* 255, which reads as plenty said. It **is** a band, because an outline is long,
|
|
560
|
+
* and the sheet did say a great deal across that triangle — it said the whole
|
|
561
|
+
* distance from the figure to the background in one step.
|
|
562
|
+
*
|
|
563
|
+
* So each `FoldLimit` also carries `stepShare`, the same step divided by the
|
|
564
|
+
* range this mesh sampled. A form's halves per refinement while its angle
|
|
565
|
+
* converges; a discontinuity's pins near 1 while the angle halves. Measured:
|
|
566
|
+
* rigc's own gallery reads 0.112 and 0.468, a synthetic raised cosine 0.394
|
|
567
|
+
* falling to 0.027 under refinement, the same cosine with one planted cliff a
|
|
568
|
+
* flat 0.50, and estimated sheets 0.92–0.99.
|
|
569
|
+
*
|
|
570
|
+
* ⛔ All three are reports. Nothing here filters the sheet, and nothing here
|
|
571
|
+
* moves a ceiling: a smoothed measurement would describe a surface the deform
|
|
572
|
+
* key is not built from, and would part company with the gate that reads the
|
|
573
|
+
* raw one.
|
|
515
574
|
*
|
|
516
575
|
* @param points Vertices in the BIND space the deform offsets are authored in.
|
|
517
576
|
* Areas are translation-invariant, so the origin does not matter; the scale
|
|
@@ -546,6 +605,18 @@ export function turnCeiling(
|
|
|
546
605
|
}
|
|
547
606
|
const floor = largest * CEILING_AREA_FLOOR;
|
|
548
607
|
|
|
608
|
+
// The denominator `stepShare` is taken against: the depth range this mesh
|
|
609
|
+
// sampled, which is `range` in the report read off the same array. Taken over
|
|
610
|
+
// the WHOLE mesh rather than per triangle, because the question the figure
|
|
611
|
+
// answers is how much of what the sheet said here one triangle said.
|
|
612
|
+
let zLo = Infinity;
|
|
613
|
+
let zHi = -Infinity;
|
|
614
|
+
for (const d of z) {
|
|
615
|
+
if (d < zLo) zLo = d;
|
|
616
|
+
if (d > zHi) zHi = d;
|
|
617
|
+
}
|
|
618
|
+
const zSpan = zHi - zLo;
|
|
619
|
+
|
|
549
620
|
// One list per axis and side, so the ceiling can say whether it is the floor
|
|
550
621
|
// of a BAND or of a single triangle. Nothing here filters: every measurable
|
|
551
622
|
// triangle goes in exactly once, in triangle order, and the sort below is
|
|
@@ -591,7 +662,11 @@ export function turnCeiling(
|
|
|
591
662
|
// `count` and `p1` are filled once the whole population is in; a minimum
|
|
592
663
|
// cannot know its own percentile while it is still being found.
|
|
593
664
|
if (held === null || degrees < held.degrees) {
|
|
594
|
-
|
|
665
|
+
// `zSpan` cannot be zero here: `aAxis !== 0` needs two of this
|
|
666
|
+
// triangle's vertices at different depths, and the span over the whole
|
|
667
|
+
// mesh is at least that difference. A guard would be an unreachable
|
|
668
|
+
// branch, and an unreachable branch is not a control.
|
|
669
|
+
out[axis][side] = { degrees, triangle: n, ids, depthStep, stepShare: depthStep / zSpan, count: 0, p1: null };
|
|
595
670
|
}
|
|
596
671
|
}
|
|
597
672
|
}
|
package/src/types.ts
CHANGED
|
@@ -1057,15 +1057,6 @@ export interface CompileResult {
|
|
|
1057
1057
|
* authored mesh was not traced.
|
|
1058
1058
|
*/
|
|
1059
1059
|
holePixels?: number;
|
|
1060
|
-
/**
|
|
1061
|
-
* What a depth map put on this mesh's vertices, when one was named.
|
|
1062
|
-
*
|
|
1063
|
-
* The digest is over the levels rather than the file, so a re-encode of the
|
|
1064
|
-
* same sheet reports the same provenance; `range` is what was actually
|
|
1065
|
-
* sampled, which is the number that says whether the map covers the part or
|
|
1066
|
-
* a corner of it. Absent when no map was named — never zeroes, which would
|
|
1067
|
-
* read as "sampled and found flat".
|
|
1068
|
-
*/
|
|
1069
1060
|
/**
|
|
1070
1061
|
* The soft region a `soft` block carried to its own bone, when one was
|
|
1071
1062
|
* named — the mask, its digest, and how many vertices it reached.
|
|
@@ -1075,6 +1066,18 @@ export interface CompileResult {
|
|
|
1075
1066
|
* and a nose does not wobble.
|
|
1076
1067
|
*/
|
|
1077
1068
|
soft?: { mask: string; digest: string; bone: string; carried: number; ramped: number };
|
|
1069
|
+
/**
|
|
1070
|
+
* What a depth map put on this mesh's vertices, when one was named.
|
|
1071
|
+
*
|
|
1072
|
+
* The digest is over the levels rather than the file, so a re-encode of the
|
|
1073
|
+
* same sheet reports the same provenance; `range` is what was actually
|
|
1074
|
+
* sampled. Absent when no map was named — never zeroes, which would read as
|
|
1075
|
+
* "sampled and found flat".
|
|
1076
|
+
*
|
|
1077
|
+
* ⚠️ This comment sat above `soft` rather than above the field it describes
|
|
1078
|
+
* until issue #449 came to add to it, which is the same drift `CUR07` was
|
|
1079
|
+
* built for one file over — nothing derives a doc comment's neighbour.
|
|
1080
|
+
*/
|
|
1078
1081
|
depth?: {
|
|
1079
1082
|
/** The sheet, as written in the spec. */
|
|
1080
1083
|
image: string;
|
|
@@ -1086,6 +1089,24 @@ export interface CompileResult {
|
|
|
1086
1089
|
tone: { gamma: number; contrast: number; bias: number };
|
|
1087
1090
|
/** Least and greatest `z` over the mesh's vertices, in attachment units. */
|
|
1088
1091
|
range: [number, number];
|
|
1092
|
+
/**
|
|
1093
|
+
* How many of the mesh's vertices took their depth from a texel **the
|
|
1094
|
+
* part image does not draw** (issue #449).
|
|
1095
|
+
*
|
|
1096
|
+
* ⚠️ Not what `range` says, and this is the field that exists because
|
|
1097
|
+
* `range` was claimed to say it. A map that is half background has
|
|
1098
|
+
* exactly as full a range as one that is all subject, because a
|
|
1099
|
+
* background level is a legitimate depth — so a full-frame sheet over a
|
|
1100
|
+
* cut-out part reports a healthy `[0, 223.97]` of 224 with 54 % of the
|
|
1101
|
+
* mesh reading background.
|
|
1102
|
+
*
|
|
1103
|
+
* A count and never a refusal: the same defect is already a named refusal
|
|
1104
|
+
* when the sheet's alpha is cut to the art, and a sheet that is opaque
|
|
1105
|
+
* everywhere is a statement rigc has no authority to guess away. Zero is
|
|
1106
|
+
* a real answer here rather than an absence — every mesh that names a
|
|
1107
|
+
* depth map also names an image, so the measurement is always taken.
|
|
1108
|
+
*/
|
|
1109
|
+
undrawn: number;
|
|
1089
1110
|
/**
|
|
1090
1111
|
* The turn this geometry takes on this sheet before a triangle reverses,
|
|
1091
1112
|
* per axis and per direction — `src/depth.ts`'s `turnCeiling`.
|