@rig-c/core 2.66.0 → 2.67.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
@@ -5381,6 +5381,39 @@ agreeing with `render` — and left out, a track steps the physics by the step i
5381
5381
  exactly as before. Which clock a player with a real clock uses is the player's
5382
5382
  question, and rigc does not answer it.
5383
5383
 
5384
+ A player's `adjust` option on `openTrack` or `openTracks` receives
5385
+ `(locals, ctx)` (issue #1540). `LocalAdjustContext`, `LocalAdjust`,
5386
+ `AdjustableLocals` and `BoneLocals` are types promised through `rig-c/core`.
5387
+ The callback runs once per evaluated pose **after all track composition**,
5388
+ before world transforms, constraints and physics. A crossfade does not call it
5389
+ for the outgoing entry as well. Existing unary callbacks keep the same writes,
5390
+ pose bytes and call schedule.
5391
+
5392
+ - `ctx.trackTime: number | null` is the **current track-0 entry's** unwrapped
5393
+ seconds from its own start. Loops do not wrap it; a held animation clamps its
5394
+ sampling time while this entry clock continues. Replacing the entry starts its
5395
+ clock at 0 even at a nonzero global time. A queued switch preserves the
5396
+ remainder the mixer has already advanced into its new entry.
5397
+ - `ctx.animation: string | null` names that **same entry**. With no current
5398
+ track-0 entry, both fields are null, even if an upper track plays. An explicit
5399
+ empty entry has a null animation name and its own numeric entry time until
5400
+ removed, matching `current(0)`.
5401
+ - `ctx.globalTime: number` is the running sum of this walk's steps, in seconds.
5402
+ On a single track it equals the entry time. It is separate from entry time in
5403
+ a composition, and is never substituted for an absent track-0 time. Neither
5404
+ time includes the optional physics clock's lead.
5405
+
5406
+ With `reset: 'animation'` the initial pose calls the hook at 0;
5407
+ `reset: 'setup'` leaves its initial setup pose unadjusted. Every explicit
5408
+ `step(0)` still evaluates a pose. This API adds no seek or cache: a caller may
5409
+ reuse a cached pose without asking the core to evaluate it again. Time-dependent
5410
+ adjustments replay exactly with the **same reset, environment and step
5411
+ partition**; arbitrary physics histories are not interchangeable. A callback
5412
+ closing over mutable external state still needs its caller's cache invalidated.
5413
+ For example, `adjust: (locals, ctx) => { locals.bone('dial').rotation =
5414
+ 30 * (ctx.trackTime ?? 0); }` explicitly chooses a value when there is no base
5415
+ entry; the core supplies null rather than inventing that choice.
5416
+
5384
5417
  🌬️ **`wind` and `gravity` are the constraint's response; the world is the
5385
5418
  consumer's** (issue #1416). In 4.3 the two are not forces but how strongly the
5386
5419
  constraint answers two directions the *skeleton* carries — `Skeleton.windX,
@@ -5420,6 +5453,47 @@ skeleton's units and axes, and its change of rotation as `physicsRotate(0, 0,
5420
5453
  Δ)`, before the `step` that poses the frame. Nothing set, every pose is the
5421
5454
  one the track posed before these existed (`CO57`).
5422
5455
 
5456
+ For a live preview of one named constraint (issue #1541), both `LiveTrack` and
5457
+ `LiveTracks` on `rig-c/core` expose:
5458
+
5459
+ ```ts
5460
+ track.setPhysicsParameters('spring', { damping: 0.3, wind: 2 });
5461
+ const { override, effective } = track.readPhysicsParameters('spring');
5462
+ track.clearPhysicsParameters('spring');
5463
+ ```
5464
+
5465
+ `PhysicsParameters` has exactly six numeric fields: `strength`, `damping`,
5466
+ `inertia`, `wind`, `gravity`, `mix`. `PhysicsParameterPatch` is a partial set of
5467
+ them; setting merges with the instance's existing patch, omitted fields keep
5468
+ their overrides, and an empty patch does nothing. Finite numbers are accepted
5469
+ without clamping or unit conversion. The patch applies after the authored,
5470
+ keyed and slider values, immediately before that active constraint's integrator.
5471
+ It persists across poses, loops and composed-entry replacement or mixing out to
5472
+ nothing, until `clearPhysicsParameters(name)` removes the whole named patch.
5473
+ No document, another instance, or another named constraint is changed.
5474
+
5475
+ `PhysicsParameterRead` contains detached snapshots: `override` is the patch
5476
+ currently in force, or `null`; `effective` is the exact six values last passed
5477
+ to that constraint's step, or `null` when it has never been evaluated. In
5478
+ particular composed tracks before `start()` and inactive constraints have no
5479
+ evaluated values. Returning resolved setup defaults there would invent a pose:
5480
+ the parser has read them, but the integrator has not used them. A single track
5481
+ evaluates pose 0 when opened, including under `reset: 'setup'`. Setting or
5482
+ clearing changes `override` immediately, while `effective` keeps describing the
5483
+ previous evaluation until the next pose. No reset or fake evaluation is done to
5484
+ fill a readout. Values omitted by the author come from the existing resolved
5485
+ records; this API defines no second default table.
5486
+
5487
+ Clear resumes whatever authored, keyed and slider values the next pose supplies,
5488
+ and retains the clock, offsets, velocities and remaining integration time. It
5489
+ does not undo motion or promise transforms equal to an untouched trajectory.
5490
+ An unknown constraint name, a name of another kind, an unknown field (including
5491
+ `mass`), or a non-finite/non-number field throws a named `CoreInputError` before
5492
+ any part of the patch changes. Set/read/clear name errors list the relevant
5493
+ name; field errors also name the field and required finite value or allowed
5494
+ field set. The composed tracks' existing refusal of animation constraint
5495
+ timelines is unchanged; this API adds no timeline mixing or solver behavior.
5496
+
5423
5497
  `mix` is a player-side `AnimationStateData` config and is **not** emitted into
5424
5498
  skeleton JSON, so the parser is the only check it gets: it is
5425
5499
  `{ "default": <seconds>, "pairs"?: [["<from>", "<to>", <seconds>], …] }`, and a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/core",
3
- "version": "2.66.0",
3
+ "version": "2.67.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": {
@@ -747,8 +747,18 @@ export interface AdjustableLocals {
747
747
  bone(name: string): BoneLocals;
748
748
  }
749
749
 
750
- /** What `openTrack`'s `adjust` is: called once per pose with the step's locals (`BoneLocals`). */
751
- export type LocalAdjust = (locals: AdjustableLocals) => void;
750
+ /** The pose being adjusted (issue #1540), independent of the physics clock. */
751
+ export interface LocalAdjustContext {
752
+ /** Current track-0 entry's unwrapped seconds, starting at 0; null without that entry. */
753
+ readonly trackTime: number | null;
754
+ /** That same entry's animation name; null without it or for an explicit empty entry. */
755
+ readonly animation: string | null;
756
+ /** Running sum of this walk's steps, in seconds; never the physics clock. */
757
+ readonly globalTime: number;
758
+ }
759
+
760
+ /** Called once per evaluated pose, after track composition and before world transforms and physics. Unary callbacks remain assignable. */
761
+ export type LocalAdjust = (locals: AdjustableLocals, ctx: LocalAdjustContext) => void;
752
762
 
753
763
  /** The seven channels a `BoneLocals` reads and writes, each with the value an unstated one reads as (`worldTransforms`' defaults). */
754
764
  const LOCAL_CHANNELS = { rotation: 0, x: 0, y: 0, scaleX: 1, scaleY: 1, shearX: 0, shearY: 0 } as const;
@@ -789,7 +799,7 @@ class StepLocals implements BoneLocals {
789
799
  }
790
800
 
791
801
  /** The adjustment run over a step's `bones` (`posedBones`' array, in the document's order), which it may replace bone by bone (`BoneLocals`). */
792
- function adjustLocals(doc: CompiledDocument, bones: ModelBone[], adjust: LocalAdjust): void {
802
+ function adjustLocals(doc: CompiledDocument, bones: ModelBone[], adjust: LocalAdjust, context: LocalAdjustContext): void {
793
803
  const views = new Map<string, StepLocals>();
794
804
  const names = doc.bones.map((b) => b.name);
795
805
  adjust({
@@ -805,7 +815,7 @@ function adjustLocals(doc: CompiledDocument, bones: ModelBone[], adjust: LocalAd
805
815
  }
806
816
  return view;
807
817
  },
808
- });
818
+ }, context);
809
819
  }
810
820
 
811
821
  const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
@@ -925,7 +935,8 @@ export function slotRowsFrom(
925
935
  export interface PoseStep {
926
936
  ctx: PhysicsStepContext;
927
937
  before: number;
928
- adjust?: LocalAdjust;
938
+ /** Callback and its authoritative evaluation identity travel together; no inferred clock fallback. */
939
+ adjustment?: { callback: LocalAdjust; context: LocalAdjustContext };
929
940
  }
930
941
 
931
942
  /**
@@ -991,7 +1002,7 @@ function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines,
991
1002
  if (constraints !== undefined && step !== undefined) return steppedLocals(doc, bones, plant, () => pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, sliders, step);
992
1003
  const active = activeBones(doc);
993
1004
  // The caller's adjustment, after the timelines and before anything reads the locals (`LocalAdjust`).
994
- if (step?.adjust !== undefined) adjustLocals(doc, bones, step.adjust);
1005
+ if (step?.adjustment !== undefined) adjustLocals(doc, bones, step.adjustment.callback, step.adjustment.context);
995
1006
  let world = (plant.evaluate ?? worldTransforms)(bones, active);
996
1007
  if (constraints !== undefined) {
997
1008
  const setupRecords = constraintRecords(doc);
@@ -1018,7 +1029,7 @@ function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines,
1018
1029
  function steppedLocals(doc: CompiledDocument, bones: ModelBone[], plant: TimelinePlant, records: () => CoreConstraintRecord[], physicsKeyed: NonNullable<CoreConstraintTimelines['physicsKeyed']>, t: number, sliders: SliderApplication[] | undefined, step: PoseStep): PosedStep {
1019
1030
  const active = activeBones(doc);
1020
1031
  // The caller's adjustment, after the timelines and before anything reads the locals (`LocalAdjust`).
1021
- if (step.adjust !== undefined) adjustLocals(doc, bones, step.adjust);
1032
+ if (step.adjustment !== undefined) adjustLocals(doc, bones, step.adjustment.callback, step.adjustment.context);
1022
1033
  let world = (plant.evaluate ?? worldTransforms)(bones, active);
1023
1034
  const stepped = stepPhysicsRecords(records(), physicsKeyed, t, active, step.ctx, step.before);
1024
1035
  world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(stepped) : stepped, null, sliders, step.ctx, undefined, solverRules(plant.solver));
@@ -1548,7 +1548,8 @@ export function applyConstraints(bones: readonly ModelBone[], world: ReadonlyMap
1548
1548
  // Under Physics.none a physics constraint applies nothing; stepped, it moves its bone in world space (`./constraints_physics.ts`).
1549
1549
  if (physics !== undefined) {
1550
1550
  const w = { ...(state.world.get(c.bone) as CoreWorld) };
1551
- const posed = physicsPose?.records.get(c.name) ?? c;
1551
+ const authored = physicsPose?.records.get(c.name) ?? c;
1552
+ const posed = physics.parameters?.apply(authored) ?? authored;
1552
1553
  if ((physics.step ?? stepPhysics)(posed, w, bone(state, c.bone).length ?? 0, physics)) {
1553
1554
  state.world.set(c.bone, w);
1554
1555
  changed = [c.bone];
@@ -583,6 +583,8 @@ export type PhysicsStepper = (r: CorePhysicsRecord, w: CoreWorld, length: number
583
583
  /** What a stepped pose carries into every physics constraint it applies. */
584
584
  export interface PhysicsStepContext {
585
585
  phase: PhysicsPhase;
586
+ /** Live track's final parameter layer, after authored/keyed/slider values (issue #1541). */
587
+ parameters?: { apply(record: CorePhysicsRecord): CorePhysicsRecord };
586
588
  /** The skeleton's clock: the sum of every step's delta so far. */
587
589
  time: number;
588
590
  /** Each constraint's state, by name, carried from update to update. */
package/src/core/entry.ts CHANGED
@@ -25,7 +25,9 @@
25
25
  * own evaluation rather than to a mirror of it, issue #1417 — re-exported,
26
26
  * never copied, because a copy is the drift the request exists to remove;
27
27
  * `CA12` holds the identity; `readSetupMesh`, issue #1538, the named setup
28
- * geometry read requested by the player, with `SetupMesh` and `SetupMeshTarget`).
28
+ * geometry read requested by the player, with `SetupMesh` and `SetupMeshTarget`;
29
+ * `PhysicsParameters`, `PhysicsParameterPatch`, `PhysicsParameterRead`, the live
30
+ * named constraint overrides requested on issue #1541).
29
31
  * The list is `scripts/install_smoke.ts`'s `OBSERVED_SYMBOLS` rows for
30
32
  * `./core`, and a symbol added here and not there is not promised.
31
33
  */
@@ -35,7 +37,8 @@ export { readSetupMesh, poseRawSetup, poseRawAnimation, poseRawAnimationEach, lo
35
37
  export type { SetupMesh, SetupMeshTarget, RawPose, RawBone, RawDrawn, RawClip, RawClipped, RawEvent, RawReset } from './raw.ts';
36
38
  export { poseWalkSetup, poseLoopingWalk } from './walk.ts';
37
39
  export { openTrack } from './track.ts';
38
- export type { LiveTrack, TrackOptions, AdjustableLocals, BoneLocals, LocalAdjust, PhysicsEnvironment } from './track.ts';
40
+ export type { LiveTrack, TrackOptions, AdjustableLocals, BoneLocals, LocalAdjust, LocalAdjustContext, PhysicsEnvironment } from './track.ts';
41
+ export type { PhysicsParameters, PhysicsParameterPatch, PhysicsParameterRead } from './physics_parameters.ts';
39
42
  export { openTracks } from './tracks.ts';
40
43
  export type { LiveTracks, TracksOptions, MixOptions, QueueOptions, TrackEntryView } from './tracks.ts';
41
44
  export type { WalkPose } from './walk.ts';
@@ -0,0 +1,72 @@
1
+ /** Live, per-instance parameter patches (issue #1541). No model writes or history resets. */
2
+ import { CoreInputError, type CompiledDocument } from './index.ts';
3
+ import type { CorePhysicsRecord } from './constraints_physics.ts';
4
+
5
+ /** The six scalar responses a consumer can override; no units or range conversion. */
6
+ export interface PhysicsParameters {
7
+ readonly strength: number;
8
+ readonly damping: number;
9
+ readonly inertia: number;
10
+ readonly wind: number;
11
+ readonly gravity: number;
12
+ readonly mix: number;
13
+ }
14
+ /** Omitted fields keep their existing override, if any. */
15
+ export type PhysicsParameterPatch = Partial<PhysicsParameters>;
16
+ /** Detached snapshots. `effective` is null until this constraint has been evaluated. */
17
+ export interface PhysicsParameterRead {
18
+ readonly override: PhysicsParameterPatch | null;
19
+ readonly effective: PhysicsParameters | null;
20
+ }
21
+
22
+ const FIELDS = ['strength', 'damping', 'inertia', 'wind', 'gravity', 'mix'] as const;
23
+ const values = (r: CorePhysicsRecord): PhysicsParameters => ({ strength: r.strength, damping: r.damping, inertia: r.inertia, wind: r.wind, gravity: r.gravity, mix: r.mix });
24
+
25
+ /** Owned by one live track, including all entries of a composed track. */
26
+ export class LivePhysicsParameters {
27
+ private readonly overrides = new Map<string, PhysicsParameterPatch>();
28
+ private readonly evaluated = new Map<string, PhysicsParameters>();
29
+
30
+ constructor(private readonly doc: CompiledDocument) {}
31
+
32
+ private named(name: string): void {
33
+ const constraint = this.doc.constraints.find((c) => c.name === name);
34
+ if (constraint === undefined) throw new CoreInputError(`physics parameters: no constraint "${String(name)}"; this document declares [${this.doc.constraints.map((c) => c.name).join(', ')}]`);
35
+ if (constraint.kind !== 'physics') throw new CoreInputError(`physics parameters: constraint "${name}" is ${constraint.kind}, not physics`);
36
+ }
37
+
38
+ set(name: string, patch: PhysicsParameterPatch): void {
39
+ this.named(name);
40
+ if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) throw new CoreInputError(`physics parameters: constraint "${name}" patch is ${String(patch)}, not an object of finite fields [${FIELDS.join(', ')}]`);
41
+ const checked: Partial<Record<(typeof FIELDS)[number], number>> = {};
42
+ // Validate the complete patch before publishing it, including unknown symbol keys.
43
+ for (const key of Reflect.ownKeys(patch)) {
44
+ if (typeof key !== 'string' || !(FIELDS as readonly string[]).includes(key)) throw new CoreInputError(`physics parameters: constraint "${name}" field "${String(key)}" is unknown; expected [${FIELDS.join(', ')}]`);
45
+ const field = key as (typeof FIELDS)[number];
46
+ const value = patch[field];
47
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new CoreInputError(`physics parameters: constraint "${name}" field "${field}" is ${String(value)}, not a finite number`);
48
+ checked[field] = value;
49
+ }
50
+ if (Reflect.ownKeys(checked).length > 0) this.overrides.set(name, { ...this.overrides.get(name), ...checked });
51
+ }
52
+
53
+ read(name: string): PhysicsParameterRead {
54
+ this.named(name);
55
+ const patch = this.overrides.get(name);
56
+ const effective = this.evaluated.get(name);
57
+ return { override: patch === undefined ? null : { ...patch }, effective: effective === undefined ? null : { ...effective } };
58
+ }
59
+
60
+ clear(name: string): void {
61
+ this.named(name);
62
+ this.overrides.delete(name);
63
+ }
64
+
65
+ /** Called only for an active constraint, after the sliders and immediately before its step. */
66
+ apply(record: CorePhysicsRecord): CorePhysicsRecord {
67
+ const patch = this.overrides.get(record.name);
68
+ const posed = patch === undefined ? record : { ...record, ...patch };
69
+ this.evaluated.set(record.name, values(posed));
70
+ return posed;
71
+ }
72
+ }
package/src/core/raw.ts CHANGED
@@ -906,8 +906,9 @@ function stepWalk<P>(track: TrackState<P>, dt: number, first = false): P {
906
906
  // `clipped` rows are empty. The live track's looping mode (`live`) fires its events across the wrap by the rule `./track.ts` states as measured.
907
907
  const fire = plant.events ?? eventsFired;
908
908
  const keys = anim.timelines.events;
909
+ const adjustment = adjust === undefined ? undefined : { callback: adjust, context: { trackTime, animation: anim.name, globalTime: trackTime } };
909
910
  const pose = poseStepFrom(doc, plant, resolve, shape, mode, ctx, first, trackTime, t, {
910
- world: (sliders) => (shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust })),
911
+ world: (sliders) => (shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjustment }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjustment })),
911
912
  slots: (sliders, placeholders) => posedSlots(doc, anim.timelines, t, raw, sliders, placeholders),
912
913
  drawOrder: anim.timelines.drawOrder,
913
914
  at: t,
@@ -1024,8 +1025,8 @@ function walkEach<P>(doc: CompiledDocument, animation: string, steps: readonly n
1024
1025
  * `stepRawTrack` steps. Every pose is assembled as the raw entry's
1025
1026
  * (`RAW_SHAPE`).
1026
1027
  */
1027
- export function openRawTrack(doc: CompiledDocument, animation: string, plant: TimelinePlant, reset: RawReset, mode: WalkMode, adjust?: LocalAdjust): { track: TrackState<RawPose>; first: RawPose } {
1028
- return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE, adjust);
1028
+ export function openRawTrack(doc: CompiledDocument, animation: string, plant: TimelinePlant, reset: RawReset, mode: WalkMode, adjust?: LocalAdjust, before?: (ctx: PhysicsStepContext) => void): { track: TrackState<RawPose>; first: RawPose } {
1029
+ return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE, adjust, before);
1029
1030
  }
1030
1031
 
1031
1032
  /** One step of an opened raw track (`openRawTrack`): `walkIn`'s step, the step itself checked by the caller. */
package/src/core/track.ts CHANGED
@@ -225,7 +225,9 @@ import { CoreInputError, type CompiledDocument } from './index.ts';
225
225
  import { checkedClock, environmentOf, openRawTrack, rotateOn, setEnvironmentOn, stepRawTrack, translateOn, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
226
226
  import type { LocalAdjust, TimelinePlant } from './animation.ts';
227
227
  import type { PhysicsEnvironment } from './constraints_physics.ts';
228
- export type { AdjustableLocals, BoneLocals, LocalAdjust } from './animation.ts';
228
+ import { LivePhysicsParameters, type PhysicsParameterPatch, type PhysicsParameterRead } from './physics_parameters.ts';
229
+ export type { PhysicsParameters, PhysicsParameterPatch, PhysicsParameterRead } from './physics_parameters.ts';
230
+ export type { AdjustableLocals, BoneLocals, LocalAdjust, LocalAdjustContext } from './animation.ts';
229
231
  export type { PhysicsEnvironment } from './constraints_physics.ts';
230
232
 
231
233
  /** 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*). */
@@ -253,6 +255,12 @@ export interface LiveTrack {
253
255
  setEnvironment(environment: Partial<PhysicsEnvironment>): void;
254
256
  /** The environment the next pose answers. */
255
257
  readonly environment: PhysicsEnvironment;
258
+ /** Merges a finite six-field patch for one physics constraint, applied from the next pose until clear. */
259
+ setPhysicsParameters(name: string, patch: PhysicsParameterPatch): void;
260
+ /** Pending patch and the last evaluated six values; null effective means not yet evaluated (e.g. inactive). */
261
+ readPhysicsParameters(name: string): PhysicsParameterRead;
262
+ /** Removes the patch for the next pose without resetting velocities, offsets or the clock. */
263
+ clearPhysicsParameters(name: string): void;
256
264
  /** spine-core's `Skeleton.physicsTranslate`: the consumer moved the whole rig by `(x, y)` in the skeleton's world (y up) since the last pose, and every physics constraint's next update sees it. */
257
265
  physicsTranslate(x: number, y: number): void;
258
266
  /** spine-core's `Skeleton.physicsRotate`: the consumer turned the whole rig by `degrees` (counter-clockwise) about `(x, y)` in the skeleton's world since the last pose. */
@@ -275,8 +283,9 @@ export function openTrack(doc: CompiledDocument, animation: string, options: Tra
275
283
  if (options.clock !== undefined && typeof options.clock !== 'function') throw new CoreInputError(`the live track's options.clock is ${typeof options.clock}, not a function`);
276
284
  const mode = options.loop ? LOOPED : HELD;
277
285
  const clock = options.clock === undefined ? undefined : checkedClock(options.clock, 'the live track\'s');
278
- const { track, first } = openRawTrack(doc, animation, plant, options.reset, clock === undefined ? mode : { ...mode, clock: (dt) => clock(dt) }, options.adjust);
279
- return new Track(track, first);
286
+ const parameters = new LivePhysicsParameters(doc);
287
+ const { track, first } = openRawTrack(doc, animation, plant, options.reset, clock === undefined ? mode : { ...mode, clock: (dt) => clock(dt) }, options.adjust, (ctx) => { ctx.parameters = parameters; });
288
+ return new Track(track, first, parameters);
280
289
  }
