spine-rigc 0.22.2 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -1
- package/cli.ts +263 -13
- package/docs/AUTHORING.md +554 -75
- package/docs/INGEST.md +238 -44
- package/docs/SPEC_COVERAGE.md +14 -3
- package/package.json +1 -1
- package/skills/ingest/SKILL.md +33 -11
- package/src/atlas.ts +135 -16
- package/src/check.ts +83 -1
- package/src/compile.ts +402 -74
- package/src/deformmeasure.ts +322 -151
- package/src/diff.ts +125 -2
- package/src/ingest.ts +1137 -0
- package/src/render.ts +113 -17
- package/src/rig.ts +92 -8
- package/src/timelines.ts +163 -0
- package/src/types.ts +74 -9
- package/src/validate.ts +765 -92
- package/tools/editor_roundtrip.ts +247 -36
package/src/deformmeasure.ts
CHANGED
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
Skeleton,
|
|
108
108
|
type SkeletonData,
|
|
109
109
|
SkeletonJson,
|
|
110
|
+
type Skin,
|
|
110
111
|
Slider,
|
|
111
112
|
SliderData,
|
|
112
113
|
TextureAtlas,
|
|
@@ -277,6 +278,18 @@ export interface DeformKeyDraw {
|
|
|
277
278
|
* none of this mesh is drawn.
|
|
278
279
|
*/
|
|
279
280
|
alpha: number;
|
|
281
|
+
/**
|
|
282
|
+
* The skin the skeleton was **wearing** when all of the above was read, off
|
|
283
|
+
* `Skeleton.skin` itself — the skin that holds this timeline's attachment
|
|
284
|
+
* (issue #583) — or `null` when it wore none.
|
|
285
|
+
*
|
|
286
|
+
* ⭐ Read off the posed skeleton rather than passed in beside it, so it cannot
|
|
287
|
+
* disagree with what was actually set. It is what makes `blank` a statement
|
|
288
|
+
* about a pose instead of a verdict on the rig: "the slot shows nothing" and
|
|
289
|
+
* "the slot shows nothing in the dress this mesh lives in" are different
|
|
290
|
+
* claims, and only the second is measurable.
|
|
291
|
+
*/
|
|
292
|
+
under: string | null;
|
|
280
293
|
/**
|
|
281
294
|
* Why this mesh puts no pixels on the screen at that time, in the words the
|
|
282
295
|
* `DEFORM` block and A39's stats line both print — or `null` when it puts some
|
|
@@ -794,147 +807,211 @@ export function surveyDeformKeys(data: SkeletonData, exempt: ReadonlySet<string>
|
|
|
794
807
|
let spansNotScanned = 0;
|
|
795
808
|
const dialTies: DeformDialTie[] = [];
|
|
796
809
|
const dialDisputes: DeformDialDispute[] = [];
|
|
797
|
-
|
|
810
|
+
/**
|
|
811
|
+
* `reachesOf` under one skin, computed once per skin the survey reaches.
|
|
812
|
+
*
|
|
813
|
+
* ⚠️ Keyed on the `Skin` OBJECT, which is what `placementOf` recovered. A map
|
|
814
|
+
* keyed on the name would fold two same-named skins into one entry and hand
|
|
815
|
+
* the second one the first's plans (#583).
|
|
816
|
+
*/
|
|
817
|
+
const reachCache = new Map<Skin | null, Map<string, Array<DialPlan | null>>>();
|
|
818
|
+
const reachesUnder = (skin: Skin | null): Map<string, Array<DialPlan | null>> => {
|
|
819
|
+
const already = reachCache.get(skin);
|
|
820
|
+
if (already !== undefined) return already;
|
|
821
|
+
const built = reachesOf(data, skin);
|
|
822
|
+
reachCache.set(skin, built);
|
|
823
|
+
return built;
|
|
824
|
+
};
|
|
825
|
+
/**
|
|
826
|
+
* Sliders whose tie or dispute is already in the rollup, by name.
|
|
827
|
+
*
|
|
828
|
+
* A slider used to be planned once per animation and therefore reported once.
|
|
829
|
+
* It is now planned once per (animation, skin) — so the guard keeps the rollup
|
|
830
|
+
* at one line per slider, and it loses nothing: a slider the skins do not
|
|
831
|
+
* touch (`skinRequired` false, and a driving bone that is active under every
|
|
832
|
+
* skin) probes identically under all of them, and one they do touch is
|
|
833
|
+
* `active` under exactly the one skin that lists it, because a rig spec is
|
|
834
|
+
* refused for putting a constraint or a bone in two skins.
|
|
835
|
+
*/
|
|
836
|
+
const dialsReported = new Set<string>();
|
|
798
837
|
for (const anim of data.animations) {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
838
|
+
/**
|
|
839
|
+
* What each of this animation's deform timelines is, and which skin holds
|
|
840
|
+
* its attachment — settled in one pass, BEFORE the frames loop (issue #583).
|
|
841
|
+
*
|
|
842
|
+
* 🚨 Once per timeline, not once per (timeline, skin, way-in). `timelines`,
|
|
843
|
+
* `notAMesh` and `exempted` are counts of the animation's own contents, and
|
|
844
|
+
* a skeleton with meshes in two skins would report each of them twice if the
|
|
845
|
+
* tally sat inside the loops below.
|
|
846
|
+
*/
|
|
847
|
+
const placed: Array<{
|
|
848
|
+
timeline: DeformTimeline;
|
|
849
|
+
attachment: MeshAttachment;
|
|
850
|
+
triangles: ArrayLike<number>;
|
|
851
|
+
slotName: string;
|
|
852
|
+
placement: ReturnType<typeof placementOf>;
|
|
853
|
+
}> = [];
|
|
854
|
+
for (const timeline of anim.timelines) {
|
|
855
|
+
if (!(timeline instanceof DeformTimeline)) continue;
|
|
856
|
+
timelines++;
|
|
857
|
+
const attachment = timeline.attachment;
|
|
858
|
+
const slotName = data.slots[timeline.slotIndex]?.name ?? `#${timeline.slotIndex}`;
|
|
859
|
+
// A bounding box, a clipping polygon and a path all have a vertex array
|
|
860
|
+
// and NO triangles, so they have no winding to keep and no area to take a
|
|
861
|
+
// ratio of. Saying nothing about them beats inventing a measurement.
|
|
862
|
+
if (!(attachment instanceof MeshAttachment)) {
|
|
863
|
+
notAMesh.add(`"${slotName}"`);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (exempt.has(slotName)) {
|
|
867
|
+
exempted.add(`"${slotName}"`);
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
const triangles = attachment.triangles;
|
|
871
|
+
if (!triangles || triangles.length < 3) continue; // A04 owns a mesh with no triangles
|
|
872
|
+
placed.push({
|
|
873
|
+
timeline,
|
|
874
|
+
attachment,
|
|
875
|
+
triangles,
|
|
876
|
+
slotName,
|
|
877
|
+
placement: placementOf(data, timeline.slotIndex, attachment),
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
if (placed.length === 0) continue;
|
|
881
|
+
// One pass per skin the animation's meshes live in, in the order its
|
|
882
|
+
// timelines name them. Every rig rigc compiles today has exactly one, and
|
|
883
|
+
// then this loop runs once and the keys come out in timeline order as they
|
|
884
|
+
// always have.
|
|
885
|
+
for (const skin of new Set(placed.map((p) => p.placement.holder))) {
|
|
886
|
+
// One pass per way in (issue #407). The animations nothing applies get the
|
|
887
|
+
// single track pass this loop has always been.
|
|
888
|
+
for (const dials of reachesUnder(skin).get(anim.name) ?? [null]) {
|
|
889
|
+
const poseFrame = (time: number): PoseOfFrame =>
|
|
890
|
+
dials === null
|
|
891
|
+
? { posed: poseAt(data, skin, anim.name, time), dial: null }
|
|
892
|
+
: poseDial(data, skin, dials, time);
|
|
893
|
+
const reach = dials === null ? TRACK_REACH : dials.reach;
|
|
894
|
+
/**
|
|
895
|
+
* The key times this plan actually **reached**, for the reach comparison
|
|
896
|
+
* below (#427).
|
|
897
|
+
*
|
|
898
|
+
* ⚠️ A key the drive itself could not select is not in here. It is already
|
|
899
|
+
* named on the stats line as `deformKeysUnreachable`, with the ask and the
|
|
900
|
+
* bound, and the artifact's answer cannot reach it either — so listing it
|
|
901
|
+
* as something the artifact's answer missed would count one defect twice
|
|
902
|
+
* and inflate a disagreement with a frame the disagreement did not cost.
|
|
903
|
+
*/
|
|
904
|
+
const posedTimes = new Set<number>();
|
|
905
|
+
for (const { timeline, attachment, triangles, slotName, placement } of placed) {
|
|
906
|
+
// Measured in the dress the format keys this timeline on, and only
|
|
907
|
+
// there: a mesh in another skin is another skin's pose (issue #583).
|
|
908
|
+
if (placement.holder !== skin) continue;
|
|
909
|
+
const named = {
|
|
910
|
+
animation: anim.name,
|
|
911
|
+
skin: placement.skin,
|
|
912
|
+
slot: slotName,
|
|
913
|
+
attachment: attachment.name,
|
|
914
|
+
placeholder: placement.placeholder,
|
|
915
|
+
};
|
|
916
|
+
/** The previous key's posed frame, kept so the span between them can be scanned. */
|
|
917
|
+
let previous: PosedFrame | null = null;
|
|
918
|
+
for (let frame = 0; frame < timeline.frames.length; frame++) {
|
|
919
|
+
const time = timeline.frames[frame];
|
|
920
|
+
const at = poseFrame(time);
|
|
921
|
+
const frameMeasure = measurePosed(at.posed, time, timeline.slotIndex, attachment, triangles, reach, at.dial);
|
|
922
|
+
// A key that draws no pixels — or one at a time no dial can select —
|
|
923
|
+
// is measured and then left out of the totals, because those totals
|
|
924
|
+
// are "what the gate ran on": A39 reads them onto its stats line and
|
|
925
|
+
// the report's rollup has to match them.
|
|
926
|
+
if (at.dial?.unreachable === true) {
|
|
927
|
+
notReachable++;
|
|
928
|
+
notReachableReversed += frameMeasure.measure.reversed.length;
|
|
929
|
+
} else if (frameMeasure.measure.draw.blank === null) {
|
|
930
|
+
trianglesMeasured += frameMeasure.measure.triangles;
|
|
931
|
+
collapsedTotal += frameMeasure.measure.collapsed;
|
|
870
932
|
} else {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
anim,
|
|
874
|
-
timeline,
|
|
875
|
-
attachment,
|
|
876
|
-
triangles,
|
|
877
|
-
named,
|
|
878
|
-
frame - 1,
|
|
879
|
-
previous,
|
|
880
|
-
frameMeasure,
|
|
881
|
-
poseFrame,
|
|
882
|
-
),
|
|
883
|
-
);
|
|
933
|
+
notDrawn++;
|
|
934
|
+
notDrawnReversed += frameMeasure.measure.reversed.length;
|
|
884
935
|
}
|
|
936
|
+
keys.push({ ...named, key: frame, time, ...frameMeasure.measure });
|
|
937
|
+
if (at.dial?.unreachable !== true) posedTimes.add(time);
|
|
938
|
+
if (previous !== null) {
|
|
939
|
+
// ⚠️ A span whose end is a frame the runtime cannot reach has no
|
|
940
|
+
// interpolation to scan: the anchors it would solve the quadratic
|
|
941
|
+
// over are two poses of some other time. Counted, never silent.
|
|
942
|
+
if (at.dial?.unreachable === true || previous.measure.dial?.unreachable === true) {
|
|
943
|
+
spansNotScanned++;
|
|
944
|
+
} else {
|
|
945
|
+
spans.push(
|
|
946
|
+
scanDeformSpan(
|
|
947
|
+
anim,
|
|
948
|
+
timeline,
|
|
949
|
+
attachment,
|
|
950
|
+
triangles,
|
|
951
|
+
named,
|
|
952
|
+
frame - 1,
|
|
953
|
+
previous,
|
|
954
|
+
frameMeasure,
|
|
955
|
+
poseFrame,
|
|
956
|
+
),
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
previous = frameMeasure;
|
|
885
961
|
}
|
|
886
|
-
previous = frameMeasure;
|
|
887
962
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
963
|
+
// --- what the artifact's own answer could NOT have posed (issue #427) ---
|
|
964
|
+
//
|
|
965
|
+
// ⭐ Posed, not predicted. The survey builds a second plan out of the field
|
|
966
|
+
// the SKELETON names and runs it through the same `poseDial` every real
|
|
967
|
+
// frame goes through, so each entry is spine-core saying "no settable value
|
|
968
|
+
// of this field lands me on that time" rather than this file inferring it
|
|
969
|
+
// off an interval. Measured (#427): the list is empty whenever the two
|
|
970
|
+
// answers reach the same span, and empty is the reading that says the
|
|
971
|
+
// disagreement changed nothing about which frames were measured.
|
|
972
|
+
//
|
|
973
|
+
// 🚨 And nothing is reported at all about a dial that posed NOTHING — a
|
|
974
|
+
// slider whose animation carries no deform timeline. `outside` would be
|
|
975
|
+
// empty there for the one reason that must never print as agreement:
|
|
976
|
+
// there were no frames to disagree about. A comparison of nothing and a
|
|
977
|
+
// comparison that came out equal are the vacuous pass this file exists
|
|
978
|
+
// to keep apart.
|
|
979
|
+
if (posedTimes.size === 0) continue;
|
|
980
|
+
// A plan is visited once per (animation, skin) — see `dialsReported` for
|
|
981
|
+
// why one line per slider is the whole of it — so the two lists come out
|
|
982
|
+
// in the skeleton's own constraint order.
|
|
983
|
+
const alreadyReported = dials !== null && dialsReported.has(dials.slider.name);
|
|
984
|
+
if (dials !== null) dialsReported.add(dials.slider.name);
|
|
985
|
+
if (dials?.tie && !alreadyReported) dialTies.push(dials.tie);
|
|
986
|
+
if (dials?.dispute && !alreadyReported) dialDisputes.push(dials.dispute);
|
|
987
|
+
if (dials?.dispute && dials.statedMap !== null) {
|
|
988
|
+
const shadow: DialPlan = {
|
|
989
|
+
...dials,
|
|
990
|
+
field: dials.statedMap.field,
|
|
991
|
+
u0: dials.statedMap.u0,
|
|
992
|
+
v0: dials.statedMap.v0,
|
|
993
|
+
u1: dials.statedMap.u1,
|
|
994
|
+
v1: dials.statedMap.v1,
|
|
995
|
+
};
|
|
996
|
+
// ⚠️ The one time posing cannot answer: a field that moves the reading by
|
|
997
|
+
// NOTHING has no map to invert, so `poseDial` divides by zero and calls
|
|
998
|
+
// every time out of bounds — including the setup time, which that field
|
|
999
|
+
// reaches by being left alone. The reach says which one that is, and it
|
|
1000
|
+
// is a single point. Nothing in spine-core 4.3 has been measured getting
|
|
1001
|
+
// here (a parent at exactly 90° still moves a world x reading by 2.3e-8),
|
|
1002
|
+
// and a report that over-stated a disagreement by one key would be the
|
|
1003
|
+
// false red this file has paid for twice.
|
|
1004
|
+
const flat = dials.statedMap.v1 === dials.statedMap.v0;
|
|
1005
|
+
const only = dials.dispute.statedReach;
|
|
1006
|
+
const outside = [...posedTimes]
|
|
1007
|
+
.filter((time) =>
|
|
1008
|
+
flat
|
|
1009
|
+
? only === null || Math.abs(time - only.lo) > DIAL_TIME_EPSILON
|
|
1010
|
+
: poseDial(data, skin, shadow, time).dial?.unreachable === true,
|
|
1011
|
+
)
|
|
1012
|
+
.sort((a, b) => a - b);
|
|
1013
|
+
dials.dispute.outside.push(...outside);
|
|
1014
|
+
}
|
|
938
1015
|
}
|
|
939
1016
|
}
|
|
940
1017
|
}
|
|
@@ -1181,6 +1258,57 @@ const DIAL_PROBE_MARGIN = 1e3;
|
|
|
1181
1258
|
*/
|
|
1182
1259
|
const DIAL_DRIVE_LIMIT = 2 ** 24;
|
|
1183
1260
|
|
|
1261
|
+
/**
|
|
1262
|
+
* A fresh skeleton with `skin` worn, which is what every pose below starts from.
|
|
1263
|
+
*
|
|
1264
|
+
* ## Why a skin, and why this one
|
|
1265
|
+
*
|
|
1266
|
+
* A deform timeline is keyed on a `skin / slot / attachment` triple, so the mesh
|
|
1267
|
+
* it deforms belongs to exactly one skin — and a skeleton nobody dressed shows
|
|
1268
|
+
* only what `SkeletonData.defaultSkin` holds. Posing with no skin therefore read
|
|
1269
|
+
* a slot that showed **nothing** for every mesh an author had moved into a named
|
|
1270
|
+
* skin, and the whole survey then reported the rig as undrawn: `A39` went from
|
|
1271
|
+
* PASS to SKIP with a sentence that blamed the rig for what the measurement was
|
|
1272
|
+
* doing (issue #583). `placementOf` already recovers which skin holds a
|
|
1273
|
+
* timeline's attachment, so the pose can wear it.
|
|
1274
|
+
*
|
|
1275
|
+
* ## What `setSkin` changes, off the runtime rather than from memory
|
|
1276
|
+
*
|
|
1277
|
+
* `Skeleton.setSkinBySkin` (spine-core 4.3.13 `Skeleton.js:292-313`) puts the
|
|
1278
|
+
* skin's art into each slot's pose and calls `updateCache`, and `updateCache`
|
|
1279
|
+
* (`Skeleton.js:142-187`) is where the other two thirds live: a `skinRequired`
|
|
1280
|
+
* bone is `active` only if the worn skin lists it, and a `skinRequired`
|
|
1281
|
+
* constraint only if `skin.constraints` includes it. Both were measured on this
|
|
1282
|
+
* fixture before the repair and both were silent in their own way —
|
|
1283
|
+
*
|
|
1284
|
+
* - a slider whose driving bone is skin-required reads a world property that
|
|
1285
|
+
* **nothing moves** while the bone is inactive, so `planDial` found no
|
|
1286
|
+
* responding field, returned `null`, and the animation was reported as
|
|
1287
|
+
* *"played on a track"* — an animation only a slider ever applies;
|
|
1288
|
+
* - a skin-required slider **constraint** is left out of the update cache, so
|
|
1289
|
+
* `SliderPose.time` never leaves its setup value and every key came back as
|
|
1290
|
+
* *"at a time no dial selects"*.
|
|
1291
|
+
*
|
|
1292
|
+
* Each pose below then calls `setupPose()`, which re-resolves every slot's setup
|
|
1293
|
+
* attachment through `Skeleton.getAttachment` — the worn skin first, then
|
|
1294
|
+
* `defaultSkin` (`Skeleton.js:335-346`) — so wearing the skin before the pose is
|
|
1295
|
+
* the whole of what is needed and nothing has to be re-attached afterwards.
|
|
1296
|
+
*
|
|
1297
|
+
* ⭐ It takes the `Skin` **object**, not its name. `placementOf` found it by
|
|
1298
|
+
* identity, and `findSkin` resolves a name to the FIRST skin that carries it —
|
|
1299
|
+
* so a round trip through the name would hand the runtime a different skin on a
|
|
1300
|
+
* skeleton that declares two of one name, and there would be nothing in the
|
|
1301
|
+
* output to say so. `src/render.ts`'s `skeletonUnderSkin` takes a name because
|
|
1302
|
+
* its name came from `--skin` on the command line and refusing an unknown one
|
|
1303
|
+
* **by name, with the names that would have worked** is the whole of its job;
|
|
1304
|
+
* here there is no name to refuse and no lookup that can fail.
|
|
1305
|
+
*/
|
|
1306
|
+
function skeletonUnderSkin(data: SkeletonData, skin: Skin | null): Skeleton {
|
|
1307
|
+
const skeleton = new Skeleton(data);
|
|
1308
|
+
if (skin !== null) skeleton.setSkin(skin);
|
|
1309
|
+
return skeleton;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1184
1312
|
/**
|
|
1185
1313
|
* Every way each animation is reached, keyed by animation name.
|
|
1186
1314
|
*
|
|
@@ -1197,8 +1325,15 @@ const DIAL_DRIVE_LIMIT = 2 ** 24;
|
|
|
1197
1325
|
* `slider.<name>.mix` from a playing animation, where the frame the deform keys
|
|
1198
1326
|
* actually occur in is that playing animation's, and rigc has no way to know
|
|
1199
1327
|
* which one that is.
|
|
1328
|
+
*
|
|
1329
|
+
* ⚠️ **Per skin, because whether a slider runs at all is per skin** (#583). A
|
|
1330
|
+
* `skinRequired` slider is out of the update cache under every skin that does
|
|
1331
|
+
* not list it, and a slider on a `skinRequired` bone reads a world property that
|
|
1332
|
+
* nothing moves there — so the same constraint plans differently depending on
|
|
1333
|
+
* what the skeleton is wearing, and the answer that matters is the one under the
|
|
1334
|
+
* skin holding the mesh being measured.
|
|
1200
1335
|
*/
|
|
1201
|
-
function reachesOf(data: SkeletonData): Map<string, Array<DialPlan | null>> {
|
|
1336
|
+
function reachesOf(data: SkeletonData, skin: Skin | null): Map<string, Array<DialPlan | null>> {
|
|
1202
1337
|
const out = new Map<string, Array<DialPlan | null>>();
|
|
1203
1338
|
for (const anim of data.animations) out.set(anim.name, []);
|
|
1204
1339
|
for (const constraint of data.constraints) {
|
|
@@ -1206,7 +1341,7 @@ function reachesOf(data: SkeletonData): Map<string, Array<DialPlan | null>> {
|
|
|
1206
1341
|
if (constraint.setupPose.mix === 0) continue;
|
|
1207
1342
|
const list = out.get(constraint.animation?.name ?? '');
|
|
1208
1343
|
if (list === undefined) continue;
|
|
1209
|
-
const plan = planDial(data, constraint);
|
|
1344
|
+
const plan = planDial(data, skin, constraint);
|
|
1210
1345
|
if (plan !== null) list.push(plan);
|
|
1211
1346
|
}
|
|
1212
1347
|
for (const list of out.values()) if (list.length === 0) list.push(null);
|
|
@@ -1254,7 +1389,7 @@ function sliderOn(skeleton: Skeleton, data: SliderData): Slider | null {
|
|
|
1254
1389
|
* ⛔ Nothing here falls back to `rotation`, or to any field, when the search does
|
|
1255
1390
|
* not settle. Two answers that disagree are two answers, printed.
|
|
1256
1391
|
*/
|
|
1257
|
-
function planDial(data: SkeletonData, slider: SliderData): DialPlan | null {
|
|
1392
|
+
function planDial(data: SkeletonData, skin: Skin | null, slider: SliderData): DialPlan | null {
|
|
1258
1393
|
const boneName = slider.bone?.name ?? '?';
|
|
1259
1394
|
const where = slider.local ? ' (local)' : ' (world)';
|
|
1260
1395
|
const reach = (discovery: DialDiscovery | null): DeformReach => {
|
|
@@ -1290,7 +1425,7 @@ function planDial(data: SkeletonData, slider: SliderData): DialPlan | null {
|
|
|
1290
1425
|
if (slider.bone === null) {
|
|
1291
1426
|
return { slider, reach: reach(null), tie: null, dispute: null, field: null, u0: 0, v0: 0, u1: 1, v1: 1, statedMap: null };
|
|
1292
1427
|
}
|
|
1293
|
-
const skeleton =
|
|
1428
|
+
const skeleton = skeletonUnderSkin(data, skin);
|
|
1294
1429
|
const instance = sliderOn(skeleton, slider);
|
|
1295
1430
|
const bone = instance?.bone ?? null;
|
|
1296
1431
|
if (instance === null || bone === null) return null;
|
|
@@ -1497,14 +1632,14 @@ function sliderTimeFor(slider: SliderData, time: number): number {
|
|
|
1497
1632
|
* against `sliderTimeFor` — spine-core's answer, not this function's. A time
|
|
1498
1633
|
* no dial value selects is reported and never guessed at.
|
|
1499
1634
|
*/
|
|
1500
|
-
function poseDial(data: SkeletonData, plan: DialPlan, time: number): PoseOfFrame {
|
|
1635
|
+
function poseDial(data: SkeletonData, skin: Skin | null, plan: DialPlan, time: number): PoseOfFrame {
|
|
1501
1636
|
const slider = plan.slider;
|
|
1502
1637
|
const wanted = sliderTimeFor(slider, time);
|
|
1503
1638
|
const value = plan.field === null ? time : slider.property.offset + (time - slider.offset) / slider.scale;
|
|
1504
|
-
const posed =
|
|
1639
|
+
const posed = skeletonUnderSkin(data, skin);
|
|
1505
1640
|
const instance = sliderOn(posed, slider);
|
|
1506
1641
|
const bone = instance?.bone ?? null;
|
|
1507
|
-
if (instance === null) return { posed: poseAt(data, slider.animation.name, time), dial: null };
|
|
1642
|
+
if (instance === null) return { posed: poseAt(data, skin, slider.animation.name, time), dial: null };
|
|
1508
1643
|
/** Pose with the driving field at `candidate`, and read both sides back. */
|
|
1509
1644
|
const at = (candidate: number): { read: number; applied: number } => {
|
|
1510
1645
|
posed.setupPose();
|
|
@@ -1586,8 +1721,8 @@ function poseDial(data: SkeletonData, plan: DialPlan, time: number): PoseOfFrame
|
|
|
1586
1721
|
* that is what lands the sample exactly ON `time` rather than one update short of
|
|
1587
1722
|
* it, and it is why a probe between two keys is as trustworthy as a key.
|
|
1588
1723
|
*/
|
|
1589
|
-
function poseAt(data: SkeletonData, animation: string, time: number): Skeleton {
|
|
1590
|
-
const posed =
|
|
1724
|
+
function poseAt(data: SkeletonData, skin: Skin | null, animation: string, time: number): Skeleton {
|
|
1725
|
+
const posed = skeletonUnderSkin(data, skin);
|
|
1591
1726
|
const state = new AnimationState(new AnimationStateData(data));
|
|
1592
1727
|
state.setAnimation(0, animation, false);
|
|
1593
1728
|
posed.setupPose();
|
|
@@ -2266,10 +2401,34 @@ function shownAt(
|
|
|
2266
2401
|
* `timelineSlots` of its own, so on a rigc-compiled skeleton the list is empty
|
|
2267
2402
|
* and this loop runs zero times; it is here because a foreign skeleton reaching
|
|
2268
2403
|
* `explain` is exactly where a silent false green would be unnoticeable.
|
|
2404
|
+
*
|
|
2405
|
+
* 🚨 **Every sentence here is about ONE skin — the one the skeleton is wearing**
|
|
2406
|
+
* (issue #583), which `under` names off `Skeleton.skin` rather than off whatever
|
|
2407
|
+
* the caller believes it set. That matters most in the loop below: a slot the
|
|
2408
|
+
* deform reaches through `timelineSlots` carries a LINKED mesh, a separate
|
|
2409
|
+
* attachment object that lives in a skin of its own, and `setSkin` dresses the
|
|
2410
|
+
* whole skeleton at once — so a linked copy whose skin is not the one worn here
|
|
2411
|
+
* resolves to nothing and `shownAt` reports alpha 0 for it, exactly as the
|
|
2412
|
+
* runtime would with that same skin on.
|
|
2413
|
+
*
|
|
2414
|
+
* ⛔ Not a reason to pose the key again under each of those other skins. Which
|
|
2415
|
+
* skin is worn is the consumer's, and a measurement taken under a skin the
|
|
2416
|
+
* timeline is not keyed on would be rigc composing a scene to make its own gate
|
|
2417
|
+
* green. What it must not do is leave the reader guessing which dress the
|
|
2418
|
+
* verdict was taken in, so the skin is in the sentence.
|
|
2269
2419
|
*/
|
|
2270
2420
|
function drawOfKey(posed: Skeleton, slotIndex: number, attachment: MeshAttachment): DeformKeyDraw {
|
|
2271
2421
|
const own = shownAt(posed, slotIndex, attachment);
|
|
2272
|
-
const
|
|
2422
|
+
const under = posed.skin?.name ?? null;
|
|
2423
|
+
const draw = { shown: own.shown?.name ?? null, showsThisMesh: own.showsThisMesh, alpha: own.alpha, under };
|
|
2424
|
+
// 🔒 On the "shows something else" branch ONLY, because that is the one branch
|
|
2425
|
+
// the skin decides: what a slot shows is resolved through the worn skin and
|
|
2426
|
+
// then `defaultSkin`, while an alpha is read off the pose and has no skin in
|
|
2427
|
+
// it. Before #583 the pose wore no skin at all, so a mesh in a named skin was
|
|
2428
|
+
// reported as a slot showing nothing — a true sentence about a pose nobody
|
|
2429
|
+
// would ever play, read as a claim about the rig. A clause on every branch
|
|
2430
|
+
// would be noise on the branch it cannot explain.
|
|
2431
|
+
const dress = under === null ? ', with no skin worn' : `, with skin "${under}" worn`;
|
|
2273
2432
|
for (const other of attachment.timelineSlots) {
|
|
2274
2433
|
if (other === slotIndex) continue;
|
|
2275
2434
|
const there = shownAt(posed, other, attachment);
|
|
@@ -2285,8 +2444,8 @@ function drawOfKey(posed: Skeleton, slotIndex: number, attachment: MeshAttachmen
|
|
|
2285
2444
|
return {
|
|
2286
2445
|
...draw,
|
|
2287
2446
|
blank:
|
|
2288
|
-
`the slot shows ${instead} at this time, not this mesh, so the runtime applies no deform to it
|
|
2289
|
-
'and draws none of it',
|
|
2447
|
+
`the slot shows ${instead} at this time${dress}, not this mesh, so the runtime applies no deform to it ` +
|
|
2448
|
+
'here and draws none of it',
|
|
2290
2449
|
};
|
|
2291
2450
|
}
|
|
2292
2451
|
if (own.alpha === 0) {
|
|
@@ -2311,19 +2470,31 @@ function drawOfKey(posed: Skeleton, slotIndex: number, attachment: MeshAttachmen
|
|
|
2311
2470
|
* general: a skin puts its own attachment behind a shared placeholder, which is
|
|
2312
2471
|
* the whole point of skins. So the scan compares the attachment object, and the
|
|
2313
2472
|
* placeholder printed is the one the spec wrote.
|
|
2473
|
+
*
|
|
2474
|
+
* ⭐ The `Skin` it found comes back with the name, because that object is what
|
|
2475
|
+
* the pose wears (`skeletonUnderSkin`, issue #583). Handing the pose the name
|
|
2476
|
+
* instead would resolve it again through `SkeletonData.findSkin`, which returns
|
|
2477
|
+
* the FIRST skin of that name — a second resolution that can land somewhere else
|
|
2478
|
+
* on a skeleton declaring two, and land there silently.
|
|
2479
|
+
*
|
|
2480
|
+
* `holder` is `null` only when no skin carries this attachment at all. Nothing
|
|
2481
|
+
* reaches that through `SkeletonJson`, which resolves a deform timeline's
|
|
2482
|
+
* attachment out of a skin before it can build the timeline, so it is the
|
|
2483
|
+
* defensive branch and not a case: the pose then wears nothing and behaves
|
|
2484
|
+
* exactly as every pose here did before #583.
|
|
2314
2485
|
*/
|
|
2315
2486
|
function placementOf(
|
|
2316
2487
|
data: SkeletonData,
|
|
2317
2488
|
slotIndex: number,
|
|
2318
2489
|
attachment: MeshAttachment,
|
|
2319
|
-
): { skin: string; placeholder: string } {
|
|
2490
|
+
): { skin: string; holder: Skin | null; placeholder: string } {
|
|
2320
2491
|
for (const skin of [data.defaultSkin, ...data.skins]) {
|
|
2321
2492
|
if (!skin) continue;
|
|
2322
2493
|
const entries: Array<{ placeholder: string; attachment: unknown }> = [];
|
|
2323
2494
|
skin.getAttachmentsForSlot(slotIndex, entries as Parameters<typeof skin.getAttachmentsForSlot>[1]);
|
|
2324
2495
|
for (const entry of entries) {
|
|
2325
|
-
if (entry.attachment === attachment) return { skin: skin.name, placeholder: entry.placeholder };
|
|
2496
|
+
if (entry.attachment === attachment) return { skin: skin.name, holder: skin, placeholder: entry.placeholder };
|
|
2326
2497
|
}
|
|
2327
2498
|
}
|
|
2328
|
-
return { skin: 'default', placeholder: attachment.name };
|
|
2499
|
+
return { skin: 'default', holder: null, placeholder: attachment.name };
|
|
2329
2500
|
}
|