@rig-c/core 2.74.1 → 2.75.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
@@ -5578,6 +5578,43 @@ skeleton's units and axes, and its change of rotation as `physicsRotate(0, 0,
5578
5578
  Δ)`, before the `step` that poses the frame. Nothing set, every pose is the
5579
5579
  one the track posed before these existed (`CO57`).
5580
5580
 
5581
+ Both `LiveTrack` and `LiveTracks` also accept an optional last `scope` argument
5582
+ for movement, a readonly array of **exact physics constraint names** (play #197):
5583
+
5584
+ ```ts
5585
+ track.physicsTranslate(dx, dy, ['hair_left', 'hair_right']);
5586
+ track.physicsRotate(pivotX, pivotY, deltaDegrees, ['hair_left', 'hair_right']);
5587
+ ```
5588
+
5589
+ Omitted or `undefined` means every existing state, exactly as before. `[]`
5590
+ selects none; repeated names select once, in the existing state order. Every
5591
+ name is checked against the document before any state changes: an unknown name
5592
+ is refused as `physics movement scope: no constraint "name"`; a non-physics
5593
+ constraint is refused naming its kind. A non-array scope or non-string name
5594
+ also refuses. Finite movement/pivot checks still apply, including an empty
5595
+ scope. A declared inactive constraint is a valid selection; movement creates
5596
+ no state for it. No document, parameter override, clock or held pose is changed.
5597
+
5598
+ These are synchronous **before-evaluation** inputs: only the selected remembered
5599
+ origins (`ux`, `uy`, `cx`, `cy`) change, so the next `step` sees extra apparent
5600
+ motion. Distances and the explicit pivot are skeleton world units, x right,
5601
+ y up; the angle is degrees CCW **in the XY plane**, not yaw or pitch. The turn
5602
+ is the displacement of each remembered origin around the pivot, not a direct
5603
+ angular impulse; an origin at the pivot has no displacement. The caller derives
5604
+ any projected yaw/pitch displacement from its own geometry and depth. Do not
5605
+ supply it again for movement the bone itself already makes.
5606
+
5607
+ The ordinary step still owns reset, inertia, limits and integration. A reset
5608
+ or first active update can overwrite this input; an intervening `step(0)` may
5609
+ consume it without the intended positive-time response. There is no queue or
5610
+ motion-history replay. Input writes leave all unselected states unchanged;
5611
+ after stepping, exact isolation holds for independent chains. Descendants or
5612
+ constraints that read a selected bone may react through their normal solver
5613
+ dependencies. Scope does not suppress that coupling or expand itself to cover it.
5614
+ `CO64` checks single/composed tracks, both resets, exact independent-state
5615
+ isolation, native per-constraint operations, empty/all/repeated selections and
5616
+ atomic named refusals; the existing `CO56`/`CO57` cover unscoped movement.
5617
+
5581
5618
  For a live preview of one named constraint (issue #1541), both `LiveTrack` and
5582
5619
  `LiveTracks` on `rig-c/core` expose:
5583
5620
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/core",
3
- "version": "2.74.1",
3
+ "version": "2.75.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": {
@@ -645,8 +645,9 @@ export const DEFAULT_PHYSICS_ENVIRONMENT: PhysicsEnvironment = Object.freeze({ w
645
645
  * pose. A constraint with no state yet has not been updated, and its first
646
646
  * update is pending, so the runtime's translation of it is read by nothing.
647
647
  */
648
- export function translatePhysics(ctx: PhysicsStepContext, x: number, y: number): void {
649
- for (const s of ctx.states.values()) {
648
+ export function translatePhysics(ctx: PhysicsStepContext, x: number, y: number, scope?: ReadonlySet<string>): void {
649
+ for (const [name, s] of ctx.states) {
650
+ if (scope !== undefined && !scope.has(name)) continue;
650
651
  s.ux -= x;
651
652
  s.uy -= y;
652
653
  s.cx -= x;
@@ -661,11 +662,12 @@ export function translatePhysics(ctx: PhysicsStepContext, x: number, y: number):
661
662
  * a translation of each constraint by how far that turn carries the origin it
662
663
  * last saw, as `PhysicsConstraint.rotate` computes it.
663
664
  */
664
- export function rotatePhysics(ctx: PhysicsStepContext, x: number, y: number, degrees: number): void {
665
+ export function rotatePhysics(ctx: PhysicsStepContext, x: number, y: number, degrees: number, scope?: ReadonlySet<string>): void {
665
666
  const r = degrees * RUNTIME_RAD;
666
667
  const cos = Math.cos(r);
667
668
  const sin = Math.sin(r);
668
- for (const s of ctx.states.values()) {
669
+ for (const [name, s] of ctx.states) {
670
+ if (scope !== undefined && !scope.has(name)) continue;
669
671
  const dx = s.cx - x;
670
672
  const dy = s.cy - y;
671
673
  const tx = dx * cos - dy * sin - dx;
package/src/core/raw.ts CHANGED
@@ -728,20 +728,36 @@ export function environmentOf(ctx: PhysicsStepContext): PhysicsEnvironment {
728
728
  return ctx.env ?? DEFAULT_PHYSICS_ENVIRONMENT;
729
729
  }
730
730
 
731
+ /** Validate the complete explicit selection before any remembered state changes. */
732
+ export function checkedPhysicsScope(doc: CompiledDocument, scope: readonly string[] | undefined, who: string): ReadonlySet<string> | undefined {
733
+ if (scope === undefined) return undefined;
734
+ if (!Array.isArray(scope)) throw new CoreInputError(`${who} physics movement scope is ${String(scope)}, not an array of physics constraint names`);
735
+ const selected = new Set<string>();
736
+ for (const name of scope) {
737
+ if (typeof name !== 'string') throw new CoreInputError(`${who} physics movement scope name ${String(name)} is not a string`);
738
+ const constraint = doc.constraints.find((c) => c.kind === 'physics' && c.name === name)
739
+ ?? doc.constraints.find((c) => c.name === name);
740
+ if (constraint === undefined) throw new CoreInputError(`${who} physics movement scope: no constraint "${name}"; this document declares [${doc.constraints.map((c) => c.name).join(', ')}]`);
741
+ if (constraint.kind !== 'physics') throw new CoreInputError(`${who} physics movement scope: constraint "${name}" is ${constraint.kind}, not physics`);
742
+ selected.add(name);
743
+ }
744
+ return selected;
745
+ }
746
+
731
747
  /** A live track's `physicsTranslate` (issue #1416): `x` and `y` finite, or refused by name before anything moves; then `translatePhysics`. */
732
- export function translateOn(ctx: PhysicsStepContext, x: number, y: number, who: string): void {
748
+ export function translateOn(ctx: PhysicsStepContext, x: number, y: number, who: string, scope?: ReadonlySet<string>): void {
733
749
  for (const [k, v] of [['x', x], ['y', y]] as const) {
734
750
  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`);
