spine-rigc 1.5.3 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/cli.ts +131 -403
- package/docs/AUTHORING.md +63 -14
- package/docs/INGEST.md +1 -0
- package/docs/MOTION.md +1 -0
- package/package.json +1 -1
- package/src/atlas.ts +25 -0
- package/src/check.ts +113 -24
- package/src/checkpics.ts +2 -0
- package/src/compile.ts +641 -506
- package/src/core/animation.ts +982 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1721 -0
- package/src/core/constraints_path.ts +942 -0
- package/src/core/constraints_physics.ts +830 -0
- package/src/core/constraints_slider.ts +505 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1103 -0
- package/src/core/raw.ts +347 -0
- package/src/core/skins.ts +104 -0
- package/src/core/uvs.ts +448 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/world.ts +233 -0
- package/src/deformmeasure.ts +337 -73
- package/src/deformreport.ts +370 -0
- package/src/depth.ts +101 -2
- package/src/emit_spine.ts +860 -0
- package/src/mesh.ts +24 -15
- package/src/model.ts +958 -0
- package/src/render.ts +649 -143
- package/src/render_core.ts +582 -0
- package/src/rig.ts +34 -6
- package/src/transform.ts +26 -4
- package/src/types.ts +18 -0
- package/src/validate.ts +30 -2
package/src/deformmeasure.ts
CHANGED
|
@@ -114,6 +114,9 @@ import {
|
|
|
114
114
|
TextureAtlas,
|
|
115
115
|
type Timeline,
|
|
116
116
|
} from '@esotericsoftware/spine-core';
|
|
117
|
+
// #969: the core's side of the seam — the model document read, and the survey's hooks (`./core/hooks.ts`).
|
|
118
|
+
import { CoreInputError, readModel, underSkin, type CompiledDocument } from './core/index.ts';
|
|
119
|
+
import { float32Rows, meshWorld, poseDial as corePoseDial, poseJump, recordIdentity, sliderRecordOf, slotDraw, type CoreSurveyPose } from './core/hooks.ts';
|
|
117
120
|
|
|
118
121
|
/**
|
|
119
122
|
* How near zero a triangle's area has to be, as a fraction of the largest
|
|
@@ -707,6 +710,19 @@ export interface DeformSurvey {
|
|
|
707
710
|
* could not have posed (issues #419, #427).
|
|
708
711
|
*/
|
|
709
712
|
dialDisputes: DeformDialDispute[];
|
|
713
|
+
/**
|
|
714
|
+
* Which poser the survey posed through, and why when it is not the one
|
|
715
|
+
* asked for (issue #969): `model` — rigc's own core over the model document;
|
|
716
|
+
* `spine-core` — the runtime over the Spine skeleton. Every other field is
|
|
717
|
+
* the same survey whichever it was; `tools/survey_hashes.ts` holds that.
|
|
718
|
+
*/
|
|
719
|
+
source: DeformSurveyRecord;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** The survey's record of its poser (`DeformSurvey.source`). */
|
|
723
|
+
export interface DeformSurveyRecord {
|
|
724
|
+
used: DeformSurveySource;
|
|
725
|
+
why: string | null;
|
|
710
726
|
}
|
|
711
727
|
|
|
712
728
|
/**
|
|
@@ -794,6 +810,16 @@ export function skeletonDataFromText(skeletonText: string, atlasText: string): S
|
|
|
794
810
|
* printed; what it is not is gated.
|
|
795
811
|
*/
|
|
796
812
|
export function surveyDeformKeys(data: SkeletonData, exempt: ReadonlySet<string> = new Set()): DeformSurvey {
|
|
813
|
+
return { ...surveyWith(data, exempt, spinePoser(data)), source: { used: 'spine-core', why: null } };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* The survey through one poser (issue #969): `surveyDeformKeys`' whole body,
|
|
818
|
+
* with every pose taken through `poser` — spine-core's or the core's. What it
|
|
819
|
+
* reads off `data` is the skeleton's structure (its timelines, keys, curves,
|
|
820
|
+
* sliders and skins), which is data and not a pose.
|
|
821
|
+
*/
|
|
822
|
+
function surveyWith(data: SkeletonData, exempt: ReadonlySet<string>, poser: SurveyPoser): Omit<DeformSurvey, 'source'> {
|
|
797
823
|
const keys: DeformKeyMeasure[] = [];
|
|
798
824
|
const spans: DeformSpan[] = [];
|
|
799
825
|
const exempted = new Set<string>();
|
|
@@ -819,7 +845,7 @@ export function surveyDeformKeys(data: SkeletonData, exempt: ReadonlySet<string>
|
|
|
819
845
|
const reachesUnder = (skin: Skin | null): Map<string, Array<DialPlan | null>> => {
|
|
820
846
|
const already = reachCache.get(skin);
|
|
821
847
|
if (already !== undefined) return already;
|
|
822
|
-
const built = reachesOf(data, skin);
|
|
848
|
+
const built = reachesOf(data, skin, poser);
|
|
823
849
|
reachCache.set(skin, built);
|
|
824
850
|
return built;
|
|
825
851
|
};
|
|
@@ -889,8 +915,8 @@ export function surveyDeformKeys(data: SkeletonData, exempt: ReadonlySet<string>
|
|
|
889
915
|
for (const dials of reachesUnder(skin).get(anim.name) ?? [null]) {
|
|
890
916
|
const poseFrame = (time: number): PoseOfFrame =>
|
|
891
917
|
dials === null
|
|
892
|
-
? { posed:
|
|
893
|
-
: poseDial(
|
|
918
|
+
? { posed: poser.track(skin, anim.name, time), dial: null }
|
|
919
|
+
: poseDial(poser, skin, dials, time);
|
|
894
920
|
const reach = dials === null ? TRACK_REACH : dials.reach;
|
|
895
921
|
/**
|
|
896
922
|
* The key times this plan actually **reached**, for the reach comparison
|
|
@@ -1008,7 +1034,7 @@ export function surveyDeformKeys(data: SkeletonData, exempt: ReadonlySet<string>
|
|
|
1008
1034
|
.filter((time) =>
|
|
1009
1035
|
flat
|
|
1010
1036
|
? only === null || Math.abs(time - only.lo) > DIAL_TIME_EPSILON
|
|
1011
|
-
: poseDial(
|
|
1037
|
+
: poseDial(poser, skin, shadow, time).dial?.unreachable === true,
|
|
1012
1038
|
)
|
|
1013
1039
|
.sort((a, b) => a - b);
|
|
1014
1040
|
dials.dispute.outside.push(...outside);
|
|
@@ -1334,7 +1360,7 @@ function skeletonUnderSkin(data: SkeletonData, skin: Skin | null): Skeleton {
|
|
|
1334
1360
|
* what the skeleton is wearing, and the answer that matters is the one under the
|
|
1335
1361
|
* skin holding the mesh being measured.
|
|
1336
1362
|
*/
|
|
1337
|
-
function reachesOf(data: SkeletonData, skin: Skin | null): Map<string, Array<DialPlan | null>> {
|
|
1363
|
+
function reachesOf(data: SkeletonData, skin: Skin | null, poser: SurveyPoser): Map<string, Array<DialPlan | null>> {
|
|
1338
1364
|
const out = new Map<string, Array<DialPlan | null>>();
|
|
1339
1365
|
for (const anim of data.animations) out.set(anim.name, []);
|
|
1340
1366
|
for (const constraint of data.constraints) {
|
|
@@ -1342,7 +1368,7 @@ function reachesOf(data: SkeletonData, skin: Skin | null): Map<string, Array<Dia
|
|
|
1342
1368
|
if (constraint.setupPose.mix === 0) continue;
|
|
1343
1369
|
const list = out.get(constraint.animation?.name ?? '');
|
|
1344
1370
|
if (list === undefined) continue;
|
|
1345
|
-
const plan = planDial(
|
|
1371
|
+
const plan = planDial(poser, skin, constraint);
|
|
1346
1372
|
if (plan !== null) list.push(plan);
|
|
1347
1373
|
}
|
|
1348
1374
|
for (const list of out.values()) if (list.length === 0) list.push(null);
|
|
@@ -1390,7 +1416,7 @@ function sliderOn(skeleton: Skeleton, data: SliderData): Slider | null {
|
|
|
1390
1416
|
* ⛔ Nothing here falls back to `rotation`, or to any field, when the search does
|
|
1391
1417
|
* not settle. Two answers that disagree are two answers, printed.
|
|
1392
1418
|
*/
|
|
1393
|
-
function planDial(
|
|
1419
|
+
function planDial(poser: SurveyPoser, skin: Skin | null, slider: SliderData): DialPlan | null {
|
|
1394
1420
|
const boneName = slider.bone?.name ?? '?';
|
|
1395
1421
|
const where = slider.local ? ' (local)' : ' (world)';
|
|
1396
1422
|
const reach = (discovery: DialDiscovery | null): DeformReach => {
|
|
@@ -1426,20 +1452,17 @@ function planDial(data: SkeletonData, skin: Skin | null, slider: SliderData): Di
|
|
|
1426
1452
|
if (slider.bone === null) {
|
|
1427
1453
|
return { slider, reach: reach(null), tie: null, dispute: null, field: null, u0: 0, v0: 0, u1: 1, v1: 1, statedMap: null };
|
|
1428
1454
|
}
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
const bone = instance?.bone ?? null;
|
|
1432
|
-
if (instance === null || bone === null) return null;
|
|
1455
|
+
const session = poser.dial(skin, slider);
|
|
1456
|
+
if (session === null || !session.hasBone) return null;
|
|
1433
1457
|
// ⚠️ Every field is kept, the dead ones included, because the ARTIFACT may name
|
|
1434
1458
|
// one of them: a reach comparison needs the map of the field the skeleton
|
|
1435
1459
|
// declares even when that field moves the reading by nothing at all (#427).
|
|
1436
1460
|
const all: DialProbe[] = [];
|
|
1437
1461
|
for (const field of DIAL_FIELDS) {
|
|
1438
1462
|
const step = dialStep(field);
|
|
1439
|
-
|
|
1440
|
-
const
|
|
1441
|
-
const
|
|
1442
|
-
const v1 = dialValue(skeleton, slider, bone, field, base + step);
|
|
1463
|
+
const base = session.base(field);
|
|
1464
|
+
const v0 = session.at(field, base).read;
|
|
1465
|
+
const v1 = session.at(field, base + step).read;
|
|
1443
1466
|
all.push({ field, response: Math.abs(v1 - v0), u0: base, v0, u1: base + step, v1 });
|
|
1444
1467
|
}
|
|
1445
1468
|
const probes = all.filter((p) => Number.isFinite(p.response) && p.response !== 0);
|
|
@@ -1569,19 +1592,6 @@ function dialDiscoveryClause(discovery: DialDiscovery, bone: string): string {
|
|
|
1569
1592
|
);
|
|
1570
1593
|
}
|
|
1571
1594
|
|
|
1572
|
-
/**
|
|
1573
|
-
* The property value spine-core reads off the driving bone with its local field
|
|
1574
|
-
* set to `u` — `Slider.update`'s own call, at its own point in the update.
|
|
1575
|
-
*/
|
|
1576
|
-
function dialValue(skeleton: Skeleton, slider: SliderData, bone: Bone, field: DialField, u: number): number {
|
|
1577
|
-
skeleton.setupPose();
|
|
1578
|
-
skeleton.update(0);
|
|
1579
|
-
bone.pose[field] = u;
|
|
1580
|
-
skeleton.updateWorldTransform(Physics.reset);
|
|
1581
|
-
if (slider.local) bone.appliedPose.validateLocalTransform(skeleton);
|
|
1582
|
-
return slider.property.value(skeleton, bone.appliedPose, slider.local, DIAL_ZERO_OFFSETS);
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
1595
|
/**
|
|
1586
1596
|
* The `offsets` argument every `FromProperty.value` takes.
|
|
1587
1597
|
*
|
|
@@ -1593,7 +1603,7 @@ const DIAL_ZERO_OFFSETS = [0, 0, 0, 0, 0, 0];
|
|
|
1593
1603
|
|
|
1594
1604
|
/** A posed frame and the dial that selected it, or `null` on a track frame. */
|
|
1595
1605
|
interface PoseOfFrame {
|
|
1596
|
-
posed:
|
|
1606
|
+
posed: SurveyPose;
|
|
1597
1607
|
dial: DeformDial | null;
|
|
1598
1608
|
}
|
|
1599
1609
|
|
|
@@ -1633,28 +1643,15 @@ function sliderTimeFor(slider: SliderData, time: number): number {
|
|
|
1633
1643
|
* against `sliderTimeFor` — spine-core's answer, not this function's. A time
|
|
1634
1644
|
* no dial value selects is reported and never guessed at.
|
|
1635
1645
|
*/
|
|
1636
|
-
function poseDial(
|
|
1646
|
+
function poseDial(poser: SurveyPoser, skin: Skin | null, plan: DialPlan, time: number): PoseOfFrame {
|
|
1637
1647
|
const slider = plan.slider;
|
|
1638
1648
|
const wanted = sliderTimeFor(slider, time);
|
|
1639
1649
|
const value = plan.field === null ? time : slider.property.offset + (time - slider.offset) / slider.scale;
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
const bone = instance?.bone ?? null;
|
|
1643
|
-
if (instance === null) return { posed: poseAt(data, skin, slider.animation.name, time), dial: null };
|
|
1650
|
+
const session = poser.dial(skin, slider);
|
|
1651
|
+
if (session === null) return { posed: poser.track(skin, slider.animation.name, time), dial: null };
|
|
1644
1652
|
/** Pose with the driving field at `candidate`, and read both sides back. */
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
posed.update(0);
|
|
1648
|
-
if (plan.field !== null && bone !== null) bone.pose[plan.field] = candidate;
|
|
1649
|
-
else instance.pose.time = candidate;
|
|
1650
|
-
posed.updateWorldTransform(Physics.reset);
|
|
1651
|
-
if (plan.field === null || bone === null) return { read: candidate, applied: instance.appliedPose.time };
|
|
1652
|
-
if (slider.local) bone.appliedPose.validateLocalTransform(posed);
|
|
1653
|
-
return {
|
|
1654
|
-
read: slider.property.value(posed, bone.appliedPose, slider.local, DIAL_ZERO_OFFSETS),
|
|
1655
|
-
applied: instance.appliedPose.time,
|
|
1656
|
-
};
|
|
1657
|
-
};
|
|
1653
|
+
const field = session.hasBone ? plan.field : null;
|
|
1654
|
+
const at = (candidate: number): { read: number; applied: number } => session.at(field, candidate);
|
|
1658
1655
|
/** The affine first guess, and what it is judged against. */
|
|
1659
1656
|
const asked = plan.u0 + ((value - plan.v0) * (plan.u1 - plan.u0)) / (plan.v1 - plan.v0);
|
|
1660
1657
|
// 🚨 The bound, and it is on the drive the answer is READ off, not on the
|
|
@@ -1700,7 +1697,7 @@ function poseDial(data: SkeletonData, skin: Skin | null, plan: DialPlan, time: n
|
|
|
1700
1697
|
got = at(u);
|
|
1701
1698
|
}
|
|
1702
1699
|
return {
|
|
1703
|
-
posed,
|
|
1700
|
+
posed: session.pose(),
|
|
1704
1701
|
dial: {
|
|
1705
1702
|
value,
|
|
1706
1703
|
driven: u,
|
|
@@ -1739,7 +1736,7 @@ function poseAt(data: SkeletonData, skin: Skin | null, animation: string, time:
|
|
|
1739
1736
|
/** One posed time, measured — and the two world arrays it was measured from. */
|
|
1740
1737
|
interface PosedFrame {
|
|
1741
1738
|
time: number;
|
|
1742
|
-
posed:
|
|
1739
|
+
posed: SurveyPose;
|
|
1743
1740
|
/** The mesh as the runtime deformed it there. */
|
|
1744
1741
|
deformed: Float32Array;
|
|
1745
1742
|
/** The same bones with the deform cleared. The denominator, by construction 1.000. */
|
|
@@ -1759,7 +1756,7 @@ interface PosedFrame {
|
|
|
1759
1756
|
* the emptied slot to evaluate the two keys' geometry at one pose.
|
|
1760
1757
|
*/
|
|
1761
1758
|
function measurePosed(
|
|
1762
|
-
posed:
|
|
1759
|
+
posed: SurveyPose,
|
|
1763
1760
|
time: number,
|
|
1764
1761
|
slotIndex: number,
|
|
1765
1762
|
attachment: MeshAttachment,
|
|
@@ -1768,18 +1765,14 @@ function measurePosed(
|
|
|
1768
1765
|
dial: DeformDial | null,
|
|
1769
1766
|
): PosedFrame {
|
|
1770
1767
|
const count = attachment.worldVerticesLength;
|
|
1771
|
-
const slot = posed.slots[slotIndex];
|
|
1772
1768
|
// Read BEFORE the deform is cleared below, and off the same posed skeleton:
|
|
1773
1769
|
// what the slot shows here and at what alpha is the other half of what this
|
|
1774
1770
|
// frame does (issue #401).
|
|
1775
1771
|
const draw = drawOfKey(posed, slotIndex, attachment);
|
|
1776
|
-
const deformed =
|
|
1777
|
-
attachment.computeWorldVertices(posed, slot, 0, count, deformed, 0, 2);
|
|
1772
|
+
const deformed = posed.deformed(slotIndex, attachment);
|
|
1778
1773
|
// The same bones, with the deform taken away. `computeWorldVertices` reads the
|
|
1779
1774
|
// array off the slot, so emptying it is the whole control.
|
|
1780
|
-
|
|
1781
|
-
const plain = new Float32Array(count);
|
|
1782
|
-
attachment.computeWorldVertices(posed, slot, 0, count, plain, 0, 2);
|
|
1775
|
+
const plain = posed.plain(slotIndex, attachment);
|
|
1783
1776
|
|
|
1784
1777
|
const before = triangleAreas(plain, triangles);
|
|
1785
1778
|
const after = triangleAreas(deformed, triangles);
|
|
@@ -2214,7 +2207,6 @@ function scanDeformSpan(
|
|
|
2214
2207
|
notDrawn: 0,
|
|
2215
2208
|
unconfirmed: false,
|
|
2216
2209
|
};
|
|
2217
|
-
const count = attachment.worldVerticesLength;
|
|
2218
2210
|
const reach = reachedFractions(legs);
|
|
2219
2211
|
const v1 = timeline.vertices[frame];
|
|
2220
2212
|
const v2 = timeline.vertices[frame + 1];
|
|
@@ -2245,9 +2237,9 @@ function scanDeformSpan(
|
|
|
2245
2237
|
// there for the runtime to have applied anything at all.
|
|
2246
2238
|
const own = anchor.measure.draw.showsThisMesh;
|
|
2247
2239
|
const a =
|
|
2248
|
-
own && anchor === from ? from.deformed :
|
|
2240
|
+
own && anchor === from ? from.deformed : anchor.posed.withDeform(timeline.slotIndex, attachment, v1);
|
|
2249
2241
|
const b =
|
|
2250
|
-
own && anchor === to ? to.deformed :
|
|
2242
|
+
own && anchor === to ? to.deformed : anchor.posed.withDeform(timeline.slotIndex, attachment, v2);
|
|
2251
2243
|
// The anchor's own plain areas, taken once when it was measured as a key —
|
|
2252
2244
|
// the same numbers, so the span cannot disagree with the key about which
|
|
2253
2245
|
// triangles have a winding to keep.
|
|
@@ -2339,8 +2331,8 @@ function worldWithDeform(
|
|
|
2339
2331
|
slotIndex: number,
|
|
2340
2332
|
attachment: MeshAttachment,
|
|
2341
2333
|
deform: ArrayLike<number>,
|
|
2342
|
-
count: number,
|
|
2343
2334
|
): Float32Array {
|
|
2335
|
+
const count = attachment.worldVerticesLength;
|
|
2344
2336
|
const slot = posed.slots[slotIndex];
|
|
2345
2337
|
const array = slot.appliedPose.deform;
|
|
2346
2338
|
array.length = deform.length;
|
|
@@ -2362,20 +2354,16 @@ function worldWithDeform(
|
|
|
2362
2354
|
* not something the skeleton data can say, and `src/render.ts` does not read it
|
|
2363
2355
|
* either.
|
|
2364
2356
|
*/
|
|
2365
|
-
function shownAt(
|
|
2366
|
-
posed: Skeleton,
|
|
2367
|
-
slotIndex: number,
|
|
2368
|
-
attachment: MeshAttachment,
|
|
2369
|
-
): { shown: Attachment | null; showsThisMesh: boolean; slotAlpha: number; attachmentAlpha: number; alpha: number } {
|
|
2357
|
+
function shownAt(posed: Skeleton, slotIndex: number, attachment: MeshAttachment): ShownReading {
|
|
2370
2358
|
const pose = posed.slots[slotIndex]?.appliedPose;
|
|
2371
|
-
const shown = pose?.attachment ?? null;
|
|
2359
|
+
const shown: Attachment | null = pose?.attachment ?? null;
|
|
2372
2360
|
// The same comparison `DeformTimeline.applyToSlot` makes before it writes
|
|
2373
2361
|
// anything, so "shown" here means exactly "the runtime deforms it here".
|
|
2374
2362
|
const showsThisMesh = shown !== null && shown.timelineAttachment === attachment;
|
|
2375
2363
|
const slotAlpha = pose?.color.a ?? 0;
|
|
2376
2364
|
const attachmentAlpha = shown instanceof MeshAttachment ? shown.color.a : 1;
|
|
2377
2365
|
return {
|
|
2378
|
-
shown,
|
|
2366
|
+
shown: shown?.name ?? null,
|
|
2379
2367
|
showsThisMesh,
|
|
2380
2368
|
slotAlpha,
|
|
2381
2369
|
attachmentAlpha,
|
|
@@ -2422,10 +2410,10 @@ function shownAt(
|
|
|
2422
2410
|
* green. What it must not do is leave the reader guessing which dress the
|
|
2423
2411
|
* verdict was taken in, so the skin is in the sentence.
|
|
2424
2412
|
*/
|
|
2425
|
-
function drawOfKey(posed:
|
|
2426
|
-
const own = shownAt(
|
|
2427
|
-
const under = posed.
|
|
2428
|
-
const draw = { shown: own.shown
|
|
2413
|
+
function drawOfKey(posed: SurveyPose, slotIndex: number, attachment: MeshAttachment): DeformKeyDraw {
|
|
2414
|
+
const own = posed.shownAt(slotIndex, attachment);
|
|
2415
|
+
const under = posed.under();
|
|
2416
|
+
const draw = { shown: own.shown, showsThisMesh: own.showsThisMesh, alpha: own.alpha, under };
|
|
2429
2417
|
// 🔒 On the "shows something else" branch ONLY, because that is the one branch
|
|
2430
2418
|
// the skin decides: what a slot shows is resolved through the worn skin and
|
|
2431
2419
|
// then `defaultSkin`, while an alpha is read off the pose and has no skin in
|
|
@@ -2436,7 +2424,7 @@ function drawOfKey(posed: Skeleton, slotIndex: number, attachment: MeshAttachmen
|
|
|
2436
2424
|
const dress = under === null ? ', with no skin worn' : `, with skin "${under}" worn`;
|
|
2437
2425
|
for (const other of attachment.timelineSlots) {
|
|
2438
2426
|
if (other === slotIndex) continue;
|
|
2439
|
-
const there = shownAt(
|
|
2427
|
+
const there = posed.shownAt(other, attachment);
|
|
2440
2428
|
if (there.alpha > 0) {
|
|
2441
2429
|
// Drawn somewhere the deform reaches, so there is nothing to exempt — and
|
|
2442
2430
|
// the geometry above was measured on a slot that is not the one drawing
|
|
@@ -2445,7 +2433,7 @@ function drawOfKey(posed: Skeleton, slotIndex: number, attachment: MeshAttachmen
|
|
|
2445
2433
|
}
|
|
2446
2434
|
}
|
|
2447
2435
|
if (!own.showsThisMesh) {
|
|
2448
|
-
const instead = own.shown === null ? 'no attachment at all' : `attachment "${own.shown
|
|
2436
|
+
const instead = own.shown === null ? 'no attachment at all' : `attachment "${own.shown}"`;
|
|
2449
2437
|
return {
|
|
2450
2438
|
...draw,
|
|
2451
2439
|
blank:
|
|
@@ -2503,3 +2491,279 @@ function placementOf(
|
|
|
2503
2491
|
}
|
|
2504
2492
|
return { skin: 'default', holder: null, placeholder: attachment.name };
|
|
2505
2493
|
}
|
|
2494
|
+
|
|
2495
|
+
// --- #969 the seam: begin ---
|
|
2496
|
+
//
|
|
2497
|
+
// The survey poses a skeleton and reads four things off it; everything else it
|
|
2498
|
+
// does is arithmetic over those readings and over the skeleton's structure. So
|
|
2499
|
+
// the seam is those poses and readings, and there are two posers behind it:
|
|
2500
|
+
// spine-core over the Spine skeleton (the survey as it always was, and
|
|
2501
|
+
// `validate.ts`'s A39, which hands it the SkeletonData it round-tripped), and
|
|
2502
|
+
// rigc's own core over the model document (`./core/hooks.ts`, issue #969),
|
|
2503
|
+
// which a build carries and `explain` hands in. The survey's record names which
|
|
2504
|
+
// one it used (`DeformSurvey.source`), and `tools/survey_hashes.ts` holds the
|
|
2505
|
+
// two to one survey, byte for byte, on every corpus.
|
|
2506
|
+
|
|
2507
|
+
/** Which poser a survey used (`DeformSurvey.source`). */
|
|
2508
|
+
export type DeformSurveySource = 'spine-core' | 'model';
|
|
2509
|
+
|
|
2510
|
+
/** What one slot shows of a mesh at one pose, and at what alpha — `shownAt`'s reading. */
|
|
2511
|
+
export interface ShownReading {
|
|
2512
|
+
/** The shown attachment's name, or `null` when the slot shows none. */
|
|
2513
|
+
shown: string | null;
|
|
2514
|
+
showsThisMesh: boolean;
|
|
2515
|
+
slotAlpha: number;
|
|
2516
|
+
attachmentAlpha: number;
|
|
2517
|
+
alpha: number;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* One posed skeleton as the survey reads it.
|
|
2522
|
+
*
|
|
2523
|
+
* ⚠️ spine-core's side is a live skeleton: `plain` empties the slot's deform
|
|
2524
|
+
* array for good and `withDeform` writes one and empties it again, exactly as
|
|
2525
|
+
* the survey always did — the survey reads `deformed` before `plain` on a pose
|
|
2526
|
+
* and never after, so the core's side, which holds no state, reads the same.
|
|
2527
|
+
*/
|
|
2528
|
+
export interface SurveyPose {
|
|
2529
|
+
/** The skin the skeleton wears, off the pose itself (`DeformKeyDraw.under`). */
|
|
2530
|
+
under(): string | null;
|
|
2531
|
+
shownAt(slotIndex: number, attachment: MeshAttachment): ShownReading;
|
|
2532
|
+
/** The mesh's world vertices with the slot's deform as posed. */
|
|
2533
|
+
deformed(slotIndex: number, attachment: MeshAttachment): Float32Array;
|
|
2534
|
+
/** The same bones with the slot's deform cleared. */
|
|
2535
|
+
plain(slotIndex: number, attachment: MeshAttachment): Float32Array;
|
|
2536
|
+
/** The same bones with `deform` written into the slot. */
|
|
2537
|
+
withDeform(slotIndex: number, attachment: MeshAttachment, deform: ArrayLike<number>): Float32Array;
|
|
2538
|
+
/**
|
|
2539
|
+
* The doubles those three are stored from — the slot's deform as posed,
|
|
2540
|
+
* cleared, or replaced — leaving the pose as it found it. The survey reads
|
|
2541
|
+
* the float32 rows; this is what measures the rows' source (`tools/survey_hashes.ts hooks`).
|
|
2542
|
+
*/
|
|
2543
|
+
rows(slotIndex: number, attachment: MeshAttachment, deform: 'posed' | 'cleared' | ArrayLike<number>): number[];
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
/** One slider's dial, posed again and again (`planDial`'s probe, `poseDial`'s solve). */
|
|
2547
|
+
export interface DialSession {
|
|
2548
|
+
/** Whether the slider has a driving bone — `false` on the bone-less form, whose dial is its time. */
|
|
2549
|
+
hasBone: boolean;
|
|
2550
|
+
/** The driving bone's setup value of `field`. */
|
|
2551
|
+
base(field: DialField): number;
|
|
2552
|
+
/** Posed with `field` at `candidate` (the slider's time when `field` is `null`): the property read, and `SliderPose.time`. */
|
|
2553
|
+
at(field: DialField | null, candidate: number): { read: number; applied: number };
|
|
2554
|
+
/** The pose the last `at` left. */
|
|
2555
|
+
pose(): SurveyPose;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
/** The two ways the survey poses: a jump on a track, and a slider's dial. */
|
|
2559
|
+
export interface SurveyPoser {
|
|
2560
|
+
track(skin: Skin | null, animation: string, time: number): SurveyPose;
|
|
2561
|
+
/** `null` when the skeleton carries no such slider. */
|
|
2562
|
+
dial(skin: Skin | null, slider: SliderData): DialSession | null;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
/** A live spine-core skeleton, read the way the survey always read it. */
|
|
2566
|
+
function spinePose(skeleton: Skeleton): SurveyPose {
|
|
2567
|
+
return {
|
|
2568
|
+
under: () => skeleton.skin?.name ?? null,
|
|
2569
|
+
shownAt: (slotIndex, attachment) => shownAt(skeleton, slotIndex, attachment),
|
|
2570
|
+
deformed: (slotIndex, attachment) => {
|
|
2571
|
+
const count = attachment.worldVerticesLength;
|
|
2572
|
+
const world = new Float32Array(count);
|
|
2573
|
+
attachment.computeWorldVertices(skeleton, skeleton.slots[slotIndex], 0, count, world, 0, 2);
|
|
2574
|
+
return world;
|
|
2575
|
+
},
|
|
2576
|
+
plain: (slotIndex, attachment) => {
|
|
2577
|
+
const count = attachment.worldVerticesLength;
|
|
2578
|
+
const slot = skeleton.slots[slotIndex];
|
|
2579
|
+
slot.appliedPose.deform.length = 0;
|
|
2580
|
+
const world = new Float32Array(count);
|
|
2581
|
+
attachment.computeWorldVertices(skeleton, slot, 0, count, world, 0, 2);
|
|
2582
|
+
return world;
|
|
2583
|
+
},
|
|
2584
|
+
withDeform: (slotIndex, attachment, deform) => worldWithDeform(skeleton, slotIndex, attachment, deform),
|
|
2585
|
+
rows: (slotIndex, attachment, deform) => {
|
|
2586
|
+
const slot = skeleton.slots[slotIndex];
|
|
2587
|
+
const array = slot.appliedPose.deform;
|
|
2588
|
+
const kept = [...array];
|
|
2589
|
+
if (deform === 'cleared') array.length = 0;
|
|
2590
|
+
else if (deform !== 'posed') {
|
|
2591
|
+
array.length = deform.length;
|
|
2592
|
+
for (let i = 0; i < deform.length; i++) array[i] = deform[i];
|
|
2593
|
+
}
|
|
2594
|
+
const out = new Array<number>(attachment.worldVerticesLength);
|
|
2595
|
+
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, out, 0, 2);
|
|
2596
|
+
array.length = kept.length;
|
|
2597
|
+
for (let i = 0; i < kept.length; i++) array[i] = kept[i];
|
|
2598
|
+
return out;
|
|
2599
|
+
},
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/** The survey's poses through spine-core — the recipes the survey has always taken (`poseAt`; the dial below). */
|
|
2604
|
+
function spinePoser(data: SkeletonData): SurveyPoser {
|
|
2605
|
+
return {
|
|
2606
|
+
track: (skin, animation, time) => spinePose(poseAt(data, skin, animation, time)),
|
|
2607
|
+
dial: (skin, slider) => {
|
|
2608
|
+
const skeleton = skeletonUnderSkin(data, skin);
|
|
2609
|
+
const instance = sliderOn(skeleton, slider);
|
|
2610
|
+
if (instance === null) return null;
|
|
2611
|
+
const bone = instance.bone;
|
|
2612
|
+
return {
|
|
2613
|
+
hasBone: bone !== null,
|
|
2614
|
+
base: (field) => {
|
|
2615
|
+
skeleton.setupPose();
|
|
2616
|
+
return (bone as Bone).pose[field];
|
|
2617
|
+
},
|
|
2618
|
+
// `Slider.update`'s own reading, at its own point in the update: the
|
|
2619
|
+
// setup pose, `update(0)`, the field (or the time) written, `Physics.reset`.
|
|
2620
|
+
at: (field, candidate) => {
|
|
2621
|
+
skeleton.setupPose();
|
|
2622
|
+
skeleton.update(0);
|
|
2623
|
+
if (field !== null && bone !== null) bone.pose[field] = candidate;
|
|
2624
|
+
else instance.pose.time = candidate;
|
|
2625
|
+
skeleton.updateWorldTransform(Physics.reset);
|
|
2626
|
+
if (field === null || bone === null) return { read: candidate, applied: instance.appliedPose.time };
|
|
2627
|
+
if (slider.local) bone.appliedPose.validateLocalTransform(skeleton);
|
|
2628
|
+
return { read: slider.property.value(skeleton, bone.appliedPose, slider.local, DIAL_ZERO_OFFSETS), applied: instance.appliedPose.time };
|
|
2629
|
+
},
|
|
2630
|
+
pose: () => spinePose(skeleton),
|
|
2631
|
+
};
|
|
2632
|
+
},
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
/** Which model record each Spine attachment object is, in `placementOf`'s search order. */
|
|
2637
|
+
function recordsOf(data: SkeletonData): Map<Attachment, { skin: string; slot: string; placeholder: string }> {
|
|
2638
|
+
const out = new Map<Attachment, { skin: string; slot: string; placeholder: string }>();
|
|
2639
|
+
for (const skin of [data.defaultSkin, ...data.skins]) {
|
|
2640
|
+
if (!skin) continue;
|
|
2641
|
+
for (let slotIndex = 0; slotIndex < data.slots.length; slotIndex++) {
|
|
2642
|
+
const entries: Array<{ placeholder: string; attachment: Attachment }> = [];
|
|
2643
|
+
skin.getAttachmentsForSlot(slotIndex, entries as Parameters<typeof skin.getAttachmentsForSlot>[1]);
|
|
2644
|
+
for (const entry of entries) {
|
|
2645
|
+
if (!out.has(entry.attachment)) out.set(entry.attachment, { skin: skin.name, slot: data.slots[slotIndex].name, placeholder: entry.placeholder });
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
return out;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
/** A pose of the core's, read through the Spine skeleton's names. */
|
|
2653
|
+
function corePose(pose: CoreSurveyPose, data: SkeletonData, records: ReadonlyMap<Attachment, { skin: string; slot: string; placeholder: string }>): SurveyPose {
|
|
2654
|
+
const slotName = (slotIndex: number): string => {
|
|
2655
|
+
const slot = data.slots[slotIndex];
|
|
2656
|
+
if (slot === undefined) throw new CoreInputError(`slot #${slotIndex} is not a slot of the skeleton`);
|
|
2657
|
+
return slot.name;
|
|
2658
|
+
};
|
|
2659
|
+
const recordOf = (attachment: MeshAttachment): { skin: string; slot: string; placeholder: string } => {
|
|
2660
|
+
const r = records.get(attachment);
|
|
2661
|
+
if (r === undefined) throw new CoreInputError(`mesh "${attachment.name}" is in no skin of the skeleton, so it names no model record`);
|
|
2662
|
+
return r;
|
|
2663
|
+
};
|
|
2664
|
+
const world = (slotIndex: number, attachment: MeshAttachment, deform: 'posed' | 'cleared' | ArrayLike<number>): Float32Array =>
|
|
2665
|
+
float32Rows(meshWorld(pose, slotName(slotIndex), recordOf(attachment), deform));
|
|
2666
|
+
return {
|
|
2667
|
+
under: () => pose.doc.skin,
|
|
2668
|
+
shownAt: (slotIndex, attachment) => {
|
|
2669
|
+
const d = slotDraw(pose, slotName(slotIndex));
|
|
2670
|
+
const r = recordOf(attachment);
|
|
2671
|
+
const showsThisMesh = d.identity !== null && d.identity === recordIdentity(r.skin, r.slot, r.placeholder);
|
|
2672
|
+
return { shown: d.shown, showsThisMesh, slotAlpha: d.slotAlpha, attachmentAlpha: d.attachmentAlpha, alpha: showsThisMesh ? d.slotAlpha * d.attachmentAlpha : 0 };
|
|
2673
|
+
},
|
|
2674
|
+
deformed: (slotIndex, attachment) => world(slotIndex, attachment, 'posed'),
|
|
2675
|
+
plain: (slotIndex, attachment) => world(slotIndex, attachment, 'cleared'),
|
|
2676
|
+
withDeform: (slotIndex, attachment, deform) => world(slotIndex, attachment, deform),
|
|
2677
|
+
rows: (slotIndex, attachment, deform) => meshWorld(pose, slotName(slotIndex), recordOf(attachment), deform),
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
/**
|
|
2682
|
+
* The survey's poses through rigc's own core over the model document
|
|
2683
|
+
* (`./core/hooks.ts`). A skin is worn by name — the core's view of one skin
|
|
2684
|
+
* (`underSkin`) — so a skeleton declaring two skins of one name, and a pose
|
|
2685
|
+
* wearing none, are refused by name: neither reaches the survey through
|
|
2686
|
+
* `SkeletonJson` (`placementOf`'s note), and neither has a view to pose.
|
|
2687
|
+
*/
|
|
2688
|
+
function corePoser(data: SkeletonData, doc: CompiledDocument): SurveyPoser {
|
|
2689
|
+
const records = recordsOf(data);
|
|
2690
|
+
const views = new Map<string, CompiledDocument>();
|
|
2691
|
+
const viewOf = (skin: Skin | null): CompiledDocument => {
|
|
2692
|
+
if (skin === null) throw new CoreInputError('the survey asked for a pose wearing no skin, and the core poses one skin\'s view');
|
|
2693
|
+
if (data.skins.filter((k) => k.name === skin.name).length > 1) throw new CoreInputError(`the skeleton declares two skins named "${skin.name}", and the model names a skin by its name`);
|
|
2694
|
+
const cached = views.get(skin.name);
|
|
2695
|
+
if (cached !== undefined) return cached;
|
|
2696
|
+
const view = underSkin(doc, skin.name);
|
|
2697
|
+
views.set(skin.name, view);
|
|
2698
|
+
return view;
|
|
2699
|
+
};
|
|
2700
|
+
return {
|
|
2701
|
+
track: (skin, animation, time) => corePose(poseJump(viewOf(skin), animation, time), data, records),
|
|
2702
|
+
dial: (skin, slider) => {
|
|
2703
|
+
const view = viewOf(skin);
|
|
2704
|
+
const record = sliderRecordOf(view, slider.name);
|
|
2705
|
+
const boneName = record.bone;
|
|
2706
|
+
let last: CoreSurveyPose | null = null;
|
|
2707
|
+
return {
|
|
2708
|
+
hasBone: boneName !== null,
|
|
2709
|
+
base: (field) => {
|
|
2710
|
+
const bone = view.bones.find((b) => b.name === boneName);
|
|
2711
|
+
if (bone === undefined) throw new CoreInputError(`slider "${slider.name}"'s bone is not a bone of the model document`);
|
|
2712
|
+
const v = bone[field];
|
|
2713
|
+
return v ?? (field === 'scaleX' || field === 'scaleY' ? 1 : 0);
|
|
2714
|
+
},
|
|
2715
|
+
at: (field, candidate) => {
|
|
2716
|
+
const posed = corePoseDial(view, field === null || boneName === null ? { slider: slider.name, time: candidate } : { bone: boneName, field, value: candidate }, slider.name);
|
|
2717
|
+
last = posed;
|
|
2718
|
+
return { read: field === null || posed.read === null ? candidate : posed.read, applied: posed.applied };
|
|
2719
|
+
},
|
|
2720
|
+
pose: () => {
|
|
2721
|
+
if (last === null) throw new CoreInputError(`slider "${slider.name}"'s dial was asked for its pose before it was posed`);
|
|
2722
|
+
return corePose(last, data, records);
|
|
2723
|
+
},
|
|
2724
|
+
};
|
|
2725
|
+
},
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
/**
|
|
2730
|
+
* Both posers over one build — spine-core's and the core's, answering the same
|
|
2731
|
+
* calls — for measurement: `tools/survey_hashes.ts hooks` and the selftest's
|
|
2732
|
+
* `DM` controls hold every hook of the one to the other at tolerance 0.
|
|
2733
|
+
*/
|
|
2734
|
+
export function deformPosers(input: DeformSurveyInput & { modelText: string }): { data: SkeletonData; spine: SurveyPoser; core: SurveyPoser } {
|
|
2735
|
+
const data = skeletonDataFromText(input.skeletonText, input.atlasText);
|
|
2736
|
+
return { data, spine: spinePoser(data), core: corePoser(data, readModel(input.modelText)) };
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
/** A build's three texts, as `explain` and `tools/survey_hashes.ts` hold them. */
|
|
2740
|
+
export interface DeformSurveyInput {
|
|
2741
|
+
skeletonText: string;
|
|
2742
|
+
atlasText: string;
|
|
2743
|
+
/** The model document (`skeleton.model.json`), or `null` when the input carries none (a Spine export). */
|
|
2744
|
+
modelText: string | null;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
/**
|
|
2748
|
+
* The survey of a build, through the poser asked for (issue #969): `model` —
|
|
2749
|
+
* the core over the model document, refused when there is none; `spine-core` —
|
|
2750
|
+
* the runtime over the Spine skeleton; `auto` — the model document when the
|
|
2751
|
+
* input carries one and the core poses it, spine-core otherwise, the reason
|
|
2752
|
+
* named in `source.why`.
|
|
2753
|
+
*/
|
|
2754
|
+
export function surveyOfBuild(input: DeformSurveyInput, exempt: ReadonlySet<string>, asked: 'auto' | DeformSurveySource): DeformSurvey {
|
|
2755
|
+
const data = skeletonDataFromText(input.skeletonText, input.atlasText);
|
|
2756
|
+
const throughSpine = (why: string | null): DeformSurvey => ({ ...surveyWith(data, exempt, spinePoser(data)), source: { used: 'spine-core', why } });
|
|
2757
|
+
if (asked === 'spine-core') return throughSpine(null);
|
|
2758
|
+
if (input.modelText === null) {
|
|
2759
|
+
if (asked === 'model') throw new CoreInputError('the survey was asked to pose the model document, and the input carries none');
|
|
2760
|
+
return throughSpine('the input carries no model document (skeleton.model.json), so the survey posed the Spine skeleton through spine-core');
|
|
2761
|
+
}
|
|
2762
|
+
try {
|
|
2763
|
+
return { ...surveyWith(data, exempt, corePoser(data, readModel(input.modelText))), source: { used: 'model', why: null } };
|
|
2764
|
+
} catch (err) {
|
|
2765
|
+
if (!(err instanceof CoreInputError) || asked === 'model') throw err;
|
|
2766
|
+
return throughSpine(`the core refused to pose the model document — ${err.message}`);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
// --- #969 the seam: end ---
|