spine-rigc 2.51.1 → 2.52.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
@@ -4953,6 +4953,41 @@ agreeing with `render` — and left out, a track steps the physics by the step i
4953
4953
  exactly as before. Which clock a player with a real clock uses is the player's
4954
4954
  question, and rigc does not answer it.
4955
4955
 
4956
+ 🌬️ **`wind` and `gravity` are the constraint's response; the world is the
4957
+ consumer's** (issue #1416). In 4.3 the two are not forces but how strongly the
4958
+ constraint answers two directions the *skeleton* carries — `Skeleton.windX,
4959
+ windY` and `gravityX, gravityY`, which start at `1, 0` and `0, 1` — so a
4960
+ constraint stating `wind: 20` is pushed along whatever direction the player
4961
+ sets, times 20 (and on `x` and `y` times `referenceScale`; on a rotation, at
4962
+ a lever of the bone's `length` over `referenceScale`). A live track of the
4963
+ core entry sets that direction with `setEnvironment({ wind: [x, y], gravity:
4964
+ [x, y] })` — x to the right, **y down**, either half left out to keep the one
4965
+ that stands — and the Spine export carries only the response, so an official
4966
+ runtime whose `Skeleton` is given the same four numbers poses what the core
4967
+ poses, to the bit (`CO56`). Two consequences an author has to know:
4968
+
4969
+ - **A constraint stating `wind: 0` feels no wind**, whatever the player sets.
4970
+ State a `wind` on every link that should answer a gust; leave it 0 on one
4971
+ that should not.
4972
+ - **Until the player sets one, `wind` pushes to the right.** The initial
4973
+ direction is the runtime's `[1, 0]`, not `[0, 0]`, so a rig stating `wind`
4974
+ leans at rest on every player that never calls `setEnvironment` — the
4975
+ official runtimes included. A player that wants calm at rest sets `wind:
4976
+ [0, 0]` and blows by setting it back. rigc does not change the initial
4977
+ value: that would make the export pose differently in the runtime than in
4978
+ the core.
4979
+
4980
+ Moving the rig is the consumer's too: `physicsTranslate(x, y)` and
4981
+ `physicsRotate(x, y, degrees)` on a live track are the runtime's methods of
4982
+ those names — the whole rig moved or turned since the last pose, in the
4983
+ skeleton's world (y up, degrees counter-clockwise) — and every constraint's
4984
+ next update sees its bone move by that much, so a rig walked across the screen
4985
+ trails its coat. A player forwards what spine-core's `SkeletonPhysicsMovement`
4986
+ forwards: its display object's change of position since the last frame in the
4987
+ skeleton's units and axes, and its change of rotation as `physicsRotate(0, 0,
4988
+ Δ)`, before the `step` that poses the frame. Nothing set, every pose is the
4989
+ one the track posed before these existed (`CO57`).
4990
+
4956
4991
  `mix` is a player-side `AnimationStateData` config and is **not** emitted into
4957
4992
  skeleton JSON, so the parser is the only check it gets: it is
4958
4993
  `{ "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.51.1",
3
+ "version": "2.52.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": {
@@ -189,6 +189,34 @@
189
189
  * reading is rejected as a schedule no player runs; the core suite's `SC`
190
190
  * controls plant it and turn the probe red on the steps after the key.
191
191
  *
192
+ * ## The environment (issue #1416)
193
+ *
194
+ * A constraint's `wind` and `gravity` are not forces: they are how strongly it
195
+ * answers two directions the SKELETON carries — spine-core 4.3.13's
196
+ * `Skeleton.windX, windY` and `gravityX, gravityY`, initially `1, 0` and `0,
197
+ * 1` (`DEFAULT_PHYSICS_ENVIRONMENT`). The step weighs them as the runtime
198
+ * does: for `x` and `y`, `ax = (wind·windX + gravity·gravityX)·R` and `ay =
199
+ * (wind·windY + gravity·gravityY)·R`, `velocity += (ax − offset·strength)·…`
200
+ * on `x` and `−= (ay + offset·strength)·…` on `y`; for the rotation and the
201
+ * scale the same two sums without `R`, `(ax·sin + ay·cos)·length/R` and `ax·cos
202
+ * − ay·sin` in place of the wind and gravity terms above. The skeleton's own
203
+ * scale, which the runtime multiplies the `x` and `y` sums by, is 1 here. So
204
+ * the environment's y points DOWN — `[0, 1]` pulls toward the bottom of the
205
+ * screen — and with the initial values every expression is the one this file
206
+ * computed before the environment existed: `wind·1 + gravity·0` is `wind` for
207
+ * every finite `gravity` (a `wind` of −0 aside, which it reads as +0, as the
208
+ * runtime does), and the render and emit bases did not move.
209
+ *
210
+ * The consumer sets it (`setEnvironment` on a live track, `./track.ts`), and
211
+ * moves the rig from outside with `translatePhysics` and `rotatePhysics` —
212
+ * `Skeleton.physicsTranslate` and `physicsRotate`, which shift what each
213
+ * constraint last saw (`ux, uy, cx, cy`) so its next update reads the bone as
214
+ * having moved. A constraint with no state yet is pending, and its first
215
+ * update overwrites both, so it is left alone, as the runtime's translation of
216
+ * it is read by nothing. The core suite's `CO56` holds all of it to spine-core
217
+ * given the same values at tolerance 0, and `CO57` that nothing set poses what
218
+ * was posed before.
219
+ *
192
220
  * ## What is not exact, and why
193
221
  *
194
222
  * Over 3,000 random rigs of three shapes (all five inherit modes; ik and
@@ -215,6 +243,9 @@
215
243
  */
216
244
  import { channelAt, keyIndexAt, poseAnimations, sampleTime, type CoreAnimationPose, type CoreCurve, type CoreKey, type SamplePhase, type TimelinePlant } from './animation.ts';
217
245
  import { RUNTIME_PI, type CoreWorld } from './world.ts';
246
+
247
+ /** Degrees to radians as the runtime converts them (`MathUtils.degRad`, `pi / 180` with the runtime's pi). */
248
+ const RUNTIME_RAD = RUNTIME_PI / 180;
218
249
  import { previousPassSlotBones, type CoreConstraintRecord } from './constraints.ts';
219
250
  import { activeBones, type CompiledDocument } from './index.ts';
220
251
 
@@ -577,6 +608,71 @@ export interface PhysicsStepContext {
577
608
  keyedNow?: Set<string>;
578
609
  /** A plant only (issue #1049, `sliderPhysicsFromCurrent: false`): each physics record's setup values, before the step's own animation. */
579
610
  setupRecords?: Map<string, CorePhysicsRecord>;
611
+ /**
612
+ * The world every constraint answers (issue #1416, the header's *The
613
+ * environment*): the skeleton's wind and gravity directions, as a caller set
614
+ * them on the live track. Absent, spine-core's own initial values
615
+ * (`DEFAULT_PHYSICS_ENVIRONMENT`), which is every walk but a live one a
616
+ * caller set it on.
617
+ */
618
+ env?: PhysicsEnvironment;
619
+ }
620
+
621
+ /**
622
+ * The world a physics constraint answers (issue #1416): spine-core 4.3.13's
623
+ * four skeleton fields `windX`, `windY`, `gravityX` and `gravityY`, as
624
+ * `wind: [windX, windY]` and `gravity: [gravityX, gravityY]` — the
625
+ * DIRECTIONS the constraints' own `wind` and `gravity` act along, which are
626
+ * each constraint's response to them (the header's *The environment*). In the
627
+ * runtime's convention: x to the right, **y down** — a positive y component
628
+ * pulls a constraint toward the bottom of the screen, as `gravity: [0, 1]`
629
+ * does.
630
+ */
631
+ export interface PhysicsEnvironment {
632
+ readonly wind: readonly [number, number];
633
+ readonly gravity: readonly [number, number];
634
+ }
635
+
636
+ /** spine-core 4.3.13's initial `Skeleton.windX, windY, gravityX, gravityY` — `1, 0, 0, 1` — the world of every walk nobody set one on. */
637
+ export const DEFAULT_PHYSICS_ENVIRONMENT: PhysicsEnvironment = Object.freeze({ wind: Object.freeze([1, 0] as const), gravity: Object.freeze([0, 1] as const) });
638
+
639
+ /**
640
+ * `Skeleton.physicsTranslate(x, y)` on the states in `ctx` (issue #1416): every
641
+ * constraint's next update sees its bone moved by `(x, y)` in the skeleton's
642
+ * world (y up) — the consumer moved the whole rig by that much since the last
643
+ * pose. A constraint with no state yet has not been updated, and its first
644
+ * update is pending, so the runtime's translation of it is read by nothing.
645
+ */
646
+ export function translatePhysics(ctx: PhysicsStepContext, x: number, y: number): void {
647
+ for (const s of ctx.states.values()) {
648
+ s.ux -= x;
649
+ s.uy -= y;
650
+ s.cx -= x;
651
+ s.cy -= y;
652
+ }
653
+ }
654
+
655
+ /**
656
+ * `Skeleton.physicsRotate(x, y, degrees)` on the states in `ctx` (issue
657
+ * #1416): every constraint's next update sees its bone turned by `degrees`
658
+ * (counter-clockwise, the runtime's pi) about `(x, y)` in the skeleton's world —
659
+ * a translation of each constraint by how far that turn carries the origin it
660
+ * last saw, as `PhysicsConstraint.rotate` computes it.
661
+ */
662
+ export function rotatePhysics(ctx: PhysicsStepContext, x: number, y: number, degrees: number): void {
663
+ const r = degrees * RUNTIME_RAD;
664
+ const cos = Math.cos(r);
665
+ const sin = Math.sin(r);
666
+ for (const s of ctx.states.values()) {
667
+ const dx = s.cx - x;
668
+ const dy = s.cy - y;
669
+ const tx = dx * cos - dy * sin - dx;
670
+ const ty = dx * sin + dy * cos - dy;
671
+ s.ux -= tx;
672
+ s.uy -= ty;
673
+ s.cx -= tx;
674
+ s.cy -= ty;
675
+ }
580
676
  }
581
677
 
582
678
  /** A constraint's state in `ctx`, created fresh on its first update. */
@@ -634,6 +730,7 @@ export function stepPhysics(r: CorePhysicsRecord, w: CoreWorld, length: number,
634
730
  s.lastTime = ctx.time;
635
731
  const bx = w.worldX;
636
732
  const by = w.worldY;
733
+ const env = ctx.env ?? DEFAULT_PHYSICS_ENVIRONMENT;
637
734
  let z = 0;
638
735
  if (s.pending) {
639
736
  s.pending = false;
@@ -664,16 +761,19 @@ export function stepPhysics(r: CorePhysicsRecord, w: CoreWorld, length: number,
664
761
  const e = r.strength;
665
762
  const wind = r.wind * f;
666
763
  const g = r.gravity * f;
764
+ // The environment's directions (issue #1416), as the runtime weighs them; the skeleton's scale is 1 here, so its factor is left out.
765
+ const ax = wind * env.wind[0] + g * env.gravity[0];
766
+ const ay = wind * env.wind[1] + g * env.gravity[1];
667
767
  const xs = s.xOffset;
668
768
  const ys = s.yOffset;
669
769
  do {
670
770
  if (x) {
671
- s.xVelocity += (wind - s.xOffset * e) * m;
771
+ s.xVelocity += (ax - s.xOffset * e) * m;
672
772
  s.xOffset += s.xVelocity * t;
673
773
  s.xVelocity *= d;
674
774
  }
675
775
  if (y) {
676
- s.yVelocity -= (g + s.yOffset * e) * m;
776
+ s.yVelocity -= (ay + s.yOffset * e) * m;
677
777
  s.yOffset += s.yVelocity * t;
678
778
  s.yVelocity *= d;
679
779
  }
@@ -720,20 +820,20 @@ export function stepPhysics(r: CorePhysicsRecord, w: CoreWorld, length: number,
720
820
  if (d === -1) d = Math.pow(r.damping, 60 * t);
721
821
  const m = r.massInverse * t;
722
822
  const e = r.strength;
723
- const wind = r.wind;
724
- const g = r.gravity;
823
+ const ax = r.wind * env.wind[0] + r.gravity * env.gravity[0];
824
+ const ay = r.wind * env.wind[1] + r.gravity * env.gravity[1];
725
825
  const h = l / f;
726
826
  const rs = s.rotateOffset;
727
827
  const ss = s.scaleOffset;
728
828
  for (;;) {
729
829
  a -= t;
730
830
  if (scaleX) {
731
- s.scaleVelocity += (wind * c - g * sn - s.scaleOffset * e) * m;
831
+ s.scaleVelocity += (ax * c - ay * sn - s.scaleOffset * e) * m;
732
832
  s.scaleOffset += s.scaleVelocity * t;
733
833
  s.scaleVelocity *= d;
734
834
  }
735
835
  if (rotateOrShearX) {
736
- s.rotateVelocity -= ((wind * sn + g * c) * h + s.rotateOffset * e) * m;
836
+ s.rotateVelocity -= ((ax * sn + ay * c) * h + s.rotateOffset * e) * m;
737
837
  s.rotateOffset += s.rotateVelocity * t;
738
838
  s.rotateVelocity *= d;
739
839
  if (a < t) break;
package/src/core/entry.ts CHANGED
@@ -18,7 +18,9 @@
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
20
  * `openTracks`, issue #1336; `physicsClockStep` and `PHYSICS_CLOCK_LEAD`,
21
- * the clock a live track's `clock` option takes, issue #1396).
21
+ * the clock a live track's `clock` option takes, issue #1396;
22
+ * `PhysicsEnvironment`, what a live track's `setEnvironment` takes, issue
23
+ * #1416).
22
24
  * The list is `scripts/install_smoke.ts`'s `OBSERVED_SYMBOLS` rows for
23
25
  * `./core`, and a symbol added here and not there is not promised.
24
26
  */
@@ -28,7 +30,7 @@ export { poseRawSetup, poseRawAnimation, poseRawAnimationEach, loopedTime, setup
28
30
  export type { RawPose, RawBone, RawDrawn, RawClip, RawClipped, RawEvent, RawReset } from './raw.ts';
29
31
  export { poseWalkSetup, poseLoopingWalk } from './walk.ts';
30
32
  export { openTrack } from './track.ts';
31
- export type { LiveTrack, TrackOptions, AdjustableLocals, BoneLocals, LocalAdjust } from './track.ts';
33
+ export type { LiveTrack, TrackOptions, AdjustableLocals, BoneLocals, LocalAdjust, PhysicsEnvironment } from './track.ts';
32
34
  export { openTracks } from './tracks.ts';
33
35
  export type { LiveTracks, TracksOptions, MixOptions, QueueOptions, TrackEntryView } from './tracks.ts';
34
36
  export type { WalkPose } from './walk.ts';
package/src/core/raw.ts CHANGED
@@ -116,7 +116,7 @@
116
116
  import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber, readColour, shownAttachment, sourceOfDoc, underNoSkin, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
117
117
  import { posedBoneWorld, posedBoneWorldAlone, posedSlots, type CoreAnimationTimelines, type LocalAdjust, type TimelinePlant } from './animation.ts';
118
118
  import { constraintsAbsentWhy, pathAnimationsWhy } from './constraints.ts';
119
- import { freshStepContext, steppedPreviousPassWhy } from './constraints_physics.ts';
119
+ import { freshStepContext, rotatePhysics, steppedPreviousPassWhy, translatePhysics, DEFAULT_PHYSICS_ENVIRONMENT, type PhysicsEnvironment, type PhysicsStepContext } from './constraints_physics.ts';
120
120
  import { attachmentStates } from './deform.ts';
121
121
  import { drawOrderAt, type CoreDrawOrderKey } from './draw_order.ts';
122
122
  import { eventsFired, type CoreEventRow } from './events.ts';
@@ -575,6 +575,53 @@ export function checkedClock(clock: (dt: number) => number, who: string): (dt: n
575
575
  };
576
576
  }
577
577
 
578
+ /**
579
+ * A live track's `setEnvironment` (issue #1416, `./track.ts` and `./tracks.ts`):
580
+ * the world every physics constraint answers from the next pose on, set on the
581
+ * track's physics context — `wind` and `gravity` each a pair of finite numbers,
582
+ * either left out to keep the one that stands, and nothing else — or refused by
583
+ * name, before anything moves. The pair is copied: the caller's array changed
584
+ * later moves nothing.
585
+ */
586
+ export function setEnvironmentOn(ctx: PhysicsStepContext, env: unknown, who: string): void {
587
+ if (typeof env !== 'object' || env === null || Array.isArray(env)) throw new CoreInputError(`${who} setEnvironment was given ${env === null ? 'null' : Array.isArray(env) ? 'an array' : typeof env}, not an object { wind?: [x, y], gravity?: [x, y] }`);
588
+ const given = env as Record<string, unknown>;
589
+ for (const key of Object.keys(given)) {
590
+ if (key !== 'wind' && key !== 'gravity') throw new CoreInputError(`${who} setEnvironment names "${key}", which is not one of [wind, gravity]`);
591
+ }
592
+ const pair = (key: 'wind' | 'gravity'): readonly [number, number] | undefined => {
593
+ const v = given[key];
594
+ if (v === undefined) return undefined;
595
+ if (!Array.isArray(v) || v.length !== 2 || v.some((n) => typeof n !== 'number' || !Number.isFinite(n))) throw new CoreInputError(`${who} setEnvironment's ${key} is ${JSON.stringify(v)}, not a pair of finite numbers [x, y] (x to the right, y down)`);
596
+ return Object.freeze([v[0] as number, v[1] as number] as const);
597
+ };
598
+ const wind = pair('wind');
599
+ const gravity = pair('gravity');
600
+ const standing = ctx.env ?? DEFAULT_PHYSICS_ENVIRONMENT;
601
+ ctx.env = Object.freeze({ wind: wind ?? standing.wind, gravity: gravity ?? standing.gravity });
602
+ }
603
+
604
+ /** The environment a live track's physics answers now (`setEnvironmentOn`): spine-core's initial one where none was set. */
605
+ export function environmentOf(ctx: PhysicsStepContext): PhysicsEnvironment {
606
+ return ctx.env ?? DEFAULT_PHYSICS_ENVIRONMENT;
607
+ }
608
+
609
+ /** A live track's `physicsTranslate` (issue #1416): `x` and `y` finite, or refused by name before anything moves; then `translatePhysics`. */
610
+ export function translateOn(ctx: PhysicsStepContext, x: number, y: number, who: string): void {
611
+ for (const [k, v] of [['x', x], ['y', y]] as const) {
612
+ if (typeof v !== 'number' || !Number.isFinite(v)) throw new CoreInputError(`${who} physicsTranslate's ${k} is ${String(v)}, not a finite distance in the skeleton's world`);
613
+ }
614
+ translatePhysics(ctx, x, y);
615
+ }
616
+
617
+ /** A live track's `physicsRotate` (issue #1416): `x`, `y` and `degrees` finite, or refused by name before anything moves; then `rotatePhysics`. */
618
+ export function rotateOn(ctx: PhysicsStepContext, x: number, y: number, degrees: number, who: string): void {
619
+ for (const [k, v] of [['x', x], ['y', y], ['degrees', degrees]] as const) {
620
+ if (typeof v !== 'number' || !Number.isFinite(v)) throw new CoreInputError(`${who} physicsRotate's ${k} is ${String(v)}, not a finite number`);
621
+ }
622
+ rotatePhysics(ctx, x, y, degrees);
623
+ }
624
+
578
625
  /**
579
626
  * The animation time a looping track applies its animation at (`./walk.ts`,
580
627
  * *The time*): the track time wrapped by the duration, and 0 over a duration
package/src/core/track.ts CHANGED
@@ -174,6 +174,36 @@
174
174
  * `AnimationState` updated by the step and its `Skeleton` by the clock), and
175
175
  * `CO55` that the times and the events are those of the walk without it.
176
176
  *
177
+ * ## The environment, and the rig moved from outside (issue #1416)
178
+ *
179
+ * The rig states how each physics constraint answers the world — its `wind`
180
+ * and `gravity` — and the consumer states the world. `setEnvironment({ wind,
181
+ * gravity })` sets it from the next pose on and it stands until set again,
182
+ * as spine-core's four `Skeleton` fields do: `wind` is `[windX, windY]`,
183
+ * `gravity` is `[gravityX, gravityY]`, x to the right and **y down**, and
184
+ * until set it is the runtime's own `{ wind: [1, 0], gravity: [0, 1] }`
185
+ * (`./constraints_physics.ts`, *The environment*). Either half may be left
186
+ * out to keep the one that stands. So a gust is the consumer setting `wind`
187
+ * to `[0, 0]` at rest and `[k, 0]` while it blows, and a constraint that
188
+ * states `wind: 0` does not feel it — the response is the rig's, the gust the
189
+ * scene's.
190
+ *
191
+ * `physicsTranslate(x, y)` and `physicsRotate(x, y, degrees)` are the
192
+ * runtime's `Skeleton` methods of those names: the consumer moved or turned
193
+ * the whole rig since the last pose — in the skeleton's world, y up, degrees
194
+ * counter-clockwise — and every constraint's next update sees its bone move
195
+ * by that much, so a rig walked across the screen trails what hangs from it.
196
+ * A player forwards what spine-core's `SkeletonPhysicsMovement` forwards: the
197
+ * change of its display object's position since the last frame, converted into
198
+ * the skeleton's units and axes, and the change of its rotation about the
199
+ * skeleton's origin, `physicsRotate(0, 0, Δ)`, both before the `step` that
200
+ * poses the frame.
201
+ *
202
+ * `CO56` holds the poses to spine-core 4.3.13 given the same values on its
203
+ * `Skeleton`, and holds red the readings the mapping rejects; `CO57` holds that
204
+ * nothing set — or the runtime's own values set on every step and a move of 0
205
+ * — poses what the track posed before the methods existed, bit for bit.
206
+ *
177
207
  * ## The oracle chain
178
208
  *
179
209
  * Nothing here is held to spine-core directly but the events: the live track
@@ -192,9 +222,11 @@
192
222
  * history, so a seek is "open, then step to the time".
193
223
  */
194
224
  import { CoreInputError, type CompiledDocument } from './index.ts';
195
- import { checkedClock, openRawTrack, stepRawTrack, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
225
+ import { checkedClock, environmentOf, openRawTrack, rotateOn, setEnvironmentOn, stepRawTrack, translateOn, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
196
226
  import type { LocalAdjust, TimelinePlant } from './animation.ts';
227
+ import type { PhysicsEnvironment } from './constraints_physics.ts';
197
228
  export type { AdjustableLocals, BoneLocals, LocalAdjust } from './animation.ts';
229
+ export type { PhysicsEnvironment } from './constraints_physics.ts';
198
230
 
199
231
  /** 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*). */
200
232
  export interface TrackOptions {
@@ -217,6 +249,14 @@ export interface LiveTrack {
217
249
  readonly animationTime: number;
218
250
  /** The runtime's duration of the animation (`CoreAnimationTimelines.duration`). */
219
251
  readonly duration: number;
252
+ /** Sets the world every physics constraint answers from the next pose on (the header's *The environment*): `wind` and `gravity` as spine-core's `Skeleton.windX, windY` and `gravityX, gravityY` — x right, y down — either left out to keep the one that stands. Until set, `{ wind: [1, 0], gravity: [0, 1] }`, the runtime's own. */
253
+ setEnvironment(environment: Partial<PhysicsEnvironment>): void;
254
+ /** The environment the next pose answers. */
255
+ readonly environment: PhysicsEnvironment;
256
+ /** 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
+ physicsTranslate(x: number, y: number): void;
258
+ /** 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. */
259
+ physicsRotate(x: number, y: number, degrees: number): void;
220
260
  }
221
261
 
222
262
  /** The two modes: a track that holds at its duration is the raw entry's walk; a looping one fires its events across the wrap and cuts its clipped rows (`live`). Neither keeps a non-finite value. */
@@ -264,6 +304,30 @@ class Track implements LiveTrack {
264
304
  return this.track.duration;
265
305
  }
266
306
 
307
+ get environment(): PhysicsEnvironment {
308
+ return environmentOf(this.track.ctx);
309
+ }
310
+
311
+ setEnvironment(environment: Partial<PhysicsEnvironment>): void {
312
+ this.live('setEnvironment');
313
+ setEnvironmentOn(this.track.ctx, environment, 'the live track\'s');
314
+ }
315
+
316
+ physicsTranslate(x: number, y: number): void {
317
+ this.live('physicsTranslate');
318
+ translateOn(this.track.ctx, x, y, 'the live track\'s');
319
+ }
320
+
321
+ physicsRotate(x: number, y: number, degrees: number): void {
322
+ this.live('physicsRotate');
323
+ rotateOn(this.track.ctx, x, y, degrees, 'the live track\'s');
324
+ }
325
+
326
+ /** A refused track refuses every call that would move it, naming the step it was refused at. */
327
+ private live(call: string): void {
328
+ if (this.refused !== null) throw new CoreInputError(`the live track was refused at step ${this.refused}, after its time and physics had moved; open the track again (${call} not made)`);
329
+ }
330
+
267
331
  step(dt: number): RawPose {
268
332
  const n = this.steps + 1;
269
333
  if (this.refused !== null) throw new CoreInputError(`the live track was refused at step ${this.refused}, after its time and physics had moved; open the track again`);
@@ -164,7 +164,8 @@ import { activeBones, CoreInputError, rawNumber, readColour, shownAttachment, ty
164
164
  import { channelAt, keyIndexAt, SLOT_COLOUR_TIMELINES, slotRowsFrom, steppedLocalsWorld, type BoneTimelineKind, type CoreKey, type LocalAdjust, type SlotBase, type SlotColourKind, type TimelinePlant } from './animation.ts';
165
165
  import { deformAt, setupArray, type CoreAttachmentTimeline } from './deform.ts';
166
166
  import { eventsFired, type CoreEventKey, type CoreEventRow } from './events.ts';
167
- import { checkedClock, openRawTracksPath, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
167
+ import { checkedClock, environmentOf, openRawTracksPath, rotateOn, setEnvironmentOn, translateOn, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
168
+ import type { PhysicsEnvironment } from './constraints_physics.ts';
168
169
  import { slotTimelinesApply } from './skins.ts';
169
170
 
170
171
  /** 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*). */
@@ -228,6 +229,14 @@ export interface LiveTracks {
228
229
  current(track: number): TrackEntryView | null;
229
230
  /** The running sum of the steps taken, 0 before the first. */
230
231
  readonly trackTime: number;
232
+ /** Sets the world every physics constraint answers from the next pose on, as `LiveTrack.setEnvironment` (`./track.ts`, *The environment*). */
233
+ setEnvironment(environment: Partial<PhysicsEnvironment>): void;
234
+ /** The environment the next pose answers. */
235
+ readonly environment: PhysicsEnvironment;
236
+ /** spine-core's `Skeleton.physicsTranslate`, as `LiveTrack.physicsTranslate`. */
237
+ physicsTranslate(x: number, y: number): void;
238
+ /** spine-core's `Skeleton.physicsRotate`, as `LiveTrack.physicsRotate`. */
239
+ physicsRotate(x: number, y: number, degrees: number): void;
231
240
  }
232
241
 
233
242
  /**
@@ -1333,6 +1342,25 @@ class Tracks implements LiveTracks {
1333
1342
  return this.time;
1334
1343
  }
1335
1344
 
1345
+ get environment(): PhysicsEnvironment {
1346
+ return environmentOf(this.path.ctx);
1347
+ }
1348
+
1349
+ setEnvironment(environment: Partial<PhysicsEnvironment>): void {
1350
+ this.live('setEnvironment');
1351
+ setEnvironmentOn(this.path.ctx, environment, 'the live tracks\'');
1352
+ }
1353
+
1354
+ physicsTranslate(x: number, y: number): void {
1355
+ this.live('physicsTranslate');
1356
+ translateOn(this.path.ctx, x, y, 'the live tracks\'');
1357
+ }
1358
+
1359
+ physicsRotate(x: number, y: number, degrees: number): void {
1360
+ this.live('physicsRotate');
1361
+ rotateOn(this.path.ctx, x, y, degrees, 'the live tracks\'');
1362
+ }
1363
+
1336
1364
  private live(call: string): void {
1337
1365
  if (this.refused !== null) throw new CoreInputError(`the live tracks were refused at ${this.refused}, after their entries and physics had moved; open them again (${call} not made)`);
1338
1366
  }