@rig-c/core 2.67.2 → 2.68.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 +1 -1
- package/docs/AUTHORING.md +37 -2
- package/package.json +1 -1
- package/src/seam.ts +70 -13
package/README.md
CHANGED
|
@@ -606,7 +606,7 @@ commands take it and what its default is.
|
|
|
606
606
|
| `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
607
|
| `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
608
|
| `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 |
|
|
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; `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
610
|
| `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
611
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
612
612
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
package/docs/AUTHORING.md
CHANGED
|
@@ -8933,7 +8933,8 @@ table's vertices, and the atlas alpha:
|
|
|
8933
8933
|
- **Drawn** is the renderer painting a cell's centre at **128 of 255** or more —
|
|
8934
8934
|
the half-opacity line `--pass-through` draws outlines at, the frame's tint
|
|
8935
8935
|
included, the setup pose drawn at the same tint (so a slot faded out is not a
|
|
8936
|
-
tear: it is faded in both).
|
|
8936
|
+
tear: it is faded in both). This threshold defines the pair's band, gap and
|
|
8937
|
+
ghost; the composite coverage partition below asks a different question.
|
|
8937
8938
|
- **The band** is the cells both drawings draw at the setup pose — where the two
|
|
8938
8939
|
were painted to overlap. It has **no width of its own**: it is the overlap the
|
|
8939
8940
|
art states, dilated by nothing. A frame whose band is empty is not read; a pair
|
|
@@ -8949,8 +8950,30 @@ table's vertices, and the atlas alpha:
|
|
|
8949
8950
|
the drawing piece, carrying it back, lands in the band, and which the other,
|
|
8950
8951
|
carrying it back, finds neither drawing at at the setup pose: the overlap swung
|
|
8951
8952
|
clear of its partner.
|
|
8953
|
+
- **ghostA**, **ghostB** — that same ghost partitioned by the piece drawing the
|
|
8954
|
+
cell: `a` only or `b` only, respectively, in the flag's named pair order. On
|
|
8955
|
+
each frame `ghost.px = ghostA.px + ghostB.px` (an absent hit contributes 0),
|
|
8956
|
+
and each owner's box lies inside the union's. Reversing the pair exchanges
|
|
8957
|
+
the complete owner readings and preserves `gap` and `ghost`.
|
|
8958
|
+
- **gapOpen**, **gapCovered** — the same gap cells partitioned by the actual
|
|
8959
|
+
skeleton composite at their centres, on the same cell grid, over a transparent
|
|
8960
|
+
background: alpha **0** is open; alpha **>0** is covered. Every skeleton drawing
|
|
8961
|
+
enters, including the pair itself, with the frame's tint, UVs and clipping.
|
|
8962
|
+
Alpha **1..127 covers too**: this is not the pair's half-opacity outline test.
|
|
8963
|
+
The renderer's existing byte quantization remains in force: it emits rounded
|
|
8964
|
+
alpha only when the sampled, tinted value exceeds 0.5 on the 0..255 scale;
|
|
8965
|
+
a value that produces no rendered alpha byte is open. No display background
|
|
8966
|
+
enters. Source-over cannot erase positive alpha, so the renderer's positive
|
|
8967
|
+
emissions give exactly the nonzero alpha of its transparent composite.
|
|
8968
|
+
|
|
8969
|
+
On every frame `gap.px = gapOpen.px + gapCovered.px` (missing hit means 0).
|
|
8970
|
+
Each partition's box lies in the union's. A gap in the pair can be covered by a
|
|
8971
|
+
third drawing and need not be a visible opening. Reversing the pair preserves
|
|
8972
|
+
both gap coverage readings.
|
|
8952
8973
|
|
|
8953
8974
|
Draw order does not enter: a cell is covered or bare whichever piece is in front.
|
|
8975
|
+
An owner says which piece drew a ghost cell; it does not say which piece is over
|
|
8976
|
+
or under the other, or whether that exposure should fail a consumer's policy.
|
|
8954
8977
|
|
|
8955
8978
|
**Its reach, stated.** A piece folded right back over its partner carries the band
|
|
8956
8979
|
across the region it vacated, so a hull that wide reads the vacated cells its
|
|
@@ -8970,7 +8993,7 @@ block carries the definitions above in words.
|
|
|
8970
8993
|
| `animation` | string or `null` | — | the set's animation; `null` for a skeleton with none (its one frame is the setup pose) |
|
|
8971
8994
|
| `skin` | string | — | the skin asked for; **absent** when none was |
|
|
8972
8995
|
| `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 |
|
|
8996
|
+
| `reading` | object | — | `read`, `cell`, `drawn`, `band`, `carried`, `gap`, `ghost`, `box`, `status`, `ghostA`, `ghostB`, `gapOpen`, `gapCovered`: the definitions, in words |
|
|
8974
8997
|
| `pairs[]` | array | — | one per pair, in the flag's order |
|
|
8975
8998
|
| `pairs[].a`, `pairs[].b` | string | — | the two slots, as the flag names them |
|
|
8976
8999
|
| `pairs[].drawn` | integer | frames | frames on which both slots draw |
|
|
@@ -8979,6 +9002,8 @@ block carries the definitions above in words.
|
|
|
8979
9002
|
| `pairs[].opens` | integer[] | frame indices | every frame with a gap or a ghost, ascending |
|
|
8980
9003
|
| `pairs[].maxPx` | integer | px (cells) | the most cells of either kind on any frame; 0 when none opens |
|
|
8981
9004
|
| `pairs[].gap`, `pairs[].ghost` | object | — | one per kind, each `{ frames, spans, worst }` |
|
|
9005
|
+
| `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` |
|
|
9006
|
+
| `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
9007
|
| `<kind>.frames[]` | hit[] | — | only the frames with a cell of that kind, in frame order |
|
|
8983
9008
|
| `<kind>.spans[]` | array | — | `<kind>.frames` as runs of consecutive indices: `{ frames: [first, last], times: [first, last], maxPx }` — frame indices, seconds, px |
|
|
8984
9009
|
| `<kind>.worst` | hit or `null` | — | the frame with the most cells, the earliest on a tie; `null` when none |
|
|
@@ -8995,6 +9020,16 @@ span.times, maxPx: span.maxPx, kind }`, and one pair over one animation is
|
|
|
8995
9020
|
`opens.length` frame(s) `[opens]`, max `maxPx` px — with `status: "skip"` the case
|
|
8996
9021
|
to say SKIP for.
|
|
8997
9022
|
|
|
9023
|
+
The owner keys and their two legend entries are additive in `rigc-seam/1`.
|
|
9024
|
+
Removing just those four keys leaves the previous file's bytes after the same
|
|
9025
|
+
two-space JSON serialization and newline. The union, its spans and worst, and
|
|
9026
|
+
the printed summary retain their existing meanings; an owner may have different
|
|
9027
|
+
spans or a different worst frame from the union.
|
|
9028
|
+
Likewise `gapOpen`, `gapCovered` and their two legend entries are additive:
|
|
9029
|
+
removing only those entries leaves the previous file's bytes after the same
|
|
9030
|
+
serialization, including `gap`, `ghostA` and `ghostB`. The coverage partition may
|
|
9031
|
+
have different spans and worst frames from the union; it changes no seam policy.
|
|
9032
|
+
|
|
8998
9033
|
**Refused, nothing written:** a slot the skeleton does not have (with the slots it
|
|
8999
9034
|
does), an entry without exactly one `:`, an empty entry or name, a slot paired with
|
|
9000
9035
|
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.68.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": {
|
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. */
|