spine-rigc 1.5.3 → 1.6.2

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