281
290
 
282
291
  class Track implements LiveTrack {
@@ -286,7 +295,7 @@ class Track implements LiveTrack {
286
295
  private latest: number;
287
296
  private refused: string | null = null;
288
297
 
289
- constructor(track: TrackState<RawPose>, first: RawPose) {
298
+ constructor(track: TrackState<RawPose>, first: RawPose, private readonly parameters: LivePhysicsParameters) {
290
299
  this.track = track;
291
300
  this.first = first;
292
301
  this.latest = first.animationTime;
@@ -313,6 +322,20 @@ class Track implements LiveTrack {
313
322
  setEnvironmentOn(this.track.ctx, environment, 'the live track\'s');
314
323
  }
315
324
 
325
+ setPhysicsParameters(name: string, patch: PhysicsParameterPatch): void {
326
+ this.live('setPhysicsParameters');
327
+ this.parameters.set(name, patch);
328
+ }
329
+
330
+ readPhysicsParameters(name: string): PhysicsParameterRead {
331
+ return this.parameters.read(name);
332
+ }
333
+
334
+ clearPhysicsParameters(name: string): void {
335
+ this.live('clearPhysicsParameters');
336
+ this.parameters.clear(name);
337
+ }
338
+
316
339
  physicsTranslate(x: number, y: number): void {
317
340
  this.live('physicsTranslate');
318
341
  translateOn(this.track.ctx, x, y, 'the live track\'s');
@@ -166,6 +166,7 @@ import { deformAt, setupArray, type CoreAttachmentTimeline } from './deform.ts';
166
166
  import { eventsFired, type CoreEventKey, type CoreEventRow } from './events.ts';
167
167
  import { checkedClock, environmentOf, openRawTracksPath, rotateOn, setEnvironmentOn, translateOn, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
168
168
  import type { PhysicsEnvironment } from './constraints_physics.ts';
169
+ import { LivePhysicsParameters, type PhysicsParameterPatch, type PhysicsParameterRead } from './physics_parameters.ts';
169
170
  import { slotTimelinesApply } from './skins.ts';
170
171
 
171
172
  /** 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*). */
@@ -233,6 +234,12 @@ export interface LiveTracks {
233
234
  setEnvironment(environment: Partial<PhysicsEnvironment>): void;
234
235
  /** The environment the next pose answers. */
235
236
  readonly environment: PhysicsEnvironment;
237
+ /** Per-instance final parameter layer, as LiveTrack.setPhysicsParameters. Persists across entry changes. */
238
+ setPhysicsParameters(name: string, patch: PhysicsParameterPatch): void;
239
+ /** Detached pending patch and last evaluated six values, or null effective before start / when never active. */
240
+ readPhysicsParameters(name: string): PhysicsParameterRead;
241
+ /** Removes the patch without resetting the physics history, as LiveTrack.clearPhysicsParameters. */
242
+ clearPhysicsParameters(name: string): void;
236
243
  /** spine-core's `Skeleton.physicsTranslate`, as `LiveTrack.physicsTranslate`. */
237
244
  physicsTranslate(x: number, y: number): void;
238
245
  /** spine-core's `Skeleton.physicsRotate`, as `LiveTrack.physicsRotate`. */
@@ -1322,6 +1329,7 @@ export function openTracks(doc: CompiledDocument, options: TracksOptions, plant:
1322
1329
 
1323
1330
  class Tracks implements LiveTracks {
1324
1331
  private readonly mixer: Mixer;
1332
+ private readonly parameters: LivePhysicsParameters;
1325
1333
  private started = false;
1326
1334
  private steps = 0;
1327
1335
  private time = 0;
@@ -1336,6 +1344,8 @@ class Tracks implements LiveTracks {
1336
1344
  private readonly path: RawTracksPath,
1337
1345
  ) {
1338
1346
  this.mixer = new Mixer(doc, plant);
1347
+ this.parameters = new LivePhysicsParameters(doc);
1348
+ this.path.ctx.parameters = this.parameters;
1339
1349
  }
1340
1350
 
1341
1351
  get trackTime(): number {
@@ -1351,6 +1361,20 @@ class Tracks implements LiveTracks {
1351
1361
  setEnvironmentOn(this.path.ctx, environment, 'the live tracks\'');
1352
1362
  }
1353
1363
 
1364
+ setPhysicsParameters(name: string, patch: PhysicsParameterPatch): void {
1365
+ this.live('setPhysicsParameters');
1366
+ this.parameters.set(name, patch);
1367
+ }
1368
+
1369
+ readPhysicsParameters(name: string): PhysicsParameterRead {
1370
+ return this.parameters.read(name);
1371
+ }
1372
+
1373
+ clearPhysicsParameters(name: string): void {
1374
+ this.live('clearPhysicsParameters');
1375
+ this.parameters.clear(name);
1376
+ }
1377
+
1354
1378
  physicsTranslate(x: number, y: number): void {
1355
1379
  this.live('physicsTranslate');
1356
1380
  translateOn(this.path.ctx, x, y, 'the live tracks\'');
@@ -1467,9 +1491,14 @@ class Tracks implements LiveTracks {
1467
1491
  ctx.time += this.clock === undefined ? dt : this.clock(dt);
1468
1492
  const pose = new Pose(doc, this.plant);
1469
1493
  const animationTime = this.mixer.apply(pose);
1494
+ const entry = this.mixer.tracks[0] ?? null;
1495
+ const adjustment = this.adjust === undefined ? undefined : {
1496
+ callback: this.adjust,
1497
+ context: { trackTime: entry?.trackTime ?? null, animation: entry?.anim.name ?? null, globalTime: this.time },
1498
+ };
1470
1499
  const mixed = new Map(doc.slots.map((s, k) => [s.name, { deform: pose.slots[k].deform, seen: pose.slots[k].seen }]));
1471
1500
  return path.step(first, this.time, animationTime, {
1472
- world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjust: this.adjust }, true),
1501
+ world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjustment }, true),
1473
1502
  slots: (sliders, placeholders) => slotRowsFrom(doc, path.raw, sliders, placeholders, (_slot, _live): SlotBase => {
1474
1503
  const s = pose.slots[doc.slots.indexOf(_slot)];
1475
1504
  return { placeholder: s.placeholder, light: s.light, dark: s.dark };