spine-rigc 2.45.1 → 2.45.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.
package/docs/AUTHORING.md CHANGED
@@ -4906,6 +4906,21 @@ file can be read; it is a **key** outside its bound that `build` refuses (§4.4)
4906
4906
  number: a non-number would reach the file as `null`, which the runtime reads as
4907
4907
  **zero**, so `"mass": "heavy"` would ship a constraint that never settles.
4908
4908
 
4909
+ ⚠️ **What `rigc render` draws a spring with is not a player's clock to the bit.** A
4910
+ constraint integrates whole steps of `1 / fps` and shows the rest as a lag weight, so
4911
+ a frame lasting a whole number of steps (12, 20, 30 or 60 fps against the default
4912
+ 60, every second frame at 24) leaves zero in exact arithmetic and a few ulps either
4913
+ side of it in doubles — and spine-core stepped by `1/fps` per frame lets the rounding
4914
+ decide, drawing every spring a frame late or on time and switching between the two on
4915
+ single frames (issue #1380: frames 5, 12, 45 and 188 of a 20 fps render, and a loop
4916
+ whose two ends sat in different regimes, 31.7 units apart). `render`, and everything
4917
+ that poses through its posers (`check`, `bonedist`, `inspect`, `--geometry`), runs
4918
+ the physics clock `2⁻³⁰` faster than the frames, so every such frame takes the steps
4919
+ exact arithmetic takes; the animation time is unchanged. A spring that has converged
4920
+ by the loop point therefore closes, which the loop-seam suite's `LS06` holds at all
4921
+ five rates. A player with a real clock is a different question and rigc does not
4922
+ answer it.
4923
+
4909
4924
  `mix` is a player-side `AnimationStateData` config and is **not** emitted into
4910
4925
  skeleton JSON, so the parser is the only check it gets: it is
4911
4926
  `{ "default": <seconds>, "pairs"?: [["<from>", "<to>", <seconds>], …] }`, and a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "2.45.1",
