spine-rigc 1.6.2 → 1.8.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/cli.ts +166 -60
- package/docs/AUTHORING.md +25 -16
- package/package.json +1 -1
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/skin_entries.ts +35 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/kinds.ts +102 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/index.ts +130 -0
- package/src/assertions/model/parse.ts +136 -0
- package/src/assertions/model/skin_entries.ts +58 -0
- package/src/assertions/model/slot_colour.ts +80 -0
- package/src/assertions/reasons.ts +127 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +26 -16
- package/src/check.ts +70 -47
- package/src/compile.ts +118 -93
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +102 -15
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +80 -24
- package/src/deformmeasure.ts +383 -157
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +114 -13
- package/src/render.ts +520 -59
- package/src/render_core.ts +178 -21
- package/src/transform.ts +113 -128
- package/src/types.ts +6 -5
- package/src/validate.ts +200 -490
package/src/render.ts
CHANGED
|
@@ -81,7 +81,12 @@
|
|
|
81
81
|
* is no honest way to render one without it. (Since issue #968 that holds for a
|
|
82
82
|
* Spine export: a rigc build is posed by rigc's own core through
|
|
83
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.
|
|
84
|
+
* the export's atlas pages and texture substitution — see *which poser* below.
|
|
85
|
+
* Since issue #1014 a rigc build the core poses touches none of it: what a
|
|
86
|
+
* render reads off the candidate besides the pose comes from its model
|
|
87
|
+
* document and the skeleton's own JSON (`loadCandidate`), and since issue
|
|
88
|
+
* #1020 its pages and a `--texture-from` atlas through rigc's own reader, so
|
|
89
|
+
* spine-core is reached only where it poses an export or a fallback.) The rule that matters is unchanged
|
|
85
90
|
* — `src/compile.ts` must stay independent of the runtime so the compiler and
|
|
86
91
|
* the gate are not checking each other's assumptions — and this file is neither.
|
|
87
92
|
* It also imports `tools/plate.ts` for the PNG codec, which is dependency-free.
|
|
@@ -105,10 +110,22 @@ import {
|
|
|
105
110
|
import { existsSync, readFileSync } from 'node:fs';
|
|
106
111
|
import { dirname, join, resolve } from 'node:path';
|
|
107
112
|
import { Plate, readPlate, type RGBA } from '../tools/plate.ts';
|
|
108
|
-
import { pageFootprint } from './atlas.ts';
|
|
113
|
+
import { pageFootprint, parseAtlasText } from './atlas.ts';
|
|
109
114
|
import { CoreInputError } from './core/index.ts';
|
|
115
|
+
import { computeUvs } from './core/uvs.ts';
|
|
110
116
|
import { MODEL_DOCUMENT_FILE } from './model.ts';
|
|
111
|
-
import {
|
|
117
|
+
import {
|
|
118
|
+
clipSourceOf,
|
|
119
|
+
coreDocumentFacts,
|
|
120
|
+
corePoser,
|
|
121
|
+
inactiveBoneSnapshot,
|
|
122
|
+
SlotSubsetError,
|
|
123
|
+
subsetOver,
|
|
124
|
+
unposedBones,
|
|
125
|
+
type CoreDocumentFacts,
|
|
126
|
+
type SubsetRoster,
|
|
127
|
+
} from './render_core.ts';
|
|
128
|
+
import { walkTimelines } from './timelines.ts';
|
|
112
129
|
|
|
113
130
|
/** Opaque, and light: both of rung 3's parts are dark slate, so is every ground. */
|
|
114
131
|
export const BACKGROUND: RGBA = [232, 232, 232, 255];
|
|
@@ -734,6 +751,274 @@ export function posableFromText(skeletonText: string, atlasText: string, atlasDi
|
|
|
734
751
|
return { data, pages };
|
|
735
752
|
}
|
|
736
753
|
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
// what a render and a check read off a candidate besides its pose (issue #1014)
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
//
|
|
758
|
+
// ⭐ `render` and `check` read a handful of facts off the candidate before and
|
|
759
|
+
// beside the pose: the animation and skin names their flags are checked
|
|
760
|
+
// against, the slot subset's roster, whether a stage is declared, the bone
|
|
761
|
+
// tree `check` draws its chains from, the skin roster the framing reads, and
|
|
762
|
+
// the atlas pages the rasteriser samples. Until issue #1014 every one of them
|
|
763
|
+
// came off the skeleton spine-core had parsed, so a rigc build the core poses
|
|
764
|
+
// still loaded and ran the runtime before its poser was chosen. Now the choice
|
|
765
|
+
// comes first (`loadCandidate`), and a build the core poses reads each fact
|
|
766
|
+
// where it is written: the names, the tree, the subset's roster and the stage
|
|
767
|
+
// off the skeleton's own JSON (`skeletonFacts`), the pages off rigc's atlas
|
|
768
|
+
// reader, and the skin roster off the model document (`coreSkinRoster` in
|
|
769
|
+
// `./render_core.ts`). Every one of those readings was measured equal to the
|
|
770
|
+
// spine-core reading it replaces on every input the tree carries — the
|
|
771
|
+
// nineteen built corpus rows and the twelve editor exports (the PR of #1014
|
|
772
|
+
// carries the counts). A Spine export, `--poser spine` and a fallback the
|
|
773
|
+
// poser line names load spine-core as before and read every fact off it.
|
|
774
|
+
//
|
|
775
|
+
// Since issue #1020 a build the core poses reads what its model document
|
|
776
|
+
// states off the document (`coreFacts`): the bone tree, the slot list and the
|
|
777
|
+
// subset's roster, and the page images by the names its `pages` section gives
|
|
778
|
+
// — so with a `rigc-compiled/2` document the atlas file is not needed at all.
|
|
779
|
+
// What only `skeleton.json` holds is still read there: the order the
|
|
780
|
+
// animations and skins are listed in (the emitter's, not the model's) and the
|
|
781
|
+
// stage.
|
|
782
|
+
|
|
783
|
+
/** What `render` and `check` read off a candidate's skeleton besides its pose. */
|
|
784
|
+
export interface SkeletonFacts {
|
|
785
|
+
/** Every animation name, in the skeleton's own order. */
|
|
786
|
+
readonly animations: readonly string[];
|
|
787
|
+
/** Every skin name, in the skeleton's own order. */
|
|
788
|
+
readonly skins: readonly string[];
|
|
789
|
+
/** Whether the header states a numeric `width` and `height` — a setup stage (issue #714). */
|
|
790
|
+
readonly declaresStage: boolean;
|
|
791
|
+
/** Every bone in declaration order, and its parent's name. */
|
|
792
|
+
readonly bones: ReadonlyArray<{ name: string; parent: string | null }>;
|
|
793
|
+
/** Every slot in declaration order, and the bone it hangs from. */
|
|
794
|
+
readonly slots: ReadonlyArray<{ name: string; bone: string }>;
|
|
795
|
+
/** `slotSubsetOf` over this skeleton — refused by `SlotSubsetError`. */
|
|
796
|
+
subset(opts: Pick<PoseOptions, 'slots' | 'hidden'> | undefined, skin: string | undefined): SlotSubset | undefined;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** The facts as spine-core loaded them — an export's reading, and every candidate's before issue #1014. */
|
|
800
|
+
export function spineFacts(data: SkeletonData): SkeletonFacts {
|
|
801
|
+
return {
|
|
802
|
+
animations: data.animations.map((a) => a.name),
|
|
803
|
+
skins: data.skins.map((s) => s.name),
|
|
804
|
+
// `SkeletonJson` copies both header fields across unconditionally, so an omitted extent is `undefined` here, not 0.
|
|
805
|
+
declaresStage: typeof data.width === 'number' && typeof data.height === 'number',
|
|
806
|
+
bones: data.bones.map((bone) => ({ name: bone.name, parent: bone.parent === null ? null : bone.parent.name })),
|
|
807
|
+
slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
|
|
808
|
+
subset: (opts, skin) => slotSubsetOf(data, opts, skin),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
type JsonObject = Record<string, unknown>;
|
|
813
|
+
|
|
814
|
+
/** A JSON value as an object, or an empty one where it is not one. */
|
|
815
|
+
function objectOf(value: unknown): JsonObject {
|
|
816
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as JsonObject) : {};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** A JSON value as a list of objects, or an empty list where it is not one. */
|
|
820
|
+
function objectsOf(value: unknown): JsonObject[] {
|
|
821
|
+
return Array.isArray(value) ? value.map(objectOf) : [];
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* The same facts off the skeleton's own JSON — a candidate the core poses
|
|
826
|
+
* (issue #1014), whose skeleton spine-core never loads.
|
|
827
|
+
*
|
|
828
|
+
* Each is the file's own statement, read in the file's order: the animation
|
|
829
|
+
* names are the keys of `animations` (the order the parser iterates them), the
|
|
830
|
+
* skins `skins[].name` (the default skin the one named `default`), a slot's
|
|
831
|
+
* carriers the skins whose `attachments` give it at least one entry, the tree
|
|
832
|
+
* `bones[]` and `slots[]`, and the stage `skeleton.width`/`height`. The core
|
|
833
|
+
* poses only a rigc build whose `skeleton.json` hashes to the digest its model
|
|
834
|
+
* document records, so the file read here is the one `build` wrote and the
|
|
835
|
+
* gate round-tripped.
|
|
836
|
+
*/
|
|
837
|
+
export function skeletonFacts(skeletonText: string): SkeletonFacts {
|
|
838
|
+
const root = objectOf(JSON.parse(skeletonText));
|
|
839
|
+
const skins = objectsOf(root.skins);
|
|
840
|
+
const slots = objectsOf(root.slots).map((slot) => ({ name: String(slot.name), bone: String(slot.bone) }));
|
|
841
|
+
const header = objectOf(root.skeleton);
|
|
842
|
+
const roster: SubsetRoster = {
|
|
843
|
+
declared: slots.map((slot) => slot.name),
|
|
844
|
+
carriers: (slot) => skins.filter((skin) => Object.keys(objectOf(objectOf(skin.attachments)[slot])).length > 0).map((skin) => String(skin.name)),
|
|
845
|
+
defaultSkin: skins.some((skin) => skin.name === 'default') ? 'default' : null,
|
|
846
|
+
};
|
|
847
|
+
return {
|
|
848
|
+
animations: Object.keys(objectOf(root.animations)),
|
|
849
|
+
skins: skins.map((skin) => String(skin.name)),
|
|
850
|
+
declaresStage: typeof header.width === 'number' && typeof header.height === 'number',
|
|
851
|
+
bones: objectsOf(root.bones).map((bone) => ({ name: String(bone.name), parent: typeof bone.parent === 'string' ? bone.parent : null })),
|
|
852
|
+
slots,
|
|
853
|
+
subset: (opts, skin) => subsetOver(roster, opts, skin),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Each animation's duration off the skeleton's own JSON, in the file's order —
|
|
859
|
+
* what `rosterDifference` holds a model document's durations to without
|
|
860
|
+
* loading the skeleton through spine-core (issue #1014).
|
|
861
|
+
*
|
|
862
|
+
* ⚠️ Skeleton JSON carries no duration, so this is the parser's derivation of
|
|
863
|
+
* one, and it is stated as measured rather than as read: the LAST key time of
|
|
864
|
+
* each timeline, the largest of them, held as a float32 — equal to
|
|
865
|
+
* spine-core's `Animation.duration` on every animation of every input the tree
|
|
866
|
+
* carries (the PR of #1014). The timelines are the ones `walkTimelines` walks,
|
|
867
|
+
* the walk `A05` and `A12` stand on, so a group it did not descend would be
|
|
868
|
+
* missing from both.
|
|
869
|
+
*/
|
|
870
|
+
function skeletonDurations(root: JsonObject): Array<{ name: string; duration: number }> {
|
|
871
|
+
return Object.entries(objectOf(root.animations)).map(([name, animation]) => {
|
|
872
|
+
let duration = 0;
|
|
873
|
+
walkTimelines({ animations: { [name]: animation } }, (_path, _kind, _timeline, keys) => {
|
|
874
|
+
if (keys.length === 0) return;
|
|
875
|
+
const last = objectOf(keys[keys.length - 1]);
|
|
876
|
+
duration = Math.max(duration, Math.fround(typeof last.time === 'number' ? last.time : 0));
|
|
877
|
+
});
|
|
878
|
+
return { name, duration };
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Every page named, read from `atlasDir` by that name — the images a candidate the core poses is drawn from. */
|
|
883
|
+
function pagesNamed(names: readonly string[], atlasDir: string): Map<string, Plate> {
|
|
884
|
+
const pages = new Map<string, Plate>();
|
|
885
|
+
for (const name of names) pages.set(name, readPlate(join(atlasDir, name)));
|
|
886
|
+
return pages;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* A candidate with no atlas beside it that has to be read through one —
|
|
891
|
+
* refused naming the file and why (issue #1020).
|
|
892
|
+
*
|
|
893
|
+
* A rigc build whose model document states where each region sits on its page
|
|
894
|
+
* (`rigc-compiled/2`, the `pages` section) is drawn by the core without its
|
|
895
|
+
* atlas. Everything else reads one: a Spine export, `--poser spine`, a
|
|
896
|
+
* `rigc-compiled/1` document, and a build the core refuses and spine-core
|
|
897
|
+
* draws instead. A class of its own so `cli.ts` refuses it as an invocation
|
|
898
|
+
* (exit 2, nothing written), as it refuses a missing atlas on an export.
|
|
899
|
+
*/
|
|
900
|
+
export class CandidateAtlasError extends Error {}
|
|
901
|
+
|
|
902
|
+
/** The refusal for a candidate that has to be read through an atlas it does not have. */
|
|
903
|
+
function atlasAbsent(atlasPath: string, label: string, why: string): CandidateAtlasError {
|
|
904
|
+
return new CandidateAtlasError(
|
|
905
|
+
`nothing at ${atlasPath}: ${label} is drawn through its atlas (${why}). ` +
|
|
906
|
+
'Only a rigc build whose skeleton.model.json is a rigc-compiled/2 document, posed by the core, is drawn ' +
|
|
907
|
+
'without one — it states where each region sits on its page',
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Posing an input through spine-core when the runtime cannot be reached —
|
|
913
|
+
* refused naming the input and why it needs the runtime (issue #1014).
|
|
914
|
+
*
|
|
915
|
+
* A Spine export, `--poser spine` and a fallback the poser line names are what
|
|
916
|
+
* spine-core is loaded for; a rigc build the core poses reads nothing through
|
|
917
|
+
* it. So the one run that can meet a runtime it cannot use is one of those
|
|
918
|
+
* three, and it says so rather than surfacing whatever the first access threw.
|
|
919
|
+
*/
|
|
920
|
+
export class SpineRuntimeError extends Error {}
|
|
921
|
+
|
|
922
|
+
/** Touch the runtime once, before anything is parsed through it, and refuse by name when it cannot be used. */
|
|
923
|
+
function requireSpineRuntime(label: string, why: string): void {
|
|
924
|
+
try {
|
|
925
|
+
// A property read on the class the load starts from: no runtime code runs, and a runtime that cannot be used throws here.
|
|
926
|
+
void TextureAtlas.prototype;
|
|
927
|
+
} catch (err) {
|
|
928
|
+
throw new SpineRuntimeError(
|
|
929
|
+
`${label} is posed through spine-core (${why}), and the runtime could not be used: ${(err as Error).message}. ` +
|
|
930
|
+
'spine-core is what poses a Spine export, --poser spine and a fallback the poser line names; ' +
|
|
931
|
+
'a rigc build the core poses reads nothing through it',
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** A candidate as `render` and `check` read it: the posers, the facts and the pages (issue #1014). */
|
|
937
|
+
export interface Candidate {
|
|
938
|
+
choice: PoserChoice;
|
|
939
|
+
facts: SkeletonFacts;
|
|
940
|
+
pages: Map<string, Plate>;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Load a candidate for `render` or `check`, choosing its poser FIRST (issue
|
|
945
|
+
* #1014): a rigc build the core poses reads its facts off its model document
|
|
946
|
+
* and its own JSON (`coreFacts`), its page images by the names the
|
|
947
|
+
* document's `pages` gives (a `rigc-compiled/1` document's, by its atlas's)
|
|
948
|
+
* and its skin roster off the document, and spine-core is never loaded for
|
|
949
|
+
* it; `input.atlasText` is `null` where the atlas file is not there, which only
|
|
950
|
+
* a `rigc-compiled/2` document the core poses is drawn without (issue #1020 —
|
|
951
|
+
* anything else is refused, `CandidateAtlasError`); anything else — a Spine export,
|
|
952
|
+
* `--poser spine`, a candidate handed over as text (`paths` null, `unplaced`
|
|
953
|
+
* the reason) — is loaded through spine-core as `posableFromText` always
|
|
954
|
+
* loaded it. The choice's spine-core poser stays unloaded until something
|
|
955
|
+
* reads it, which on a rigc build is a fallback the poser line names.
|
|
956
|
+
*
|
|
957
|
+
* `--poser core` on an input that cannot carry it is NOT refused here: the
|
|
958
|
+
* caller says it where it always has (`refuseUnchosen`), after the flags that
|
|
959
|
+
* refuse before it.
|
|
960
|
+
*/
|
|
961
|
+
export function loadCandidate(
|
|
962
|
+
input: { skeletonText: string; atlasText: string | null; atlasDir: string; label: string },
|
|
963
|
+
paths: { skeleton: string; atlas: string } | null,
|
|
964
|
+
forced: PoserName | undefined,
|
|
965
|
+
options: { make?: MakeCorePoser; unplaced?: string } = {},
|
|
966
|
+
): Candidate {
|
|
967
|
+
let data: SkeletonData | null = null;
|
|
968
|
+
let choice: PoserChoice | null = null;
|
|
969
|
+
// `null` is an atlas file that is not there (issue #1020): a candidate the core draws from its document needs none, and anything read through one is refused naming the file before the runtime is touched.
|
|
970
|
+
const atlasText = (why: string): string => {
|
|
971
|
+
if (input.atlasText === null) throw atlasAbsent(paths?.atlas ?? '(no atlas)', input.label, why);
|
|
972
|
+
return input.atlasText;
|
|
973
|
+
};
|
|
974
|
+
const spineData = (): SkeletonData => {
|
|
975
|
+
if (data === null) {
|
|
976
|
+
const why = choice === null || choice.core !== null ? 'a fallback from the core poser' : choice.why;
|
|
977
|
+
const text = atlasText(why);
|
|
978
|
+
requireSpineRuntime(input.label, why);
|
|
979
|
+
data = new SkeletonJson(new AtlasAttachmentLoader(new TextureAtlas(text))).readSkeletonData(JSON.parse(input.skeletonText));
|
|
980
|
+
}
|
|
981
|
+
return data;
|
|
982
|
+
};
|
|
983
|
+
const chosen =
|
|
984
|
+
paths === null
|
|
985
|
+
? { choice: posersOver(forced, null, forced === 'spine' ? '--poser spine' : (options.unplaced ?? 'no path to find a model document beside'), spineData), document: null }
|
|
986
|
+
: choosePosers(paths.skeleton, paths.atlas, input.atlasText, forced, spineData, options.make ?? corePoser);
|
|
987
|
+
choice = chosen.choice;
|
|
988
|
+
if (choice.core !== null && chosen.document !== null) {
|
|
989
|
+
// A rigc-compiled/2 document names its pages; a /1 document is posed only with its atlas beside it, which names them (`choosePosers`).
|
|
990
|
+
const names = chosen.document.pageNames ?? parseAtlasText(atlasText(choice.why)).pages.map((page) => page.name);
|
|
991
|
+
return { choice, facts: coreFacts(input.skeletonText, choice.core, chosen.document), pages: pagesNamed(names, input.atlasDir) };
|
|
992
|
+
}
|
|
993
|
+
const text = atlasText(choice.why);
|
|
994
|
+
requireSpineRuntime(input.label, choice.why);
|
|
995
|
+
const posable = posableFromText(input.skeletonText, text, input.atlasDir);
|
|
996
|
+
data = posable.data;
|
|
997
|
+
return { choice, facts: spineFacts(posable.data), pages: posable.pages };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* A candidate's facts when the core poses it (issue #1020): what the model
|
|
1002
|
+
* document states, read from it — the bone tree and the slot list (the core
|
|
1003
|
+
* poser's own, which `rosterDifference` held to the Spine file's before the
|
|
1004
|
+
* core was chosen) and the slot subset's roster (`coreDocumentFacts`) — and,
|
|
1005
|
+
* off the skeleton's own JSON, what it does not: the order the animations and
|
|
1006
|
+
* the skins are listed in (the Spine file's — the emitter's order, not the
|
|
1007
|
+
* model's) and whether a stage is declared (the header's `width`/`height`,
|
|
1008
|
+
* which the document has no field for).
|
|
1009
|
+
*/
|
|
1010
|
+
function coreFacts(skeletonText: string, poser: Poser, document: CoreDocumentFacts): SkeletonFacts {
|
|
1011
|
+
const spine = skeletonFacts(skeletonText);
|
|
1012
|
+
return {
|
|
1013
|
+
animations: spine.animations,
|
|
1014
|
+
skins: spine.skins,
|
|
1015
|
+
declaresStage: spine.declaresStage,
|
|
1016
|
+
bones: poser.bones,
|
|
1017
|
+
slots: poser.slots,
|
|
1018
|
+
subset: (opts, skin) => subsetOver(document.subset, opts, skin),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
737
1022
|
// ---------------------------------------------------------------------------
|
|
738
1023
|
// the posing seam (issue #965, step 3a of #380)
|
|
739
1024
|
// ---------------------------------------------------------------------------
|
|
@@ -758,9 +1043,10 @@ export function posableFromText(skeletonText: string, atlasText: string, atlasDi
|
|
|
758
1043
|
// The framing samples are not a fourth entry: `framingViewport` is the same
|
|
759
1044
|
// animation entry at `FRAMING_FPS` with the clip off.
|
|
760
1045
|
//
|
|
761
|
-
// 🔒 What stays spine-core's and outside the seam, deliberately:
|
|
762
|
-
// (`posableFromText`'s pages, `substituteTexture`'s region lookup
|
|
763
|
-
//
|
|
1046
|
+
// 🔒 What stays spine-core's and outside the seam, deliberately: an export's
|
|
1047
|
+
// atlas (`posableFromText`'s pages, and `substituteTexture`'s region lookup on
|
|
1048
|
+
// anything spine-core poses — a rigc build the core poses reads both through
|
|
1049
|
+
// rigc's own reader since issue #1020) and `nonFiniteOfPosed`, which `validate.ts` calls on a spine-core
|
|
764
1050
|
// skeleton it stepped itself (A10), so the round trip keeps its spine-core
|
|
765
1051
|
// entry whatever poses the renders.
|
|
766
1052
|
|
|
@@ -819,8 +1105,19 @@ export interface Poser {
|
|
|
819
1105
|
/** What every sampler takes: a poser, or spine-core's parsed skeleton, which is posed through `spinePoser`. */
|
|
820
1106
|
export type PoseSource = Poser | SkeletonData;
|
|
821
1107
|
|
|
1108
|
+
/**
|
|
1109
|
+
* Whether a pose source is a `Poser` — told by the seam's own entries rather
|
|
1110
|
+
* than by `instanceof SkeletonData`, which reads the runtime's class and so
|
|
1111
|
+
* reached spine-core on every sample a rigc build took through the core
|
|
1112
|
+
* (issue #1014). A parsed skeleton carries none of the three.
|
|
1113
|
+
*/
|
|
1114
|
+
function isPoser(source: PoseSource): source is Poser {
|
|
1115
|
+
const seam = source as Partial<Poser>;
|
|
1116
|
+
return typeof seam.setup === 'function' && typeof seam.animation === 'function' && typeof seam.rest === 'function';
|
|
1117
|
+
}
|
|
1118
|
+
|
|
822
1119
|
function poserOf(source: PoseSource): Poser {
|
|
823
|
-
return source
|
|
1120
|
+
return isPoser(source) ? source : spinePoser(source);
|
|
824
1121
|
}
|
|
825
1122
|
|
|
826
1123
|
/** A sampler's `PoseOptions` as a draw walk reads them, the subset resolved once, on the first frame posed. */
|
|
@@ -998,7 +1295,43 @@ export interface PoserChoice {
|
|
|
998
1295
|
core: Poser | null;
|
|
999
1296
|
/** For a core poser, the document it poses; otherwise why there is none. */
|
|
1000
1297
|
why: string;
|
|
1001
|
-
|
|
1298
|
+
/**
|
|
1299
|
+
* spine-core's poser over the same skeleton — loaded the first time it is
|
|
1300
|
+
* read (issue #1014), which on a rigc build the core poses is a fallback the
|
|
1301
|
+
* poser line names, or never.
|
|
1302
|
+
*/
|
|
1303
|
+
readonly spine: Poser;
|
|
1304
|
+
/**
|
|
1305
|
+
* The skin roster behind `poser` (`SkinRoster`): the model document's for
|
|
1306
|
+
* the core poser (`coreSkinRoster`), the parsed skeleton's for spine-core's
|
|
1307
|
+
* (`skinRosterOf`) — so a render the core poses reads its framing's roster
|
|
1308
|
+
* without loading the runtime (issue #1014).
|
|
1309
|
+
*/
|
|
1310
|
+
rosterOf(poser: Poser): SkinRoster;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/** What builds the core poser — `corePoser`, or a planted copy the suite passes (`RC02`, `CH01`). */
|
|
1314
|
+
export type MakeCorePoser = (modelText: string, atlasText: string, where: string, skeleton: { path: string; bytes: Uint8Array }) => Poser;
|
|
1315
|
+
|
|
1316
|
+
/** A skeleton's three rosters, as `rosterDifference` compares them. */
|
|
1317
|
+
interface SkeletonRosters {
|
|
1318
|
+
bones: ReadonlyArray<{ name: string; parent: string | null }>;
|
|
1319
|
+
slots: ReadonlyArray<{ name: string; bone: string }>;
|
|
1320
|
+
animations: ReadonlyArray<{ name: string; duration: number }>;
|
|
1321
|
+
/** Every skin name, in the skeleton's order — the roster `coreSkinRoster` names skins in. */
|
|
1322
|
+
skins: readonly string[];
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* The rosters off the skeleton's own JSON (issue #1014): its bones, slots and
|
|
1327
|
+
* skins as `skeletonFacts` reads them, and its animations' durations as
|
|
1328
|
+
* `skeletonDurations` derives them. Before #1014 these were spine-core's parse
|
|
1329
|
+
* of the same file; the two were measured equal on every input the tree
|
|
1330
|
+
* carries.
|
|
1331
|
+
*/
|
|
1332
|
+
function skeletonRosters(skeletonText: string): SkeletonRosters {
|
|
1333
|
+
const facts = skeletonFacts(skeletonText);
|
|
1334
|
+
return { bones: facts.bones, slots: facts.slots, animations: skeletonDurations(objectOf(JSON.parse(skeletonText))), skins: facts.skins };
|
|
1002
1335
|
}
|
|
1003
1336
|
|
|
1004
1337
|
/**
|
|
@@ -1006,39 +1339,67 @@ export interface PoserChoice {
|
|
|
1006
1339
|
* sit beside, or `null` — a document from another build would pose another
|
|
1007
1340
|
* rig in the same files' name, so it is refused rather than drawn.
|
|
1008
1341
|
*/
|
|
1009
|
-
function rosterDifference(core: Poser,
|
|
1342
|
+
function rosterDifference(core: Poser, skeleton: SkeletonRosters): string | null {
|
|
1010
1343
|
const bones = (list: ReadonlyArray<{ name: string; parent: string | null }>): string => list.map((b) => `${b.name}<${b.parent ?? ''}`).join('|');
|
|
1011
1344
|
const slots = (list: ReadonlyArray<{ name: string; bone: string }>): string => list.map((x) => `${x.name}@${x.bone}`).join('|');
|
|
1012
|
-
|
|
1013
|
-
if (
|
|
1014
|
-
if (slots(core.slots) !== slots(spine.slots)) return 'the slots (names, bones or draw order) differ';
|
|
1345
|
+
if (bones(core.bones) !== bones(skeleton.bones)) return 'the bones (names, parents or order) differ';
|
|
1346
|
+
if (slots(core.slots) !== slots(skeleton.slots)) return 'the slots (names, bones or draw order) differ';
|
|
1015
1347
|
const animations = (list: ReadonlyArray<{ name: string; duration: number }>): string =>
|
|
1016
1348
|
list.map((a) => `${a.name}=${a.duration}`).sort().join('|');
|
|
1017
|
-
if (animations(core.animations) !== animations(
|
|
1349
|
+
if (animations(core.animations) !== animations(skeleton.animations)) return 'the animations (names or durations) differ';
|
|
1018
1350
|
return null;
|
|
1019
1351
|
}
|
|
1020
1352
|
|
|
1021
1353
|
/**
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
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`).
|
|
1354
|
+
* A `PoserChoice` over a core poser (or none) and a skeleton spine-core loads
|
|
1355
|
+
* only when something reads its side — `spine`, or the roster behind it.
|
|
1029
1356
|
*/
|
|
1030
|
-
|
|
1031
|
-
|
|
1357
|
+
function posersOver(
|
|
1358
|
+
forced: PoserName | undefined,
|
|
1359
|
+
core: { poser: Poser; roster: SkinRoster } | null,
|
|
1360
|
+
why: string,
|
|
1361
|
+
spineData: () => SkeletonData,
|
|
1362
|
+
): PoserChoice {
|
|
1363
|
+
let spine: Poser | null = null;
|
|
1364
|
+
let spineRoster: SkinRoster | null = null;
|
|
1365
|
+
return {
|
|
1366
|
+
forced,
|
|
1367
|
+
core: core === null ? null : core.poser,
|
|
1368
|
+
why,
|
|
1369
|
+
get spine(): Poser {
|
|
1370
|
+
spine ??= spinePoser(spineData());
|
|
1371
|
+
return spine;
|
|
1372
|
+
},
|
|
1373
|
+
rosterOf: (poser) => {
|
|
1374
|
+
if (core !== null && poser === core.poser) return core.roster;
|
|
1375
|
+
spineRoster ??= skinRosterOf(spineData());
|
|
1376
|
+
return spineRoster;
|
|
1377
|
+
},
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* The choice `candidatePosers` and `loadCandidate` share, refusing nothing:
|
|
1383
|
+
* the core poser when `skeleton.model.json` sits beside the skeleton, the
|
|
1384
|
+
* skeleton's bytes hash to the digest the document records (`spine.sha256`:
|
|
1385
|
+
* it is the file that build wrote, not one edited after it), the atlas is the
|
|
1386
|
+
* one beside it too, the core reads both and the document's rosters are the
|
|
1387
|
+
* skeleton's — read off the skeleton's own JSON (`skeletonRosters`), so
|
|
1388
|
+
* choosing loads nothing through spine-core (issue #1014).
|
|
1389
|
+
*/
|
|
1390
|
+
function choosePosers(
|
|
1032
1391
|
skeletonPath: string,
|
|
1033
1392
|
atlasPath: string,
|
|
1393
|
+
/** The atlas file's text, or `null` when there is no file at `atlasPath` (issue #1020). */
|
|
1394
|
+
atlasText: string | null,
|
|
1034
1395
|
forced: PoserName | undefined,
|
|
1035
|
-
|
|
1036
|
-
make:
|
|
1037
|
-
): PoserChoice {
|
|
1038
|
-
const spine = spinePoser(data);
|
|
1396
|
+
spineData: () => SkeletonData,
|
|
1397
|
+
make: MakeCorePoser,
|
|
1398
|
+
): { choice: PoserChoice; document: CoreDocumentFacts | null } {
|
|
1039
1399
|
const dir = dirname(resolve(skeletonPath));
|
|
1040
1400
|
const modelPath = join(dir, MODEL_DOCUMENT_FILE);
|
|
1041
|
-
let core: Poser | null = null;
|
|
1401
|
+
let core: { poser: Poser; roster: SkinRoster } | null = null;
|
|
1402
|
+
let document: CoreDocumentFacts | null = null;
|
|
1042
1403
|
let why: string;
|
|
1043
1404
|
if (forced === 'spine') why = '--poser spine';
|
|
1044
1405
|
else if (!existsSync(modelPath)) why = `no ${MODEL_DOCUMENT_FILE} beside ${resolve(skeletonPath)} — a Spine export, not a rigc build`;
|
|
@@ -1048,11 +1409,20 @@ export function candidatePosers(
|
|
|
1048
1409
|
"atlas's, and the core would pose them against another one's pages";
|
|
1049
1410
|
} else {
|
|
1050
1411
|
try {
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1412
|
+
const modelText = readFileSync(modelPath, 'utf8');
|
|
1413
|
+
const bytes = readFileSync(skeletonPath);
|
|
1414
|
+
// No atlas is `''`: a rigc-compiled/2 document draws from its own `pages`, and a /1 document is refused by name (`placementOf`). An atlas that is there is held to the document's `pages` (#1016) and refused, naming the first difference, when it is not the one the build wrote.
|
|
1415
|
+
const candidate = make(modelText, atlasText ?? '', modelPath, { path: resolve(skeletonPath), bytes });
|
|
1416
|
+
const rosters = skeletonRosters(bytes.toString('utf8'));
|
|
1417
|
+
const differs = rosterDifference(candidate, rosters);
|
|
1053
1418
|
if (differs === null) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1419
|
+
document = coreDocumentFacts(modelText, modelPath, rosters.skins);
|
|
1420
|
+
core = { poser: candidate, roster: document.roster };
|
|
1421
|
+
// The poser line names where the placement came from when it is not the document's own (issue #1020): a /1 document states none, and the core reads it from the atlas beside it.
|
|
1422
|
+
why =
|
|
1423
|
+
document.pageNames === null
|
|
1424
|
+
? `${modelPath} — a ${document.spec} document, which does not state where each region sits on its page: that is read from ${resolve(atlasPath)}`
|
|
1425
|
+
: modelPath;
|
|
1056
1426
|
} else why = `${modelPath} does not describe ${resolve(skeletonPath)}: ${differs}`;
|
|
1057
1427
|
} catch (err) {
|
|
1058
1428
|
if (!(err instanceof CoreInputError)) throw err;
|
|
@@ -1060,8 +1430,35 @@ export function candidatePosers(
|
|
|
1060
1430
|
why = `the core refused ${err.message.startsWith(`${modelPath}: `) ? err.message : `${modelPath}: ${err.message}`}`;
|
|
1061
1431
|
}
|
|
1062
1432
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1433
|
+
return { choice: posersOver(forced, core, why, spineData), document: core === null ? null : document };
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* `--poser core` on an input that cannot carry it, refused by name
|
|
1438
|
+
* (`PoserChoiceError`) — said by the caller where its refusals have always
|
|
1439
|
+
* stood, after the flags that refuse before it. Returns the choice otherwise.
|
|
1440
|
+
*/
|
|
1441
|
+
export function refuseUnchosen(choice: PoserChoice): PoserChoice {
|
|
1442
|
+
if (choice.forced === 'core' && choice.core === null) throw new PoserChoiceError(`--poser core: ${choice.why}`);
|
|
1443
|
+
return choice;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Both posers for the skeleton at `skeletonPath` drawn through the atlas at
|
|
1448
|
+
* `atlasPath`, with spine-core's parse of it already in hand (`data`) — the
|
|
1449
|
+
* choice `loadCandidate` makes, for a caller that loaded the pair itself.
|
|
1450
|
+
* `forced` is `--poser`; `core` on an input that cannot carry it is refused by
|
|
1451
|
+
* name (`PoserChoiceError`).
|
|
1452
|
+
*/
|
|
1453
|
+
export function candidatePosers(
|
|
1454
|
+
data: SkeletonData,
|
|
1455
|
+
skeletonPath: string,
|
|
1456
|
+
atlasPath: string,
|
|
1457
|
+
forced: PoserName | undefined,
|
|
1458
|
+
/** What builds the core poser: `corePoser` — the suite's `RC02` passes a planted copy, and nothing else passes any. */
|
|
1459
|
+
make: MakeCorePoser = corePoser,
|
|
1460
|
+
): PoserChoice {
|
|
1461
|
+
return refuseUnchosen(choosePosers(skeletonPath, atlasPath, readFileSync(atlasPath, 'utf8'), forced, () => data, make).choice);
|
|
1065
1462
|
}
|
|
1066
1463
|
|
|
1067
1464
|
/**
|
|
@@ -1070,19 +1467,22 @@ export function candidatePosers(
|
|
|
1070
1467
|
* (`CoreInputError`, e.g. a clip polygon that is not simple), in
|
|
1071
1468
|
* which case `run` starts again from nothing on spine-core and `note` names the
|
|
1072
1469
|
* refusal. Under `--poser core` that refusal is a `PoserChoiceError` instead.
|
|
1073
|
-
* `run` must write nothing: a fallback re-runs it whole.
|
|
1470
|
+
* `run` must write nothing: a fallback re-runs it whole. It is handed the
|
|
1471
|
+
* skin roster behind the poser it runs (`PoserChoice.rosterOf`), so a run
|
|
1472
|
+
* through the core reads nothing through spine-core (issue #1014).
|
|
1074
1473
|
*/
|
|
1075
|
-
export function throughPoser<T>(choice: PoserChoice, run: (poser: Poser) => T): { value: T; poser: PoserName; note: string } {
|
|
1474
|
+
export function throughPoser<T>(choice: PoserChoice, run: (poser: Poser, roster: SkinRoster) => T): { value: T; poser: PoserName; note: string } {
|
|
1475
|
+
const through = (poser: Poser): T => run(poser, choice.rosterOf(poser));
|
|
1076
1476
|
if (choice.core !== null) {
|
|
1077
1477
|
try {
|
|
1078
|
-
return { value:
|
|
1478
|
+
return { value: through(choice.core), poser: 'core', note: `rigc core — ${choice.why}` };
|
|
1079
1479
|
} catch (err) {
|
|
1080
1480
|
if (!(err instanceof CoreInputError)) throw err;
|
|
1081
1481
|
if (choice.forced === 'core') throw new PoserChoiceError(`--poser core: the core refused this input: ${err.message}`);
|
|
1082
|
-
return { value:
|
|
1482
|
+
return { value: through(choice.spine), poser: 'spine', note: `spine-core — the core refused this input: ${err.message}` };
|
|
1083
1483
|
}
|
|
1084
1484
|
}
|
|
1085
|
-
return { value:
|
|
1485
|
+
return { value: through(choice.spine), poser: 'spine', note: `spine-core — ${choice.why}` };
|
|
1086
1486
|
}
|
|
1087
1487
|
|
|
1088
1488
|
/**
|
|
@@ -1785,7 +2185,7 @@ function artUvsOf(attachment: MeshAttachment | RegionAttachment, region: Texture
|
|
|
1785
2185
|
* sequence packs several regions under one name and `findRegion` returns only the
|
|
1786
2186
|
* first of them.
|
|
1787
2187
|
*/
|
|
1788
|
-
function regionKey(region:
|
|
2188
|
+
function regionKey(region: { name: string; index: number }): string {
|
|
1789
2189
|
return `${region.name.trim()}#${region.index}`;
|
|
1790
2190
|
}
|
|
1791
2191
|
|
|
@@ -1795,28 +2195,86 @@ function regionKey(region: TextureAtlasRegion): string {
|
|
|
1795
2195
|
*/
|
|
1796
2196
|
export const SUBSTITUTE_PAGE = 'texture-from:';
|
|
1797
2197
|
|
|
2198
|
+
/**
|
|
2199
|
+
* One region of a substituting atlas, as `substituteTexture` reads it: where
|
|
2200
|
+
* it sits on which page, and the mapping from the drawing's own coordinates
|
|
2201
|
+
* onto it (`MeshAttachment.computeUVs`'s job).
|
|
2202
|
+
*/
|
|
2203
|
+
export interface SubstituteRegion {
|
|
2204
|
+
page: { name: string; width: number; height: number };
|
|
2205
|
+
x: number;
|
|
2206
|
+
y: number;
|
|
2207
|
+
width: number;
|
|
2208
|
+
height: number;
|
|
2209
|
+
degrees: number;
|
|
2210
|
+
/** Art-space UVs (`u, v` per vertex) mapped onto this region of its page, in doubles. */
|
|
2211
|
+
pageUvs(art: readonly number[]): number[];
|
|
2212
|
+
}
|
|
2213
|
+
|
|
1798
2214
|
/** An atlas whose texels can stand in for another's — see `substituteTexture`. */
|
|
1799
2215
|
export interface TextureSubstitution {
|
|
1800
2216
|
/** Prefixed page name → the page, ready to merge into a render's page map. */
|
|
1801
2217
|
pages: Map<string, Plate>;
|
|
1802
2218
|
/** The atlas's regions, by the key both sides agree on — see `regionKey`. */
|
|
1803
|
-
regions: Map<string,
|
|
2219
|
+
regions: Map<string, SubstituteRegion>;
|
|
1804
2220
|
/** Every `scale:` the atlas text declares, in the order the pages declare them. */
|
|
1805
2221
|
scales: number[];
|
|
1806
2222
|
}
|
|
1807
2223
|
|
|
1808
|
-
/**
|
|
1809
|
-
|
|
1810
|
-
|
|
2224
|
+
/**
|
|
2225
|
+
* Who reads a substituting atlas (issue #1020): `rigc` — `parseAtlasText` and
|
|
2226
|
+
* `./core/uvs.ts`'s `computeUvs`, for a candidate the core poses, so a rigc
|
|
2227
|
+
* build's `check --texture-from` reads nothing through spine-core — or
|
|
2228
|
+
* `spine`, the runtime's `TextureAtlas` and `MeshAttachment.computeUVs`, for
|
|
2229
|
+
* everything spine-core poses (an export keeps the reading it always had).
|
|
2230
|
+
*
|
|
2231
|
+
* The two were measured to agree: the region list (`TextureAtlas.regions` is
|
|
2232
|
+
* file order, and so is `parseAtlasText`'s, the pairing the selftest holds on
|
|
2233
|
+
* every corpus atlas) and the mapping (`computeUvs`, bit for bit in doubles
|
|
2234
|
+
* over 6,000 calls — its header). The PR of #1020 carries the substituted
|
|
2235
|
+
* frames, pixel for pixel, on the corpus rows that exercise it.
|
|
2236
|
+
*/
|
|
2237
|
+
export type SubstitutionReader = 'rigc' | 'spine';
|
|
2238
|
+
|
|
2239
|
+
/** Load an atlas and its pages as a substitution source, read by `reader` — see `SubstitutionReader`. */
|
|
2240
|
+
export function textureSubstitutionFromText(atlasText: string, atlasDir: string, reader: SubstitutionReader = 'spine'): TextureSubstitution {
|
|
2241
|
+
const regions = new Map<string, SubstituteRegion>();
|
|
2242
|
+
const names: string[] = [];
|
|
2243
|
+
if (reader === 'rigc') {
|
|
2244
|
+
for (const page of parseAtlasText(atlasText).pages) {
|
|
2245
|
+
names.push(page.name);
|
|
2246
|
+
const at = { name: page.name, width: page.width, height: page.height };
|
|
2247
|
+
for (const region of page.regions) {
|
|
2248
|
+
regions.set(regionKey(region), { page: at, x: region.x, y: region.y, width: region.width, height: region.height, degrees: region.degrees, pageUvs: (art) => computeUvs(region, at, art) });
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
} else {
|
|
2252
|
+
const atlas = new TextureAtlas(atlasText);
|
|
2253
|
+
for (const page of atlas.pages) names.push(page.name);
|
|
2254
|
+
for (const region of atlas.regions) {
|
|
2255
|
+
const page = { name: region.page.name, width: region.page.width, height: region.page.height };
|
|
2256
|
+
regions.set(regionKey(region), {
|
|
2257
|
+
page,
|
|
2258
|
+
x: region.x,
|
|
2259
|
+
y: region.y,
|
|
2260
|
+
width: region.width,
|
|
2261
|
+
height: region.height,
|
|
2262
|
+
degrees: region.degrees,
|
|
2263
|
+
pageUvs: (art) => {
|
|
2264
|
+
const uvs = new Array<number>(art.length).fill(0);
|
|
2265
|
+
MeshAttachment.computeUVs(region, [...art], uvs);
|
|
2266
|
+
return uvs;
|
|
2267
|
+
},
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
1811
2271
|
const pages = new Map<string, Plate>();
|
|
1812
|
-
for (const
|
|
1813
|
-
if (
|
|
1814
|
-
throw new Error(`atlas page "${
|
|
2272
|
+
for (const name of names) {
|
|
2273
|
+
if (name.startsWith(SUBSTITUTE_PAGE)) {
|
|
2274
|
+
throw new Error(`atlas page "${name}" starts with the reserved prefix ${JSON.stringify(SUBSTITUTE_PAGE)}`);
|
|
1815
2275
|
}
|
|
1816
|
-
pages.set(SUBSTITUTE_PAGE +
|
|
2276
|
+
pages.set(SUBSTITUTE_PAGE + name, readPlate(join(atlasDir, name)));
|
|
1817
2277
|
}
|
|
1818
|
-
const regions = new Map<string, TextureAtlasRegion>();
|
|
1819
|
-
for (const region of atlas.regions) regions.set(regionKey(region), region);
|
|
1820
2278
|
return { pages, regions, scales: atlasScales(atlasText) };
|
|
1821
2279
|
}
|
|
1822
2280
|
|
|
@@ -1862,7 +2320,7 @@ export function atlasScales(atlasText: string): number[] {
|
|
|
1862
2320
|
* rectangle; two of them had it wrong at 270 (issue #579), so it is one function
|
|
1863
2321
|
* now and this is one of its callers.
|
|
1864
2322
|
*/
|
|
1865
|
-
function windowOf(region:
|
|
2323
|
+
function windowOf(region: SubstituteRegion): UvWindow {
|
|
1866
2324
|
const rect = pageFootprint(region);
|
|
1867
2325
|
const page = region.page;
|
|
1868
2326
|
return {
|
|
@@ -1918,17 +2376,16 @@ export function substituteTexture(
|
|
|
1918
2376
|
pieces.push(piece);
|
|
1919
2377
|
continue;
|
|
1920
2378
|
}
|
|
1921
|
-
|
|
1922
|
-
// `
|
|
1923
|
-
//
|
|
1924
|
-
//
|
|
1925
|
-
//
|
|
1926
|
-
|
|
2379
|
+
// The mapping from the drawing's coordinates into a page's, which is where
|
|
2380
|
+
// `rotate:` (all four of them) and the trim offsets are handled: spine-core's
|
|
2381
|
+
// own `MeshAttachment.computeUVs`, or `./core/uvs.ts`'s `computeUvs`, measured
|
|
2382
|
+
// equal to it bit for bit — never a third opinion about the atlas format
|
|
2383
|
+
// (`SubstitutionReader`).
|
|
2384
|
+
const uvs = region.pageUvs(texture.artUvs);
|
|
1927
2385
|
// A clipped piece's source map is re-seated the same way, through the drawing's own coordinates (`Mesh.source`).
|
|
1928
2386
|
if (piece.kind === 'mesh' && piece.source !== undefined) {
|
|
1929
2387
|
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 =
|
|
1931
|
-
MeshAttachment.computeUVs(region, texture.sourceArtUvs, sourceUvs);
|
|
2388
|
+
const sourceUvs = region.pageUvs(texture.sourceArtUvs);
|
|
1932
2389
|
pieces.push({ ...piece, page: SUBSTITUTE_PAGE + region.page.name, uvs, uvWindow: windowOf(region), source: { world: piece.source.world, uvs: sourceUvs } });
|
|
1933
2390
|
continue;
|
|
1934
2391
|
}
|
|
@@ -2135,6 +2592,10 @@ export class UnframeablePoseError extends GeometryError {}
|
|
|
2135
2592
|
* structure the unframeable sentence names a skin from (issue #997). Not a
|
|
2136
2593
|
* poser's: the `Poser` seam carries no skin roster, and both posers pose the one
|
|
2137
2594
|
* Spine file this is read from (the core's document is bound to it by hash).
|
|
2595
|
+
* Two readings stand behind it — the runtime's (`skinRosterOf`) and the model
|
|
2596
|
+
* document's (`coreSkinRoster` in `./render_core.ts`, issue #1014), measured
|
|
2597
|
+
* to name the same bones under every skin — and `PoserChoice.rosterOf` hands
|
|
2598
|
+
* each poser its own.
|
|
2138
2599
|
*/
|
|
2139
2600
|
export interface SkinRoster {
|
|
2140
2601
|
/** Every skin, in declaration order. */
|
|
@@ -2162,7 +2623,7 @@ export function skinRosterOf(data: SkeletonData): SkinRoster {
|
|
|
2162
2623
|
|
|
2163
2624
|
/** The roster behind a pose source: its own when it is Spine data, else the one the caller passed. */
|
|
2164
2625
|
function rosterBehind(source: PoseSource, roster: SkinRoster | undefined): SkinRoster | undefined {
|
|
2165
|
-
return source
|
|
2626
|
+
return isPoser(source) ? roster : skinRosterOf(source);
|
|
2166
2627
|
}
|
|
2167
2628
|
|
|
2168
2629
|
/**
|