spine-rigc 1.6.2 → 1.7.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 +82 -43
- package/docs/AUTHORING.md +9 -6
- package/package.json +1 -1
- package/src/atlas.ts +26 -16
- package/src/check.ts +37 -39
- package/src/compile.ts +59 -74
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +95 -10
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +79 -24
- package/src/deformmeasure.ts +6 -3
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +109 -11
- package/src/render.ts +347 -34
- package/src/render_core.ts +115 -18
- package/src/transform.ts +62 -126
- package/src/types.ts +6 -5
- package/src/validate.ts +51 -29
package/src/render.ts
CHANGED
|
@@ -81,7 +81,11 @@
|
|
|
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 the skeleton's
|
|
87
|
+
* own JSON and rigc's atlas reader (`loadCandidate`), and spine-core is reached
|
|
88
|
+
* only where it poses or substitutes.) The rule that matters is unchanged
|
|
85
89
|
* — `src/compile.ts` must stay independent of the runtime so the compiler and
|
|
86
90
|
* the gate are not checking each other's assumptions — and this file is neither.
|
|
87
91
|
* It also imports `tools/plate.ts` for the PNG codec, which is dependency-free.
|
|
@@ -105,10 +109,11 @@ import {
|
|
|
105
109
|
import { existsSync, readFileSync } from 'node:fs';
|
|
106
110
|
import { dirname, join, resolve } from 'node:path';
|
|
107
111
|
import { Plate, readPlate, type RGBA } from '../tools/plate.ts';
|
|
108
|
-
import { pageFootprint } from './atlas.ts';
|
|
112
|
+
import { pageFootprint, parseAtlasText } from './atlas.ts';
|
|
109
113
|
import { CoreInputError } from './core/index.ts';
|
|
110
114
|
import { MODEL_DOCUMENT_FILE } from './model.ts';
|
|
111
|
-
import { clipSourceOf, corePoser, inactiveBoneSnapshot, SlotSubsetError, subsetOver, unposedBones } from './render_core.ts';
|
|
115
|
+
import { clipSourceOf, corePoser, coreSkinRoster, inactiveBoneSnapshot, SlotSubsetError, subsetOver, unposedBones, type SubsetRoster } from './render_core.ts';
|
|
116
|
+
import { walkTimelines } from './timelines.ts';
|
|
112
117
|
|
|
113
118
|
/** Opaque, and light: both of rung 3's parts are dark slate, so is every ground. */
|
|
114
119
|
export const BACKGROUND: RGBA = [232, 232, 232, 255];
|
|
@@ -734,6 +739,205 @@ export function posableFromText(skeletonText: string, atlasText: string, atlasDi
|
|
|
734
739
|
return { data, pages };
|
|
735
740
|
}
|
|
736
741
|
|
|
742
|
+
// ---------------------------------------------------------------------------
|
|
743
|
+
// what a render and a check read off a candidate besides its pose (issue #1014)
|
|
744
|
+
// ---------------------------------------------------------------------------
|
|
745
|
+
//
|
|
746
|
+
// ⭐ `render` and `check` read a handful of facts off the candidate before and
|
|
747
|
+
// beside the pose: the animation and skin names their flags are checked
|
|
748
|
+
// against, the slot subset's roster, whether a stage is declared, the bone
|
|
749
|
+
// tree `check` draws its chains from, the skin roster the framing reads, and
|
|
750
|
+
// the atlas pages the rasteriser samples. Until issue #1014 every one of them
|
|
751
|
+
// came off the skeleton spine-core had parsed, so a rigc build the core poses
|
|
752
|
+
// still loaded and ran the runtime before its poser was chosen. Now the choice
|
|
753
|
+
// comes first (`loadCandidate`), and a build the core poses reads each fact
|
|
754
|
+
// where it is written: the names, the tree, the subset's roster and the stage
|
|
755
|
+
// off the skeleton's own JSON (`skeletonFacts`), the pages off rigc's atlas
|
|
756
|
+
// reader, and the skin roster off the model document (`coreSkinRoster` in
|
|
757
|
+
// `./render_core.ts`). Every one of those readings was measured equal to the
|
|
758
|
+
// spine-core reading it replaces on every input the tree carries — the
|
|
759
|
+
// nineteen built corpus rows and the twelve editor exports (the PR of #1014
|
|
760
|
+
// carries the counts). A Spine export, `--poser spine` and a fallback the
|
|
761
|
+
// poser line names load spine-core as before and read every fact off it.
|
|
762
|
+
|
|
763
|
+
/** What `render` and `check` read off a candidate's skeleton besides its pose. */
|
|
764
|
+
export interface SkeletonFacts {
|
|
765
|
+
/** Every animation name, in the skeleton's own order. */
|
|
766
|
+
readonly animations: readonly string[];
|
|
767
|
+
/** Every skin name, in the skeleton's own order. */
|
|
768
|
+
readonly skins: readonly string[];
|
|
769
|
+
/** Whether the header states a numeric `width` and `height` — a setup stage (issue #714). */
|
|
770
|
+
readonly declaresStage: boolean;
|
|
771
|
+
/** Every bone in declaration order, and its parent's name. */
|
|
772
|
+
readonly bones: ReadonlyArray<{ name: string; parent: string | null }>;
|
|
773
|
+
/** Every slot in declaration order, and the bone it hangs from. */
|
|
774
|
+
readonly slots: ReadonlyArray<{ name: string; bone: string }>;
|
|
775
|
+
/** `slotSubsetOf` over this skeleton — refused by `SlotSubsetError`. */
|
|
776
|
+
subset(opts: Pick<PoseOptions, 'slots' | 'hidden'> | undefined, skin: string | undefined): SlotSubset | undefined;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** The facts as spine-core loaded them — an export's reading, and every candidate's before issue #1014. */
|
|
780
|
+
export function spineFacts(data: SkeletonData): SkeletonFacts {
|
|
781
|
+
return {
|
|
782
|
+
animations: data.animations.map((a) => a.name),
|
|
783
|
+
skins: data.skins.map((s) => s.name),
|
|
784
|
+
// `SkeletonJson` copies both header fields across unconditionally, so an omitted extent is `undefined` here, not 0.
|
|
785
|
+
declaresStage: typeof data.width === 'number' && typeof data.height === 'number',
|
|
786
|
+
bones: data.bones.map((bone) => ({ name: bone.name, parent: bone.parent === null ? null : bone.parent.name })),
|
|
787
|
+
slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
|
|
788
|
+
subset: (opts, skin) => slotSubsetOf(data, opts, skin),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
type JsonObject = Record<string, unknown>;
|
|
793
|
+
|
|
794
|
+
/** A JSON value as an object, or an empty one where it is not one. */
|
|
795
|
+
function objectOf(value: unknown): JsonObject {
|
|
796
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as JsonObject) : {};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** A JSON value as a list of objects, or an empty list where it is not one. */
|
|
800
|
+
function objectsOf(value: unknown): JsonObject[] {
|
|
801
|
+
return Array.isArray(value) ? value.map(objectOf) : [];
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* The same facts off the skeleton's own JSON — a candidate the core poses
|
|
806
|
+
* (issue #1014), whose skeleton spine-core never loads.
|
|
807
|
+
*
|
|
808
|
+
* Each is the file's own statement, read in the file's order: the animation
|
|
809
|
+
* names are the keys of `animations` (the order the parser iterates them), the
|
|
810
|
+
* skins `skins[].name` (the default skin the one named `default`), a slot's
|
|
811
|
+
* carriers the skins whose `attachments` give it at least one entry, the tree
|
|
812
|
+
* `bones[]` and `slots[]`, and the stage `skeleton.width`/`height`. The core
|
|
813
|
+
* poses only a rigc build whose `skeleton.json` hashes to the digest its model
|
|
814
|
+
* document records, so the file read here is the one `build` wrote and the
|
|
815
|
+
* gate round-tripped.
|
|
816
|
+
*/
|
|
817
|
+
export function skeletonFacts(skeletonText: string): SkeletonFacts {
|
|
818
|
+
const root = objectOf(JSON.parse(skeletonText));
|
|
819
|
+
const skins = objectsOf(root.skins);
|
|
820
|
+
const slots = objectsOf(root.slots).map((slot) => ({ name: String(slot.name), bone: String(slot.bone) }));
|
|
821
|
+
const header = objectOf(root.skeleton);
|
|
822
|
+
const roster: SubsetRoster = {
|
|
823
|
+
declared: slots.map((slot) => slot.name),
|
|
824
|
+
carriers: (slot) => skins.filter((skin) => Object.keys(objectOf(objectOf(skin.attachments)[slot])).length > 0).map((skin) => String(skin.name)),
|
|
825
|
+
defaultSkin: skins.some((skin) => skin.name === 'default') ? 'default' : null,
|
|
826
|
+
};
|
|
827
|
+
return {
|
|
828
|
+
animations: Object.keys(objectOf(root.animations)),
|
|
829
|
+
skins: skins.map((skin) => String(skin.name)),
|
|
830
|
+
declaresStage: typeof header.width === 'number' && typeof header.height === 'number',
|
|
831
|
+
bones: objectsOf(root.bones).map((bone) => ({ name: String(bone.name), parent: typeof bone.parent === 'string' ? bone.parent : null })),
|
|
832
|
+
slots,
|
|
833
|
+
subset: (opts, skin) => subsetOver(roster, opts, skin),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Each animation's duration off the skeleton's own JSON, in the file's order —
|
|
839
|
+
* what `rosterDifference` holds a model document's durations to without
|
|
840
|
+
* loading the skeleton through spine-core (issue #1014).
|
|
841
|
+
*
|
|
842
|
+
* ⚠️ Skeleton JSON carries no duration, so this is the parser's derivation of
|
|
843
|
+
* one, and it is stated as measured rather than as read: the LAST key time of
|
|
844
|
+
* each timeline, the largest of them, held as a float32 — equal to
|
|
845
|
+
* spine-core's `Animation.duration` on every animation of every input the tree
|
|
846
|
+
* carries (the PR of #1014). The timelines are the ones `walkTimelines` walks,
|
|
847
|
+
* the walk `A05` and `A12` stand on, so a group it did not descend would be
|
|
848
|
+
* missing from both.
|
|
849
|
+
*/
|
|
850
|
+
function skeletonDurations(root: JsonObject): Array<{ name: string; duration: number }> {
|
|
851
|
+
return Object.entries(objectOf(root.animations)).map(([name, animation]) => {
|
|
852
|
+
let duration = 0;
|
|
853
|
+
walkTimelines({ animations: { [name]: animation } }, (_path, _kind, _timeline, keys) => {
|
|
854
|
+
if (keys.length === 0) return;
|
|
855
|
+
const last = objectOf(keys[keys.length - 1]);
|
|
856
|
+
duration = Math.max(duration, Math.fround(typeof last.time === 'number' ? last.time : 0));
|
|
857
|
+
});
|
|
858
|
+
return { name, duration };
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** Every page an atlas declares, read by rigc's own atlas reader — the pages a candidate the core poses is drawn from. */
|
|
863
|
+
function atlasPagesOf(atlasText: string, atlasDir: string): Map<string, Plate> {
|
|
864
|
+
const pages = new Map<string, Plate>();
|
|
865
|
+
for (const page of parseAtlasText(atlasText).pages) pages.set(page.name, readPlate(join(atlasDir, page.name)));
|
|
866
|
+
return pages;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Posing an input through spine-core when the runtime cannot be reached —
|
|
871
|
+
* refused naming the input and why it needs the runtime (issue #1014).
|
|
872
|
+
*
|
|
873
|
+
* A Spine export, `--poser spine` and a fallback the poser line names are what
|
|
874
|
+
* spine-core is loaded for; a rigc build the core poses reads nothing through
|
|
875
|
+
* it. So the one run that can meet a runtime it cannot use is one of those
|
|
876
|
+
* three, and it says so rather than surfacing whatever the first access threw.
|
|
877
|
+
*/
|
|
878
|
+
export class SpineRuntimeError extends Error {}
|
|
879
|
+
|
|
880
|
+
/** Touch the runtime once, before anything is parsed through it, and refuse by name when it cannot be used. */
|
|
881
|
+
function requireSpineRuntime(label: string, why: string): void {
|
|
882
|
+
try {
|
|
883
|
+
// A property read on the class the load starts from: no runtime code runs, and a runtime that cannot be used throws here.
|
|
884
|
+
void TextureAtlas.prototype;
|
|
885
|
+
} catch (err) {
|
|
886
|
+
throw new SpineRuntimeError(
|
|
887
|
+
`${label} is posed through spine-core (${why}), and the runtime could not be used: ${(err as Error).message}. ` +
|
|
888
|
+
'spine-core is what poses a Spine export, --poser spine and a fallback the poser line names; ' +
|
|
889
|
+
'a rigc build the core poses reads nothing through it',
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** A candidate as `render` and `check` read it: the posers, the facts and the pages (issue #1014). */
|
|
895
|
+
export interface Candidate {
|
|
896
|
+
choice: PoserChoice;
|
|
897
|
+
facts: SkeletonFacts;
|
|
898
|
+
pages: Map<string, Plate>;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Load a candidate for `render` or `check`, choosing its poser FIRST (issue
|
|
903
|
+
* #1014): a rigc build the core poses reads its facts off its own JSON, its
|
|
904
|
+
* pages off rigc's atlas reader and its skin roster off the model document,
|
|
905
|
+
* and spine-core is never loaded for it; anything else — a Spine export,
|
|
906
|
+
* `--poser spine`, a candidate handed over as text (`paths` null, `unplaced`
|
|
907
|
+
* the reason) — is loaded through spine-core as `posableFromText` always
|
|
908
|
+
* loaded it. The choice's spine-core poser stays unloaded until something
|
|
909
|
+
* reads it, which on a rigc build is a fallback the poser line names.
|
|
910
|
+
*
|
|
911
|
+
* `--poser core` on an input that cannot carry it is NOT refused here: the
|
|
912
|
+
* caller says it where it always has (`refuseUnchosen`), after the flags that
|
|
913
|
+
* refuse before it.
|
|
914
|
+
*/
|
|
915
|
+
export function loadCandidate(
|
|
916
|
+
input: { skeletonText: string; atlasText: string; atlasDir: string; label: string },
|
|
917
|
+
paths: { skeleton: string; atlas: string } | null,
|
|
918
|
+
forced: PoserName | undefined,
|
|
919
|
+
options: { make?: MakeCorePoser; unplaced?: string } = {},
|
|
920
|
+
): Candidate {
|
|
921
|
+
let data: SkeletonData | null = null;
|
|
922
|
+
let choice: PoserChoice | null = null;
|
|
923
|
+
const spineData = (): SkeletonData => {
|
|
924
|
+
if (data === null) {
|
|
925
|
+
requireSpineRuntime(input.label, choice === null || choice.core !== null ? 'a fallback from the core poser' : choice.why);
|
|
926
|
+
data = new SkeletonJson(new AtlasAttachmentLoader(new TextureAtlas(input.atlasText))).readSkeletonData(JSON.parse(input.skeletonText));
|
|
927
|
+
}
|
|
928
|
+
return data;
|
|
929
|
+
};
|
|
930
|
+
choice =
|
|
931
|
+
paths === null
|
|
932
|
+
? posersOver(forced, null, forced === 'spine' ? '--poser spine' : (options.unplaced ?? 'no path to find a model document beside'), spineData)
|
|
933
|
+
: choosePosers(paths.skeleton, paths.atlas, forced, spineData, options.make ?? corePoser);
|
|
934
|
+
if (choice.core !== null) return { choice, facts: skeletonFacts(input.skeletonText), pages: atlasPagesOf(input.atlasText, input.atlasDir) };
|
|
935
|
+
requireSpineRuntime(input.label, choice.why);
|
|
936
|
+
const posable = posableFromText(input.skeletonText, input.atlasText, input.atlasDir);
|
|
937
|
+
data = posable.data;
|
|
938
|
+
return { choice, facts: spineFacts(posable.data), pages: posable.pages };
|
|
939
|
+
}
|
|
940
|
+
|
|
737
941
|
// ---------------------------------------------------------------------------
|
|
738
942
|
// the posing seam (issue #965, step 3a of #380)
|
|
739
943
|
// ---------------------------------------------------------------------------
|
|
@@ -819,8 +1023,19 @@ export interface Poser {
|
|
|
819
1023
|
/** What every sampler takes: a poser, or spine-core's parsed skeleton, which is posed through `spinePoser`. */
|
|
820
1024
|
export type PoseSource = Poser | SkeletonData;
|
|
821
1025
|
|
|
1026
|
+
/**
|
|
1027
|
+
* Whether a pose source is a `Poser` — told by the seam's own entries rather
|
|
1028
|
+
* than by `instanceof SkeletonData`, which reads the runtime's class and so
|
|
1029
|
+
* reached spine-core on every sample a rigc build took through the core
|
|
1030
|
+
* (issue #1014). A parsed skeleton carries none of the three.
|
|
1031
|
+
*/
|
|
1032
|
+
function isPoser(source: PoseSource): source is Poser {
|
|
1033
|
+
const seam = source as Partial<Poser>;
|
|
1034
|
+
return typeof seam.setup === 'function' && typeof seam.animation === 'function' && typeof seam.rest === 'function';
|
|
1035
|
+
}
|
|
1036
|
+
|
|
822
1037
|
function poserOf(source: PoseSource): Poser {
|
|
823
|
-
return source
|
|
1038
|
+
return isPoser(source) ? source : spinePoser(source);
|
|
824
1039
|
}
|
|
825
1040
|
|
|
826
1041
|
/** A sampler's `PoseOptions` as a draw walk reads them, the subset resolved once, on the first frame posed. */
|
|
@@ -998,7 +1213,43 @@ export interface PoserChoice {
|
|
|
998
1213
|
core: Poser | null;
|
|
999
1214
|
/** For a core poser, the document it poses; otherwise why there is none. */
|
|
1000
1215
|
why: string;
|
|
1001
|
-
|
|
1216
|
+
/**
|
|
1217
|
+
* spine-core's poser over the same skeleton — loaded the first time it is
|
|
1218
|
+
* read (issue #1014), which on a rigc build the core poses is a fallback the
|
|
1219
|
+
* poser line names, or never.
|
|
1220
|
+
*/
|
|
1221
|
+
readonly spine: Poser;
|
|
1222
|
+
/**
|
|
1223
|
+
* The skin roster behind `poser` (`SkinRoster`): the model document's for
|
|
1224
|
+
* the core poser (`coreSkinRoster`), the parsed skeleton's for spine-core's
|
|
1225
|
+
* (`skinRosterOf`) — so a render the core poses reads its framing's roster
|
|
1226
|
+
* without loading the runtime (issue #1014).
|
|
1227
|
+
*/
|
|
1228
|
+
rosterOf(poser: Poser): SkinRoster;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/** What builds the core poser — `corePoser`, or a planted copy the suite passes (`RC02`, `CH01`). */
|
|
1232
|
+
export type MakeCorePoser = (modelText: string, atlasText: string, where: string, skeleton: { path: string; bytes: Uint8Array }) => Poser;
|
|
1233
|
+
|
|
1234
|
+
/** A skeleton's three rosters, as `rosterDifference` compares them. */
|
|
1235
|
+
interface SkeletonRosters {
|
|
1236
|
+
bones: ReadonlyArray<{ name: string; parent: string | null }>;
|
|
1237
|
+
slots: ReadonlyArray<{ name: string; bone: string }>;
|
|
1238
|
+
animations: ReadonlyArray<{ name: string; duration: number }>;
|
|
1239
|
+
/** Every skin name, in the skeleton's order — the roster `coreSkinRoster` names skins in. */
|
|
1240
|
+
skins: readonly string[];
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* The rosters off the skeleton's own JSON (issue #1014): its bones, slots and
|
|
1245
|
+
* skins as `skeletonFacts` reads them, and its animations' durations as
|
|
1246
|
+
* `skeletonDurations` derives them. Before #1014 these were spine-core's parse
|
|
1247
|
+
* of the same file; the two were measured equal on every input the tree
|
|
1248
|
+
* carries.
|
|
1249
|
+
*/
|
|
1250
|
+
function skeletonRosters(skeletonText: string): SkeletonRosters {
|
|
1251
|
+
const facts = skeletonFacts(skeletonText);
|
|
1252
|
+
return { bones: facts.bones, slots: facts.slots, animations: skeletonDurations(objectOf(JSON.parse(skeletonText))), skins: facts.skins };
|
|
1002
1253
|
}
|
|
1003
1254
|
|
|
1004
1255
|
/**
|
|
@@ -1006,39 +1257,64 @@ export interface PoserChoice {
|
|
|
1006
1257
|
* sit beside, or `null` — a document from another build would pose another
|
|
1007
1258
|
* rig in the same files' name, so it is refused rather than drawn.
|
|
1008
1259
|
*/
|
|
1009
|
-
function rosterDifference(core: Poser,
|
|
1260
|
+
function rosterDifference(core: Poser, skeleton: SkeletonRosters): string | null {
|
|
1010
1261
|
const bones = (list: ReadonlyArray<{ name: string; parent: string | null }>): string => list.map((b) => `${b.name}<${b.parent ?? ''}`).join('|');
|
|
1011
1262
|
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';
|
|
1263
|
+
if (bones(core.bones) !== bones(skeleton.bones)) return 'the bones (names, parents or order) differ';
|
|
1264
|
+
if (slots(core.slots) !== slots(skeleton.slots)) return 'the slots (names, bones or draw order) differ';
|
|
1015
1265
|
const animations = (list: ReadonlyArray<{ name: string; duration: number }>): string =>
|
|
1016
1266
|
list.map((a) => `${a.name}=${a.duration}`).sort().join('|');
|
|
1017
|
-
if (animations(core.animations) !== animations(
|
|
1267
|
+
if (animations(core.animations) !== animations(skeleton.animations)) return 'the animations (names or durations) differ';
|
|
1018
1268
|
return null;
|
|
1019
1269
|
}
|
|
1020
1270
|
|
|
1021
1271
|
/**
|
|
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`).
|
|
1272
|
+
* A `PoserChoice` over a core poser (or none) and a skeleton spine-core loads
|
|
1273
|
+
* only when something reads its side — `spine`, or the roster behind it.
|
|
1029
1274
|
*/
|
|
1030
|
-
|
|
1031
|
-
|
|
1275
|
+
function posersOver(
|
|
1276
|
+
forced: PoserName | undefined,
|
|
1277
|
+
core: { poser: Poser; roster: SkinRoster } | null,
|
|
1278
|
+
why: string,
|
|
1279
|
+
spineData: () => SkeletonData,
|
|
1280
|
+
): PoserChoice {
|
|
1281
|
+
let spine: Poser | null = null;
|
|
1282
|
+
let spineRoster: SkinRoster | null = null;
|
|
1283
|
+
return {
|
|
1284
|
+
forced,
|
|
1285
|
+
core: core === null ? null : core.poser,
|
|
1286
|
+
why,
|
|
1287
|
+
get spine(): Poser {
|
|
1288
|
+
spine ??= spinePoser(spineData());
|
|
1289
|
+
return spine;
|
|
1290
|
+
},
|
|
1291
|
+
rosterOf: (poser) => {
|
|
1292
|
+
if (core !== null && poser === core.poser) return core.roster;
|
|
1293
|
+
spineRoster ??= skinRosterOf(spineData());
|
|
1294
|
+
return spineRoster;
|
|
1295
|
+
},
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* The choice `candidatePosers` and `loadCandidate` share, refusing nothing:
|
|
1301
|
+
* the core poser when `skeleton.model.json` sits beside the skeleton, the
|
|
1302
|
+
* skeleton's bytes hash to the digest the document records (`spine.sha256`:
|
|
1303
|
+
* it is the file that build wrote, not one edited after it), the atlas is the
|
|
1304
|
+
* one beside it too, the core reads both and the document's rosters are the
|
|
1305
|
+
* skeleton's — read off the skeleton's own JSON (`skeletonRosters`), so
|
|
1306
|
+
* choosing loads nothing through spine-core (issue #1014).
|
|
1307
|
+
*/
|
|
1308
|
+
function choosePosers(
|
|
1032
1309
|
skeletonPath: string,
|
|
1033
1310
|
atlasPath: string,
|
|
1034
1311
|
forced: PoserName | undefined,
|
|
1035
|
-
|
|
1036
|
-
make:
|
|
1312
|
+
spineData: () => SkeletonData,
|
|
1313
|
+
make: MakeCorePoser,
|
|
1037
1314
|
): PoserChoice {
|
|
1038
|
-
const spine = spinePoser(data);
|
|
1039
1315
|
const dir = dirname(resolve(skeletonPath));
|
|
1040
1316
|
const modelPath = join(dir, MODEL_DOCUMENT_FILE);
|
|
1041
|
-
let core: Poser | null = null;
|
|
1317
|
+
let core: { poser: Poser; roster: SkinRoster } | null = null;
|
|
1042
1318
|
let why: string;
|
|
1043
1319
|
if (forced === 'spine') why = '--poser spine';
|
|
1044
1320
|
else if (!existsSync(modelPath)) why = `no ${MODEL_DOCUMENT_FILE} beside ${resolve(skeletonPath)} — a Spine export, not a rigc build`;
|
|
@@ -1048,10 +1324,13 @@ export function candidatePosers(
|
|
|
1048
1324
|
"atlas's, and the core would pose them against another one's pages";
|
|
1049
1325
|
} else {
|
|
1050
1326
|
try {
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1327
|
+
const modelText = readFileSync(modelPath, 'utf8');
|
|
1328
|
+
const bytes = readFileSync(skeletonPath);
|
|
1329
|
+
const candidate = make(modelText, readFileSync(atlasPath, 'utf8'), modelPath, { path: resolve(skeletonPath), bytes });
|
|
1330
|
+
const rosters = skeletonRosters(bytes.toString('utf8'));
|
|
1331
|
+
const differs = rosterDifference(candidate, rosters);
|
|
1053
1332
|
if (differs === null) {
|
|
1054
|
-
core = candidate;
|
|
1333
|
+
core = { poser: candidate, roster: coreSkinRoster(modelText, modelPath, rosters.skins) };
|
|
1055
1334
|
why = modelPath;
|
|
1056
1335
|
} else why = `${modelPath} does not describe ${resolve(skeletonPath)}: ${differs}`;
|
|
1057
1336
|
} catch (err) {
|
|
@@ -1060,8 +1339,35 @@ export function candidatePosers(
|
|
|
1060
1339
|
why = `the core refused ${err.message.startsWith(`${modelPath}: `) ? err.message : `${modelPath}: ${err.message}`}`;
|
|
1061
1340
|
}
|
|
1062
1341
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1342
|
+
return posersOver(forced, core, why, spineData);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* `--poser core` on an input that cannot carry it, refused by name
|
|
1347
|
+
* (`PoserChoiceError`) — said by the caller where its refusals have always
|
|
1348
|
+
* stood, after the flags that refuse before it. Returns the choice otherwise.
|
|
1349
|
+
*/
|
|
1350
|
+
export function refuseUnchosen(choice: PoserChoice): PoserChoice {
|
|
1351
|
+
if (choice.forced === 'core' && choice.core === null) throw new PoserChoiceError(`--poser core: ${choice.why}`);
|
|
1352
|
+
return choice;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Both posers for the skeleton at `skeletonPath` drawn through the atlas at
|
|
1357
|
+
* `atlasPath`, with spine-core's parse of it already in hand (`data`) — the
|
|
1358
|
+
* choice `loadCandidate` makes, for a caller that loaded the pair itself.
|
|
1359
|
+
* `forced` is `--poser`; `core` on an input that cannot carry it is refused by
|
|
1360
|
+
* name (`PoserChoiceError`).
|
|
1361
|
+
*/
|
|
1362
|
+
export function candidatePosers(
|
|
1363
|
+
data: SkeletonData,
|
|
1364
|
+
skeletonPath: string,
|
|
1365
|
+
atlasPath: string,
|
|
1366
|
+
forced: PoserName | undefined,
|
|
1367
|
+
/** What builds the core poser: `corePoser` — the suite's `RC02` passes a planted copy, and nothing else passes any. */
|
|
1368
|
+
make: MakeCorePoser = corePoser,
|
|
1369
|
+
): PoserChoice {
|
|
1370
|
+
return refuseUnchosen(choosePosers(skeletonPath, atlasPath, forced, () => data, make));
|
|
1065
1371
|
}
|
|
1066
1372
|
|
|
1067
1373
|
/**
|
|
@@ -1070,19 +1376,22 @@ export function candidatePosers(
|
|
|
1070
1376
|
* (`CoreInputError`, e.g. a clip polygon that is not simple), in
|
|
1071
1377
|
* which case `run` starts again from nothing on spine-core and `note` names the
|
|
1072
1378
|
* refusal. Under `--poser core` that refusal is a `PoserChoiceError` instead.
|
|
1073
|
-
* `run` must write nothing: a fallback re-runs it whole.
|
|
1379
|
+
* `run` must write nothing: a fallback re-runs it whole. It is handed the
|
|
1380
|
+
* skin roster behind the poser it runs (`PoserChoice.rosterOf`), so a run
|
|
1381
|
+
* through the core reads nothing through spine-core (issue #1014).
|
|
1074
1382
|
*/
|
|
1075
|
-
export function throughPoser<T>(choice: PoserChoice, run: (poser: Poser) => T): { value: T; poser: PoserName; note: string } {
|
|
1383
|
+
export function throughPoser<T>(choice: PoserChoice, run: (poser: Poser, roster: SkinRoster) => T): { value: T; poser: PoserName; note: string } {
|
|
1384
|
+
const through = (poser: Poser): T => run(poser, choice.rosterOf(poser));
|
|
1076
1385
|
if (choice.core !== null) {
|
|
1077
1386
|
try {
|
|
1078
|
-
return { value:
|
|
1387
|
+
return { value: through(choice.core), poser: 'core', note: `rigc core — ${choice.why}` };
|
|
1079
1388
|
} catch (err) {
|
|
1080
1389
|
if (!(err instanceof CoreInputError)) throw err;
|
|
1081
1390
|
if (choice.forced === 'core') throw new PoserChoiceError(`--poser core: the core refused this input: ${err.message}`);
|
|
1082
|
-
return { value:
|
|
1391
|
+
return { value: through(choice.spine), poser: 'spine', note: `spine-core — the core refused this input: ${err.message}` };
|
|
1083
1392
|
}
|
|
1084
1393
|
}
|
|
1085
|
-
return { value:
|
|
1394
|
+
return { value: through(choice.spine), poser: 'spine', note: `spine-core — ${choice.why}` };
|
|
1086
1395
|
}
|
|
1087
1396
|
|
|
1088
1397
|
/**
|
|
@@ -2135,6 +2444,10 @@ export class UnframeablePoseError extends GeometryError {}
|
|
|
2135
2444
|
* structure the unframeable sentence names a skin from (issue #997). Not a
|
|
2136
2445
|
* poser's: the `Poser` seam carries no skin roster, and both posers pose the one
|
|
2137
2446
|
* Spine file this is read from (the core's document is bound to it by hash).
|
|
2447
|
+
* Two readings stand behind it — the runtime's (`skinRosterOf`) and the model
|
|
2448
|
+
* document's (`coreSkinRoster` in `./render_core.ts`, issue #1014), measured
|
|
2449
|
+
* to name the same bones under every skin — and `PoserChoice.rosterOf` hands
|
|
2450
|
+
* each poser its own.
|
|
2138
2451
|
*/
|
|
2139
2452
|
export interface SkinRoster {
|
|
2140
2453
|
/** Every skin, in declaration order. */
|
|
@@ -2162,7 +2475,7 @@ export function skinRosterOf(data: SkeletonData): SkinRoster {
|
|
|
2162
2475
|
|
|
2163
2476
|
/** The roster behind a pose source: its own when it is Spine data, else the one the caller passed. */
|
|
2164
2477
|
function rosterBehind(source: PoseSource, roster: SkinRoster | undefined): SkinRoster | undefined {
|
|
2165
|
-
return source
|
|
2478
|
+
return isPoser(source) ? roster : skinRosterOf(source);
|
|
2166
2479
|
}
|
|
2167
2480
|
|
|
2168
2481
|
/**
|