@rig-c/core 2.67.2 → 2.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/docs/AUTHORING.md +99 -2
- package/package.json +2 -1
- package/src/render_read.ts +55 -0
- package/src/seam.ts +70 -13
package/README.md
CHANGED
|
@@ -186,6 +186,16 @@ of them: it reads whatever frames you point it at, so it runs from the installed
|
|
|
186
186
|
package on pictures of your own — which is what *Where to go next* below tells you
|
|
187
187
|
to do with it, and it is the one instrument here that can see a wrong animation.
|
|
188
188
|
|
|
189
|
+
Programmatic checked consumers can import `rig-c/render-shared`: the official
|
|
190
|
+
candidate chooser (`loadCandidate`, `throughPoser`), setup and animation samplers
|
|
191
|
+
(`sampleSetupPose`, `sampleAnimation`) and raster reads (`pageFor`, `projector`,
|
|
192
|
+
`rasterisePiece`, `renderFrame`), with their argument/result types and named
|
|
193
|
+
failures. It loads without the optional Spine runtime; a Spine input requires
|
|
194
|
+
explicitly loading `rig-c/render`, whose eager runtime dependency is unchanged.
|
|
195
|
+
The promised scope and planned first minor release are in
|
|
196
|
+
[RELEASING.md, The import surface](https://github.com/rig-c/core/blob/main/RELEASING.md#the-import-surface);
|
|
197
|
+
[AUTHORING §0.5](docs/AUTHORING.md#05-reading-a-build-from-code) shows the call chain.
|
|
198
|
+
|
|
189
199
|
### Install it into your agent
|
|
190
200
|
|
|
191
201
|
The guides under [Documentation](#documentation) also ship as
|
|
@@ -606,7 +616,7 @@ commands take it and what its default is.
|
|
|
606
616
|
| `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 |
|
|
607
617
|
| `split --rig … --motion … --slot <name> --out <dir>` | the answer to a mesh refused because its triangles are **several pieces that share no vertex** — two legs after the triangles bridging them were deleted. It reads the two specs and writes them back into an absent or empty `--out` with one mesh per piece: piece 1 stays in the slot, piece k gets a new slot `<slot>_<k>` after it, each with the original's uvs and weights unchanged, its triangles renumbered outline first where the hull rule needs it, and every deform key re-sliced to the new indices — plus `split.json`, where every original vertex and triangle went. One piece is said so and nothing is written; what it cannot carry is refused by name ([AUTHORING §3.4.11](docs/AUTHORING.md)) |
|
|
608
618
|
| `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 |
|
|
609
|
-
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--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. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — a reading, not a gate. `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene |
|
|
619
|
+
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--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. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box; `ghostA` and `ghostB` partition the same ghost by which named pair member drew it, each with its own frames, spans and worst; `gapOpen` and `gapCovered` partition `gap` by zero versus positive rendered alpha from the whole skeleton, including faint and clipped drawings ([AUTHORING §8.8](docs/AUTHORING.md#88-where-two-slots-stop-meeting--render---seam)) — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — a reading, not a gate. `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene |
|
|
610
620
|
| `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` |
|
|
611
621
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
612
622
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
package/docs/AUTHORING.md
CHANGED
|
@@ -1617,6 +1617,68 @@ written`, followed by `rigc repack: nothing was written to <out>; the work direc
|
|
|
1617
1617
|
|
|
1618
1618
|
---
|
|
1619
1619
|
|
|
1620
|
+
### 0.5 Reading a build from code
|
|
1621
|
+
|
|
1622
|
+
The named `rig-c/render-shared` entry promises the official renderer reads for
|
|
1623
|
+
checked consumers (issue #1561). It is planned for the next minor, 2.69.0; wait
|
|
1624
|
+
for the measured release before using `>=2.69.0 <3.0.0`. It requires Bun and the
|
|
1625
|
+
candidate's real files, but imports no optional Spine runtime. The existing
|
|
1626
|
+
`rig-c/render` entry still eagerly imports that runtime.
|
|
1627
|
+
|
|
1628
|
+
```ts
|
|
1629
|
+
import { readFileSync } from 'node:fs';
|
|
1630
|
+
import { join } from 'node:path';
|
|
1631
|
+
import {
|
|
1632
|
+
loadCandidate, throughPoser, sampleSetupPose, sampleAnimation,
|
|
1633
|
+
pageFor, projector, rasterisePiece, renderFrame,
|
|
1634
|
+
type Viewport,
|
|
1635
|
+
} from 'rig-c/render-shared';
|
|
1636
|
+
|
|
1637
|
+
const dir = 'build';
|
|
1638
|
+
const skeleton = join(dir, 'skeleton.json');
|
|
1639
|
+
const atlas = join(dir, 'skeleton.atlas');
|
|
1640
|
+
const candidate = loadCandidate({
|
|
1641
|
+
skeletonText: readFileSync(skeleton, 'utf8'),
|
|
1642
|
+
atlasText: readFileSync(atlas, 'utf8'),
|
|
1643
|
+
atlasDir: dir, label: skeleton,
|
|
1644
|
+
}, { skeleton, atlas }, undefined);
|
|
1645
|
+
const read = throughPoser(candidate.choice, (poser, roster) => ({
|
|
1646
|
+
setup: sampleSetupPose(poser, { bones: true, geometry: true })[0],
|
|
1647
|
+
frames: sampleAnimation(poser, 'wave', 12),
|
|
1648
|
+
skins: roster.skins,
|
|
1649
|
+
}));
|
|
1650
|
+
// The consumer supplies the world box and pixel grid it intends to measure.
|
|
1651
|
+
const viewport: Viewport = {
|
|
1652
|
+
minX: 0, minY: 0, maxX: 256, maxY: 256,
|
|
1653
|
+
scale: 1, width: 256, height: 256,
|
|
1654
|
+
};
|
|
1655
|
+
const project = projector(viewport);
|
|
1656
|
+
for (const piece of read.value.setup.pieces) {
|
|
1657
|
+
rasterisePiece(pageFor(candidate.pages, piece), piece, project, viewport,
|
|
1658
|
+
(x, y, r, g, b, a) => { /* read each covered sample */ });
|
|
1659
|
+
}
|
|
1660
|
+
const image = renderFrame(read.value.setup, candidate.pages, viewport, [0, 0, 0, 0]);
|
|
1661
|
+
```
|
|
1662
|
+
|
|
1663
|
+
The animation name and viewport above belong to the consumer; the loader does
|
|
1664
|
+
not invent them. The chooser checks the model document beside the skeleton,
|
|
1665
|
+
its `spine.sha256`, atlas placement, page files and bone/slot/animation rosters.
|
|
1666
|
+
A /3 document can be loaded with `atlasText: null` when its atlas is absent.
|
|
1667
|
+
`throughPoser` returns `poser` and `note` with its value. Keep its callback free
|
|
1668
|
+
of writes: an explicitly registered runtime fallback can rerun it.
|
|
1669
|
+
|
|
1670
|
+
The entry also exports the original `CandidateAtlasError` (missing required
|
|
1671
|
+
atlas), `CandidatePairError` (unloadable pair or missing page),
|
|
1672
|
+
`SpineRuntimeError` (input needs an unregistered or unusable Spine side),
|
|
1673
|
+
`PoserChoiceError` (a forced-core pose refusal), `SlotSubsetError` (invalid
|
|
1674
|
+
slot subset) and `CoreInputError` (core input/pose refusal). The chooser's
|
|
1675
|
+
mismatch reason remains in the refusal: no silent substitute is selected by
|
|
1676
|
+
this entry. Unknown animation and missing `pageFor` page are `Error`s naming
|
|
1677
|
+
the requested name and available names. A forced-core flag keeps the existing
|
|
1678
|
+
deferred choice check; it alone does not prove `candidate.choice.core` exists.
|
|
1679
|
+
To use a Spine export or `forced: 'spine'`, install the runtime and explicitly
|
|
1680
|
+
import `rig-c/render` first; importing `render-shared` never registers it.
|
|
1681
|
+
|
|
1620
1682
|
## 1. The two files
|
|
1621
1683
|
|
|
1622
1684
|
### 1.1 A complete minimal rig spec
|
|
@@ -8933,7 +8995,8 @@ table's vertices, and the atlas alpha:
|
|
|
8933
8995
|
- **Drawn** is the renderer painting a cell's centre at **128 of 255** or more —
|
|
8934
8996
|
the half-opacity line `--pass-through` draws outlines at, the frame's tint
|
|
8935
8997
|
included, the setup pose drawn at the same tint (so a slot faded out is not a
|
|
8936
|
-
tear: it is faded in both).
|
|
8998
|
+
tear: it is faded in both). This threshold defines the pair's band, gap and
|
|
8999
|
+
ghost; the composite coverage partition below asks a different question.
|
|
8937
9000
|
- **The band** is the cells both drawings draw at the setup pose — where the two
|
|
8938
9001
|
were painted to overlap. It has **no width of its own**: it is the overlap the
|
|
8939
9002
|
art states, dilated by nothing. A frame whose band is empty is not read; a pair
|
|
@@ -8949,8 +9012,30 @@ table's vertices, and the atlas alpha:
|
|
|
8949
9012
|
the drawing piece, carrying it back, lands in the band, and which the other,
|
|
8950
9013
|
carrying it back, finds neither drawing at at the setup pose: the overlap swung
|
|
8951
9014
|
clear of its partner.
|
|
9015
|
+
- **ghostA**, **ghostB** — that same ghost partitioned by the piece drawing the
|
|
9016
|
+
cell: `a` only or `b` only, respectively, in the flag's named pair order. On
|
|
9017
|
+
each frame `ghost.px = ghostA.px + ghostB.px` (an absent hit contributes 0),
|
|
9018
|
+
and each owner's box lies inside the union's. Reversing the pair exchanges
|
|
9019
|
+
the complete owner readings and preserves `gap` and `ghost`.
|
|
9020
|
+
- **gapOpen**, **gapCovered** — the same gap cells partitioned by the actual
|
|
9021
|
+
skeleton composite at their centres, on the same cell grid, over a transparent
|
|
9022
|
+
background: alpha **0** is open; alpha **>0** is covered. Every skeleton drawing
|
|
9023
|
+
enters, including the pair itself, with the frame's tint, UVs and clipping.
|
|
9024
|
+
Alpha **1..127 covers too**: this is not the pair's half-opacity outline test.
|
|
9025
|
+
The renderer's existing byte quantization remains in force: it emits rounded
|
|
9026
|
+
alpha only when the sampled, tinted value exceeds 0.5 on the 0..255 scale;
|
|
9027
|
+
a value that produces no rendered alpha byte is open. No display background
|
|
9028
|
+
enters. Source-over cannot erase positive alpha, so the renderer's positive
|
|
9029
|
+
emissions give exactly the nonzero alpha of its transparent composite.
|
|
9030
|
+
|
|
9031
|
+
On every frame `gap.px = gapOpen.px + gapCovered.px` (missing hit means 0).
|
|
9032
|
+
Each partition's box lies in the union's. A gap in the pair can be covered by a
|
|
9033
|
+
third drawing and need not be a visible opening. Reversing the pair preserves
|
|
9034
|
+
both gap coverage readings.
|
|
8952
9035
|
|
|
8953
9036
|
Draw order does not enter: a cell is covered or bare whichever piece is in front.
|
|
9037
|
+
An owner says which piece drew a ghost cell; it does not say which piece is over
|
|
9038
|
+
or under the other, or whether that exposure should fail a consumer's policy.
|
|
8954
9039
|
|
|
8955
9040
|
**Its reach, stated.** A piece folded right back over its partner carries the band
|
|
8956
9041
|
across the region it vacated, so a hull that wide reads the vacated cells its
|
|
@@ -8970,7 +9055,7 @@ block carries the definitions above in words.
|
|
|
8970
9055
|
| `animation` | string or `null` | — | the set's animation; `null` for a skeleton with none (its one frame is the setup pose) |
|
|
8971
9056
|
| `skin` | string | — | the skin asked for; **absent** when none was |
|
|
8972
9057
|
| `fps` | number | frames per second | the rate the set was sampled at |
|
|
8973
|
-
| `reading` | object | — | `read`, `cell`, `drawn`, `band`, `carried`, `gap`, `ghost`, `box`, `status`: the definitions, in words |
|
|
9058
|
+
| `reading` | object | — | `read`, `cell`, `drawn`, `band`, `carried`, `gap`, `ghost`, `box`, `status`, `ghostA`, `ghostB`, `gapOpen`, `gapCovered`: the definitions, in words |
|
|
8974
9059
|
| `pairs[]` | array | — | one per pair, in the flag's order |
|
|
8975
9060
|
| `pairs[].a`, `pairs[].b` | string | — | the two slots, as the flag names them |
|
|
8976
9061
|
| `pairs[].drawn` | integer | frames | frames on which both slots draw |
|
|
@@ -8979,6 +9064,8 @@ block carries the definitions above in words.
|
|
|
8979
9064
|
| `pairs[].opens` | integer[] | frame indices | every frame with a gap or a ghost, ascending |
|
|
8980
9065
|
| `pairs[].maxPx` | integer | px (cells) | the most cells of either kind on any frame; 0 when none opens |
|
|
8981
9066
|
| `pairs[].gap`, `pairs[].ghost` | object | — | one per kind, each `{ frames, spans, worst }` |
|
|
9067
|
+
| `pairs[].ghostA`, `pairs[].ghostB` | object | — | the same `{ frames, spans, worst }` shape for ghost cells drawn by `a` only and `b` only; an empty owner has `frames: []`, `spans: []`, `worst: null` |
|
|
9068
|
+
| `pairs[].gapOpen`, `pairs[].gapCovered` | object | — | the same `{ frames, spans, worst }` shape for gap cells with zero or positive rendered skeleton-composite alpha, respectively; an empty kind has `frames: []`, `spans: []`, `worst: null` |
|
|
8982
9069
|
| `<kind>.frames[]` | hit[] | — | only the frames with a cell of that kind, in frame order |
|
|
8983
9070
|
| `<kind>.spans[]` | array | — | `<kind>.frames` as runs of consecutive indices: `{ frames: [first, last], times: [first, last], maxPx }` — frame indices, seconds, px |
|
|
8984
9071
|
| `<kind>.worst` | hit or `null` | — | the frame with the most cells, the earliest on a tie; `null` when none |
|
|
@@ -8995,6 +9082,16 @@ span.times, maxPx: span.maxPx, kind }`, and one pair over one animation is
|
|
|
8995
9082
|
`opens.length` frame(s) `[opens]`, max `maxPx` px — with `status: "skip"` the case
|
|
8996
9083
|
to say SKIP for.
|
|
8997
9084
|
|
|
9085
|
+
The owner keys and their two legend entries are additive in `rigc-seam/1`.
|
|
9086
|
+
Removing just those four keys leaves the previous file's bytes after the same
|
|
9087
|
+
two-space JSON serialization and newline. The union, its spans and worst, and
|
|
9088
|
+
the printed summary retain their existing meanings; an owner may have different
|
|
9089
|
+
spans or a different worst frame from the union.
|
|
9090
|
+
Likewise `gapOpen`, `gapCovered` and their two legend entries are additive:
|
|
9091
|
+
removing only those entries leaves the previous file's bytes after the same
|
|
9092
|
+
serialization, including `gap`, `ghostA` and `ghostB`. The coverage partition may
|
|
9093
|
+
have different spans and worst frames from the union; it changes no seam policy.
|
|
9094
|
+
|
|
8998
9095
|
**Refused, nothing written:** a slot the skeleton does not have (with the slots it
|
|
8999
9096
|
does), an entry without exactly one `:`, an empty entry or name, a slot paired with
|
|
9000
9097
|
itself (a seam is between two drawings), a pair named twice in either order (the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.69.0",
|
|
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": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"./font5x7": "./tools/font5x7.ts",
|
|
12
12
|
"./transform": "./src/transform.ts",
|
|
13
13
|
"./render": "./src/render.ts",
|
|
14
|
+
"./render-shared": "./src/render_read.ts",
|
|
14
15
|
"./png": "./src/png.ts",
|
|
15
16
|
"./compile": "./src/compile.ts",
|
|
16
17
|
"./rig": "./src/rig.ts",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The promised renderer reads for checked consumers (issue #1561).
|
|
3
|
+
*
|
|
4
|
+
* This entry registers no Spine side. Rigc builds use the official candidate
|
|
5
|
+
* chooser and core poser; inputs needing Spine are refused by SpineRuntimeError
|
|
6
|
+
* unless the consumer has explicitly loaded rig-c/render. That entry keeps its
|
|
7
|
+
* eager runtime import and registers the same implementation this seam uses.
|
|
8
|
+
* Only the agreed reads, their types and their existing failure classes belong
|
|
9
|
+
* here. The broader implementation module is not the public contract.
|
|
10
|
+
*/
|
|
11
|
+
export {
|
|
12
|
+
loadCandidate,
|
|
13
|
+
throughPoser,
|
|
14
|
+
sampleAnimation,
|
|
15
|
+
sampleSetupPose,
|
|
16
|
+
pageFor,
|
|
17
|
+
projector,
|
|
18
|
+
rasterisePiece,
|
|
19
|
+
renderFrame,
|
|
20
|
+
CandidateAtlasError,
|
|
21
|
+
CandidatePairError,
|
|
22
|
+
PoserChoiceError,
|
|
23
|
+
SlotSubsetError,
|
|
24
|
+
} from './render_shared.ts';
|
|
25
|
+
export { SpineRuntimeError } from './spine_side.ts';
|
|
26
|
+
export { CoreInputError } from './core/index.ts';
|
|
27
|
+
export type {
|
|
28
|
+
Candidate,
|
|
29
|
+
SkeletonFacts,
|
|
30
|
+
PoserChoice,
|
|
31
|
+
PoserName,
|
|
32
|
+
MakeCorePoser,
|
|
33
|
+
Poser,
|
|
34
|
+
Posed,
|
|
35
|
+
PoseSource,
|
|
36
|
+
PoseOptions,
|
|
37
|
+
DrawOptions,
|
|
38
|
+
SkinRoster,
|
|
39
|
+
SlotSubset,
|
|
40
|
+
Frame,
|
|
41
|
+
BoneSnapshot,
|
|
42
|
+
AttachmentPose,
|
|
43
|
+
AttachmentRest,
|
|
44
|
+
Piece,
|
|
45
|
+
PieceCommon,
|
|
46
|
+
PieceTexture,
|
|
47
|
+
UvWindow,
|
|
48
|
+
Quad,
|
|
49
|
+
Mesh,
|
|
50
|
+
ClipSource,
|
|
51
|
+
Viewport,
|
|
52
|
+
EmitPixel,
|
|
53
|
+
} from './render_shared.ts';
|
|
54
|
+
export type { Plate, RGBA } from '../tools/plate.ts';
|
|
55
|
+
export type { EnvironmentStep, EnvironmentChange } from './environment.ts';
|
package/src/seam.ts
CHANGED
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
*
|
|
57
57
|
* Each frame's figures are the cell counts (`px`), the cell size, their area,
|
|
58
58
|
* and the world box of the counted cells (each cell's whole square).
|
|
59
|
+
* The pair's gap is partitioned into gapOpen (the transparent skeleton frame
|
|
60
|
+
* has alpha 0 there) and gapCovered (positive rendered alpha from any piece,
|
|
61
|
+
* including a faint pair member). This does not change the half-opacity pair
|
|
62
|
+
* predicate: another drawing can cover what the pair no longer covers.
|
|
59
63
|
*
|
|
60
64
|
* Known reach, stated rather than discovered: a piece folded right back over its
|
|
61
65
|
* partner carries the band across the region it vacated, so a hull that wide
|
|
@@ -71,13 +75,13 @@
|
|
|
71
75
|
import type { Plate } from '../tools/plate.ts';
|
|
72
76
|
import { indexRuns } from './folds.ts';
|
|
73
77
|
import { DRAWN_ALPHA, drawnCells, drawsAt, sameVertices, texelSize } from './passthrough.ts';
|
|
74
|
-
import type
|
|
78
|
+
import { type AttachmentPose, type AttachmentRest, type Piece, projector, rasterisePiece, type Viewport } from './render_shared.ts';
|
|
75
79
|
|
|
76
80
|
/** The reading's file name inside an animation's frame directory, and its format tag. */
|
|
77
81
|
export const SEAM_FILE = 'seam.json';
|
|
78
82
|
export const SEAM_SPEC = 'rigc-seam/1';
|
|
79
83
|
|
|
80
|
-
/** One pair as the flag names it.
|
|
84
|
+
/** One pair as the flag names it. Reversing it preserves the union and exchanges the ghost owners. */
|
|
81
85
|
export interface SeamPair {
|
|
82
86
|
a: string;
|
|
83
87
|
b: string;
|
|
@@ -176,6 +180,14 @@ export interface SeamPairReading {
|
|
|
176
180
|
maxPx: number;
|
|
177
181
|
gap: SeamKindReading;
|
|
178
182
|
ghost: SeamKindReading;
|
|
183
|
+
/** Ghost cells drawn by `a` only; together with ghostB, an exact partition of ghost. */
|
|
184
|
+
ghostA: SeamKindReading;
|
|
185
|
+
/** Ghost cells drawn by `b` only. Ownership is the flag's order, not draw order. */
|
|
186
|
+
ghostB: SeamKindReading;
|
|
187
|
+
/** Gap cells whose skeleton composite has no rendered alpha at the cell centre. */
|
|
188
|
+
gapOpen: SeamKindReading;
|
|
189
|
+
/** Gap cells whose skeleton composite has positive rendered alpha, even below half opacity. */
|
|
190
|
+
gapCovered: SeamKindReading;
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
/** `seam.json`: one frame set's reading, every pair the flag names, in the flag's order. */
|
|
@@ -186,12 +198,12 @@ export interface SeamFile {
|
|
|
186
198
|
skin?: string;
|
|
187
199
|
fps: number;
|
|
188
200
|
/** What the numbers mean, stated in the file so a reader does not need this module. */
|
|
189
|
-
reading: { read: string; cell: string; drawn: string; band: string; carried: string; gap: string; ghost: string; box: string; status: string };
|
|
201
|
+
reading: { read: string; cell: string; drawn: string; band: string; carried: string; gap: string; ghost: string; box: string; status: string; ghostA: string; ghostB: string; gapOpen: string; gapCovered: string };
|
|
190
202
|
pairs: SeamPairReading[];
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
/**
|
|
194
|
-
* A planted defect, for the suite's mutants only (`
|
|
206
|
+
* A planted defect, for the suite's mutants only (`SE` controls in
|
|
195
207
|
* `selftest.ts`). Production passes none.
|
|
196
208
|
*/
|
|
197
209
|
export interface SeamPlant {
|
|
@@ -203,6 +215,10 @@ export interface SeamPlant {
|
|
|
203
215
|
bothViews?: boolean;
|
|
204
216
|
/** Count a ghost cell wherever a piece draws what its partner's view had none of, not only from the band. */
|
|
205
217
|
ghostAnyArt?: boolean;
|
|
218
|
+
/** Attribute every ghost to a, regardless of which piece draws it. */
|
|
219
|
+
ghostOnlyA?: boolean;
|
|
220
|
+
/** Wrong composite readings: half opacity, only the pair, or every gap forced to one kind. */
|
|
221
|
+
gapCoverage?: 'half' | 'pair' | 'open' | 'covered';
|
|
206
222
|
/** Report a pair with no band as read, rather than as a SKIP. */
|
|
207
223
|
skipAsRead?: boolean;
|
|
208
224
|
/** Count nothing. */
|
|
@@ -347,8 +363,28 @@ interface Cells {
|
|
|
347
363
|
|
|
348
364
|
const restPiece = (side: Side): Piece => ({ ...side.piece, world: Array.from(side.rest) });
|
|
349
365
|
|
|
350
|
-
/**
|
|
351
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Positive alpha of the transparent skeleton composite, on the pair's cell grid.
|
|
368
|
+
* The renderer emits rounded alpha bytes through this same traversal, including
|
|
369
|
+
* tint, UV windows and already-clipped pieces. Source-over cannot erase positive
|
|
370
|
+
* alpha: OR of positive emissions is exactly composite alpha > 0. No background
|
|
371
|
+
* enters, and the pair's half-opacity outline is not this test.
|
|
372
|
+
*/
|
|
373
|
+
function frameAlphaCells(pieces: readonly Piece[], pages: ReadonlyMap<string, Plate>, viewport: Viewport, half: boolean): Uint8Array {
|
|
374
|
+
const cells = new Uint8Array(viewport.width * viewport.height);
|
|
375
|
+
const project = projector(viewport);
|
|
376
|
+
for (const piece of pieces) {
|
|
377
|
+
const page = pages.get(piece.page);
|
|
378
|
+
if (page === undefined) throw new SeamError(`slot "${piece.slot}" samples atlas page "${piece.page}", which is not among [${[...pages.keys()].join(', ')}]`);
|
|
379
|
+
rasterisePiece(page, piece, project, viewport, (x, y, _r, _g, _b, alpha) => {
|
|
380
|
+
if (half ? alpha >= DRAWN_ALPHA : alpha > 0) cells[y * viewport.width + x] = 1;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return cells;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** One frame's counts, or `null` when the two drawings share no cell at the setup pose. See the module comment. */
|
|
387
|
+
function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: SeamFrame, pages: ReadonlyMap<string, Plate>): { gap: Cells; ghost: Cells; ghostA: Cells; ghostB: Cells; gapOpen: Cells; gapCovered: Cells } | null {
|
|
352
388
|
// One grid over both drawings at the setup pose and at the frame, on multiples of the cell from the world origin.
|
|
353
389
|
let [x0, y0, x1, y1] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
354
390
|
for (const xy of [A.rest, B.rest, A.piece.world, B.piece.world]) {
|
|
@@ -401,6 +437,11 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant): { gap: Cel
|
|
|
401
437
|
const composite = (p: [number, number] | null): boolean => drawnAtRest(p, restA, A.page) || drawnAtRest(p, restB, B.page);
|
|
402
438
|
const gap: Cells = { px: 0, box: null };
|
|
403
439
|
const ghost: Cells = { px: 0, box: null };
|
|
440
|
+
const ghostA: Cells = { px: 0, box: null };
|
|
441
|
+
const ghostB: Cells = { px: 0, box: null };
|
|
442
|
+
const gapOpen: Cells = { px: 0, box: null };
|
|
443
|
+
const gapCovered: Cells = { px: 0, box: null };
|
|
444
|
+
let alphaCells: Uint8Array | null = null;
|
|
404
445
|
const take = (into: Cells, qx: number, qy: number): void => {
|
|
405
446
|
into.px++;
|
|
406
447
|
const h = cell / 2;
|
|
@@ -410,7 +451,7 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant): { gap: Cel
|
|
|
410
451
|
into.box[2] = Math.max(into.box[2], qx + h);
|
|
411
452
|
into.box[3] = Math.max(into.box[3], qy + h);
|
|
412
453
|
};
|
|
413
|
-
if (plant.never) return { gap, ghost };
|
|
454
|
+
if (plant.never) return { gap, ghost, ghostA, ghostB, gapOpen, gapCovered };
|
|
414
455
|
const inBand = (p: [number, number] | null): boolean => p !== null && p[0] >= bx0 - cell / 2 && p[0] <= bx1 + cell / 2 && p[1] >= by0 - cell / 2 && p[1] <= by1 + cell / 2;
|
|
415
456
|
for (let py = 0; py < height; py++) {
|
|
416
457
|
for (let px = 0; px < width; px++) {
|
|
@@ -423,7 +464,12 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant): { gap: Cel
|
|
|
423
464
|
if (!plant.noHull && !inHull(hull, qx, qy)) continue;
|
|
424
465
|
const fromA = composite(back(A, posedIdsA, k, qx, qy));
|
|
425
466
|
const fromB = composite(back(B, posedIdsB, k, qx, qy));
|
|
426
|
-
if (plant.bothViews ? fromA && fromB : fromA || fromB)
|
|
467
|
+
if (plant.bothViews ? fromA && fromB : fromA || fromB) {
|
|
468
|
+
take(gap, qx, qy);
|
|
469
|
+
const covered = plant.gapCoverage === 'covered' || (plant.gapCoverage !== 'open' &&
|
|
470
|
+
(alphaCells ??= frameAlphaCells(plant.gapCoverage === 'pair' ? [A.piece, B.piece] : frame.pieces, pages, grid.viewport, plant.gapCoverage === 'half'))[k] !== 0);
|
|
471
|
+
take(covered ? gapCovered : gapOpen, qx, qy);
|
|
472
|
+
}
|
|
427
473
|
continue;
|
|
428
474
|
}
|
|
429
475
|
if (a && b) continue;
|
|
@@ -432,10 +478,13 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant): { gap: Cel
|
|
|
432
478
|
const otherRest = a ? restB : restA;
|
|
433
479
|
const p = back(mine, myIds, k, qx, qy);
|
|
434
480
|
if (!plant.ghostAnyArt && !(inBand(p) && drawnAtRest(p, myRest, mine.page) && drawnAtRest(p, otherRest, other.page))) continue;
|
|
435
|
-
if (!composite(back(other, otherIds, k, qx, qy)))
|
|
481
|
+
if (!composite(back(other, otherIds, k, qx, qy))) {
|
|
482
|
+
take(ghost, qx, qy);
|
|
483
|
+
take(a || plant.ghostOnlyA ? ghostA : ghostB, qx, qy);
|
|
484
|
+
}
|
|
436
485
|
}
|
|
437
486
|
}
|
|
438
|
-
return { gap, ghost };
|
|
487
|
+
return { gap, ghost, ghostA, ghostB, gapOpen, gapCovered };
|
|
439
488
|
}
|
|
440
489
|
|
|
441
490
|
const restKey = (slot: string, attachment: string): string => `${slot}\u0000${attachment}`;
|
|
@@ -494,6 +543,10 @@ export function seamsOf(
|
|
|
494
543
|
let banded = 0;
|
|
495
544
|
const gaps: SeamHit[] = [];
|
|
496
545
|
const ghosts: SeamHit[] = [];
|
|
546
|
+
const ghostsA: SeamHit[] = [];
|
|
547
|
+
const ghostsB: SeamHit[] = [];
|
|
548
|
+
const openGaps: SeamHit[] = [];
|
|
549
|
+
const coveredGaps: SeamHit[] = [];
|
|
497
550
|
for (const frame of frames) {
|
|
498
551
|
const A = sideOf(frame, a);
|
|
499
552
|
const B = sideOf(frame, b);
|
|
@@ -504,10 +557,10 @@ export function seamsOf(
|
|
|
504
557
|
const flat = cellA === null ? a : cellB === null ? b : null;
|
|
505
558
|
if (cellA === null || cellB === null) throw new SeamError(`frame ${frame.index}: slot "${flat}" draws no area at its setup pose, so it has no texel to grid the seam by`);
|
|
506
559
|
const cell = Math.max(cellA, cellB);
|
|
507
|
-
const read = readFrame(A, B, cell, plant);
|
|
560
|
+
const read = readFrame(A, B, cell, plant, frame, pages);
|
|
508
561
|
if (read === null) continue;
|
|
509
562
|
banded++;
|
|
510
|
-
for (const [cells, into] of [[read.gap, gaps], [read.ghost, ghosts]] as const) {
|
|
563
|
+
for (const [cells, into] of [[read.gap, gaps], [read.ghost, ghosts], [read.ghostA, ghostsA], [read.ghostB, ghostsB], [read.gapOpen, openGaps], [read.gapCovered, coveredGaps]] as const) {
|
|
511
564
|
if (cells.px > 0 && cells.box !== null) into.push({ index: frame.index, time: frame.time, px: cells.px, cell, area: cells.px * cell * cell, box: cells.box });
|
|
512
565
|
}
|
|
513
566
|
}
|
|
@@ -516,7 +569,7 @@ export function seamsOf(
|
|
|
516
569
|
const opens = [...new Set([...gaps, ...ghosts].map((f) => f.index))].sort((x, y) => x - y);
|
|
517
570
|
const maxPx = Math.max(0, ...gaps.map((f) => f.px), ...ghosts.map((f) => f.px));
|
|
518
571
|
const status = banded === 0 && !plant.skipAsRead ? 'skip' : 'read';
|
|
519
|
-
return { a, b, drawn, banded, status, opens, maxPx, gap, ghost };
|
|
572
|
+
return { a, b, drawn, banded, status, opens, maxPx, gap, ghost, ghostA: kindOf(ghostsA), ghostB: kindOf(ghostsB), gapOpen: kindOf(openGaps), gapCovered: kindOf(coveredGaps) };
|
|
520
573
|
});
|
|
521
574
|
}
|
|
522
575
|
|
|
@@ -532,6 +585,10 @@ export const SEAM_READING: SeamFile['reading'] = {
|
|
|
532
585
|
ghost: "a ghost cell is drawn at the frame by one piece and not the other; the drawing piece, carrying it back, lands it in the band, and the other piece, carrying it back, finds neither drawing there at the setup pose",
|
|
533
586
|
box: "a frame's box is [minX, minY, maxX, maxY] in world units (y up) over the whole squares of that kind's counted cells",
|
|
534
587
|
status: 'a pair is skip when no frame is read — the two never draw together, or never share a cell at the setup pose — which is nothing measured, not a seam that held',
|
|
588
|
+
ghostA: 'the ghost cells drawn by a only, with the same frames, spans and worst shape as ghost; ghostA and ghostB partition ghost on every frame',
|
|
589
|
+
ghostB: 'the ghost cells drawn by b only; a and b are the named pair order, not which piece is in front — reversing the pair exchanges ghostA and ghostB',
|
|
590
|
+
gapOpen: 'the gap cells whose centre has alpha 0 in the transparent frame composite of every skeleton drawing, using the renderer on the same cell grid; gapOpen and gapCovered partition gap',
|
|
591
|
+
gapCovered: 'the gap cells whose centre has positive rendered alpha from any skeleton drawing, including the pair; alpha 1..127 covers too — this is not the pair\'s half-opacity test, and no presentation background enters',
|
|
535
592
|
};
|
|
536
593
|
|
|
537
594
|
/** The file for one frame set. */
|