spine-rigc 1.6.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.ts +166 -60
- package/docs/AUTHORING.md +25 -16
- package/package.json +1 -1
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/skin_entries.ts +35 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/kinds.ts +102 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/index.ts +130 -0
- package/src/assertions/model/parse.ts +136 -0
- package/src/assertions/model/skin_entries.ts +58 -0
- package/src/assertions/model/slot_colour.ts +80 -0
- package/src/assertions/reasons.ts +127 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +26 -16
- package/src/check.ts +70 -47
- package/src/compile.ts +118 -93
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +102 -15
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +80 -24
- package/src/deformmeasure.ts +383 -157
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +114 -13
- package/src/render.ts +520 -59
- package/src/render_core.ts +178 -21
- package/src/transform.ts +113 -128
- package/src/types.ts +6 -5
- package/src/validate.ts +200 -490
package/src/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
|
|
|
@@ -1610,14 +1625,17 @@ interface PoseOfFrame {
|
|
|
1610
1625
|
/**
|
|
1611
1626
|
* What `Slider.update` stores in `SliderPose.time` for a wanted animation time.
|
|
1612
1627
|
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1628
|
+
* On a slider driven by a bone: the time wrapped into the animation under
|
|
1629
|
+
* `loop`, else held at 0 or above. Not assumed — `poseDial`'s step 3 reads
|
|
1630
|
+
* `SliderPose.time` off the skeleton each time it poses a dial and compares
|
|
1631
|
+
* it with this, and a disagreement is reported, never absorbed. It is stated at all because that
|
|
1632
|
+
* verification compares against what the runtime STORED and not against what
|
|
1633
|
+
* was asked for. ⚠️ `loop` on a zero-length animation gives NaN here exactly
|
|
1616
1634
|
* as it does there, which is `A37_SLIDER_CONSTRAINT_EFFECTIVE`'s refusal.
|
|
1617
1635
|
*/
|
|
1618
|
-
function sliderTimeFor(slider:
|
|
1636
|
+
function sliderTimeFor(slider: SurveySlider, time: number): number {
|
|
1619
1637
|
if (slider.bone === null) return time;
|
|
1620
|
-
if (slider.loop) return slider.
|
|
1638
|
+
if (slider.loop) return slider.duration + (time % slider.duration);
|
|
1621
1639
|
return Math.max(0, time);
|
|
1622
1640
|
}
|
|
1623
1641
|
|
|
@@ -1643,12 +1661,12 @@ function sliderTimeFor(slider: SliderData, time: number): number {
|
|
|
1643
1661
|
* against `sliderTimeFor` — spine-core's answer, not this function's. A time
|
|
1644
1662
|
* no dial value selects is reported and never guessed at.
|
|
1645
1663
|
*/
|
|
1646
|
-
function poseDial(poser: SurveyPoser, skin:
|
|
1664
|
+
function poseDial(poser: SurveyPoser, skin: SurveySkin | null, plan: DialPlan, time: number): PoseOfFrame {
|
|
1647
1665
|
const slider = plan.slider;
|
|
1648
1666
|
const wanted = sliderTimeFor(slider, time);
|
|
1649
|
-
const value = plan.field === null ? time : slider.
|
|
1667
|
+
const value = plan.field === null ? time : slider.from + (time - slider.to) / slider.scale;
|
|
1650
1668
|
const session = poser.dial(skin, slider);
|
|
1651
|
-
if (session === null) return { posed: poser.track(skin, slider.animation
|
|
1669
|
+
if (session === null) return { posed: poser.track(skin, slider.animation, time), dial: null };
|
|
1652
1670
|
/** Pose with the driving field at `candidate`, and read both sides back. */
|
|
1653
1671
|
const field = session.hasBone ? plan.field : null;
|
|
1654
1672
|
const at = (candidate: number): { read: number; applied: number } => session.at(field, candidate);
|
|
@@ -1759,7 +1777,7 @@ function measurePosed(
|
|
|
1759
1777
|
posed: SurveyPose,
|
|
1760
1778
|
time: number,
|
|
1761
1779
|
slotIndex: number,
|
|
1762
|
-
attachment:
|
|
1780
|
+
attachment: SurveyMesh,
|
|
1763
1781
|
triangles: ArrayLike<number>,
|
|
1764
1782
|
reach: DeformReach,
|
|
1765
1783
|
dial: DeformDial | null,
|
|
@@ -1898,33 +1916,37 @@ interface CurveLeg {
|
|
|
1898
1916
|
* The runtime's own interpolation fraction over one span, as a polyline in
|
|
1899
1917
|
* `(time, fraction)`.
|
|
1900
1918
|
*
|
|
1901
|
-
* ⭐ **
|
|
1902
|
-
* `DeformTimeline.getCurvePercent` evaluates a bezier by walking the
|
|
1903
|
-
*
|
|
1904
|
-
* so the polyline below is not an approximation of what the runtime does,
|
|
1905
|
-
* what the runtime does. A curve that overshoots (a fraction below 0 or
|
|
1906
|
-
* which the format allows and `back`-style easings produce) is therefore
|
|
1907
|
-
* 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.
|
|
1908
1930
|
*/
|
|
1909
|
-
function curveLegs(timeline:
|
|
1931
|
+
function curveLegs(timeline: SurveyDeformTimeline, frame: number): { kind: DeformSpanCurve; legs: CurveLeg[] } {
|
|
1910
1932
|
const t0 = timeline.frames[frame];
|
|
1911
1933
|
const t1 = timeline.frames[frame + 1];
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
//
|
|
1915
|
-
|
|
1916
|
-
if (
|
|
1917
|
-
|
|
1918
|
-
//
|
|
1919
|
-
//
|
|
1920
|
-
|
|
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;
|
|
1921
1943
|
const legs: CurveLeg[] = [];
|
|
1922
1944
|
let x = t0;
|
|
1923
1945
|
let y = 0;
|
|
1924
|
-
for (let i =
|
|
1925
|
-
legs.push({ t0: x, p0: y, t1:
|
|
1926
|
-
x =
|
|
1927
|
-
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];
|
|
1928
1950
|
}
|
|
1929
1951
|
legs.push({ t0: x, p0: y, t1, p1: 1 });
|
|
1930
1952
|
return { kind: 'bezier', legs };
|
|
@@ -1948,6 +1970,16 @@ export const BEZIER_POINTS = 9;
|
|
|
1948
1970
|
* thing this file's opening paragraph forbids — and would then be checking
|
|
1949
1971
|
* rigc's copy of spine-core's maths rather than spine-core's.
|
|
1950
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
|
+
*
|
|
1951
1983
|
* `A05` already gates the emitted curve arrays, and `DW18` is the control that
|
|
1952
1984
|
* the reading here matches what the runtime does with them.
|
|
1953
1985
|
*
|
|
@@ -2091,7 +2123,7 @@ function wrongSignFractions(
|
|
|
2091
2123
|
return a > 0 ? clip(left, right) : [...clip(-Infinity, left), ...clip(right, Infinity)];
|
|
2092
2124
|
}
|
|
2093
2125
|
|
|
2094
|
-
|
|
2126
|
+
/*
|
|
2095
2127
|
* The times at which the slot's **visibility** can change across a span.
|
|
2096
2128
|
*
|
|
2097
2129
|
* ⚠️ This is the half of issue #403 that is about the fade rather than the fold,
|
|
@@ -2106,21 +2138,11 @@ function wrongSignFractions(
|
|
|
2106
2138
|
* window at those times leaves pieces on which "does this draw?" has one answer,
|
|
2107
2139
|
* and the probe inside a piece is representative of the whole piece.
|
|
2108
2140
|
*
|
|
2109
|
-
* Every timeline
|
|
2110
|
-
*
|
|
2111
|
-
*
|
|
2112
|
-
*
|
|
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).
|
|
2113
2145
|
*/
|
|
2114
|
-
function visibilityKeyTimes(timelines: readonly Timeline[], slots: ReadonlySet<number>): number[] {
|
|
2115
|
-
const times: number[] = [];
|
|
2116
|
-
for (const timeline of timelines) {
|
|
2117
|
-
const carrier = timeline as unknown as { slotIndex?: unknown };
|
|
2118
|
-
if (typeof carrier.slotIndex !== 'number' || !slots.has(carrier.slotIndex)) continue;
|
|
2119
|
-
const entries = timeline.getFrameEntries();
|
|
2120
|
-
for (let i = 0; i < timeline.getFrameCount(); i++) times.push(timeline.frames[i * entries]);
|
|
2121
|
-
}
|
|
2122
|
-
return times;
|
|
2123
|
-
}
|
|
2124
2146
|
|
|
2125
2147
|
/**
|
|
2126
2148
|
* Scan the interval between two consecutive deform keys (issue #403).
|
|
@@ -2176,9 +2198,9 @@ function visibilityKeyTimes(timelines: readonly Timeline[], slots: ReadonlySet<n
|
|
|
2176
2198
|
* being refused anyway.
|
|
2177
2199
|
*/
|
|
2178
2200
|
function scanDeformSpan(
|
|
2179
|
-
anim:
|
|
2180
|
-
timeline:
|
|
2181
|
-
attachment:
|
|
2201
|
+
anim: SurveyAnimation,
|
|
2202
|
+
timeline: SurveyDeformTimeline,
|
|
2203
|
+
attachment: SurveyMesh,
|
|
2182
2204
|
triangles: ArrayLike<number>,
|
|
2183
2205
|
named: { animation: string; skin: string; slot: string; attachment: string; placeholder: string },
|
|
2184
2206
|
/** The index of the key the span starts at — `frame + 1` is the one it ends at. */
|
|
@@ -2208,8 +2230,8 @@ function scanDeformSpan(
|
|
|
2208
2230
|
unconfirmed: false,
|
|
2209
2231
|
};
|
|
2210
2232
|
const reach = reachedFractions(legs);
|
|
2211
|
-
const v1 = timeline.vertices
|
|
2212
|
-
const v2 = timeline.vertices
|
|
2233
|
+
const v1 = timeline.vertices(frame);
|
|
2234
|
+
const v2 = timeline.vertices(frame + 1);
|
|
2213
2235
|
if (!v1 || !v2) return span;
|
|
2214
2236
|
|
|
2215
2237
|
const windows: Interval[] = [];
|
|
@@ -2225,7 +2247,7 @@ function scanDeformSpan(
|
|
|
2225
2247
|
// both of the span's own key poses are taken and the union used. `DW17` is
|
|
2226
2248
|
// the control for the claim: a bone rotation across the span moves nothing
|
|
2227
2249
|
// about where an unweighted mesh is found to fold.
|
|
2228
|
-
for (const anchor of attachment.
|
|
2250
|
+
for (const anchor of attachment.weighted ? [from, to] : [from]) {
|
|
2229
2251
|
// The two ends of the straight line every vertex travels, evaluated at THIS
|
|
2230
2252
|
// anchor's bones. `measurePosed` has already emptied the slot's deform array
|
|
2231
2253
|
// to take its plain side, so writing into it is how the two are posed.
|
|
@@ -2278,7 +2300,7 @@ function scanDeformSpan(
|
|
|
2278
2300
|
// straddles a change in what is drawn. Every point of a merged window is
|
|
2279
2301
|
// inside at least one triangle's window, so its middle is a fold and not a gap.
|
|
2280
2302
|
const slots = new Set<number>([timeline.slotIndex, ...attachment.timelineSlots]);
|
|
2281
|
-
const cuts =
|
|
2303
|
+
const cuts = anim.slotKeyTimes(slots);
|
|
2282
2304
|
const pieces: Interval[] = [];
|
|
2283
2305
|
for (const window of mergeIntervals(windows)) {
|
|
2284
2306
|
const inner = [...new Set(cuts.filter((t) => t > window.lo && t < window.hi))].sort((x, y) => x - y);
|
|
@@ -2410,7 +2432,7 @@ function shownAt(posed: Skeleton, slotIndex: number, attachment: MeshAttachment)
|
|
|
2410
2432
|
* green. What it must not do is leave the reader guessing which dress the
|
|
2411
2433
|
* verdict was taken in, so the skin is in the sentence.
|
|
2412
2434
|
*/
|
|
2413
|
-
function drawOfKey(posed: SurveyPose, slotIndex: number, attachment:
|
|
2435
|
+
function drawOfKey(posed: SurveyPose, slotIndex: number, attachment: SurveyMesh): DeformKeyDraw {
|
|
2414
2436
|
const own = posed.shownAt(slotIndex, attachment);
|
|
2415
2437
|
const under = posed.under();
|
|
2416
2438
|
const draw = { shown: own.shown, showsThisMesh: own.showsThisMesh, alpha: own.alpha, under };
|
|
@@ -2479,7 +2501,7 @@ function drawOfKey(posed: SurveyPose, slotIndex: number, attachment: MeshAttachm
|
|
|
2479
2501
|
function placementOf(
|
|
2480
2502
|
data: SkeletonData,
|
|
2481
2503
|
slotIndex: number,
|
|
2482
|
-
attachment:
|
|
2504
|
+
attachment: Attachment,
|
|
2483
2505
|
): { skin: string; holder: Skin | null; placeholder: string } {
|
|
2484
2506
|
for (const skin of [data.defaultSkin, ...data.skins]) {
|
|
2485
2507
|
if (!skin) continue;
|
|
@@ -2500,8 +2522,12 @@ function placementOf(
|
|
|
2500
2522
|
// spine-core over the Spine skeleton (the survey as it always was, and
|
|
2501
2523
|
// `validate.ts`'s A39, which hands it the SkeletonData it round-tripped), and
|
|
2502
2524
|
// rigc's own core over the model document (`./core/hooks.ts`, issue #969),
|
|
2503
|
-
// which a build carries and `explain` hands in.
|
|
2504
|
-
//
|
|
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
|
|
2505
2531
|
// two to one survey, byte for byte, on every corpus.
|
|
2506
2532
|
|
|
2507
2533
|
/** Which poser a survey used (`DeformSurvey.source`). */
|
|
@@ -2528,19 +2554,19 @@ export interface ShownReading {
|
|
|
2528
2554
|
export interface SurveyPose {
|
|
2529
2555
|
/** The skin the skeleton wears, off the pose itself (`DeformKeyDraw.under`). */
|
|
2530
2556
|
under(): string | null;
|
|
2531
|
-
shownAt(slotIndex: number, attachment:
|
|
2557
|
+
shownAt(slotIndex: number, attachment: SurveyMesh): ShownReading;
|
|
2532
2558
|
/** The mesh's world vertices with the slot's deform as posed. */
|
|
2533
|
-
deformed(slotIndex: number, attachment:
|
|
2559
|
+
deformed(slotIndex: number, attachment: SurveyMesh): Float32Array;
|
|
2534
2560
|
/** The same bones with the slot's deform cleared. */
|
|
2535
|
-
plain(slotIndex: number, attachment:
|
|
2561
|
+
plain(slotIndex: number, attachment: SurveyMesh): Float32Array;
|
|
2536
2562
|
/** The same bones with `deform` written into the slot. */
|
|
2537
|
-
withDeform(slotIndex: number, attachment:
|
|
2563
|
+
withDeform(slotIndex: number, attachment: SurveyMesh, deform: ArrayLike<number>): Float32Array;
|
|
2538
2564
|
/**
|
|
2539
2565
|
* The doubles those three are stored from — the slot's deform as posed,
|
|
2540
2566
|
* cleared, or replaced — leaving the pose as it found it. The survey reads
|
|
2541
2567
|
* the float32 rows; this is what measures the rows' source (`tools/survey_hashes.ts hooks`).
|
|
2542
2568
|
*/
|
|
2543
|
-
rows(slotIndex: number, attachment:
|
|
2569
|
+
rows(slotIndex: number, attachment: SurveyMesh, deform: 'posed' | 'cleared' | ArrayLike<number>): number[];
|
|
2544
2570
|
}
|
|
2545
2571
|
|
|
2546
2572
|
/** One slider's dial, posed again and again (`planDial`'s probe, `poseDial`'s solve). */
|
|
@@ -2557,23 +2583,186 @@ export interface DialSession {
|
|
|
2557
2583
|
|
|
2558
2584
|
/** The two ways the survey poses: a jump on a track, and a slider's dial. */
|
|
2559
2585
|
export interface SurveyPoser {
|
|
2560
|
-
track(skin:
|
|
2586
|
+
track(skin: SurveySkin | null, animation: string, time: number): SurveyPose;
|
|
2561
2587
|
/** `null` when the skeleton carries no such slider. */
|
|
2562
|
-
dial(skin:
|
|
2588
|
+
dial(skin: SurveySkin | null, slider: SurveySlider): DialSession | null;
|
|
2563
2589
|
}
|
|
2564
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
|
+
|
|
2565
2752
|
/** A live spine-core skeleton, read the way the survey always read it. */
|
|
2566
|
-
function spinePose(skeleton: Skeleton): SurveyPose {
|
|
2753
|
+
function spinePose(skeleton: Skeleton, links: RuntimeLinks): SurveyPose {
|
|
2567
2754
|
return {
|
|
2568
2755
|
under: () => skeleton.skin?.name ?? null,
|
|
2569
|
-
shownAt: (slotIndex,
|
|
2570
|
-
deformed: (slotIndex,
|
|
2756
|
+
shownAt: (slotIndex, handle) => shownAt(skeleton, slotIndex, links.mesh(handle)),
|
|
2757
|
+
deformed: (slotIndex, handle) => {
|
|
2758
|
+
const attachment = links.mesh(handle);
|
|
2571
2759
|
const count = attachment.worldVerticesLength;
|
|
2572
2760
|
const world = new Float32Array(count);
|
|
2573
2761
|
attachment.computeWorldVertices(skeleton, skeleton.slots[slotIndex], 0, count, world, 0, 2);
|
|
2574
2762
|
return world;
|
|
2575
2763
|
},
|
|
2576
|
-
plain: (slotIndex,
|
|
2764
|
+
plain: (slotIndex, handle) => {
|
|
2765
|
+
const attachment = links.mesh(handle);
|
|
2577
2766
|
const count = attachment.worldVerticesLength;
|
|
2578
2767
|
const slot = skeleton.slots[slotIndex];
|
|
2579
2768
|
slot.appliedPose.deform.length = 0;
|
|
@@ -2581,8 +2770,9 @@ function spinePose(skeleton: Skeleton): SurveyPose {
|
|
|
2581
2770
|
attachment.computeWorldVertices(skeleton, slot, 0, count, world, 0, 2);
|
|
2582
2771
|
return world;
|
|
2583
2772
|
},
|
|
2584
|
-
withDeform: (slotIndex,
|
|
2585
|
-
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);
|
|
2586
2776
|
const slot = skeleton.slots[slotIndex];
|
|
2587
2777
|
const array = slot.appliedPose.deform;
|
|
2588
2778
|
const kept = [...array];
|
|
@@ -2601,11 +2791,12 @@ function spinePose(skeleton: Skeleton): SurveyPose {
|
|
|
2601
2791
|
}
|
|
2602
2792
|
|
|
2603
2793
|
/** The survey's poses through spine-core — the recipes the survey has always taken (`poseAt`; the dial below). */
|
|
2604
|
-
function spinePoser(data: SkeletonData): SurveyPoser {
|
|
2794
|
+
function spinePoser(data: SkeletonData, links: RuntimeLinks): SurveyPoser {
|
|
2605
2795
|
return {
|
|
2606
|
-
track: (skin, animation, time) => spinePose(poseAt(data, skin, animation, time)),
|
|
2607
|
-
dial: (skin,
|
|
2608
|
-
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));
|
|
2609
2800
|
const instance = sliderOn(skeleton, slider);
|
|
2610
2801
|
if (instance === null) return null;
|
|
2611
2802
|
const bone = instance.bone;
|
|
@@ -2627,41 +2818,25 @@ function spinePoser(data: SkeletonData): SurveyPoser {
|
|
|
2627
2818
|
if (slider.local) bone.appliedPose.validateLocalTransform(skeleton);
|
|
2628
2819
|
return { read: slider.property.value(skeleton, bone.appliedPose, slider.local, DIAL_ZERO_OFFSETS), applied: instance.appliedPose.time };
|
|
2629
2820
|
},
|
|
2630
|
-
pose: () => spinePose(skeleton),
|
|
2821
|
+
pose: () => spinePose(skeleton, links),
|
|
2631
2822
|
};
|
|
2632
2823
|
},
|
|
2633
2824
|
};
|
|
2634
2825
|
}
|
|
2635
2826
|
|
|
2636
|
-
/**
|
|
2637
|
-
function
|
|
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 {
|
|
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 {
|
|
2654
2829
|
const slotName = (slotIndex: number): string => {
|
|
2655
|
-
const
|
|
2656
|
-
if (
|
|
2657
|
-
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;
|
|
2658
2833
|
};
|
|
2659
|
-
const recordOf = (attachment:
|
|
2660
|
-
const r =
|
|
2661
|
-
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`);
|
|
2662
2837
|
return r;
|
|
2663
2838
|
};
|
|
2664
|
-
const world = (slotIndex: number, attachment:
|
|
2839
|
+
const world = (slotIndex: number, attachment: SurveyMesh, deform: 'posed' | 'cleared' | ArrayLike<number>): Float32Array =>
|
|
2665
2840
|
float32Rows(meshWorld(pose, slotName(slotIndex), recordOf(attachment), deform));
|
|
2666
2841
|
return {
|
|
2667
2842
|
under: () => pose.doc.skin,
|
|
@@ -2684,13 +2859,14 @@ function corePose(pose: CoreSurveyPose, data: SkeletonData, records: ReadonlyMap
|
|
|
2684
2859
|
* (`underSkin`) — so a skeleton declaring two skins of one name, and a pose
|
|
2685
2860
|
* wearing none, are refused by name: neither reaches the survey through
|
|
2686
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.
|
|
2687
2864
|
*/
|
|
2688
|
-
function corePoser(
|
|
2689
|
-
const records = recordsOf(data);
|
|
2865
|
+
function corePoser(structure: SurveyStructure, doc: CompiledDocument): SurveyPoser {
|
|
2690
2866
|
const views = new Map<string, CompiledDocument>();
|
|
2691
|
-
const viewOf = (skin:
|
|
2867
|
+
const viewOf = (skin: SurveySkin | null): CompiledDocument => {
|
|
2692
2868
|
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 (
|
|
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`);
|
|
2694
2870
|
const cached = views.get(skin.name);
|
|
2695
2871
|
if (cached !== undefined) return cached;
|
|
2696
2872
|
const view = underSkin(doc, skin.name);
|
|
@@ -2698,7 +2874,7 @@ function corePoser(data: SkeletonData, doc: CompiledDocument): SurveyPoser {
|
|
|
2698
2874
|
return view;
|
|
2699
2875
|
};
|
|
2700
2876
|
return {
|
|
2701
|
-
track: (skin, animation, time) => corePose(poseJump(viewOf(skin), animation, time),
|
|
2877
|
+
track: (skin, animation, time) => corePose(poseJump(viewOf(skin), animation, time), structure),
|
|
2702
2878
|
dial: (skin, slider) => {
|
|
2703
2879
|
const view = viewOf(skin);
|
|
2704
2880
|
const record = sliderRecordOf(view, slider.name);
|
|
@@ -2719,7 +2895,7 @@ function corePoser(data: SkeletonData, doc: CompiledDocument): SurveyPoser {
|
|
|
2719
2895
|
},
|
|
2720
2896
|
pose: () => {
|
|
2721
2897
|
if (last === null) throw new CoreInputError(`slider "${slider.name}"'s dial was asked for its pose before it was posed`);
|
|
2722
|
-
return corePose(last,
|
|
2898
|
+
return corePose(last, structure);
|
|
2723
2899
|
},
|
|
2724
2900
|
};
|
|
2725
2901
|
},
|
|
@@ -2727,13 +2903,19 @@ function corePoser(data: SkeletonData, doc: CompiledDocument): SurveyPoser {
|
|
|
2727
2903
|
}
|
|
2728
2904
|
|
|
2729
2905
|
/**
|
|
2730
|
-
* Both posers over one build
|
|
2731
|
-
*
|
|
2732
|
-
*
|
|
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.
|
|
2733
2913
|
*/
|
|
2734
|
-
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 } {
|
|
2735
2915
|
const data = skeletonDataFromText(input.skeletonText, input.atlasText);
|
|
2736
|
-
|
|
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) };
|
|
2737
2919
|
}
|
|
2738
2920
|
|
|
2739
2921
|
/** A build's three texts, as `explain` and `tools/survey_hashes.ts` hold them. */
|
|
@@ -2742,25 +2924,69 @@ export interface DeformSurveyInput {
|
|
|
2742
2924
|
atlasText: string;
|
|
2743
2925
|
/** The model document (`skeleton.model.json`), or `null` when the input carries none (a Spine export). */
|
|
2744
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 } };
|
|
2745
2959
|
}
|
|
2746
2960
|
|
|
2747
2961
|
/**
|
|
2748
|
-
* The survey of a build, through the poser asked for (
|
|
2749
|
-
*
|
|
2750
|
-
*
|
|
2751
|
-
*
|
|
2752
|
-
*
|
|
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.
|
|
2753
2969
|
*/
|
|
2754
2970
|
export function surveyOfBuild(input: DeformSurveyInput, exempt: ReadonlySet<string>, asked: 'auto' | DeformSurveySource): DeformSurvey {
|
|
2755
|
-
const
|
|
2756
|
-
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
|
+
};
|
|
2757
2977
|
if (asked === 'spine-core') return throughSpine(null);
|
|
2758
2978
|
if (input.modelText === null) {
|
|
2759
2979
|
if (asked === 'model') throw new CoreInputError('the survey was asked to pose the model document, and the input carries none');
|
|
2760
2980
|
return throughSpine('the input carries no model document (skeleton.model.json), so the survey posed the Spine skeleton through spine-core');
|
|
2761
2981
|
}
|
|
2762
2982
|
try {
|
|
2763
|
-
|
|
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);
|
|
2764
2990
|
} catch (err) {
|
|
2765
2991
|
if (!(err instanceof CoreInputError) || asked === 'model') throw err;
|
|
2766
2992
|
return throughSpine(`the core refused to pose the model document — ${err.message}`);
|