@rig-c/parts 1.12.0 → 1.13.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 +8 -4
- package/cli.ts +13 -4
- package/docs/AUTHORING.md +100 -7
- package/docs/STABILITY.md +1 -1
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +8 -5
- package/src/build.ts +11 -1
- package/src/check.ts +65 -4
- package/src/meshdraft.ts +36 -2
- package/src/propose.ts +55 -2
- package/src/squeeze.ts +274 -0
package/README.md
CHANGED
|
@@ -228,7 +228,7 @@ than queueing behind someone else's job.
|
|
|
228
228
|
## The loop, for an agent
|
|
229
229
|
|
|
230
230
|
```sh
|
|
231
|
-
rig-parts inputs --source painting.png --config config.json --out inputs # st_input_full.png
|
|
231
|
+
rig-parts inputs --source painting.png --config config.json --out inputs # st_input_full.png; the config needs assemble.rig_scale
|
|
232
232
|
# See-through on st_input_full.png (external, or `rig-parts comfy seethrough`) -> layers/full
|
|
233
233
|
rig-parts layers layers/full # every layer: box, opaque px, depth, plausibility figures; WARN lines
|
|
234
234
|
rig-parts propose --head-box --full layers/full --canvas 1664x2432
|
|
@@ -323,9 +323,13 @@ These are limits of the approach, stated so nobody reads more into a green run:
|
|
|
323
323
|
existence proof, not a rate.
|
|
324
324
|
- **`loop` writes GIF and APNG (lossless and indexed), not WebP**: an animated WebP needs a VP8/VP8L
|
|
325
325
|
encoder, which this package does not carry.
|
|
326
|
-
- **The proposer
|
|
327
|
-
put the hip below
|
|
328
|
-
|
|
326
|
+
- **The proposer reads the torso by screen y unless it is told the pose.** Its ratios
|
|
327
|
+
put the hip below `bottomwear`'s top edge and the chest above it, and hang the sleeves
|
|
328
|
+
from the shoulders. A figure whose joints are not there — reclining, an arm raised, or
|
|
329
|
+
seated in a skirt that does not start at the hip — needs `propose --keypoints`; sitting
|
|
330
|
+
alone does not, but a clean LINT does not show the hip is right (it asks only that the
|
|
331
|
+
hip be below the chest and the shoulders), so read the overlay ([AUTHORING §3](docs/AUTHORING.md)). The
|
|
332
|
+
file of joints comes from any pose
|
|
329
333
|
estimator or a hand; this package does not estimate a pose itself, and its
|
|
330
334
|
keypoint support has been measured on generated figures and on files read off the
|
|
331
335
|
examples' own configs, never on an estimator's output.
|
package/cli.ts
CHANGED
|
@@ -96,8 +96,13 @@ usage:
|
|
|
96
96
|
link of a fringe chain (hung from head on a head-run front hair mesh, its tip
|
|
97
97
|
no lower than the chin) that drifts more than ${FRINGE_STEP_PX} px plus a pixel's
|
|
98
98
|
rounding across the head's down axis (the eye line's perpendicular), for a
|
|
99
|
-
hip that is not below the chest,
|
|
100
|
-
height (the shoulders)
|
|
99
|
+
hip that is not below the chest, for a hip above ${HIP_MIN_FRACTION} of the figure's
|
|
100
|
+
height (the shoulders), and for two keyed chains on one lattice mesh whose
|
|
101
|
+
own swing, every other bone held, takes an edge of the rig stage's lattice
|
|
102
|
+
past check's TEXTURE_STRETCH bar (with each chain alone, the tips'
|
|
103
|
+
distance, and each remedy's own figure: one sign for opposed amplitudes,
|
|
104
|
+
dropping a chain; a contour or automatic mesh, or rule "heat", is said in
|
|
105
|
+
a "squeeze not read" note); the fringe's three chains stand in the head's frame,
|
|
101
106
|
rooted on the part's art, and a note says they are a template, not its
|
|
102
107
|
locks; a headwear/earwear layer with hanging strands
|
|
103
108
|
gets one pendulum chain per strand and a note with each strand's x, rows
|
|
@@ -273,8 +278,11 @@ usage:
|
|
|
273
278
|
than its lattice, and the report records each part's triangle and
|
|
274
279
|
overdraw difference. A part that passes is drafted, its note saying
|
|
275
280
|
what it was measured against; one that fails keeps its entry, and the
|
|
276
|
-
report names each failed reading and by how much.
|
|
277
|
-
drafted
|
|
281
|
+
report names each failed reading and by how much. An auto or contour
|
|
282
|
+
entry is neither drafted nor compared with anything: a NOT COMPARED line
|
|
283
|
+
and the report's not_compared name each, so build it once on a lattice
|
|
284
|
+
and compare the rig stage's mesh lines (AUTHORING §3, issue #307).
|
|
285
|
+
Written only when the drafted config's check is PASS. --requirements, --idle-keys,
|
|
278
286
|
--page-edges and --pack-shape are as for rig and check.
|
|
279
287
|
|
|
280
288
|
rig-parts check --rig <dir> --out <dir> [--parts <dir>] [--source <painting.png>]
|
|
@@ -880,6 +888,7 @@ function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes:
|
|
|
880
888
|
for (const f of res.findings) console.log(lintLine(f));
|
|
881
889
|
for (const m of res.unknownMeshes) console.log(`note: mesh ${JSON.stringify(m)} names no part in parts.json, so it was not linted`);
|
|
882
890
|
for (const w of res.lockUnread) console.log(`note: off-lock not read: ${w}`);
|
|
891
|
+
for (const w of res.squeezeUnread) console.log(`note: squeeze not read: ${w}`);
|
|
883
892
|
for (const b of res.missingTorsoBones) {
|
|
884
893
|
console.log(
|
|
885
894
|
res.basis?.torso.read === true
|
package/docs/AUTHORING.md
CHANGED
|
@@ -187,10 +187,58 @@ template the proposer wrote before issue #249 fires it on any head tilted more t
|
|
|
187
187
|
degrees (the trailer character, 23.7 degrees: all six links; `reference-demo`, 3.2
|
|
188
188
|
degrees, and `reference-sample`, -4.9 degrees: one link each).
|
|
189
189
|
|
|
190
|
-
**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
**Two chains on one lattice (issue #314).** `lint` swings every pair of chains a lattice
|
|
191
|
+
mesh's `segments` both name, both keyed by a chain track, on the rig stage's own lattice and
|
|
192
|
+
weights (`src/squeeze.ts`: the part padded as the rig stage pads it, `latticeMesh`, and
|
|
193
|
+
`influences` under the mesh's `r`, `exponent`, `influences` and `stray`, rounded as the
|
|
194
|
+
lattice rounds — so its vertex and triangle numbers are the ones `rig.json` and `check`
|
|
195
|
+
use). Each link turns as the idle's sine states it at every frame `check` renders, every
|
|
196
|
+
other bone held at the setup pose, and each edge is read by `check`'s own reader. A pair
|
|
197
|
+
whose worst edge is past `TEXTURE_STRETCH`'s bar (max(ratio, 1/ratio) above 1.926544)
|
|
198
|
+
prints
|
|
199
|
+
`LINT <a> and <b> on mesh '<part>' squeeze it: swung together, every other bone held, edge <i>-<j> of triangle <t> closes to <r> times its rest length at idle frame <f> (max(ratio, 1/ratio) <s>; <= 1.926544 is required, check's TEXTURE_STRETCH bar) — vertex <i> rides <bone> <w>, vertex <j> rides <bone> <w>; alone, <a> reads <x> and <b> <y>. Their tips are <d> px apart and …`
|
|
200
|
+
(`stretch it` and `opens` when the edge grows). The end names the amplitudes and the
|
|
201
|
+
remedies whose own figure passes the bar: with the two chains' amplitudes opposed link for
|
|
202
|
+
link, `give both the same sign (then <s'>)` — `<s'>` the same reading with the second
|
|
203
|
+
chain's signs turned; `drop one chain` when each alone passes, or `drop <b>, which alone is
|
|
204
|
+
past the bar too` when only one does; and, when none passes,
|
|
205
|
+
`lower the amplitudes`. A contour or automatic mesh, or `rule: "heat"`, is not rebuilt here:
|
|
206
|
+
a `note: squeeze not read: …` line names the mesh and its pairs. What it predicts is
|
|
207
|
+
`check`'s measurement without the other bones' motion. Measured: on `fixtures/squeeze.ts`
|
|
208
|
+
(two chains 39 px apart at their tips, the proposer's mirrored back-hair amplitudes) it
|
|
209
|
+
names the triangle, edge and frame `check` fails, 2.494 predicted against 2.488 measured,
|
|
210
|
+
and one sign passes both (1.054); on the four public examples it prints nothing; on
|
|
211
|
+
`reference-demo` the back-hair pair reads 1.393 at `hair_back` edge 43-42, frame 26 — the
|
|
212
|
+
edge and frame `check` reports as that example's worst, 1.388, the figure the bar is the
|
|
213
|
+
square of — and 1.075 with one sign; on `reference-sample` the sleeve pair reads 1.230 at
|
|
214
|
+
the edge and frame `check` reads 1.227 at.
|
|
215
|
+
|
|
216
|
+
**Why the back hair mirrors.** The proposer drafts `hairback_r` with amplitudes
|
|
217
|
+
`[0.6, 1.4, 2.4]` and `hairback_l` with their negatives, while the fringe, lock and skirt chains share one sign (the two sleeve chains are mirrored
|
|
218
|
+
the same way). The mirror is the reference proposer's draft,
|
|
219
|
+
ported as it was (`src/propose.ts`, `SIDES`); no reason is recorded with it. Opposed, the
|
|
220
|
+
two tips swing towards each other and apart on the same beat. On two hanks far apart that
|
|
221
|
+
costs nothing a bar reads (`demo`, tips 371 px apart: 1.050 for the pair, as for either
|
|
222
|
+
alone), and on one mass with the two chains close it squeezes the lattice between them
|
|
223
|
+
(issue #314: tips 39 px apart, an edge at 0.065 of its rest length). Whether the hair is one
|
|
224
|
+
mass is not something the proposer can read off the opaque shape — both chains always hang
|
|
225
|
+
on the one `back hair` part — so the draft is unchanged and the squeeze is measured instead,
|
|
226
|
+
by the lint above.
|
|
227
|
+
|
|
228
|
+
**A posed figure: `propose --keypoints`.** Without joints the proposer reads the
|
|
229
|
+
torso by screen y: the hip 0.14 face heights below `bottomwear`'s top edge (or below
|
|
230
|
+
the waist: *The hip*, below), the chest half way up to the neck, sleeves hanging from the
|
|
231
|
+
shoulders. A figure whose joints are not where those rules put them — a reclining
|
|
232
|
+
figure, an arm raised or crossed, or a seated figure whose skirt does not start at
|
|
233
|
+
the hip — is told where its joints are with one explicit file (issue #75,
|
|
234
|
+
`src/keypoints.ts`). Sitting alone does not call for it: a seated figure whose skirt
|
|
235
|
+
starts at the waist has its hip there (issue #314: a seated character proposed without
|
|
236
|
+
joints, its hip placed and `lint` clean, built green once its back-hair chains were
|
|
237
|
+
fixed). But a clean `lint` does not say the hip is right: the screen-y rules ask only
|
|
238
|
+
that the hip be below the chest and below 0.25 of the figure's height, and a hip at a
|
|
239
|
+
seated skirt's top passes both (selftest `PR56`: the rule's hip 28 px above where the
|
|
240
|
+
thighs leave the torso, no LINT line) — read the overlay, and where the hip or a sleeve
|
|
241
|
+
is off its joint, give the joints. A pose file named by
|
|
194
242
|
`generation.control.skeleton` is not that file and is not offered as its default:
|
|
195
243
|
the control skeleton is what was asked of the model, not where the joints ended up
|
|
196
244
|
(a consumer measured a generated picture not following the control's raised hand),
|
|
@@ -450,6 +498,48 @@ contour region is (each share to 5 places, a 0 dropped, the remainder to the hea
|
|
|
450
498
|
the cut it is rounded as the lattice always was. In the contour mode a region's bone counts
|
|
451
499
|
toward the cap like any other (*The vertex's weights* in `src/localweights.ts`).
|
|
452
500
|
|
|
501
|
+
### Which mode comes out smaller (issue #307)
|
|
502
|
+
|
|
503
|
+
The names suggest that `auto`, the mode that reduces a mesh, is the smallest. Measured on one
|
|
504
|
+
figure, it is not always: a reduction held to a large declared bend keeps many vertices to stay
|
|
505
|
+
near its dense source, and on such a part the outline `mesh-draft` drafts over a coarse grid
|
|
506
|
+
came out two to three times lighter. On parts that bend little, `auto` was far the smallest.
|
|
507
|
+
|
|
508
|
+
The figure is the trailer's (`rig-c/trailer`, 1108 px tall). Triangles per part, as the rig
|
|
509
|
+
stage's `mesh` lines print them: the kept modes from commit 45becfb; the `auto` counts of the
|
|
510
|
+
three parts that left it from the commit before, 24bd8ec (each `auto` with the film's bends
|
|
511
|
+
declared as `auto.stimulus` and `auto.motion.maxLocalDeformation` 3 px); the lattice column each
|
|
512
|
+
part at `"grid": 30`, the spacing `propose` drafts for this figure, in 45becfb's config
|
|
513
|
+
(`topwear` left on `auto`); and `mesh-draft` run over that lattice config. The bends are the
|
|
514
|
+
largest turns the trailer's `WORKING.md` states for its film, not re-measured here.
|
|
515
|
+
|
|
516
|
+
| part | the film bends it | `auto`, bends declared, 3 px | `mesh-draft` | lattice at `"grid": 30` |
|
|
517
|
+
| --- | --- | --- | --- | --- |
|
|
518
|
+
| `bottomwear` | 24° | 276 | 132 | 140 |
|
|
519
|
+
| `handwear_r` | 33° | 148 | 49 | 52 |
|
|
520
|
+
| `hairpin` | 95° (refused by `auto`; `contour` at its own spacing 13: 120) | — | 47, kept on its lattice (below) | 46 |
|
|
521
|
+
| `legwear` | 12° | 77 | — | 176 |
|
|
522
|
+
| `hair_back` | a few degrees | 28 | — | 120 |
|
|
523
|
+
| `handwear_l` | 4° | 31 | — | 72 |
|
|
524
|
+
| `neck` | — | 7 | — | 12 |
|
|
525
|
+
| `hair_front` | 11° | 112 | — | 114 |
|
|
526
|
+
|
|
527
|
+
Every count reproduces with `@rig-c/parts` 1.11.0 and rig-c 2.47.0 (the trailer's pins at
|
|
528
|
+
45becfb, both configs over the same assembled parts), and the lattice and `mesh-draft` columns
|
|
529
|
+
again with this package on rig-c 2.40.0. `mesh-draft` kept the hairpin on its lattice: its
|
|
530
|
+
candidate, 47 triangles, sat 1.055 rig px from the lattice over the idle against a line of 1, and
|
|
531
|
+
had one triangle more than the lattice's 46 (with the hairpin's lattice declaring
|
|
532
|
+
`influences: {floor: "continuous"}`, as 45becfb's contour entry does, the idle reads 0.822 and
|
|
533
|
+
passes, and the triangle line alone keeps it). The trailer's 47 is that candidate, written by
|
|
534
|
+
hand. The film's two cuts were compared at their most different frame, enlarged; no difference
|
|
535
|
+
showed, and every `check` bar passed on both.
|
|
536
|
+
|
|
537
|
+
So, before keeping an `auto` entry on a part a chain or physics bends far: build the part once on
|
|
538
|
+
a lattice, run `mesh-draft` (§4, step 12), and compare the two `mesh` lines. Nothing else reports
|
|
539
|
+
the difference; `mesh-draft` names every `auto` or `contour` entry as not compared, because it
|
|
540
|
+
drafts lattice entries only. This is one figure, and the two examples here were not measured this
|
|
541
|
+
way.
|
|
542
|
+
|
|
453
543
|
### A contour mesh (`meshes.<part>.contour`, issue #84)
|
|
454
544
|
|
|
455
545
|
The lattice keeps every grid cell that holds an art pixel, so its boundary and its interior
|
|
@@ -1425,7 +1515,10 @@ full loader before its assemble stage writes anything.
|
|
|
1425
1515
|
The two divisors are calibrated on three figures of similar size (1106 to
|
|
1426
1516
|
1185 px), and the bend set is a calibration, not your animation: a chain
|
|
1427
1517
|
your animations or physics bend further than 15° a link was not measured
|
|
1428
|
-
there. An `auto` or `contour` entry is yours and is left as it is
|
|
1518
|
+
there. An `auto` or `contour` entry is yours and is left as it is, and it
|
|
1519
|
+
is compared with nothing: a `NOT COMPARED` line and the report's
|
|
1520
|
+
`not_compared` name each such part, since which mode comes out smaller is
|
|
1521
|
+
measured, not named (§3, *Which mode comes out smaller*; issue #307). A
|
|
1429
1522
|
relative `motion.animations_from` or `generation.control.skeleton` keeps its
|
|
1430
1523
|
text, so it resolves against `draft/`; the command says so.
|
|
1431
1524
|
|
|
@@ -1439,7 +1532,7 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
|
|
|
1439
1532
|
| --- | --- | --- | --- |
|
|
1440
1533
|
| `layers` | the table, then one `author-made: layer "author:<label>" — …` line per layer the author made (group `author` in the table; §5, *A layer the author made*), then the `WARN` lines | every tag the plan will need has opaque pixels; one `face` in the head run; `0 WARN line(s)` | a run whose layer PNG is not its box's size (`LAYERS_PNG_MATCHES_BBOX`); a `WARN PLAN_LAYER_…` line (§6, *Plausibility*) |
|
|
1441
1534
|
| `sheet` of both runs | the tile list (and the sheet, if you can see) | eyes, irises, lashes and brows as left/right pairs in the head run | a head box that cut off an ornament: move `head_box`, re-run the head crop |
|
|
1442
|
-
| `assemble` | one line per part, the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`, then one `uncovered: <n> of the <N> uncovered error px lie on the full run's "<tag>", in a body of <px> px at x,y wxh; <the rule that left it out>` line per full-run layer the plan does not take from the full run whose uncovered error holds a body (5x5-opened, 8-connected, at least 150 full-run px in rig px), then one `drift: "<part>" refuses <n> px of <run>:<tag> (its colour more than 90 off the painting, max channel), in a body of <px> px at x,y wxh (…); by the plan's draw order "<holder>" holds <px> px of it (…), "<holder>" drawn in front of "<part>"` line per part whose drift refusals hold a body (the same opening and bar) that another part holds in the draw order — that layer is not what the painting shows there (none on the five public paintings); and look at `render/recomposite_error_rig.png`. Between the part lines and the totals, one `author-made: part "<part>" (<run>:author:<label>) — …` line per part from a layer the author made (below, *A layer the author made*), then one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then one `fringe pushed back: "<part>" <px> px, where the painting shows "<holder>" <px> px, …` line per part whose fringe was cleared (below) | on the examples: `reference-sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `reference-demo` 2.34 / 95.8 % / 10,340 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (reference-demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. When neither run holds it at all, an `assemble.patches` entry cuts it from the painting (below) |
|
|
1535
|
+
| `assemble` | one line per part — its counts (`op=` opaque, `vis=` visible, `src=` taken / candidates, `unproj=` visible but not projected, `drift=<n> (<p>% of core)`, `merged=`, `seam=`; *parts.json*, below). `drift=` is `refused_drift_px` in rig px and its share of the part's candidates, `projected_core_px`, which it is a subset of (issue #314); no bar reads either. On the four public examples the share runs to 94.5 % (`scarf` `hair_back`, 479 of 507) and the count to 26,472 (`scarf` `topwear`, 22.3 %), and the seam override repaints refused top-most pixels afterwards, so a large count says where See-through's colour was not taken, not that the part is unlike the painting: the `drift:` line below is the one that names a body of them another part holds. Then the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`, then one `uncovered: <n> of the <N> uncovered error px lie on the full run's "<tag>", in a body of <px> px at x,y wxh; <the rule that left it out>` line per full-run layer the plan does not take from the full run whose uncovered error holds a body (5x5-opened, 8-connected, at least 150 full-run px in rig px), then one `drift: "<part>" refuses <n> px of <run>:<tag> (its colour more than 90 off the painting, max channel), in a body of <px> px at x,y wxh (…); by the plan's draw order "<holder>" holds <px> px of it (…), "<holder>" drawn in front of "<part>"` line per part whose drift refusals hold a body (the same opening and bar) that another part holds in the draw order — that layer is not what the painting shows there (none on the five public paintings); and look at `render/recomposite_error_rig.png`. Between the part lines and the totals, one `author-made: part "<part>" (<run>:author:<label>) — …` line per part from a layer the author made (below, *A layer the author made*), then one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then one `fringe pushed back: "<part>" <px> px, where the painting shows "<holder>" <px> px, …` line per part whose fringe was cleared (below) | on the examples: `reference-sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `reference-demo` 2.34 / 95.8 % / 10,340 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (reference-demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. When neither run holds it at all, an `assemble.patches` entry cuts it from the painting (below) |
|
|
1443
1536
|
| `propose` | `note:` lines, `LINT` lines, the `coverage` lines, `landmarks.png`, `basis.json` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter; the `coverage:` summary says how many bones that verdict covers — a `not checked` bone was read by no check (its line says why), and a `ratio` or `fallback` bone in `basis.json` is a guess | a link off the art (a bone on the background) — move it onto the layer; `LINT <link> at … runs to … N degrees off the head's down axis …` — a fringe link that does not lean with the head (§3, the fringe in the head's frame): move it along the lock it drives; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for; a `no eyewhite part for eye_<s> …` note — the iris and lash parts it names ride `head` and do not blink, because no `eyewhite-<s>` part made that side's eye bone: take the eyewhite from the other run, or keep them on `head`; a `no face part: face box derived from …` note (always the first) — `head`, `neck` and every face-height scale come from a box guessed off the hair and neck (§3): check `head` and `neck` on `landmarks_head.png` and move them; when it adds `the blink shuts the eyes over no face part`, read `BLINK_NO_HOLE` after `check` — an eye with no face under it can open a hole (the `reference-sample` with only its face removed: 587 px). Under `--keypoints`: the `lint rule set:` line first (joints, or screen and why), then the joint notes — `missing: the rule "…" placed bone X instead` is a bone still guessed; `not used — …` names why an arm's joints did not place its sleeve (for a wrist on no part, the nearest part and the `--wrist-snap` that would bind it); a `wrist snap: …` note is a wrist bound past 15 px by the flag — check that chain's tip on `landmarks.png`; a `one handwear part holds both wrists` note is two chains on one mesh — cut the part with `assemble.cuts` to separate them; `LINT chest at … is not between the neck joint …`, `LINT hip at … is not past chest …`, `LINT hip at … is nearer the neck joint …` and `LINT <chain><i> at … does not advance from <s>_shoulder toward <s>_wrist` are the joint-frame lines: move that bone between, past or along the joints they name |
|
|
1444
1537
|
| `compare` | one line per pair (origin, parent, tip, length, direction — or SKIP and why), then the per-row summary, the unmapped bones of each side, `required:`, each side's roles and the `controls:` line | proposal against the config you corrected: the rows you changed and no others. Config against the `rig.json` `build` writes for it: measured on both examples, every origin within 6.19e-7 px (the rig's offsets are written to 6 places) and every chain link's tip and length within 4.99e-4 px (`length` is written to 3), printed to three places as `0.000` (a signed figure as `+0.000` or `-0.000`); `parent … is the right's ancestor at depth 2, <bone>_ctl between` for every keyed mesh bone, no `DIFFERENT` and no `NOT MAPPED` | `parent DIFFERENT: left a, right b` — a bone hangs from another parent than the other side's; `frames not related` — a rig with no stage (`skeleton.width`/`height`), two configs at different `rig_scale`: the distance rows SKIP, so declare the frame in a `--map` file if the author knows it; `FAIL STRUCTURE_REQUIRED_PRESENT` — a bone the map requires is missing through its pairs |
|
|
1445
1538
|
| `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`); in `rig.json`, each chain link's (or its `_ctl`'s) `length` and `rotation` | `cover 1.00000` on every mesh, both gate summaries `0 failed` (the compile and the packed pages); every link's `length` the distance to the next link and `rotation` its direction (Spine degrees, counter-clockwise, y up, local to the parent), every offset under it — a child bone's `x, y`, a weight's bind `x, y`, a region's `x, y` — in that turned frame, and a region on a link carrying `rotation` that turns it back upright. Nothing moved: `flattenRig` (`src/rig.ts`) turns every offset back and gives the unturned numbers | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
|
|
@@ -2030,7 +2123,7 @@ exits 0, and `mesh_draft.json` and the console name the reading and the amount.
|
|
|
2030
2123
|
| `CHECK_CHAIN_LAG` | a rotate track leads (or does not lag) the keyed bone above it, or a chain link swings less than the link above — or, in a chain that carries a part the scene file declares `rests`, more than the link above (issue #245; the object names the part and the file) | that chain track's `phase`/`lag` (a positive `lag`, a child `phase` above its parent's) or its `amps` (non-decreasing toward the tip; non-increasing on a chain declared resting) |
|
|
2031
2124
|
| `CHECK_TIP_OVER_ROOT` | a `handwear`/`bottomwear` part's art in the lower half of its box travels less than 1.4725 times as far as in the upper half, measured from the idle's posed geometry (rig px quoted) | the chain track's `amps` (grow toward the tip), or the mesh's `segments` (the chain must be among them), or its mode: on `scarf`'s short skirt the proposal's lattice read 1.207 and the same part in `contour` mode 1.738 with the same segments and idle (issue #118) — both carried the `chest` segment #262 removed, after which the lattice reads 3.487. When the detail says `the root travels with the breath (x px of y px): it is carried by \`<bone>\`` (issue #262) the chain is not the cause: a bone an idle `translate` track moves (the breath) moves the root more than the tip, and with that translate held still (the part posed again without it, in proportion to each vertex's weight on the bone and those under it) the ratio passes — the row adds `carried_by`, `breath_rig_px`, `root_without_breath_rig_px` and `tip_without_breath_rig_px`; take that bone's segment off the mesh, or bind the root to the bone the part hangs from. A part the scene file declares `rests` (issue #245) is held the other way, its tip half's travel at most its root half's (ratio ≤ 1); the detail then reads `the scene file's rests declares its tip at rest, so a ratio <= 1 is required`, and for it the fix is the chain's `amps` (shrinking toward the tip) or the mesh's tip weighted to a bone the idle does not swing |
|
|
2032
2125
|
| `CHECK_STILL_REGIONS_DARK` | a slot of the face's still set (the `face` slots and every slot drawn over the face that the head bone alone carries) moves relative to the bone the face's slot rides, or a `footwear` slot moves on the screen, by more than the arithmetic's own rounding — measured from the idle's posed geometry (the slot, its largest displacement in rig px, the idle frame and the tolerance quoted) | on the face: a face mesh with a weight on a bone other than the head (`segments`), or one deformed by another bone; on the feet: a mesh weighted to a swinging bone, or a region on the wrong bone. A bang swinging over the face is not this line's failure: its depth is reported (`crossing`), with no bar |
|
|
2033
|
-
| `CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other |
|
|
2126
|
+
| `CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named, and — issue #314 — the bones the edge's two vertices hang on: each vertex's two largest weights from `rig.json`, `vertex <i> is weighted to "<bone>" <w>, …`, then `on the idle's chains, the chain from "<first link>" carries "<bone>", …` — each chain `CHAIN_LAG` reads named once, by its first link — with a bone on no chain said, or `none of these bones hangs on a chain of the idle`); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other. Two chains named, with opposed amplitudes: give both one sign, or drop one — `propose --from-config` measures the pair before a build (§3, *Two chains on one lattice*) |
|
|
2034
2127
|
| `CHECK_SEAM_FRAME_SIZE`, `FRAMES_SIDECAR`, `CHECK_GEOMETRY_FILE` | rigc's render is not what its `frames.json` says, or its `geometry.json` is not a whole `rigc-geometry/1` export of the frames beside it (a frame count, a mesh whose vertex count differs from its rest entry; for the face half, another viewport than `frames.json`'s, other frame indices, the head bone or a feature's bone missing from a frame, a non-finite transform or time) | a rigc problem; report it |
|
|
2035
2128
|
| `LOOP_ENCODE` | the loop encoder refused a frame (translucent pixel in a GIF, a size change) | the frames; for a translucent frame write the lossless or the indexed APNG, which keep alpha |
|
|
2036
2129
|
| `LOOP_LEAD_IN_IDLE` | `build --lead-in`: the idle cannot be repeated without inventing a value (a track not keyed from 0 to the duration, or ending where it does not start; a field besides `duration`, `loop`, `note`, `tracks`), the motion already has an `idle_lead_in`, or the render did not write whole periods | the refusal names the track or field; the demo's idle, as the rig stage writes it, repeats as written |
|
package/docs/STABILITY.md
CHANGED
|
@@ -175,7 +175,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
175
175
|
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
176
176
|
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `turn` with `--full`/`--head`, and `notes` |
|
|
177
177
|
| `basis.json` | `propose` | spec `spine-parts-basis/1` |
|
|
178
|
-
| `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate`, `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note` |
|
|
178
|
+
| `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate`, `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto` or `contour` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
|
|
179
179
|
| the scene report | `compose` | spec `spine-parts-scene-report/1` |
|
|
180
180
|
| `idle.png`, `idle-indexed.png`, `idle.gif` | `loop`, `build --loop` | APNG and GIF encodings of the rendered idle |
|
|
181
181
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -82,10 +82,12 @@ stage writes are the whole interface.
|
|
|
82
82
|
keeps under the piece's edge; 1 px closed the seam on scarf) — and give the piece its own
|
|
83
83
|
`meshes` entry; the `cut:` line gives the split. At overlap 0 a cut through
|
|
84
84
|
opaque art leaves a seam `check` reads (AUTHORING §5).
|
|
85
|
-
A
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
A figure whose joints are not where the screen-y rules put them — reclining,
|
|
86
|
+
an arm raised, seated in a skirt that does not start at the hip: add
|
|
87
|
+
`--keypoints <file>` (body-18 joints in painting px, each observed, occluded
|
|
88
|
+
or missing; AUTHORING §3, *A posed figure*) so the neck, hip, chest and
|
|
89
|
+
sleeves follow the pose and LINT reads the joints, not screen y. A clean
|
|
90
|
+
LINT does not show a seated hip is right; the overlay does. Never write a joint you did not get from an
|
|
89
91
|
estimator or the painting: say `missing` and let the rule stand in.
|
|
90
92
|
6. Correct against `render/landmarks.png` and `note:` lines; a
|
|
91
93
|
`coverage … not checked` line is a bone no check read (the line says why),
|
|
@@ -109,7 +111,8 @@ stage writes are the whole interface.
|
|
|
109
111
|
8. Read `out/check/check.json` — the six judgement lines the same way as the seam
|
|
110
112
|
and the loop: a FAIL names the field to change (AUTHORING §6);
|
|
111
113
|
`TEXTURE_STRETCH` names the worst mesh triangle (slot, triangle, vertices, edge,
|
|
112
|
-
frame), so quote it even on a PASS
|
|
114
|
+
frame), so quote it even on a PASS; its FAIL line also names the bones the
|
|
115
|
+
edge's two vertices are weighted to and the idle chains they hang on. Quote
|
|
113
116
|
`RECOMPOSITE_HOLES` (uncovered px, hole count, the largest hole's box) in the
|
|
114
117
|
report whatever `PASS` says. The artifact is the three paths the build prints
|
|
115
118
|
last: `check/build/skeleton.json`, `skeleton.atlas` and the packed page. Report
|
package/src/build.ts
CHANGED
|
@@ -227,7 +227,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
227
227
|
log(
|
|
228
228
|
` ${p.name.padEnd(11)} ${p.from.padEnd(16)} ${`${p.w}x${p.h}`.padEnd(9)} @${String(p.x).padStart(4)},${String(p.y).padStart(4)} ` +
|
|
229
229
|
`op=${String(p.opaque_px).padStart(6)} vis=${String(p.visible_px).padStart(6)} src=${String(p.source_px_taken).padStart(6)}/${String(p.projected_core_px).padStart(6)} ` +
|
|
230
|
-
`unproj=${String(p.visible_not_projected_px).padStart(5)} drift=${String(p.refused_drift_px).padStart(5)} merged=${p.merged_px} seam=${p.seam_override_px}`,
|
|
230
|
+
`unproj=${String(p.visible_not_projected_px).padStart(5)} drift=${String(p.refused_drift_px).padStart(5)} ${driftShare(p.refused_drift_px, p.projected_core_px)} merged=${p.merged_px} seam=${p.seam_override_px}`,
|
|
231
231
|
);
|
|
232
232
|
}
|
|
233
233
|
// Issue #256: one line per part from an author-made layer, so it never reads as See-through's. None without one.
|
|
@@ -253,6 +253,16 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
253
253
|
return result;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/**
|
|
257
|
+
* The scale the per-part line prints `drift=` with (issue #314): `refused_drift_px` counts the part's projection
|
|
258
|
+
* candidates (`projected_core_px`, the `src=` column's second figure) refused because See-through's colour sits more
|
|
259
|
+
* than the drift limit off the painting's, so it is read against them — `(<p>% of core)`, one decimal; a part with no
|
|
260
|
+
* candidate has nothing to refuse and says so. No bar reads either figure (AUTHORING §5, `assemble`).
|
|
261
|
+
*/
|
|
262
|
+
export function driftShare(refused: number, core: number): string {
|
|
263
|
+
return core === 0 ? '(no core)' : `(${((100 * refused) / core).toFixed(1)}% of core)`;
|
|
264
|
+
}
|
|
265
|
+
|
|
256
266
|
// ---------------------------------------------------------------------------
|
|
257
267
|
// rig
|
|
258
268
|
// ---------------------------------------------------------------------------
|
package/src/check.ts
CHANGED
|
@@ -1948,7 +1948,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
1948
1948
|
// One reader for the one geometry file: the stretch reads its meshes, the still regions every slot and the head bone.
|
|
1949
1949
|
const geometryPath = join(idle.dir, GEOMETRY_FILE);
|
|
1950
1950
|
const geometry = readGeometry(geometryPath, idle.written);
|
|
1951
|
-
stretch = stretchLine(geometry, geometryPath, problems);
|
|
1951
|
+
stretch = stretchLine(geometry, geometryPath, problems, inp);
|
|
1952
1952
|
if (full !== null) still = stillLine(full, idle, geometry, geometryPath, problems, notPlanted);
|
|
1953
1953
|
}
|
|
1954
1954
|
|
|
@@ -3300,15 +3300,76 @@ function stretchAtText(s: { triangle: number; vertices: readonly number[]; edge:
|
|
|
3300
3300
|
return `triangle ${s.triangle} (vertices ${s.vertices.join(' ')}), edge ${s.edge.join('-')}, idle frame ${s.frame}`;
|
|
3301
3301
|
}
|
|
3302
3302
|
|
|
3303
|
+
/**
|
|
3304
|
+
* Issue #314: what a failing edge's two vertices hang on, read off rig.json and motion.json — so the cause reads off
|
|
3305
|
+
* the line rather than off a hand decoding of the weight runs. Each vertex's bones from the default skin's weights,
|
|
3306
|
+
* the largest two by weight (equal weights in rig.json's order), with how many it has; then every bone named, grouped
|
|
3307
|
+
* by the idle chain it hangs on — the nearest link at or above it of a chain `CHAIN_LAG` reads
|
|
3308
|
+
* ({@link chainFigures}: two or more rotate-keyed links at one period), each chain named once, by its first link — and
|
|
3309
|
+
* the bones on none. A mesh with no weights rides its slot's bone, and an attachment the skin does not hold, or a
|
|
3310
|
+
* vertex its weights do not reach, is said rather than guessed.
|
|
3311
|
+
*/
|
|
3312
|
+
export function edgeBonesText(rig: Record<string, unknown>, motion: Record<string, unknown>, slot: string, attachment: string, edge: readonly number[]): string {
|
|
3313
|
+
const skins = isRecord(rig.skins) ? rig.skins : {};
|
|
3314
|
+
const skin = isRecord(skins.default) ? skins.default : {};
|
|
3315
|
+
const onSlot = isRecord(skin[slot]) ? skin[slot] : {};
|
|
3316
|
+
const att = onSlot[attachment];
|
|
3317
|
+
if (!isRecord(att)) return `its weights are not read: rig.json's default skin holds no attachment "${attachment}" on slot "${slot}"`;
|
|
3318
|
+
if (!Array.isArray(att.weights)) {
|
|
3319
|
+
const sl = (Array.isArray(rig.slots) ? rig.slots : []).filter(isRecord).find((x) => x.name === slot);
|
|
3320
|
+
return `the mesh carries no weights, so vertex ${edge.join(' and vertex ')} ride slot "${slot}"'s bone ${sl !== undefined && typeof sl.bone === 'string' ? `"${sl.bone}"` : '(rig.json names none)'}`;
|
|
3321
|
+
}
|
|
3322
|
+
const weights = att.weights;
|
|
3323
|
+
const missing = edge.filter((v) => !Array.isArray(weights[v]));
|
|
3324
|
+
if (missing.length > 0) return `its weights are not read: rig.json's mesh "${attachment}" weights ${weights.length} vertices, and vertex ${missing.join(' and ')} is not among them`;
|
|
3325
|
+
const named: string[] = [];
|
|
3326
|
+
const parts = edge.map((v, i) => {
|
|
3327
|
+
const list = (weights[v] as unknown[]).filter(isRecord).filter((w) => typeof w.bone === 'string' && typeof w.weight === 'number') as Array<{ bone: string; weight: number }>;
|
|
3328
|
+
const top = [...list].sort((a, b) => b.weight - a.weight).slice(0, 2);
|
|
3329
|
+
for (const w of top) if (!named.includes(w.bone)) named.push(w.bone);
|
|
3330
|
+
const shown = top.map((w) => `"${w.bone}" ${w.weight}`).join(', ');
|
|
3331
|
+
return `${i === 0 ? `vertex ${v} is weighted to` : `vertex ${v} to`} ${shown}${list.length > 2 ? ` (the largest 2 of ${list.length})` : ''}`;
|
|
3332
|
+
});
|
|
3333
|
+
const bones = (Array.isArray(rig.bones) ? rig.bones : []).filter(isRecord);
|
|
3334
|
+
const parent = new Map(bones.filter((b) => typeof b.name === 'string').map((b) => [String(b.name), typeof b.parent === 'string' ? b.parent : null]));
|
|
3335
|
+
const firstLink = new Map<string, string>();
|
|
3336
|
+
for (const c of chainFigures(rig, motion).chains) if (c.length >= 2) for (const l of c) firstLink.set(l.bone, c[0].bone);
|
|
3337
|
+
const carries = new Map<string, string[]>();
|
|
3338
|
+
const none: string[] = [];
|
|
3339
|
+
for (const b of named) {
|
|
3340
|
+
let chain: string | null = null;
|
|
3341
|
+
const seen = new Set<string>();
|
|
3342
|
+
for (let x: string | null = b; x !== null && !seen.has(x); x = parent.get(x) ?? null) {
|
|
3343
|
+
const f = firstLink.get(x);
|
|
3344
|
+
if (f !== undefined) {
|
|
3345
|
+
chain = f;
|
|
3346
|
+
break;
|
|
3347
|
+
}
|
|
3348
|
+
seen.add(x);
|
|
3349
|
+
}
|
|
3350
|
+
if (chain === null) none.push(b);
|
|
3351
|
+
else carries.set(chain, [...(carries.get(chain) ?? []), b]);
|
|
3352
|
+
}
|
|
3353
|
+
const quoted = (l: readonly string[]): string => l.map((n) => `"${n}"`).join(', ');
|
|
3354
|
+
const chains =
|
|
3355
|
+
carries.size === 0
|
|
3356
|
+
? 'none of these bones hangs on a chain of the idle'
|
|
3357
|
+
: `on the idle's chains, ${[...carries].map(([c, l]) => `the chain from "${c}" carries ${quoted(l)}`).join(' and ')}${none.length === 0 ? '' : `, and ${quoted(none)} ${none.length === 1 ? 'hangs' : 'hang'} on none`}`;
|
|
3358
|
+
return `${parts.join(' and ')}; ${chains}`;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3303
3361
|
/**
|
|
3304
3362
|
* (6) No visible texture stretch: for every mesh triangle, each edge's length
|
|
3305
3363
|
* in each idle frame over its rest length (the setup pose's bones, no deform —
|
|
3306
3364
|
* which on both examples is the art's own proportions, the attachment's `uvs`
|
|
3307
3365
|
* times its size, to 2.4e-5). The figure is the rig's worst max(ratio,
|
|
3308
3366
|
* 1/ratio); each mesh's largest and smallest ratio, and where, are the detail.
|
|
3309
|
-
* SKIP when the render wrote no geometry file or the rig draws no mesh.
|
|
3367
|
+
* SKIP when the render wrote no geometry file or the rig draws no mesh. With
|
|
3368
|
+
* `bound`, the rig and motion `check` read, a failing edge's line also names the
|
|
3369
|
+
* bones its two vertices hang on and their chains ({@link edgeBonesText}, issue
|
|
3370
|
+
* #314).
|
|
3310
3371
|
*/
|
|
3311
|
-
export function stretchLine(geo: IdleGeometry | null, path: string, problems: Problem[]): JudgementLine {
|
|
3372
|
+
export function stretchLine(geo: IdleGeometry | null, path: string, problems: Problem[], bound?: Pick<CheckInputs, 'rig' | 'motion'>): JudgementLine {
|
|
3312
3373
|
if (geo === null) return skip(`${path} does not exist; \`rigc render --geometry\` (rig-c ${RIGC_GEOMETRY_VERSION} or later) writes it, and the skinned mesh vertices are read from it`);
|
|
3313
3374
|
if (geo.meshes.length === 0) return skip('the rig draws no mesh attachment (every slot is a region), so there is no triangle to stretch');
|
|
3314
3375
|
const figures = stretchFigures(geo.meshes, geo.frames);
|
|
@@ -3343,7 +3404,7 @@ export function stretchLine(geo: IdleGeometry | null, path: string, problems: Pr
|
|
|
3343
3404
|
problems.push({
|
|
3344
3405
|
code: 'CHECK_TEXTURE_STRETCH',
|
|
3345
3406
|
object: `mesh "${m.slot}" ${stretchAtText(at)}`,
|
|
3346
|
-
detail: `the edge is ${round3(at.ratio)} times its rest length (max(ratio, 1/ratio) ${severity}); <= ${TEXTURE_STRETCH_CEILING} is required — the texture on it is ${at.ratio >= 1 ? 'stretched' : 'squeezed'}: the bones this mesh is weighted to move apart (amplitudes too large down a chain), or a vertex blends bones that move against each other`,
|
|
3407
|
+
detail: `the edge is ${round3(at.ratio)} times its rest length (max(ratio, 1/ratio) ${severity}); <= ${TEXTURE_STRETCH_CEILING} is required${bound === undefined ? '' : `; ${edgeBonesText(bound.rig, bound.motion, m.slot, m.attachment, at.edge)}`} — the texture on it is ${at.ratio >= 1 ? 'stretched' : 'squeezed'}: the bones this mesh is weighted to move apart (amplitudes too large down a chain), or a vertex blends bones that move against each other`,
|
|
3347
3408
|
});
|
|
3348
3409
|
}
|
|
3349
3410
|
}
|
package/src/meshdraft.ts
CHANGED
|
@@ -118,6 +118,33 @@ export const MORE_TRIANGLES_FLAG = '--more-triangles';
|
|
|
118
118
|
*/
|
|
119
119
|
export const RELATIVE_SENTENCE = 'the gate is a judgment relative to this config\'s lattice, not an absolute quality: when the lattice coarsens, the reference moves with it';
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* What `mesh-draft` says of a part whose config entry is not a lattice (`contour` or `auto`, issue #307): it drafts
|
|
123
|
+
* lattice entries only, so such a part was neither drafted nor compared. Said because the answer to "which mode comes
|
|
124
|
+
* out smaller" is not the one the names suggest: on one figure (rig-c/trailer 45becfb) an `auto` reduction held to a
|
|
125
|
+
* large declared bend kept two to three times the triangles of the outline this command drafts (AUTHORING §3, *Which
|
|
126
|
+
* mode comes out smaller*).
|
|
127
|
+
*/
|
|
128
|
+
export const NOT_COMPARED_SENTENCE = 'mesh-draft drafts lattice entries only, so it drafted nothing for this part and compared it with nothing; whether a lattice, or the outline mesh-draft drafts over one, comes out smaller is not known from this run — build the part once on a lattice, run mesh-draft, and compare the rig stage\'s mesh lines before keeping this entry';
|
|
129
|
+
|
|
130
|
+
/** A config entry `mesh-draft` did not draft because it is not a lattice (issue #307), in config order. */
|
|
131
|
+
export interface NotCompared {
|
|
132
|
+
part: string;
|
|
133
|
+
mode: 'contour' | 'auto';
|
|
134
|
+
note: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The entries of `meshes` that are not lattices, in config order, each with its note ({@link NOT_COMPARED_SENTENCE}). */
|
|
138
|
+
export function notCompared(meshes: CharacterConfig['meshes']): NotCompared[] {
|
|
139
|
+
const out: NotCompared[] = [];
|
|
140
|
+
for (const [part, spec] of Object.entries(meshes)) {
|
|
141
|
+
if ('grid' in spec) continue;
|
|
142
|
+
const mode = 'auto' in spec ? 'auto' : 'contour';
|
|
143
|
+
out.push({ part, mode, note: `its entry is "${mode}", not a lattice: ${NOT_COMPARED_SENTENCE}` });
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
/** The five readings, in the order the gate reads them. */
|
|
122
149
|
export const READINGS = ['bars', 'idle', 'bend inverted', 'bend worst', 'triangles'] as const;
|
|
123
150
|
export type Reading = (typeof READINGS)[number];
|
|
@@ -534,6 +561,8 @@ export interface PartRow {
|
|
|
534
561
|
export interface MeshDraftResult {
|
|
535
562
|
rules: FigureRules;
|
|
536
563
|
rows: PartRow[];
|
|
564
|
+
/** The entries not drafted because they are not lattices (issue #307), in config order. */
|
|
565
|
+
notCompared: NotCompared[];
|
|
537
566
|
written: string[];
|
|
538
567
|
}
|
|
539
568
|
|
|
@@ -810,6 +839,7 @@ export function meshDraftStage(input: MeshDraftInput, rigc: RigcRunner, checkBin
|
|
|
810
839
|
}
|
|
811
840
|
const configText = splicedConfig(text, drafted);
|
|
812
841
|
const rowsOut = [...rows.values()];
|
|
842
|
+
const others = notCompared(cfg.meshes);
|
|
813
843
|
const report = {
|
|
814
844
|
spec: MESH_DRAFT_SPEC,
|
|
815
845
|
rules: {
|
|
@@ -834,6 +864,9 @@ export function meshDraftStage(input: MeshDraftInput, rigc: RigcRunner, checkBin
|
|
|
834
864
|
gate: r.gate,
|
|
835
865
|
note: r.note,
|
|
836
866
|
})),
|
|
867
|
+
// Issue #307: present only when the config holds an entry that is not a lattice, so a config of lattices alone
|
|
868
|
+
// writes the bytes it wrote before the key existed.
|
|
869
|
+
...(others.length === 0 ? {} : { not_compared: others }),
|
|
837
870
|
};
|
|
838
871
|
mkdirSync(input.out, { recursive: true });
|
|
839
872
|
const written = [join(input.out, 'config.json'), join(input.out, MESH_DRAFT_REPORT)];
|
|
@@ -845,14 +878,15 @@ export function meshDraftStage(input: MeshDraftInput, rigc: RigcRunner, checkBin
|
|
|
845
878
|
for (const g of r.gate) log(` ${g.pass ? 'pass' : 'FAIL'} ${g.reading}: ${g.text}`);
|
|
846
879
|
if (r.mode === 'grid' && r.gate.length === 0) log(` ${r.note}`);
|
|
847
880
|
}
|
|
881
|
+
for (const o of others) log(` NOT COMPARED ${o.part}: ${o.note}`);
|
|
848
882
|
const nDrafted = rowsOut.filter((r) => r.mode === 'contour').length;
|
|
849
883
|
const tri = (pick: (r: PartRow) => number): number => rowsOut.reduce((a, r) => a + pick(r), 0);
|
|
850
|
-
log(`rig-parts mesh-draft: ${nDrafted} of ${rowsOut.length} lattice part(s) drafted; their triangles ${tri((r) => r.lattice.triangles)} -> ${tri((r) => (r.mode === 'contour' && r.candidate !== null ? r.candidate.triangles : r.lattice.triangles))}, the figure's overdraw over them ${report.figure_overdraw.lattice} -> ${report.figure_overdraw.written}${input.moreTriangles === true ? ` (${MORE_TRIANGLES_FLAG}: triangles reported, not a line)` : ''}; the drafted config's check: ${final.report.figures.PASS ? 'PASS' : 'FAIL'}`);
|
|
884
|
+
log(`rig-parts mesh-draft: ${nDrafted} of ${rowsOut.length} lattice part(s) drafted; their triangles ${tri((r) => r.lattice.triangles)} -> ${tri((r) => (r.mode === 'contour' && r.candidate !== null ? r.candidate.triangles : r.lattice.triangles))}, the figure's overdraw over them ${report.figure_overdraw.lattice} -> ${report.figure_overdraw.written}${input.moreTriangles === true ? ` (${MORE_TRIANGLES_FLAG}: triangles reported, not a line)` : ''}${others.length === 0 ? '' : `; ${others.length} part(s) not a lattice, not drafted and not compared: ${others.map((o) => `${o.part} (${o.mode})`).join(', ')}`}; the drafted config's check: ${final.report.figures.PASS ? 'PASS' : 'FAIL'}`);
|
|
851
885
|
if (resolve(configDir) !== resolve(input.out)) {
|
|
852
886
|
for (const r of relativePaths(raw)) log(` note: ${r.at} is "${r.value}", relative to the config's directory; the written config keeps the text, so it resolves against ${input.out} — copy the file beside the written config, or the written config beside it`);
|
|
853
887
|
}
|
|
854
888
|
log(`rig-parts mesh-draft: wrote ${written.join(' and ')}`);
|
|
855
|
-
return { rules, rows: rowsOut, written };
|
|
889
|
+
return { rules, rows: rowsOut, notCompared: others, written };
|
|
856
890
|
}
|
|
857
891
|
|
|
858
892
|
/**
|
package/src/propose.ts
CHANGED
|
@@ -136,6 +136,8 @@ import { configRoles, type RoleOf } from './structure.ts';
|
|
|
136
136
|
import { AUTHOR_NO_ROLE, isAuthorTag } from './tags.ts';
|
|
137
137
|
import { TURN_BONE, TURN_CARRIER_TAG, TURN_PLACES, TURN_RIDER_OFFSET, TURN_RIDER_TAGS, turnFractions, turnNearest, turnPivot, turnRiderDepth, type TurnSpec } from './turn.ts';
|
|
138
138
|
import { edgeLength } from './length.ts';
|
|
139
|
+
import { chainSqueezes, type SqueezeFinding } from './squeeze.ts';
|
|
140
|
+
import { TEXTURE_STRETCH_CEILING } from './check.ts';
|
|
139
141
|
|
|
140
142
|
// ---------------------------------------------------------------------------
|
|
141
143
|
// the output shape — config-shaped, key order as the reference writes it
|
|
@@ -2905,7 +2907,10 @@ export interface ChainNotAdvancingFinding {
|
|
|
2905
2907
|
toJoint: KeypointName;
|
|
2906
2908
|
}
|
|
2907
2909
|
|
|
2908
|
-
|
|
2910
|
+
/** Issue #314: two chains on one lattice whose own swing closes or opens an edge between them past check's TEXTURE_STRETCH bar (`src/squeeze.ts`). */
|
|
2911
|
+
export type SqueezeLintFinding = { kind: 'squeeze' } & SqueezeFinding;
|
|
2912
|
+
|
|
2913
|
+
export type LintFinding = OffArtFinding | OffLockFinding | HipAboveChestFinding | HipTooHighFinding | ChestNotBetweenFinding | HipNotPastChestFinding | HipNearerNeckFinding | ChainNotAdvancingFinding | SqueezeLintFinding;
|
|
2909
2914
|
|
|
2910
2915
|
/** Which rule set read the torso and the sleeve chains, under `--keypoints`. */
|
|
2911
2916
|
export interface LintBasis {
|
|
@@ -2923,6 +2928,8 @@ export interface LintResult {
|
|
|
2923
2928
|
lockUnread: string[];
|
|
2924
2929
|
/** Single bones the torso checks read (`hip`, `chest`) that the bones do not hold: those checks did not run. */
|
|
2925
2930
|
missingTorsoBones: string[];
|
|
2931
|
+
/** Issue #314: the pairs of keyed chains on one mesh the squeeze check could not build a lattice for, and why. Empty when it read every pair or had none. */
|
|
2932
|
+
squeezeUnread: string[];
|
|
2926
2933
|
/** Present only under `--keypoints`: the rule set that read the torso and the chains. */
|
|
2927
2934
|
basis?: LintBasis;
|
|
2928
2935
|
/** One record per bone of the spec, in its bone order (chain links as `<chain><i>`): which checks read it, or why none did (issue #86). */
|
|
@@ -3130,8 +3137,21 @@ export function lint(
|
|
|
3130
3137
|
}
|
|
3131
3138
|
}
|
|
3132
3139
|
}
|
|
3140
|
+
// Issue #314: every pair of keyed chains a lattice mesh names, swung on the rig stage's own lattice and weights, held
|
|
3141
|
+
// to check's TEXTURE_STRETCH bar (src/squeeze.ts). Only with a motion: without one nothing swings.
|
|
3142
|
+
const sq = chainSqueezes({ bones: spec.bones, meshes: spec.meshes, motion: spec.motion }, new Map(P.recs.filter((p) => P.images.has(p.name)).map((p) => [p.name, { x: p.x, y: p.y, image: P.images.get(p.name) as Raster }])));
|
|
3143
|
+
for (const { mesh, a, b } of sq.read) {
|
|
3144
|
+
for (const [c, other] of [[a, b], [b, a]]) {
|
|
3145
|
+
const e = spec.bones.find((x) => 'chain' in x && x.chain === c);
|
|
3146
|
+
if (e !== undefined && 'chain' in e) e.points.forEach((_, i) => readBy(`${c}${i}`, `squeeze with ${other} on mesh ${pyRepr(mesh)}`));
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
for (const f of sq.findings) findings.push({ kind: 'squeeze', ...f });
|
|
3150
|
+
const squeezeUnread = sq.unread;
|
|
3133
3151
|
const coverage = lintCoverage(spec, byName, read, findings, unknownMeshes, basis);
|
|
3134
|
-
return basis === undefined
|
|
3152
|
+
return basis === undefined
|
|
3153
|
+
? { findings, unknownMeshes, lockUnread, missingTorsoBones, squeezeUnread, coverage }
|
|
3154
|
+
: { findings, unknownMeshes, lockUnread, missingTorsoBones, squeezeUnread, basis, coverage };
|
|
3135
3155
|
}
|
|
3136
3156
|
|
|
3137
3157
|
/** The bones a finding names: the off-art link; hip and chest for a relation between them; a chain's last link for its tip. */
|
|
@@ -3148,6 +3168,8 @@ function findingBones(f: LintFinding, bones: readonly BoneEntry[]): string[] {
|
|
|
3148
3168
|
return ['hip'];
|
|
3149
3169
|
case 'chest-not-between':
|
|
3150
3170
|
return ['chest'];
|
|
3171
|
+
case 'squeeze':
|
|
3172
|
+
return [...new Set(f.vertices.map((v) => v.weights[0]?.bone).filter((n): n is string => n !== undefined))];
|
|
3151
3173
|
case 'chain-not-advancing': {
|
|
3152
3174
|
if (!f.point.endsWith(' tip')) return [f.point];
|
|
3153
3175
|
const chain = f.point.slice(0, -' tip'.length);
|
|
@@ -3313,11 +3335,42 @@ export function lintLine(f: LintFinding): string {
|
|
|
3313
3335
|
return `LINT hip at ${pyIntList(f.hip)} is not past chest at ${pyIntList(f.chest)} on the way from the neck joint to the hip joints (hip at ${pyFixed(f.tHip, 2)}, chest at ${pyFixed(f.tChest, 2)} of the way): breathing and the skirt hang from the wrong end of the torso`;
|
|
3314
3336
|
case 'hip-nearer-neck':
|
|
3315
3337
|
return `LINT hip at ${pyIntList(f.hip)} is nearer the neck joint ${fmtAt(f.neck)} (${pyFixed(f.dNeck, 1)} px) than the midpoint of the hip joints ${fmtAt(f.hips)} (${pyFixed(f.dHips, 1)} px): a hip at the shoulders`;
|
|
3338
|
+
case 'squeeze':
|
|
3339
|
+
return squeezeLine(f);
|
|
3316
3340
|
case 'chain-not-advancing':
|
|
3317
3341
|
return `LINT ${f.point} at ${pyIntList(f.at)} does not advance from ${f.fromJoint} toward ${f.toJoint}: it falls ${pyFixed(f.t, 1)} px along that line and ${f.previous} ${pyFixed(f.tPrevious, 1)} px, and each must fall further than the one before`;
|
|
3318
3342
|
}
|
|
3319
3343
|
}
|
|
3320
3344
|
|
|
3345
|
+
/**
|
|
3346
|
+
* The squeeze LINT line (issue #314): the pair and the mesh, the worst edge with both swinging and the bone each of
|
|
3347
|
+
* its two vertices rides most, the bar it is held to (check's), each chain swung alone, the tips' distance and the
|
|
3348
|
+
* amplitudes, and the remedies whose figure passes the bar — one sign when the amplitudes are opposed, dropping a
|
|
3349
|
+
* chain when the other alone passes — or, when none does, that the amplitudes are too large for this lattice.
|
|
3350
|
+
*/
|
|
3351
|
+
function squeezeLine(f: SqueezeLintFinding): string {
|
|
3352
|
+
const e = f.both.at;
|
|
3353
|
+
const C = TEXTURE_STRETCH_CEILING;
|
|
3354
|
+
const rides = f.vertices.map((v) => (v.weights.length === 0 ? `vertex ${v.vertex} rides no bone` : `vertex ${v.vertex} rides ${v.weights[0].bone} ${v.weights[0].weight}`)).join(', ');
|
|
3355
|
+
const amps = (c: { amps: readonly number[] }): string => `[${c.amps.join(', ')}]`;
|
|
3356
|
+
const remedies: string[] = [];
|
|
3357
|
+
if (f.turned !== null && f.turned <= C) remedies.push(`give both the same sign (then ${pyFixed(f.turned, 3)})`);
|
|
3358
|
+
if (f.aloneA <= C && f.aloneB <= C) remedies.push('drop one chain');
|
|
3359
|
+
else if (f.aloneA <= C) remedies.push(`drop ${f.b.chain}, which alone is past the bar too`);
|
|
3360
|
+
else if (f.aloneB <= C) remedies.push(`drop ${f.a.chain}, which alone is past the bar too`);
|
|
3361
|
+
const said =
|
|
3362
|
+
f.turned !== null
|
|
3363
|
+
? `their amplitudes are opposed (${f.a.chain} ${amps(f.a)}, ${f.b.chain} ${amps(f.b)}), so their tips swing towards each other and apart`
|
|
3364
|
+
: `their amplitudes are ${f.a.chain} ${amps(f.a)} and ${f.b.chain} ${amps(f.b)}`;
|
|
3365
|
+
const remedy = remedies.length > 0 ? remedies.join(', or ') : 'neither one sign nor either chain alone passes the bar: lower the amplitudes';
|
|
3366
|
+
return (
|
|
3367
|
+
`LINT ${f.a.chain} and ${f.b.chain} on mesh ${pyRepr(f.mesh)} ${e.ratio >= 1 ? 'stretch' : 'squeeze'} it: swung together, every other bone held, ` +
|
|
3368
|
+
`edge ${e.edge.join('-')} of triangle ${e.triangle} ${e.ratio >= 1 ? 'opens' : 'closes'} to ${pyFixed(e.ratio, 3)} times its rest length at idle frame ${e.frame} (max(ratio, 1/ratio) ${pyFixed(f.both.severity, 3)}; ` +
|
|
3369
|
+
`<= ${C} is required, check's TEXTURE_STRETCH bar) — ${rides}; alone, ${f.a.chain} reads ${pyFixed(f.aloneA, 3)} and ${f.b.chain} ${pyFixed(f.aloneB, 3)}. ` +
|
|
3370
|
+
`Their tips are ${pyFixed(f.tipGap, 1)} px apart and ${said}: ${remedy}`
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3321
3374
|
export interface Comparison {
|
|
3322
3375
|
rows: Array<[string, number]>;
|
|
3323
3376
|
onlyConfig: string[];
|
package/src/squeeze.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two chains on one lattice that the idle swings into each other (issue #314).
|
|
3
|
+
*
|
|
4
|
+
* A pair of chains a mesh's `segments` both name is a squeeze when their own
|
|
5
|
+
* swing, every other bone held at the setup pose, closes (or opens) a lattice
|
|
6
|
+
* edge between the art each one carries past the bar `check`'s
|
|
7
|
+
* `TEXTURE_STRETCH` holds every edge to ({@link TEXTURE_STRETCH_CEILING}, on
|
|
8
|
+
* max(ratio, 1/ratio)). Read here from what `propose` and `--from-config` hold —
|
|
9
|
+
* the parts, the bones, the mesh's spec and the idle's chain tracks — so the
|
|
10
|
+
* squeeze is named before anything is built.
|
|
11
|
+
*
|
|
12
|
+
* What is built is the rig stage's own lattice and weights, by the rig stage's
|
|
13
|
+
* own functions: the part's image padded by {@link PAD}, its art (alpha above
|
|
14
|
+
* {@link ART_ALPHA}, less what a declared `stray` leaves out) cut into the
|
|
15
|
+
* lattice by `latticeMesh`, and each vertex weighted by `influences` under the
|
|
16
|
+
* mesh's `r`, `exponent` and `influences`, rounded as the lattice rounds — so
|
|
17
|
+
* the vertex and triangle numbers are the ones `rig.json` and `check` use. Only
|
|
18
|
+
* the lattice mode under the distance rule is read; a contour or automatic mesh,
|
|
19
|
+
* or `rule: "heat"`, is said not read, by name.
|
|
20
|
+
*
|
|
21
|
+
* Each chain's links are posed as the idle's sine states them —
|
|
22
|
+
* `amps[i] * sin(2 pi (t / period - (phase + lag * i)))` degrees on link `i`,
|
|
23
|
+
* `src/motion.ts` — turned down the chain (a link carries every link below it),
|
|
24
|
+
* at the frames `check` renders: `t = f / IDLE_FPS` for every whole frame up to
|
|
25
|
+
* the idle's duration. Every other bone stays where the setup pose has it, so a
|
|
26
|
+
* figure is the pair's own: the head's roll or the breath carry both chains
|
|
27
|
+
* alike and are not in it. A vertex moves as linear blend skinning moves it,
|
|
28
|
+
* each weight times where that bone's motion takes the vertex. The edge ratios
|
|
29
|
+
* are `check`'s own reader on these poses ({@link stretchFigures}).
|
|
30
|
+
*
|
|
31
|
+
* The pair is named when both swinging together pass the bar, with each
|
|
32
|
+
* chain swung alone beside it — so the remedy "drop one chain" carries the
|
|
33
|
+
* figure the other is left with, and a chain past the bar on its own reads as
|
|
34
|
+
* that. With the two chains' amplitudes opposed link for link (every `amps[i]`
|
|
35
|
+
* of one the other sign of the other's), the same reading is taken with the
|
|
36
|
+
* second chain's signs turned, so the remedy "give both the same sign" carries
|
|
37
|
+
* its own figure too. A remedy whose figure is past the bar is not offered.
|
|
38
|
+
*/
|
|
39
|
+
import { TEXTURE_STRETCH_CEILING } from './check.ts';
|
|
40
|
+
import type { BoneEntry, MeshSpec, Motion, Point } from './config.ts';
|
|
41
|
+
import { stretchFigures, stretchSeverity, type StretchAt } from './instruments.ts';
|
|
42
|
+
import { edgeLength } from './length.ts';
|
|
43
|
+
import { ART_ALPHA, latticeMesh, latticeStray } from './mesh.ts';
|
|
44
|
+
import { IDLE_FPS, TIME_SLACK } from './motion.ts';
|
|
45
|
+
import { pad } from './raster/composite.ts';
|
|
46
|
+
import { alphaAbove, type Raster } from './raster/types.ts';
|
|
47
|
+
import { latticeShares, PAD, roundShares } from './rig.ts';
|
|
48
|
+
import { DISTANCE_EXPONENT, influences, meshLimits, type Segment } from './weights.ts';
|
|
49
|
+
|
|
50
|
+
/** One chain of the pair as the finding prints it: its name, its `amps` as written, and its tip. */
|
|
51
|
+
export interface SqueezeChain {
|
|
52
|
+
chain: string;
|
|
53
|
+
amps: number[];
|
|
54
|
+
tip: Point;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The worst edge of one reading: where, its ratio, and max(ratio, 1/ratio). */
|
|
58
|
+
export interface SqueezeEdge {
|
|
59
|
+
at: StretchAt;
|
|
60
|
+
severity: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A pair named: both chains, the mesh, the worst edge with both swinging, and each alone (and turned, when opposed). */
|
|
64
|
+
export interface SqueezeFinding {
|
|
65
|
+
mesh: string;
|
|
66
|
+
a: SqueezeChain;
|
|
67
|
+
b: SqueezeChain;
|
|
68
|
+
/** The two tips' distance at the setup pose, rig px. */
|
|
69
|
+
tipGap: number;
|
|
70
|
+
both: SqueezeEdge;
|
|
71
|
+
/** The two vertices of `both`'s edge, each with its two largest weights (bone, weight as rig.json writes it), and how many it has. */
|
|
72
|
+
vertices: Array<{ vertex: number; weights: Array<{ bone: string; weight: number }>; of: number }>;
|
|
73
|
+
/** The worst edge with the first chain swung alone, and with the second. */
|
|
74
|
+
aloneA: number;
|
|
75
|
+
aloneB: number;
|
|
76
|
+
/** Both opposed link for link: the worst edge with the second chain's signs turned. Null when they are not opposed. */
|
|
77
|
+
turned: number | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SqueezeResult {
|
|
81
|
+
findings: SqueezeFinding[];
|
|
82
|
+
/** Pairs on one mesh this did not read, and why. */
|
|
83
|
+
unread: string[];
|
|
84
|
+
/** Every pair read: the mesh and the two chains. */
|
|
85
|
+
read: Array<{ mesh: string; a: string; b: string }>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type ChainTrackLike = { chain: string; amps: number[]; period: number; phase: number; lag: number };
|
|
89
|
+
|
|
90
|
+
/** Each frame's time, as `check` renders them: every whole frame at {@link IDLE_FPS} up to the idle's duration. */
|
|
91
|
+
function frameTimes(duration: number): number[] {
|
|
92
|
+
const out: number[] = [];
|
|
93
|
+
for (let f = 0; f / IDLE_FPS <= duration + TIME_SLACK; f++) out.push(f / IDLE_FPS);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A chain's posed links at time `t`: per link, its setup origin, its posed origin, and the turn it carries (degrees,
|
|
99
|
+
* the sum of its own and every link above it). Positive is Spine's counter-clockwise (y up), which on the crop's y-down
|
|
100
|
+
* page turns a vector (dx, dy) to (dx cos + dy sin, -dx sin + dy cos).
|
|
101
|
+
*/
|
|
102
|
+
function posedLinks(pts: readonly Point[], track: ChainTrackLike, sign: 1 | -1, t: number): Array<{ rest: Point; at: [number, number]; turn: number }> {
|
|
103
|
+
const out: Array<{ rest: Point; at: [number, number]; turn: number }> = [];
|
|
104
|
+
let at: [number, number] = [pts[0][0], pts[0][1]];
|
|
105
|
+
let turn = 0;
|
|
106
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
107
|
+
const amp = i < track.amps.length ? track.amps[i] : 0;
|
|
108
|
+
turn += sign * amp * Math.sin(2 * Math.PI * (t / track.period - (track.phase + track.lag * i)));
|
|
109
|
+
out.push({ rest: pts[i], at, turn });
|
|
110
|
+
const rad = (turn * Math.PI) / 180;
|
|
111
|
+
const dx = pts[i + 1][0] - pts[i][0];
|
|
112
|
+
const dy = pts[i + 1][1] - pts[i][1];
|
|
113
|
+
at = [at[0] + dx * Math.cos(rad) + dy * Math.sin(rad), at[1] - dx * Math.sin(rad) + dy * Math.cos(rad)];
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Where a bone's motion takes the point `v`: a link turns it about its origin and carries it to the link's posed origin. */
|
|
119
|
+
function moved(link: { rest: Point; at: [number, number]; turn: number }, v: readonly [number, number]): [number, number] {
|
|
120
|
+
const rad = (link.turn * Math.PI) / 180;
|
|
121
|
+
const dx = v[0] - link.rest[0];
|
|
122
|
+
const dy = v[1] - link.rest[1];
|
|
123
|
+
return [link.at[0] + dx * Math.cos(rad) + dy * Math.sin(rad), link.at[1] - dx * Math.sin(rad) + dy * Math.cos(rad)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Every pair of chains a lattice mesh's `segments` name, both keyed by a chain track, read as the module doc says.
|
|
128
|
+
* `images` holds each part's PNG by name, its `box` its parts.json box (rig px).
|
|
129
|
+
*/
|
|
130
|
+
export function chainSqueezes(
|
|
131
|
+
spec: { bones: readonly BoneEntry[]; meshes: Readonly<Record<string, MeshSpec>>; motion?: Motion },
|
|
132
|
+
parts: ReadonlyMap<string, { x: number; y: number; image: Raster }>,
|
|
133
|
+
): SqueezeResult {
|
|
134
|
+
const findings: SqueezeFinding[] = [];
|
|
135
|
+
const unread: string[] = [];
|
|
136
|
+
const read: SqueezeResult['read'] = [];
|
|
137
|
+
const motion = spec.motion;
|
|
138
|
+
if (motion === undefined) return { findings, unread, read };
|
|
139
|
+
const tracks = new Map<string, ChainTrackLike>();
|
|
140
|
+
for (const t of motion.tracks) if ('chain' in t) tracks.set(t.chain, t);
|
|
141
|
+
const chains = new Map<string, { points: Point[]; tip: Point; parent: string }>();
|
|
142
|
+
for (const e of spec.bones) if ('chain' in e) chains.set(e.chain, { points: e.points, tip: e.tip, parent: e.parent });
|
|
143
|
+
// Every bone's parent, chain links as <chain><i>, so a bone under a link is carried by it.
|
|
144
|
+
const parent = new Map<string, string>();
|
|
145
|
+
for (const e of spec.bones) {
|
|
146
|
+
if ('chain' in e) e.points.forEach((_, i) => parent.set(`${e.chain}${i}`, i === 0 ? e.parent : `${e.chain}${i - 1}`));
|
|
147
|
+
else parent.set(e.name, e.parent);
|
|
148
|
+
}
|
|
149
|
+
const linkOf = new Map<string, [string, number]>();
|
|
150
|
+
for (const [c, e] of chains) e.points.forEach((_, i) => linkOf.set(`${c}${i}`, [c, i]));
|
|
151
|
+
// The nearest link at or above a bone, among the chains in `moving`.
|
|
152
|
+
const carrier = (bone: string, moving: ReadonlySet<string>): [string, number] | null => {
|
|
153
|
+
const seen = new Set<string>();
|
|
154
|
+
for (let b: string | undefined = bone; b !== undefined && !seen.has(b); b = parent.get(b)) {
|
|
155
|
+
const l = linkOf.get(b);
|
|
156
|
+
if (l !== undefined && moving.has(l[0])) return l;
|
|
157
|
+
seen.add(b);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
const times = frameTimes(motion.duration);
|
|
162
|
+
|
|
163
|
+
for (const [mname, m] of Object.entries(spec.meshes)) {
|
|
164
|
+
const keyed = m.segments.filter((s): s is string => typeof s === 'string' && chains.has(s) && tracks.has(s));
|
|
165
|
+
if (keyed.length < 2) continue;
|
|
166
|
+
const pairs: Array<[string, string]> = [];
|
|
167
|
+
for (let i = 0; i < keyed.length; i++) for (let j = i + 1; j < keyed.length; j++) pairs.push([keyed[i], keyed[j]]);
|
|
168
|
+
const names = pairs.map(([a, b]) => `${a} and ${b}`).join(', ');
|
|
169
|
+
if ('contour' in m || 'auto' in m) {
|
|
170
|
+
unread.push(`mesh ${JSON.stringify(mname)} (${names}) is a ${'contour' in m ? 'contour' : 'automatic'} mesh; only a lattice is built here`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (m.rule === 'heat') {
|
|
174
|
+
unread.push(`mesh ${JSON.stringify(mname)} (${names}) is weighted by rule "heat"; only the distance rule is read here`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const part = parts.get(mname);
|
|
178
|
+
if (part === undefined) continue;
|
|
179
|
+
// The rig stage's segments: a chain is its links, each from its origin to the next link or the tip.
|
|
180
|
+
const segs: Segment[] = [];
|
|
181
|
+
let unresolved: string | null = null;
|
|
182
|
+
for (const sp of m.segments) {
|
|
183
|
+
if (typeof sp !== 'string') {
|
|
184
|
+
segs.push({ bone: sp[0], a: [sp[1][0], sp[1][1]], b: [sp[2][0], sp[2][1]] });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const c = chains.get(sp);
|
|
188
|
+
if (c !== undefined) {
|
|
189
|
+
const line = [...c.points, c.tip];
|
|
190
|
+
c.points.forEach((q, i) => segs.push({ bone: `${sp}${i}`, a: [q[0], q[1]], b: [line[i + 1][0], line[i + 1][1]] }));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const single = spec.bones.find((e) => 'name' in e && e.name === sp);
|
|
194
|
+
if (single === undefined || !('name' in single) || single.tip === undefined) {
|
|
195
|
+
unresolved = `its segment "${sp}" is not a chain or a single bone with a tip`;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
segs.push({ bone: sp, a: [single.at[0], single.at[1]], b: [single.tip[0], single.tip[1]] });
|
|
199
|
+
}
|
|
200
|
+
if (unresolved !== null) {
|
|
201
|
+
unread.push(`mesh ${JSON.stringify(mname)} (${names}): ${unresolved}, so it has no weights to read`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const img = pad(part.image, PAD, PAD, PAD, PAD, [0, 0, 0, 0]);
|
|
205
|
+
const allArt = alphaAbove(img, ART_ALPHA);
|
|
206
|
+
const art = m.stray === undefined ? allArt : latticeStray(allArt, m.stray).art;
|
|
207
|
+
const lm = latticeMesh(mname, art, m.grid);
|
|
208
|
+
if ('code' in lm) {
|
|
209
|
+
unread.push(`mesh ${JSON.stringify(mname)} (${names}): its lattice is refused (${lm.code}), so there is no triangle to read`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const ox = part.x - PAD;
|
|
213
|
+
const oy = part.y - PAD;
|
|
214
|
+
const limits = meshLimits(m.influences);
|
|
215
|
+
const rest: Array<[number, number]> = lm.vertices.map(([x, y]) => [x + ox, y + oy]);
|
|
216
|
+
const weights = rest.map((p) => {
|
|
217
|
+
const shares = influences(p, segs, m.r, limits, m.exponent ?? DISTANCE_EXPONENT);
|
|
218
|
+
return shares.length === 0 ? [] : limits.floor === 'continuous' ? roundShares(shares) : latticeShares(shares);
|
|
219
|
+
});
|
|
220
|
+
const restFlat = rest.flatMap(([x, y]) => [x, y]);
|
|
221
|
+
const worst = (moving: ReadonlyMap<string, 1 | -1>): SqueezeEdge | null => {
|
|
222
|
+
const set = new Set(moving.keys());
|
|
223
|
+
const frames = times.map((t, index) => {
|
|
224
|
+
const posed = new Map<string, ReturnType<typeof posedLinks>>();
|
|
225
|
+
for (const [c, sign] of moving) {
|
|
226
|
+
const e = chains.get(c) as { points: Point[]; tip: Point };
|
|
227
|
+
posed.set(c, posedLinks([...e.points, e.tip], tracks.get(c) as ChainTrackLike, sign, t));
|
|
228
|
+
}
|
|
229
|
+
const vertices: number[] = [];
|
|
230
|
+
rest.forEach((v, vi) => {
|
|
231
|
+
let x = 0;
|
|
232
|
+
let y = 0;
|
|
233
|
+
for (const { bone, weight } of weights[vi]) {
|
|
234
|
+
const l = carrier(bone, set);
|
|
235
|
+
const to = l === null ? v : moved((posed.get(l[0]) as ReturnType<typeof posedLinks>)[l[1]], v);
|
|
236
|
+
x += weight * to[0];
|
|
237
|
+
y += weight * to[1];
|
|
238
|
+
}
|
|
239
|
+
vertices.push(x, y);
|
|
240
|
+
});
|
|
241
|
+
return { index, attachments: [{ slot: mname, attachment: mname, vertices }] };
|
|
242
|
+
});
|
|
243
|
+
const [fig] = stretchFigures([{ slot: mname, attachment: mname, vertices: restFlat, triangles: lm.triangles }], frames);
|
|
244
|
+
if (fig.max === null || fig.min === null) return null;
|
|
245
|
+
const sMax = stretchSeverity(fig.max.ratio);
|
|
246
|
+
const sMin = stretchSeverity(fig.min.ratio);
|
|
247
|
+
return sMin > sMax ? { at: fig.min, severity: sMin } : { at: fig.max, severity: sMax };
|
|
248
|
+
};
|
|
249
|
+
for (const [a, b] of pairs) {
|
|
250
|
+
read.push({ mesh: mname, a, b });
|
|
251
|
+
const both = worst(new Map([[a, 1], [b, 1]]));
|
|
252
|
+
if (both === null || both.severity <= TEXTURE_STRETCH_CEILING) continue;
|
|
253
|
+
const aloneA = (worst(new Map([[a, 1]])) as SqueezeEdge).severity;
|
|
254
|
+
const aloneB = (worst(new Map([[b, 1]])) as SqueezeEdge).severity;
|
|
255
|
+
const ta = tracks.get(a) as ChainTrackLike;
|
|
256
|
+
const tb = tracks.get(b) as ChainTrackLike;
|
|
257
|
+
const opposed = ta.amps.length === tb.amps.length && ta.amps.every((v, i) => v * tb.amps[i] < 0);
|
|
258
|
+
const ca = chains.get(a) as { tip: Point };
|
|
259
|
+
const cb = chains.get(b) as { tip: Point };
|
|
260
|
+
findings.push({
|
|
261
|
+
mesh: mname,
|
|
262
|
+
a: { chain: a, amps: [...ta.amps], tip: ca.tip },
|
|
263
|
+
b: { chain: b, amps: [...tb.amps], tip: cb.tip },
|
|
264
|
+
tipGap: edgeLength(ca.tip[0] - cb.tip[0], ca.tip[1] - cb.tip[1]),
|
|
265
|
+
both,
|
|
266
|
+
vertices: both.at.edge.map((vertex) => ({ vertex, weights: [...weights[vertex]].sort((p, q) => q.weight - p.weight).slice(0, 2), of: weights[vertex].length })),
|
|
267
|
+
aloneA,
|
|
268
|
+
aloneB,
|
|
269
|
+
turned: opposed ? (worst(new Map<string, 1 | -1>([[a, 1], [b, -1]])) as SqueezeEdge).severity : null,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { findings, unread, read };
|
|
274
|
+
}
|