spine-rigc 2.47.0 → 2.47.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.47.0",
3
+ "version": "2.47.1",
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
@@ -496,13 +496,71 @@ export function poseRawAnimation(doc: CompiledDocument, animation: string, steps
496
496
  * A refusal is thrown from the pose it is met at, after `visit` has seen the
497
497
  * poses before it. `clock`, when given, is what each step moves the physics
498
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
499
+ * (`physicsClockStep` below, issue #1380); the track time and
500
500
  * the animation time are the steps' running sum either way.
501
501
  */
502
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
503
  walkEach(doc, animation, steps, plant, reset, clock === undefined ? RAW_MODE : { ...RAW_MODE, clock: (dt) => clock(dt) }, RAW_SHAPE, visit);
504
504
  }
505
505
 
506
+ /**
507
+ * How much faster than the frames a render's physics clock runs — and, since issue
508
+ * #1389, a mesh comparison's (`src/meshcompare.ts`, `comparisonWalk`): each frame
509
+ * moves the skeleton's clock — the time a physics constraint integrates — by
510
+ * `physicsClockStep(1 / fps)`, the frame's step and `2⁻³⁰` of it more, while
511
+ * the animation is applied at the running sum of the frame steps as before
512
+ * (issue #1380).
513
+ *
514
+ * ⭐ **Why a render's physics clock is not the frame clock.** A physics
515
+ * constraint keeps the time it has not yet integrated (`remaining`), takes
516
+ * whole steps of `1 / fps` (its own `fps`, 60 by default) while at least one
517
+ * step remains, and shows `offset − lag·(1 − remaining / step)`, `lag` being
518
+ * everything this update's steps moved (`src/core/constraints_physics.ts`,
519
+ * *The step*). Whenever a frame lasts a whole number of physics steps — 12,
520
+ * 20, 30 and 60 fps against 60 Hz, and every second frame at 24 — the time
521
+ * left at the frame's end is zero in exact arithmetic, and in doubles it is
522
+ * either a few ulps above zero or a few ulps short of one whole step. Which of
523
+ * the two is decided by rounding, not by the motion: the shown weight is 1 or
524
+ * 0, so the spring is drawn one frame late or on time, and it changes regime
525
+ * on a single frame. Measured on the public trailer (`show`, 20 fps, 321
526
+ * frames): every spring jumps at frames 5, 12, 45 and 188 — the four frames
527
+ * that arithmetic alone predicts — and the loop's start (frame 80, on time)
528
+ * and end (frame 320, a frame late, which is what exact arithmetic draws)
529
+ * differ by 31.70 units of vertex and 10.37 of bone
530
+ * while every keyed bone agrees to 1.3e-12. spine-core 4.3.13 stepped the
531
+ * same way draws the same frames to the bit: this is the runtime under a
532
+ * fixed offline step, not a defect of the core's port, which is why the fix
533
+ * is the render's schedule and the physics is untouched.
534
+ *
535
+ * Running the clock `2⁻³⁰` fast puts the time left a little ABOVE every
536
+ * whole number of steps instead of on it, by `2⁻³⁰ · t` after `t` seconds:
537
+ * 1.5e-8 s at 16 s, against a rounding error of at most a few ulps of the
538
+ * clock (3.6e-15 s at 16 s) — so every tie resolves the way exact arithmetic
539
+ * resolves it, a step taken, and the shown weight sits at 1 − 9e-7 rather
540
+ * than flipping. It stays below the smallest gap a step boundary can fall
541
+ * into for 7.8e5 s even at 23 fps against 60 Hz (a gap of 1/1380 s), and a
542
+ * physics `reset` key, which restarts the time left from zero, is resolved
543
+ * the same way on every frame after it, because the lead is taken on every
544
+ * frame rather than once (read off the clock arithmetic with a reset at frame
545
+ * 100, 16 s at the five rates: no tie; no tree probe keys a reset under the
546
+ * render's clock). A pose stays a function of its frame
547
+ * index alone: the lead does not depend on how many frames are asked for.
548
+ *
549
+ * Rejected, measured over the same arithmetic (16 s at 12, 20, 24, 25, 30 and
550
+ * 60 fps): the clock as `i / fps` rather than a running sum (90 frames still
551
+ * flipped at 20 fps); a one-time lead of half a step (114 flipped at 24 fps,
552
+ * where half a step lands on the other tie); a one-time lead of any size,
553
+ * which a `reset` key cancels; and snapping `remaining` inside the physics,
554
+ * which changes the runtime's physics rather than the render's schedule and
555
+ * which spine-core's poser could not follow.
556
+ */
557
+ export const PHYSICS_CLOCK_LEAD = 2 ** -30;
558
+
559
+ /** What one frame of `step` seconds moves a render's physics clock by — `PHYSICS_CLOCK_LEAD`; both render posers and the mesh comparison step with it. */
560
+ export function physicsClockStep(step: number): number {
561
+ return step + step * PHYSICS_CLOCK_LEAD;
562
+ }
563
+
506
564
  /**
507
565
  * The animation time a looping track applies its animation at (`./walk.ts`,
508
566
  * *The time*): the track time wrapped by the duration, and 0 over a duration
package/src/folds.ts CHANGED
@@ -170,6 +170,8 @@ export interface FoldPlant {
170
170
  boxWholeMesh?: boolean;
171
171
  /** Size the folded triangles by their unsigned area as posed at the frame, rather than at the setup pose. */
172
172
  areaPosed?: boolean;
173
+ /** Read degeneracy against this share of the largest coordinate rather than `DEGENERATE_SHARE` — 0 drops the line (issue #1390). */
174
+ degenerateShare?: number;
173
175
  }
