spine-rigc 2.46.0 → 2.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/AUTHORING.md +54 -17
- package/package.json +1 -1
- package/src/cli/shared.ts +7 -4
- package/src/core/raw.ts +59 -1
- package/src/folds.ts +109 -12
- package/src/inspect.ts +133 -17
- package/src/meshcompare.ts +58 -13
- package/src/render_core.ts +5 -56
package/README.md
CHANGED
|
@@ -414,10 +414,10 @@ and over it any of `bones` (origin to tip, a bone of length 0 marked as one),
|
|
|
414
414
|
(each mesh's edges and outline), `weights:<bone>` (what that bone moves, as a heat),
|
|
415
415
|
`density` (each part's triangles per area against the figure's, on a log ramp),
|
|
416
416
|
`folds` (each mesh triangle turned over against the setup pose, filled — `render --folds`
|
|
417
|
-
names them per animation and frame), `slots` (the draw order at the pose), `grid` (in world units) and `declared`
|
|
417
|
+
names them per animation and frame, with where and how big), `slots` (the draw order at the pose), `grid` (in world units) and `declared`
|
|
418
418
|
(each `invariants.axes` line and the bones held to it), at the setup pose, at
|
|
419
419
|
`--animation <name> --at <s[,s…]>` — one picture, or a sheet — or zoomed with
|
|
420
|
-
`--zoom <bone|slot>[:pad]` or `--viewport x,y,w,h`; `--bone`/`--hide-bone` pick the
|
|
420
|
+
`--zoom <bone|slot|folds>[:pad]` or `--viewport x,y,w,h`; `--bone`/`--hide-bone` pick the
|
|
421
421
|
bones by glob, `--overlay-slot`/`--hide-overlay-slot` the slots whose mesh, hull,
|
|
422
422
|
weights, density, folds and number are drawn over the whole painting, and `--compare <dir>` draws a second build beside the first on one
|
|
423
423
|
transform, to see what a change moved. Beside the PNG it writes a legend:
|
|
@@ -601,7 +601,7 @@ commands take it and what its default is.
|
|
|
601
601
|
| `repack <dir> --out <dir> --keep-pages` | **`skeleton.model.json` alone, for the pages already in place** — for a player that reads the document beside an atlas and pages that already shipped. The same read and stage rules, then `build --atlas-in` over the input atlas in place of the lift and the pack; it writes the document, and nothing else, only after its `pages` are shown to state the atlas as it stands, the rebuilt skeleton byte-identical to the one on disk (so `spine.sha256` names it — no acceptance flag here) and the gate green. `--out` is usually the build's own directory and must not already hold a `skeleton.model.json`; a page or region line the document has no field for is refused by name ([AUTHORING §0.4](docs/AUTHORING.md)) |
|
|
602
602
|
| `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all |
|
|
603
603
|
| `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
|
|
604
|
-
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles |
|
|
604
|
+
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area |
|
|
605
605
|
| `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version — a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` |
|
|
606
606
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
607
607
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
package/docs/AUTHORING.md
CHANGED
|
@@ -243,9 +243,9 @@ What the flags mean:
|
|
|
243
243
|
| `--report` | `build` and `repack`: also write the gate's report as a JSON document (`build-report/1`) to this file — every gate's `PASS`, `SKIP` and `FAIL` rows, its summary figures and stats line, which supplier judged it, and every `pack:` line's figures. Written when the command writes `--out` and when a gate is red, before the exit; a file already at the path is removed first, so a compile error or a refused repack leaves none. Never inside `--out`, refused by name. The printed lines are byte-identical with it and without it — **§5.3** |
|
|
244
244
|
| `--candidate` | `check`, `bench`, `render`, `inspect`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. A directory holding no `skeleton.json`, or a path with nothing at it, is refused `nothing at <path>`, exit 2, nothing written, by every command that takes a build — `validate <dir>` and a directory given to `diff` included (`RC27`). `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. A build moved or copied whole away from the directory it was built in stops at the same place, on either poser, and is refused naming the page and the file that names it: `nothing at <the page>: <the skeleton.model.json> names it as a page, and the core draws this build from it. A page path is relative to the build's directory, so a build moved or copied away from the directory it was built in names pages that are not here — build it again where it is, or build it with --copy-images, which writes its pages beside it` (under `--poser spine`, `<the atlas> names it as a page, and spine-core draws this pair through that atlas (--poser spine)` with the same ending), exit 2, nothing written (`RC21`); only where the core refused the files does a page refusal say the directory is not one build. `bench --bones` and `rigc bonedist` — on its `--candidate` and on its `--reference` — refuse a pair the runtime cannot load in the words above with `spine-core poses this pair (bonedist's candidate: …)` or `(bonedist's reference: …)`, exit 2, nothing written (`RC20`); they read no page image, so a build whose pages are elsewhere is measured as it is in place. A `skeleton.json` that is not JSON is refused `rigc: cannot read <the file>: <the parser's words> (line N, column N)`, exit 2, by `render`, `check`, `bench` and `bonedist` as by every other command that reads one (`RC22`). With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
245
245
|
| `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
|
|
246
|
-
| `--show`, `--dim`, `--at`, `--zoom` | `inspect` only — what it draws, whether the art is faded, which times of `--animation` it poses and the bone or
|
|
246
|
+
| `--show`, `--dim`, `--at`, `--zoom` | `inspect` only — what it draws, whether the art is faded, which times of `--animation` it poses and the bone, slot or folds it frames to; §13 says each, with the refusals |
|
|
247
247
|
| `--geometry` | `render` only: also write a `geometry.json` into each frame directory — per frame, every bone's world transform and every slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry and topology, on the frames' own grid and viewport. Every other file is byte for byte what the render writes without it. Refused with `--slot`/`--hide`, because the geometry is the whole pose whatever is drawn — **§8.2** |
|
|
248
|
-
| `--folds` | `render` only: also read which mesh triangles **fold** — turn over against the setup pose — and write a `folds.json` into each frame directory: per mesh, the frames with a folded triangle, the worst frame's count and which triangles, by index. One printed line per mesh that folds; a triangle degenerate or inverted at the setup pose is said once and never counted. A reading, not a gate. Every other file is byte for byte what the render writes without it, `geometry.json` included. Refused with `--slot`/`--hide`, for `--geometry`'s reason — **§8.3** |
|
|
248
|
+
| `--folds` | `render` only: also read which mesh triangles **fold** — turn over against the setup pose — and write a `folds.json` into each frame directory: per mesh, the frames with a folded triangle, the worst frame's count and which triangles, by index, with their world box and setup-pose area. One printed line per mesh that folds; a triangle degenerate or inverted at the setup pose is said once and never counted. A reading, not a gate. Every other file is byte for byte what the render writes without it, `geometry.json` included. Refused with `--slot`/`--hide`, for `--geometry`'s reason — **§8.3** |
|
|
249
249
|
| `--poser` | `render`, `inspect`, `check` and `explain`: `core` or `spine` — on `render`, `inspect` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`, `RC34`). On the entry that links none of spine-core — the installed `rigc` without the runtime beside it, `bun cli_core.ts` in a clone — that refusal ends on the route to the entry that does (``the entry that links it runs them — installed, the same `rigc` once @esotericsoftware/spine-core is installed beside the package; from a source checkout, `bun cli.ts` ``); a runtime that is installed and fails to load is refused without it, since installing it is not what that reader has to do. On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1), ending on the same route where the entry links none of it |
|
|
250
250
|
| `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
|
|
251
251
|
| `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
|
|
@@ -7297,9 +7297,25 @@ and nothing in a report says so. `--folds` reads it off the frames `render` draw
|
|
|
7297
7297
|
bun cli.ts render --candidate path/to/spine --folds [--animation …] [--skin …] [--fps 12]
|
|
7298
7298
|
# rigc render
|
|
7299
7299
|
# .. show 321 frame(s), 16.000s + contact.png + folds.json (2 of 11 mesh(es) fold) -> …/show@20fps
|
|
7300
|
-
# .. folds "show": mesh "hair_long" (slot "hair_long") folds on 88 frame(s) [33-54, 113-134, 194-215, 273-294] at 20 fps; worst frame 40 (t=2.0000s): 3 of 459 triangle(s) [137, 139, 140]; 3 triangle(s) fold on some frame [137, 139, 140]
|
|
7300
|
+
# .. folds "show": mesh "hair_long" (slot "hair_long") folds on 88 frame(s) [33-54, 113-134, 194-215, 273-294] at 20 fps; worst frame 40 (t=2.0000s): 3 of 459 triangle(s) [137, 139, 140] in world box x [112.04, 133.28] y [323.01, 339.19] (21.22 × 16.16), setup-pose area 122.00 units² = 0.0686% of the mesh's; 3 triangle(s) fold on some frame [137, 139, 140]
|
|
7301
7301
|
```
|
|
7302
7302
|
|
|
7303
|
+
**Where and how big.** The line says, for the worst frame, where the folded
|
|
7304
|
+
triangles are — the **world box** of their vertices as posed at that frame, every
|
|
7305
|
+
vertex of each and nothing else, printed rounded *outward* to hundredths so the
|
|
7306
|
+
printed box still holds every one (the file states it exactly) — and how big they
|
|
7307
|
+
are: their summed **setup-pose area**, each triangle's unsigned area in `rest`, in
|
|
7308
|
+
world units², and that as a share of the mesh's own setup-pose area (every
|
|
7309
|
+
triangle's, summed). "3 of 459 triangles" reads the same for three slivers at a
|
|
7310
|
+
hair tip and three panels of a skirt; the share does not.
|
|
7311
|
+
|
|
7312
|
+
⭐ The box is the frame's and the area is the setup pose's, on purpose. The box is
|
|
7313
|
+
where to look, so it is where the triangles are drawn. The area is how much of the
|
|
7314
|
+
art is turned over, and a folded triangle's *posed* area cannot say that: it passes
|
|
7315
|
+
through 0 at the frame the triangle turns, so one triangle reads a different size on
|
|
7316
|
+
every frame it is folded — on the film above, `hair_long`'s three range from 0.09 to
|
|
7317
|
+
24.2 units² posed across their 88 frames against 122.00 at the setup pose.
|
|
7318
|
+
|
|
7303
7319
|
**What folded means.** A mesh triangle is folded at a frame when its orientation
|
|
7304
7320
|
there — the sign of its signed area in world space — is the opposite of its
|
|
7305
7321
|
orientation at the setup pose. The setup pose is `rest`'s (§8.2): the setup pose's
|
|
@@ -7329,9 +7345,13 @@ on a `setup` line the first time a set shows its mesh, and never counted:
|
|
|
7329
7345
|
`skin` (absent when none was set), `fps`, `reading` (these definitions, stated in
|
|
7330
7346
|
the file), and `meshes[]` — every mesh any frame of the set shows, in `rest`'s
|
|
7331
7347
|
order — each with `slot`, `attachment`, `triangleCount`, `setup` (`degenerate` and
|
|
7332
|
-
`inverted`, triangle indices), `
|
|
7333
|
-
`
|
|
7334
|
-
|
|
7348
|
+
`inverted`, triangle indices), `area` (the mesh's setup-pose area, world units²),
|
|
7349
|
+
`frames[]` (only the frames with a folded triangle: `index`, `time`, `triangles`,
|
|
7350
|
+
and that frame's `box` — `[minX, minY, maxX, maxY]`, world units, y up — `area` and
|
|
7351
|
+
`share`, as above), `worst` (the frame with the most, the earliest on a tie, with the
|
|
7352
|
+
same fields; `null` when it never folds) and `folded` (every triangle folded on some
|
|
7353
|
+
frame). `area`, `box` and `share` arrived in 2.47 (issue #1386) as added fields:
|
|
7354
|
+
every field 2.46 wrote means what it meant, so the tag is still `rigc-folds/1`.
|
|
7335
7355
|
A triangle is named by its index `i` in `rest`'s `triangles`: vertices
|
|
7336
7356
|
`triangles[3i]`, `triangles[3i+1]`, `triangles[3i+2]`. A mesh that never folds is in
|
|
7337
7357
|
the file and on no printed line.
|
|
@@ -7346,9 +7366,11 @@ reading derived from it is computed from it — `foldsOf` over `geometry.json`'s
|
|
|
7346
7366
|
shows.
|
|
7347
7367
|
|
|
7348
7368
|
**A reading, not a gate.** Whether a fold is acceptable is yours: a tassel that swings
|
|
7349
|
-
past 90° folds honestly. To look at one, draw the
|
|
7350
|
-
|
|
7351
|
-
|
|
7369
|
+
past 90° folds honestly, and no size is too small to print. To look at one, draw the
|
|
7370
|
+
worst frame framed to it:
|
|
7371
|
+
`inspect --show art,folds,mesh --animation <name> --at <worst time> --zoom folds`
|
|
7372
|
+
fills the folded triangles at that pose, frames the picture to their box, and lists
|
|
7373
|
+
them with the same box and area in its legend (§13).
|
|
7352
7374
|
|
|
7353
7375
|
---
|
|
7354
7376
|
|
|
@@ -10176,7 +10198,7 @@ pixel (`IN09`).
|
|
|
10176
10198
|
| What does this bone move? | `--show art,mesh,weights:<bone> --dim` | the bone's weight over every region and mesh it moves, as a heat: a pixel whose centre lies in a triangle is painted the ramp's colour at the blend of its corners' weights — blue near 0, yellow at 0.5, red at 1 (the stops are in the legend) — and a weight of 0 is not painted, so the art shows where the bone does not reach. A region and an unweighted mesh are moved whole by their slot's bone. The weights are read off `skeleton.json`, the file both posers skin from; a bone that weights nothing paints nothing and the run says so on a line of its own |
|
|
10177
10199
|
| Does this part's edge follow that bone? | `--show art,weights:<bone>,mesh --overlay-slot <part> --zoom slot:<part>` | the part's own triangles and the bone's weight over them alone, over the whole painting. Where slots stack — a neck under the face, the hair, a collar — every mesh crossing the box is drawn and the heat is the union of every slot the bone moves, so the part's edge has no pixel of its own until the others are left out. The legend's `meshes[]` gives the part's weight for the bone at every vertex, so "does the top row follow the head" is read as numbers as well as colour |
|
|
10178
10200
|
| Is this rig's density where its motion is? | `--show art,density --dim` | each region and mesh filled by its **ratio** — its triangles per world unit² over the figure's — on one ramp, log-scaled and centred on 1×: grey is as dense as the figure, pink to magenta denser (4×, 16×), light to dark green sparser (1/4×, 1/16×), clamped past the end stops (the stops are in the legend). A part's area is its posed mesh's own, the sum of its triangles' areas at the pose drawn, not the image's box; a region is two triangles over its quad. Where parts overlap the pixel is the part drawn last, as in the art, so a part under others is read with `--overlay-slot <part>`. The figure's totals are always over every slot the pose shows — the filter picks what is drawn and listed, never what the figure is — and the legend's `density` gives each slot's attachment, kind, vertices, triangles, hull, area, triangles per area, ratio and the bones it is weighted to, in draw order. A measurement, not a verdict: no bar is drawn, and `A13_MESH_BUDGET` stays the one gate on counts |
|
|
10179
|
-
| Does anything fold in this animation? | `render --folds --animation <name>` (§8.3), then `inspect --show art,folds,mesh --
|
|
10201
|
+
| Does anything fold in this animation? | `render --folds --animation <name>` (§8.3), then `inspect --show art,folds,mesh --animation <name> --at <the worst frame's time> --zoom folds` | `render --folds` prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count, which triangles, their world box at that frame and their setup-pose area. The view fills those triangles at the pose drawn — every pixel whose centre a folded triangle holds — in the colour the legend's `folds` states, and the legend's `folds[]` names, per mesh, the folded triangles by index with the same box and area, and the setup pose's degenerate and inverted ones, which are never counted. A fold is usually a sliver a few units across, invisible in a picture of the whole mesh: `--zoom folds` frames the picture to the folded triangles' box. A thin sliver still needs pixels — raise `--max` when the fill is a line. A reading, not a verdict |
|
|
10180
10202
|
| How is this part triangulated? | `--show art,mesh,hull --zoom slot:<slot>` | each mesh's triangle edges, and over them its hull — vertex 0 to 1 to … to `hull − 1` and back. Regions are not drawn: a region is not a mesh. The legend lists every vertex in world units and in pixels |
|
|
10181
10203
|
| Which is drawn over which? | `--show art,slots` | each slot that shows a region or mesh numbered by its place in the draw order **at the pose drawn** (0 is drawn first), on a white box at the centre of its attachment's box — so a `drawOrder` key (§4.7) shows from the first frame `render` draws it on |
|
|
10182
10204
|
| Did this change move anything? | `--compare <dir>` beside any view | the same view of a second build beside the first, at the same times, on **one** world box and one transform, each half labelled `A`/`B` with the poser that posed it. A mark on a different pixel in the two halves is at a different place in the two rigs |
|
|
@@ -10199,7 +10221,16 @@ pixel (`IN09`).
|
|
|
10199
10221
|
vertices over every time drawn, padded by `<pad>` world units on every side
|
|
10200
10222
|
(default a quarter of the box's longer side). A name that is both a bone and a slot
|
|
10201
10223
|
is refused until it is written `bone:<name>` or `slot:<name>`; a bone of length 0
|
|
10202
|
-
with no pad is one point, and is refused asking for one.
|
|
10224
|
+
with no pad is one point, and is refused asking for one. **The folds** —
|
|
10225
|
+
`--zoom folds[:<pad>]`: the box of every triangle folded over (§8.3) at the times
|
|
10226
|
+
drawn, in every mesh `--overlay-slot`/`--hide-overlay-slot` keep — the union of
|
|
10227
|
+
the legend's `folds[].box` over the tiles — padded as above. With nothing folded at
|
|
10228
|
+
those times it is refused, naming the meshes that fold on the animation's grid and
|
|
10229
|
+
their frames (`--zoom folds: nothing to frame — no mesh has a triangle folded over
|
|
10230
|
+
at t=1s of "turn" — at 12 fps these do: "hair_lock_l" on frame(s) [22-23], worst
|
|
10231
|
+
frame 22 (t=1.8333s); …`); at the setup pose — what a fold is read against —
|
|
10232
|
+
nothing folds, and it says so. A rig with a bone or slot named `folds` is refused
|
|
10233
|
+
until the name is written `bone:folds` or `slot:folds`.
|
|
10203
10234
|
- **Two builds** — `--compare <dir | skeleton.json>`. The second build is loaded as
|
|
10204
10235
|
the first is (its own poser, chosen by what it carries, on a `poser B:` line),
|
|
10205
10236
|
posed at the same times, and drawn beside it: one time is two halves side by side,
|
|
@@ -10276,7 +10307,10 @@ pixel (`IN09`).
|
|
|
10276
10307
|
`why`; it is not painted. With `folds`, `folds[]`: every kept mesh with a
|
|
10277
10308
|
triangle folded at this tile's pose or a setup fault, in draw order — `slot`,
|
|
10278
10309
|
`attachment`, `triangleCount`, `folded` (the triangles filled, by index),
|
|
10279
|
-
`degenerate` and `inverted` (the setup pose's, never counted)
|
|
10310
|
+
`degenerate` and `inverted` (the setup pose's, never counted), `box` (the folded
|
|
10311
|
+
triangles' world box at this pose, `[minX, minY, maxX, maxY]`; `null` when none is
|
|
10312
|
+
folded), `area` (their setup-pose area, world units²) and `share` (that over the
|
|
10313
|
+
mesh's setup-pose area) — §8.3's numbers; empty when none.
|
|
10280
10314
|
With `mesh`, `hull`, `weights`, `density` or `folds`,
|
|
10281
10315
|
`meshes[]`: every region and mesh the pose shows, in draw order, with its `kind`,
|
|
10282
10316
|
`vertexCount`, `triangleCount`, `hull` (a mesh's hull vertex count), its unique
|
|
@@ -10287,8 +10321,8 @@ pixel (`IN09`).
|
|
|
10287
10321
|
`build` — `A` or `B`.
|
|
10288
10322
|
- `colours` — every colour the picture draws, as RGBA, with what it means.
|
|
10289
10323
|
- `poser`, `show`, `dim`, `skin`, `animation`, `fps` and `framing` — what was drawn
|
|
10290
|
-
and how it was framed (`shot`, `zoom` with the bone or slot
|
|
10291
|
-
was stated, or `viewport`).
|
|
10324
|
+
and how it was framed (`shot`, `zoom` with the bone or slot — or with `folds`, what
|
|
10325
|
+
the box is of — the pad and whether it was stated, or `viewport`).
|
|
10292
10326
|
- `weights` — with `weights:<bone>`: the bone, the ramp's stops as RGBA, the floor
|
|
10293
10327
|
below which nothing is painted, and how a pixel's weight is read.
|
|
10294
10328
|
- `density` — with `density`: the ramp's stops (`ratio`, RGBA), the `scale` it is
|
|
@@ -10296,7 +10330,8 @@ pixel (`IN09`).
|
|
|
10296
10330
|
the figure's `totals` and the `ratio` are and how a pixel is painted.
|
|
10297
10331
|
- `folds` — with `folds`: the colour a folded triangle is filled with, as RGBA, and
|
|
10298
10332
|
what it means; `degenerateShare` (2⁻³⁰, §8.3); and in words what `folded`,
|
|
10299
|
-
`degenerate` and `inverted` are
|
|
10333
|
+
`degenerate` and `inverted` are, how a triangle is named by `index`, and what
|
|
10334
|
+
`box` and `area` are. Its colour
|
|
10300
10335
|
is stated here rather than in `colours`, which every legend lists whole, so a legend
|
|
10301
10336
|
drawn without `folds` is the bytes it was before the view existed.
|
|
10302
10337
|
- `boneFilter` — `--bone` and `--hide-bone` as given.
|
|
@@ -10319,9 +10354,11 @@ beside it whose `spine.sha256` is its own — a Spine export states no rig invar
|
|
|
10319
10354
|
`weights` with no bone or with a bone the build lacks (the bones listed), a
|
|
10320
10355
|
`--bone`/`--hide-bone` pattern that matches no bone (the bones listed), an
|
|
10321
10356
|
`--overlay-slot`/`--hide-overlay-slot` pattern that matches no slot (the slots
|
|
10322
|
-
listed),
|
|
10357
|
+
listed), `--zoom folds` with no triangle folded over at the times drawn (the meshes
|
|
10358
|
+
that fold on the grid and their frames listed), and, with
|
|
10323
10359
|
`--compare`, any of these that holds of the second build, said as `the --compare
|
|
10324
|
-
build:
|
|
10360
|
+
build: …` — except `--zoom folds`, which frames to whichever build folds and is
|
|
10361
|
+
refused only when neither does, each build's frames named.
|
|
10325
10362
|
A rig that declares no `invariants.axes` is not refused: the picture draws nothing
|
|
10326
10363
|
for `declared` and the run says so on a line of its own.
|
|
10327
10364
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.47.1",
|
|
4
4
|
"description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli/shared.ts
CHANGED
|
@@ -999,7 +999,8 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
999
999
|
'frames\' own grid and viewport. Not with --slot/--hide: the geometry is the whole pose whatever is drawn',
|
|
1000
1000
|
folds:
|
|
1001
1001
|
`also read which mesh triangles fold — turn over against the setup pose — and write ${FOLDS_FILE} into each frame ` +
|
|
1002
|
-
'directory: per mesh, the frames with a folded triangle, the worst frame\'s count and which triangles, by index
|
|
1002
|
+
'directory: per mesh, the frames with a folded triangle, the worst frame\'s count and which triangles, by index, ' +
|
|
1003
|
+
'with where they are (the world box of their posed vertices) and how big (their setup-pose area, and its share of the mesh\'s). ' +
|
|
1003
1004
|
'One line per mesh that folds; a triangle degenerate or inverted at the setup pose is said once and never counted. ' +
|
|
1004
1005
|
'A reading, not a gate. Not with --slot/--hide: it reads every mesh the pose shows',
|
|
1005
1006
|
poser:
|
|
@@ -1089,7 +1090,9 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1089
1090
|
zoom:
|
|
1090
1091
|
'frame the picture to one bone (origin to tip) or one slot (its attachment\'s vertices) over every time drawn, padded ' +
|
|
1091
1092
|
'by <pad> world units on every side (default: a quarter of the box\'s longer side). A name that is both a bone and a ' +
|
|
1092
|
-
'slot is refused until written bone:<name> or slot:<name>; a name that is neither lists both rosters.
|
|
1093
|
+
'slot is refused until written bone:<name> or slot:<name>; a name that is neither lists both rosters. `folds` frames it to ' +
|
|
1094
|
+
'every folded triangle of every mesh --overlay-slot keeps, at the times drawn — refused, naming the frames that do fold, ' +
|
|
1095
|
+
'when none is folded there. Not with --viewport',
|
|
1093
1096
|
bone:
|
|
1094
1097
|
"draw only the bones these globs match (`*` any run of characters, `?` one, the whole name; comma-separated) — on `inspect`, " +
|
|
1095
1098
|
"for bones, axes and names; every bone stays in the legend, a left-out one saying why in `undrawn`. A pattern that matches no " +
|
|
@@ -1209,7 +1212,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1209
1212
|
max: '<px>',
|
|
1210
1213
|
show: '<view[,view…]>',
|
|
1211
1214
|
at: '<s[,s…]>',
|
|
1212
|
-
zoom: '<name>[:<pad>]',
|
|
1215
|
+
zoom: '<name|folds>[:<pad>]',
|
|
1213
1216
|
bone: '<glob[,glob…]>',
|
|
1214
1217
|
'hide-bone': '<glob[,glob…]>',
|
|
1215
1218
|
'overlay-slot': '<glob[,glob…]>',
|
|
@@ -1600,7 +1603,7 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1600
1603
|
usage: [
|
|
1601
1604
|
'rigc inspect --candidate <dir | skeleton.json> [--show art,bones,…] [--dim] [--max 256] [--poser core|spine] [--out inspect.png] (the setup pose)',
|
|
1602
1605
|
'rigc inspect … --animation <name> --at <s[,s…]> [--fps 12] [--skin <name>] (one time, or a sheet of several on one grid)',
|
|
1603
|
-
'rigc inspect … --zoom <bone|slot>[:<pad>] | --viewport <x,y,w,h> (a box, in world units)',
|
|
1606
|
+
'rigc inspect … --zoom <bone|slot|folds>[:<pad>] | --viewport <x,y,w,h> (a box, in world units)',
|
|
1604
1607
|
"rigc inspect … --bone 'bang_*' --hide-bone '*_ctl' (which bones bones, axes and names draw)",
|
|
1605
1608
|
'rigc inspect … --overlay-slot neck --hide-overlay-slot <glob> (which slots mesh, hull, weights, density, folds and slots draw; the art stays whole)',
|
|
1606
1609
|
'rigc inspect … --compare <dir | skeleton.json> (a second build beside the first, on one transform)',
|
package/src/core/raw.ts
CHANGED
|
@@ -496,13 +496,71 @@ export function poseRawAnimation(doc: CompiledDocument, animation: string, steps
|
|
|
496
496
|
* A refusal is thrown from the pose it is met at, after `visit` has seen the
|
|
497
497
|
* poses before it. `clock`, when given, is what each step moves the physics
|
|
498
498
|
* clock by in place of the step itself — the render's offline schedule
|
|
499
|
-
* (`physicsClockStep`
|
|
499
|
+
* (`physicsClockStep` below, issue #1380); the track time and
|
|
500
500
|
* the animation time are the steps' running sum either way.
|
|
501
501
|
*/
|
|
502
502
|
export function poseRawAnimationEach(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, visit: (pose: RawPose, index: number) => void, clock?: (dt: number) => number): void {
|
|
503
503
|
walkEach(doc, animation, steps, plant, reset, clock === undefined ? RAW_MODE : { ...RAW_MODE, clock: (dt) => clock(dt) }, RAW_SHAPE, visit);
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
/**
|
|
507
|
+
* How much faster than the frames a render's physics clock runs — and, since issue
|
|
508
|
+
* #1389, a mesh comparison's (`src/meshcompare.ts`, `comparisonWalk`): each frame
|
|
509
|
+
* moves the skeleton's clock — the time a physics constraint integrates — by
|
|
510
|
+
* `physicsClockStep(1 / fps)`, the frame's step and `2⁻³⁰` of it more, while
|
|
511
|
+
* the animation is applied at the running sum of the frame steps as before
|
|
512
|
+
* (issue #1380).
|
|
513
|
+
*
|
|
514
|
+
* ⭐ **Why a render's physics clock is not the frame clock.** A physics
|
|
515
|
+
* constraint keeps the time it has not yet integrated (`remaining`), takes
|
|
516
|
+
* whole steps of `1 / fps` (its own `fps`, 60 by default) while at least one
|
|
517
|
+
* step remains, and shows `offset − lag·(1 − remaining / step)`, `lag` being
|
|
518
|
+
* everything this update's steps moved (`src/core/constraints_physics.ts`,
|
|
519
|
+
* *The step*). Whenever a frame lasts a whole number of physics steps — 12,
|
|
520
|
+
* 20, 30 and 60 fps against 60 Hz, and every second frame at 24 — the time
|
|
521
|
+
* left at the frame's end is zero in exact arithmetic, and in doubles it is
|
|
522
|
+
* either a few ulps above zero or a few ulps short of one whole step. Which of
|
|
523
|
+
* the two is decided by rounding, not by the motion: the shown weight is 1 or
|
|
524
|
+
* 0, so the spring is drawn one frame late or on time, and it changes regime
|
|
525
|
+
* on a single frame. Measured on the public trailer (`show`, 20 fps, 321
|
|
526
|
+
* frames): every spring jumps at frames 5, 12, 45 and 188 — the four frames
|
|
527
|
+
* that arithmetic alone predicts — and the loop's start (frame 80, on time)
|
|
528
|
+
* and end (frame 320, a frame late, which is what exact arithmetic draws)
|
|
529
|
+
* differ by 31.70 units of vertex and 10.37 of bone
|
|
530
|
+
* while every keyed bone agrees to 1.3e-12. spine-core 4.3.13 stepped the
|
|
531
|
+
* same way draws the same frames to the bit: this is the runtime under a
|
|
532
|
+
* fixed offline step, not a defect of the core's port, which is why the fix
|
|
533
|
+
* is the render's schedule and the physics is untouched.
|
|
534
|
+
*
|
|
535
|
+
* Running the clock `2⁻³⁰` fast puts the time left a little ABOVE every
|
|
536
|
+
* whole number of steps instead of on it, by `2⁻³⁰ · t` after `t` seconds:
|
|
537
|
+
* 1.5e-8 s at 16 s, against a rounding error of at most a few ulps of the
|
|
538
|
+
* clock (3.6e-15 s at 16 s) — so every tie resolves the way exact arithmetic
|
|
539
|
+
* resolves it, a step taken, and the shown weight sits at 1 − 9e-7 rather
|
|
540
|
+
* than flipping. It stays below the smallest gap a step boundary can fall
|
|
541
|
+
* into for 7.8e5 s even at 23 fps against 60 Hz (a gap of 1/1380 s), and a
|
|
542
|
+
* physics `reset` key, which restarts the time left from zero, is resolved
|
|
543
|
+
* the same way on every frame after it, because the lead is taken on every
|
|
544
|
+
* frame rather than once (read off the clock arithmetic with a reset at frame
|
|
545
|
+
* 100, 16 s at the five rates: no tie; no tree probe keys a reset under the
|
|
546
|
+
* render's clock). A pose stays a function of its frame
|
|
547
|
+
* index alone: the lead does not depend on how many frames are asked for.
|
|
548
|
+
*
|
|
549
|
+
* Rejected, measured over the same arithmetic (16 s at 12, 20, 24, 25, 30 and
|
|
550
|
+
* 60 fps): the clock as `i / fps` rather than a running sum (90 frames still
|
|
551
|
+
* flipped at 20 fps); a one-time lead of half a step (114 flipped at 24 fps,
|
|
552
|
+
* where half a step lands on the other tie); a one-time lead of any size,
|
|
553
|
+
* which a `reset` key cancels; and snapping `remaining` inside the physics,
|
|
554
|
+
* which changes the runtime's physics rather than the render's schedule and
|
|
555
|
+
* which spine-core's poser could not follow.
|
|
556
|
+
*/
|
|
557
|
+
export const PHYSICS_CLOCK_LEAD = 2 ** -30;
|
|
558
|
+
|
|
559
|
+
/** What one frame of `step` seconds moves a render's physics clock by — `PHYSICS_CLOCK_LEAD`; both render posers and the mesh comparison step with it. */
|
|
560
|
+
export function physicsClockStep(step: number): number {
|
|
561
|
+
return step + step * PHYSICS_CLOCK_LEAD;
|
|
562
|
+
}
|
|
563
|
+
|
|
506
564
|
/**
|
|
507
565
|
* The animation time a looping track applies its animation at (`./walk.ts`,
|
|
508
566
|
* *The time*): the track time wrapped by the duration, and 0 over a duration
|
package/src/folds.ts
CHANGED
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
* A mesh mirrored whole by a negative scale reads every triangle as folded: that
|
|
37
37
|
* is what "opposite to the setup pose" is, and the count says which it was.
|
|
38
38
|
*
|
|
39
|
+
* ## Where and how big (issue #1386)
|
|
40
|
+
*
|
|
41
|
+
* Each folded frame also carries the world box of its folded triangles' posed
|
|
42
|
+
* vertices and their summed setup-pose area, alone and as a share of the mesh's
|
|
43
|
+
* — `foldExtent` says why the box is the frame's and the area the setup pose's.
|
|
44
|
+
*
|
|
39
45
|
* Pure: no clock, no randomness, no runtime. The posed vertices are handed in by
|
|
40
46
|
* whichever poser drew the frames (rigc's core for a rigc build).
|
|
41
47
|
*/
|
|
@@ -98,8 +104,21 @@ export interface SetupFaults {
|
|
|
98
104
|
inverted: number[];
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
/** A world box `[minX, minY, maxX, maxY]`, world units, y up. */
|
|
108
|
+
export type FoldBox = [number, number, number, number];
|
|
109
|
+
|
|
110
|
+
/** Where a set of folded triangles is at one pose, and how big they are (issue #1386). */
|
|
111
|
+
export interface FoldExtent {
|
|
112
|
+
/** The world box of the folded triangles' vertices as posed at this frame — every vertex of each, nothing else. */
|
|
113
|
+
box: FoldBox;
|
|
114
|
+
/** Their summed area at the setup pose, world units² — each triangle's unsigned area in the rest table. */
|
|
115
|
+
area: number;
|
|
116
|
+
/** `area` over the mesh's own setup-pose area (every triangle's, unsigned): the share of the mesh's art that is turned over. */
|
|
117
|
+
share: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
101
120
|
/** One frame on which a mesh has a folded triangle. */
|
|
102
|
-
export interface FoldedFrame {
|
|
121
|
+
export interface FoldedFrame extends FoldExtent {
|
|
103
122
|
index: number;
|
|
104
123
|
time: number;
|
|
105
124
|
/** The folded triangles, by index (`triangles[3i..3i+2]` of the rest table), ascending. */
|
|
@@ -112,6 +131,8 @@ export interface MeshFolds {
|
|
|
112
131
|
attachment: string;
|
|
113
132
|
/** How many triangles the mesh has. */
|
|
114
133
|
triangleCount: number;
|
|
134
|
+
/** The mesh's area at the setup pose, world units²: every triangle's unsigned area in the rest table, summed — what `share` is over. */
|
|
135
|
+
area: number;
|
|
115
136
|
setup: SetupFaults;
|
|
116
137
|
/** Every frame with a folded triangle, in frame order; empty when the mesh never folds. */
|
|
117
138
|
frames: FoldedFrame[];
|
|
@@ -129,7 +150,7 @@ export interface FoldsFile {
|
|
|
129
150
|
skin?: string;
|
|
130
151
|
fps: number;
|
|
131
152
|
/** What the numbers mean, stated in the file so a reader does not need this module. */
|
|
132
|
-
reading: { folded: string; degenerate: string; inverted: string; index: string };
|
|
153
|
+
reading: { folded: string; degenerate: string; inverted: string; index: string; box: string; area: string };
|
|
133
154
|
/** Every mesh any frame of the set shows, in the order the rest table lists them. */
|
|
134
155
|
meshes: MeshFolds[];
|
|
135
156
|
}
|
|
@@ -145,6 +166,12 @@ export interface FoldPlant {
|
|
|
145
166
|
againstPreviousFrame?: boolean;
|
|
146
167
|
/** Count the setup pose's degenerate and inverted triangles like any other. */
|
|
147
168
|
countSetupFaults?: boolean;
|
|
169
|
+
/** Box every vertex of the mesh at the frame, rather than the folded triangles' vertices. */
|
|
170
|
+
boxWholeMesh?: boolean;
|
|
171
|
+
/** Size the folded triangles by their unsigned area as posed at the frame, rather than at the setup pose. */
|
|
172
|
+
areaPosed?: boolean;
|
|
173
|
+
/** Read degeneracy against this share of the largest coordinate rather than `DEGENERATE_SHARE` — 0 drops the line (issue #1390). */
|
|
174
|
+
degenerateShare?: number;
|
|
148
175
|
}
|
|
149
176
|
|
|
150
177
|
/** Twice the signed area of triangle `t` in `xy` (`x, y` per vertex). */
|
|
@@ -176,12 +203,13 @@ function magnitude(xy: readonly number[]): number {
|
|
|
176
203
|
|
|
177
204
|
/**
|
|
178
205
|
* The sign of triangle `t`'s area in `xy`: +1, −1, or 0 when it is degenerate
|
|
179
|
-
* — its height over its longest edge at most `
|
|
206
|
+
* — its height over its longest edge at most `share` (`DEGENERATE_SHARE`
|
|
207
|
+
* unplanted) of `scale`.
|
|
180
208
|
*/
|
|
181
|
-
function orientation(xy: readonly number[], triangles: readonly number[], t: number, scale: number): -1 | 0 | 1 {
|
|
209
|
+
function orientation(xy: readonly number[], triangles: readonly number[], t: number, scale: number, share: number): -1 | 0 | 1 {
|
|
182
210
|
const twice = cross2(xy, triangles, t);
|
|
183
211
|
const edge = longestEdge(xy, triangles, t);
|
|
184
|
-
if (edge === 0 || Math.abs(twice) / edge <=
|
|
212
|
+
if (edge === 0 || Math.abs(twice) / edge <= share * scale) return 0;
|
|
185
213
|
return twice > 0 ? 1 : -1;
|
|
186
214
|
}
|
|
187
215
|
|
|
@@ -195,6 +223,7 @@ export interface SetupReading {
|
|
|
195
223
|
/** The setup reading of one mesh — see the module comment for what degenerate and inverted mean. */
|
|
196
224
|
export function setupReading(rest: FoldRest, plant: FoldPlant = {}): SetupReading {
|
|
197
225
|
const count = Math.floor(rest.triangles.length / 3);
|
|
226
|
+
const share = plant.degenerateShare ?? DEGENERATE_SHARE;
|
|
198
227
|
const signs = new Int8Array(count);
|
|
199
228
|
const world: number[] = [];
|
|
200
229
|
const uvSigns: number[] = [];
|
|
@@ -204,8 +233,8 @@ export function setupReading(rest: FoldRest, plant: FoldPlant = {}): SetupReadin
|
|
|
204
233
|
let sense = 0;
|
|
205
234
|
const degenerate: number[] = [];
|
|
206
235
|
for (let t = 0; t < count; t++) {
|
|
207
|
-
const w = orientation(rest.vertices, rest.triangles, t, worldScale);
|
|
208
|
-
const u = uvs.length === rest.vertices.length ? orientation(uvs, rest.triangles, t, uvScale) : 1;
|
|
236
|
+
const w = orientation(rest.vertices, rest.triangles, t, worldScale, share);
|
|
237
|
+
const u = uvs.length === rest.vertices.length ? orientation(uvs, rest.triangles, t, uvScale, share) : 1;
|
|
209
238
|
world.push(w);
|
|
210
239
|
uvSigns.push(u);
|
|
211
240
|
if (w === 0 || u === 0) {
|
|
@@ -238,17 +267,61 @@ export function foldedAt(rest: FoldRest, setup: SetupReading, vertices: readonly
|
|
|
238
267
|
);
|
|
239
268
|
}
|
|
240
269
|
const scale = magnitude(vertices);
|
|
270
|
+
const share = plant.degenerateShare ?? DEGENERATE_SHARE;
|
|
241
271
|
const out: number[] = [];
|
|
242
272
|
for (let t = 0; t < setup.signs.length; t++) {
|
|
243
273
|
const before = setup.signs[t];
|
|
244
274
|
if (before === 0) continue;
|
|
245
|
-
const now = orientation(vertices, rest.triangles, t, scale);
|
|
275
|
+
const now = orientation(vertices, rest.triangles, t, scale, share);
|
|
246
276
|
if (now === 0) continue;
|
|
247
277
|
if (plant.signFlipped ? now === before : now === -before) out.push(t);
|
|
248
278
|
}
|
|
249
279
|
return out;
|
|
250
280
|
}
|
|
251
281
|
|
|
282
|
+
/** A mesh's area at the setup pose: every triangle's unsigned area in the rest table, summed in index order. */
|
|
283
|
+
export function setupArea(rest: FoldRest): number {
|
|
284
|
+
let area = 0;
|
|
285
|
+
for (let t = 0; t < Math.floor(rest.triangles.length / 3); t++) area += Math.abs(cross2(rest.vertices, rest.triangles, t)) / 2;
|
|
286
|
+
return area;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Where `triangles` of one mesh are at one pose and how big they are: the world
|
|
291
|
+
* box of their posed vertices, and their summed setup-pose area, alone and as a
|
|
292
|
+
* share of `meshArea` (`setupArea(rest)`). `null` for no triangle.
|
|
293
|
+
*
|
|
294
|
+
* ⭐ The box is the pose's and the area is the setup pose's, on purpose. The box
|
|
295
|
+
* answers "where is it in this frame" — it is what a picture is framed to. The
|
|
296
|
+
* area answers "how much of the art is turned over", and a folded triangle's
|
|
297
|
+
* posed area cannot: it passes through 0 at the frame the triangle turns, so one
|
|
298
|
+
* triangle reads a different size on every frame it is folded and none of them
|
|
299
|
+
* says how much art it carries.
|
|
300
|
+
*/
|
|
301
|
+
export function foldExtent(
|
|
302
|
+
rest: FoldRest,
|
|
303
|
+
vertices: readonly number[],
|
|
304
|
+
triangles: readonly number[],
|
|
305
|
+
meshArea: number,
|
|
306
|
+
plant: FoldPlant = {},
|
|
307
|
+
): FoldExtent | null {
|
|
308
|
+
if (triangles.length === 0) return null;
|
|
309
|
+
const box: FoldBox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
310
|
+
const take = (v: number): void => {
|
|
311
|
+
box[0] = Math.min(box[0], vertices[2 * v]);
|
|
312
|
+
box[1] = Math.min(box[1], vertices[2 * v + 1]);
|
|
313
|
+
box[2] = Math.max(box[2], vertices[2 * v]);
|
|
314
|
+
box[3] = Math.max(box[3], vertices[2 * v + 1]);
|
|
315
|
+
};
|
|
316
|
+
if (plant.boxWholeMesh) for (let v = 0; v < vertices.length / 2; v++) take(v);
|
|
317
|
+
let area = 0;
|
|
318
|
+
for (const t of triangles) {
|
|
319
|
+
if (!plant.boxWholeMesh) for (let k = 0; k < 3; k++) take(rest.triangles[3 * t + k]);
|
|
320
|
+
area += Math.abs(cross2(plant.areaPosed ? vertices : rest.vertices, rest.triangles, t)) / 2;
|
|
321
|
+
}
|
|
322
|
+
return { box, area, share: meshArea > 0 ? area / meshArea : 0 };
|
|
323
|
+
}
|
|
324
|
+
|
|
252
325
|
const restKey = (slot: string, attachment: string): string => `${slot}\u0000${attachment}`;
|
|
253
326
|
|
|
254
327
|
/**
|
|
@@ -264,6 +337,7 @@ export function foldsOf(rest: readonly FoldRest[], frames: readonly FoldFrame[],
|
|
|
264
337
|
for (const r of rest) {
|
|
265
338
|
if (r.kind !== 'mesh' || !shown.has(restKey(r.slot, r.attachment))) continue;
|
|
266
339
|
let setup = setupReading(r, plant);
|
|
340
|
+
const area = setupArea(r);
|
|
267
341
|
const folded: FoldedFrame[] = [];
|
|
268
342
|
const union = new Set<number>();
|
|
269
343
|
for (const frame of frames) {
|
|
@@ -274,9 +348,10 @@ export function foldsOf(rest: readonly FoldRest[], frames: readonly FoldFrame[],
|
|
|
274
348
|
const signs = Int8Array.from(setup.signs, (sign, t) => (triangles.includes(t) ? -sign : sign));
|
|
275
349
|
setup = { ...setup, signs };
|
|
276
350
|
}
|
|
277
|
-
|
|
351
|
+
const extent = foldExtent(r, posed.vertices, triangles, area, plant);
|
|
352
|
+
if (extent === null) continue;
|
|
278
353
|
for (const t of triangles) union.add(t);
|
|
279
|
-
folded.push({ index: frame.index, time: frame.time, triangles });
|
|
354
|
+
folded.push({ index: frame.index, time: frame.time, triangles, ...extent });
|
|
280
355
|
}
|
|
281
356
|
let worst: FoldedFrame | null = null;
|
|
282
357
|
for (const f of folded) if (worst === null || f.triangles.length > worst.triangles.length) worst = f;
|
|
@@ -284,6 +359,7 @@ export function foldsOf(rest: readonly FoldRest[], frames: readonly FoldFrame[],
|
|
|
284
359
|
slot: r.slot,
|
|
285
360
|
attachment: r.attachment,
|
|
286
361
|
triangleCount: setup.signs.length,
|
|
362
|
+
area,
|
|
287
363
|
setup: setup.faults,
|
|
288
364
|
frames: folded,
|
|
289
365
|
worst,
|
|
@@ -301,6 +377,9 @@ export const FOLDS_READING: FoldsFile['reading'] = {
|
|
|
301
377
|
inverted:
|
|
302
378
|
"a triangle drawn mirrored against the rest of its mesh at the setup pose: the sign of world area x UV area is the opposite of the mesh's own sense (that product summed over its triangles, weighted by world area). Setup faults are listed once and never counted as folds",
|
|
303
379
|
index: 'a triangle is named by its index i in the rest table: vertices triangles[3i], triangles[3i+1], triangles[3i+2]',
|
|
380
|
+
box: "a frame's box is [minX, minY, maxX, maxY] in world units (y up) over every vertex of that frame's folded triangles as posed there, and nothing else",
|
|
381
|
+
area:
|
|
382
|
+
"a frame's area is its folded triangles' unsigned areas at the setup pose (the rest table), summed, in world units²; share is that over the mesh's area, every triangle's unsigned setup-pose area summed. The setup pose's, not the frame's: a folded triangle's posed area passes through 0 as it turns, so it says how far the turn has gone rather than how much art is turned over",
|
|
304
383
|
};
|
|
305
384
|
|
|
306
385
|
/** The file for one frame set. */
|
|
@@ -325,10 +404,27 @@ export function indexRuns(indices: readonly number[]): string {
|
|
|
325
404
|
return runs.join(', ');
|
|
326
405
|
}
|
|
327
406
|
|
|
407
|
+
/** `x` rounded to hundredths away from the box's inside, so the printed box holds every vertex the exact one does. */
|
|
408
|
+
const outward = (x: number, up: boolean): string => ((up ? Math.ceil(x * 100) : Math.floor(x * 100)) / 100).toFixed(2);
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Where and how big, as the fold line and the inspect note print it:
|
|
412
|
+
* `in world box x [x0, x1] y [y0, y1] (w × h), setup-pose area A units² = S% of the mesh's`.
|
|
413
|
+
* The box is rounded outward to hundredths; the file states it exactly.
|
|
414
|
+
*/
|
|
415
|
+
export function extentText(extent: FoldExtent): string {
|
|
416
|
+
const [x0, y0, x1, y1] = extent.box;
|
|
417
|
+
return (
|
|
418
|
+
`in world box x [${outward(x0, false)}, ${outward(x1, true)}] y [${outward(y0, false)}, ${outward(y1, true)}] ` +
|
|
419
|
+
`(${(x1 - x0).toFixed(2)} × ${(y1 - y0).toFixed(2)}), setup-pose area ${extent.area.toFixed(2)} units² = ${(100 * extent.share).toPrecision(3)}% of the mesh's`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
328
423
|
/**
|
|
329
424
|
* The printed line for a mesh that folds, or `null` for one that never does.
|
|
330
425
|
* It names the animation, the mesh and its slot, the frames, the worst frame's
|
|
331
|
-
* count
|
|
426
|
+
* count, its triangles, where they are and how big, and every triangle folded
|
|
427
|
+
* on any frame.
|
|
332
428
|
*/
|
|
333
429
|
export function foldLine(animation: string | null, fps: number, mesh: MeshFolds): string | null {
|
|
334
430
|
const worst = mesh.worst;
|
|
@@ -337,7 +433,8 @@ export function foldLine(animation: string | null, fps: number, mesh: MeshFolds)
|
|
|
337
433
|
const frames = mesh.frames.map((f) => f.index);
|
|
338
434
|
return (
|
|
339
435
|
`${where}: mesh "${mesh.attachment}" (slot "${mesh.slot}") folds on ${frames.length} frame(s) [${indexRuns(frames)}] at ${fps} fps; ` +
|
|
340
|
-
`worst frame ${worst.index} (t=${worst.time.toFixed(4)}s): ${worst.triangles.length} of ${mesh.triangleCount} triangle(s) [${worst.triangles.join(', ')}]
|
|
436
|
+
`worst frame ${worst.index} (t=${worst.time.toFixed(4)}s): ${worst.triangles.length} of ${mesh.triangleCount} triangle(s) [${worst.triangles.join(', ')}] ` +
|
|
437
|
+
`${extentText(worst)}; ` +
|
|
341
438
|
`${mesh.folded.length} triangle(s) fold on some frame [${mesh.folded.join(', ')}]`
|
|
342
439
|
);
|
|
343
440
|
}
|
package/src/inspect.ts
CHANGED
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
* ⭐ **A fold is read, never judged** (issue #1383). `folds` fills each mesh
|
|
46
46
|
* triangle that is turned over at the pose drawn — its world orientation the
|
|
47
47
|
* opposite of the setup pose's — by `./folds.ts`, the reading `render --folds`
|
|
48
|
-
* prints, so the picture and that line name the same triangles
|
|
48
|
+
* prints, so the picture and that line name the same triangles — and the same
|
|
49
|
+
* box and area, which `--zoom folds` frames the picture to (issue #1386).
|
|
49
50
|
*
|
|
50
51
|
* Links nothing from the runtime: the poser is handed in.
|
|
51
52
|
*/
|
|
@@ -76,7 +77,7 @@ import {
|
|
|
76
77
|
viewportFor,
|
|
77
78
|
} from './render_shared.ts';
|
|
78
79
|
import { hypot2 } from './numeric.ts';
|
|
79
|
-
import { DEGENERATE_SHARE, foldedAt, FOLDS_READING, setupReading } from './folds.ts';
|
|
80
|
+
import { DEGENERATE_SHARE, extentText, type FoldBox, foldedAt, foldExtent, type FoldPlant, FOLDS_READING, foldsOf, indexRuns, setupArea, setupReading } from './folds.ts';
|
|
80
81
|
|
|
81
82
|
/** The legend's format tag. */
|
|
82
83
|
export const INSPECT_SPEC = 'rigc-inspect/1';
|
|
@@ -313,6 +314,8 @@ export interface InspectPlant {
|
|
|
313
314
|
densityTotalsFiltered?: boolean;
|
|
314
315
|
/** Folds: fill every triangle of every kept mesh, folded or not. */
|
|
315
316
|
foldsFillAll?: boolean;
|
|
317
|
+
/** Folds: a planted fold reading (`./folds.ts`) for the box and area the legend states and `--zoom folds` frames to. */
|
|
318
|
+
foldPlant?: FoldPlant;
|
|
316
319
|
}
|
|
317
320
|
|
|
318
321
|
/** A point in both spaces. */
|
|
@@ -458,6 +461,12 @@ export interface LegendFold {
|
|
|
458
461
|
degenerate: number[];
|
|
459
462
|
/** Drawn mirrored against the rest of the mesh at the setup pose, never counted — by index. */
|
|
460
463
|
inverted: number[];
|
|
464
|
+
/** The world box of the folded triangles' vertices at this pose, `[minX, minY, maxX, maxY]`; `null` when none is folded (issue #1386). */
|
|
465
|
+
box: FoldBox | null;
|
|
466
|
+
/** The folded triangles' summed setup-pose area, world units² — 0 when none is folded. */
|
|
467
|
+
area: number;
|
|
468
|
+
/** `area` over the mesh's setup-pose area. */
|
|
469
|
+
share: number;
|
|
461
470
|
}
|
|
462
471
|
|
|
463
472
|
export interface LegendTile {
|
|
@@ -497,7 +506,7 @@ export interface InspectLegend {
|
|
|
497
506
|
fps: number;
|
|
498
507
|
framing:
|
|
499
508
|
| { kind: 'shot'; note: string }
|
|
500
|
-
| { kind: 'zoom'; bone?: string; slot?: string; pad: number; padFrom: 'stated' | 'default' }
|
|
509
|
+
| { kind: 'zoom'; bone?: string; slot?: string; folds?: string; pad: number; padFrom: 'stated' | 'default' }
|
|
501
510
|
| { kind: 'viewport' };
|
|
502
511
|
/** `--bone` / `--hide-bone` as given — each bone they leave out says so in its `undrawn`. */
|
|
503
512
|
boneFilter?: { include: string[] | null; exclude: string[] | null };
|
|
@@ -508,7 +517,7 @@ export interface InspectLegend {
|
|
|
508
517
|
/** With `density`: the ramp, its scale, and how area, totals and ratio are read. */
|
|
509
518
|
density?: { ramp: Array<{ ratio: number; rgba: number[] }>; scale: string; area: string; totals: string; ratio: string; reading: string };
|
|
510
519
|
/** With `folds`: the colour a folded triangle is filled with, the degenerate share, and what folded, degenerate and inverted mean. */
|
|
511
|
-
folds?: { rgba: number[]; meaning: string; degenerateShare: number; folded: string; degenerate: string; inverted: string; index: string };
|
|
520
|
+
folds?: { rgba: number[]; meaning: string; degenerateShare: number; folded: string; degenerate: string; inverted: string; index: string; box: string; area: string };
|
|
512
521
|
/** With `--compare`: each build, the poser that posed it and its `skeleton.json`'s sha256; a tile names its own in `build`. */
|
|
513
522
|
builds?: Array<{ build: 'A' | 'B'; poser: 'core' | 'spine'; skeletonSha256: string }>;
|
|
514
523
|
coordinates: { world: string; image: string; toPixel: string; toWorld: string };
|
|
@@ -598,7 +607,7 @@ export function readTimes(raw: string | undefined): number[] {
|
|
|
598
607
|
return times;
|
|
599
608
|
}
|
|
600
609
|
|
|
601
|
-
/** `--zoom <name>[:<pad>]`: the name, and the pad in world units when one is stated. */
|
|
610
|
+
/** `--zoom <name>[:<pad>]`: the name — a bone, a slot or `folds` — and the pad in world units when one is stated. */
|
|
602
611
|
export function readZoom(raw: string): { target: string; pad: number | null } {
|
|
603
612
|
const colon = raw.lastIndexOf(':');
|
|
604
613
|
if (colon > 0) {
|
|
@@ -609,7 +618,7 @@ export function readZoom(raw: string): { target: string; pad: number | null } {
|
|
|
609
618
|
return { target: raw.slice(0, colon), pad };
|
|
610
619
|
}
|
|
611
620
|
}
|
|
612
|
-
if (raw === '') throw new InspectError('--zoom needs a bone or slot name: --zoom <name>[:<pad>]');
|
|
621
|
+
if (raw === '') throw new InspectError('--zoom needs a bone or slot name, or folds: --zoom <name|folds>[:<pad>]');
|
|
613
622
|
return { target: raw, pad: null };
|
|
614
623
|
}
|
|
615
624
|
|
|
@@ -1075,10 +1084,17 @@ function slotsLeftOut(slots: ReadonlyArray<{ name: string }>, filter: SlotFilter
|
|
|
1075
1084
|
// the view
|
|
1076
1085
|
// ---------------------------------------------------------------------------
|
|
1077
1086
|
|
|
1078
|
-
/**
|
|
1079
|
-
|
|
1087
|
+
/**
|
|
1088
|
+
* The poses one picture is of: the frames, in the order the request named their
|
|
1089
|
+
* times — and every frame of the grid they were taken from, which `--zoom folds`
|
|
1090
|
+
* reads when nothing is folded at the times drawn.
|
|
1091
|
+
*/
|
|
1092
|
+
function posesOf(poser: Poser, request: InspectRequest, needAttachments: boolean): { poses: Array<{ frame: Frame; index: number }>; grid: Frame[] } {
|
|
1080
1093
|
const opts: PoseOptions = { bones: true, ...(needAttachments ? { geometry: true } : {}), ...(request.skin === undefined ? {} : { skin: request.skin }) };
|
|
1081
|
-
if (request.animation === null)
|
|
1094
|
+
if (request.animation === null) {
|
|
1095
|
+
const grid = sampleSetupPose(poser, opts);
|
|
1096
|
+
return { poses: [{ frame: grid[0], index: 0 }], grid };
|
|
1097
|
+
}
|
|
1082
1098
|
const animation = poser.animations.find((a) => a.name === request.animation);
|
|
1083
1099
|
if (animation === undefined) {
|
|
1084
1100
|
throw new InspectError(`no animation ${JSON.stringify(request.animation)} in this skeleton; it has [${poser.animations.map((a) => a.name).join(', ') || 'none'}]`);
|
|
@@ -1090,11 +1106,67 @@ function posesOf(poser: Poser, request: InspectRequest, needAttachments: boolean
|
|
|
1090
1106
|
seen.add(index);
|
|
1091
1107
|
}
|
|
1092
1108
|
const frames = sampleAnimation(poser, animation.name, request.fps, opts);
|
|
1093
|
-
return indices.map((index) => ({ frame: frames[index], index }));
|
|
1109
|
+
return { poses: indices.map((index) => ({ frame: frames[index], index })), grid: frames };
|
|
1094
1110
|
}
|
|
1095
1111
|
|
|
1096
1112
|
type WorldBox = [number, number, number, number];
|
|
1097
1113
|
|
|
1114
|
+
/** The word `--zoom` takes for the fold reading's box rather than a bone's or a slot's (issue #1386). */
|
|
1115
|
+
export const ZOOM_FOLDS = 'folds';
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* `--zoom folds`: the world box of every folded triangle of every kept mesh at
|
|
1119
|
+
* the times drawn — the box the legend's `folds[]` states per tile, unioned —
|
|
1120
|
+
* or, when none is folded there, `null` and the sentence that says which frames
|
|
1121
|
+
* of the grid do fold, so the refusal points at them.
|
|
1122
|
+
*/
|
|
1123
|
+
function foldsZoomBox(
|
|
1124
|
+
poser: Poser,
|
|
1125
|
+
poses: ReadonlyArray<{ frame: Frame }>,
|
|
1126
|
+
grid: readonly Frame[],
|
|
1127
|
+
slotsOut: ReadonlySet<string>,
|
|
1128
|
+
request: InspectRequest,
|
|
1129
|
+
plant: InspectPlant,
|
|
1130
|
+
): { box: WorldBox; folds: string } | { miss: string } {
|
|
1131
|
+
const rests = new Map<string, AttachmentRest>();
|
|
1132
|
+
const restOf = (frames: ReadonlyArray<{ frame: Frame }>): void => {
|
|
1133
|
+
for (const r of poser.rest(request.skin, frames.map((p) => p.frame.attachments ?? []))) rests.set(restKey(r.slot, r.attachment), r);
|
|
1134
|
+
};
|
|
1135
|
+
restOf(poses);
|
|
1136
|
+
const box: WorldBox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1137
|
+
for (const { frame } of poses) {
|
|
1138
|
+
for (const shown of frame.attachments ?? []) {
|
|
1139
|
+
if (slotsOut.has(shown.slot)) continue;
|
|
1140
|
+
const rest = rests.get(restKey(shown.slot, shown.attachment));
|
|
1141
|
+
if (rest === undefined || rest.kind !== 'mesh') continue;
|
|
1142
|
+
const extent = foldExtent(rest, shown.vertices, foldedAt(rest, setupReading(rest), shown.vertices), setupArea(rest), plant.foldPlant);
|
|
1143
|
+
if (extent === null) continue;
|
|
1144
|
+
box[0] = Math.min(box[0], extent.box[0]);
|
|
1145
|
+
box[1] = Math.min(box[1], extent.box[1]);
|
|
1146
|
+
box[2] = Math.max(box[2], extent.box[2]);
|
|
1147
|
+
box[3] = Math.max(box[3], extent.box[3]);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const kept = request.slotFilter === undefined ? 'mesh' : 'mesh the filter keeps';
|
|
1151
|
+
if (box.every(Number.isFinite)) return { box, folds: `every triangle of every ${kept} folded over at the time(s) drawn` };
|
|
1152
|
+
const at = request.animation === null ? 'the setup pose' : `t=${request.times.map((t) => `${t}s`).join(', ')}`;
|
|
1153
|
+
if (request.animation === null) {
|
|
1154
|
+
return { miss: `no ${kept} has a triangle folded over at ${at} — the setup pose is what a fold is read against, so nothing folds there; name --animation <name> --at <s>` };
|
|
1155
|
+
}
|
|
1156
|
+
// Where the grid does fold: the reading `render --folds` prints, over every frame of it, for the kept meshes.
|
|
1157
|
+
restOf(grid.map((frame) => ({ frame })));
|
|
1158
|
+
const keptRest = [...rests.values()].filter((r) => !slotsOut.has(r.slot));
|
|
1159
|
+
const read = grid.map((frame, index) => ({ index, time: frame.time, attachments: (frame.attachments ?? []).filter((a) => !slotsOut.has(a.slot)) }));
|
|
1160
|
+
const folding = foldsOf(keptRest, read).filter((m) => m.worst !== null);
|
|
1161
|
+
const where =
|
|
1162
|
+
folding.length === 0
|
|
1163
|
+
? `no ${kept} folds on any frame of ${JSON.stringify(request.animation)} at ${request.fps} fps`
|
|
1164
|
+
: `at ${request.fps} fps these do: ${folding
|
|
1165
|
+
.map((m) => `"${m.slot}" on frame(s) [${indexRuns(m.frames.map((f) => f.index))}], worst frame ${m.worst?.index} (t=${m.worst?.time.toFixed(4)}s)`)
|
|
1166
|
+
.join('; ')}`;
|
|
1167
|
+
return { miss: `no ${kept} has a triangle folded over at ${at} of ${JSON.stringify(request.animation)} — ${where}` };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1098
1170
|
/** The world box of what `--zoom` names, over every pose of the picture. */
|
|
1099
1171
|
function zoomBox(
|
|
1100
1172
|
poser: Poser,
|
|
@@ -1167,7 +1239,9 @@ interface Gathered {
|
|
|
1167
1239
|
poses: Array<{ frame: Frame; index: number }>;
|
|
1168
1240
|
/** `render`'s framing of the build — taken only when the picture is framed as a shot. */
|
|
1169
1241
|
shot: Viewport | null;
|
|
1170
|
-
zoom: { box: WorldBox; bone?: string; slot?: string } | null;
|
|
1242
|
+
zoom: { box: WorldBox; bone?: string; slot?: string; folds?: string } | null;
|
|
1243
|
+
/** `--zoom folds` with nothing folded at the times drawn: which frames do — the refusal's words, when no build has a box. */
|
|
1244
|
+
foldsMiss: string | null;
|
|
1171
1245
|
/** The rest table's topology for every attachment a pose shows, by `slot\0attachment` — taken only when a mesh view is drawn. */
|
|
1172
1246
|
rest: Map<string, AttachmentRest> | null;
|
|
1173
1247
|
pages: Map<string, Plate>;
|
|
@@ -1202,18 +1276,35 @@ function gather(tag: 'A' | 'B', candidate: Candidate, texts: InspectTexts, reque
|
|
|
1202
1276
|
const lengths = boneLengths(texts.skeletonText);
|
|
1203
1277
|
const meshViews = request.show.some((w) => MESH_VIEWS.includes(w));
|
|
1204
1278
|
const needAttachments = request.frame.kind === 'zoom' || meshViews || request.show.includes('slots');
|
|
1279
|
+
const zoomFolds = request.frame.kind === 'zoom' && request.frame.target === ZOOM_FOLDS && plant.zoomBone === undefined;
|
|
1205
1280
|
const posed = throughPoser(candidate.choice, (poser, roster) => {
|
|
1206
|
-
|
|
1281
|
+
if (zoomFolds && (poser.bones.some((b) => b.name === ZOOM_FOLDS) || poser.slots.some((s) => s.name === ZOOM_FOLDS))) {
|
|
1282
|
+
// The word and a name in the rig would mean two boxes; which is meant is the caller's to say, as with a bone and a slot.
|
|
1283
|
+
throw new InspectError(
|
|
1284
|
+
`--zoom ${ZOOM_FOLDS} is the fold reading's box, and this skeleton also has a ${poser.bones.some((b) => b.name === ZOOM_FOLDS) ? 'bone' : 'slot'} named "${ZOOM_FOLDS}"; ` +
|
|
1285
|
+
`for that one write --zoom bone:${ZOOM_FOLDS} or --zoom slot:${ZOOM_FOLDS}, and for the folds a --viewport around the box render --folds prints`,
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
const { poses, grid } = posesOf(poser, request, needAttachments);
|
|
1207
1289
|
const shot =
|
|
1208
1290
|
request.frame.kind === 'shot' ? framingViewport(poser, request.maxSide, request.skin === undefined ? undefined : { skin: request.skin }, roster) : null;
|
|
1209
1291
|
if (request.frame.kind === 'shot' && shot === null) {
|
|
1210
1292
|
throw new InspectError('this skeleton poses no drawable attachment in any animation or in its setup pose — there is nothing to frame; name a box with --viewport');
|
|
1211
1293
|
}
|
|
1212
|
-
const
|
|
1294
|
+
const folds = zoomFolds ? foldsZoomBox(poser, poses, grid, slotsLeftOut(poser.slots, request.slotFilter, plant.ignoreSlotFilter !== true), request, plant) : null;
|
|
1295
|
+
const zoom =
|
|
1296
|
+
folds !== null
|
|
1297
|
+
? 'box' in folds
|
|
1298
|
+
? folds
|
|
1299
|
+
: null
|
|
1300
|
+
: request.frame.kind === 'zoom'
|
|
1301
|
+
? zoomBox(poser, poses, plant.zoomBone ?? request.frame.target, lengths)
|
|
1302
|
+
: null;
|
|
1303
|
+
const foldsMiss = folds !== null && 'miss' in folds ? folds.miss : null;
|
|
1213
1304
|
const rest = meshViews
|
|
1214
1305
|
? new Map(poser.rest(request.skin, poses.map((p) => p.frame.attachments ?? [])).map((r) => [restKey(r.slot, r.attachment), r]))
|
|
1215
1306
|
: null;
|
|
1216
|
-
return { poses, shot, zoom, rest, bones: poser.bones, slots: poser.slots };
|
|
1307
|
+
return { poses, shot, zoom, foldsMiss, rest, bones: poser.bones, slots: poser.slots };
|
|
1217
1308
|
});
|
|
1218
1309
|
return {
|
|
1219
1310
|
tag,
|
|
@@ -1275,6 +1366,11 @@ function view(builds: readonly Gathered[], request: InspectRequest, plant: Inspe
|
|
|
1275
1366
|
framing = { kind: 'viewport' };
|
|
1276
1367
|
} else if (request.frame.kind === 'zoom') {
|
|
1277
1368
|
const found = builds.map((b) => b.zoom).filter((z): z is NonNullable<Gathered['zoom']> => z !== null);
|
|
1369
|
+
if (found.length === 0) {
|
|
1370
|
+
// Only `--zoom folds` finds nothing without having refused already: a bone or slot that names nothing is refused while posing.
|
|
1371
|
+
const misses = builds.map((b) => (compare ? `${b.tag === 'A' ? 'the --candidate build' : 'the --compare build'}: ${b.foldsMiss ?? ''}` : (b.foldsMiss ?? '')));
|
|
1372
|
+
throw new InspectError(`--zoom ${request.frame.target}: nothing to frame — ${misses.join('; ')}`);
|
|
1373
|
+
}
|
|
1278
1374
|
const [x0, y0, x1, y1] = found.reduce<WorldBox>(
|
|
1279
1375
|
(u, z) => [Math.min(u[0], z.box[0]), Math.min(u[1], z.box[1]), Math.max(u[2], z.box[2]), Math.max(u[3], z.box[3])],
|
|
1280
1376
|
[Infinity, Infinity, -Infinity, -Infinity],
|
|
@@ -1289,7 +1385,14 @@ function view(builds: readonly Gathered[], request: InspectRequest, plant: Inspe
|
|
|
1289
1385
|
}
|
|
1290
1386
|
viewport = viewportFor(x0 - pad, y0 - pad, x1 + pad, y1 + pad, request.maxSide);
|
|
1291
1387
|
const first = found[0];
|
|
1292
|
-
framing = {
|
|
1388
|
+
framing = {
|
|
1389
|
+
kind: 'zoom',
|
|
1390
|
+
...(first?.bone === undefined ? {} : { bone: first.bone }),
|
|
1391
|
+
...(first?.slot === undefined ? {} : { slot: first.slot }),
|
|
1392
|
+
...(first?.folds === undefined ? {} : { folds: first.folds }),
|
|
1393
|
+
pad,
|
|
1394
|
+
padFrom: request.frame.pad === null ? 'default' : 'stated',
|
|
1395
|
+
};
|
|
1293
1396
|
} else {
|
|
1294
1397
|
const shots = builds.map((b) => b.shot).filter((v): v is Viewport => v !== null);
|
|
1295
1398
|
if (!compare) {
|
|
@@ -1587,7 +1690,18 @@ function drawTile(
|
|
|
1587
1690
|
};
|
|
1588
1691
|
for (const t of fill) fillTriangle(tile, [at(rest.triangles[3 * t]), at(rest.triangles[3 * t + 1]), at(rest.triangles[3 * t + 2])], [...FOLD_COLOUR.rgba] as RGBA);
|
|
1589
1692
|
if (folded.length === 0 && degenerate.length === 0 && inverted.length === 0) continue;
|
|
1590
|
-
|
|
1693
|
+
const extent = foldExtent(rest, shown.vertices, folded, setupArea(rest), plant.foldPlant);
|
|
1694
|
+
legendTile.folds.push({
|
|
1695
|
+
slot: shown.slot,
|
|
1696
|
+
attachment: shown.attachment,
|
|
1697
|
+
triangleCount: rest.triangles.length / 3,
|
|
1698
|
+
folded,
|
|
1699
|
+
degenerate,
|
|
1700
|
+
inverted,
|
|
1701
|
+
box: extent === null ? null : extent.box,
|
|
1702
|
+
area: extent === null ? 0 : extent.area,
|
|
1703
|
+
share: extent === null ? 0 : extent.share,
|
|
1704
|
+
});
|
|
1591
1705
|
}
|
|
1592
1706
|
if (first) {
|
|
1593
1707
|
const count = legendTile.folds.reduce((n, f) => n + f.folded.length, 0);
|
|
@@ -1595,7 +1709,9 @@ function drawTile(
|
|
|
1595
1709
|
notes.push(
|
|
1596
1710
|
count === 0
|
|
1597
1711
|
? `folds: no mesh${request.slotFilter === undefined ? '' : ' the filter keeps'} has a triangle folded over at this pose — nothing filled`
|
|
1598
|
-
: `folds: ${count} triangle(s) folded over across ${meshes.length} mesh(es) — ${meshes
|
|
1712
|
+
: `folds: ${count} triangle(s) folded over across ${meshes.length} mesh(es) — ${meshes
|
|
1713
|
+
.map((f) => `"${f.slot}" [${f.folded.join(', ')}]${f.box === null ? '' : ` ${extentText({ box: f.box, area: f.area, share: f.share })}`}`)
|
|
1714
|
+
.join(', ')} — every one is in the legend`,
|
|
1599
1715
|
);
|
|
1600
1716
|
const faulted = legendTile.folds.filter((f) => f.degenerate.length > 0 || f.inverted.length > 0);
|
|
1601
1717
|
if (faulted.length > 0) notes.push(`folds: setup faults, never counted — ${faulted.map((f) => `"${f.slot}" degenerate [${f.degenerate.join(', ')}] inverted [${f.inverted.join(', ')}]`).join(', ')}`);
|
package/src/meshcompare.ts
CHANGED
|
@@ -66,7 +66,11 @@
|
|
|
66
66
|
* on every walk and stepped at the declared `dt` to each frame (the oracle's
|
|
67
67
|
* `stepSchedule` rule: from the last frame, steps of `dt` while before the
|
|
68
68
|
* next, then to it), the same steps for the reference and every candidate. A
|
|
69
|
-
* nonzero `warmupSteps` is refused, never run as zero.
|
|
69
|
+
* nonzero `warmupSteps` is refused, never run as zero. The physics clock
|
|
70
|
+
* moves by `physicsClockStep` of each step, the render's clock
|
|
71
|
+
* (`PHYSICS_CLOCK_LEAD` in `src/core/raw.ts`, issue #1380), so a step
|
|
72
|
+
* lasting a whole number of a constraint's steps takes them as exact
|
|
73
|
+
* arithmetic does (`comparisonWalk`, issue #1389).
|
|
70
74
|
*
|
|
71
75
|
* ## What it never does
|
|
72
76
|
*
|
|
@@ -106,8 +110,8 @@ import {
|
|
|
106
110
|
import { areaBand, stretchSingularValues, triangleAreas } from './areaband.ts';
|
|
107
111
|
import { uvCarriers, uvCarriersWith, type Carrier, type CarrierPlant } from './meshcarriers.ts';
|
|
108
112
|
import { CoreInputError, readModel, underNoSkin, underSkin, type CompiledDocument } from './core/index.ts';
|
|
109
|
-
import { poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
|
|
110
|
-
import { sampleTime } from './core/animation.ts';
|
|
113
|
+
import { physicsClockStep, poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
|
|
114
|
+
import { sampleTime, type TimelinePlant } from './core/animation.ts';
|
|
111
115
|
import { CORE_DEFAULT_SKIN } from './core/skins.ts';
|
|
112
116
|
import { readVersion } from './package_meta.ts';
|
|
113
117
|
import { hypot2 } from './numeric.ts';
|
|
@@ -187,7 +191,9 @@ export interface MotionComparisonInput {
|
|
|
187
191
|
* Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
|
|
188
192
|
* negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
|
|
189
193
|
* before the builds are read; `null` echoed for a field left out; and the load counted as a required row, so the
|
|
190
|
-
* amplitude moves a verdict. `compareMeshesInMotion` plants none. Issue #
|
|
194
|
+
* amplitude moves a verdict. `compareMeshesInMotion` plants none. Issue #1389 adds `'frame-physics-clock'`: every
|
|
195
|
+
* build posed with the physics clock moved by the schedule's step itself, the walk before that issue
|
|
196
|
+
* (`comparisonWalk`). Issue #1323 adds `CarrierPlant`
|
|
191
197
|
* (`src/meshcarriers.ts`): a fault in resolving a sample's several carriers, planted in every `uvCarriers` the
|
|
192
198
|
* comparison reads.
|
|
193
199
|
*/
|
|
@@ -197,6 +203,7 @@ export type ComparePlant =
|
|
|
197
203
|
| 'amplitude-unvalidated'
|
|
198
204
|
| 'echo-when-unset'
|
|
199
205
|
| 'amplitude-gates-acceptance'
|
|
206
|
+
| 'frame-physics-clock'
|
|
200
207
|
| OverBoundPlant
|
|
201
208
|
| CarrierPlant;
|
|
202
209
|
|
|
@@ -594,7 +601,37 @@ function drawnOf(pose: RawPose, ref: AttachmentRef): number[] | null {
|
|
|
594
601
|
return row === undefined ? null : row.vertices;
|
|
595
602
|
}
|
|
596
603
|
|
|
597
|
-
|
|
604
|
+
/**
|
|
605
|
+
* The walk every build of a comparison is posed on (issue #1389): `poseRawAnimationEach`, reset at the animation, the
|
|
606
|
+
* physics clock moved by `physicsClockStep` of each step — the render's clock (`src/core/raw.ts`). A walk time that
|
|
607
|
+
* ends on a whole number of a physics constraint's steps leaves zero time over in exact arithmetic and a few ulps
|
|
608
|
+
* either side of zero in doubles, so with the step itself as the clock the constraint took one step too few or one
|
|
609
|
+
* too many by rounding. Measured over the clock arithmetic on 300 durations as the core reads them (float32, as Spine
|
|
610
|
+
* does: n/12 s to 20 s and tenths to 8 s): at 12 fps and `dt` 1/12, the rate `@rig-c/parts` walks, 11,248 of 44,486
|
|
611
|
+
* grid updates sit on a tie against a 60 Hz constraint and the frame clock happens to resolve every one as exact
|
|
612
|
+
* arithmetic does, while against 30, 24 and 50 Hz it mis-steps 3,274, 3,350 and 3,160 of them; at 4 fps and `dt` 1/60
|
|
613
|
+
* it mis-steps 19,876 grid and 594 irr updates against 60 Hz. The render's clock mis-steps none of any, so no time
|
|
614
|
+
* that is not a tie came within its lead of a whole step. `clock: 'frame'` is that walk, the step itself — `ComparePlant`
|
|
615
|
+
* `'frame-physics-clock'` and `MQ178`'s planted red. With no physics constraint the clock is read by nothing, so every
|
|
616
|
+
* pose is the same either way (`MQ179`); with one, the lead moves the shown lag weight by about `2⁻³⁰` of the time
|
|
617
|
+
* walked even where no step moves: on `MQ179`'s spring at parts' rate and 60 Hz, one unit of a row's sixth decimal,
|
|
618
|
+
* which can move the worst sample to a neighbour that nearly ties it.
|
|
619
|
+
*/
|
|
620
|
+
export function comparisonWalk(doc: CompiledDocument, animation: string, deltas: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void, clock: 'render' | 'frame' = 'render'): void {
|
|
621
|
+
poseRawAnimationEach(doc, animation, deltas, plant, 'animation', visit, clock === 'render' ? physicsClockStep : undefined);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* The walks `schedule` plans over the model document `model`, in walk order, each with the steps every build is posed
|
|
626
|
+
* on — `planSchedule`'s, read for the `mesh-compare` suite's `MQ178`. Internal: merely exported, so not promised; it
|
|
627
|
+
* reads the schedule without the input checks `compareMeshesInMotion` runs first.
|
|
628
|
+
*/
|
|
629
|
+
export function comparisonWalks(schedule: MotionSchedule, model: string): Array<{ animation: string; phase: 'grid' | 'irr' | null; deltas: number[] }> {
|
|
630
|
+
const doc = readModel(model, 'the model document');
|
|
631
|
+
return planSchedule(schedule, doc, doc.constraints.filter((c) => c.kind === 'physics').length).walks.map((w) => ({ animation: w.animation, phase: w.phase, deltas: [...w.deltas] }));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function poseBuild(doc: CompiledDocument, refs: readonly AttachmentRef[], frames: readonly Planned[], walks: readonly Walk[], clock: 'render' | 'frame'): Posed | { refused: string } {
|
|
598
635
|
try {
|
|
599
636
|
const view = viewOf(doc, refs[0].skin);
|
|
600
637
|
const setupPose = poseRawSetup(view);
|
|
@@ -612,12 +649,19 @@ function poseBuild(doc: CompiledDocument, refs: readonly AttachmentRef[], frames
|
|
|
612
649
|
list.push(i);
|
|
613
650
|
wanted.set(f.pose, list);
|
|
614
651
|
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
652
|
+
comparisonWalk(
|
|
653
|
+
view,
|
|
654
|
+
w.animation,
|
|
655
|
+
w.deltas,
|
|
656
|
+
{},
|
|
657
|
+
(pose, index) => {
|
|
658
|
+
const at = wanted.get(index);
|
|
659
|
+
if (at === undefined) return;
|
|
660
|
+
const drawn = refs.map((r) => drawnOf(pose, r));
|
|
661
|
+
for (const i of at) out[i] = drawn;
|
|
662
|
+
},
|
|
663
|
+
clock,
|
|
664
|
+
);
|
|
621
665
|
});
|
|
622
666
|
return { setup, frames: out, setupBones };
|
|
623
667
|
} catch (err) {
|
|
@@ -1003,11 +1047,12 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1003
1047
|
const carriersOf = (uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> =>
|
|
1004
1048
|
uvCarriersWith(uvs, triangles, samples, who, carrierPlant);
|
|
1005
1049
|
const refCarriers = order.map((i, k) => carriersOf(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
|
|
1006
|
-
const
|
|
1050
|
+
const clock = plant === 'frame-physics-clock' ? 'frame' : 'render';
|
|
1051
|
+
const posedRef = poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks, clock);
|
|
1007
1052
|
|
|
1008
1053
|
motionOf = (b: Build, isReference: boolean) => {
|
|
1009
1054
|
const carriers = isReference ? refCarriers : order.map((i, k) => carriersOf(b.meshes[i].uvs, b.meshes[i].triangles, samplesOf[k].samples, `candidate "${b.id}", attachment ${nameOf(refs[i])}`));
|
|
1010
|
-
const posed = isReference ? posedRef : poseBuild(b.doc, order.map((i) => refs[i]), frames, walks);
|
|
1055
|
+
const posed = isReference ? posedRef : poseBuild(b.doc, order.map((i) => refs[i]), frames, walks, clock);
|
|
1011
1056
|
const built: Built[] = [];
|
|
1012
1057
|
const perFrame: NonNullable<CandidateReport['perFrame']> = [];
|
|
1013
1058
|
// Issue #1315: kept for a candidate only, and only when asked — a call without the field does none of this.
|
package/src/render_core.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* the same walk handing each pose over as it is posed (`CORE_RAW_WALK`), so
|
|
31
31
|
* one pose is held at a time. Since issue #1380 the physics clock — and
|
|
32
32
|
* nothing else — moves by `physicsClockStep(1/fps)` rather than by the step
|
|
33
|
-
* (`PHYSICS_CLOCK_LEAD`
|
|
33
|
+
* (`PHYSICS_CLOCK_LEAD`, `./core/raw.ts`), in both posers: `CR03` still holds the walk
|
|
34
34
|
* under the step itself, and the loop-seam suite's `LS06` holds the two
|
|
35
35
|
* posers equal under the render's clock.
|
|
36
36
|
* - **Vertices, triangles, bones, slot colours, draw order** are the raw
|
|
@@ -100,7 +100,7 @@ import { atlasRegionLookup, parseAtlasText } from './atlas.ts';
|
|
|
100
100
|
import { pagesOfAtlas, spineFileSha256, type ModelPage } from './model.ts';
|
|
101
101
|
import { clipThrough, type ClipShape, type ShapeClipper } from './core/clipping.ts';
|
|
102
102
|
import { activeBones, CoreInputError, readModel, sourceOfDoc, underNoSkin, underSkin, type CompiledDocument, type CoreSlotRow } from './core/index.ts';
|
|
103
|
-
import { poseRawAnimationEach, poseRawSetup, type RawDrawn, type RawPose } from './core/raw.ts';
|
|
103
|
+
import { PHYSICS_CLOCK_LEAD, physicsClockStep, poseRawAnimationEach, poseRawSetup, type RawDrawn, type RawPose } from './core/raw.ts';
|
|
104
104
|
import type { TimelinePlant } from './core/animation.ts';
|
|
105
105
|
import { CORE_DEFAULT_SKIN, lookupSkins } from './core/skins.ts';
|
|
106
106
|
import { documentPageLookup, drawnRegions, meshPageUvs, readUvSequences, regionPageUvs, type DrawnRegion, type UvRegion, type UvSource } from './core/uvs.ts';
|
|
@@ -369,61 +369,10 @@ export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: read
|
|
|
369
369
|
export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit) => poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit, physicsClockStep);
|
|
370
370
|
|
|
371
371
|
/**
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* `physicsClockStep(1 / fps)`, the frame's step and `2⁻³⁰` of it more, while
|
|
375
|
-
* the animation is applied at the running sum of the frame steps as before
|
|
376
|
-
* (issue #1380).
|
|
377
|
-
*
|
|
378
|
-
* ⭐ **Why a render's physics clock is not the frame clock.** A physics
|
|
379
|
-
* constraint keeps the time it has not yet integrated (`remaining`), takes
|
|
380
|
-
* whole steps of `1 / fps` (its own `fps`, 60 by default) while at least one
|
|
381
|
-
* step remains, and shows `offset − lag·(1 − remaining / step)`, `lag` being
|
|
382
|
-
* everything this update's steps moved (`src/core/constraints_physics.ts`,
|
|
383
|
-
* *The step*). Whenever a frame lasts a whole number of physics steps — 12,
|
|
384
|
-
* 20, 30 and 60 fps against 60 Hz, and every second frame at 24 — the time
|
|
385
|
-
* left at the frame's end is zero in exact arithmetic, and in doubles it is
|
|
386
|
-
* either a few ulps above zero or a few ulps short of one whole step. Which of
|
|
387
|
-
* the two is decided by rounding, not by the motion: the shown weight is 1 or
|
|
388
|
-
* 0, so the spring is drawn one frame late or on time, and it changes regime
|
|
389
|
-
* on a single frame. Measured on the public trailer (`show`, 20 fps, 321
|
|
390
|
-
* frames): every spring jumps at frames 5, 12, 45 and 188 — the four frames
|
|
391
|
-
* that arithmetic alone predicts — and the loop's start (frame 80, on time)
|
|
392
|
-
* and end (frame 320, a frame late, which is what exact arithmetic draws)
|
|
393
|
-
* differ by 31.70 units of vertex and 10.37 of bone
|
|
394
|
-
* while every keyed bone agrees to 1.3e-12. spine-core 4.3.13 stepped the
|
|
395
|
-
* same way draws the same frames to the bit: this is the runtime under a
|
|
396
|
-
* fixed offline step, not a defect of the core's port, which is why the fix
|
|
397
|
-
* is the render's schedule and the physics is untouched.
|
|
398
|
-
*
|
|
399
|
-
* Running the clock `2⁻³⁰` fast puts the time left a little ABOVE every
|
|
400
|
-
* whole number of steps instead of on it, by `2⁻³⁰ · t` after `t` seconds:
|
|
401
|
-
* 1.5e-8 s at 16 s, against a rounding error of at most a few ulps of the
|
|
402
|
-
* clock (3.6e-15 s at 16 s) — so every tie resolves the way exact arithmetic
|
|
403
|
-
* resolves it, a step taken, and the shown weight sits at 1 − 9e-7 rather
|
|
404
|
-
* than flipping. It stays below the smallest gap a step boundary can fall
|
|
405
|
-
* into for 7.8e5 s even at 23 fps against 60 Hz (a gap of 1/1380 s), and a
|
|
406
|
-
* physics `reset` key, which restarts the time left from zero, is resolved
|
|
407
|
-
* the same way on every frame after it, because the lead is taken on every
|
|
408
|
-
* frame rather than once (read off the clock arithmetic with a reset at frame
|
|
409
|
-
* 100, 16 s at the five rates: no tie; no tree probe keys a reset under the
|
|
410
|
-
* render's clock). A pose stays a function of its frame
|
|
411
|
-
* index alone: the lead does not depend on how many frames are asked for.
|
|
412
|
-
*
|
|
413
|
-
* Rejected, measured over the same arithmetic (16 s at 12, 20, 24, 25, 30 and
|
|
414
|
-
* 60 fps): the clock as `i / fps` rather than a running sum (90 frames still
|
|
415
|
-
* flipped at 20 fps); a one-time lead of half a step (114 flipped at 24 fps,
|
|
416
|
-
* where half a step lands on the other tie); a one-time lead of any size,
|
|
417
|
-
* which a `reset` key cancels; and snapping `remaining` inside the physics,
|
|
418
|
-
* which changes the runtime's physics rather than the render's schedule and
|
|
419
|
-
* which spine-core's poser could not follow.
|
|
372
|
+
* The render's physics clock (issue #1380): defined beside the walk that takes it (`./core/raw.ts`), where the mesh
|
|
373
|
+
* comparison (`./meshcompare.ts`, issue #1389) reads the same clock without this module's closure.
|
|
420
374
|
*/
|
|
421
|
-
export
|
|
422
|
-
|
|
423
|
-
/** What one frame of `step` seconds moves a render's physics clock by — `PHYSICS_CLOCK_LEAD`; both posers step with it. */
|
|
424
|
-
export function physicsClockStep(step: number): number {
|
|
425
|
-
return step + step * PHYSICS_CLOCK_LEAD;
|
|
426
|
-
}
|
|
375
|
+
export { PHYSICS_CLOCK_LEAD, physicsClockStep };
|
|
427
376
|
|
|
428
377
|
/** What a suite passes `corePoser` in place of the poser's own parts — `PieceArrays` (`RC43`) and the raw walk (`RC44`). */
|
|
429
378
|
export interface CorePoserPlants {
|