spine-rigc 2.48.0 → 2.49.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/docs/AUTHORING.md CHANGED
@@ -4927,8 +4927,15 @@ that poses through its posers (`check`, `bonedist`, `inspect`, `--geometry`), ru
4927
4927
  the physics clock `2⁻³⁰` faster than the frames, so every such frame takes the steps
4928
4928
  exact arithmetic takes; the animation time is unchanged. A spring that has converged
4929
4929
  by the loop point therefore closes, which the loop-seam suite's `LS06` holds at all
4930
- five rates. A player with a real clock is a different question and rigc does not
4931
- answer it.
4930
+ five rates. A player that steps the core entry's live tracks at a fixed rate — a
4931
+ capture, an export, a test harness — is the render's case and takes the render's
4932
+ clock: `openTrack` and `openTracks` accept `clock`, what each step moves the physics
4933
+ clock by, and `physicsClockStep` (with `PHYSICS_CLOCK_LEAD`) is exported from
4934
+ `rig-c/core` to pass to it (issue #1396). It moves the physics alone — the track
4935
+ time, the animation time and the events stay on the steps, so the player keeps
4936
+ agreeing with `render` — and left out, a track steps the physics by the step itself,
4937
+ exactly as before. Which clock a player with a real clock uses is the player's
4938
+ question, and rigc does not answer it.
4932
4939
 
4933
4940
  `mix` is a player-side `AnimationStateData` config and is **not** emitted into
4934
4941
  skeleton JSON, so the parser is the only check it gets: it is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "2.48.0",
3
+ "version": "2.49.0",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/core/entry.ts CHANGED
@@ -17,13 +17,14 @@
17
17
  *
18
18
  * ⚠️ A symbol is listed here only because a dependant was seen using it,
19
19
  * or asked for it in writing before it could be (`openTrack`, issue #1276;
20
- * `openTracks`, issue #1336).
20
+ * `openTracks`, issue #1336; `physicsClockStep` and `PHYSICS_CLOCK_LEAD`,
21
+ * the clock a live track's `clock` option takes, issue #1396).
21
22
  * The list is `scripts/install_smoke.ts`'s `OBSERVED_SYMBOLS` rows for
22
23
  * `./core`, and a symbol added here and not there is not promised.
23
24
  */
24
25
  export { readModel, underSkin, underNoSkin, CoreInputError, CORE_DOCUMENT_SPEC, CORE_DOCUMENT_SPECS, CORE_BLEND_MODES, activeBones } from './index.ts';
25
26
  export type { CompiledDocument, CoreSlotRow, CoreBlendMode } from './index.ts';
26
- export { poseRawSetup, poseRawAnimation, poseRawAnimationEach, loopedTime, setupBounds } from './raw.ts';
27
+ export { poseRawSetup, poseRawAnimation, poseRawAnimationEach, loopedTime, setupBounds, physicsClockStep, PHYSICS_CLOCK_LEAD } from './raw.ts';
27
28
  export type { RawPose, RawBone, RawDrawn, RawClip, RawClipped, RawEvent, RawReset } from './raw.ts';
28
29
  export { poseWalkSetup, poseLoopingWalk } from './walk.ts';
29
30
  export { openTrack } from './track.ts';
package/src/core/raw.ts CHANGED
@@ -257,7 +257,7 @@ export interface WalkMode {
257
257
  keep: boolean;
258
258
  /** A plant only (the core suite's `CO26`): the animation time a looping step applies at, from its track time, the duration and the track time before it — `loopedTime` unless given. */
259
259
  time?: (trackTime: number, duration: number, before: number) => number;
260
- /** What the physics clock moves by, from the step and how far the animation time moved — the step itself unless given: a plant (`CO26`), or the render's offline clock passed to `poseRawAnimationEach` (issue #1380). */
260
+ /** What the physics clock moves by, from the step and how far the animation time moved — the step itself unless given: a plant (`CO26`), the render's offline clock passed to `poseRawAnimationEach` (issue #1380), or a live track's `clock` option (`./track.ts`, issue #1396). */
261
261
  clock?: (dt: number, moved: number) => number;
262
262
  /** A plant only (`CO26`): `false` fires no `reset` key across the wrap — the reading the walk was measured against and rejected. */
263
263
  wrapResets?: boolean;
@@ -561,6 +561,20 @@ export function physicsClockStep(step: number): number {
561
561
  return step + step * PHYSICS_CLOCK_LEAD;
562
562
  }
563
563
 
564
+ /**
565
+ * A live track's `clock` option (issue #1396, `./track.ts` and `./tracks.ts`):
566
+ * the caller's function, called with each step, its answer checked before the
567
+ * skeleton's clock moves by it — a finite time at or above 0, or the step
568
+ * refused by name, as a step itself is. `who` names the track in the refusal.
569
+ */
570
+ export function checkedClock(clock: (dt: number) => number, who: string): (dt: number) => number {
571
+ return (dt) => {
572
+ const moved = clock(dt);
573
+ if (typeof moved !== 'number' || !Number.isFinite(moved) || moved < 0) throw new CoreInputError(`${who} clock moved the physics by ${String(moved)} for a step of ${dt}, not a finite time at or above 0`);
574
+ return moved;
575
+ };
576
+ }
577
+
564
578
  /**
565
579
  * The animation time a looping track applies its animation at (`./walk.ts`,
566
580
  * *The time*): the track time wrapped by the duration, and 0 over a duration
package/src/core/track.ts CHANGED
@@ -26,7 +26,8 @@
26
26
  *
27
27
  * - **Held** (`loop: false`): the raw entry's walk — the track time the
28
28
  * running sum of the steps, the animation time held at the duration, the
29
- * physics clock moved by the step, events over `(previous animation time,
29
+ * physics clock moved by the step (or by `clock`, *The physics clock*
30
+ * below), events over `(previous animation time,
30
31
  * animation time]` (`./raw.ts`, *One step*). `CO46` holds it to
31
32
  * `poseRawAnimation` pose for pose, every field, bit for bit.
32
33
  * - **Looped** (`loop: true`): A10's looping walk's time, clock and `reset`
@@ -135,6 +136,44 @@
135
136
  * children, the meshes they carry and the physics do not.
136
137
  * - **Pose 0 not adjusted** under `reset: 'animation'`.
137
138
  *
139
+ * ## The physics clock (issue #1396)
140
+ *
141
+ * `clock` is what each step moves the skeleton's physics clock by — the time
142
+ * a physics constraint integrates — with `poseRawAnimationEach`'s meaning.
143
+ * Absent, it is the step itself, and every pose is the one the track posed
144
+ * before the option existed (`CO46`–`CO50`, unchanged). `physicsClockStep` is
145
+ * the render's clock (issue #1380), for a player stepping at a fixed rate.
146
+ * The track time, the animation time, the wrap, the events and `trackTime`
147
+ * stay on the running sum of the steps: the clock moves the physics and
148
+ * nothing else.
149
+ *
150
+ * - **Why the track takes it.** Stepped at a fixed rate whose frame lasts a
151
+ * whole number of physics steps, a spring shows the render's defect. On the
152
+ * public trailer's `show`, read with play#103's detector, the frame clock
153
+ * adds outlier poses 5, 11–13, 44–45 and 187–189 at a fixed 1/20 and 8,
154
+ * 15–17, 31–33 and 505–507 at 1/60, and `clock: physicsClockStep` removes
155
+ * every one (the PR of issue #1396 carries the table). A player scaling its
156
+ * own `dt` instead moves the animation time too: a key at 0.5 s fires on
157
+ * the tenth step of 1/20 where the render and the runtime fire it on the
158
+ * eleventh, and the player stops agreeing with `rigc render`.
159
+ * - **The reset.** Pose 0 under `reset: 'animation'` is a step of 0 and moves
160
+ * the clock by `clock(0)`, as the render's walk does — 0 for
161
+ * `physicsClockStep`; under `setup` nothing is stepped. A physics `reset`
162
+ * key restarts a constraint's time from the clock as it stands, so it is
163
+ * read on the same clock as every other step.
164
+ * - **The seek** stays the caller's — open, then step to the time — and its
165
+ * steps go through the same clock, so a seek's physics is the physics of
166
+ * the steps it took.
167
+ * - **A clock that returns no time** — not finite, or below 0 — refuses the
168
+ * step by name after the track time has moved, so the track stays refused.
169
+ *
170
+ * `CO53` holds the mechanism on LS05's spring probe (every constraint update
171
+ * takes the steps exact arithmetic takes; the frame clock is red), `CO54` the
172
+ * poses (equal to the render's walk, to `poseRawAnimationEach` on the same
173
+ * clock, between the two live entries, and to spine-core with its
174
+ * `AnimationState` updated by the step and its `Skeleton` by the clock), and
175
+ * `CO55` that the times and the events are those of the walk without it.
176
+ *
138
177
  * ## The oracle chain
139
178
  *
140
179
  * Nothing here is held to spine-core directly but the events: the live track
@@ -153,15 +192,17 @@
153
192
  * history, so a seek is "open, then step to the time".
154
193
  */
155
194
  import { CoreInputError, type CompiledDocument } from './index.ts';
156
- import { openRawTrack, stepRawTrack, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
195
+ import { checkedClock, openRawTrack, stepRawTrack, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
157
196
  import type { LocalAdjust, TimelinePlant } from './animation.ts';
158
197
  export type { AdjustableLocals, BoneLocals, LocalAdjust } from './animation.ts';
159
198
 
160
- /** How a live track is opened: whether it loops, and where its physics is reset (`RawReset`) — both the caller's, neither with a default — and the caller's adjustment of every pose's bone locals (the header's *The adjustment*). */
199
+ /** How a live track is opened: whether it loops, and where its physics is reset (`RawReset`) — both the caller's, neither with a default — the caller's adjustment of every pose's bone locals (the header's *The adjustment*), and what each step moves the physics clock by (the header's *The physics clock*). */
161
200
  export interface TrackOptions {
162
201
  loop: boolean;
163
202
  reset: RawReset;
164
203
  adjust?: LocalAdjust;
204
+ /** What each step of `dt` moves the skeleton's physics clock by — `physicsClockStep` is the render's; absent, the step itself. The track time, the animation time and the events stay on the running sum of the steps. */
205
+ clock?: (dt: number) => number;
165
206
  }
166
207
 
167
208
  /** A live track (`openTrack`): pose 0, then one pose per `step`. */
@@ -191,7 +232,10 @@ export function openTrack(doc: CompiledDocument, animation: string, options: Tra
191
232
  if (typeof options !== 'object' || options === null || typeof options.loop !== 'boolean') throw new CoreInputError(`the live track's options.loop is ${JSON.stringify((options as Partial<TrackOptions> | null)?.loop)}, not true or false`);
192
233
  if (options.reset !== 'animation' && options.reset !== 'setup') throw new CoreInputError(`the live track's options.reset is ${JSON.stringify(options.reset)}, not "animation" or "setup"`);
193
234
  if (options.adjust !== undefined && typeof options.adjust !== 'function') throw new CoreInputError(`the live track's options.adjust is ${typeof options.adjust}, not a function`);
194
- const { track, first } = openRawTrack(doc, animation, plant, options.reset, options.loop ? LOOPED : HELD, options.adjust);
235
+ if (options.clock !== undefined && typeof options.clock !== 'function') throw new CoreInputError(`the live track's options.clock is ${typeof options.clock}, not a function`);
236
+ const mode = options.loop ? LOOPED : HELD;
237
+ const clock = options.clock === undefined ? undefined : checkedClock(options.clock, 'the live track\'s');
238
+ const { track, first } = openRawTrack(doc, animation, plant, options.reset, clock === undefined ? mode : { ...mode, clock: (dt) => clock(dt) }, options.adjust);
195
239
  return new Track(track, first);
196
240
  }
197
241
 
@@ -73,7 +73,12 @@
73
73
  * out fires none** — not "until the mix passes half way": `eventThreshold`
74
74
  * defaults to 0 and an entry's events are applied only while `mix <
75
75
  * eventThreshold`, which no mix is. `complete` is not carried (issue #1276).
76
- * - **Physics** steps on top of the mixed locals, as on one track.
76
+ * - **Physics** steps on top of the mixed locals, as on one track — and on
77
+ * the same clock: `TracksOptions.clock`, what each step moves the physics
78
+ * clock by, has the single track's meaning (`./track.ts`, *The physics
79
+ * clock*, issue #1396). The entries' track times, the mixes, the queue and
80
+ * the events stay on the steps; `start()` under `reset: 'animation'` moves
81
+ * the clock by `clock(0)`.
77
82
  *
78
83
  * `CO51` holds all of it to spine-core 4.3.13 at tolerance 0 — every bone's
79
84
  * world transform, every drawn attachment's world vertices, every slot's
@@ -159,13 +164,15 @@ import { activeBones, CoreInputError, rawNumber, readColour, shownAttachment, ty
159
164
  import { channelAt, keyIndexAt, SLOT_COLOUR_TIMELINES, slotRowsFrom, steppedLocalsWorld, type BoneTimelineKind, type CoreKey, type LocalAdjust, type SlotBase, type SlotColourKind, type TimelinePlant } from './animation.ts';
160
165
  import { deformAt, setupArray, type CoreAttachmentTimeline } from './deform.ts';
161
166
  import { eventsFired, type CoreEventKey, type CoreEventRow } from './events.ts';
162
- import { openRawTracksPath, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
167
+ import { checkedClock, openRawTracksPath, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
163
168
  import { slotTimelinesApply } from './skins.ts';
164
169
 
165
- /** How the live tracks are opened: where the physics is reset (`RawReset`, the caller's, no default) and the caller's adjustment of every pose's bone locals. */
170
+ /** How the live tracks are opened: where the physics is reset (`RawReset`, the caller's, no default), the caller's adjustment of every pose's bone locals, and what each step moves the physics clock by (the header's *The physics clock*). */
166
171
  export interface TracksOptions {
167
172
  reset: RawReset;
168
173
  adjust?: LocalAdjust;
174
+ /** What each step of `dt` moves the skeleton's physics clock by — `physicsClockStep` is the render's; absent, the step itself. Every entry's track time, the mixes, the queue and the events stay on the steps. */
175
+ clock?: (dt: number) => number;
169
176
  }
170
177
 
171
178
  /**
@@ -1299,7 +1306,9 @@ function delayOf(v: number, call: string): number {
1299
1306
  export function openTracks(doc: CompiledDocument, options: TracksOptions, plant: TracksPlant = {}): LiveTracks {
1300
1307
  if (typeof options !== 'object' || options === null || (options.reset !== 'animation' && options.reset !== 'setup')) throw new CoreInputError(`the live tracks' options.reset is ${JSON.stringify((options as Partial<TracksOptions> | null)?.reset)}, not "animation" or "setup"`);
1301
1308
  if (options.adjust !== undefined && typeof options.adjust !== 'function') throw new CoreInputError(`the live tracks' options.adjust is ${typeof options.adjust}, not a function`);
1302
- return new Tracks(doc, options.reset, options.adjust, plant, openRawTracksPath(doc, plant));
1309
+ if (options.clock !== undefined && typeof options.clock !== 'function') throw new CoreInputError(`the live tracks' options.clock is ${typeof options.clock}, not a function`);
1310
+ const clock = options.clock === undefined ? undefined : checkedClock(options.clock, 'the live tracks\'');
1311
+ return new Tracks(doc, options.reset, options.adjust, clock, plant, openRawTracksPath(doc, plant));
1303
1312
  }
1304
1313
 
1305
1314
  class Tracks implements LiveTracks {
@@ -1313,6 +1322,7 @@ class Tracks implements LiveTracks {
1313
1322
  private readonly doc: CompiledDocument,
1314
1323
  private readonly reset: RawReset,
1315
1324
  private readonly adjust: LocalAdjust | undefined,
1325
+ private readonly clock: ((dt: number) => number) | undefined,
1316
1326
  private readonly plant: TracksPlant,
1317
1327
  private readonly path: RawTracksPath,
1318
1328
  ) {
@@ -1426,7 +1436,7 @@ class Tracks implements LiveTracks {
1426
1436
  const { doc, path } = this;
1427
1437
  const ctx = path.ctx;
1428
1438
  const before = ctx.time;
1429
- ctx.time += dt;
1439
+ ctx.time += this.clock === undefined ? dt : this.clock(dt);
1430
1440
  const pose = new Pose(doc, this.plant);
1431
1441
  const animationTime = this.mixer.apply(pose);
1432
1442
  const mixed = new Map(doc.slots.map((s, k) => [s.name, { deform: pose.slots[k].deform, seen: pose.slots[k].seen }]));