@rig-c/core 2.83.0 → 2.84.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/docs/AUTHORING.md +42 -2
- package/package.json +1 -1
- package/src/cli/shared.ts +1 -1
- package/src/seam.ts +77 -7
package/docs/AUTHORING.md
CHANGED
|
@@ -9186,7 +9186,7 @@ inside it, and a second copy of the leg on the same bone):
|
|
|
9186
9186
|
bun cli.ts render --candidate path/to/spine --animation swing --seam skirt:leg,leg_front:leg [--skin …] [--fps 12]
|
|
9187
9187
|
# rigc render
|
|
9188
9188
|
# One seam line per pair, including no gap/no ghost or SKIP, followed by
|
|
9189
|
-
# exposure "<slot>"
|
|
9189
|
+
# exposure "<slot>" and joint contact: joined/separated/unavailable frames.
|
|
9190
9190
|
```
|
|
9191
9191
|
|
|
9192
9192
|
The skirt swinging off the leg opens background between the leg's right edge and
|
|
@@ -9241,6 +9241,33 @@ table's vertices, and the atlas alpha:
|
|
|
9241
9241
|
The rectangle controls derive eight real tear cells; a notched branch derives
|
|
9242
9242
|
sixteen adjacent background cells excluded by this rule, including when both
|
|
9243
9243
|
kinds occur in the same frame.
|
|
9244
|
+
- **jointContact** — a separate observation of **pair material connectivity**,
|
|
9245
|
+
on that same half-opacity texel grid. It requires a setup band and exactly
|
|
9246
|
+
one nonempty edge-connected material component for each named drawing, at
|
|
9247
|
+
both setup and frame. `joined` means the two posed components overlap or
|
|
9248
|
+
share a cell edge; `separated` means their union is disconnected. Sharing
|
|
9249
|
+
only a corner is not edge contact. Every sampled frame is present, including
|
|
9250
|
+
`unavailable` with a reason when a member is absent, the setup band is empty,
|
|
9251
|
+
material is empty or has multiple islands, or a named slot draws multiple
|
|
9252
|
+
fragments. No samples yields an empty list, not a successful observation.
|
|
9253
|
+
|
|
9254
|
+
**Raw gap measures swept support, not certified tearing.** An attached limb
|
|
9255
|
+
can rotate well away from its parent and leave an external crescent of
|
|
9256
|
+
background inside that support: `gap` can be positive while `jointContact`
|
|
9257
|
+
is `joined`. Removing enough proximal material can leave initial overlap but
|
|
9258
|
+
disconnect the same two drawings under the same attachment maps; that is
|
|
9259
|
+
operational material separation, reported as `separated`. This uses no pivot
|
|
9260
|
+
radius, anatomical assumption or new opacity threshold. The synthetic
|
|
9261
|
+
rectangular controls use an independent endpoint set oracle and actual gated
|
|
9262
|
+
builds rendered through both posers.
|
|
9263
|
+
|
|
9264
|
+
Contact does **not** infer anatomical joint intent. A joined pair can have a
|
|
9265
|
+
local notch or hole, including a genuine local tear while other material
|
|
9266
|
+
remains connected; contact cannot classify individual gap cells as harmless
|
|
9267
|
+
background or tearing. A separated pair need not be artistically wrong and
|
|
9268
|
+
can have zero raw gap outside the existing swept support. The grid cannot
|
|
9269
|
+
resolve sub-cell contact. Other skeleton drawings and draw order do not
|
|
9270
|
+
enter this pair-material observation. `unavailable` is never zero or PASS.
|
|
9244
9271
|
- **ghost** — a cell one piece draws at the frame and the other does not, which
|
|
9245
9272
|
the drawing piece, carrying it back, lands in the band, and which the other,
|
|
9246
9273
|
carrying it back, finds neither drawing at the setup pose: the overlap swung
|
|
@@ -9336,6 +9363,7 @@ block carries the definitions above in words.
|
|
|
9336
9363
|
| `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` |
|
|
9337
9364
|
| `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` |
|
|
9338
9365
|
| `pairs[].exposureA`, `pairs[].exposureB` | object | — | `{ frames, spans, worst }` for the whole named slot’s positive-alpha cells on otherwise zero-alpha skeleton background; measured independently of the band, including SKIP pairs; not part of `opens` or `maxPx` |
|
|
9366
|
+
| `pairs[].jointContact.frames[]` | object | seconds for `time` | `{ index, time, status, reason }` for every sampled frame; `status` is `joined`, `separated` or `unavailable`; `reason` is `null` for measured contact and a named explanation for unavailable contact. Independent of raw gap/ghost, `opens` and `maxPx` |
|
|
9339
9367
|
| `<kind>.frames[]` | hit[] | — | only the frames with a cell of that kind, in frame order |
|
|
9340
9368
|
| `<kind>.spans[]` | array | — | `<kind>.frames` as runs of consecutive indices: `{ frames: [first, last], times: [first, last], maxPx }` — frame indices, seconds, px |
|
|
9341
9369
|
| `<kind>.worst` | hit or `null` | — | the frame with the most cells, the earliest on a tie; `null` when none |
|
|
@@ -9362,6 +9390,18 @@ Clean stationary/shared-motion pair fields remain unchanged apart from exposure;
|
|
|
9362
9390
|
the `reading` legend states the revised definitions. Historical measurements
|
|
9363
9391
|
above pin their old reader and must not be treated as current measurements.
|
|
9364
9392
|
|
|
9393
|
+
The subsequent `jointContact` addition preserves **all** existing pair fields,
|
|
9394
|
+
raw gap/coverage/ghost/exposure numbers, `opens`, `maxPx` and their definitions.
|
|
9395
|
+
It appends one pair field, a legend entry and a contact clause to the printed
|
|
9396
|
+
line; `rigc-seam/1` is unchanged. Readers accepting additive keys can keep their
|
|
9397
|
+
existing policy. In particular, a consumer using raw `gapOpen` plus an owned
|
|
9398
|
+
ghost as its policy still uses those same numbers; adding contact does not
|
|
9399
|
+
change that verdict. Adopting material contact as a separate consumer policy
|
|
9400
|
+
requires an explicit choice. Readers comparing entire JSON objects or exact
|
|
9401
|
+
console lines must account for the added fields and clause. `stays closed` and
|
|
9402
|
+
`opens` in the existing clause continue to describe raw gap/ghost; use the
|
|
9403
|
+
separate contact clause for operational material separation.
|
|
9404
|
+
|
|
9365
9405
|
**Refused, nothing written:** a slot the skeleton does not have (with the slots it
|
|
9366
9406
|
does), an entry without exactly one `:`, an empty entry or name, a slot paired with
|
|
9367
9407
|
itself (a seam is between two drawings), a pair named twice in either order (the
|
|
@@ -12577,7 +12617,7 @@ pixel (`IN09`).
|
|
|
12577
12617
|
| 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 |
|
|
12578
12618
|
| 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 |
|
|
12579
12619
|
| 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 |
|
|
12580
|
-
| Does a
|
|
12620
|
+
| Does a pair lose material contact in this animation? | `render --seam <a>:<b> --animation <name>` (§8.8) | Raw swept-support gap and visible ghost counts, plus separate pair material contact: joined, separated or explicitly unavailable. Gap alone is not certified tearing; contact does not infer anatomical intent or rule out local notches. `seam.json` records the frames and times. A reading, not a verdict — the exit status never moves |
|
|
12581
12621
|
| Which slot covers which in this animation? | `render --overlaps --animation <name>` (§8.9) | One `OVERLAP "<under>" under "<over>"` line per pair that moves: the frames it covers on, as runs, and the most pixels it covers and the frame of them; one line counting the layering the motion does not move. `overlaps.json` carries every frame's pairs with their pixel count and box, and `overlaps-f<NNNN>.png` draws each covered frame's contours, the covered part dashed. A reading, not a verdict — the draw-order key is the remedy |
|
|
12582
12622
|
| 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 |
|
|
12583
12623
|
| 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 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.84.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/cli/shared.ts
CHANGED
|
@@ -1073,7 +1073,7 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1073
1073
|
'per pair, the frames with a gap — background inside the carried overlap hull and a local sweep of the same setup band cell through both drawings — ' +
|
|
1074
1074
|
'and the frames with a visible ghost after draw order — the band of one drawing showing where, from its partner\'s point of view, nothing was drawn at the setup pose — each with ' +
|
|
1075
1075
|
`its cell count, area and world box. Cells are one texel of the coarser drawing, drawn at ${DRAWN_ALPHA} of 255 or more. One line per pair; a pair whose ` +
|
|
1076
|
-
'drawings share no cell at the setup pose is a SKIP. exposureA/B count each named slot’s positive-alpha cells on otherwise empty skeleton background, even without a band or partner. A reading, not a gate: the exit status never moves. An unknown slot, a slot with itself, ' +
|
|
1076
|
+
'drawings share no cell at the setup pose is a SKIP. Raw gap measures swept support, not certified tearing. jointContact separately reports pair material connectivity: joined, separated, or unavailable with a reason; it does not infer anatomical joint intent or rule out local notches. exposureA/B count each named slot’s positive-alpha cells on otherwise empty skeleton background, even without a band or partner. A reading, not a gate: the exit status never moves. An unknown slot, a slot with itself, ' +
|
|
1077
1077
|
'a slot that draws on no frame, a malformed list and a pair named twice in either order are refused by name. Not with --slot/--hide',
|
|
1078
1078
|
'angle-range':
|
|
1079
1079
|
'also read, for each <bone>:<relative-to> pair, the range of the first bone\'s world direction against the second\'s over each set\'s ' +
|
package/src/seam.ts
CHANGED
|
@@ -63,6 +63,13 @@
|
|
|
63
63
|
* Shared transforms read closed. The outer-hull reach remains bounded: tears
|
|
64
64
|
* outside the transported setup overlap are not certified absent by a zero.
|
|
65
65
|
*
|
|
66
|
+
* `jointContact` separately observes material connectivity on that same grid.
|
|
67
|
+
* Initially overlapping, single-component drawings are joined if their posed
|
|
68
|
+
* union is edge-connected, separated otherwise. Missing bands or non-single
|
|
69
|
+
* components are unavailable. Joined does not rule out local holes or notches;
|
|
70
|
+
* separated does not infer artistic intent. Raw gap can include an external
|
|
71
|
+
* rotational crescent of a joined pair and is deliberately unchanged.
|
|
72
|
+
*
|
|
66
73
|
* Pure: no clock, no randomness, no runtime. The pieces, the attachments and the
|
|
67
74
|
* rest table are handed in by whichever poser drew the frames.
|
|
68
75
|
*/
|
|
@@ -159,6 +166,15 @@ export interface SeamKindReading {
|
|
|
159
166
|
worst: SeamHit | null;
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
/** Connectivity of two initially overlapping single material components, at grid resolution. */
|
|
170
|
+
export interface SeamContactFrame {
|
|
171
|
+
index: number;
|
|
172
|
+
time: number;
|
|
173
|
+
status: 'joined' | 'separated' | 'unavailable';
|
|
174
|
+
/** Non-null exactly when the observation is unavailable; never a measured zero. */
|
|
175
|
+
reason: string | null;
|
|
176
|
+
}
|
|
177
|
+
|
|
162
178
|
/** One pair's reading over one frame set. */
|
|
163
179
|
export interface SeamPairReading {
|
|
164
180
|
a: string;
|
|
@@ -186,6 +202,8 @@ export interface SeamPairReading {
|
|
|
186
202
|
/** Positive-alpha cells contributed by the named slot where all OTHER slots have zero alpha. Independent of the band. */
|
|
187
203
|
exposureA: SeamKindReading;
|
|
188
204
|
exposureB: SeamKindReading;
|
|
205
|
+
/** Every sampled frame, including unavailable frames. Independent of raw gap/ghost and their summaries. */
|
|
206
|
+
jointContact: { frames: SeamContactFrame[] };
|
|
189
207
|
}
|
|
190
208
|
|
|
191
209
|
/** `seam.json`: one frame set's reading, every pair the flag names, in the flag's order. */
|
|
@@ -196,7 +214,7 @@ export interface SeamFile {
|
|
|
196
214
|
skin?: string;
|
|
197
215
|
fps: number;
|
|
198
216
|
/** What the numbers mean, stated in the file so a reader does not need this module. */
|
|
199
|
-
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; exposureA: string; exposureB: string };
|
|
217
|
+
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; exposureA: string; exposureB: string; jointContact: string };
|
|
200
218
|
pairs: SeamPairReading[];
|
|
201
219
|
}
|
|
202
220
|
|
|
@@ -219,6 +237,8 @@ export interface SeamPlant {
|
|
|
219
237
|
hiddenGhost?: boolean;
|
|
220
238
|
/** Bridge unrelated overlap cells with the old global hull. */
|
|
221
239
|
globalHull?: boolean;
|
|
240
|
+
/** Force a measured contact to one answer, ignoring the rendered material. */
|
|
241
|
+
contact?: 'joined' | 'separated';
|
|
222
242
|
/** Wrong composite readings: half opacity, only the pair, or every gap forced to one kind. */
|
|
223
243
|
gapCoverage?: 'half' | 'pair' | 'open' | 'covered';
|
|
224
244
|
/** Report a pair with no band as read, rather than as a SKIP. */
|
|
@@ -473,8 +493,44 @@ function markHull(hull: ReadonlyArray<[number, number]>, grid: Grid, cells: Uint
|
|
|
473
493
|
}
|
|
474
494
|
}
|
|
475
495
|
|
|
496
|
+
/** Count edge-connected components, stopping after the second: only exactly one is supported. */
|
|
497
|
+
function components(cells: Uint8Array, width: number): number {
|
|
498
|
+
const seen = new Uint8Array(cells.length);
|
|
499
|
+
const queue = new Int32Array(cells.length);
|
|
500
|
+
let count = 0;
|
|
501
|
+
for (let k = 0; k < cells.length; k++) {
|
|
502
|
+
if (!cells[k] || seen[k]) continue;
|
|
503
|
+
if (++count === 2) return count;
|
|
504
|
+
let head = 0, tail = 1;
|
|
505
|
+
queue[0] = k; seen[k] = 1;
|
|
506
|
+
while (head < tail) {
|
|
507
|
+
const i = queue[head++];
|
|
508
|
+
const visit = (j: number): void => {
|
|
509
|
+
if (j >= 0 && j < cells.length && cells[j] && !seen[j]) { seen[j] = 1; queue[tail++] = j; }
|
|
510
|
+
};
|
|
511
|
+
if (i % width > 0) visit(i - 1);
|
|
512
|
+
if (i % width < width - 1) visit(i + 1);
|
|
513
|
+
visit(i - width); visit(i + width);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return count;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
type Contact = Pick<SeamContactFrame, 'status' | 'reason'>;
|
|
520
|
+
const unavailableContact = (reason: string): Contact => ({ status: 'unavailable', reason });
|
|
521
|
+
|
|
522
|
+
/** Four neighbours mean a shared cell edge; diagonals alone do not connect material. No distance parameter. */
|
|
523
|
+
function contactOf(restA: Uint8Array, restB: Uint8Array, drawnA: Uint8Array, drawnB: Uint8Array, width: number, plant: SeamPlant): Contact {
|
|
524
|
+
for (const [name, cells] of [['a setup', restA], ['b setup', restB], ['a frame', drawnA], ['b frame', drawnB]] as const) {
|
|
525
|
+
const n = components(cells, width);
|
|
526
|
+
if (n !== 1) return unavailableContact(`${name}: ${n === 0 ? 'no' : 'multiple'} edge-connected half-opacity material components; exactly one is required`);
|
|
527
|
+
}
|
|
528
|
+
const union = drawnA.map((v, k) => v || drawnB[k]);
|
|
529
|
+
return { status: plant.contact ?? (components(union, width) === 1 ? 'joined' : 'separated'), reason: null };
|
|
530
|
+
}
|
|
531
|
+
|
|
476
532
|
/** One frame's counts, or `null` when the two drawings share no cell at the setup pose. See the module comment. */
|
|
477
|
-
function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: SeamFrame, pages: ReadonlyMap<string, Plate>, grid: Grid, visibility: readonly Uint8Array[]): { gap: Cells; ghost: Cells; ghostA: Cells; ghostB: Cells; gapOpen: Cells; gapCovered: Cells } | null {
|
|
533
|
+
function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: SeamFrame, pages: ReadonlyMap<string, Plate>, grid: Grid, visibility: readonly Uint8Array[]): { gap: Cells; ghost: Cells; ghostA: Cells; ghostB: Cells; gapOpen: Cells; gapCovered: Cells; contact: Contact } | null {
|
|
478
534
|
const { width, height } = grid.viewport;
|
|
479
535
|
const restA = restPiece(A);
|
|
480
536
|
const restB = restPiece(B);
|
|
@@ -520,6 +576,7 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: Seam
|
|
|
520
576
|
const hull = hullOf(carried);
|
|
521
577
|
const drawnA = drawnCells(A.piece, A.page, grid.viewport);
|
|
522
578
|
const drawnB = drawnCells(B.piece, B.page, grid.viewport);
|
|
579
|
+
const contact = contactOf(bandA, bandB, drawnA, drawnB, width, plant);
|
|
523
580
|
const [visibleA, visibleB] = visibility;
|
|
524
581
|
const posedIdsA = triangleIds(A.triangles, A.piece.world, grid);
|
|
525
582
|
const posedIdsB = triangleIds(B.triangles, B.piece.world, grid);
|
|
@@ -536,7 +593,7 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: Seam
|
|
|
536
593
|
const gapCovered: Cells = { px: 0, box: null };
|
|
537
594
|
let alphaCells: Uint8Array | null = null;
|
|
538
595
|
const take = (into: Cells, qx: number, qy: number): void => takeCell(into, qx, qy, cell);
|
|
539
|
-
if (plant.never) return { gap, ghost, ghostA, ghostB, gapOpen, gapCovered };
|
|
596
|
+
if (plant.never) return { gap, ghost, ghostA, ghostB, gapOpen, gapCovered, contact };
|
|
540
597
|
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;
|
|
541
598
|
for (let py = 0; py < height; py++) {
|
|
542
599
|
for (let px = 0; px < width; px++) {
|
|
@@ -570,7 +627,7 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: Seam
|
|
|
570
627
|
}
|
|
571
628
|
}
|
|
572
629
|
}
|
|
573
|
-
return { gap, ghost, ghostA, ghostB, gapOpen, gapCovered };
|
|
630
|
+
return { gap, ghost, ghostA, ghostB, gapOpen, gapCovered, contact };
|
|
574
631
|
}
|
|
575
632
|
|
|
576
633
|
const restKey = (slot: string, attachment: string): string => `${slot}\u0000${attachment}`;
|
|
@@ -634,9 +691,12 @@ export function seamsOf(
|
|
|
634
691
|
const openGaps: SeamHit[] = [];
|
|
635
692
|
const coveredGaps: SeamHit[] = [];
|
|
636
693
|
const exposuresA: SeamHit[] = [], exposuresB: SeamHit[] = [];
|
|
694
|
+
const contacts: SeamContactFrame[] = [];
|
|
637
695
|
for (const frame of frames) {
|
|
638
696
|
const A = sideOf(frame, a);
|
|
639
697
|
const B = sideOf(frame, b);
|
|
698
|
+
const contact = (reading: Contact): void => { contacts.push({ index: frame.index, time: frame.time, ...reading }); };
|
|
699
|
+
if (A === null || B === null) contact(unavailableContact('both named slots must draw on this frame'));
|
|
640
700
|
const sides = [A, B].filter((side): side is Side => side !== null);
|
|
641
701
|
if (sides.length === 0) continue;
|
|
642
702
|
const sizes = sides.map((side) => texelSize(side.piece, side.rest, side.page));
|
|
@@ -652,7 +712,9 @@ export function seamsOf(
|
|
|
652
712
|
drawn++;
|
|
653
713
|
const cell = exposureCell;
|
|
654
714
|
const read = readFrame(A, B, cell, plant, frame, pages, exposureGrid, exposure.map((m) => m.visible));
|
|
655
|
-
if (read === null) continue;
|
|
715
|
+
if (read === null) { contact(unavailableContact('no shared half-opacity cell at the setup pose; no band to identify a joint')); continue; }
|
|
716
|
+
const fragmented = [a, b].some(slot => frame.pieces.filter(p => p.slot === slot).length !== 1);
|
|
717
|
+
contact(fragmented ? unavailableContact('each named slot must draw exactly one fragment for material contact') : read.contact);
|
|
656
718
|
banded++;
|
|
657
719
|
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) {
|
|
658
720
|
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 });
|
|
@@ -663,7 +725,7 @@ export function seamsOf(
|
|
|
663
725
|
const opens = [...new Set([...gaps, ...ghosts].map((f) => f.index))].sort((x, y) => x - y);
|
|
664
726
|
const maxPx = Math.max(0, ...gaps.map((f) => f.px), ...ghosts.map((f) => f.px));
|
|
665
727
|
const status = banded === 0 && !plant.skipAsRead ? 'skip' : 'read';
|
|
666
|
-
return { a, b, drawn, banded, status, opens, maxPx, gap, ghost, ghostA: kindOf(ghostsA), ghostB: kindOf(ghostsB), gapOpen: kindOf(openGaps), gapCovered: kindOf(coveredGaps), exposureA: kindOf(exposuresA), exposureB: kindOf(exposuresB) };
|
|
728
|
+
return { a, b, drawn, banded, status, opens, maxPx, gap, ghost, ghostA: kindOf(ghostsA), ghostB: kindOf(ghostsB), gapOpen: kindOf(openGaps), gapCovered: kindOf(coveredGaps), exposureA: kindOf(exposuresA), exposureB: kindOf(exposuresB), jointContact: { frames: contacts } };
|
|
667
729
|
});
|
|
668
730
|
}
|
|
669
731
|
|
|
@@ -685,6 +747,7 @@ export const SEAM_READING: SeamFile['reading'] = {
|
|
|
685
747
|
exposureA: 'positive-rendered-alpha cells from named slot a where removing every fragment of that slot leaves alpha 0 from all other skeleton slots, regardless of draw order; measured over the whole drawing even without a seam band or partner; same hit, span and worst units as gap',
|
|
686
748
|
exposureB: 'the same background exposure for named slot b; swapping the pair exchanges exposureA and exposureB; exposure does not enter opens or maxPx and is not a change-from-setup measurement',
|
|
687
749
|
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',
|
|
750
|
+
jointContact: 'every sampled frame: joined or separated only when a setup band exists and each named drawing has exactly one edge-connected half-opacity material component at setup and frame; joined means their frame union is edge-connected (overlap or shared cell edge), separated means it is not; diagonal contact alone is separated. Otherwise unavailable with a reason, including multiple fragments of a named slot. Uses the existing origin-aligned texel grid, alpha and attachment maps, independent of other slots and draw order. Raw gap measures swept support, not certified tearing. Contact is pair material connectivity, not anatomical joint intent; joined does not rule out local holes/notches. No statement about sub-cell contact; raw gap/ghost, opens/maxPx and exit status are unchanged',
|
|
688
751
|
};
|
|
689
752
|
|
|
690
753
|
/** The file for one frame set. */
|
|
@@ -730,5 +793,12 @@ function seamSummary(animation: string | null, fps: number, pair: SeamPairReadin
|
|
|
730
793
|
|
|
731
794
|
/** The seam summary plus whole-drawing background exposures, including on SKIP pairs. */
|
|
732
795
|
export function seamLine(animation: string | null, fps: number, pair: SeamPairReading): string {
|
|
733
|
-
|
|
796
|
+
const contacts = pair.jointContact.frames;
|
|
797
|
+
const contactText = contacts.length === 0 ? 'unavailable: no sampled frames' : null;
|
|
798
|
+
const summary = (status: SeamContactFrame['status']): string => {
|
|
799
|
+
const frames = contacts.filter(f => f.status === status);
|
|
800
|
+
return `${status} on ${frames.length} frame(s) [${indexRuns(frames.map(f => f.index))}]`;
|
|
801
|
+
};
|
|
802
|
+
const reasons = [...new Set(contacts.flatMap(f => f.reason === null ? [] : [f.reason]))];
|
|
803
|
+
return `${seamSummary(animation, fps, pair)}; ${kindText(`exposure ${JSON.stringify(pair.a)}`, pair.exposureA)}; ${kindText(`exposure ${JSON.stringify(pair.b)}`, pair.exposureB)}; joint contact: ${contactText ?? `${summary('joined')}, ${summary('separated')}, ${summary('unavailable')}`}${reasons.length ? ` (${reasons.join('; ')})` : ''}`;
|
|
734
804
|
}
|