spine-rigc 2.14.0 → 2.14.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "2.14.0",
3
+ "version": "2.14.1",
4
4
  "description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,15 @@
13
13
  * rather than refusing it. It is held equal to spine-core's walk at
14
14
  * tolerance 0 by `tools/core_gate.ts`'s walk block and the selftest's
15
15
  * `CO25`, and the poses A10 asks for here are compared with spine-core's on
16
- * every call with a model in hand (`VF13`).
16
+ * every call with a model in hand (`VF13`). Since issue #1179's second part
17
+ * the poses come through that walk's scan assembler (`scanWalkSetup`,
18
+ * `scanLoopingWalk`): the same walk, each pose carrying what A10 reads — the
19
+ * bones' matrix and origin, the slot rows, the drawn attachments' slot, name
20
+ * and vertices — and none of the bones' getter readings and oracle rows or
21
+ * the drawn rows' UVs, triangles and colour, which nothing it reads is
22
+ * computed from. The core suite's `CO43` holds the scan to the public walk's
23
+ * numbers bit for bit, `CO44` what it leaves out to move none of them, and
24
+ * `CO45` that the vertex arrays it hands on uncopied are each pose's own.
17
25
  * - **A bone's mode**: the mode the core's bone timelines pose at the time
18
26
  * (`posedBones`, a fresh non-looping track: the time held at the duration),
19
27
  * folded as the runtime's lookup folds it (`foldInheritMode`). `readModel`
@@ -30,7 +38,8 @@
30
38
  import { fileAnimationOrder } from '../../compile.ts';
31
39
  import { foldInheritMode, type CompiledDocument } from '../../core/index.ts';
32
40
  import { posedBones } from '../../core/animation.ts';
33
- import { poseLoopingWalk, poseWalkSetup, type WalkPose } from '../../core/walk.ts';
41
+ import { scanLoopingWalk, scanWalkSetup } from '../../core/walk.ts';
42
+ import type { ScanPose } from '../../core/raw.ts';
34
43
  import { noSkinView } from '../../render_core.ts';
35
44
  import type { ModelBone } from '../../model.ts';
36
45
  import type { SteppedFrame, SteppedPoseFacts } from '../facts/stepped_poses.ts';
@@ -45,11 +54,11 @@ const channel = (v: number | null): number => (v === null ? Number.NaN : v);
45
54
  /** Each bone's posed mode as the facts spell it (`modeOf`), by name. */
46
55
  const modesOf = (posed: readonly ModelBone[]): ReadonlyMap<string, string | null> => new Map(posed.map((b) => [b.name, modeOf(b)]));
47
56
 
