@rig-c/core 2.82.1 → 2.83.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 +45 -43
- package/docs/AUTHORING.md +100 -55
- package/docs/INGEST.md +3 -3
- package/docs/MOTION.md +21 -11
- package/package.json +1 -1
- package/src/cli/shared.ts +4 -4
- package/src/pose.ts +209 -147
- package/src/seam.ts +154 -57
package/src/seam.ts
CHANGED
|
@@ -39,34 +39,29 @@
|
|
|
39
39
|
* read, and a pair whose band is empty on every frame both draw is a **SKIP**,
|
|
40
40
|
* never a pass. The band has no width of its own: it is the overlap the art
|
|
41
41
|
* states, dilated by nothing.
|
|
42
|
-
* - **gap** —
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
42
|
+
* - **gap** — neither drawing paints the cell, and at least one inverse
|
|
43
|
+
* attachment map finds setup art there. The candidate must lie in both the
|
|
44
|
+
* outer hull of the transported band centres AND a local sweep joining the
|
|
45
|
+
* SAME setup band cell through the two attachment maps. Local sweeps are
|
|
46
|
+
* convex hulls of that cell's eight carried corners; they do not bridge
|
|
47
|
+
* unrelated overlap islands or concave notches. The rig's alpha and triangle
|
|
48
|
+
* maps supply this joint support, with no pivot radius. This is an explicit
|
|
49
|
+
* geometric reading, not an inference of artistic intent or all possible tears.
|
|
50
|
+
* - **ghost** — a one-sided drawing carried from the setup band whose partner's
|
|
51
|
+
* inverse map finds no setup art. Count it only if that slot still contributes
|
|
52
|
+
* after source-over in frame draw order: later opaque art hides it, partial
|
|
53
|
+
* alpha retains a contribution. All fragments of later slots participate.
|
|
54
|
+
* - **exposureA/B** — whole-drawing positive-alpha cells of each named slot
|
|
55
|
+
* where removing all fragments of that slot leaves alpha zero. Other slots
|
|
56
|
+
* exclude exposure regardless of draw order, even below half opacity. Exposure
|
|
57
|
+
* is independent of the band and is measured with an absent partner too; it
|
|
58
|
+
* never enters opens/maxPx. It is not a difference from setup.
|
|
56
59
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* predicate: another drawing can cover what the pair no longer covers.
|
|
63
|
-
*
|
|
64
|
-
* Known reach, stated rather than discovered: a piece folded right back over its
|
|
65
|
-
* partner carries the band across the region it vacated, so a hull that wide
|
|
66
|
-
* reads the vacated cells its partner's view still finds drawn; a mesh whose
|
|
67
|
-
* vertices stretch the art the band lies in reads the stretched band, which is
|
|
68
|
-
* what it draws. Draw order does not enter: a cell is covered or bare whichever
|
|
69
|
-
* piece is in front.
|
|
60
|
+
* Hits carry cell count, area and whole-cell world box; sparse spans and earliest
|
|
61
|
+
* worst-frame summaries share one derivation. gapOpen/gapCovered still partition
|
|
62
|
+
* gap by zero/positive alpha of the transparent whole-skeleton composite.
|
|
63
|
+
* Shared transforms read closed. The outer-hull reach remains bounded: tears
|
|
64
|
+
* outside the transported setup overlap are not certified absent by a zero.
|
|
70
65
|
*
|
|
71
66
|
* Pure: no clock, no randomness, no runtime. The pieces, the attachments and the
|
|
72
67
|
* rest table are handed in by whichever poser drew the frames.
|
|
@@ -188,6 +183,9 @@ export interface SeamPairReading {
|
|
|
188
183
|
gapOpen: SeamKindReading;
|
|
189
184
|
/** Gap cells whose skeleton composite has positive rendered alpha, even below half opacity. */
|
|
190
185
|
gapCovered: SeamKindReading;
|
|
186
|
+
/** Positive-alpha cells contributed by the named slot where all OTHER slots have zero alpha. Independent of the band. */
|
|
187
|
+
exposureA: SeamKindReading;
|
|
188
|
+
exposureB: SeamKindReading;
|
|
191
189
|
}
|
|
192
190
|
|
|
193
191
|
/** `seam.json`: one frame set's reading, every pair the flag names, in the flag's order. */
|
|
@@ -198,7 +196,7 @@ export interface SeamFile {
|
|
|
198
196
|
skin?: string;
|
|
199
197
|
fps: number;
|
|
200
198
|
/** What the numbers mean, stated in the file so a reader does not need this module. */
|
|
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 };
|
|
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 };
|
|
202
200
|
pairs: SeamPairReading[];
|
|
203
201
|
}
|
|
204
202
|
|
|
@@ -217,6 +215,10 @@ export interface SeamPlant {
|
|
|
217
215
|
ghostAnyArt?: boolean;
|
|
218
216
|
/** Attribute every ghost to a, regardless of which piece draws it. */
|
|
219
217
|
ghostOnlyA?: boolean;
|
|
218
|
+
/** Ignore later drawings when reading ghosts. */
|
|
219
|
+
hiddenGhost?: boolean;
|
|
220
|
+
/** Bridge unrelated overlap cells with the old global hull. */
|
|
221
|
+
globalHull?: boolean;
|
|
220
222
|
/** Wrong composite readings: half opacity, only the pair, or every gap forced to one kind. */
|
|
221
223
|
gapCoverage?: 'half' | 'pair' | 'open' | 'covered';
|
|
222
224
|
/** Report a pair with no band as read, rather than as a SKIP. */
|
|
@@ -387,11 +389,10 @@ function frameAlphaCells(pieces: readonly Piece[], pages: ReadonlyMap<string, Pl
|
|
|
387
389
|
return cells;
|
|
388
390
|
}
|
|
389
391
|
|
|
390
|
-
/** One
|
|
391
|
-
function
|
|
392
|
-
// One grid over both drawings at the setup pose and at the frame, on multiples of the cell from the world origin.
|
|
392
|
+
/** One origin-aligned texel grid, also used when only one named slot draws. */
|
|
393
|
+
function gridOf(sides: readonly Side[], cell: number, extra: readonly Piece[] = []): Grid {
|
|
393
394
|
let [x0, y0, x1, y1] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
394
|
-
for (const xy of [
|
|
395
|
+
for (const xy of [...sides.flatMap((side) => [side.rest, side.piece.world]), ...extra.map((piece) => piece.world)]) {
|
|
395
396
|
for (let i = 0; i + 1 < xy.length; i += 2) {
|
|
396
397
|
x0 = Math.min(x0, xy[i]);
|
|
397
398
|
y0 = Math.min(y0, xy[i + 1]);
|
|
@@ -403,7 +404,78 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: Seam
|
|
|
403
404
|
const maxY = (Math.ceil(y1 / cell) + 1) * cell;
|
|
404
405
|
const width = Math.ceil((x1 - minX) / cell) + 1;
|
|
405
406
|
const height = Math.ceil((maxY - y0) / cell) + 1;
|
|
406
|
-
|
|
407
|
+
return { viewport: { minX, minY: maxY - height * cell, maxX: minX + width * cell, maxY, scale: 1 / cell, width, height }, cell };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const emptyCells = (): Cells => ({ px: 0, box: null });
|
|
411
|
+
function takeCell(into: Cells, qx: number, qy: number, cell: number): void {
|
|
412
|
+
into.px++;
|
|
413
|
+
const h = cell / 2;
|
|
414
|
+
into.box ??= [Infinity, Infinity, -Infinity, -Infinity];
|
|
415
|
+
into.box[0] = Math.min(into.box[0], qx - h);
|
|
416
|
+
into.box[1] = Math.min(into.box[1], qy - h);
|
|
417
|
+
into.box[2] = Math.max(into.box[2], qx + h);
|
|
418
|
+
into.box[3] = Math.max(into.box[3], qy + h);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Symbolic nonzero source-over contribution per named slot. Later alpha 255
|
|
423
|
+
* erases earlier contributions; any positive emission of that slot adds one.
|
|
424
|
+
* Partial alpha cannot erase a nonzero contribution. Traverse EVERY fragment,
|
|
425
|
+
* including multiple pieces of the same slot and clipped source meshes.
|
|
426
|
+
* Exposure asks a separate counterfactual: would removing this slot leave
|
|
427
|
+
* alpha zero? It therefore excludes even faint art of any other slot, before
|
|
428
|
+
* OR after the named slot. No setup comparison or seam band enters.
|
|
429
|
+
*/
|
|
430
|
+
function slotCells(frame: SeamFrame, pages: ReadonlyMap<string, Plate>, grid: Grid, slots: readonly string[]): { visible: Uint8Array; exposure: Cells }[] {
|
|
431
|
+
const n = grid.viewport.width * grid.viewport.height;
|
|
432
|
+
const masks = slots.map(() => ({ visible: new Uint8Array(n), own: new Uint8Array(n), other: new Uint8Array(n), exposure: emptyCells() }));
|
|
433
|
+
const project = projector(grid.viewport);
|
|
434
|
+
for (const piece of frame.pieces) {
|
|
435
|
+
const page = pages.get(piece.page);
|
|
436
|
+
if (page === undefined) throw new SeamError(`slot "${piece.slot}" samples atlas page "${piece.page}", which is not among [${[...pages.keys()].join(', ')}]`);
|
|
437
|
+
rasterisePiece(page, piece, project, grid.viewport, (x, y, _r, _g, _b, alpha) => {
|
|
438
|
+
const k = y * grid.viewport.width + x;
|
|
439
|
+
for (let i = 0; i < slots.length; i++) {
|
|
440
|
+
const m = masks[i];
|
|
441
|
+
if (alpha === 255) m.visible[k] = 0;
|
|
442
|
+
if (alpha > 0) {
|
|
443
|
+
if (piece.slot === slots[i]) m.own[k] = m.visible[k] = 1;
|
|
444
|
+
else m.other[k] = 1;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
for (const m of masks) for (let k = 0; k < n; k++) if (m.own[k] && !m.other[k]) {
|
|
450
|
+
takeCell(m.exposure, centreX(grid, k % grid.viewport.width), centreY(grid, Math.floor(k / grid.viewport.width)), grid.cell);
|
|
451
|
+
}
|
|
452
|
+
return masks;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Rasterise a convex swept cell by scanline; never fill between unrelated band cells. */
|
|
456
|
+
function markHull(hull: ReadonlyArray<[number, number]>, grid: Grid, cells: Uint8Array): void {
|
|
457
|
+
if (hull.length === 0) return;
|
|
458
|
+
const { minX, maxY, width, height } = grid.viewport;
|
|
459
|
+
const lo = Math.max(0, Math.ceil((maxY - Math.max(...hull.map((p) => p[1]))) / grid.cell - 0.5));
|
|
460
|
+
const hi = Math.min(height - 1, Math.floor((maxY - Math.min(...hull.map((p) => p[1]))) / grid.cell - 0.5));
|
|
461
|
+
for (let py = lo; py <= hi; py++) {
|
|
462
|
+
const y = centreY(grid, py);
|
|
463
|
+
let left = Infinity, right = -Infinity;
|
|
464
|
+
for (let i = 0; i < hull.length; i++) {
|
|
465
|
+
const [x0, y0] = hull[i], [x1, y1] = hull[(i + 1) % hull.length];
|
|
466
|
+
if (y < Math.min(y0, y1) || y > Math.max(y0, y1)) continue;
|
|
467
|
+
if (y0 === y1) { left = Math.min(left, x0, x1); right = Math.max(right, x0, x1); }
|
|
468
|
+
else { const x = x0 + (y - y0) * (x1 - x0) / (y1 - y0); left = Math.min(left, x); right = Math.max(right, x); }
|
|
469
|
+
}
|
|
470
|
+
const x0 = Math.max(0, Math.ceil((left - minX) / grid.cell - 0.5));
|
|
471
|
+
const x1 = Math.min(width - 1, Math.floor((right - minX) / grid.cell - 0.5));
|
|
472
|
+
for (let px = x0; px <= x1; px++) cells[py * width + px] = 1;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** 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 {
|
|
478
|
+
const { width, height } = grid.viewport;
|
|
407
479
|
const restA = restPiece(A);
|
|
408
480
|
const restB = restPiece(B);
|
|
409
481
|
const bandA = drawnCells(restA, A.page, grid.viewport);
|
|
@@ -411,7 +483,13 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: Seam
|
|
|
411
483
|
// The band: both drawings at the setup pose. Carried to the frame by each piece, its centres span the joint's hull.
|
|
412
484
|
const restIdsA = triangleIds(A.triangles, A.rest, grid);
|
|
413
485
|
const restIdsB = triangleIds(B.triangles, B.rest, grid);
|
|
486
|
+
// The corner lattice is offset by half a cell. Reuse the same lowest-index
|
|
487
|
+
// containing-triangle rule as centres, including overlapping setup meshes.
|
|
488
|
+
const corners: Grid = { cell, viewport: { ...grid.viewport, minX: grid.viewport.minX - cell / 2, minY: grid.viewport.minY - cell / 2, maxX: grid.viewport.maxX + cell / 2, maxY: grid.viewport.maxY + cell / 2, width: width + 1, height: height + 1 } };
|
|
489
|
+
const cornerIdsA = triangleIds(A.triangles, A.rest, corners);
|
|
490
|
+
const cornerIdsB = triangleIds(B.triangles, B.rest, corners);
|
|
414
491
|
const carried: Array<[number, number]> = [];
|
|
492
|
+
const joint = new Uint8Array(width * height);
|
|
415
493
|
let band = 0;
|
|
416
494
|
let [bx0, by0, bx1, by1] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
417
495
|
for (let py = 0; py < height; py++) {
|
|
@@ -422,16 +500,27 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: Seam
|
|
|
422
500
|
const qx = centreX(grid, px);
|
|
423
501
|
const qy = centreY(grid, py);
|
|
424
502
|
[bx0, by0, bx1, by1] = [Math.min(bx0, qx), Math.min(by0, qy), Math.max(bx1, qx), Math.max(by1, qy)];
|
|
425
|
-
|
|
503
|
+
const local: Array<[number, number]> = [];
|
|
504
|
+
for (const [side, ids, cornerIds] of [[A, restIdsA, cornerIdsA], [B, restIdsB, cornerIdsB]] as const) {
|
|
426
505
|
const p = plant.world ? ([qx, qy] as [number, number]) : carry(side.triangles, side.rest, side.piece.world, ids[k], qx, qy);
|
|
427
506
|
if (p !== null) carried.push(p);
|
|
507
|
+
// The same material cell through BOTH attachment maps. Corners use
|
|
508
|
+
// their own triangle when the cell crosses a skinned mesh edge.
|
|
509
|
+
for (const ox of [0, 1]) for (const oy of [0, 1]) {
|
|
510
|
+
const x = grid.viewport.minX + (px + ox) * cell, y = grid.viewport.maxY - (py + oy) * cell;
|
|
511
|
+
const id = cornerIds[(py + oy) * (width + 1) + px + ox];
|
|
512
|
+
const corner = plant.world ? [x, y] as [number, number] : carry(side.triangles, side.rest, side.piece.world, id, x, y);
|
|
513
|
+
if (corner !== null) local.push(corner);
|
|
514
|
+
}
|
|
428
515
|
}
|
|
516
|
+
markHull(hullOf(local), grid, joint);
|
|
429
517
|
}
|
|
430
518
|
}
|
|
431
519
|
if (band === 0) return null;
|
|
432
520
|
const hull = hullOf(carried);
|
|
433
521
|
const drawnA = drawnCells(A.piece, A.page, grid.viewport);
|
|
434
522
|
const drawnB = drawnCells(B.piece, B.page, grid.viewport);
|
|
523
|
+
const [visibleA, visibleB] = visibility;
|
|
435
524
|
const posedIdsA = triangleIds(A.triangles, A.piece.world, grid);
|
|
436
525
|
const posedIdsB = triangleIds(B.triangles, B.piece.world, grid);
|
|
437
526
|
// A frame point carried back into a side's setup pose.
|
|
@@ -446,15 +535,7 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: Seam
|
|
|
446
535
|
const gapOpen: Cells = { px: 0, box: null };
|
|
447
536
|
const gapCovered: Cells = { px: 0, box: null };
|
|
448
537
|
let alphaCells: Uint8Array | null = null;
|
|
449
|
-
const take = (into: Cells, qx: number, qy: number): void =>
|
|
450
|
-
into.px++;
|
|
451
|
-
const h = cell / 2;
|
|
452
|
-
into.box ??= [Infinity, Infinity, -Infinity, -Infinity];
|
|
453
|
-
into.box[0] = Math.min(into.box[0], qx - h);
|
|
454
|
-
into.box[1] = Math.min(into.box[1], qy - h);
|
|
455
|
-
into.box[2] = Math.max(into.box[2], qx + h);
|
|
456
|
-
into.box[3] = Math.max(into.box[3], qy + h);
|
|
457
|
-
};
|
|
538
|
+
const take = (into: Cells, qx: number, qy: number): void => takeCell(into, qx, qy, cell);
|
|
458
539
|
if (plant.never) return { gap, ghost, ghostA, ghostB, gapOpen, gapCovered };
|
|
459
540
|
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;
|
|
460
541
|
for (let py = 0; py < height; py++) {
|
|
@@ -465,7 +546,7 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: Seam
|
|
|
465
546
|
const a = drawnA[k] !== 0;
|
|
466
547
|
const b = drawnB[k] !== 0;
|
|
467
548
|
if (!a && !b) {
|
|
468
|
-
if (!plant.noHull && !inHull(hull, qx, qy)) continue;
|
|
549
|
+
if (!plant.noHull && (!inHull(hull, qx, qy) || (!plant.globalHull && !joint[k]))) continue;
|
|
469
550
|
const fromA = composite(back(A, posedIdsA, k, qx, qy));
|
|
470
551
|
const fromB = composite(back(B, posedIdsB, k, qx, qy));
|
|
471
552
|
if (plant.bothViews ? fromA && fromB : fromA || fromB) {
|
|
@@ -477,6 +558,7 @@ function readFrame(A: Side, B: Side, cell: number, plant: SeamPlant, frame: Seam
|
|
|
477
558
|
continue;
|
|
478
559
|
}
|
|
479
560
|
if (a && b) continue;
|
|
561
|
+
if (!plant.hiddenGhost && !(a ? visibleA[k] : visibleB[k])) continue;
|
|
480
562
|
// One piece draws the cell and its partner does not.
|
|
481
563
|
const [mine, myIds, myRest, other, otherIds] = a ? [A, posedIdsA, restA, B, posedIdsB] : [B, posedIdsB, restB, A, posedIdsA];
|
|
482
564
|
const otherRest = a ? restB : restA;
|
|
@@ -551,17 +633,25 @@ export function seamsOf(
|
|
|
551
633
|
const ghostsB: SeamHit[] = [];
|
|
552
634
|
const openGaps: SeamHit[] = [];
|
|
553
635
|
const coveredGaps: SeamHit[] = [];
|
|
636
|
+
const exposuresA: SeamHit[] = [], exposuresB: SeamHit[] = [];
|
|
554
637
|
for (const frame of frames) {
|
|
555
638
|
const A = sideOf(frame, a);
|
|
556
639
|
const B = sideOf(frame, b);
|
|
640
|
+
const sides = [A, B].filter((side): side is Side => side !== null);
|
|
641
|
+
if (sides.length === 0) continue;
|
|
642
|
+
const sizes = sides.map((side) => texelSize(side.piece, side.rest, side.page));
|
|
643
|
+
if (sizes.some((size) => size === null)) throw new SeamError(`frame ${frame.index}: slot "${sides[sizes.indexOf(null)].piece.slot}" draws no area at its setup pose, so it has no texel to grid the seam by`);
|
|
644
|
+
const exposureCell = Math.max(...sizes.filter((size): size is number => size !== null));
|
|
645
|
+
const exposureGrid = gridOf(sides, exposureCell, frame.pieces.filter((piece) => piece.slot === a || piece.slot === b));
|
|
646
|
+
const exposure = slotCells(frame, pages, exposureGrid, [a, b]);
|
|
647
|
+
for (const [i, into] of [[0, exposuresA], [1, exposuresB]] as const) {
|
|
648
|
+
const cells = exposure[i].exposure;
|
|
649
|
+
if (cells.px > 0 && cells.box !== null) into.push({ index: frame.index, time: frame.time, px: cells.px, cell: exposureCell, area: cells.px * exposureCell * exposureCell, box: cells.box });
|
|
650
|
+
}
|
|
557
651
|
if (A === null || B === null) continue;
|
|
558
652
|
drawn++;
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
const flat = cellA === null ? a : cellB === null ? b : null;
|
|
562
|
-
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`);
|
|
563
|
-
const cell = Math.max(cellA, cellB);
|
|
564
|
-
const read = readFrame(A, B, cell, plant, frame, pages);
|
|
653
|
+
const cell = exposureCell;
|
|
654
|
+
const read = readFrame(A, B, cell, plant, frame, pages, exposureGrid, exposure.map((m) => m.visible));
|
|
565
655
|
if (read === null) continue;
|
|
566
656
|
banded++;
|
|
567
657
|
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) {
|
|
@@ -573,25 +663,27 @@ export function seamsOf(
|
|
|
573
663
|
const opens = [...new Set([...gaps, ...ghosts].map((f) => f.index))].sort((x, y) => x - y);
|
|
574
664
|
const maxPx = Math.max(0, ...gaps.map((f) => f.px), ...ghosts.map((f) => f.px));
|
|
575
665
|
const status = banded === 0 && !plant.skipAsRead ? 'skip' : 'read';
|
|
576
|
-
return { a, b, drawn, banded, status, opens, maxPx, gap, ghost, ghostA: kindOf(ghostsA), ghostB: kindOf(ghostsB), gapOpen: kindOf(openGaps), gapCovered: kindOf(coveredGaps) };
|
|
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) };
|
|
577
667
|
});
|
|
578
668
|
}
|
|
579
669
|
|
|
580
670
|
/** What `seam.json` states its numbers mean — the one wording the file and the docs share. */
|
|
581
671
|
export const SEAM_READING: SeamFile['reading'] = {
|
|
582
|
-
read: 'a frame is read when both slots draw and their two drawings share a cell at the setup pose (the band); draw order
|
|
583
|
-
cell: "every count is of cells one texel of the coarser of the two drawings at the setup pose (the rest table's vertices), in world units, on multiples of that texel from the world origin; px is a count of cells, cell the cell's side in world units, area px × cell² in world units²",
|
|
672
|
+
read: 'a frame is read when both slots draw and their two drawings share a cell at the setup pose (the band); ghost visibility follows frame draw order; exposure is also measured without a band',
|
|
673
|
+
cell: "every count is of cells one texel of the coarser of the two drawings at the setup pose (the rest table's vertices; the present member alone for exposure when its partner is absent), in world units, on multiples of that texel from the world origin; px is a count of cells, cell the cell's side in world units, area px × cell² in world units²",
|
|
584
674
|
drawn: `a cell is drawn by a piece when the renderer paints its centre at ${DRAWN_ALPHA} of 255 or more (half opacity) — the slot's art through its page, the frame's tint included, at the setup pose drawn at the same tint`,
|
|
585
675
|
band: "the band is the cells both drawings draw at the setup pose — where the two were painted to overlap, dilated by nothing",
|
|
586
676
|
carried:
|
|
587
677
|
"a frame point is carried into a piece's setup pose by the affine map of the piece's triangle holding it as the frame poses it, or, outside the piece's mesh, of the triangle it is least outside of (the largest smallest barycentric weight, the lowest index on a tie); a band cell is carried to the frame the other way",
|
|
588
|
-
gap: "a gap cell lies inside the convex hull of the band's cell centres carried to the frame by both pieces, is drawn by neither piece at the frame, and at least one piece, carrying it back, finds it drawn by either drawing at the setup pose",
|
|
589
|
-
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",
|
|
678
|
+
gap: "a gap cell lies inside the convex hull of the band's cell centres carried to the frame by both pieces, is inside the union of local sweeps of the SAME setup-overlap cell through both attachment maps, is drawn by neither piece at the frame, and at least one piece, carrying it back, finds it drawn by either drawing at the setup pose",
|
|
679
|
+
ghost: "a ghost cell has nonzero source-over contribution from its named slot after all later drawings (opaque later art hides it; partial alpha does not), 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",
|
|
590
680
|
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",
|
|
591
681
|
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',
|
|
592
682
|
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',
|
|
593
683
|
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',
|
|
594
684
|
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',
|
|
685
|
+
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
|
+
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',
|
|
595
687
|
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',
|
|
596
688
|
};
|
|
597
689
|
|
|
@@ -609,7 +701,7 @@ export function seamText(file: SeamFile): string {
|
|
|
609
701
|
const outward = (x: number, up: boolean): string => ((up ? Math.ceil(x * 100) : Math.floor(x * 100)) / 100).toFixed(2);
|
|
610
702
|
|
|
611
703
|
/** One kind's clause: `gap on N frame(s) [runs], worst frame i (t=…s): P px (A units² at C units a px) in world box …`, or `no gap`. */
|
|
612
|
-
function kindText(kind:
|
|
704
|
+
function kindText(kind: string, reading: SeamKindReading): string {
|
|
613
705
|
const w = reading.worst;
|
|
614
706
|
if (w === null) return `no ${kind}`;
|
|
615
707
|
const [x0, y0, x1, y1] = w.box;
|
|
@@ -624,7 +716,7 @@ function kindText(kind: 'gap' | 'ghost', reading: SeamKindReading): string {
|
|
|
624
716
|
* The line for one pair over one set — printed for every pair, so a pair that
|
|
625
717
|
* stays closed says so, and a pair with nothing to read says SKIP and why.
|
|
626
718
|
*/
|
|
627
|
-
|
|
719
|
+
function seamSummary(animation: string | null, fps: number, pair: SeamPairReading): string {
|
|
628
720
|
const where = animation === null ? 'the setup pose' : JSON.stringify(animation);
|
|
629
721
|
const head = `${where}: seam "${pair.a}" | "${pair.b}"`;
|
|
630
722
|
if (pair.drawn === 0) return `${head}: SKIP — the two slots draw together on no frame, so there is no seam to read`;
|
|
@@ -635,3 +727,8 @@ export function seamLine(animation: string | null, fps: number, pair: SeamPairRe
|
|
|
635
727
|
`${kindText('gap', pair.gap)}; ${kindText('ghost', pair.ghost)}`
|
|
636
728
|
);
|
|
637
729
|
}
|
|
730
|
+
|
|
731
|
+
/** The seam summary plus whole-drawing background exposures, including on SKIP pairs. */
|
|
732
|
+
export function seamLine(animation: string | null, fps: number, pair: SeamPairReading): string {
|
|
733
|
+
return `${seamSummary(animation, fps, pair)}; ${kindText(`exposure ${JSON.stringify(pair.a)}`, pair.exposureA)}; ${kindText(`exposure ${JSON.stringify(pair.b)}`, pair.exposureB)}`;
|
|
734
|
+
}
|