spine-rigc 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.ts +87 -20
- package/docs/AUTHORING.md +18 -12
- package/package.json +1 -1
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/skin_entries.ts +35 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/kinds.ts +102 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/index.ts +130 -0
- package/src/assertions/model/parse.ts +136 -0
- package/src/assertions/model/skin_entries.ts +58 -0
- package/src/assertions/model/slot_colour.ts +80 -0
- package/src/assertions/reasons.ts +127 -0
- package/src/assertions/values.ts +39 -0
- package/src/check.ts +33 -8
- package/src/compile.ts +59 -19
- package/src/core/index.ts +7 -5
- package/src/core/world.ts +21 -20
- package/src/deformmeasure.ts +377 -154
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/model.ts +5 -2
- package/src/render.ts +195 -47
- package/src/render_core.ts +68 -8
- package/src/transform.ts +79 -30
- package/src/validate.ts +149 -461
package/src/deformmeasure.ts
CHANGED
|
@@ -117,6 +117,19 @@ import {
|
|
|
117
117
|
// #969: the core's side of the seam — the model document read, and the survey's hooks (`./core/hooks.ts`).
|
|
118
118
|
import { CoreInputError, readModel, underSkin, type CompiledDocument } from './core/index.ts';
|
|
119
119
|
import { float32Rows, meshWorld, poseDial as corePoseDial, poseJump, recordIdentity, sliderRecordOf, slotDraw, type CoreSurveyPose } from './core/hooks.ts';
|
|
120
|
+
// #1019: what the survey reads of the skeleton's structure, and the model document's reading of it.
|
|
121
|
+
import {
|
|
122
|
+
modelStructure,
|
|
123
|
+
type SurveyAnimation,
|
|
124
|
+
type SurveyCurve,
|
|
125
|
+
type SurveyDeformTimeline,
|
|
126
|
+
type SurveyMesh,
|
|
127
|
+
type SurveySkin,
|
|
128
|
+
type SurveySlider,
|
|
129
|
+
type SurveyStructure,
|
|
130
|
+
} from './deformstructure.ts';
|
|
131
|
+
import { spineFileSha256 } from './model.ts';
|
|
132
|
+
import { SpineRuntimeError } from './render.ts';
|
|
120
133
|
|
|
121
134
|
/**
|
|
122
135
|
* How near zero a triangle's area has to be, as a fraction of the largest
|
|
@@ -810,16 +823,19 @@ export function skeletonDataFromText(skeletonText: string, atlasText: string): S
|
|
|
810
823
|
* printed; what it is not is gated.
|
|
811
824
|
*/
|
|
812
825
|
export function surveyDeformKeys(data: SkeletonData, exempt: ReadonlySet<string> = new Set()): DeformSurvey {
|
|
813
|
-
|
|
826
|
+
const side = runtimeSide(data);
|
|
827
|
+
return { ...surveyWith(side.structure, exempt, side.poser), source: { used: 'spine-core', why: null } };
|
|
814
828
|
}
|
|
815
829
|
|
|
816
830
|
/**
|
|
817
|
-
* The survey through one poser (
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
831
|
+
* The survey through one reader and one poser (issues #969, #1019):
|
|
832
|
+
* `surveyDeformKeys`' whole body, with the skeleton's structure — its
|
|
833
|
+
* timelines, keys, curves, sliders and skins — read through `structure`
|
|
834
|
+
* (`./deformstructure.ts`), and every pose taken through `poser`. The two come
|
|
835
|
+
* in pairs: spine-core's reading of the Spine skeleton with spine-core's
|
|
836
|
+
* poses, or the model document's with the core's.
|
|
821
837
|
*/
|
|
822
|
-
function surveyWith(
|
|
838
|
+
function surveyWith(structure: SurveyStructure, exempt: ReadonlySet<string>, poser: SurveyPoser): Omit<DeformSurvey, 'source'> {
|
|
823
839
|
const keys: DeformKeyMeasure[] = [];
|
|
824
840
|
const spans: DeformSpan[] = [];
|
|
825
841
|
const exempted = new Set<string>();
|
|
@@ -837,15 +853,16 @@ function surveyWith(data: SkeletonData, exempt: ReadonlySet<string>, poser: Surv
|
|
|
837
853
|
/**
|
|
838
854
|
* `reachesOf` under one skin, computed once per skin the survey reaches.
|
|
839
855
|
*
|
|
840
|
-
* ⚠️ Keyed on the
|
|
841
|
-
* keyed on the name would fold two
|
|
842
|
-
* the second one the first's plans
|
|
856
|
+
* ⚠️ Keyed on the skin's HANDLE, which is what `placementOf` recovered — one
|
|
857
|
+
* per skin, whichever reader made it. A map keyed on the name would fold two
|
|
858
|
+
* same-named skins into one entry and hand the second one the first's plans
|
|
859
|
+
* (#583).
|
|
843
860
|
*/
|
|
844
|
-
const reachCache = new Map<
|
|
845
|
-
const reachesUnder = (skin:
|
|
861
|
+
const reachCache = new Map<SurveySkin | null, Map<string, Array<DialPlan | null>>>();
|
|
862
|
+
const reachesUnder = (skin: SurveySkin | null): Map<string, Array<DialPlan | null>> => {
|
|
846
863
|
const already = reachCache.get(skin);
|
|
847
864
|
if (already !== undefined) return already;
|
|
848
|
-
const built = reachesOf(
|
|
865
|
+
const built = reachesOf(structure, skin, poser);
|
|
849
866
|
reachCache.set(skin, built);
|
|
850
867
|
return built;
|
|
851
868
|
};
|
|
@@ -861,7 +878,7 @@ function surveyWith(data: SkeletonData, exempt: ReadonlySet<string>, poser: Surv
|
|
|
861
878
|
* refused for putting a constraint or a bone in two skins.
|
|
862
879
|
*/
|
|
863
880
|
const dialsReported = new Set<string>();
|
|
864
|
-
for (const anim of
|
|
881
|
+
for (const anim of structure.animations) {
|
|
865
882
|
/**
|
|
866
883
|
* What each of this animation's deform timelines is, and which skin holds
|
|
867
884
|
* its attachment — settled in one pass, BEFORE the frames loop (issue #583).
|
|
@@ -872,21 +889,20 @@ function surveyWith(data: SkeletonData, exempt: ReadonlySet<string>, poser: Surv
|
|
|
872
889
|
* tally sat inside the loops below.
|
|
873
890
|
*/
|
|
874
891
|
const placed: Array<{
|
|
875
|
-
timeline:
|
|
876
|
-
attachment:
|
|
892
|
+
timeline: SurveyDeformTimeline;
|
|
893
|
+
attachment: SurveyMesh;
|
|
877
894
|
triangles: ArrayLike<number>;
|
|
878
895
|
slotName: string;
|
|
879
|
-
placement: ReturnType<
|
|
896
|
+
placement: ReturnType<SurveyDeformTimeline['placement']>;
|
|
880
897
|
}> = [];
|
|
881
|
-
for (const timeline of anim.
|
|
882
|
-
if (!(timeline instanceof DeformTimeline)) continue;
|
|
898
|
+
for (const timeline of anim.deforms) {
|
|
883
899
|
timelines++;
|
|
884
|
-
const attachment = timeline.
|
|
885
|
-
const slotName =
|
|
900
|
+
const attachment = timeline.mesh;
|
|
901
|
+
const slotName = structure.slotName(timeline.slotIndex);
|
|
886
902
|
// A bounding box, a clipping polygon and a path all have a vertex array
|
|
887
903
|
// and NO triangles, so they have no winding to keep and no area to take a
|
|
888
904
|
// ratio of. Saying nothing about them beats inventing a measurement.
|
|
889
|
-
if (
|
|
905
|
+
if (attachment === null) {
|
|
890
906
|
notAMesh.add(`"${slotName}"`);
|
|
891
907
|
continue;
|
|
892
908
|
}
|
|
@@ -901,7 +917,7 @@ function surveyWith(data: SkeletonData, exempt: ReadonlySet<string>, poser: Surv
|
|
|
901
917
|
attachment,
|
|
902
918
|
triangles,
|
|
903
919
|
slotName,
|
|
904
|
-
placement:
|
|
920
|
+
placement: timeline.placement(),
|
|
905
921
|
});
|
|
906
922
|
}
|
|
907
923
|
if (placed.length === 0) continue;
|
|
@@ -1180,7 +1196,7 @@ interface DialDiscovery {
|
|
|
1180
1196
|
* measured off the runtime rather than assumed.
|
|
1181
1197
|
*/
|
|
1182
1198
|
interface DialPlan {
|
|
1183
|
-
slider:
|
|
1199
|
+
slider: SurveySlider;
|
|
1184
1200
|
reach: DeformReach;
|
|
1185
1201
|
/**
|
|
1186
1202
|
* The probe's tie, when it had one — the survey collects these so a `build`
|
|
@@ -1360,15 +1376,14 @@ function skeletonUnderSkin(data: SkeletonData, skin: Skin | null): Skeleton {
|
|
|
1360
1376
|
* what the skeleton is wearing, and the answer that matters is the one under the
|
|
1361
1377
|
* skin holding the mesh being measured.
|
|
1362
1378
|
*/
|
|
1363
|
-
function reachesOf(
|
|
1379
|
+
function reachesOf(structure: SurveyStructure, skin: SurveySkin | null, poser: SurveyPoser): Map<string, Array<DialPlan | null>> {
|
|
1364
1380
|
const out = new Map<string, Array<DialPlan | null>>();
|
|
1365
|
-
for (const anim of
|
|
1366
|
-
for (const
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1369
|
-
const list = out.get(constraint.animation?.name ?? '');
|
|
1381
|
+
for (const anim of structure.animations) out.set(anim.name, []);
|
|
1382
|
+
for (const slider of structure.sliders) {
|
|
1383
|
+
if (slider.mix === 0) continue;
|
|
1384
|
+
const list = out.get(slider.animation);
|
|
1370
1385
|
if (list === undefined) continue;
|
|
1371
|
-
const plan = planDial(poser, skin,
|
|
1386
|
+
const plan = planDial(poser, skin, slider);
|
|
1372
1387
|
if (plan !== null) list.push(plan);
|
|
1373
1388
|
}
|
|
1374
1389
|
for (const list of out.values()) if (list.length === 0) list.push(null);
|
|
@@ -1416,15 +1431,15 @@ function sliderOn(skeleton: Skeleton, data: SliderData): Slider | null {
|
|
|
1416
1431
|
* ⛔ Nothing here falls back to `rotation`, or to any field, when the search does
|
|
1417
1432
|
* not settle. Two answers that disagree are two answers, printed.
|
|
1418
1433
|
*/
|
|
1419
|
-
function planDial(poser: SurveyPoser, skin:
|
|
1420
|
-
const boneName = slider.bone
|
|
1434
|
+
function planDial(poser: SurveyPoser, skin: SurveySkin | null, slider: SurveySlider): DialPlan | null {
|
|
1435
|
+
const boneName = slider.bone ?? '?';
|
|
1421
1436
|
const where = slider.local ? ' (local)' : ' (world)';
|
|
1422
1437
|
const reach = (discovery: DialDiscovery | null): DeformReach => {
|
|
1423
1438
|
if (discovery === null) {
|
|
1424
1439
|
return {
|
|
1425
1440
|
kind: 'slider',
|
|
1426
1441
|
slider: slider.name,
|
|
1427
|
-
bone: slider.bone
|
|
1442
|
+
bone: slider.bone,
|
|
1428
1443
|
property: null,
|
|
1429
1444
|
drive: null,
|
|
1430
1445
|
local: slider.local,
|
|
@@ -1438,7 +1453,7 @@ function planDial(poser: SurveyPoser, skin: Skin | null, slider: SliderData): Di
|
|
|
1438
1453
|
return {
|
|
1439
1454
|
kind: 'slider',
|
|
1440
1455
|
slider: slider.name,
|
|
1441
|
-
bone: slider.bone
|
|
1456
|
+
bone: slider.bone,
|
|
1442
1457
|
property,
|
|
1443
1458
|
drive,
|
|
1444
1459
|
local: slider.local,
|
|
@@ -1474,14 +1489,14 @@ function planDial(poser: SurveyPoser, skin: Skin | null, slider: SliderData): Di
|
|
|
1474
1489
|
// that broke ties would put the arbitrary choice back where nothing sees it.
|
|
1475
1490
|
probes.sort((a, b) => b.response - a.response);
|
|
1476
1491
|
const winner = probes[0];
|
|
1477
|
-
const stated =
|
|
1492
|
+
const stated = slider.stated;
|
|
1478
1493
|
const leaders = probes.filter((p) => p.response * DIAL_PROBE_MARGIN >= winner.response);
|
|
1479
1494
|
const decisive = leaders.length === 1;
|
|
1480
1495
|
const chosen = decisive || stated === null ? winner : (leaders.find((p) => p.field === stated) ?? winner);
|
|
1481
1496
|
const verdict: DialVerdict = chosen.field === stated ? (decisive ? 'agreed' : 'settled') : 'disagreed';
|
|
1482
1497
|
const discovery: DialDiscovery = {
|
|
1483
1498
|
stated,
|
|
1484
|
-
reader: slider.
|
|
1499
|
+
reader: slider.reader,
|
|
1485
1500
|
drive: chosen.field,
|
|
1486
1501
|
driveResponse: chosen.response,
|
|
1487
1502
|
rivals: leaders.filter((p) => p.field !== chosen.field).map((p) => ({ field: p.field, response: p.response })),
|
|
@@ -1550,15 +1565,15 @@ function planDial(poser: SurveyPoser, skin: Skin | null, slider: SliderData): Di
|
|
|
1550
1565
|
* interval contains. That is why the frames a disputed dial could not have posed
|
|
1551
1566
|
* are POSED rather than read off here.
|
|
1552
1567
|
*/
|
|
1553
|
-
function dialReachOf(slider:
|
|
1568
|
+
function dialReachOf(slider: SurveySlider, map: DialProbe): DialSpan | null {
|
|
1554
1569
|
const timeAt = (u: number): number => {
|
|
1555
1570
|
const value = map.v0 + ((u - map.u0) * (map.v1 - map.v0)) / (map.u1 - map.u0);
|
|
1556
|
-
return slider.
|
|
1571
|
+
return slider.to + (value - slider.from) * slider.scale;
|
|
1557
1572
|
};
|
|
1558
1573
|
const ends = [timeAt(-DIAL_DRIVE_LIMIT), timeAt(DIAL_DRIVE_LIMIT)];
|
|
1559
1574
|
if (!ends.every((t) => Number.isFinite(t))) return null;
|
|
1560
1575
|
const lo = Math.max(0, Math.min(ends[0], ends[1]));
|
|
1561
|
-
const hi = Math.min(slider.
|
|
1576
|
+
const hi = Math.min(slider.duration, Math.max(ends[0], ends[1]));
|
|
1562
1577
|
return lo <= hi ? { lo, hi } : null;
|
|
1563
1578
|
}
|
|
1564
1579
|
|
|
@@ -1618,9 +1633,9 @@ interface PoseOfFrame {
|
|
|
1618
1633
|
* was asked for. ⚠️ `loop` on a zero-length animation gives NaN here exactly
|
|
1619
1634
|
* as it does there, which is `A37_SLIDER_CONSTRAINT_EFFECTIVE`'s refusal.
|
|
1620
1635
|
*/
|
|
1621
|
-
function sliderTimeFor(slider:
|
|
1636
|
+
function sliderTimeFor(slider: SurveySlider, time: number): number {
|
|
1622
1637
|
if (slider.bone === null) return time;
|
|
1623
|
-
if (slider.loop) return slider.
|
|
1638
|
+
if (slider.loop) return slider.duration + (time % slider.duration);
|
|
1624
1639
|
return Math.max(0, time);
|
|
1625
1640
|
}
|
|
1626
1641
|
|
|
@@ -1646,12 +1661,12 @@ function sliderTimeFor(slider: SliderData, time: number): number {
|
|
|
1646
1661
|
* against `sliderTimeFor` — spine-core's answer, not this function's. A time
|
|
1647
1662
|
* no dial value selects is reported and never guessed at.
|
|
1648
1663
|
*/
|
|
1649
|
-
function poseDial(poser: SurveyPoser, skin:
|
|
1664
|
+
function poseDial(poser: SurveyPoser, skin: SurveySkin | null, plan: DialPlan, time: number): PoseOfFrame {
|
|
1650
1665
|
const slider = plan.slider;
|
|
1651
1666
|
const wanted = sliderTimeFor(slider, time);
|
|
1652
|
-
const value = plan.field === null ? time : slider.
|
|
1667
|
+
const value = plan.field === null ? time : slider.from + (time - slider.to) / slider.scale;
|
|
1653
1668
|
const session = poser.dial(skin, slider);
|
|
1654
|
-
if (session === null) return { posed: poser.track(skin, slider.animation
|
|
1669
|
+
if (session === null) return { posed: poser.track(skin, slider.animation, time), dial: null };
|
|
1655
1670
|
/** Pose with the driving field at `candidate`, and read both sides back. */
|
|
1656
1671
|
const field = session.hasBone ? plan.field : null;
|
|
1657
1672
|
const at = (candidate: number): { read: number; applied: number } => session.at(field, candidate);
|
|
@@ -1762,7 +1777,7 @@ function measurePosed(
|
|
|
1762
1777
|
posed: SurveyPose,
|
|
1763
1778
|
time: number,
|
|
1764
1779
|
slotIndex: number,
|
|
1765
|
-
attachment:
|
|
1780
|
+
attachment: SurveyMesh,
|
|
1766
1781
|
triangles: ArrayLike<number>,
|
|
1767
1782
|
reach: DeformReach,
|
|
1768
1783
|
dial: DeformDial | null,
|
|
@@ -1901,33 +1916,37 @@ interface CurveLeg {
|
|
|
1901
1916
|
* The runtime's own interpolation fraction over one span, as a polyline in
|
|
1902
1917
|
* `(time, fraction)`.
|
|
1903
1918
|
*
|
|
1904
|
-
* ⭐ **
|
|
1905
|
-
* `DeformTimeline.getCurvePercent` evaluates a bezier by walking the
|
|
1906
|
-
*
|
|
1907
|
-
* so the polyline below is not an approximation of what the runtime does,
|
|
1908
|
-
* what the runtime does. A curve that overshoots (a fraction below 0 or
|
|
1909
|
-
* which the format allows and `back`-style easings produce) is therefore
|
|
1910
|
-
* this rather than assumed away.
|
|
1919
|
+
* ⭐ **The points the runtime stores, not the cubic.**
|
|
1920
|
+
* `DeformTimeline.getCurvePercent` evaluates a bezier by walking the points the
|
|
1921
|
+
* parser sampled into its curve storage and interpolating *linearly* between
|
|
1922
|
+
* them — so the polyline below is not an approximation of what the runtime does,
|
|
1923
|
+
* it is what the runtime does. A curve that overshoots (a fraction below 0 or
|
|
1924
|
+
* above 1, which the format allows and `back`-style easings produce) is therefore
|
|
1925
|
+
* inside this rather than assumed away. Which reader supplies the points is the
|
|
1926
|
+
* structure's (`SurveyCurve`, issue #1019): spine-core's reads that storage
|
|
1927
|
+
* (`curveStorage`); the model document's computes the same nine from the key's
|
|
1928
|
+
* handles with the core's deform curve (`./deformstructure.ts`), measured equal
|
|
1929
|
+
* point for point on every corpus Bézier.
|
|
1911
1930
|
*/
|
|
1912
|
-
function curveLegs(timeline:
|
|
1931
|
+
function curveLegs(timeline: SurveyDeformTimeline, frame: number): { kind: DeformSpanCurve; legs: CurveLeg[] } {
|
|
1913
1932
|
const t0 = timeline.frames[frame];
|
|
1914
1933
|
const t1 = timeline.frames[frame + 1];
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
//
|
|
1918
|
-
|
|
1919
|
-
if (
|
|
1920
|
-
|
|
1921
|
-
//
|
|
1922
|
-
//
|
|
1923
|
-
|
|
1934
|
+
const curve = timeline.curve(frame);
|
|
1935
|
+
// STEPPED: `getCurvePercent` returns a flat 0 across the whole span, so the
|
|
1936
|
+
// runtime holds the earlier key's geometry and interpolates nothing.
|
|
1937
|
+
if (curve.kind === 'stepped') return { kind: 'stepped', legs: [{ t0, p0: 0, t1, p1: 0 }] };
|
|
1938
|
+
if (curve.kind === 'linear') return { kind: 'linear', legs: [{ t0, p0: 0, t1, p1: 1 }] };
|
|
1939
|
+
// BEZIER: nine sampled points are stored; the runtime ramps into the first
|
|
1940
|
+
// from (t0, 0) and out of the last to (t1, 1), which is the two legs added
|
|
1941
|
+
// either side.
|
|
1942
|
+
const points = curve.points;
|
|
1924
1943
|
const legs: CurveLeg[] = [];
|
|
1925
1944
|
let x = t0;
|
|
1926
1945
|
let y = 0;
|
|
1927
|
-
for (let i =
|
|
1928
|
-
legs.push({ t0: x, p0: y, t1:
|
|
1929
|
-
x =
|
|
1930
|
-
y =
|
|
1946
|
+
for (let i = 0; i < BEZIER_POINTS * 2; i += 2) {
|
|
1947
|
+
legs.push({ t0: x, p0: y, t1: points[i], p1: points[i + 1] });
|
|
1948
|
+
x = points[i];
|
|
1949
|
+
y = points[i + 1];
|
|
1931
1950
|
}
|
|
1932
1951
|
legs.push({ t0: x, p0: y, t1, p1: 1 });
|
|
1933
1952
|
return { kind: 'bezier', legs };
|
|
@@ -1951,6 +1970,16 @@ export const BEZIER_POINTS = 9;
|
|
|
1951
1970
|
* thing this file's opening paragraph forbids — and would then be checking
|
|
1952
1971
|
* rigc's copy of spine-core's maths rather than spine-core's.
|
|
1953
1972
|
*
|
|
1973
|
+
* ⚠️ That alternative is what the model document's reader does since issue
|
|
1974
|
+
* #1019, and the objection does not reach it, because it is not a copy written
|
|
1975
|
+
* here: it is the core's own deform curve (`bezierPolyline` with
|
|
1976
|
+
* `DEFORM_CURVE_END`, `./core/deform.ts`), the one the core POSES with, measured
|
|
1977
|
+
* against the runtime there. On a build the core poses, the pose and the span
|
|
1978
|
+
* scan read one derivation rather than two, and `DM12` (the gallery) and
|
|
1979
|
+
* `tools/survey_hashes.ts structure` (every corpus row) hold its points to this
|
|
1980
|
+
* array's. What spine-core's reader of the survey reads
|
|
1981
|
+
* is still this array.
|
|
1982
|
+
*
|
|
1954
1983
|
* `A05` already gates the emitted curve arrays, and `DW18` is the control that
|
|
1955
1984
|
* the reading here matches what the runtime does with them.
|
|
1956
1985
|
*
|
|
@@ -2094,7 +2123,7 @@ function wrongSignFractions(
|
|
|
2094
2123
|
return a > 0 ? clip(left, right) : [...clip(-Infinity, left), ...clip(right, Infinity)];
|
|
2095
2124
|
}
|
|
2096
2125
|
|
|
2097
|
-
|
|
2126
|
+
/*
|
|
2098
2127
|
* The times at which the slot's **visibility** can change across a span.
|
|
2099
2128
|
*
|
|
2100
2129
|
* ⚠️ This is the half of issue #403 that is about the fade rather than the fold,
|
|
@@ -2109,21 +2138,11 @@ function wrongSignFractions(
|
|
|
2109
2138
|
* window at those times leaves pieces on which "does this draw?" has one answer,
|
|
2110
2139
|
* and the probe inside a piece is representative of the whole piece.
|
|
2111
2140
|
*
|
|
2112
|
-
* Every timeline
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2141
|
+
* Every timeline filed under this slot, or under any other slot the deform
|
|
2142
|
+
* reaches (`timelineSlots`), counts — `SurveyAnimation.slotKeyTimes`, which each
|
|
2143
|
+
* reader answers (`visibilityKeyTimes` below for spine-core's; the model
|
|
2144
|
+
* document's slot and attachment timelines for the core's).
|
|
2116
2145
|
*/
|
|
2117
|
-
function visibilityKeyTimes(timelines: readonly Timeline[], slots: ReadonlySet<number>): number[] {
|
|
2118
|
-
const times: number[] = [];
|
|
2119
|
-
for (const timeline of timelines) {
|
|
2120
|
-
const carrier = timeline as unknown as { slotIndex?: unknown };
|
|
2121
|
-
if (typeof carrier.slotIndex !== 'number' || !slots.has(carrier.slotIndex)) continue;
|
|
2122
|
-
const entries = timeline.getFrameEntries();
|
|
2123
|
-
for (let i = 0; i < timeline.getFrameCount(); i++) times.push(timeline.frames[i * entries]);
|
|
2124
|
-
}
|
|
2125
|
-
return times;
|
|
2126
|
-
}
|
|
2127
2146
|
|
|
2128
2147
|
/**
|
|
2129
2148
|
* Scan the interval between two consecutive deform keys (issue #403).
|
|
@@ -2179,9 +2198,9 @@ function visibilityKeyTimes(timelines: readonly Timeline[], slots: ReadonlySet<n
|
|
|
2179
2198
|
* being refused anyway.
|
|
2180
2199
|
*/
|
|
2181
2200
|
function scanDeformSpan(
|
|
2182
|
-
anim:
|
|
2183
|
-
timeline:
|
|
2184
|
-
attachment:
|
|
2201
|
+
anim: SurveyAnimation,
|
|
2202
|
+
timeline: SurveyDeformTimeline,
|
|
2203
|
+
attachment: SurveyMesh,
|
|
2185
2204
|
triangles: ArrayLike<number>,
|
|
2186
2205
|
named: { animation: string; skin: string; slot: string; attachment: string; placeholder: string },
|
|
2187
2206
|
/** The index of the key the span starts at — `frame + 1` is the one it ends at. */
|
|
@@ -2211,8 +2230,8 @@ function scanDeformSpan(
|
|
|
2211
2230
|
unconfirmed: false,
|
|
2212
2231
|
};
|
|
2213
2232
|
const reach = reachedFractions(legs);
|
|
2214
|
-
const v1 = timeline.vertices
|
|
2215
|
-
const v2 = timeline.vertices
|
|
2233
|
+
const v1 = timeline.vertices(frame);
|
|
2234
|
+
const v2 = timeline.vertices(frame + 1);
|
|
2216
2235
|
if (!v1 || !v2) return span;
|
|
2217
2236
|
|
|
2218
2237
|
const windows: Interval[] = [];
|
|
@@ -2228,7 +2247,7 @@ function scanDeformSpan(
|
|
|
2228
2247
|
// both of the span's own key poses are taken and the union used. `DW17` is
|
|
2229
2248
|
// the control for the claim: a bone rotation across the span moves nothing
|
|
2230
2249
|
// about where an unweighted mesh is found to fold.
|
|
2231
|
-
for (const anchor of attachment.
|
|
2250
|
+
for (const anchor of attachment.weighted ? [from, to] : [from]) {
|
|
2232
2251
|
// The two ends of the straight line every vertex travels, evaluated at THIS
|
|
2233
2252
|
// anchor's bones. `measurePosed` has already emptied the slot's deform array
|
|
2234
2253
|
// to take its plain side, so writing into it is how the two are posed.
|
|
@@ -2281,7 +2300,7 @@ function scanDeformSpan(
|
|
|
2281
2300
|
// straddles a change in what is drawn. Every point of a merged window is
|
|
2282
2301
|
// inside at least one triangle's window, so its middle is a fold and not a gap.
|
|
2283
2302
|
const slots = new Set<number>([timeline.slotIndex, ...attachment.timelineSlots]);
|
|
2284
|
-
const cuts =
|
|
2303
|
+
const cuts = anim.slotKeyTimes(slots);
|
|
2285
2304
|
const pieces: Interval[] = [];
|
|
2286
2305
|
for (const window of mergeIntervals(windows)) {
|
|
2287
2306
|
const inner = [...new Set(cuts.filter((t) => t > window.lo && t < window.hi))].sort((x, y) => x - y);
|
|
@@ -2413,7 +2432,7 @@ function shownAt(posed: Skeleton, slotIndex: number, attachment: MeshAttachment)
|
|
|
2413
2432
|
* green. What it must not do is leave the reader guessing which dress the
|
|
2414
2433
|
* verdict was taken in, so the skin is in the sentence.
|
|
2415
2434
|
*/
|
|
2416
|
-
function drawOfKey(posed: SurveyPose, slotIndex: number, attachment:
|
|
2435
|
+
function drawOfKey(posed: SurveyPose, slotIndex: number, attachment: SurveyMesh): DeformKeyDraw {
|
|
2417
2436
|
const own = posed.shownAt(slotIndex, attachment);
|
|
2418
2437
|
const under = posed.under();
|
|
2419
2438
|
const draw = { shown: own.shown, showsThisMesh: own.showsThisMesh, alpha: own.alpha, under };
|
|
@@ -2482,7 +2501,7 @@ function drawOfKey(posed: SurveyPose, slotIndex: number, attachment: MeshAttachm
|
|
|
2482
2501
|
function placementOf(
|
|
2483
2502
|
data: SkeletonData,
|
|
2484
2503
|
slotIndex: number,
|
|
2485
|
-
attachment:
|
|
2504
|
+
attachment: Attachment,
|
|
2486
2505
|
): { skin: string; holder: Skin | null; placeholder: string } {
|
|
2487
2506
|
for (const skin of [data.defaultSkin, ...data.skins]) {
|
|
2488
2507
|
if (!skin) continue;
|
|
@@ -2503,8 +2522,12 @@ function placementOf(
|
|
|
2503
2522
|
// spine-core over the Spine skeleton (the survey as it always was, and
|
|
2504
2523
|
// `validate.ts`'s A39, which hands it the SkeletonData it round-tripped), and
|
|
2505
2524
|
// rigc's own core over the model document (`./core/hooks.ts`, issue #969),
|
|
2506
|
-
// which a build carries and `explain` hands in.
|
|
2507
|
-
//
|
|
2525
|
+
// which a build carries and `explain` hands in. Since issue #1019 the
|
|
2526
|
+
// structure comes in the same two kinds, paired with the posers: spine-core's
|
|
2527
|
+
// parse read by `runtimeSide` below, and the model document read by
|
|
2528
|
+
// `modelStructure` (`./deformstructure.ts`) — so a build the core poses is
|
|
2529
|
+
// surveyed without touching the runtime at all. The survey's record names which
|
|
2530
|
+
// pair it used (`DeformSurvey.source`), and `tools/survey_hashes.ts` holds the
|
|
2508
2531
|
// two to one survey, byte for byte, on every corpus.
|
|
2509
2532
|
|
|
2510
2533
|
/** Which poser a survey used (`DeformSurvey.source`). */
|
|
@@ -2531,19 +2554,19 @@ export interface ShownReading {
|
|
|
2531
2554
|
export interface SurveyPose {
|
|
2532
2555
|
/** The skin the skeleton wears, off the pose itself (`DeformKeyDraw.under`). */
|
|
2533
2556
|
under(): string | null;
|
|
2534
|
-
shownAt(slotIndex: number, attachment:
|
|
2557
|
+
shownAt(slotIndex: number, attachment: SurveyMesh): ShownReading;
|
|
2535
2558
|
/** The mesh's world vertices with the slot's deform as posed. */
|
|
2536
|
-
deformed(slotIndex: number, attachment:
|
|
2559
|
+
deformed(slotIndex: number, attachment: SurveyMesh): Float32Array;
|
|
2537
2560
|
/** The same bones with the slot's deform cleared. */
|
|
2538
|
-
plain(slotIndex: number, attachment:
|
|
2561
|
+
plain(slotIndex: number, attachment: SurveyMesh): Float32Array;
|
|
2539
2562
|
/** The same bones with `deform` written into the slot. */
|
|
2540
|
-
withDeform(slotIndex: number, attachment:
|
|
2563
|
+
withDeform(slotIndex: number, attachment: SurveyMesh, deform: ArrayLike<number>): Float32Array;
|
|
2541
2564
|
/**
|
|
2542
2565
|
* The doubles those three are stored from — the slot's deform as posed,
|
|
2543
2566
|
* cleared, or replaced — leaving the pose as it found it. The survey reads
|
|
2544
2567
|
* the float32 rows; this is what measures the rows' source (`tools/survey_hashes.ts hooks`).
|
|
2545
2568
|
*/
|
|
2546
|
-
rows(slotIndex: number, attachment:
|
|
2569
|
+
rows(slotIndex: number, attachment: SurveyMesh, deform: 'posed' | 'cleared' | ArrayLike<number>): number[];
|
|
2547
2570
|
}
|
|
2548
2571
|
|
|
2549
2572
|
/** One slider's dial, posed again and again (`planDial`'s probe, `poseDial`'s solve). */
|
|
@@ -2560,23 +2583,186 @@ export interface DialSession {
|
|
|
2560
2583
|
|
|
2561
2584
|
/** The two ways the survey poses: a jump on a track, and a slider's dial. */
|
|
2562
2585
|
export interface SurveyPoser {
|
|
2563
|
-
track(skin:
|
|
2586
|
+
track(skin: SurveySkin | null, animation: string, time: number): SurveyPose;
|
|
2564
2587
|
/** `null` when the skeleton carries no such slider. */
|
|
2565
|
-
dial(skin:
|
|
2588
|
+
dial(skin: SurveySkin | null, slider: SurveySlider): DialSession | null;
|
|
2566
2589
|
}
|
|
2567
2590
|
|
|
2591
|
+
// --- #1019 the runtime's reading of the structure: begin ---
|
|
2592
|
+
//
|
|
2593
|
+
// `src/deformstructure.ts` states what the survey reads of a skeleton's
|
|
2594
|
+
// structure and gives the model document's reading of it. This is the other
|
|
2595
|
+
// one — the survey's reads as they always were, off spine-core's parsed
|
|
2596
|
+
// `SkeletonData`, moved behind the interface unchanged — and the spine-core
|
|
2597
|
+
// poser over the same objects. The two are made together because the poser
|
|
2598
|
+
// needs the parsed objects the handles stand for: a handle the model
|
|
2599
|
+
// document's reader made carries none, and is refused here by name.
|
|
2600
|
+
|
|
2601
|
+
/** The parsed objects behind the runtime reader's handles — what spine-core's poser is handed. */
|
|
2602
|
+
interface RuntimeLinks {
|
|
2603
|
+
skin(handle: SurveySkin | null): Skin | null;
|
|
2604
|
+
mesh(handle: SurveyMesh): MeshAttachment;
|
|
2605
|
+
slider(handle: SurveySlider): SliderData;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
/**
|
|
2609
|
+
* The slot timelines' key times on `slots` — every timeline carrying a
|
|
2610
|
+
* `slotIndex`, duck-typed (see `scanDeformSpan`'s visibility split).
|
|
2611
|
+
*
|
|
2612
|
+
* ⭐ Duck-typed rather than matched against a list of classes, because a list
|
|
2613
|
+
* is a thing that goes stale when the format grows a timeline and the failure
|
|
2614
|
+
* would be silent.
|
|
2615
|
+
*/
|
|
2616
|
+
function visibilityKeyTimes(timelines: readonly Timeline[], slots: ReadonlySet<number>): number[] {
|
|
2617
|
+
const times: number[] = [];
|
|
2618
|
+
for (const timeline of timelines) {
|
|
2619
|
+
const carrier = timeline as unknown as { slotIndex?: unknown };
|
|
2620
|
+
if (typeof carrier.slotIndex !== 'number' || !slots.has(carrier.slotIndex)) continue;
|
|
2621
|
+
const entries = timeline.getFrameEntries();
|
|
2622
|
+
for (let i = 0; i < timeline.getFrameCount(); i++) times.push(timeline.frames[i * entries]);
|
|
2623
|
+
}
|
|
2624
|
+
return times;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
/**
|
|
2628
|
+
* spine-core's parsed skeleton read as the survey's structure, and posed by
|
|
2629
|
+
* spine-core — the survey as it always was (the section's note).
|
|
2630
|
+
*/
|
|
2631
|
+
function runtimeSide(data: SkeletonData): { structure: SurveyStructure; poser: SurveyPoser; links: RuntimeLinks } {
|
|
2632
|
+
const skins = new Map<Skin, SurveySkin>();
|
|
2633
|
+
const skinBack = new Map<SurveySkin, Skin>();
|
|
2634
|
+
const skinOf = (skin: Skin | null): SurveySkin | null => {
|
|
2635
|
+
if (skin === null) return null;
|
|
2636
|
+
let handle = skins.get(skin);
|
|
2637
|
+
if (handle === undefined) {
|
|
2638
|
+
handle = { name: skin.name };
|
|
2639
|
+
skins.set(skin, handle);
|
|
2640
|
+
skinBack.set(handle, skin);
|
|
2641
|
+
}
|
|
2642
|
+
return handle;
|
|
2643
|
+
};
|
|
2644
|
+
const meshes = new Map<MeshAttachment, SurveyMesh>();
|
|
2645
|
+
const meshBack = new Map<SurveyMesh, MeshAttachment>();
|
|
2646
|
+
const meshOf = (attachment: MeshAttachment, slotIndex: number): SurveyMesh => {
|
|
2647
|
+
const already = meshes.get(attachment);
|
|
2648
|
+
if (already !== undefined) return already;
|
|
2649
|
+
const placed = placementOf(data, slotIndex, attachment);
|
|
2650
|
+
const handle: SurveyMesh = {
|
|
2651
|
+
name: attachment.name,
|
|
2652
|
+
triangles: attachment.triangles,
|
|
2653
|
+
worldVerticesLength: attachment.worldVerticesLength,
|
|
2654
|
+
weighted: attachment.bones !== null,
|
|
2655
|
+
timelineSlots: attachment.timelineSlots,
|
|
2656
|
+
record: placed.holder === null ? null : { skin: placed.skin, slot: data.slots[slotIndex]?.name ?? `#${slotIndex}`, placeholder: placed.placeholder },
|
|
2657
|
+
};
|
|
2658
|
+
meshes.set(attachment, handle);
|
|
2659
|
+
meshBack.set(handle, attachment);
|
|
2660
|
+
return handle;
|
|
2661
|
+
};
|
|
2662
|
+
const deformOf = (timeline: DeformTimeline): SurveyDeformTimeline => {
|
|
2663
|
+
const attachment = timeline.attachment;
|
|
2664
|
+
const mesh = attachment instanceof MeshAttachment ? meshOf(attachment, timeline.slotIndex) : null;
|
|
2665
|
+
return {
|
|
2666
|
+
slotIndex: timeline.slotIndex,
|
|
2667
|
+
mesh,
|
|
2668
|
+
frames: timeline.frames,
|
|
2669
|
+
vertices: (frame) => timeline.vertices[frame],
|
|
2670
|
+
curve: (frame): SurveyCurve => {
|
|
2671
|
+
const curves = curveStorage(timeline);
|
|
2672
|
+
const code = curves[frame];
|
|
2673
|
+
// 1 is STEPPED and 0 LINEAR; 2 + i is BEZIER, its sampled points starting at `i`.
|
|
2674
|
+
if (code === 1) return { kind: 'stepped' };
|
|
2675
|
+
if (code === 0) return { kind: 'linear' };
|
|
2676
|
+
const points: number[] = [];
|
|
2677
|
+
for (let i = code - 2, n = code - 2 + BEZIER_POINTS * 2; i < n; i++) points.push(curves[i]);
|
|
2678
|
+
return { kind: 'bezier', points };
|
|
2679
|
+
},
|
|
2680
|
+
placement: () => {
|
|
2681
|
+
const placed = placementOf(data, timeline.slotIndex, attachment);
|
|
2682
|
+
return { skin: placed.skin, holder: skinOf(placed.holder), placeholder: placed.placeholder };
|
|
2683
|
+
},
|
|
2684
|
+
};
|
|
2685
|
+
};
|
|
2686
|
+
const animations: SurveyAnimation[] = data.animations.map((anim) => ({
|
|
2687
|
+
name: anim.name,
|
|
2688
|
+
deforms: anim.timelines.flatMap((timeline) => (timeline instanceof DeformTimeline ? [deformOf(timeline)] : [])),
|
|
2689
|
+
slotKeyTimes: (slots) => visibilityKeyTimes(anim.timelines, slots),
|
|
2690
|
+
}));
|
|
2691
|
+
const sliderBack = new Map<SurveySlider, SliderData>();
|
|
2692
|
+
const sliders: SurveySlider[] = [];
|
|
2693
|
+
for (const constraint of data.constraints) {
|
|
2694
|
+
if (!(constraint instanceof SliderData)) continue;
|
|
2695
|
+
// The animation and the property are read when they are used, as they always were: only a slider with an
|
|
2696
|
+
// animation is planned, and only one with a bone has a property — a bone-less one's is not set at all.
|
|
2697
|
+
const handle: SurveySlider = {
|
|
2698
|
+
name: constraint.name,
|
|
2699
|
+
mix: constraint.setupPose.mix,
|
|
2700
|
+
animation: constraint.animation?.name ?? '',
|
|
2701
|
+
get duration() {
|
|
2702
|
+
return constraint.animation.duration;
|
|
2703
|
+
},
|
|
2704
|
+
bone: constraint.bone?.name ?? null,
|
|
2705
|
+
local: constraint.local,
|
|
2706
|
+
get stated() {
|
|
2707
|
+
return readerField(constraint.property);
|
|
2708
|
+
},
|
|
2709
|
+
get reader() {
|
|
2710
|
+
return constraint.property.constructor.name;
|
|
2711
|
+
},
|
|
2712
|
+
get from() {
|
|
2713
|
+
return constraint.property.offset;
|
|
2714
|
+
},
|
|
2715
|
+
to: constraint.offset,
|
|
2716
|
+
scale: constraint.scale,
|
|
2717
|
+
loop: constraint.loop,
|
|
2718
|
+
};
|
|
2719
|
+
sliders.push(handle);
|
|
2720
|
+
sliderBack.set(handle, constraint);
|
|
2721
|
+
}
|
|
2722
|
+
const structure: SurveyStructure = {
|
|
2723
|
+
skins: [data.defaultSkin, ...data.skins].flatMap((k, i, all) => (k !== null && all.indexOf(k) === i ? [skinOf(k) as SurveySkin] : [])),
|
|
2724
|
+
animations,
|
|
2725
|
+
sliders,
|
|
2726
|
+
slotName: (index) => data.slots[index]?.name ?? `#${index}`,
|
|
2727
|
+
skinsNamed: (name) => data.skins.filter((k) => k.name === name).length,
|
|
2728
|
+
};
|
|
2729
|
+
const foreign = (what: string): Error => new Error(`internal: spine-core's poser was handed a ${what} another reader made — it poses the parsed skeleton's own objects`);
|
|
2730
|
+
const links: RuntimeLinks = {
|
|
2731
|
+
skin: (handle) => {
|
|
2732
|
+
if (handle === null) return null;
|
|
2733
|
+
const skin = skinBack.get(handle);
|
|
2734
|
+
if (skin === undefined) throw foreign(`skin "${handle.name}"`);
|
|
2735
|
+
return skin;
|
|
2736
|
+
},
|
|
2737
|
+
mesh: (handle) => {
|
|
2738
|
+
const mesh = meshBack.get(handle);
|
|
2739
|
+
if (mesh === undefined) throw foreign(`mesh "${handle.name}"`);
|
|
2740
|
+
return mesh;
|
|
2741
|
+
},
|
|
2742
|
+
slider: (handle) => {
|
|
2743
|
+
const slider = sliderBack.get(handle);
|
|
2744
|
+
if (slider === undefined) throw foreign(`slider "${handle.name}"`);
|
|
2745
|
+
return slider;
|
|
2746
|
+
},
|
|
2747
|
+
};
|
|
2748
|
+
return { structure, poser: spinePoser(data, links), links };
|
|
2749
|
+
}
|
|
2750
|
+
// --- #1019 the runtime's reading of the structure: end ---
|
|
2751
|
+
|
|
2568
2752
|
/** A live spine-core skeleton, read the way the survey always read it. */
|
|
2569
|
-
function spinePose(skeleton: Skeleton): SurveyPose {
|
|
2753
|
+
function spinePose(skeleton: Skeleton, links: RuntimeLinks): SurveyPose {
|
|
2570
2754
|
return {
|
|
2571
2755
|
under: () => skeleton.skin?.name ?? null,
|
|
2572
|
-
shownAt: (slotIndex,
|
|
2573
|
-
deformed: (slotIndex,
|
|
2756
|
+
shownAt: (slotIndex, handle) => shownAt(skeleton, slotIndex, links.mesh(handle)),
|
|
2757
|
+
deformed: (slotIndex, handle) => {
|
|
2758
|
+
const attachment = links.mesh(handle);
|
|
2574
2759
|
const count = attachment.worldVerticesLength;
|
|
2575
2760
|
const world = new Float32Array(count);
|
|
2576
2761
|
attachment.computeWorldVertices(skeleton, skeleton.slots[slotIndex], 0, count, world, 0, 2);
|
|
2577
2762
|
return world;
|
|
2578
2763
|
},
|
|
2579
|
-
plain: (slotIndex,
|
|
2764
|
+
plain: (slotIndex, handle) => {
|
|
2765
|
+
const attachment = links.mesh(handle);
|
|
2580
2766
|
const count = attachment.worldVerticesLength;
|
|
2581
2767
|
const slot = skeleton.slots[slotIndex];
|
|
2582
2768
|
slot.appliedPose.deform.length = 0;
|
|
@@ -2584,8 +2770,9 @@ function spinePose(skeleton: Skeleton): SurveyPose {
|
|
|
2584
2770
|
attachment.computeWorldVertices(skeleton, slot, 0, count, world, 0, 2);
|
|
2585
2771
|
return world;
|
|
2586
2772
|
},
|
|
2587
|
-
withDeform: (slotIndex,
|
|
2588
|
-
rows: (slotIndex,
|
|
2773
|
+
withDeform: (slotIndex, handle, deform) => worldWithDeform(skeleton, slotIndex, links.mesh(handle), deform),
|
|
2774
|
+
rows: (slotIndex, handle, deform) => {
|
|
2775
|
+
const attachment = links.mesh(handle);
|
|
2589
2776
|
const slot = skeleton.slots[slotIndex];
|
|
2590
2777
|
const array = slot.appliedPose.deform;
|
|
2591
2778
|
const kept = [...array];
|
|
@@ -2604,11 +2791,12 @@ function spinePose(skeleton: Skeleton): SurveyPose {
|
|
|
2604
2791
|
}
|
|
2605
2792
|
|
|
2606
2793
|
/** The survey's poses through spine-core — the recipes the survey has always taken (`poseAt`; the dial below). */
|
|
2607
|
-
function spinePoser(data: SkeletonData): SurveyPoser {
|
|
2794
|
+
function spinePoser(data: SkeletonData, links: RuntimeLinks): SurveyPoser {
|
|
2608
2795
|
return {
|
|
2609
|
-
track: (skin, animation, time) => spinePose(poseAt(data, skin, animation, time)),
|
|
2610
|
-
dial: (skin,
|
|
2611
|
-
const
|
|
2796
|
+
track: (skin, animation, time) => spinePose(poseAt(data, links.skin(skin), animation, time), links),
|
|
2797
|
+
dial: (skin, handle) => {
|
|
2798
|
+
const slider = links.slider(handle);
|
|
2799
|
+
const skeleton = skeletonUnderSkin(data, links.skin(skin));
|
|
2612
2800
|
const instance = sliderOn(skeleton, slider);
|
|
2613
2801
|
if (instance === null) return null;
|
|
2614
2802
|
const bone = instance.bone;
|
|
@@ -2630,41 +2818,25 @@ function spinePoser(data: SkeletonData): SurveyPoser {
|
|
|
2630
2818
|
if (slider.local) bone.appliedPose.validateLocalTransform(skeleton);
|
|
2631
2819
|
return { read: slider.property.value(skeleton, bone.appliedPose, slider.local, DIAL_ZERO_OFFSETS), applied: instance.appliedPose.time };
|
|
2632
2820
|
},
|
|
2633
|
-
pose: () => spinePose(skeleton),
|
|
2821
|
+
pose: () => spinePose(skeleton, links),
|
|
2634
2822
|
};
|
|
2635
2823
|
},
|
|
2636
2824
|
};
|
|
2637
2825
|
}
|
|
2638
2826
|
|
|
2639
|
-
/**
|
|
2640
|
-
function
|
|
2641
|
-
const out = new Map<Attachment, { skin: string; slot: string; placeholder: string }>();
|
|
2642
|
-
for (const skin of [data.defaultSkin, ...data.skins]) {
|
|
2643
|
-
if (!skin) continue;
|
|
2644
|
-
for (let slotIndex = 0; slotIndex < data.slots.length; slotIndex++) {
|
|
2645
|
-
const entries: Array<{ placeholder: string; attachment: Attachment }> = [];
|
|
2646
|
-
skin.getAttachmentsForSlot(slotIndex, entries as Parameters<typeof skin.getAttachmentsForSlot>[1]);
|
|
2647
|
-
for (const entry of entries) {
|
|
2648
|
-
if (!out.has(entry.attachment)) out.set(entry.attachment, { skin: skin.name, slot: data.slots[slotIndex].name, placeholder: entry.placeholder });
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
return out;
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
/** A pose of the core's, read through the Spine skeleton's names. */
|
|
2656
|
-
function corePose(pose: CoreSurveyPose, data: SkeletonData, records: ReadonlyMap<Attachment, { skin: string; slot: string; placeholder: string }>): SurveyPose {
|
|
2827
|
+
/** A pose of the core's, read through the structure's slot names and each mesh's model record. */
|
|
2828
|
+
function corePose(pose: CoreSurveyPose, structure: SurveyStructure): SurveyPose {
|
|
2657
2829
|
const slotName = (slotIndex: number): string => {
|
|
2658
|
-
const
|
|
2659
|
-
if (
|
|
2660
|
-
return
|
|
2830
|
+
const name = structure.slotName(slotIndex);
|
|
2831
|
+
if (name === `#${slotIndex}`) throw new CoreInputError(`slot #${slotIndex} is not a slot of the skeleton`);
|
|
2832
|
+
return name;
|
|
2661
2833
|
};
|
|
2662
|
-
const recordOf = (attachment:
|
|
2663
|
-
const r =
|
|
2664
|
-
if (r ===
|
|
2834
|
+
const recordOf = (attachment: SurveyMesh): { skin: string; slot: string; placeholder: string } => {
|
|
2835
|
+
const r = attachment.record;
|
|
2836
|
+
if (r === null) throw new CoreInputError(`mesh "${attachment.name}" is in no skin of the skeleton, so it names no model record`);
|
|
2665
2837
|
return r;
|
|
2666
2838
|
};
|
|
2667
|
-
const world = (slotIndex: number, attachment:
|
|
2839
|
+
const world = (slotIndex: number, attachment: SurveyMesh, deform: 'posed' | 'cleared' | ArrayLike<number>): Float32Array =>
|
|
2668
2840
|
float32Rows(meshWorld(pose, slotName(slotIndex), recordOf(attachment), deform));
|
|
2669
2841
|
return {
|
|
2670
2842
|
under: () => pose.doc.skin,
|
|
@@ -2687,13 +2859,14 @@ function corePose(pose: CoreSurveyPose, data: SkeletonData, records: ReadonlyMap
|
|
|
2687
2859
|
* (`underSkin`) — so a skeleton declaring two skins of one name, and a pose
|
|
2688
2860
|
* wearing none, are refused by name: neither reaches the survey through
|
|
2689
2861
|
* `SkeletonJson` (`placementOf`'s note), and neither has a view to pose.
|
|
2862
|
+
* `structure` is whichever reader's: the model document's on a build the core
|
|
2863
|
+
* poses, the runtime's where the hooks are held to spine-core's.
|
|
2690
2864
|
*/
|
|
2691
|
-
function corePoser(
|
|
2692
|
-
const records = recordsOf(data);
|
|
2865
|
+
function corePoser(structure: SurveyStructure, doc: CompiledDocument): SurveyPoser {
|
|
2693
2866
|
const views = new Map<string, CompiledDocument>();
|
|
2694
|
-
const viewOf = (skin:
|
|
2867
|
+
const viewOf = (skin: SurveySkin | null): CompiledDocument => {
|
|
2695
2868
|
if (skin === null) throw new CoreInputError('the survey asked for a pose wearing no skin, and the core poses one skin\'s view');
|
|
2696
|
-
if (
|
|
2869
|
+
if (structure.skinsNamed(skin.name) > 1) throw new CoreInputError(`the skeleton declares two skins named "${skin.name}", and the model names a skin by its name`);
|
|
2697
2870
|
const cached = views.get(skin.name);
|
|
2698
2871
|
if (cached !== undefined) return cached;
|
|
2699
2872
|
const view = underSkin(doc, skin.name);
|
|
@@ -2701,7 +2874,7 @@ function corePoser(data: SkeletonData, doc: CompiledDocument): SurveyPoser {
|
|
|
2701
2874
|
return view;
|
|
2702
2875
|
};
|
|
2703
2876
|
return {
|
|
2704
|
-
track: (skin, animation, time) => corePose(poseJump(viewOf(skin), animation, time),
|
|
2877
|
+
track: (skin, animation, time) => corePose(poseJump(viewOf(skin), animation, time), structure),
|
|
2705
2878
|
dial: (skin, slider) => {
|
|
2706
2879
|
const view = viewOf(skin);
|
|
2707
2880
|
const record = sliderRecordOf(view, slider.name);
|
|
@@ -2722,7 +2895,7 @@ function corePoser(data: SkeletonData, doc: CompiledDocument): SurveyPoser {
|
|
|
2722
2895
|
},
|
|
2723
2896
|
pose: () => {
|
|
2724
2897
|
if (last === null) throw new CoreInputError(`slider "${slider.name}"'s dial was asked for its pose before it was posed`);
|
|
2725
|
-
return corePose(last,
|
|
2898
|
+
return corePose(last, structure);
|
|
2726
2899
|
},
|
|
2727
2900
|
};
|
|
2728
2901
|
},
|
|
@@ -2730,13 +2903,19 @@ function corePoser(data: SkeletonData, doc: CompiledDocument): SurveyPoser {
|
|
|
2730
2903
|
}
|
|
2731
2904
|
|
|
2732
2905
|
/**
|
|
2733
|
-
* Both posers over one build
|
|
2734
|
-
*
|
|
2735
|
-
*
|
|
2906
|
+
* Both readers and both posers over one build, for measurement:
|
|
2907
|
+
* `tools/survey_hashes.ts` holds every hook of the core's poser to
|
|
2908
|
+
* spine-core's at tolerance 0 over the runtime's structure (`hooks`), and the
|
|
2909
|
+
* model document's structure to the runtime's (`structure`); the selftest's
|
|
2910
|
+
* `DM` controls hold the same. `spine` and `core` both pose `structure`'s
|
|
2911
|
+
* handles; `model` is the model document's reading, which only `core` and the
|
|
2912
|
+
* model's own poser can pose.
|
|
2736
2913
|
*/
|
|
2737
|
-
export function deformPosers(input: DeformSurveyInput & { modelText: string }): { data: SkeletonData; spine: SurveyPoser; core: SurveyPoser } {
|
|
2914
|
+
export function deformPosers(input: DeformSurveyInput & { modelText: string }): { data: SkeletonData; structure: SurveyStructure; model: SurveyStructure; spine: SurveyPoser; core: SurveyPoser } {
|
|
2738
2915
|
const data = skeletonDataFromText(input.skeletonText, input.atlasText);
|
|
2739
|
-
|
|
2916
|
+
const side = runtimeSide(data);
|
|
2917
|
+
const doc = readModel(input.modelText);
|
|
2918
|
+
return { data, structure: side.structure, model: modelStructure(doc), spine: side.poser, core: corePoser(side.structure, doc) };
|
|
2740
2919
|
}
|
|
2741
2920
|
|
|
2742
2921
|
/** A build's three texts, as `explain` and `tools/survey_hashes.ts` hold them. */
|
|
@@ -2745,25 +2924,69 @@ export interface DeformSurveyInput {
|
|
|
2745
2924
|
atlasText: string;
|
|
2746
2925
|
/** The model document (`skeleton.model.json`), or `null` when the input carries none (a Spine export). */
|
|
2747
2926
|
modelText: string | null;
|
|
2927
|
+
/** What a refusal names the survey's input as; `the deform survey` unstated. */
|
|
2928
|
+
label?: string;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
/**
|
|
2932
|
+
* Touch the runtime once, before the skeleton is parsed through it, and refuse
|
|
2933
|
+
* by name when it cannot be used (issue #1019) — the sentence `render` and
|
|
2934
|
+
* `check` give a run that needs the runtime and cannot use it
|
|
2935
|
+
* (`SpineRuntimeError`, issue #1014). A survey whose input the core poses
|
|
2936
|
+
* never comes here.
|
|
2937
|
+
*/
|
|
2938
|
+
function requireSpineRuntime(label: string, why: string): void {
|
|
2939
|
+
try {
|
|
2940
|
+
// A property read on the class the parse starts from: no runtime code runs, and a runtime that cannot be used throws here.
|
|
2941
|
+
void TextureAtlas.prototype;
|
|
2942
|
+
} catch (err) {
|
|
2943
|
+
throw new SpineRuntimeError(
|
|
2944
|
+
`${label} is posed through spine-core (${why}), and the runtime could not be used: ${(err as Error).message}. ` +
|
|
2945
|
+
'spine-core is what reads and poses a Spine export, --poser spine and a fallback the survey names; ' +
|
|
2946
|
+
'a rigc build the core poses reads nothing through it',
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
/**
|
|
2952
|
+
* The survey off a model document alone: its structure read by `read` —
|
|
2953
|
+
* `modelStructure` unless a control plants a misreading (`DM13`) — and posed
|
|
2954
|
+
* through the core, as `surveyOfBuild`'s model path takes it.
|
|
2955
|
+
*/
|
|
2956
|
+
export function surveyOfModel(doc: CompiledDocument, exempt: ReadonlySet<string>, read: (doc: CompiledDocument) => SurveyStructure = modelStructure): DeformSurvey {
|
|
2957
|
+
const structure = read(doc);
|
|
2958
|
+
return { ...surveyWith(structure, exempt, corePoser(structure, doc)), source: { used: 'model', why: null } };
|
|
2748
2959
|
}
|
|
2749
2960
|
|
|
2750
2961
|
/**
|
|
2751
|
-
* The survey of a build, through the poser asked for (
|
|
2752
|
-
*
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
2755
|
-
*
|
|
2962
|
+
* The survey of a build, through the reader and poser asked for (issues #969,
|
|
2963
|
+
* #1019): `model` — the model document's structure posed by the core,
|
|
2964
|
+
* refused when there is none or when the Spine file beside it is not the one
|
|
2965
|
+
* it records; `spine-core` — the Spine skeleton read and posed by the runtime;
|
|
2966
|
+
* `auto` — the model document when the input carries one and the core poses
|
|
2967
|
+
* it, spine-core otherwise, the reason named in `source.why`. On the model
|
|
2968
|
+
* path spine-core is not touched: the Spine text is hashed, never parsed.
|
|
2756
2969
|
*/
|
|
2757
2970
|
export function surveyOfBuild(input: DeformSurveyInput, exempt: ReadonlySet<string>, asked: 'auto' | DeformSurveySource): DeformSurvey {
|
|
2758
|
-
const
|
|
2759
|
-
const throughSpine = (why: string | null): DeformSurvey =>
|
|
2971
|
+
const label = input.label ?? 'the deform survey';
|
|
2972
|
+
const throughSpine = (why: string | null): DeformSurvey => {
|
|
2973
|
+
requireSpineRuntime(label, why ?? '--poser spine');
|
|
2974
|
+
const side = runtimeSide(skeletonDataFromText(input.skeletonText, input.atlasText));
|
|
2975
|
+
return { ...surveyWith(side.structure, exempt, side.poser), source: { used: 'spine-core', why } };
|
|
2976
|
+
};
|
|
2760
2977
|
if (asked === 'spine-core') return throughSpine(null);
|
|
2761
2978
|
if (input.modelText === null) {
|
|
2762
2979
|
if (asked === 'model') throw new CoreInputError('the survey was asked to pose the model document, and the input carries none');
|
|
2763
2980
|
return throughSpine('the input carries no model document (skeleton.model.json), so the survey posed the Spine skeleton through spine-core');
|
|
2764
2981
|
}
|
|
2765
2982
|
try {
|
|
2766
|
-
|
|
2983
|
+
const doc = readModel(input.modelText);
|
|
2984
|
+
// The document reads the rig it was built with; the Spine file beside it must be that build's (issue #968's rule).
|
|
2985
|
+
const found = spineFileSha256(input.skeletonText);
|
|
2986
|
+
if (found !== doc.spine.sha256) {
|
|
2987
|
+
throw new CoreInputError(`the skeleton is not the one the model document was written beside: its sha256 is ${found}, the document records ${doc.spine.sha256 || 'none'}`);
|
|
2988
|
+
}
|
|
2989
|
+
return surveyOfModel(doc, exempt);
|
|
2767
2990
|
} catch (err) {
|
|
2768
2991
|
if (!(err instanceof CoreInputError) || asked === 'model') throw err;
|
|
2769
2992
|
return throughSpine(`the core refused to pose the model document — ${err.message}`);
|