48
- /** One walk pose as A10 reads it, each bone's mode taken from `modes` (`modesOf` the bones the timelines posed at the pose's time). */
49
- function frameOf(pose: WalkPose, modes: ReadonlyMap<string, string | null>): SteppedFrame {
57
+ /** One scan pose as A10 reads it, each bone's mode taken from `modes` (`modesOf` the bones the timelines posed at the pose's time); the drawn attachments are the scan's own rows, already in the shape A10 reads. */
58
+ function frameOf(pose: ScanPose, modes: ReadonlyMap<string, string | null>): SteppedFrame {
50
59
  return {
51
60
  bones: pose.bones.map((b) => ({ name: b.name, a: b.a, b: b.b, c: b.c, d: b.d, worldX: b.worldX, worldY: b.worldY, inherit: modes.get(b.name) ?? null })),
52
- drawn: pose.drawn.map((d) => ({ slot: d.slot, attachment: d.attachment, vertices: d.vertices })),
61
+ drawn: pose.drawn,
53
62
  slots: pose.slots.map((row) => ({
54
63
  name: row[0],
55
64
  colour: [channel(row[2]), channel(row[3]), channel(row[4]), channel(row[5])] as const,
@@ -82,10 +91,10 @@ export function modelSteppedPoses(read: ReadDocument): SteppedPoseFacts {
82
91
  },
83
92
  // The document's stated mode — asked only of a bone posing none, which the reader leaves this side no document to hold.
84
93
  statedInherit: (name) => `${JSON.stringify(read.doc.bones.find((b) => b.name === name)?.inheritMode)}`,
85
- setup: () => frameOf(poseWalkSetup(viewOf()), modesOf(viewOf().bones)),
94
+ setup: () => frameOf(scanWalkSetup(viewOf()), modesOf(viewOf().bones)),
86
95
  walk: (animation, step, frames) => {
87
96
  const anim = animationOf(animation);
88
- const poses = poseLoopingWalk(viewOf(), animation, new Array<number>(frames).fill(step));
97
+ const poses = scanLoopingWalk(viewOf(), animation, new Array<number>(frames).fill(step));
89
98
  // A bone's posed mode moves only by an `inherit` timeline: every other one `posedBones` reads leaves the setup's mode on its copy. So an animation
90
99
  // keying none poses the setup's modes at every time, and they are read once rather than posed again at each of its frames (issue #1179).
91
100
  const keysInherit = anim.timelines.bones.some((target) => target.timelines.some((tl) => tl.kind === 'inherit'));
@@ -797,6 +797,17 @@ export function posedBoneWorld(doc: CompiledDocument, timelines: CoreAnimationTi
797
797
  return { rows: stepRows(posed), world: posed.world };
798
798
  }
799
799
 
800
+ /**
801
+ * `posedBoneWorld`'s world transforms without its rows (issue #1179, the
802
+ * second part): the same pose by the same operations in the same order, since
803
+ * `stepRows` only reads the world once it is posed. A walk reads `world` and
804
+ * no row, so the scan's walk (`scanWalkIn` in `./raw.ts`, A10's) asks for
805
+ * this; the raw entry's walk still forms the rows it does not read, unchanged.
806
+ */
807
+ export function posedBoneWorldAlone(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step: { ctx: PhysicsStepContext; before: number } | undefined): Map<string, CoreWorld> {
808
+ return posedBoneStep(doc, timelines, t, plant, constraints, sliders, step).world;
809
+ }
810
+
800
811
  /**
801
812
  * One pose of the bones before its rows are read: the world transforms, and
802
813
  * what the rows are rounded from. A stepped walk reads rows only off the step
package/src/core/raw.ts CHANGED
@@ -114,14 +114,14 @@
114
114
  * missing would draw a different picture in silence.
115
115
  */
116
116
  import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber, readColour, shownAttachment, sourceOfDoc, underNoSkin, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
117
- import { posedBoneWorld, posedSlots, type TimelinePlant } from './animation.ts';
117
+ import { posedBoneWorld, posedBoneWorldAlone, posedSlots, type TimelinePlant } from './animation.ts';
118
118
  import { constraintsAbsentWhy, pathAnimationsWhy } from './constraints.ts';
119
119
  import { freshStepContext, steppedPreviousPassWhy } from './constraints_physics.ts';
120
120
  import { attachmentStates } from './deform.ts';
121
121
  import { drawOrderAt } from './draw_order.ts';
122
122
  import { eventsFired } from './events.ts';
123
- import { REGION_TRIANGLES, REGION_UVS, type ShapeClipper } from './clipping.ts';
124
- import { poseGeometry, type ShownGeometry } from './vertices.ts';
123
+ import { REGION_TRIANGLES, REGION_UVS, type CoreClippedRow, type ShapeClipper } from './clipping.ts';
124
+ import { poseGeometry, type CoreAttachmentRow, type CoreClipRow, type ShownGeometry } from './vertices.ts';
125
125
  import { RUNTIME_DEG, type CoreWorld } from './world.ts';
126
126
  import type { SliderApplication } from './constraints_slider.ts';
127
127
 
@@ -195,6 +195,43 @@ export interface RawPose {
195
195
  // --- #968 render: end ---
196
196
  }
197
197
 
198
+ /**
199
+ * One bone as the scan reads it (issue #1179, the second part): the world
200
+ * matrix and origin `RawBone` carries, and none of the readings it forms from
201
+ * them. `A10_NO_NAN_AFTER_STEPPING` reads these six numbers and the name off a
202
+ * bone, and nothing else (`firstNonFinite` in `src/nonfinite.ts`).
203
+ */
204
+ export interface ScanBone {
205
+ name: string;
206
+ a: number;
207
+ b: number;
208
+ c: number;
209
+ d: number;
210
+ worldX: number;
211
+ worldY: number;
212
+ }
213
+
214
+ /**
215
+ * One pose as the scan reads it (issue #1179, the second part) — the scan's
216
+ * walk (`scanWalkIn`, `scanSetupIn`): the time the animation was applied at,
217
+ * and the bones, slot rows and drawn attachments computed exactly as a
218
+ * `RawPose`'s are. Internal to A10's walk (`./walk.ts`); no exported pose type
219
+ * changed for it.
220
+ */
221
+ export interface ScanPose {
222
+ animationTime: number;
223
+ bones: ScanBone[];
224
+ slots: CoreSlotRow[];
225
+ drawn: ScanDrawn[];
226
+ }
227
+
228
+ /** One region or mesh drawn, as the scan reads it: its slot, its name and its world vertices, as `RawDrawn` carries them. */
229
+ export interface ScanDrawn {
230
+ slot: string;
231
+ attachment: string;
232
+ vertices: number[];
233
+ }
234
+
198
235
  // --- #968 render: begin ---
199
236
  /** The shown records in `order`, the pose's draw order (a pose's `shown`). */
200
237
  function drawOrderOf(shown: readonly ShownGeometry[], order: readonly string[]): ShownGeometry[] {
@@ -274,17 +311,96 @@ function rawBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>):
274
311
  });
275
312
  }
276
313
 
277
- /** Every region and mesh the shown records draw, with what the renderer reads past the vertices (the header's `drawn`). */
278
- function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode = RAW_MODE): { drawn: RawDrawn[]; clips: RawClip[]; clipped: RawClipped[] } {
314
+ /**
315
+ * The bones of a scan pose (`ScanBone`): `rawBones` less the parent, the
316
+ * active flag and the four getter readings, which nothing the scan reads is
317
+ * computed from — each reading is formed from the world after it is posed,
318
+ * and the matrix and origin are copied as they are.
319
+ */
320
+ function scanBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>): ScanBone[] {
321
+ return doc.bones.map((b): ScanBone => {
322
+ const w = world.get(b.name);
323
+ if (w === undefined) throw new CoreInputError(`bone "${b.name}" has no world transform`);
324
+ return { name: b.name, a: w.a, b: w.b, c: w.c, d: w.d, worldX: w.worldX, worldY: w.worldY };
325
+ });
326
+ }
327
+
328
+ /**
329
+ * What a pose draws, posed and refused where the core leaves it out: the
330
+ * attachment rows, the clip rows and the drawn clipped rows — what `rawDrawn`
331
+ * and `scanDrawn` both assemble from.
332
+ */
333
+ function drawnGeometry(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode): { rows: CoreAttachmentRow[]; clips: CoreClipRow[]; drawnClipped: CoreClippedRow[] } {
279
334
  const draw = drawWalkOf(doc, order, plant);
280
335
  const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices }, mode.loop && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
281
336
  if (geometry.attachments === null) throw new CoreInputError(`the raw pose leaves the attachments out: ${geometry.attachmentsWhy}`);
282
337
  // The drawn rows (issue #964): the oracle's `clipped` rows where it is posed, and a concave or inverse clip cut through the core's own decomposition.
283
338
  if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
339
+ return { rows: geometry.attachments, clips: geometry.clips, drawnClipped: geometry.drawnClipped };
340
+ }
341
+
342
+ /**
343
+ * The drawn attachments of a scan pose (`ScanDrawn`): `rawDrawn`'s rows, each
344
+ * its slot, its name and its world vertices through the same `numberOf`, and
345
+ * none of what `rawDrawn` reads past them for a renderer — the UVs and
346
+ * triangles it copies, the record it looks up for the colour and the sequence
347
+ * frame, the hull, the clip rows' numbers. No vertex is computed from any of
348
+ * those. The one refusal `rawDrawn` makes past the rows, a linked mesh whose
349
+ * source carries no mesh, is not reachable here: `poseGeometry` resolved the
350
+ * same source by the same lookup (`sourceOfDoc`) and throws before it returns.
351
+ *
352
+ * 🔁 **No copy of the vertices** (issue #1179, the second part): `rawDrawn`
353
+ * maps each row through `numberOf` into a new array; under `keep` that map is
354
+ * the identity on every number and turns only a `null` into NaN. The scan
355
+ * hands the row's own array on wherever it holds no `null` (`allNumbers`) and
356
+ * maps it as before where it does, so A10 reads the same values either way.
357
+ * What makes the row's array safe to keep past the step — A10 collects an
358
+ * animation's 120 frames before it scans one — is that it is the pose's own:
359
+ * `poseGeometry` (`./vertices.ts`) builds every row's vertices with
360
+ * `.map(round)` over the corners or the skinned vertices it just computed, a
361
+ * new array per attachment per pose, held by nothing but the row. The core
362
+ * suite's `CO45` plants a poser that hands every pose the same array and reads
363
+ * the retained frames red.
364
+ */
365
+ function scanDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode, scanPlant: ScanPlant = {}): ScanDrawn[] {
366
+ const { rows } = drawnGeometry(doc, shown, world, order, plant, mode);
367
+ const drawn: ScanDrawn[] = [];
368
+ let k = 0;
369
+ for (const s of shown) {
370
+ const g = s.geometry;
371
+ if (g.kind !== 'region' && g.kind !== 'mesh' && g.kind !== 'linkedmesh') continue;
372
+ const row = rows[k++];
373
+ if (row === undefined || row[0] !== s.slot) throw new CoreInputError(`slot "${s.slot}": the attachment rows are not in the shown records' order`);
374
+ const vertices = row[3];
375
+ const kept = mode.keep && allNumbers(vertices) ? vertices : vertices.map((v) => numberOf(v, `vertex of slot "${s.slot}"`, mode));
376
+ drawn.push({ slot: s.slot, attachment: row[1], vertices: scanPlant.vertices === undefined ? kept : scanPlant.vertices(kept, s.slot) });
377
+ }
378
+ return drawn;
379
+ }
380
+
381
+ /** Whether a row's vertices hold no `null` — what `numberOf` would map — so the row's own array can be handed on as numbers. */
382
+ function allNumbers(values: Array<number | null>): values is number[] {
383
+ for (const v of values) if (v === null) return false;
384
+ return true;
385
+ }
386
+
387
+ /**
388
+ * The scan's one plant (the core suite's `CO45`): what a drawn row's vertices
389
+ * are handed on as, given the array the scan would hand on. A poser that
390
+ * reuses one buffer per slot across steps is the plant `CO45` passes.
391
+ * Nothing but a control passes one.
392
+ */
393
+ export interface ScanPlant {
394
+ vertices?: (vertices: number[], slot: string) => number[];
395
+ }
396
+
397
+ /** Every region and mesh the shown records draw, with what the renderer reads past the vertices (the header's `drawn`). */
398
+ function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode = RAW_MODE): { drawn: RawDrawn[]; clips: RawClip[]; clipped: RawClipped[] } {
399
+ const geometry = drawnGeometry(doc, shown, world, order, plant, mode);
284
400
  // Under a concave or inverse clip the oracle's `clipped` block is absent (the runtime's own triangle list); what the core draws is `drawnClipped`,
285
401
  // its own decomposition, and the render samples each drawn triangle at its source triangle's affine UV, so the pixels are the decomposition's
286
402
  // coverage alone (issue #964, `Mesh.source` in src/render.ts).
287
- const rows = geometry.attachments;
403
+ const rows = geometry.rows;
288
404
  const drawn: RawDrawn[] = [];
289
405
  let k = 0;
290
406
  for (const s of shown) {
@@ -326,14 +442,27 @@ export function poseRawSetup(doc: CompiledDocument, plant: CorePlant = {}): RawP
326
442
 
327
443
  /** `poseRawSetup` in `mode` — under `keep`, a value that is not finite stays in the pose (`./walk.ts`). */
328
444
  export function setupPoseIn(doc: CompiledDocument, plant: CorePlant, mode: WalkMode): RawPose {
445
+ const { world, shown, slots, drawOrder } = setupPosed(doc, plant, mode);
446
+ const drawn = rawDrawn(doc, shown, world, drawOrder, plant, mode);
447
+ return { trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...drawn, events: [], shown: drawOrderOf(shown, drawOrder) };
448
+ }
449
+
450
+ /** `setupPoseIn` as the scan reads it (`ScanPose`): the same setup pose, the same refusals in the same order, assembled into the scan's pose. */
451
+ export function scanSetupIn(doc: CompiledDocument, plant: CorePlant, mode: WalkMode, scanPlant: ScanPlant = {}): ScanPose {
452
+ const { world, shown, slots, drawOrder } = setupPosed(doc, plant, mode);
453
+ const drawn = scanDrawn(doc, shown, world, drawOrder, plant, mode, scanPlant);
454
+ return { animationTime: 0, bones: scanBones(doc, world), slots, drawn };
455
+ }
456
+
457
+ /** The setup pose `setupPoseIn` and `scanSetupIn` assemble, refused by name where the core leaves a block of it out. */
458
+ function setupPosed(doc: CompiledDocument, plant: CorePlant, mode: WalkMode): { world: Map<string, CoreWorld>; shown: ShownGeometry[]; slots: CoreSlotRow[]; drawOrder: string[] } {
329
459
  const posed = poseSetup(doc, { ...plant, ...roundPlant(mode) }, freshStepContext(plant.physicsStep));
330
460
  // `setup.clipped` is the oracle's block, left out under a concave or inverse clip whose triangle list is the runtime's own; what the core draws there is `rawDrawn`'s to refuse or pose (issue #964).
331
461
  const absent = posed.absent.filter(([block]) => block !== 'setup.clipped');
332
462
  if (absent.length > 0) throw new CoreInputError(`the raw setup pose leaves ${absent.map(([b, why]) => `${b} out (${why})`).join('; ')}`);
333
463
  const { setup, world, shown } = posed;
334
464
  if (world === null || shown === null || setup.slots === null || setup.drawOrder === null) throw new CoreInputError('the raw setup pose was not posed');
335
- const drawn = rawDrawn(doc, shown, world, setup.drawOrder, plant, mode);
336
- return { trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots: setup.slots, drawOrder: setup.drawOrder, ...drawn, events: [], shown: drawOrderOf(shown, setup.drawOrder) };
465
+ return { world, shown, slots: setup.slots, drawOrder: setup.drawOrder };
337
466
  }
338
467
 
339
468
  /**
@@ -361,7 +490,7 @@ export function poseRawAnimation(doc: CompiledDocument, animation: string, steps
361
490
  * poses before it.
362
491
  */
363
492
  export function poseRawAnimationEach(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, visit: (pose: RawPose, index: number) => void): void {
364
- walkEach(doc, animation, steps, plant, reset, RAW_MODE, visit);
493
+ walkEach(doc, animation, steps, plant, reset, RAW_MODE, RAW_SHAPE, visit);
365
494
  }
366
495
 
367
496
  /**
@@ -381,12 +510,81 @@ export function loopedTime(trackTime: number, duration: number): number {
381
510
  */
382
511
  export function walkIn(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode): RawPose[] {
383
512
  const poses: RawPose[] = [];
384
- walkEach(doc, animation, steps, plant, reset, mode, (pose) => poses.push(pose));
513
+ walkEach(doc, animation, steps, plant, reset, mode, RAW_SHAPE, (pose) => poses.push(pose));
385
514
  return poses;
386
515
  }
387
516
 
388
- /** `walkIn`'s walk, each pose handed to `visit` in order as it is posed (`poseRawAnimationEach`). */
389
- function walkEach(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode, visit: (pose: RawPose, index: number) => void): void {
517
+ /**
518
+ * `walkIn` as the scan reads it (`ScanPose`, issue #1179, the second part):
519
+ * the same walk — every pose posed by the same operations in the same order,
520
+ * every refusal at the same place — each pose assembled into what A10 reads.
521
+ * `./walk.ts`'s `scanLoopingWalk` is its one caller.
522
+ */
523
+ export function scanWalkIn(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode, scanPlant: ScanPlant = {}): ScanPose[] {
524
+ const poses: ScanPose[] = [];
525
+ walkEach(doc, animation, steps, plant, reset, mode, scanPlant.vertices === undefined ? SCAN_SHAPE : scanShape(scanPlant), (pose) => poses.push(pose));
526
+ return poses;
527
+ }
528
+
529
+ /** What one step of a walk posed, before a pose is assembled from it (`PoseShape`). */
530
+ interface WalkStep {
531
+ trackTime: number;
532
+ animationTime: number;
533
+ world: Map<string, CoreWorld>;
534
+ slots: CoreSlotRow[];
535
+ drawOrder: string[];
536
+ shown: ShownGeometry[];
537
+ /** The events the step fired, computed when called — after the drawn rows, as the raw entry has always ordered its refusals. */
538
+ events: () => RawEvent[];
539
+ }
540
+
541
+ /**
542
+ * What a walk assembles each pose into (issue #1179, the second part): the raw
543
+ * entry's whole pose (`RAW_SHAPE`), or the scan's (`SCAN_SHAPE`). The walk
544
+ * (`walkEach`) is one body: every number a pose carries is computed there and
545
+ * in the two assemblers below, by the same calls, whatever the shape. A shape
546
+ * chooses only what is assembled from what was posed — the scan drops the
547
+ * readings, rows and copies nothing it reads is computed from — and keeps the
548
+ * order of every call that can refuse.
549
+ */
550
+ interface PoseShape<P> {
551
+ /** Whether a step forms the oracle's bone rows beside its world (`posedBoneWorld`), which no walk pose carries; `false` poses the world alone (`posedBoneWorldAlone`). */
552
+ rows: boolean;
553
+ /** The reset pose at the setup (`reset: 'setup'`). */
554
+ setup: (doc: CompiledDocument, world: Map<string, CoreWorld>, slots: CoreSlotRow[], drawOrder: string[], shown: ShownGeometry[], plant: CorePlant, mode: WalkMode) => P;
555
+ /** One step's pose. */
556
+ step: (doc: CompiledDocument, posed: WalkStep, plant: CorePlant, mode: WalkMode) => P;
557
+ }
558
+
559
+ /** The raw entry's pose, assembled as it always was: at the reset the bones, then the drawn rows; at a step the drawn rows, the events, then the bones. */
560
+ const RAW_SHAPE: PoseShape<RawPose> = {
561
+ rows: true,
562
+ setup: (doc, world, slots, drawOrder, shown, plant, mode) => ({ trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...rawDrawn(doc, shown, world, drawOrder, plant, mode), events: [], shown: drawOrderOf(shown, drawOrder) }),
563
+ step: (doc, p, plant, mode) => {
564
+ const drawn = rawDrawn(doc, p.shown, p.world, p.drawOrder, plant, mode);
565
+ const events = p.events();
566
+ return { trackTime: p.trackTime, animationTime: p.animationTime, bones: rawBones(doc, p.world), slots: p.slots, drawOrder: p.drawOrder, ...drawn, events, shown: p.shown };
567
+ },
568
+ };
569
+
570
+ /** The scan's pose (`ScanPose`), in the raw shape's order: the scan's walk is a looping one, which fires no event. */
571
+ function scanShape(scanPlant: ScanPlant): PoseShape<ScanPose> {
572
+ return {
573
+ rows: false,
574
+ setup: (doc, world, slots, drawOrder, shown, plant, mode) => {
575
+ const bones = scanBones(doc, world);
576
+ return { animationTime: 0, bones, slots, drawn: scanDrawn(doc, shown, world, drawOrder, plant, mode, scanPlant) };
577
+ },
578
+ step: (doc, p, plant, mode) => {
579
+ const drawn = scanDrawn(doc, p.shown, p.world, p.drawOrder, plant, mode, scanPlant);
580
+ return { animationTime: p.animationTime, bones: scanBones(doc, p.world), slots: p.slots, drawn };
581
+ },
582
+ };
583
+ }
584
+ const SCAN_SHAPE: PoseShape<ScanPose> = scanShape({});
585
+
586
+ /** `walkIn`'s walk, each pose assembled in `shape` and handed to `visit` in order as it is posed (`poseRawAnimationEach`). */
587
+ function walkEach<P>(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, mode: WalkMode, shape: PoseShape<P>, visit: (pose: P, index: number) => void): void {
390
588
  const anim = doc.animations.find((a) => a.name === animation);
391
589
  if (anim === undefined) throw new CoreInputError(`animation "${animation}" is not one of this document's [${doc.animations.map((a) => a.name).join(', ')}]`);
392
590
  const bad = steps.findIndex((s) => !Number.isFinite(s) || s < 0);
@@ -408,7 +606,7 @@ function walkEach(doc: CompiledDocument, animation: string, steps: readonly numb
408
606
  if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join('; ')}`);
409
607
  if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError('the raw walk\'s reset pose was not posed');
410
608
  ctx.phase = 'update';
411
- visit({ trackTime: 0, animationTime: 0, bones: rawBones(doc, setup.world), slots: setup.setup.slots, drawOrder: setup.setup.drawOrder, ...rawDrawn(doc, setup.shown, setup.world, setup.setup.drawOrder, plant, mode), events: [], shown: drawOrderOf(setup.shown, setup.setup.drawOrder) }, visited++);
609
+ visit(shape.setup(doc, setup.world, setup.setup.slots, setup.setup.drawOrder, setup.shown, plant, mode), visited++);
412
610
  }
413
611
  for (let i = reset === 'setup' ? 1 : 0; i <= steps.length; i++) {
414
612
  const dt = i === 0 ? 0 : steps[i - 1];
@@ -417,7 +615,7 @@ function walkEach(doc: CompiledDocument, animation: string, steps: readonly numb
417
615
  const before = ctx.time;
418
616
  ctx.time += mode.clock === undefined ? dt : mode.clock(dt, t - (i === 0 ? 0 : Math.max(last, 0)));
419
617
  const sliders: SliderApplication[] = [];
420
- const posed = posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before });
618
+ const world = shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before });
421
619
  if (i === 0) ctx.phase = 'update';
422
620
  const placeholders = new Map<string, string | null>();
423
621
  const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
@@ -430,12 +628,13 @@ function walkEach(doc: CompiledDocument, animation: string, steps: readonly numb
430
628
  if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
431
629
  const rank = new Map(drawOrder.map((n, r) => [n, r]));
432
630
  const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
433
- const drawn = rawDrawn(doc, shown, posed.world, drawOrder, plant, mode);
434
631
  // A looping walk fires no event here: what fires across the wrap was not measured, and the walk's one reader (A10) reads none. Nor does it cut its
435
632
  // drawn attachments through a clip (`NO_CLIPPED_ROWS`), so its `clipped` rows are empty.
436
- const events = mode.loop ? [] : (plant.events ?? eventsFired)(anim.timelines.events, last, t, rawNumber).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]);
633
+ const from = last;
634
+ const events = (): RawEvent[] => (mode.loop ? [] : (plant.events ?? eventsFired)(anim.timelines.events, from, t, rawNumber).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]));
635
+ const pose = shape.step(doc, { trackTime, animationTime: t, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
437
636
  last = t;
438
- visit({ trackTime, animationTime: t, bones: rawBones(doc, posed.world), slots: slots.rows, drawOrder, ...drawn, events, shown }, visited++);
637
+ visit(pose, visited++);
439
638
  }
440
639
  }
441
640
 
package/src/core/walk.ts CHANGED
@@ -76,7 +76,7 @@
76
76
  * poses the class as the raw entry does, and refuses nothing the raw entry
77
77
  * does not.
78
78
  */
79
- import { setupPoseIn, walkIn, type RawBone, type RawDrawn, type WalkMode } from './raw.ts';
79
+ import { scanSetupIn, scanWalkIn, setupPoseIn, walkIn, type RawBone, type RawDrawn, type ScanPlant, type ScanPose, type WalkMode } from './raw.ts';
80
80
  import { activeBones, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
81
81
  import { historyTaint } from './constraints.ts';
82
82
  import type { TimelinePlant } from './animation.ts';
@@ -175,3 +175,23 @@ export function poseLoopingWalk(doc: CompiledDocument, animation: string, steps:
175
175
  const mode: WalkMode = { ...LOOPING, ...(walkPlant.time === undefined ? {} : { time: walkPlant.time }), ...(walkPlant.clock === undefined ? {} : { clock: walkPlant.clock }), ...(walkPlant.wrapResets === undefined ? {} : { wrapResets: walkPlant.wrapResets }) };
176
176
  return walkIn(doc, animation, steps, plant, 'setup', mode).map((p) => narrow(p, walkPlant.number));
177
177
  }
178
+
179
+ /**
180
+ * The scan's walk (issue #1179, the second part): `poseLoopingWalk` and
181
+ * `poseWalkSetup` as A10 reads them, each pose a `ScanPose` — the bones' world
182
+ * matrix and origin, the slot rows and the drawn attachments, and nothing the
183
+ * walk forms beside them for other readers (the bones' oracle rows and getter
184
+ * readings). The same walk: `walkIn`'s body in `./raw.ts`, posing each step by
185
+ * the same operations in the same order and refusing at the same places, with
186
+ * another assembler (`PoseShape`). `src/assertions/model/stepped_poses.ts` is
187
+ * its one caller; the public entries above are unchanged, and the core suite
188
+ * holds the two to the same numbers (`CO43`, `CO44`, `CO45`).
189
+ */
190
+ export function scanLoopingWalk(doc: CompiledDocument, animation: string, steps: readonly number[], scanPlant: ScanPlant = {}): ScanPose[] {
191
+ return scanWalkIn(doc, animation, steps, {}, 'setup', LOOPING, scanPlant);
192
+ }
193
+
194
+ /** `poseWalkSetup` as the scan reads it (`scanLoopingWalk`). */
195
+ export function scanWalkSetup(doc: CompiledDocument): ScanPose {
196
+ return scanSetupIn(doc, {}, LOOPING);
197
+ }