spine-rigc 1.5.3 → 1.6.2
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 +2 -1
- package/cli.ts +131 -403
- package/docs/AUTHORING.md +63 -14
- package/docs/INGEST.md +1 -0
- package/docs/MOTION.md +1 -0
- package/package.json +1 -1
- package/src/atlas.ts +25 -0
- package/src/check.ts +113 -24
- package/src/checkpics.ts +2 -0
- package/src/compile.ts +641 -506
- package/src/core/animation.ts +982 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1721 -0
- package/src/core/constraints_path.ts +942 -0
- package/src/core/constraints_physics.ts +830 -0
- package/src/core/constraints_slider.ts +505 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1103 -0
- package/src/core/raw.ts +347 -0
- package/src/core/skins.ts +104 -0
- package/src/core/uvs.ts +448 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/world.ts +233 -0
- package/src/deformmeasure.ts +337 -73
- package/src/deformreport.ts +370 -0
- package/src/depth.ts +101 -2
- package/src/emit_spine.ts +860 -0
- package/src/mesh.ts +24 -15
- package/src/model.ts +958 -0
- package/src/render.ts +649 -143
- package/src/render_core.ts +582 -0
- package/src/rig.ts +34 -6
- package/src/transform.ts +26 -4
- package/src/types.ts +18 -0
- package/src/validate.ts +30 -2
package/src/render.ts
CHANGED
|
@@ -78,7 +78,10 @@
|
|
|
78
78
|
*
|
|
79
79
|
* ⚠️ Two notes on where this sits. It imports `spine-core`, which `src/` is
|
|
80
80
|
* otherwise careful about: posing a skeleton *is* running the runtime, and there
|
|
81
|
-
* is no honest way to render one without it.
|
|
81
|
+
* is no honest way to render one without it. (Since issue #968 that holds for a
|
|
82
|
+
* Spine export: a rigc build is posed by rigc's own core through
|
|
83
|
+
* `./render_core.ts`, and this file keeps the runtime for the export's poser,
|
|
84
|
+
* the atlas pages and texture substitution — see *which poser* below.) The rule that matters is unchanged
|
|
82
85
|
* — `src/compile.ts` must stay independent of the runtime so the compiler and
|
|
83
86
|
* the gate are not checking each other's assumptions — and this file is neither.
|
|
84
87
|
* It also imports `tools/plate.ts` for the PNG codec, which is dependency-free.
|
|
@@ -96,13 +99,16 @@ import {
|
|
|
96
99
|
SkeletonJson,
|
|
97
100
|
TextureAtlas,
|
|
98
101
|
TextureAtlasRegion,
|
|
99
|
-
|
|
102
|
+
SkeletonData,
|
|
100
103
|
type Slot,
|
|
101
104
|
} from '@esotericsoftware/spine-core';
|
|
102
|
-
import { readFileSync } from 'node:fs';
|
|
103
|
-
import { join } from 'node:path';
|
|
105
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
106
|
+
import { dirname, join, resolve } from 'node:path';
|
|
104
107
|
import { Plate, readPlate, type RGBA } from '../tools/plate.ts';
|
|
105
108
|
import { pageFootprint } from './atlas.ts';
|
|
109
|
+
import { CoreInputError } from './core/index.ts';
|
|
110
|
+
import { MODEL_DOCUMENT_FILE } from './model.ts';
|
|
111
|
+
import { clipSourceOf, corePoser, inactiveBoneSnapshot, SlotSubsetError, subsetOver, unposedBones } from './render_core.ts';
|
|
106
112
|
|
|
107
113
|
/** Opaque, and light: both of rung 3's parts are dark slate, so is every ground. */
|
|
108
114
|
export const BACKGROUND: RGBA = [232, 232, 232, 255];
|
|
@@ -316,6 +322,12 @@ export interface PieceTexture {
|
|
|
316
322
|
region: string;
|
|
317
323
|
/** Original-art coordinates, `u, v` per vertex, parallel to `uvs`. */
|
|
318
324
|
artUvs: number[];
|
|
325
|
+
/**
|
|
326
|
+
* A clipped piece only (`Mesh.source`): the original-art UVs of each drawn
|
|
327
|
+
* triangle's SOURCE triangle, six numbers per drawn triangle, parallel to
|
|
328
|
+
* `Mesh.source.uvs` — what `substituteTexture` re-seats the source map with.
|
|
329
|
+
*/
|
|
330
|
+
sourceArtUvs?: number[];
|
|
319
331
|
}
|
|
320
332
|
|
|
321
333
|
/** A page-UV rectangle outside which a piece samples nothing. */
|
|
@@ -398,16 +410,10 @@ export interface PoseOptions {
|
|
|
398
410
|
}
|
|
399
411
|
|
|
400
412
|
/**
|
|
401
|
-
* Why a slot subset cannot be drawn —
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
* A class of its own so `cli.ts` can turn it into a usage refusal (exit 2,
|
|
405
|
-
* nothing written) without reading a message to decide what kind it is.
|
|
413
|
+
* Why a slot subset cannot be drawn — `./render_core.ts` declares it, so the
|
|
414
|
+
* spine-core poser and the core poser throw one class (issue #968).
|
|
406
415
|
*/
|
|
407
|
-
export
|
|
408
|
-
|
|
409
|
-
/** The flag spelling each half of a subset is refused under — the UI's, since that is who reads it. */
|
|
410
|
-
const SUBSET_FLAG = { slots: '--slot', hidden: '--hide' } as const;
|
|
416
|
+
export { SlotSubsetError };
|
|
411
417
|
|
|
412
418
|
/**
|
|
413
419
|
* A slot subset resolved against a skeleton: which half was asked for, and the
|
|
@@ -449,45 +455,20 @@ export function slotSubsetOf(
|
|
|
449
455
|
opts: Pick<PoseOptions, 'slots' | 'hidden'> | undefined,
|
|
450
456
|
skin: string | undefined,
|
|
451
457
|
): SlotSubset | undefined {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
unknown.length === 0
|
|
467
|
-
? 'was given no slot name'
|
|
468
|
-
: `${unknown.map((name) => JSON.stringify(name)).join(', ')} ${unknown.length === 1 ? 'names' : 'name'} no slot`;
|
|
469
|
-
throw new SlotSubsetError(
|
|
470
|
-
`${flag} ${named}; this skeleton declares, in draw order: ${declared.join(', ') || 'none'} (${declared.length})`,
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const resolving = new Set([skin ?? null, data.defaultSkin?.name ?? null]);
|
|
475
|
-
const underThisPose =
|
|
476
|
-
skin === undefined ? 'under no skin (the default skin alone)' : `under skin ${JSON.stringify(skin)}`;
|
|
477
|
-
for (const name of asked) {
|
|
478
|
-
const index = data.findSlot(name)?.index ?? -1;
|
|
479
|
-
const carriers = data.skins.filter((s) => s.getAttachments().some((entry) => entry.slotIndex === index));
|
|
480
|
-
if (carriers.length === 0 || carriers.some((s) => resolving.has(s.name))) continue;
|
|
481
|
-
const skins = carriers.map((s) => JSON.stringify(s.name));
|
|
482
|
-
throw new SlotSubsetError(
|
|
483
|
-
`${flag} ${JSON.stringify(name)} draws nothing ${underThisPose}: its attachments are declared only under ` +
|
|
484
|
-
`${skins.length === 1 ? 'skin' : 'skins'} ${skins.join(', ')} — pass --skin ${
|
|
485
|
-
skins.length === 1 ? skins[0] : 'with one of them'
|
|
486
|
-
}`,
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
const chosen = new Set(asked);
|
|
490
|
-
return { mode, names: declared.filter((name) => chosen.has(name)) };
|
|
458
|
+
// The rule is `subsetOver`'s (`./render_core.ts`), shared with the core
|
|
459
|
+
// poser; this is the roster a parsed Spine skeleton gives it.
|
|
460
|
+
return subsetOver(
|
|
461
|
+
{
|
|
462
|
+
declared: data.slots.map((slot) => slot.name),
|
|
463
|
+
carriers: (name) => {
|
|
464
|
+
const index = data.findSlot(name)?.index ?? -1;
|
|
465
|
+
return data.skins.filter((s) => s.getAttachments().some((entry) => entry.slotIndex === index)).map((s) => s.name);
|
|
466
|
+
},
|
|
467
|
+
defaultSkin: data.defaultSkin?.name ?? null,
|
|
468
|
+
},
|
|
469
|
+
opts,
|
|
470
|
+
skin,
|
|
471
|
+
);
|
|
491
472
|
}
|
|
492
473
|
|
|
493
474
|
/**
|
|
@@ -527,18 +508,6 @@ function skeletonUnderSkin(data: SkeletonData, skin: string | undefined): Skelet
|
|
|
527
508
|
return skeleton;
|
|
528
509
|
}
|
|
529
510
|
|
|
530
|
-
/**
|
|
531
|
-
* The same options with the skin taken off — what the samplers hand `piecesOf`.
|
|
532
|
-
*
|
|
533
|
-
* Spelled once, so the one function that must not see a `skin` cannot come to
|
|
534
|
-
* see one because a second call site forgot.
|
|
535
|
-
*/
|
|
536
|
-
function piecesOptions(opts: PoseOptions | undefined): PoseOptions | undefined {
|
|
537
|
-
if (opts === undefined || opts.skin === undefined) return opts;
|
|
538
|
-
const { skin: _applied, ...rest } = opts;
|
|
539
|
-
return rest;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
511
|
/**
|
|
543
512
|
* One bone's world transform in one posed frame.
|
|
544
513
|
*
|
|
@@ -571,9 +540,17 @@ export interface BoneSnapshot {
|
|
|
571
540
|
scaleY: number;
|
|
572
541
|
}
|
|
573
542
|
|
|
574
|
-
/**
|
|
543
|
+
/**
|
|
544
|
+
* Every bone's world transform in the skeleton's own declaration order — a bone
|
|
545
|
+
* the posed skin leaves unposed (inactive, or below an inactive bone) written as the zero transform
|
|
546
|
+
* (`inactiveBoneSnapshot` in `./render_core.ts`: it is not posed, and what a
|
|
547
|
+
* constraint left in its matrix is not a pose — issue #968).
|
|
548
|
+
*/
|
|
575
549
|
export function boneSnapshots(skeleton: Skeleton): BoneSnapshot[] {
|
|
550
|
+
const unposed = unposedBones(skeleton.bones.map((bone) => ({ name: bone.data.name, parent: bone.parent?.data.name ?? null, active: bone.active })));
|
|
576
551
|
return skeleton.bones.map((bone) => {
|
|
552
|
+
// Not posed under this skin: the seam's zero snapshot (`inactiveBoneSnapshot`, issue #968).
|
|
553
|
+
if (unposed.has(bone.data.name)) return inactiveBoneSnapshot(bone.data.name);
|
|
577
554
|
const pose = bone.appliedPose;
|
|
578
555
|
return {
|
|
579
556
|
name: bone.data.name,
|
|
@@ -614,6 +591,23 @@ export interface Mesh extends PieceCommon {
|
|
|
614
591
|
uvs: ArrayLike<number>;
|
|
615
592
|
/** Vertex index triplets. */
|
|
616
593
|
triangles: ArrayLike<number>;
|
|
594
|
+
/**
|
|
595
|
+
* A piece a clip cut only: for each drawn triangle, in triangle order, the
|
|
596
|
+
* SOURCE triangle it was cut from — its three world corners and their page
|
|
597
|
+
* UVs, six numbers each per drawn triangle (`ClipSource`). The rasteriser
|
|
598
|
+
* samples such a triangle's pixels at the source triangle's affine UV map,
|
|
599
|
+
* so the picture does not depend on which convex pieces the clipper cut
|
|
600
|
+
* (issue #964). Absent on every piece no clip cut, whose path is unchanged.
|
|
601
|
+
*/
|
|
602
|
+
source?: ClipSource;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** The source triangles of a clipped piece's drawn triangles — see `Mesh.source`. */
|
|
606
|
+
export interface ClipSource {
|
|
607
|
+
/** Three world corners (`x, y` each) per drawn triangle. */
|
|
608
|
+
world: number[];
|
|
609
|
+
/** The page UVs of those corners, parallel to `world`. */
|
|
610
|
+
uvs: number[];
|
|
617
611
|
}
|
|
618
612
|
|
|
619
613
|
/** One drawable in a posed frame. */
|
|
@@ -740,50 +734,134 @@ export function posableFromText(skeletonText: string, atlasText: string, atlasDi
|
|
|
740
734
|
return { data, pages };
|
|
741
735
|
}
|
|
742
736
|
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
// the posing seam (issue #965, step 3a of #380)
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
//
|
|
741
|
+
// ⭐ Everything this file reads off a posed skeleton goes through one
|
|
742
|
+
// interface, `Poser`, and the samplers below are written against it alone. The
|
|
743
|
+
// implementation behind it today is spine-core's (`spinePoser`, further down);
|
|
744
|
+
// the core-backed poser of issue #968 is a SECOND implementation of the same
|
|
745
|
+
// interface, handed to the same samplers, rather than a rewrite of them. What
|
|
746
|
+
// the interface fixes is exactly what a consumer of a posed frame reads:
|
|
747
|
+
//
|
|
748
|
+
// - the setup pose, under a skin or under none;
|
|
749
|
+
// - an animation's frames at `i/fps` for `i = 0..count`, one continuous
|
|
750
|
+
// trajectory stepped once per frame (the stepping recipe is the
|
|
751
|
+
// implementation's; the schedule — which `i`, what `count`, what time a
|
|
752
|
+
// frame is filed under — is the sampler's, and stays here);
|
|
753
|
+
// - per posed frame: the pieces in draw order (world vertices, page UVs,
|
|
754
|
+
// triangles, tint, dark, page, clip output), the bone snapshots, and every
|
|
755
|
+
// slot's whole attachment geometry;
|
|
756
|
+
// - the rest table, and the names a geometry file and a slot subset read.
|
|
757
|
+
//
|
|
758
|
+
// The framing samples are not a fourth entry: `framingViewport` is the same
|
|
759
|
+
// animation entry at `FRAMING_FPS` with the clip off.
|
|
760
|
+
//
|
|
761
|
+
// 🔒 What stays spine-core's and outside the seam, deliberately: the atlas
|
|
762
|
+
// (`posableFromText`'s pages, `substituteTexture`'s region lookup — step 3c of
|
|
763
|
+
// #380) and `nonFiniteOfPosed`, which `validate.ts` calls on a spine-core
|
|
764
|
+
// skeleton it stepped itself (A10), so the round trip keeps its spine-core
|
|
765
|
+
// entry whatever poses the renders.
|
|
766
|
+
|
|
767
|
+
/** What a sampler hands a posed frame's draw walk — `PoseOptions` with the subset already resolved. */
|
|
768
|
+
export interface DrawOptions {
|
|
769
|
+
/** The slots drawn, resolved against the posed skeleton (`slotSubsetOf`); `undefined` draws every slot. */
|
|
770
|
+
subset: SlotSubset | undefined;
|
|
771
|
+
/** Every attachment whole, no clip applied — the framing box's reading. */
|
|
772
|
+
unclipped: boolean;
|
|
773
|
+
/** Also record each piece's original-art UVs — see `PieceTexture`. */
|
|
774
|
+
texture: boolean;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* One posed moment, readable only while the `Poser` call that produced it is
|
|
779
|
+
* running: an implementation may step one skeleton in place, so a reader takes
|
|
780
|
+
* what it needs before the next frame is posed.
|
|
781
|
+
*/
|
|
782
|
+
export interface Posed {
|
|
783
|
+
/** The drawables in draw order — see `piecesOf` for the walk and the clip. */
|
|
784
|
+
pieces(draw: DrawOptions): Piece[];
|
|
785
|
+
/** Every bone's world transform, in the skeleton's declaration order. */
|
|
786
|
+
bones(): BoneSnapshot[];
|
|
787
|
+
/** Every slot's region or mesh attachment, whole, in the posed draw order. */
|
|
788
|
+
attachments(): AttachmentPose[];
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* The posing seam: one skeleton, posed on demand.
|
|
793
|
+
*
|
|
794
|
+
* Named for what it does rather than for the runtime behind it, because the
|
|
795
|
+
* point of the name is that there are two: `spinePoser` today, the core's at
|
|
796
|
+
* #968. Every sampler, the framing, the geometry export and the non-finite
|
|
797
|
+
* sentence take one (or a `SkeletonData`, which they wrap in `spinePoser`).
|
|
798
|
+
*/
|
|
799
|
+
export interface Poser {
|
|
800
|
+
/** Every animation, in declaration order, with its duration in seconds. */
|
|
801
|
+
readonly animations: ReadonlyArray<{ name: string; duration: number }>;
|
|
802
|
+
/** Every bone in declaration order, and its parent's name. */
|
|
803
|
+
readonly bones: ReadonlyArray<{ name: string; parent: string | null }>;
|
|
804
|
+
/** Every slot in declaration order, and the bone it hangs from. */
|
|
805
|
+
readonly slots: ReadonlyArray<{ name: string; bone: string }>;
|
|
806
|
+
/** `slotSubsetOf` against this skeleton posed under `skin` — refused by `SlotSubsetError`. */
|
|
807
|
+
subset(opts: Pick<PoseOptions, 'slots' | 'hidden'> | undefined, skin: string | undefined): SlotSubset | undefined;
|
|
808
|
+
/** The setup pose under `skin` (absent: no skin set at all). */
|
|
809
|
+
setup(skin: string | undefined): Posed;
|
|
810
|
+
/**
|
|
811
|
+
* Animation `name` under `skin`, not looping: `visit(i, posed)` for every
|
|
812
|
+
* `i` from 0 to `count`, the pose at `i/fps`. The caller has checked `name`.
|
|
813
|
+
*/
|
|
814
|
+
animation(name: string, skin: string | undefined, fps: number, count: number, visit: (index: number, posed: Posed) => void): void;
|
|
815
|
+
/** The rest table for the (slot, attachment) pairs `shown` holds — see `AttachmentRest`. */
|
|
816
|
+
rest(skin: string | undefined, shown: readonly AttachmentPose[][]): AttachmentRest[];
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** What every sampler takes: a poser, or spine-core's parsed skeleton, which is posed through `spinePoser`. */
|
|
820
|
+
export type PoseSource = Poser | SkeletonData;
|
|
821
|
+
|
|
822
|
+
function poserOf(source: PoseSource): Poser {
|
|
823
|
+
return source instanceof SkeletonData ? spinePoser(source) : source;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** A sampler's `PoseOptions` as a draw walk reads them, the subset resolved once, on the first frame posed. */
|
|
827
|
+
function drawResolver(poser: Poser, opts: PoseOptions | undefined): () => DrawOptions {
|
|
828
|
+
let draw: DrawOptions | undefined;
|
|
829
|
+
return () => {
|
|
830
|
+
draw ??= { subset: poser.subset(opts, opts?.skin), unclipped: opts?.unclipped === true, texture: opts?.texture === true };
|
|
831
|
+
return draw;
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** One frame off one posed moment, with what `opts` asked to record beside the pieces. */
|
|
836
|
+
function frameOf(index: number, time: number, posed: Posed, draw: DrawOptions, opts: PoseOptions | undefined): Frame {
|
|
837
|
+
return {
|
|
838
|
+
index,
|
|
839
|
+
time,
|
|
840
|
+
pieces: posed.pieces(draw),
|
|
841
|
+
...(opts?.bones ? { bones: posed.bones() } : {}),
|
|
842
|
+
...(opts?.geometry ? { attachments: posed.attachments() } : {}),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
743
846
|
/**
|
|
744
847
|
* Sample one animation at a fixed rate and collect the posed pieces per frame.
|
|
745
848
|
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
* takes a `MixFrom` that only the state machine has any business choosing.
|
|
849
|
+
* Frame `i` is the pose at `i/fps`, for `i = 0..round(duration·fps)` — the
|
|
850
|
+
* schedule is this function's; how a pose reaches `i/fps` is the poser's.
|
|
749
851
|
*/
|
|
750
|
-
export function sampleAnimation(
|
|
751
|
-
const
|
|
852
|
+
export function sampleAnimation(source: PoseSource, name: string, fps: number, opts?: PoseOptions): Frame[] {
|
|
853
|
+
const poser = poserOf(source);
|
|
854
|
+
const animation = poser.animations.find((a) => a.name === name);
|
|
752
855
|
if (!animation) {
|
|
753
856
|
throw new Error(
|
|
754
|
-
`no animation "${name}" in this skeleton; it has [${
|
|
857
|
+
`no animation "${name}" in this skeleton; it has [${poser.animations.map((a) => a.name).join(', ') || 'none'}]`,
|
|
755
858
|
);
|
|
756
859
|
}
|
|
757
|
-
const skeleton = skeletonUnderSkin(data, opts?.skin);
|
|
758
|
-
const pieceOpts = piecesOptions(opts);
|
|
759
|
-
const state = new AnimationState(new AnimationStateData(data));
|
|
760
|
-
// Not looping: the last frame sits at the animation's duration, and a looping
|
|
761
|
-
// entry would wrap it back onto the first pose.
|
|
762
|
-
state.setAnimation(0, name, false);
|
|
763
|
-
skeleton.setupPose();
|
|
764
|
-
|
|
765
860
|
const step = 1 / fps;
|
|
766
861
|
const count = Math.round(animation.duration * fps);
|
|
862
|
+
const draw = drawResolver(poser, opts);
|
|
767
863
|
const frames: Frame[] = [];
|
|
768
|
-
|
|
769
|
-
if (i > 0) {
|
|
770
|
-
state.update(step);
|
|
771
|
-
state.apply(skeleton);
|
|
772
|
-
skeleton.update(step);
|
|
773
|
-
skeleton.updateWorldTransform(Physics.update);
|
|
774
|
-
} else {
|
|
775
|
-
state.apply(skeleton);
|
|
776
|
-
skeleton.update(0);
|
|
777
|
-
skeleton.updateWorldTransform(Physics.reset);
|
|
778
|
-
}
|
|
779
|
-
frames.push({
|
|
780
|
-
index: i,
|
|
781
|
-
time: i * step,
|
|
782
|
-
pieces: piecesOf(skeleton, pieceOpts),
|
|
783
|
-
...(opts?.bones ? { bones: boneSnapshots(skeleton) } : {}),
|
|
784
|
-
...(opts?.geometry ? { attachments: attachmentsOf(skeleton) } : {}),
|
|
785
|
-
});
|
|
786
|
-
}
|
|
864
|
+
poser.animation(name, opts?.skin, fps, count, (i, posed) => frames.push(frameOf(i, i * step, posed, draw(), opts)));
|
|
787
865
|
return frames;
|
|
788
866
|
}
|
|
789
867
|
|
|
@@ -795,17 +873,69 @@ export function sampleAnimation(data: SkeletonData, name: string, fps: number, o
|
|
|
795
873
|
* ladder's first rung ships one (`1-weight-and-mass`'s second export), and its
|
|
796
874
|
* whole content is the setup pose.
|
|
797
875
|
*/
|
|
798
|
-
export function sampleSetupPose(
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
876
|
+
export function sampleSetupPose(source: PoseSource, opts?: PoseOptions): Frame[] {
|
|
877
|
+
const poser = poserOf(source);
|
|
878
|
+
const posed = poser.setup(opts?.skin);
|
|
879
|
+
return [frameOf(0, 0, posed, drawResolver(poser, opts)(), opts)];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ---------------------------------------------------------------------------
|
|
883
|
+
// the spine-core implementation of the seam
|
|
884
|
+
// ---------------------------------------------------------------------------
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* `Poser` over spine-core: the runtime's own `Skeleton`, `AnimationState` and
|
|
888
|
+
* `SkeletonClipping`, stepped the way a runtime steps them.
|
|
889
|
+
*
|
|
890
|
+
* The pose is driven through `AnimationState` rather than `Animation.apply`
|
|
891
|
+
* because that is the path a runtime actually takes, and 4.3's
|
|
892
|
+
* `Animation.apply` takes a `MixFrom` that only the state machine has any
|
|
893
|
+
* business choosing. Frame 0 applies, updates by 0 and resets physics; every
|
|
894
|
+
* later frame is one `state.update(1/fps)`, apply, `skeleton.update(1/fps)` and
|
|
895
|
+
* `Physics.update` — one continuous trajectory, so the track time of frame `i`
|
|
896
|
+
* is the sum of `i` steps.
|
|
897
|
+
*/
|
|
898
|
+
export function spinePoser(data: SkeletonData): Poser {
|
|
899
|
+
return {
|
|
900
|
+
animations: data.animations.map((a) => ({ name: a.name, duration: a.duration })),
|
|
901
|
+
bones: data.bones.map((bone) => ({ name: bone.name, parent: bone.parent?.name ?? null })),
|
|
902
|
+
slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
|
|
903
|
+
subset: (opts, skin) => slotSubsetOf(data, opts, skin),
|
|
904
|
+
setup: (skin) => spinePosed(setupPosed(data, skin)),
|
|
905
|
+
animation: (name, skin, fps, count, visit) => {
|
|
906
|
+
const skeleton = skeletonUnderSkin(data, skin);
|
|
907
|
+
const state = new AnimationState(new AnimationStateData(data));
|
|
908
|
+
// Not looping: the last frame sits at the animation's duration, and a looping
|
|
909
|
+
// entry would wrap it back onto the first pose.
|
|
910
|
+
state.setAnimation(0, name, false);
|
|
911
|
+
skeleton.setupPose();
|
|
912
|
+
const step = 1 / fps;
|
|
913
|
+
const posed = spinePosed(skeleton);
|
|
914
|
+
for (let i = 0; i <= count; i++) {
|
|
915
|
+
if (i > 0) {
|
|
916
|
+
state.update(step);
|
|
917
|
+
state.apply(skeleton);
|
|
918
|
+
skeleton.update(step);
|
|
919
|
+
skeleton.updateWorldTransform(Physics.update);
|
|
920
|
+
} else {
|
|
921
|
+
state.apply(skeleton);
|
|
922
|
+
skeleton.update(0);
|
|
923
|
+
skeleton.updateWorldTransform(Physics.reset);
|
|
924
|
+
}
|
|
925
|
+
visit(i, posed);
|
|
926
|
+
}
|
|
807
927
|
},
|
|
808
|
-
|
|
928
|
+
rest: (skin, shown) => restOf(data, skin, shown),
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** A spine-core skeleton as it stands posed, read through the seam's `Posed`. */
|
|
933
|
+
function spinePosed(skeleton: Skeleton): Posed {
|
|
934
|
+
return {
|
|
935
|
+
pieces: (draw) => drawPieces(skeleton, draw),
|
|
936
|
+
bones: () => boneSnapshots(skeleton),
|
|
937
|
+
attachments: () => attachmentsOf(skeleton),
|
|
938
|
+
};
|
|
809
939
|
}
|
|
810
940
|
|
|
811
941
|
/**
|
|
@@ -826,13 +956,135 @@ function setupPosed(data: SkeletonData, skin: string | undefined): Skeleton {
|
|
|
826
956
|
* filed under. A skeleton with no animation at all contributes its setup pose
|
|
827
957
|
* under `SETUP_POSE_DIR`.
|
|
828
958
|
*/
|
|
829
|
-
export function sampleAll(
|
|
959
|
+
export function sampleAll(source: PoseSource, fps: number, opts?: PoseOptions): Map<string, Frame[]> {
|
|
960
|
+
const poser = poserOf(source);
|
|
830
961
|
const out = new Map<string, Frame[]>();
|
|
831
|
-
if (
|
|
832
|
-
else for (const animation of
|
|
962
|
+
if (poser.animations.length === 0) out.set(SETUP_POSE_DIR, sampleSetupPose(poser, opts));
|
|
963
|
+
else for (const animation of poser.animations) out.set(animation.name, sampleAnimation(poser, animation.name, fps, opts));
|
|
833
964
|
return out;
|
|
834
965
|
}
|
|
835
966
|
|
|
967
|
+
// ---------------------------------------------------------------------------
|
|
968
|
+
// which poser a render poses through (issue #968, step 3d of #380)
|
|
969
|
+
// ---------------------------------------------------------------------------
|
|
970
|
+
//
|
|
971
|
+
// ⭐ Two implementations of the seam, chosen by what the input carries and
|
|
972
|
+
// never guessed: a rigc build writes `skeleton.model.json` beside the Spine
|
|
973
|
+
// pair, and that document is what the core poses (`./render_core.ts`); a
|
|
974
|
+
// Spine export (`bench/reference/*`, `examples/*/export/*`) has none and
|
|
975
|
+
// poses through spine-core. The choice, and the reason for it, is returned
|
|
976
|
+
// beside the result so the caller can say it (`render` prints it as its
|
|
977
|
+
// `poser` line) — a render that fell back without saying so would be a
|
|
978
|
+
// second opinion about the pose that nobody could see.
|
|
979
|
+
|
|
980
|
+
/** Which implementation of the seam posed a render: rigc's own core, or spine-core. */
|
|
981
|
+
export type PoserName = 'core' | 'spine';
|
|
982
|
+
|
|
983
|
+
/** The `--poser` spellings, in the order the usage lists them. */
|
|
984
|
+
export const POSER_NAMES: readonly PoserName[] = ['core', 'spine'];
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* A `--poser` the input cannot carry — `--poser core` on a Spine export, or on
|
|
988
|
+
* a build whose document the core refuses. A class of its own so `cli.ts`
|
|
989
|
+
* refuses it as a usage error (exit 2, nothing written).
|
|
990
|
+
*/
|
|
991
|
+
export class PoserChoiceError extends Error {}
|
|
992
|
+
|
|
993
|
+
/** Both posers for one input, and why the core one is or is not there. */
|
|
994
|
+
export interface PoserChoice {
|
|
995
|
+
/** What `--poser` asked for, or `undefined` for the input's own choice. */
|
|
996
|
+
forced: PoserName | undefined;
|
|
997
|
+
/** The core poser, or `null` when the input cannot carry it (`why`). */
|
|
998
|
+
core: Poser | null;
|
|
999
|
+
/** For a core poser, the document it poses; otherwise why there is none. */
|
|
1000
|
+
why: string;
|
|
1001
|
+
spine: Poser;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* The first way a model document's rosters differ from the Spine skeleton they
|
|
1006
|
+
* sit beside, or `null` — a document from another build would pose another
|
|
1007
|
+
* rig in the same files' name, so it is refused rather than drawn.
|
|
1008
|
+
*/
|
|
1009
|
+
function rosterDifference(core: Poser, data: SkeletonData): string | null {
|
|
1010
|
+
const bones = (list: ReadonlyArray<{ name: string; parent: string | null }>): string => list.map((b) => `${b.name}<${b.parent ?? ''}`).join('|');
|
|
1011
|
+
const slots = (list: ReadonlyArray<{ name: string; bone: string }>): string => list.map((x) => `${x.name}@${x.bone}`).join('|');
|
|
1012
|
+
const spine = spinePoser(data);
|
|
1013
|
+
if (bones(core.bones) !== bones(spine.bones)) return 'the bones (names, parents or order) differ';
|
|
1014
|
+
if (slots(core.slots) !== slots(spine.slots)) return 'the slots (names, bones or draw order) differ';
|
|
1015
|
+
const animations = (list: ReadonlyArray<{ name: string; duration: number }>): string =>
|
|
1016
|
+
list.map((a) => `${a.name}=${a.duration}`).sort().join('|');
|
|
1017
|
+
if (animations(core.animations) !== animations(spine.animations)) return 'the animations (names or durations) differ';
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Both posers for the skeleton at `skeletonPath` drawn through the atlas at
|
|
1023
|
+
* `atlasPath` — the core one when `skeleton.model.json` sits beside the
|
|
1024
|
+
* skeleton, the skeleton's bytes hash to the digest the document records
|
|
1025
|
+
* (`spine.sha256`: it is the file that build wrote, not one edited after it),
|
|
1026
|
+
* the atlas is the one beside it too, the core reads both and the document's
|
|
1027
|
+
* rosters are the skeleton's. `forced` is `--poser`; `core` on an
|
|
1028
|
+
* input that cannot carry it is refused by name (`PoserChoiceError`).
|
|
1029
|
+
*/
|
|
1030
|
+
export function candidatePosers(
|
|
1031
|
+
data: SkeletonData,
|
|
1032
|
+
skeletonPath: string,
|
|
1033
|
+
atlasPath: string,
|
|
1034
|
+
forced: PoserName | undefined,
|
|
1035
|
+
/** What builds the core poser: `corePoser` — the suite's `RC02` passes a planted copy, and nothing else passes any. */
|
|
1036
|
+
make: (modelText: string, atlasText: string, where: string, skeleton: { path: string; bytes: Uint8Array }) => Poser = corePoser,
|
|
1037
|
+
): PoserChoice {
|
|
1038
|
+
const spine = spinePoser(data);
|
|
1039
|
+
const dir = dirname(resolve(skeletonPath));
|
|
1040
|
+
const modelPath = join(dir, MODEL_DOCUMENT_FILE);
|
|
1041
|
+
let core: Poser | null = null;
|
|
1042
|
+
let why: string;
|
|
1043
|
+
if (forced === 'spine') why = '--poser spine';
|
|
1044
|
+
else if (!existsSync(modelPath)) why = `no ${MODEL_DOCUMENT_FILE} beside ${resolve(skeletonPath)} — a Spine export, not a rigc build`;
|
|
1045
|
+
else if (dirname(resolve(atlasPath)) !== dir) {
|
|
1046
|
+
why =
|
|
1047
|
+
`the atlas ${resolve(atlasPath)} is not beside ${modelPath}: the document's region trims are its own build's ` +
|
|
1048
|
+
"atlas's, and the core would pose them against another one's pages";
|
|
1049
|
+
} else {
|
|
1050
|
+
try {
|
|
1051
|
+
const candidate = make(readFileSync(modelPath, 'utf8'), readFileSync(atlasPath, 'utf8'), modelPath, { path: resolve(skeletonPath), bytes: readFileSync(skeletonPath) });
|
|
1052
|
+
const differs = rosterDifference(candidate, data);
|
|
1053
|
+
if (differs === null) {
|
|
1054
|
+
core = candidate;
|
|
1055
|
+
why = modelPath;
|
|
1056
|
+
} else why = `${modelPath} does not describe ${resolve(skeletonPath)}: ${differs}`;
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
if (!(err instanceof CoreInputError)) throw err;
|
|
1059
|
+
// The reader names the document itself (`readModel`'s `where`), so a message that already starts with its path is not given it twice.
|
|
1060
|
+
why = `the core refused ${err.message.startsWith(`${modelPath}: `) ? err.message : `${modelPath}: ${err.message}`}`;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (forced === 'core' && core === null) throw new PoserChoiceError(`--poser core: ${why}`);
|
|
1064
|
+
return { forced, core, why, spine };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* `run` through the chosen poser: the core one when there is one, and
|
|
1069
|
+
* spine-core otherwise — or when the core refuses the input partway
|
|
1070
|
+
* (`CoreInputError`, e.g. a clip polygon that is not simple), in
|
|
1071
|
+
* which case `run` starts again from nothing on spine-core and `note` names the
|
|
1072
|
+
* refusal. Under `--poser core` that refusal is a `PoserChoiceError` instead.
|
|
1073
|
+
* `run` must write nothing: a fallback re-runs it whole.
|
|
1074
|
+
*/
|
|
1075
|
+
export function throughPoser<T>(choice: PoserChoice, run: (poser: Poser) => T): { value: T; poser: PoserName; note: string } {
|
|
1076
|
+
if (choice.core !== null) {
|
|
1077
|
+
try {
|
|
1078
|
+
return { value: run(choice.core), poser: 'core', note: `rigc core — ${choice.why}` };
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
if (!(err instanceof CoreInputError)) throw err;
|
|
1081
|
+
if (choice.forced === 'core') throw new PoserChoiceError(`--poser core: the core refused this input: ${err.message}`);
|
|
1082
|
+
return { value: run(choice.spine), poser: 'spine', note: `spine-core — the core refused this input: ${err.message}` };
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return { value: run(choice.spine), poser: 'spine', note: `spine-core — ${choice.why}` };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
836
1088
|
/**
|
|
837
1089
|
* The posed drawables of one frame, in draw order.
|
|
838
1090
|
*
|
|
@@ -868,6 +1120,17 @@ export function sampleAll(data: SkeletonData, fps: number, opts?: PoseOptions):
|
|
|
868
1120
|
* list — and one wholly outside becomes a mesh with no triangle, which keeps the
|
|
869
1121
|
* slot in the frame, undrawn, rather than absent.
|
|
870
1122
|
*
|
|
1123
|
+
* ⭐ A cut piece's pixels are sampled at each drawn triangle's SOURCE triangle's
|
|
1124
|
+
* affine UV (`Mesh.source`), not at its own float32 corner UVs (issue #964):
|
|
1125
|
+
* which convex pieces a clipper cuts a concave polygon into is the clipper's —
|
|
1126
|
+
* spine-core's, the core's, or spine-core's under another spelling of the same
|
|
1127
|
+
* polygon — and only this keeps the picture from depending on it. Measured: the
|
|
1128
|
+
* same notched square spelled from four start vertices moved up to 18 pixels one
|
|
1129
|
+
* level against itself through spine-core before, 0 after; the 19 tree rows'
|
|
1130
|
+
* renders did not move (spineboy-pro's portal, the one clipped row, included).
|
|
1131
|
+
* An unclipped piece carries no `source` and is rasterised as before, byte for
|
|
1132
|
+
* byte.
|
|
1133
|
+
*
|
|
871
1134
|
* The clip is applied whatever `slots`/`hidden` draw: a hidden clip still masks
|
|
872
1135
|
* what is shown, so a subset frame is the whole frame's pixels for those slots.
|
|
873
1136
|
* `unclipped` turns it off for the framing box alone — see its note.
|
|
@@ -888,8 +1151,14 @@ export function piecesOf(skeleton: Skeleton, opts?: PoseOptions): Piece[] {
|
|
|
888
1151
|
// it was set to — so a name that draws nothing is refused here, where the one
|
|
889
1152
|
// application point is, rather than matching no piece in silence.
|
|
890
1153
|
const subset = slotSubsetOf(skeleton.data, opts, skeleton.skin?.name);
|
|
1154
|
+
return drawPieces(skeleton, { subset, unclipped: opts?.unclipped === true, texture: opts?.texture === true });
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** `piecesOf`'s walk over a posed spine-core skeleton, the subset already resolved — the spine poser's `Posed.pieces`. */
|
|
1158
|
+
function drawPieces(skeleton: Skeleton, draw: DrawOptions): Piece[] {
|
|
1159
|
+
const { subset } = draw;
|
|
891
1160
|
const named = subset === undefined ? undefined : new Set(subset.names);
|
|
892
|
-
const clipper =
|
|
1161
|
+
const clipper = draw.unclipped ? null : new SkeletonClipping();
|
|
893
1162
|
const pieces: Piece[] = [];
|
|
894
1163
|
for (const slot of skeleton.drawOrder.appliedPose) {
|
|
895
1164
|
const attachment = slot.appliedPose.attachment;
|
|
@@ -903,7 +1172,7 @@ export function piecesOf(skeleton: Skeleton, opts?: PoseOptions): Piece[] {
|
|
|
903
1172
|
continue;
|
|
904
1173
|
}
|
|
905
1174
|
const drawn = subset === undefined || named === undefined || named.has(slot.data.name) === (subset.mode === 'slots');
|
|
906
|
-
const piece = drawn ? pieceOf(skeleton, slot,
|
|
1175
|
+
const piece = drawn ? pieceOf(skeleton, slot, draw.texture, clipper) : null;
|
|
907
1176
|
if (piece !== null) pieces.push(piece);
|
|
908
1177
|
clipper?.clipEnd(slot);
|
|
909
1178
|
}
|
|
@@ -921,7 +1190,7 @@ const QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
|
|
|
921
1190
|
function pieceOf(
|
|
922
1191
|
skeleton: Skeleton,
|
|
923
1192
|
slot: Slot,
|
|
924
|
-
|
|
1193
|
+
withTexture: boolean,
|
|
925
1194
|
clipper: SkeletonClipping | null,
|
|
926
1195
|
): Piece | null {
|
|
927
1196
|
const pose = slot.appliedPose;
|
|
@@ -946,7 +1215,7 @@ function pieceOf(
|
|
|
946
1215
|
const dark: [number, number, number] | undefined =
|
|
947
1216
|
darkPose === null ? undefined : [darkPose.r, darkPose.g, darkPose.b];
|
|
948
1217
|
const common = { tint, dark, slot: slot.data.name, page: region.page.name };
|
|
949
|
-
const texture =
|
|
1218
|
+
const texture = withTexture ? artUvsOf(attachment, region) : undefined;
|
|
950
1219
|
const uvs = attachment.sequence.getUVs(index);
|
|
951
1220
|
|
|
952
1221
|
let piece: Piece;
|
|
@@ -1032,7 +1301,20 @@ function clippedPiece(piece: Piece, triangles: number[], uvs: Float32Array, clip
|
|
|
1032
1301
|
const world = Array.from(clipper.clippedVerticesTyped);
|
|
1033
1302
|
const clippedUvs = Array.from(clipper.clippedUVsTyped);
|
|
1034
1303
|
const clipped = Array.from(clipper.clippedTrianglesTyped);
|
|
1304
|
+
// Which source triangle each drawn triangle was cut from (`Mesh.source`): the clipper cuts triangle by triangle, so each source
|
|
1305
|
+
// triangle is clipped alone and its drawn triangles counted; the concatenation is held to the whole call's output, vertex for vertex.
|
|
1306
|
+
const sources: number[] = [];
|
|
1307
|
+
const again: number[] = [];
|
|
1308
|
+
for (let t = 0; t + 2 < triangles.length; t += 3) {
|
|
1309
|
+
clipper.clipTrianglesUnpacked(piece.world, 0, triangles.slice(t, t + 3), 3, uvs, 2);
|
|
1310
|
+
for (let k = 0; k < clipper.clippedTrianglesTyped.length; k += 3) sources.push(t / 3);
|
|
1311
|
+
again.push(...clipper.clippedVerticesTyped);
|
|
1312
|
+
}
|
|
1313
|
+
if (sources.length !== clipped.length / 3 || again.length !== world.length || again.some((v, i) => v !== world[i])) {
|
|
1314
|
+
throw new Error(`slot "${piece.slot}": the clipper cut ${clipped.length / 3} triangle(s) in one call and ${sources.length} triangle by triangle, over other vertices — the source of each drawn triangle cannot be named`);
|
|
1315
|
+
}
|
|
1035
1316
|
let texture = piece.texture;
|
|
1317
|
+
const source = clipSourceOf(piece.world, Array.from(uvs), triangles, sources, texture?.artUvs);
|
|
1036
1318
|
if (texture !== undefined) {
|
|
1037
1319
|
clipper.clipTrianglesUnpacked(piece.world, 0, triangles, triangles.length, texture.artUvs, 2);
|
|
1038
1320
|
const artUvs = Array.from(clipper.clippedUVsTyped);
|
|
@@ -1042,10 +1324,10 @@ function clippedPiece(piece: Piece, triangles: number[], uvs: Float32Array, clip
|
|
|
1042
1324
|
`${artUvs.length / 2} for the original-art UVs over the same geometry`,
|
|
1043
1325
|
);
|
|
1044
1326
|
}
|
|
1045
|
-
texture = { region: texture.region, artUvs };
|
|
1327
|
+
texture = { region: texture.region, artUvs, sourceArtUvs: source.artUvs };
|
|
1046
1328
|
}
|
|
1047
1329
|
const { tint, dark, slot, page } = piece;
|
|
1048
|
-
return { kind: 'mesh', tint, dark, slot, page, texture, world, uvs: clippedUvs, triangles: clipped };
|
|
1330
|
+
return { kind: 'mesh', tint, dark, slot, page, texture, world, uvs: clippedUvs, triangles: clipped, source: { world: source.world, uvs: source.uvs } };
|
|
1049
1331
|
}
|
|
1050
1332
|
|
|
1051
1333
|
// ---------------------------------------------------------------------------
|
|
@@ -1166,7 +1448,7 @@ export function sidecarViewport(v: Viewport): FramesSidecar['viewport'] {
|
|
|
1166
1448
|
* vertex (or the bone), when any number in it is not finite.
|
|
1167
1449
|
*/
|
|
1168
1450
|
export function geometryFileOf(
|
|
1169
|
-
|
|
1451
|
+
source: PoseSource,
|
|
1170
1452
|
animation: string | null,
|
|
1171
1453
|
fps: number,
|
|
1172
1454
|
frames: Frame[],
|
|
@@ -1187,7 +1469,8 @@ export function geometryFileOf(
|
|
|
1187
1469
|
time: frame.time,
|
|
1188
1470
|
...geometryFrame(frame, `frame ${frame.index}`),
|
|
1189
1471
|
}));
|
|
1190
|
-
const
|
|
1472
|
+
const poser = poserOf(source);
|
|
1473
|
+
const setupFrame = sampleSetupPose(poser, { ...(skin === undefined ? {} : { skin }), bones: true, geometry: true })[0];
|
|
1191
1474
|
const setup = geometryFrame(setupFrame, 'the setup pose');
|
|
1192
1475
|
const file: GeometryFile = {
|
|
1193
1476
|
spec: GEOMETRY_SPEC,
|
|
@@ -1196,9 +1479,9 @@ export function geometryFileOf(
|
|
|
1196
1479
|
...(skin === undefined ? {} : { skin }),
|
|
1197
1480
|
fps,
|
|
1198
1481
|
viewport: sidecarViewport(viewport),
|
|
1199
|
-
bones:
|
|
1200
|
-
slots:
|
|
1201
|
-
rest:
|
|
1482
|
+
bones: poser.bones.map(({ name, parent }) => ({ name, parent })),
|
|
1483
|
+
slots: poser.slots.map(({ name, bone }) => ({ name, bone })),
|
|
1484
|
+
rest: poser.rest(skin, [setup.attachments, ...posed.map((frame) => frame.attachments)]),
|
|
1202
1485
|
setup,
|
|
1203
1486
|
frames: posed,
|
|
1204
1487
|
};
|
|
@@ -1215,7 +1498,7 @@ export function geometryFileOf(
|
|
|
1215
1498
|
* frames drew. A region's rest corners are read for the sequence frame the setup
|
|
1216
1499
|
* pose resolves, which is the frame its setup pose draws.
|
|
1217
1500
|
*/
|
|
1218
|
-
function restOf(data: SkeletonData, skin: string | undefined, shown: AttachmentPose[][]): AttachmentRest[] {
|
|
1501
|
+
function restOf(data: SkeletonData, skin: string | undefined, shown: readonly AttachmentPose[][]): AttachmentRest[] {
|
|
1219
1502
|
const skeleton = setupPosed(data, skin);
|
|
1220
1503
|
// Slot, then attachment: two maps rather than one joined key, so no pair of
|
|
1221
1504
|
// names can fold into another's entry whatever characters they carry.
|
|
@@ -1384,10 +1667,11 @@ function refuseNonFinite(file: GeometryFile): void {
|
|
|
1384
1667
|
* pays for it.
|
|
1385
1668
|
*/
|
|
1386
1669
|
export function nonFinitePoseOf(
|
|
1387
|
-
|
|
1670
|
+
source: PoseSource,
|
|
1388
1671
|
skin: string | undefined,
|
|
1389
1672
|
sets: ReadonlyArray<{ animation: string | null; fps: number }>,
|
|
1390
1673
|
): string | null {
|
|
1674
|
+
const poser = poserOf(source);
|
|
1391
1675
|
const opts: PoseOptions = { ...(skin === undefined ? {} : { skin }), bones: true, geometry: true };
|
|
1392
1676
|
// Every attachment list the frames showed, for the rest table's roster.
|
|
1393
1677
|
const shown: AttachmentPose[][] = [];
|
|
@@ -1398,11 +1682,11 @@ export function nonFinitePoseOf(
|
|
|
1398
1682
|
shown.push(frame.attachments);
|
|
1399
1683
|
return { where, attachments: frame.attachments, bones: frame.bones };
|
|
1400
1684
|
};
|
|
1401
|
-
const setup = named(sampleSetupPose(
|
|
1685
|
+
const setup = named(sampleSetupPose(poser, opts)[0], 'the setup pose');
|
|
1402
1686
|
const frames = sets.flatMap(({ animation, fps }) =>
|
|
1403
1687
|
animation === null
|
|
1404
1688
|
? []
|
|
1405
|
-
: sampleAnimation(
|
|
1689
|
+
: sampleAnimation(poser, animation, fps, opts).map((frame) =>
|
|
1406
1690
|
named(frame, frameWhere(animation, frame.index, frame.time, fps)),
|
|
1407
1691
|
),
|
|
1408
1692
|
);
|
|
@@ -1410,7 +1694,7 @@ export function nonFinitePoseOf(
|
|
|
1410
1694
|
// what they show, and would be posing the same overflow a third time.
|
|
1411
1695
|
const found = nonFiniteSentence(setup, frames, []);
|
|
1412
1696
|
if (found !== null) return found;
|
|
1413
|
-
return nonFiniteSentence({ attachments: [], bones: [] }, [],
|
|
1697
|
+
return nonFiniteSentence({ attachments: [], bones: [] }, [], poser.rest(skin, shown));
|
|
1414
1698
|
}
|
|
1415
1699
|
|
|
1416
1700
|
/**
|
|
@@ -1640,6 +1924,14 @@ export function substituteTexture(
|
|
|
1640
1924
|
// handled. Calling it rather than repeating it is what keeps this from being
|
|
1641
1925
|
// a second opinion about the atlas format.
|
|
1642
1926
|
MeshAttachment.computeUVs(region, texture.artUvs, uvs);
|
|
1927
|
+
// A clipped piece's source map is re-seated the same way, through the drawing's own coordinates (`Mesh.source`).
|
|
1928
|
+
if (piece.kind === 'mesh' && piece.source !== undefined) {
|
|
1929
|
+
if (texture.sourceArtUvs === undefined) throw new Error(`slot "${piece.slot}": a clipped piece carries no original-art UVs for its source triangles`);
|
|
1930
|
+
const sourceUvs = new Array<number>(texture.sourceArtUvs.length).fill(0);
|
|
1931
|
+
MeshAttachment.computeUVs(region, texture.sourceArtUvs, sourceUvs);
|
|
1932
|
+
pieces.push({ ...piece, page: SUBSTITUTE_PAGE + region.page.name, uvs, uvWindow: windowOf(region), source: { world: piece.source.world, uvs: sourceUvs } });
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1643
1935
|
pieces.push({ ...piece, page: SUBSTITUTE_PAGE + region.page.name, uvs, uvWindow: windowOf(region) });
|
|
1644
1936
|
}
|
|
1645
1937
|
return { frame: { ...frame, pieces }, unmatched };
|
|
@@ -1818,6 +2110,157 @@ export function trimmedUnionBounds(
|
|
|
1818
2110
|
return { minX, minY, maxX, maxY };
|
|
1819
2111
|
}
|
|
1820
2112
|
|
|
2113
|
+
/**
|
|
2114
|
+
* A pose that drew vertices and cannot be framed, because every one of them sits
|
|
2115
|
+
* at one point (issue #997) — refused by the slots that drew, the bone each hangs
|
|
2116
|
+
* from and, where those bones are unposed, the skins that would pose them.
|
|
2117
|
+
*
|
|
2118
|
+
* A `GeometryError`, because it is the same family as the non-finite pose of
|
|
2119
|
+
* #873: a picture that has no size is as unwritable as a vertex at Infinity. It
|
|
2120
|
+
* is a class of its own because the reader's next step is different — there the
|
|
2121
|
+
* rig posed a number no picture can hold, here the invocation posed the rig under
|
|
2122
|
+
* a skin that leaves every drawn bone without a world transform, and `--skin` is
|
|
2123
|
+
* usually the whole fix — so `cli.ts` exits 2 on it, as it does on the other
|
|
2124
|
+
* framing refusal, *nothing to draw*.
|
|
2125
|
+
*
|
|
2126
|
+
* Before it the framing answered a viewport over the one point: `maxSide / 0` is
|
|
2127
|
+
* Infinity, `0 · Infinity` is NaN, and `render` wrote a 0×0 frame set with exit
|
|
2128
|
+
* 0, its summary saying `NaNxNaNpx` and `frames.json` a viewport of width 0 and
|
|
2129
|
+
* scale `null`.
|
|
2130
|
+
*/
|
|
2131
|
+
export class UnframeablePoseError extends GeometryError {}
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* Which bones a skin leaves unposed, and which skins there are — the rig
|
|
2135
|
+
* structure the unframeable sentence names a skin from (issue #997). Not a
|
|
2136
|
+
* poser's: the `Poser` seam carries no skin roster, and both posers pose the one
|
|
2137
|
+
* Spine file this is read from (the core's document is bound to it by hash).
|
|
2138
|
+
*/
|
|
2139
|
+
export interface SkinRoster {
|
|
2140
|
+
/** Every skin, in declaration order. */
|
|
2141
|
+
readonly skins: readonly string[];
|
|
2142
|
+
/** The bones left unposed under `skin` (absent: no skin set) — inactive, or below an inactive bone. */
|
|
2143
|
+
unposedUnder(skin: string | undefined): Set<string>;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/**
|
|
2147
|
+
* The skin roster of a parsed Spine file, as the runtime flags it —
|
|
2148
|
+
* `unposedBones` over a fresh skeleton's `active` under each skin, so Spine's
|
|
2149
|
+
* activation rule is the runtime's and is not restated here. Nothing is posed
|
|
2150
|
+
* until a question is asked: the framing asks one (the skin it frames under,
|
|
2151
|
+
* `slotsOnUnposedBones`) and the refusal asks one per skin.
|
|
2152
|
+
*/
|
|
2153
|
+
export function skinRosterOf(data: SkeletonData): SkinRoster {
|
|
2154
|
+
return {
|
|
2155
|
+
skins: data.skins.map((k) => k.name),
|
|
2156
|
+
unposedUnder: (skin) => {
|
|
2157
|
+
const skeleton = skeletonUnderSkin(data, skin);
|
|
2158
|
+
return unposedBones(skeleton.bones.map((bone) => ({ name: bone.data.name, parent: bone.parent?.data.name ?? null, active: bone.active })));
|
|
2159
|
+
},
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/** The roster behind a pose source: its own when it is Spine data, else the one the caller passed. */
|
|
2164
|
+
function rosterBehind(source: PoseSource, roster: SkinRoster | undefined): SkinRoster | undefined {
|
|
2165
|
+
return source instanceof SkeletonData ? skinRosterOf(source) : roster;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/**
|
|
2169
|
+
* ⭐ **The one derivation of "a drawn slot on an unposed bone"** (issues #997,
|
|
2170
|
+
* #1000): the names of the slots of `slots` whose bone `roster` says `skin`
|
|
2171
|
+
* leaves unposed — inactive itself, or below an inactive bone. Both posers draw
|
|
2172
|
+
* such a slot through the zero matrix, so it draws no pixel and every vertex of
|
|
2173
|
+
* it sits at the origin. `framingViewport` takes these slots off the box
|
|
2174
|
+
* (#1000) and `unframeableSentence` refuses a pose that drew nothing else
|
|
2175
|
+
* (#997), from this one set.
|
|
2176
|
+
*
|
|
2177
|
+
* Empty without a roster: a bare `Poser` carries no skin structure, and what
|
|
2178
|
+
* cannot be read is not guessed — every slot then counts, as it did before.
|
|
2179
|
+
*/
|
|
2180
|
+
export function slotsOnUnposedBones(
|
|
2181
|
+
slots: ReadonlyArray<{ name: string; bone: string }>,
|
|
2182
|
+
skin: string | undefined,
|
|
2183
|
+
roster: SkinRoster | undefined,
|
|
2184
|
+
): Set<string> {
|
|
2185
|
+
if (roster === undefined) return new Set();
|
|
2186
|
+
const unposed = roster.unposedUnder(skin);
|
|
2187
|
+
return new Set(slots.filter((slot) => unposed.has(slot.bone)).map((slot) => slot.name));
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/** How many drawn slots a refusal names before it says how many more there are. */
|
|
2191
|
+
const UNFRAMEABLE_NAMED = 3;
|
|
2192
|
+
|
|
2193
|
+
/**
|
|
2194
|
+
* ⭐ **The one derivation of the unframeable sentence** (issue #997): `null`
|
|
2195
|
+
* when the vertices of `frameSets` span a box with extent, and otherwise the
|
|
2196
|
+
* sentence `UnframeablePoseError` carries. `framingViewport` and `check` both
|
|
2197
|
+
* reach it, so `render`, `render --geometry`, `check` and a library caller
|
|
2198
|
+
* refuse one rig in the same words.
|
|
2199
|
+
*
|
|
2200
|
+
* Two sentences, told apart by what the reader must change:
|
|
2201
|
+
*
|
|
2202
|
+
* - **every drawn slot hangs from a bone the skin leaves unposed** — inactive
|
|
2203
|
+
* itself or below an inactive bone (`unposedBones`). Both posers draw such a
|
|
2204
|
+
* slot through the zero matrix, so every vertex lands on the origin. Each
|
|
2205
|
+
* bone is named with the skins that pose it, read off the runtime's own
|
|
2206
|
+
* `active` flag under each skin rather than restating Spine's rule here.
|
|
2207
|
+
* - otherwise **every drawn vertex sits at one point** — the bones posed, and
|
|
2208
|
+
* collapsed their attachments (a world scale of 0 does).
|
|
2209
|
+
*
|
|
2210
|
+
* `roster` is the skin structure of the Spine file the poser poses
|
|
2211
|
+
* (`skinRosterOf`), which is what says whether a bone is unposed
|
|
2212
|
+
* (`slotsOnUnposedBones`, the set the framing box leaves out) and which skins
|
|
2213
|
+
* pose it. Without it the first sentence cannot be told from the second, and
|
|
2214
|
+
* the second is what is said.
|
|
2215
|
+
*
|
|
2216
|
+
* Distinct from *nothing to draw*: that is a skeleton that posed no vertex at
|
|
2217
|
+
* all, and its fix is art; this one posed vertices, and they have no place.
|
|
2218
|
+
*/
|
|
2219
|
+
export function unframeableSentence(
|
|
2220
|
+
frameSets: ReadonlyArray<readonly Frame[]>,
|
|
2221
|
+
slots: ReadonlyArray<{ name: string; bone: string }>,
|
|
2222
|
+
skin: string | undefined,
|
|
2223
|
+
roster: SkinRoster | undefined,
|
|
2224
|
+
): string | null {
|
|
2225
|
+
const box = unionBounds(frameSets.map((frames) => [...frames]));
|
|
2226
|
+
if (![box.minX, box.minY, box.maxX, box.maxY].every(Number.isFinite)) return null;
|
|
2227
|
+
if (box.maxX - box.minX !== 0 || box.maxY - box.minY !== 0) return null;
|
|
2228
|
+
const drew = new Set<string>();
|
|
2229
|
+
for (const frames of frameSets) {
|
|
2230
|
+
for (const frame of frames) for (const piece of frame.pieces) if (piece.world.length > 0) drew.add(piece.slot);
|
|
2231
|
+
}
|
|
2232
|
+
// Declaration order, so the sentence does not depend on which frame drew first.
|
|
2233
|
+
const drawn = slots.filter((slot) => drew.has(slot.name));
|
|
2234
|
+
const under = skin === undefined ? 'under no skin' : `under skin ${JSON.stringify(skin)}`;
|
|
2235
|
+
const counted = `${drawn.length} drawn slot${drawn.length === 1 ? '' : 's'}`;
|
|
2236
|
+
const more = drawn.length > UNFRAMEABLE_NAMED ? `; and ${drawn.length - UNFRAMEABLE_NAMED} more` : '';
|
|
2237
|
+
const named = (describe: (slot: { name: string; bone: string }) => string): string =>
|
|
2238
|
+
drawn.slice(0, UNFRAMEABLE_NAMED).map(describe).join('; ') + more;
|
|
2239
|
+
if (roster !== undefined) {
|
|
2240
|
+
const offBox = slotsOnUnposedBones(slots, skin, roster);
|
|
2241
|
+
if (drawn.length > 0 && drawn.every((slot) => offBox.has(slot.name))) {
|
|
2242
|
+
const bySkin = roster.skins.map((name) => ({ name, unposed: roster.unposedUnder(name) }));
|
|
2243
|
+
const posers = (bone: string): string => {
|
|
2244
|
+
const names = bySkin.filter((k) => !k.unposed.has(bone)).map((k) => JSON.stringify(k.name));
|
|
2245
|
+
if (names.length === 0) return 'which no skin poses';
|
|
2246
|
+
return `which skin${names.length === 1 ? '' : 's'} ${names.join(', ')} pose${names.length === 1 ? 's' : ''}`;
|
|
2247
|
+
};
|
|
2248
|
+
return (
|
|
2249
|
+
`${under}, every drawn slot hangs from a bone that skin leaves unposed — ${counted}: ` +
|
|
2250
|
+
named((slot) => `slot ${JSON.stringify(slot.name)} on bone ${JSON.stringify(slot.bone)}, ${posers(slot.bone)}`) +
|
|
2251
|
+
' — so no drawn vertex has a world transform and the frame would be 0x0: pose it under a skin that poses ' +
|
|
2252
|
+
'those bones (`--skin`), or name the bones in the skin it is posed under'
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return (
|
|
2257
|
+
`${under}, every drawn vertex sits at the one point (${box.minX}, ${box.minY}) — ${counted}: ` +
|
|
2258
|
+
named((slot) => `slot ${JSON.stringify(slot.name)} on bone ${JSON.stringify(slot.bone)}`) +
|
|
2259
|
+
' — so the posed box has no extent and the frame would be 0x0: the bones those slots hang from collapse ' +
|
|
2260
|
+
'every vertex onto it (a world scale of 0 does)'
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1821
2264
|
/**
|
|
1822
2265
|
* The viewport a skeleton is framed to: its union box at `FRAMING_FPS`, padded,
|
|
1823
2266
|
* scaled so the long side is `maxSide` pixels.
|
|
@@ -1827,9 +2270,26 @@ export function trimmedUnionBounds(
|
|
|
1827
2270
|
*
|
|
1828
2271
|
* `null` means the skeleton posed no vertex at all — nothing to draw. A pose
|
|
1829
2272
|
* holding a vertex at Infinity or NaN is refused by a `GeometryError` naming the
|
|
1830
|
-
* bone or vertex and its value (issue #873), never answered `null`.
|
|
2273
|
+
* bone or vertex and its value (issue #873), never answered `null`. A pose whose
|
|
2274
|
+
* every vertex sits at one point is refused by an `UnframeablePoseError`
|
|
2275
|
+
* (`unframeableSentence`, issue #997), never framed at a scale of Infinity.
|
|
2276
|
+
*
|
|
2277
|
+
* The box is over the slots that POSE (issue #1000): a drawn slot whose bone
|
|
2278
|
+
* the skin leaves unposed (`slotsOnUnposedBones`) draws no pixel — both posers
|
|
2279
|
+
* put it through the zero matrix — so its vertices at the origin are not part
|
|
2280
|
+
* of the shot, and counting them framed every frame of a rig that carries one
|
|
2281
|
+
* to a point nothing is drawn at (256x94 where the posed slot alone gives
|
|
2282
|
+
* 256x55). When every drawn slot is such a slot there is no posed box at all,
|
|
2283
|
+
* and that is #997's refusal, never an empty or invented one.
|
|
2284
|
+
*
|
|
2285
|
+
* `roster` is the skin roster behind a `Poser` source (`skinRosterOf`) — what
|
|
2286
|
+
* says which bones the skin leaves unposed, and lets the refusal name the skins
|
|
2287
|
+
* that pose one. Spine data as the source is its own. A bare `Poser` with no
|
|
2288
|
+
* roster cannot tell an unposed bone from a posed one, so every drawn slot
|
|
2289
|
+
* counts there; every CLI caller passes the roster, so both posers frame alike.
|
|
1831
2290
|
*/
|
|
1832
|
-
export function framingViewport(
|
|
2291
|
+
export function framingViewport(source: PoseSource, maxSide: number, opts?: PoseOptions, rosterGiven?: SkinRoster): Viewport | null {
|
|
2292
|
+
const poser = poserOf(source);
|
|
1833
2293
|
// The skin belongs here as much as in the frames: the union box is over the
|
|
1834
2294
|
// attachments that POSE, and two skins fill a slot with art of different sizes
|
|
1835
2295
|
// in different places. Framing one skin's shot with another skin's box would
|
|
@@ -1848,9 +2308,9 @@ export function framingViewport(data: SkeletonData, maxSide: number, opts?: Pose
|
|
|
1848
2308
|
const { slots: _drawn, hidden: _hidden, bones: _bones, geometry: _geometry, ...whole } = opts ?? {};
|
|
1849
2309
|
const framed: PoseOptions = { ...whole, unclipped: true };
|
|
1850
2310
|
const sets =
|
|
1851
|
-
|
|
1852
|
-
? [sampleSetupPose(
|
|
1853
|
-
:
|
|
2311
|
+
poser.animations.length === 0
|
|
2312
|
+
? [sampleSetupPose(poser, framed)]
|
|
2313
|
+
: poser.animations.map((a) => sampleAnimation(poser, a.name, FRAMING_FPS, framed));
|
|
1854
2314
|
// ⚠️ Two different reasons a box is not finite, told apart here and nowhere
|
|
1855
2315
|
// else (issue #873). A skeleton that posed no vertex at all has nothing to
|
|
1856
2316
|
// draw, and that is `null`. One that posed a vertex at Infinity or NaN has a
|
|
@@ -1859,16 +2319,27 @@ export function framingViewport(data: SkeletonData, maxSide: number, opts?: Pose
|
|
|
1859
2319
|
// this, the first case's `null` covered both, and a single overflowing bone
|
|
1860
2320
|
// among finite ones reached neither: its box was finite on one side, and
|
|
1861
2321
|
// `render` wrote a NaN-by-NaN frame set with exit 0.
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
2322
|
+
const drewAny = (frameSets: ReadonlyArray<readonly Frame[]>): boolean =>
|
|
2323
|
+
frameSets.some((frames) => frames.some((frame) => frame.pieces.some((piece) => piece.world.length > 0)));
|
|
2324
|
+
if (!drewAny(sets)) return null;
|
|
2325
|
+
// ⭐ The box is over the slots that pose (issue #1000). A drawn slot on a bone
|
|
2326
|
+
// the skin leaves unposed is taken off — it draws no pixel — unless nothing
|
|
2327
|
+
// else drew, in which case every set is kept whole so the pose reaches #997's
|
|
2328
|
+
// refusal below exactly as it did before this: the same inputs, the same
|
|
2329
|
+
// sentence, never an empty box framed to something.
|
|
2330
|
+
const roster = rosterBehind(source, rosterGiven);
|
|
2331
|
+
const offBox = slotsOnUnposedBones(poser.slots, framed.skin, roster);
|
|
2332
|
+
const posedSets =
|
|
2333
|
+
offBox.size === 0 ? sets : sets.map((frames) => frames.map((frame) => ({ ...frame, pieces: frame.pieces.filter((piece) => !offBox.has(piece.slot)) })));
|
|
2334
|
+
const boxed = drewAny(posedSets) ? posedSets : sets;
|
|
2335
|
+
const box = unionBounds(boxed);
|
|
1865
2336
|
if (![box.minX, box.minY, box.maxX, box.maxY].every(Number.isFinite)) {
|
|
1866
2337
|
const found = nonFinitePoseOf(
|
|
1867
|
-
|
|
2338
|
+
poser,
|
|
1868
2339
|
framed.skin,
|
|
1869
|
-
|
|
2340
|
+
poser.animations.length === 0
|
|
1870
2341
|
? [{ animation: null, fps: FRAMING_FPS }]
|
|
1871
|
-
:
|
|
2342
|
+
: poser.animations.map((a) => ({ animation: a.name, fps: FRAMING_FPS })),
|
|
1872
2343
|
);
|
|
1873
2344
|
// Reaching here with nothing found would mean the pieces and the whole
|
|
1874
2345
|
// attachments disagree about one pose — a defect here, said as one.
|
|
@@ -1880,6 +2351,11 @@ export function framingViewport(data: SkeletonData, maxSide: number, opts?: Pose
|
|
|
1880
2351
|
}
|
|
1881
2352
|
throw new GeometryError(found);
|
|
1882
2353
|
}
|
|
2354
|
+
// A finite box over one point (issue #997): every drawn slot on a bone the
|
|
2355
|
+
// skin leaves unposed, or every vertex collapsed. Framed, it is a scale of
|
|
2356
|
+
// Infinity and a frame of NaN by NaN pixels, written as 0x0 with exit 0.
|
|
2357
|
+
const unframeable = unframeableSentence(boxed, poser.slots, framed.skin, roster);
|
|
2358
|
+
if (unframeable !== null) throw new UnframeablePoseError(unframeable);
|
|
1883
2359
|
const pad = Math.max(box.maxX - box.minX, box.maxY - box.minY) * PAD;
|
|
1884
2360
|
return viewportFor(box.minX - pad, box.minY - pad, box.maxX + pad, box.maxY + pad, maxSide);
|
|
1885
2361
|
}
|
|
@@ -2146,6 +2622,10 @@ export function rasteriseMesh(
|
|
|
2146
2622
|
const u2 = mesh.uvs[i2 * 2];
|
|
2147
2623
|
const v2 = mesh.uvs[i2 * 2 + 1];
|
|
2148
2624
|
|
|
2625
|
+
// A clipped piece (`Mesh.source`, issue #964): the UV of a pixel is the SOURCE triangle's affine map, in doubles from its own
|
|
2626
|
+
// projected corners and page UVs, so which convex pieces the clipper cut changes no pixel. Coverage is still this triangle's.
|
|
2627
|
+
const src = mesh.source === undefined ? null : sourceMap(mesh.source, t / 3, project);
|
|
2628
|
+
|
|
2149
2629
|
for (let y = minY; y <= maxY; y++) {
|
|
2150
2630
|
const sy = y + 0.5;
|
|
2151
2631
|
for (let x = minX; x <= maxX; x++) {
|
|
@@ -2157,11 +2637,15 @@ export function rasteriseMesh(
|
|
|
2157
2637
|
const w2 = (x1 - x0) * (sy - y0) - (y1 - y0) * (sx - x0);
|
|
2158
2638
|
if (topLeft2 ? w2 < 0 : w2 <= 0) continue;
|
|
2159
2639
|
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2640
|
+
let u: number;
|
|
2641
|
+
let v: number;
|
|
2642
|
+
if (src === null) {
|
|
2643
|
+
const b0 = w0 / area;
|
|
2644
|
+
const b1 = w1 / area;
|
|
2645
|
+
const b2 = w2 / area;
|
|
2646
|
+
u = b0 * u0 + b1 * u1 + b2 * u2;
|
|
2647
|
+
v = b0 * v0 + b1 * v1 + b2 * v2;
|
|
2648
|
+
} else [u, v] = src(sx, sy);
|
|
2165
2649
|
if (outsideWindow(mesh.uvWindow, u, v)) continue;
|
|
2166
2650
|
const sample = bilinear(page, u * page.width - 0.5, v * page.height - 0.5);
|
|
2167
2651
|
const alpha = sample[3] * mesh.tint[3];
|
|
@@ -2179,6 +2663,28 @@ export function rasteriseMesh(
|
|
|
2179
2663
|
}
|
|
2180
2664
|
}
|
|
2181
2665
|
|
|
2666
|
+
/**
|
|
2667
|
+
* The UV map of a clipped piece's drawn triangle `t`: its source triangle's
|
|
2668
|
+
* affine map (`Mesh.source`), from the three projected source corners and their
|
|
2669
|
+
* page UVs, in doubles — the barycentric weights of the pixel centre in the
|
|
2670
|
+
* projected source triangle times the corners' UVs. The same for every
|
|
2671
|
+
* decomposition of the source triangle, which is the point (issue #964).
|
|
2672
|
+
*/
|
|
2673
|
+
function sourceMap(source: ClipSource, t: number, project: (wx: number, wy: number) => [number, number]): (sx: number, sy: number) => [number, number] {
|
|
2674
|
+
const o = t * 6;
|
|
2675
|
+
const [ax, ay] = project(source.world[o], source.world[o + 1]);
|
|
2676
|
+
const [bx, by] = project(source.world[o + 2], source.world[o + 3]);
|
|
2677
|
+
const [cx, cy] = project(source.world[o + 4], source.world[o + 5]);
|
|
2678
|
+
const uv = source.uvs;
|
|
2679
|
+
const area = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
2680
|
+
return (sx, sy) => {
|
|
2681
|
+
const wa = ((cx - bx) * (sy - by) - (cy - by) * (sx - bx)) / area;
|
|
2682
|
+
const wb = ((ax - cx) * (sy - cy) - (ay - cy) * (sx - cx)) / area;
|
|
2683
|
+
const wc = ((bx - ax) * (sy - ay) - (by - ay) * (sx - ax)) / area;
|
|
2684
|
+
return [wa * uv[o] + wb * uv[o + 2] + wc * uv[o + 4], wa * uv[o + 1] + wb * uv[o + 3] + wc * uv[o + 5]];
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2182
2688
|
/**
|
|
2183
2689
|
* Rasterise whichever shape this piece is.
|
|
2184
2690
|
*
|