174
176
 
175
177
  /** Twice the signed area of triangle `t` in `xy` (`x, y` per vertex). */
@@ -201,12 +203,13 @@ function magnitude(xy: readonly number[]): number {
201
203
 
202
204
  /**
203
205
  * The sign of triangle `t`'s area in `xy`: +1, −1, or 0 when it is degenerate
204
- * — its height over its longest edge at most `DEGENERATE_SHARE` of `scale`.
206
+ * — its height over its longest edge at most `share` (`DEGENERATE_SHARE`
207
+ * unplanted) of `scale`.
205
208
  */
206
- function orientation(xy: readonly number[], triangles: readonly number[], t: number, scale: number): -1 | 0 | 1 {
209
+ function orientation(xy: readonly number[], triangles: readonly number[], t: number, scale: number, share: number): -1 | 0 | 1 {
207
210
  const twice = cross2(xy, triangles, t);
208
211
  const edge = longestEdge(xy, triangles, t);
209
- if (edge === 0 || Math.abs(twice) / edge <= DEGENERATE_SHARE * scale) return 0;
212
+ if (edge === 0 || Math.abs(twice) / edge <= share * scale) return 0;
210
213
  return twice > 0 ? 1 : -1;
211
214
  }
212
215
 
@@ -220,6 +223,7 @@ export interface SetupReading {
220
223
  /** The setup reading of one mesh — see the module comment for what degenerate and inverted mean. */
221
224
  export function setupReading(rest: FoldRest, plant: FoldPlant = {}): SetupReading {
222
225
  const count = Math.floor(rest.triangles.length / 3);
226
+ const share = plant.degenerateShare ?? DEGENERATE_SHARE;
223
227
  const signs = new Int8Array(count);
224
228
  const world: number[] = [];
225
229
  const uvSigns: number[] = [];
@@ -229,8 +233,8 @@ export function setupReading(rest: FoldRest, plant: FoldPlant = {}): SetupReadin
229
233
  let sense = 0;
230
234
  const degenerate: number[] = [];
231
235
  for (let t = 0; t < count; t++) {
232
- const w = orientation(rest.vertices, rest.triangles, t, worldScale);
233
- const u = uvs.length === rest.vertices.length ? orientation(uvs, rest.triangles, t, uvScale) : 1;
236
+ const w = orientation(rest.vertices, rest.triangles, t, worldScale, share);
237
+ const u = uvs.length === rest.vertices.length ? orientation(uvs, rest.triangles, t, uvScale, share) : 1;
234
238
  world.push(w);
235
239
  uvSigns.push(u);
236
240
  if (w === 0 || u === 0) {
@@ -263,11 +267,12 @@ export function foldedAt(rest: FoldRest, setup: SetupReading, vertices: readonly
263
267
  );
264
268
  }
265
269
  const scale = magnitude(vertices);
270
+ const share = plant.degenerateShare ?? DEGENERATE_SHARE;
266
271
  const out: number[] = [];
267
272
  for (let t = 0; t < setup.signs.length; t++) {
268
273
  const before = setup.signs[t];
269
274
  if (before === 0) continue;
270
- const now = orientation(vertices, rest.triangles, t, scale);
275
+ const now = orientation(vertices, rest.triangles, t, scale, share);
271
276
  if (now === 0) continue;
272
277
  if (plant.signFlipped ? now === before : now === -before) out.push(t);
273
278
  }
@@ -66,7 +66,11 @@
66
66
  * on every walk and stepped at the declared `dt` to each frame (the oracle's
67
67
  * `stepSchedule` rule: from the last frame, steps of `dt` while before the
68
68
  * next, then to it), the same steps for the reference and every candidate. A
69
- * nonzero `warmupSteps` is refused, never run as zero.
69
+ * nonzero `warmupSteps` is refused, never run as zero. The physics clock
70
+ * moves by `physicsClockStep` of each step, the render's clock
71
+ * (`PHYSICS_CLOCK_LEAD` in `src/core/raw.ts`, issue #1380), so a step
72
+ * lasting a whole number of a constraint's steps takes them as exact
73
+ * arithmetic does (`comparisonWalk`, issue #1389).
70
74
  *
71
75
  * ## What it never does
72
76
  *
@@ -106,8 +110,8 @@ import {
106
110
  import { areaBand, stretchSingularValues, triangleAreas } from './areaband.ts';
107
111
  import { uvCarriers, uvCarriersWith, type Carrier, type CarrierPlant } from './meshcarriers.ts';
108
112
  import { CoreInputError, readModel, underNoSkin, underSkin, type CompiledDocument } from './core/index.ts';
109
- import { poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
110
- import { sampleTime } from './core/animation.ts';
113
+ import { physicsClockStep, poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
114
+ import { sampleTime, type TimelinePlant } from './core/animation.ts';
111
115
  import { CORE_DEFAULT_SKIN } from './core/skins.ts';
112
116
  import { readVersion } from './package_meta.ts';
113
117
  import { hypot2 } from './numeric.ts';
@@ -187,7 +191,9 @@ export interface MotionComparisonInput {
187
191
  * Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
188
192
  * negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
189
193
  * before the builds are read; `null` echoed for a field left out; and the load counted as a required row, so the
190
- * amplitude moves a verdict. `compareMeshesInMotion` plants none. Issue #1323 adds `CarrierPlant`
194
+ * amplitude moves a verdict. `compareMeshesInMotion` plants none. Issue #1389 adds `'frame-physics-clock'`: every
195
+ * build posed with the physics clock moved by the schedule's step itself, the walk before that issue
196
+ * (`comparisonWalk`). Issue #1323 adds `CarrierPlant`
191
197
  * (`src/meshcarriers.ts`): a fault in resolving a sample's several carriers, planted in every `uvCarriers` the
192
198
  * comparison reads.
193
199
  */
@@ -197,6 +203,7 @@ export type ComparePlant =
197
203
  | 'amplitude-unvalidated'
198
204
  | 'echo-when-unset'
199
205
  | 'amplitude-gates-acceptance'
206
+ | 'frame-physics-clock'
200
207
  | OverBoundPlant
201
208
  | CarrierPlant;
202
209
 
@@ -594,7 +601,37 @@ function drawnOf(pose: RawPose, ref: AttachmentRef): number[] | null {
594
601
  return row === undefined ? null : row.vertices;
595
602
  }
596
603
 
597
- function poseBuild(doc: CompiledDocument, refs: readonly AttachmentRef[], frames: readonly Planned[], walks: readonly Walk[]): Posed | { refused: string } {
604
+ /**
605
+ * The walk every build of a comparison is posed on (issue #1389): `poseRawAnimationEach`, reset at the animation, the
606
+ * physics clock moved by `physicsClockStep` of each step — the render's clock (`src/core/raw.ts`). A walk time that
607
+ * ends on a whole number of a physics constraint's steps leaves zero time over in exact arithmetic and a few ulps
608
+ * either side of zero in doubles, so with the step itself as the clock the constraint took one step too few or one
609
+ * too many by rounding. Measured over the clock arithmetic on 300 durations as the core reads them (float32, as Spine
610
+ * does: n/12 s to 20 s and tenths to 8 s): at 12 fps and `dt` 1/12, the rate `@rig-c/parts` walks, 11,248 of 44,486
611
+ * grid updates sit on a tie against a 60 Hz constraint and the frame clock happens to resolve every one as exact
612
+ * arithmetic does, while against 30, 24 and 50 Hz it mis-steps 3,274, 3,350 and 3,160 of them; at 4 fps and `dt` 1/60
613
+ * it mis-steps 19,876 grid and 594 irr updates against 60 Hz. The render's clock mis-steps none of any, so no time
614
+ * that is not a tie came within its lead of a whole step. `clock: 'frame'` is that walk, the step itself — `ComparePlant`
615
+ * `'frame-physics-clock'` and `MQ178`'s planted red. With no physics constraint the clock is read by nothing, so every
616
+ * pose is the same either way (`MQ179`); with one, the lead moves the shown lag weight by about `2⁻³⁰` of the time
617
+ * walked even where no step moves: on `MQ179`'s spring at parts' rate and 60 Hz, one unit of a row's sixth decimal,
618
+ * which can move the worst sample to a neighbour that nearly ties it.
619
+ */
620
+ export function comparisonWalk(doc: CompiledDocument, animation: string, deltas: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void, clock: 'render' | 'frame' = 'render'): void {
621
+ poseRawAnimationEach(doc, animation, deltas, plant, 'animation', visit, clock === 'render' ? physicsClockStep : undefined);
622
+ }
623
+
624
+ /**
625
+ * The walks `schedule` plans over the model document `model`, in walk order, each with the steps every build is posed
626
+ * on — `planSchedule`'s, read for the `mesh-compare` suite's `MQ178`. Internal: merely exported, so not promised; it
627
+ * reads the schedule without the input checks `compareMeshesInMotion` runs first.
628
+ */
629
+ export function comparisonWalks(schedule: MotionSchedule, model: string): Array<{ animation: string; phase: 'grid' | 'irr' | null; deltas: number[] }> {
630
+ const doc = readModel(model, 'the model document');
631
+ return planSchedule(schedule, doc, doc.constraints.filter((c) => c.kind === 'physics').length).walks.map((w) => ({ animation: w.animation, phase: w.phase, deltas: [...w.deltas] }));
632
+ }
633
+
634
+ function poseBuild(doc: CompiledDocument, refs: readonly AttachmentRef[], frames: readonly Planned[], walks: readonly Walk[], clock: 'render' | 'frame'): Posed | { refused: string } {
598
635
  try {
599
636
  const view = viewOf(doc, refs[0].skin);
600
637
  const setupPose = poseRawSetup(view);
@@ -612,12 +649,19 @@ function poseBuild(doc: CompiledDocument, refs: readonly AttachmentRef[], frames
612
649
  list.push(i);
613
650
  wanted.set(f.pose, list);
614
651
  });
615
- poseRawAnimationEach(view, w.animation, w.deltas, {}, 'animation', (pose, index) => {
616
- const at = wanted.get(index);
617
- if (at === undefined) return;
618
- const drawn = refs.map((r) => drawnOf(pose, r));
619
- for (const i of at) out[i] = drawn;
620
- });
652
+ comparisonWalk(
653
+ view,
654
+ w.animation,
655
+ w.deltas,
656
+ {},
657
+ (pose, index) => {
658
+ const at = wanted.get(index);
659
+ if (at === undefined) return;
660
+ const drawn = refs.map((r) => drawnOf(pose, r));
661
+ for (const i of at) out[i] = drawn;
662
+ },
663
+ clock,
664
+ );
621
665
  });
622
666
  return { setup, frames: out, setupBones };
623
667
  } catch (err) {
@@ -1003,11 +1047,12 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
1003
1047
  const carriersOf = (uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> =>
1004
1048
  uvCarriersWith(uvs, triangles, samples, who, carrierPlant);
1005
1049
  const refCarriers = order.map((i, k) => carriersOf(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
1006
- const posedRef = poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks);
1050
+ const clock = plant === 'frame-physics-clock' ? 'frame' : 'render';
1051
+ const posedRef = poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks, clock);
1007
1052
 
1008
1053
  motionOf = (b: Build, isReference: boolean) => {
1009
1054
  const carriers = isReference ? refCarriers : order.map((i, k) => carriersOf(b.meshes[i].uvs, b.meshes[i].triangles, samplesOf[k].samples, `candidate "${b.id}", attachment ${nameOf(refs[i])}`));
1010
- const posed = isReference ? posedRef : poseBuild(b.doc, order.map((i) => refs[i]), frames, walks);
1055
+ const posed = isReference ? posedRef : poseBuild(b.doc, order.map((i) => refs[i]), frames, walks, clock);
1011
1056
  const built: Built[] = [];
1012
1057
  const perFrame: NonNullable<CandidateReport['perFrame']> = [];
1013
1058
  // Issue #1315: kept for a candidate only, and only when asked — a call without the field does none of this.
@@ -30,7 +30,7 @@
30
30
  * the same walk handing each pose over as it is posed (`CORE_RAW_WALK`), so
31
31
  * one pose is held at a time. Since issue #1380 the physics clock — and
32
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
33
+ * (`PHYSICS_CLOCK_LEAD`, `./core/raw.ts`), in both posers: `CR03` still holds the walk
34
34
  * under the step itself, and the loop-seam suite's `LS06` holds the two
35
35
  * posers equal under the render's clock.
36
36
  * - **Vertices, triangles, bones, slot colours, draw order** are the raw
@@ -100,7 +100,7 @@ import { atlasRegionLookup, parseAtlasText } from './atlas.ts';
100
100
  import { pagesOfAtlas, spineFileSha256, type ModelPage } from './model.ts';
101
101
  import { clipThrough, type ClipShape, type ShapeClipper } from './core/clipping.ts';
102
102
  import { activeBones, CoreInputError, readModel, sourceOfDoc, underNoSkin, underSkin, type CompiledDocument, type CoreSlotRow } from './core/index.ts';
103
- import { poseRawAnimationEach, poseRawSetup, type RawDrawn, type RawPose } from './core/raw.ts';
103
+ import { PHYSICS_CLOCK_LEAD, physicsClockStep, poseRawAnimationEach, poseRawSetup, type RawDrawn, type RawPose } from './core/raw.ts';
104
104
  import type { TimelinePlant } from './core/animation.ts';
105
105
  import { CORE_DEFAULT_SKIN, lookupSkins } from './core/skins.ts';
106
106
  import { documentPageLookup, drawnRegions, meshPageUvs, readUvSequences, regionPageUvs, type DrawnRegion, type UvRegion, type UvSource } from './core/uvs.ts';
@@ -369,61 +369,10 @@ export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: read
369
369
  export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit) => poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit, physicsClockStep);
370
370
 
371
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.
372
+ * The render's physics clock (issue #1380): defined beside the walk that takes it (`./core/raw.ts`), where the mesh
373
+ * comparison (`./meshcompare.ts`, issue #1389) reads the same clock without this module's closure.
420
374
  */
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
- }
375
+ export { PHYSICS_CLOCK_LEAD, physicsClockStep };
427
376
 
428
377
  /** What a suite passes `corePoser` in place of the poser's own parts — `PieceArrays` (`RC43`) and the raw walk (`RC44`). */
429
378
  export interface CorePoserPlants {