3
+ "version": "2.45.2",
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/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
- /** A plant only (`CO26`): what the physics clock moves by, from the step and how far the animation time moved — the step itself unless given. */
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). */
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;
@@ -494,10 +494,13 @@ export function poseRawAnimation(doc: CompiledDocument, animation: string, steps
494
494
  * animation's series — on the production rig the core poser's render peaked
495
495
  * highest on, the largest animation's series was 153 MiB of retained heap.
496
496
  * A refusal is thrown from the pose it is met at, after `visit` has seen the
497
- * poses before it.
497
+ * poses before it. `clock`, when given, is what each step moves the physics
498
+ * clock by in place of the step itself — the render's offline schedule
499
+ * (`physicsClockStep` in `src/render_core.ts`, issue #1380); the track time and
500
+ * the animation time are the steps' running sum either way.
498
501
  */
499
- export function poseRawAnimationEach(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, visit: (pose: RawPose, index: number) => void): void {
500
- walkEach(doc, animation, steps, plant, reset, RAW_MODE, RAW_SHAPE, visit);
502
+ export function poseRawAnimationEach(doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, reset: RawReset, visit: (pose: RawPose, index: number) => void, clock?: (dt: number) => number): void {
503
+ walkEach(doc, animation, steps, plant, reset, clock === undefined ? RAW_MODE : { ...RAW_MODE, clock: (dt) => clock(dt) }, RAW_SHAPE, visit);
501
504
  }
502
505
 
503
506
  /**
package/src/render.ts CHANGED
@@ -128,7 +128,7 @@ import { readFileSync } from 'node:fs';
128
128
  // `loadPosedSkeleton`, and kept, the plant reads red for its own reason rather than for a missing name.
129
129
  import { dirname, join } from 'node:path';
130
130
  import { Plate, readPlate } from '../tools/plate.ts';
131
- import { clipSourceOf, corePoser, inactiveBoneSnapshot, subsetOver, unposedBones } from './render_core.ts';
131
+ import { clipSourceOf, corePoser, inactiveBoneSnapshot, physicsClockStep, subsetOver, unposedBones } from './render_core.ts';
132
132
  import { type PosedVertices, type WorldTransform } from './nonfinite.ts';
133
133
  import {
134
134
  atlasScales,
@@ -488,9 +488,10 @@ export type PoseSource = Poser | SkeletonData;
488
488
  * because that is the path a runtime actually takes, and 4.3's
489
489
  * `Animation.apply` takes a `MixFrom` that only the state machine has any
490
490
  * business choosing. Frame 0 applies, updates by 0 and resets physics; every
491
- * later frame is one `state.update(1/fps)`, apply, `skeleton.update(1/fps)` and
491
+ * later frame is one `state.update(1/fps)`, apply, `skeleton.update` and
492
492
  * `Physics.update` — one continuous trajectory, so the track time of frame `i`
493
- * is the sum of `i` steps.
493
+ * is the sum of `i` steps. The skeleton's clock, which only the physics reads,
494
+ * moves by `physicsClockStep(1/fps)` (`PHYSICS_CLOCK_LEAD`, issue #1380).
494
495
  */
495
496
  export function spinePoser(data: SkeletonData): Poser {
496
497
  return {
@@ -507,12 +508,14 @@ export function spinePoser(data: SkeletonData): Poser {
507
508
  state.setAnimation(0, name, false);
508
509
  skeleton.setupPose();
509
510
  const step = 1 / fps;
511
+ // The physics clock runs `PHYSICS_CLOCK_LEAD` fast, so no frame ends on a physics step's tie (issue #1380).
512
+ const clock = physicsClockStep(step);
510
513
  const posed = spinePosed(skeleton);
511
514
  for (let i = 0; i <= count; i++) {
512
515
  if (i > 0) {
513
516
  state.update(step);
514
517
  state.apply(skeleton);
515
- skeleton.update(step);
518
+ skeleton.update(clock);
516
519
  skeleton.updateWorldTransform(Physics.update);
517
520
  } else {
518
521
  state.apply(skeleton);
@@ -28,7 +28,11 @@
28
28
  * frame of the nineteen tree rows by the core suite's `CR03`. The setup pose
29
29
  * is `poseRawSetup`. Since issue #1180 the walk is `poseRawAnimationEach`,
30
30
  * the same walk handing each pose over as it is posed (`CORE_RAW_WALK`), so
31
- * one pose is held at a time.
31
+ * one pose is held at a time. Since issue #1380 the physics clock — and
32
+ * nothing else — moves by `physicsClockStep(1/fps)` rather than by the step
33
+ * (`PHYSICS_CLOCK_LEAD` below), in both posers: `CR03` still holds the walk
34
+ * under the step itself, and the loop-seam suite's `LS06` holds the two
35
+ * posers equal under the render's clock.
32
36
  * - **Vertices, triangles, bones, slot colours, draw order** are the raw
33
37
  * pose's, unchanged: the same doubles `computeWorldVertices`,
34
38
  * `getWorldRotationX` and the slot pose hold (`CR03`).
@@ -361,8 +365,65 @@ export function keptPieceArrays(): PieceArrays {
361
365
  */
362
366
  export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void) => void;
363
367
 
364
- /** The walk `corePoser` steps with unless a plant passes another: one pose posed, drawn and released before the next. */
365
- export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit) => poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit);
368
+ /** The walk `corePoser` steps with unless a plant passes another: one pose posed, drawn and released before the next, the physics clock moved by `physicsClockStep` (issue #1380). */
369
+ export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit) => poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit, physicsClockStep);
370
+
371
+ /**
372
+ * How much faster than the frames a render's physics clock runs: each frame
373
+ * moves the skeleton's clock — the time a physics constraint integrates — by
374
+ * `physicsClockStep(1 / fps)`, the frame's step and `2⁻³⁰` of it more, while
375
+ * the animation is applied at the running sum of the frame steps as before
376
+ * (issue #1380).
377
+ *
378
+ * ⭐ **Why a render's physics clock is not the frame clock.** A physics
379
+ * constraint keeps the time it has not yet integrated (`remaining`), takes
380
+ * whole steps of `1 / fps` (its own `fps`, 60 by default) while at least one
381
+ * step remains, and shows `offset − lag·(1 − remaining / step)`, `lag` being
382
+ * everything this update's steps moved (`src/core/constraints_physics.ts`,
383
+ * *The step*). Whenever a frame lasts a whole number of physics steps — 12,
384
+ * 20, 30 and 60 fps against 60 Hz, and every second frame at 24 — the time
385
+ * left at the frame's end is zero in exact arithmetic, and in doubles it is
386
+ * either a few ulps above zero or a few ulps short of one whole step. Which of
387
+ * the two is decided by rounding, not by the motion: the shown weight is 1 or
388
+ * 0, so the spring is drawn one frame late or on time, and it changes regime
389
+ * on a single frame. Measured on the public trailer (`show`, 20 fps, 321
390
+ * frames): every spring jumps at frames 5, 12, 45 and 188 — the four frames
391
+ * that arithmetic alone predicts — and the loop's start (frame 80, on time)
392
+ * and end (frame 320, a frame late, which is what exact arithmetic draws)
393
+ * differ by 31.70 units of vertex and 10.37 of bone
394
+ * while every keyed bone agrees to 1.3e-12. spine-core 4.3.13 stepped the
395
+ * same way draws the same frames to the bit: this is the runtime under a
396
+ * fixed offline step, not a defect of the core's port, which is why the fix
397
+ * is the render's schedule and the physics is untouched.
398
+ *
399
+ * Running the clock `2⁻³⁰` fast puts the time left a little ABOVE every
400
+ * whole number of steps instead of on it, by `2⁻³⁰ · t` after `t` seconds:
401
+ * 1.5e-8 s at 16 s, against a rounding error of at most a few ulps of the
402
+ * clock (3.6e-15 s at 16 s) — so every tie resolves the way exact arithmetic
403
+ * resolves it, a step taken, and the shown weight sits at 1 − 9e-7 rather
404
+ * than flipping. It stays below the smallest gap a step boundary can fall
405
+ * into for 7.8e5 s even at 23 fps against 60 Hz (a gap of 1/1380 s), and a
406
+ * physics `reset` key, which restarts the time left from zero, is resolved
407
+ * the same way on every frame after it, because the lead is taken on every
408
+ * frame rather than once (read off the clock arithmetic with a reset at frame
409
+ * 100, 16 s at the five rates: no tie; no tree probe keys a reset under the
410
+ * render's clock). A pose stays a function of its frame
411
+ * index alone: the lead does not depend on how many frames are asked for.
412
+ *
413
+ * Rejected, measured over the same arithmetic (16 s at 12, 20, 24, 25, 30 and
414
+ * 60 fps): the clock as `i / fps` rather than a running sum (90 frames still
415
+ * flipped at 20 fps); a one-time lead of half a step (114 flipped at 24 fps,
416
+ * where half a step lands on the other tie); a one-time lead of any size,
417
+ * which a `reset` key cancels; and snapping `remaining` inside the physics,
418
+ * which changes the runtime's physics rather than the render's schedule and
419
+ * which spine-core's poser could not follow.
420
+ */
421
+ export const PHYSICS_CLOCK_LEAD = 2 ** -30;
422
+
423
+ /** What one frame of `step` seconds moves a render's physics clock by — `PHYSICS_CLOCK_LEAD`; both posers step with it. */
424
+ export function physicsClockStep(step: number): number {
425
+ return step + step * PHYSICS_CLOCK_LEAD;
426
+ }
366
427
 
367
428
  /** What a suite passes `corePoser` in place of the poser's own parts — `PieceArrays` (`RC43`) and the raw walk (`RC44`). */
368
429
  export interface CorePoserPlants {