735
751
  }
736
- translatePhysics(ctx, x, y);
752
+ translatePhysics(ctx, x, y, scope);
737
753
  }
738
754
 
739
755
  /** A live track's `physicsRotate` (issue #1416): `x`, `y` and `degrees` finite, or refused by name before anything moves; then `rotatePhysics`. */
740
- export function rotateOn(ctx: PhysicsStepContext, x: number, y: number, degrees: number, who: string): void {
756
+ export function rotateOn(ctx: PhysicsStepContext, x: number, y: number, degrees: number, who: string, scope?: ReadonlySet<string>): void {
741
757
  for (const [k, v] of [['x', x], ['y', y], ['degrees', degrees]] as const) {
742
758
  if (typeof v !== 'number' || !Number.isFinite(v)) throw new CoreInputError(`${who} physicsRotate's ${k} is ${String(v)}, not a finite number`);
743
759
  }
744
- rotatePhysics(ctx, x, y, degrees);
760
+ rotatePhysics(ctx, x, y, degrees, scope);
745
761
  }
746
762
 
747
763
  /**
package/src/core/track.ts CHANGED
@@ -222,7 +222,7 @@
222
222
  * history, so a seek is "open, then step to the time".
223
223
  */
224
224
  import { CoreInputError, type CompiledDocument } from './index.ts';
225
- import { checkedClock, environmentOf, openRawTrack, rotateOn, setEnvironmentOn, stepRawTrack, translateOn, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
225
+ import { checkedClock, checkedPhysicsScope, 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
228
  import { LivePhysicsParameters, type PhysicsParameterPatch, type PhysicsParameterRead } from './physics_parameters.ts';
@@ -261,10 +261,12 @@ export interface LiveTrack {
261
261
  readPhysicsParameters(name: string): PhysicsParameterRead;
262
262
  /** Removes the patch for the next pose without resetting velocities, offsets or the clock. */
263
263
  clearPhysicsParameters(name: string): void;
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. */
265
- physicsTranslate(x: number, y: number): void;
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. */
267
- physicsRotate(x: number, y: number, degrees: number): void;
264
+ /** Scope omitted: all physics; otherwise exact physics names, validated atomically. Empty selects none, duplicates select once.
265
+ * Movement changes remembered world origins synchronously, before the next evaluation.
266
+ * 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. */
267
+ physicsTranslate(x: number, y: number, scope?: readonly string[]): void;
268
+ /** Same scope contract as physicsTranslate. The consumer turned by `degrees` (counter-clockwise) about explicit `(x, y)` in the skeleton's world; changes remembered origins, not tip direction or angular velocity. */
269
+ physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void;
268
270
  }
269
271
 
270
272
  /** 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. */
@@ -336,14 +338,14 @@ class Track implements LiveTrack {
336
338
  this.parameters.clear(name);
337
339
  }
338
340
 
339
- physicsTranslate(x: number, y: number): void {
341
+ physicsTranslate(x: number, y: number, scope?: readonly string[]): void {
340
342
  this.live('physicsTranslate');
341
- translateOn(this.track.ctx, x, y, 'the live track\'s');
343
+ translateOn(this.track.ctx, x, y, 'the live track\'s', checkedPhysicsScope(this.track.doc, scope, 'the live track\'s'));
342
344
  }
343
345
 
344
- physicsRotate(x: number, y: number, degrees: number): void {
346
+ physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void {
345
347
  this.live('physicsRotate');
346
- rotateOn(this.track.ctx, x, y, degrees, 'the live track\'s');
348
+ rotateOn(this.track.ctx, x, y, degrees, 'the live track\'s', checkedPhysicsScope(this.track.doc, scope, 'the live track\'s'));
347
349
  }
348
350
 
349
351
  /** A refused track refuses every call that would move it, naming the step it was refused at. */
@@ -164,7 +164,7 @@ import { activeBones, CoreInputError, rawNumber, shownAttachment, viewColours, W
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, environmentOf, openRawTracksPath, rotateOn, setEnvironmentOn, translateOn, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
167
+ import { checkedClock, checkedPhysicsScope, environmentOf, openRawTracksPath, rotateOn, setEnvironmentOn, translateOn, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
168
168
  import type { PhysicsEnvironment } from './constraints_physics.ts';
169
169
  import { LivePhysicsParameters, type PhysicsParameterPatch, type PhysicsParameterRead } from './physics_parameters.ts';
170
170
  import { slotTimelinesApply } from './skins.ts';
@@ -240,10 +240,12 @@ export interface LiveTracks {
240
240
  readPhysicsParameters(name: string): PhysicsParameterRead;
241
241
  /** Removes the patch without resetting the physics history, as LiveTrack.clearPhysicsParameters. */
242
242
  clearPhysicsParameters(name: string): void;
243
- /** spine-core's `Skeleton.physicsTranslate`, as `LiveTrack.physicsTranslate`. */
244
- physicsTranslate(x: number, y: number): void;
245
- /** spine-core's `Skeleton.physicsRotate`, as `LiveTrack.physicsRotate`. */
246
- physicsRotate(x: number, y: number, degrees: number): void;
243
+ /** Scope omitted: all physics; otherwise exact physics names, validated atomically. Empty selects none, duplicates select once.
244
+ * Movement changes remembered world origins synchronously, before the next evaluation.
245
+ * spine-core's `Skeleton.physicsTranslate`, as `LiveTrack.physicsTranslate`. */
246
+ physicsTranslate(x: number, y: number, scope?: readonly string[]): void;
247
+ /** Same exact-name scope and explicit world pivot as `LiveTrack.physicsRotate`. */
248
+ physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void;
247
249
  }
248
250
 
249
251
  /**
@@ -1384,14 +1386,14 @@ class Tracks implements LiveTracks {
1384
1386
  this.parameters.clear(name);
1385
1387
  }
1386
1388
 
1387
- physicsTranslate(x: number, y: number): void {
1389
+ physicsTranslate(x: number, y: number, scope?: readonly string[]): void {
1388
1390
  this.live('physicsTranslate');
1389
- translateOn(this.path.ctx, x, y, 'the live tracks\'');
1391
+ translateOn(this.path.ctx, x, y, 'the live tracks\'', checkedPhysicsScope(this.doc, scope, 'the live tracks\''));
1390
1392
  }
1391
1393
 
1392
- physicsRotate(x: number, y: number, degrees: number): void {
1394
+ physicsRotate(x: number, y: number, degrees: number, scope?: readonly string[]): void {
1393
1395
  this.live('physicsRotate');
1394
- rotateOn(this.path.ctx, x, y, degrees, 'the live tracks\'');
1396
+ rotateOn(this.path.ctx, x, y, degrees, 'the live tracks\'', checkedPhysicsScope(this.doc, scope, 'the live tracks\''));
1395
1397
  }
1396
1398
 
1397
1399
  private live(call: string): void {