spine-html 1.13.0 → 1.14.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/README.md CHANGED
@@ -685,6 +685,77 @@ firing events or drawing. A non-finite value, a bone the document does not have,
685
685
  or an error the function throws refuses that step in the core's words, and the
686
686
  track is reopened by the next `play` or `seek`.
687
687
 
688
+ The callback also receives a `BoneAdjustContext` from the core:
689
+
690
+ ```ts
691
+ player.adjust = (bones, ctx) => {
692
+ bones.bone('head').rotation = 12 * (ctx.trackTime ?? 0);
693
+ };
694
+ ```
695
+
696
+ `ctx.trackTime` is the **current track-0 entry's unwrapped seconds**, starting at
697
+ 0 for each new entry. Loops do not wrap it, and a nonlooping entry keeps counting
698
+ past its duration. `ctx.animation` names that entry. Without a track-0 entry both
699
+ are null; an explicit empty entry has **numeric trackTime and null animation**.
700
+ Upper tracks supply no fallback. `ctx.globalTime` is the core walk's running sum
701
+ of steps in seconds, so it can differ from a new entry's time. Neither clock is
702
+ wall time, wrapped animation time, nor the physics clock's small lead. Unary
703
+ callbacks remain compatible. The hook runs once after composition, including a
704
+ crossfade, rather than once for each contributing entry.
705
+
706
+ Seek evaluates its replay times (`seekStep` plus any final remainder), independent
707
+ of the currently displayed player time. Exact cache hits may reuse a pose without
708
+ calling the hook. A continuous walk and seek are bit-identical for an **unchanged
709
+ patch and matched history**: the same animation, environment, adjustment, reset
710
+ and step partition. Closed-over callback state still needs `repose()` or the
711
+ adjust setter to invalidate cached poses. Pause, zero advance and renderer mode
712
+ changes add no evaluations; explicit `repose()` keeps the same context time.
713
+
714
+ ### Live physics parameters
715
+
716
+ ```ts
717
+ player.play('hang');
718
+ player.setPhysicsParameters('link_1_phys', { damping: 0.4, wind: 8 });
719
+ player.readPhysicsParameters('link_1_phys'); // { override, effective }
720
+ player.clearPhysicsParameters('link_1_phys');
721
+ ```
722
+
723
+ `setPhysicsParameters(name, patch)` forwards a per-player patch of `strength`,
724
+ `damping`, `inertia`, `wind`, `gravity`, and `mix` to the core. Omitted fields keep
725
+ their standing override. Values are runtime scalars with no clamping, conversion,
726
+ new clock or solver. The core applies them **after authored/keyed values and
727
+ sliders, before the integrator**. Unknown constraints, a non-physics constraint,
728
+ unknown fields and non-finite/non-number values are refused atomically by name.
729
+ These methods require a live track: before `play`/`queue`, set, read and clear
730
+ refuse with `no live track` instead of creating one. The model and other players
731
+ sharing its assets are untouched.
732
+
733
+ `PhysicsParameterRead` contains detached readonly snapshots: `override` is the
734
+ pending patch or null; `effective` is the six values from the last active
735
+ evaluation, or null until evaluated. An inactive constraint can retain its last
736
+ active values within that live core instance. Setting or clearing changes no
737
+ pose, time or physics state, and **does not evaluate**: effective keeps the last
738
+ values until a normal evaluation (or an explicitly requested `repose()`). Clear
739
+ removes the patch; authored keys resume at the next evaluation. It preserves
740
+ integrated history and does not instantly undo the motion caused by old values.
741
+
742
+ The player retains the **current standing patch** across new play, seek, skin
743
+ replay and promotion to composed tracks. Like `environment()`, it records no
744
+ historical changes. **After a mid-run patch change, a backward seek recomputes the
745
+ past interval with the patch standing now**, under the current environment; it
746
+ does not reproduce the old trajectory across that change. New single tracks and
747
+ new composed tracks first evaluate their ordinary authored first pose, then
748
+ receive the standing patch as pending, with no extra adjustment callback. At
749
+ seek time 0, effective can therefore still be the initial authored value while
750
+ override already reports the patch. The next normal core evaluation uses it.
751
+ Changing entries on existing composed tracks follows that core instance's
752
+ lifetime. Existing seek/skin restrictions after composition remain unchanged.
753
+
754
+ Set/clear invalidate seek caches without re-posing. Seek snapshots retain the
755
+ public effective read alongside their held pose, so backward and exact cache
756
+ hits report that pose's evaluation rather than the frontier's latest evaluation.
757
+ Readback itself never advances, evaluates or resets physics.
758
+
688
759
  ### One load, several players
689
760
 
690
761
  One `loadModel` backs any number of players: the regions are cut once, and
@@ -1043,8 +1114,9 @@ name, and unloading is still elements first, bitmaps second.
1043
1114
  | dark (two-colour) tint: unsupported | `DomRenderer`: unsupported. `CanvasRenderer`: drawn | Carried as `dark` on a `PosedSlot`. The canvas mode's canvas2d fallback draws alpha only. |
1044
1115
  | `renderer.dispose()` | `player.dispose()` | Then `assets.dispose()`, as before. `DomRenderer.dispose()` and `CanvasRenderer.dispose()` exist for a renderer you constructed yourself. |
1045
1116
  | `skeleton.findBone('head').worldX`, `.worldY`, `.a` … `.d` | `player.bone('head')` | `{ x, y, a, b, c, d, rotationX, rotationY, scaleX, scaleY, active }`, the core's world transform for the pose the player holds (the setup pose before `play`). An unknown name is refused by name. Since 1.1.0. |
1046
- | `bone.scaleY = …` on a `Skeleton` between `state.apply` and `updateWorldTransform` | `RigPlayerOptions.adjust`, `player.adjust`, `player.repose()` | A function called once per pose with the pose's bone locals (`bones.bone('mouth').scaleY = v`). A write lasts one pose: the core poses every step from the setup pose, so write the value you want, every pose — nothing compounds. See *Adjusting bones*. Since 1.2.0. |
1117
+ | `bone.scaleY = …` on a `Skeleton` between `state.apply` and `updateWorldTransform` | `RigPlayerOptions.adjust`, `player.adjust`, `player.repose()` | A function `(bones, ctx)` called once per pose with the pose's bone locals and current-entry context (unary callbacks remain valid) (`bones.bone('mouth').scaleY = v`). A write lasts one pose: the core poses every step from the setup pose, so write the value you want, every pose — nothing compounds. See *Adjusting bones*. Since 1.2.0. |
1047
1118
  | `skeleton.getBounds(offset, size)` | `player.bounds()` | `{ x, y, width, height }` or null, world units, Y up: the box of every drawn region's corners and mesh's vertices, a slot under a clip counted inside the clip's own box. `ModelAssets.stage` remains the declared framing, not this box. Since 1.1.0. |
1119
+ | live named physics constraint tuning | `player.setPhysicsParameters(name, patch)`, `readPhysicsParameters(name)`, `clearPhysicsParameters(name)` | Six scalar fields, per-player pending patch plus last evaluated effective values. Clear resumes authored values without resetting history. Replay uses the current standing patch, with no historical journal. See *Live physics parameters*. |
1048
1120
  | named setup mesh geometry | `readSetupMesh(assets, { slot, attachment, skin? })` | Detached full-double world x/y (Y up), triangles and hull for any named mesh/linked mesh, including non-setup selections; no player/time mutation. Selected geometry includes setup-slider deform; unselected geometry is stored geometry through unchanged setup matrices. See the measured official-runtime distinction under *Reading the pose*. |
1049
1121
 
1050
1122
  ### No counterpart in 1.0.0
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { type CanvasBackend, CanvasRenderer, type CanvasViewport } from './Canva
2
2
  export { DomRenderer, type MeshBackend, type MeshSourceRect } from './DomRenderer.js';
3
3
  export { type PageLayout, type PageRegion, type RegionEncoding, type RegionImage, revokeRegions, type UnpackRegionsOptions, unpackRegions } from './DomTexture.js';
4
4
  export { type LoadModelOptions, loadModel, type ModelAssets, type ModelDocument, readSetupMesh, type RegionLoading, type RigSet, type SetupMesh, type SetupMeshTarget } from './loadModel.js';
5
- export { type AdjustableBones, type BoneAdjust, type BoneLocals, type BoneWorld, type PlayOptions, type PoseBounds, type QueueOptions, type RenderMode, type RigComplete, type RigEnvironment, type RigEvent, RigPlayer, type RigPlayerOptions, type SlotFilter, type SlotIdentity, type SlotIdentityReason, type SlotIdentitySnapshot, type TrackEntry } from './player.js';
5
+ export { type AdjustableBones, type BoneAdjust, type BoneAdjustContext, type BoneLocals, type BoneWorld, type PhysicsParameters, type PhysicsParameterPatch, type PhysicsParameterRead, type PlayOptions, type PoseBounds, type QueueOptions, type RenderMode, type RigComplete, type RigEnvironment, type RigEvent, RigPlayer, type RigPlayerOptions, type SlotFilter, type SlotIdentity, type SlotIdentityReason, type SlotIdentitySnapshot, type TrackEntry } from './player.js';
6
6
  export type { BlendMode, PosedClip, PosedFrame, PosedMesh, PosedRegion, PosedSlot, } from './posedFrame.js';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnK,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9L,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC7W,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnK,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9L,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,KAAK,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACpd,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -436,23 +436,23 @@ function slotSwitches(slots, slot) {
436
436
  }
437
437
  return index.get(slot);
438
438
  }
439
- function applyAttachment(now, values, timelines, t, blend, evalDeform, blendOf2, evalFrame) {
440
- if (now === null || now.identity === null) return values;
439
+ function applyAttachment(now, values2, timelines, t, blend, evalDeform, blendOf2, evalFrame) {
440
+ if (now === null || now.identity === null) return values2;
441
441
  const matching = matchingAttachments(timelines, now.identity);
442
- if (matching === void 0) return values;
443
- values ??= { deform: null, frame: null, seen: [] };
444
- if (!values.seen.includes(now.identity)) values.seen.push(now.identity);
442
+ if (matching === void 0) return values2;
443
+ values2 ??= { deform: null, frame: null, seen: [] };
444
+ if (!values2.seen.includes(now.identity)) values2.seen.push(now.identity);
445
445
  for (const a of matching) {
446
446
  if (a.deform !== null && now.vertices !== null) {
447
447
  const target = evalDeform(now.vertices, a.deform, t);
448
- if (target !== null) values.deform = blend === null ? target : blendOf2(now.vertices, values.deform, target, blend.alpha, blend.additive);
448
+ if (target !== null) values2.deform = blend === null ? target : blendOf2(now.vertices, values2.deform, target, blend.alpha, blend.additive);
449
449
  }
450
450
  if (a.sequence !== null && now.count !== void 0) {
451
451
  const f = evalFrame(a.sequence, now.count, t);
452
- if (f !== null) values.frame = f;
452
+ if (f !== null) values2.frame = f;
453
453
  }
454
454
  }
455
- return values;
455
+ return values2;
456
456
  }
457
457
  function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {}, mixed) {
458
458
  const evalDeform = plant.deform ?? deformAt;
@@ -466,9 +466,9 @@ function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {
466
466
  const live = gate(doc, slot, active);
467
467
  let placeholder = placeholders.get(slot.name) ?? null;
468
468
  const base = live ? mixed?.get(slot.name) : void 0;
469
- let values = base === void 0 || base.deform === null && base.seen.length === 0 ? null : { deform: base.deform, frame: null, seen: [...base.seen] };
469
+ let values2 = base === void 0 || base.deform === null && base.seen.length === 0 ? null : { deform: base.deform, frame: null, seen: [...base.seen] };
470
470
  let now = identityOf(doc, resolve, slot, placeholder);
471
- if (sample !== null && live) values = applyAttachment(now, values, sample.timelines.attachments, sample.t, null, evalDeform, blendOf2, evalFrame);
471
+ if (sample !== null && live) values2 = applyAttachment(now, values2, sample.timelines.attachments, sample.t, null, evalDeform, blendOf2, evalFrame);
472
472
  if (live) for (const app of sliders) {
473
473
  const switches = slotSwitches(app.timelines.slots, slot.name);
474
474
  if (switches !== void 0) for (const keys of switches) {
@@ -476,26 +476,26 @@ function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {
476
476
  if (i < 0) continue;
477
477
  const next = keys[i].name ?? null;
478
478
  if (next !== placeholder) {
479
- if (values !== null) {
480
- values.deform = null;
481
- values.frame = null;
479
+ if (values2 !== null) {
480
+ values2.deform = null;
481
+ values2.frame = null;
482
482
  }
483
483
  placeholder = next;
484
484
  now = identityOf(doc, resolve, slot, placeholder);
485
485
  }
486
486
  }
487
- values = applyAttachment(now, values, app.timelines.attachments, app.at, app, evalDeform, blendOf2, evalFrame);
487
+ values2 = applyAttachment(now, values2, app.timelines.attachments, app.at, app, evalDeform, blendOf2, evalFrame);
488
488
  }
489
489
  if (now === null) continue;
490
- const entry = values === null || values.deform === null && values.frame === null ? now.entry : { ...now.entry };
490
+ const entry = values2 === null || values2.deform === null && values2.frame === null ? now.entry : { ...now.entry };
491
491
  shown.push(entry);
492
- if (values !== null && values.seen.length > 0 && now.fillers.length > 1) {
493
- why.push(`slot "${slot.name}" shows placeholder "${now.entry.placeholder}", which skins ${now.fillers.join(", ")} fill, and ${values.seen.map((x) => `"${x}"`).join(", ")} key(s) it — which record --skin all shows, and so whether the timeline moves it, is the Spine file's skin order, not the model's`);
492
+ if (values2 !== null && values2.seen.length > 0 && now.fillers.length > 1) {
493
+ why.push(`slot "${slot.name}" shows placeholder "${now.entry.placeholder}", which skins ${now.fillers.join(", ")} fill, and ${values2.seen.map((x) => `"${x}"`).join(", ")} key(s) it — which record --skin all shows, and so whether the timeline moves it, is the Spine file's skin order, not the model's`);
494
494
  continue;
495
495
  }
496
- if (values !== null) {
497
- if (values.deform !== null) entry.deform = values.deform;
498
- if (values.frame !== null) entry.frame = values.frame;
496
+ if (values2 !== null) {
497
+ if (values2.deform !== null) entry.deform = values2.deform;
498
+ if (values2.frame !== null) entry.frame = values2.frame;
499
499
  }
500
500
  }
501
501
  return { shown, why };
@@ -1236,16 +1236,16 @@ function readPathGeometry(raw, vertices, where, problems) {
1236
1236
  if (problems.length !== before) return void 0;
1237
1237
  return { kind: "path", vertices, closed: raw.closed === true, constantSpeed: raw.constantSpeed !== false, lengths };
1238
1238
  }
1239
- function roundedRow(values, round) {
1240
- for (let i = 0; i < values.length; i++) {
1241
- const r = round(values[i]);
1242
- if (Object.is(r, values[i])) continue;
1243
- const out = values.slice(0, i);
1239
+ function roundedRow(values2, round) {
1240
+ for (let i = 0; i < values2.length; i++) {
1241
+ const r = round(values2[i]);
1242
+ if (Object.is(r, values2[i])) continue;
1243
+ const out = values2.slice(0, i);
1244
1244
  out.push(r);
1245
- for (let j = i + 1; j < values.length; j++) out.push(round(values[j]));
1245
+ for (let j = i + 1; j < values2.length; j++) out.push(round(values2[j]));
1246
1246
  return out;
1247
1247
  }
1248
- return values;
1248
+ return values2;
1249
1249
  }
1250
1250
  function poseGeometry(shown, world, sourceOf, round, plant = {}, draw) {
1251
1251
  const region = plant.region ?? regionCorners;
@@ -3431,7 +3431,8 @@ function applyConstraints(bones, world, active, records, previous = null, applie
3431
3431
  if (c.kind === "physics") {
3432
3432
  if (physics !== void 0) {
3433
3433
  const w = { ...state.world.get(c.bone) };
3434
- const posed = physicsPose?.records.get(c.name) ?? c;
3434
+ const authored = physicsPose?.records.get(c.name) ?? c;
3435
+ const posed = physics.parameters?.apply(authored) ?? authored;
3435
3436
  if ((physics.step ?? stepPhysics)(posed, w, bone(state, c.bone).length ?? 0, physics)) {
3436
3437
  state.world.set(c.bone, w);
3437
3438
  changed = [c.bone];
@@ -3712,7 +3713,7 @@ function readBoneTimeline(kind, keysValue, where, problems) {
3712
3713
  }
3713
3714
  const channels = BONE_TIMELINE_CHANNELS[kind];
3714
3715
  unknownKeyFields(raw, ["time", ...channels, "curve"], at, problems);
3715
- const values = channels.map((c) => {
3716
+ const values2 = channels.map((c) => {
3716
3717
  const v = raw[c];
3717
3718
  if (typeof v !== "number" || !Number.isFinite(v)) {
3718
3719
  problems.push(`${at}: ${c} is ${JSON.stringify(v)}, not a finite number — the writer states every channel on every key`);
@@ -3720,7 +3721,7 @@ function readBoneTimeline(kind, keysValue, where, problems) {
3720
3721
  }
3721
3722
  return v;
3722
3723
  });
3723
- return { time: Math.fround(time), values: values.map(Math.fround), stated: { time, values }, curve: readCurve(raw, channels.length, i === listed.length - 1, at, problems) };
3724
+ return { time: Math.fround(time), values: values2.map(Math.fround), stated: { time, values: values2 }, curve: readCurve(raw, channels.length, i === listed.length - 1, at, problems) };
3724
3725
  });
3725
3726
  }
3726
3727
  function hexChannels(hex, count) {
@@ -3737,22 +3738,22 @@ function readSlotTimeline(kind, keysValue, where, problems) {
3737
3738
  return { time: Math.fround(time), values: [], stated: { time, values: [] }, curve: "stepped", name: typeof raw.name === "string" ? raw.name : null };
3738
3739
  }
3739
3740
  const shape = SLOT_COLOUR_TIMELINES[kind];
3740
- let values = [];
3741
+ let values2 = [];
3741
3742
  if (kind === "alpha") {
3742
3743
  unknownKeyFields(raw, ["time", "value", "curve"], at, problems);
3743
3744
  const v = raw.value;
3744
3745
  if (typeof v !== "number" || !Number.isFinite(v)) problems.push(`${at}: value is ${JSON.stringify(v)}, not a finite number`);
3745
- else values = [v];
3746
+ else values2 = [v];
3746
3747
  } else {
3747
3748
  unknownKeyFields(raw, ["time", ...shape.fields.map(([f]) => f), "curve"], at, problems);
3748
3749
  for (const [field, digits] of shape.fields) {
3749
3750
  const v = raw[field];
3750
3751
  if (!isHex(v, digits)) problems.push(`${at}: ${field} is ${JSON.stringify(v)}, not ${digits} hex digits — the one spelling the writer gives a ${kind} key's ${field}`);
3751
- else values.push(...hexChannels(v, field === "dark" || digits === 6 ? 3 : 4));
3752
+ else values2.push(...hexChannels(v, field === "dark" || digits === 6 ? 3 : 4));
3752
3753
  }
3753
3754
  }
3754
- if (values.length !== shape.channels.length) values = shape.channels.map(() => 0);
3755
- return { time: Math.fround(time), values: values.map(Math.fround), stated: { time, values }, curve: readCurve(raw, shape.channels.length, i === listed.length - 1, at, problems) };
3755
+ if (values2.length !== shape.channels.length) values2 = shape.channels.map(() => 0);
3756
+ return { time: Math.fround(time), values: values2.map(Math.fround), stated: { time, values: values2 }, curve: readCurve(raw, shape.channels.length, i === listed.length - 1, at, problems) };
3756
3757
  });
3757
3758
  }
3758
3759
  function constraintKeyTimes(value, where, problems) {
@@ -4048,7 +4049,7 @@ class StepLocals {
4048
4049
  this.write("shearY", v);
4049
4050
  }
4050
4051
  }
4051
- function adjustLocals(doc, bones, adjust) {
4052
+ function adjustLocals(doc, bones, adjust, context) {
4052
4053
  const views = /* @__PURE__ */ new Map();
4053
4054
  const names = doc.bones.map((b) => b.name);
4054
4055
  adjust({
@@ -4063,7 +4064,7 @@ function adjustLocals(doc, bones, adjust) {
4063
4064
  }
4064
4065
  return view;
4065
4066
  }
4066
- });
4067
+ }, context);
4067
4068
  }
4068
4069
  const clamp01$1 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
4069
4070
  function posedSlots(doc, timelines, t, plant = {}, sliders = [], placeholders) {
@@ -4150,7 +4151,7 @@ function posedBoneStep(doc, timelines, t, plant, constraints, sliders, step) {
4150
4151
  const bones = posedBones(doc, timelines, t, plant);
4151
4152
  if (constraints !== void 0 && step !== void 0) return steppedLocals(doc, bones, plant, () => pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, sliders, step);
4152
4153
  const active = activeBones(doc);
4153
- if (step?.adjust !== void 0) adjustLocals(doc, bones, step.adjust);
4154
+ if (step?.adjustment !== void 0) adjustLocals(doc, bones, step.adjustment.callback, step.adjustment.context);
4154
4155
  let world = (plant.evaluate ?? worldTransforms)(bones, active);
4155
4156
  if (constraints !== void 0) {
4156
4157
  const setupRecords = constraintRecords(doc);
@@ -4163,7 +4164,7 @@ function posedBoneStep(doc, timelines, t, plant, constraints, sliders, step) {
4163
4164
  }
4164
4165
  function steppedLocals(doc, bones, plant, records, physicsKeyed, t, sliders, step) {
4165
4166
  const active = activeBones(doc);
4166
- if (step.adjust !== void 0) adjustLocals(doc, bones, step.adjust);
4167
+ if (step.adjustment !== void 0) adjustLocals(doc, bones, step.adjustment.callback, step.adjustment.context);
4167
4168
  let world = (plant.evaluate ?? worldTransforms)(bones, active);
4168
4169
  const stepped = stepPhysicsRecords(records(), physicsKeyed, t, active, step.ctx, step.before);
4169
4170
  world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(stepped) : stepped, null, sliders, step.ctx, void 0, solverRules(plant.solver));
@@ -5021,11 +5022,11 @@ function drawnGeometry(doc, shown, world, order, plant, mode) {
5021
5022
  if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
5022
5023
  return { rows: geometry.attachments, clips: geometry.clips, drawnClipped: geometry.drawnClipped };
5023
5024
  }
5024
- function numbersOf(values, what, mode) {
5025
- return allNumbers(values) ? values : values.map((v) => numberOf(v, what, mode));
5025
+ function numbersOf(values2, what, mode) {
5026
+ return allNumbers(values2) ? values2 : values2.map((v) => numberOf(v, what, mode));
5026
5027
  }
5027
- function allNumbers(values) {
5028
- for (const v of values) if (v === null) return false;
5028
+ function allNumbers(values2) {
5029
+ for (const v of values2) if (v === null) return false;
5029
5030
  return true;
5030
5031
  }
5031
5032
  function rawDrawn(doc, shown, world, order, plant, mode = RAW_MODE) {
@@ -5171,6 +5172,7 @@ function openWalk(doc, anim, plant, reset, mode, shape, adjust, before) {
5171
5172
  const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
5172
5173
  if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
5173
5174
  const ctx = freshStepContext(plant.physicsStep);
5175
+ before?.(ctx);
5174
5176
  if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
5175
5177
  const track = { doc, anim, duration: anim.timelines.duration, mode, plant, raw: { ...plant, ...roundPlant(mode) }, resolve: plant.shown ?? shownAttachment, shape, ctx, adjust, trackTime: 0, last: -1 };
5176
5178
  if (reset === "setup") {
@@ -5193,8 +5195,9 @@ function stepWalk(track, dt, first = false) {
5193
5195
  ctx.time += mode.clock === void 0 ? dt : mode.clock(dt, t - (first ? 0 : Math.max(last, 0)));
5194
5196
  const fire = plant.events ?? eventsFired;
5195
5197
  const keys = anim.timelines.events;
5198
+ const adjustment = adjust === void 0 ? void 0 : { callback: adjust, context: { trackTime, animation: anim.name, globalTime: trackTime } };
5196
5199
  const pose = poseStepFrom(doc, plant, resolve, shape, mode, ctx, first, trackTime, t, {
5197
- 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 }),
5200
+ 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 }),
5198
5201
  slots: (sliders, placeholders) => posedSlots(doc, anim.timelines, t, raw, sliders, placeholders),
5199
5202
  drawOrder: anim.timelines.drawOrder,
5200
5203
  at: t,
@@ -5242,12 +5245,56 @@ function openRawTracksPath(doc, plant) {
5242
5245
  step: (first, trackTime, animationTime, posers) => poseStepFrom(doc, plant, resolve, RAW_SHAPE, RAW_MODE, ctx, first, trackTime, animationTime, posers)
5243
5246
  };
5244
5247
  }
5245
- function openRawTrack(doc, animation, plant, reset, mode, adjust) {
5246
- return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE, adjust);
5248
+ function openRawTrack(doc, animation, plant, reset, mode, adjust, before) {
5249
+ return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE, adjust, before);
5247
5250
  }
5248
5251
  function stepRawTrack(track, dt) {
5249
5252
  return stepWalk(track, dt);
5250
5253
  }
5254
+ const FIELDS = ["strength", "damping", "inertia", "wind", "gravity", "mix"];
5255
+ const values = (r) => ({ strength: r.strength, damping: r.damping, inertia: r.inertia, wind: r.wind, gravity: r.gravity, mix: r.mix });
5256
+ class LivePhysicsParameters {
5257
+ constructor(doc) {
5258
+ this.doc = doc;
5259
+ this.overrides = /* @__PURE__ */ new Map();
5260
+ this.evaluated = /* @__PURE__ */ new Map();
5261
+ }
5262
+ named(name) {
5263
+ const constraint = this.doc.constraints.find((c) => c.name === name);
5264
+ if (constraint === void 0) throw new CoreInputError(`physics parameters: no constraint "${String(name)}"; this document declares [${this.doc.constraints.map((c) => c.name).join(", ")}]`);
5265
+ if (constraint.kind !== "physics") throw new CoreInputError(`physics parameters: constraint "${name}" is ${constraint.kind}, not physics`);
5266
+ }
5267
+ set(name, patch) {
5268
+ this.named(name);
5269
+ 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(", ")}]`);
5270
+ const checked = {};
5271
+ for (const key of Reflect.ownKeys(patch)) {
5272
+ if (typeof key !== "string" || !FIELDS.includes(key)) throw new CoreInputError(`physics parameters: constraint "${name}" field "${String(key)}" is unknown; expected [${FIELDS.join(", ")}]`);
5273
+ const field = key;
5274
+ const value = patch[field];
5275
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new CoreInputError(`physics parameters: constraint "${name}" field "${field}" is ${String(value)}, not a finite number`);
5276
+ checked[field] = value;
5277
+ }
5278
+ if (Reflect.ownKeys(checked).length > 0) this.overrides.set(name, { ...this.overrides.get(name), ...checked });
5279
+ }
5280
+ read(name) {
5281
+ this.named(name);
5282
+ const patch = this.overrides.get(name);
5283
+ const effective = this.evaluated.get(name);
5284
+ return { override: patch === void 0 ? null : { ...patch }, effective: effective === void 0 ? null : { ...effective } };
5285
+ }
5286
+ clear(name) {
5287
+ this.named(name);
5288
+ this.overrides.delete(name);
5289
+ }
5290
+ /** Called only for an active constraint, after the sliders and immediately before its step. */
5291
+ apply(record) {
5292
+ const patch = this.overrides.get(record.name);
5293
+ const posed = patch === void 0 ? record : { ...record, ...patch };
5294
+ this.evaluated.set(record.name, values(posed));
5295
+ return posed;
5296
+ }
5297
+ }
5251
5298
  const HELD = { loop: false, keep: false };
5252
5299
  const LOOPED = { loop: true, keep: false, live: true };
5253
5300
  function openTrack(doc, animation, options, plant = {}) {
@@ -5257,11 +5304,15 @@ function openTrack(doc, animation, options, plant = {}) {
5257
5304
  if (options.clock !== void 0 && typeof options.clock !== "function") throw new CoreInputError(`the live track's options.clock is ${typeof options.clock}, not a function`);
5258
5305
  const mode = options.loop ? LOOPED : HELD;
5259
5306
  const clock = options.clock === void 0 ? void 0 : checkedClock(options.clock, "the live track's");
5260
- const { track, first } = openRawTrack(doc, animation, plant, options.reset, clock === void 0 ? mode : { ...mode, clock: (dt) => clock(dt) }, options.adjust);
5261
- return new Track(track, first);
5307
+ const parameters = new LivePhysicsParameters(doc);
5308
+ const { track, first } = openRawTrack(doc, animation, plant, options.reset, clock === void 0 ? mode : { ...mode, clock: (dt) => clock(dt) }, options.adjust, (ctx) => {
5309
+ ctx.parameters = parameters;
5310
+ });
5311
+ return new Track(track, first, parameters);
5262
5312
  }
5263
5313
  class Track {
5264
- constructor(track, first) {
5314
+ constructor(track, first, parameters) {
5315
+ this.parameters = parameters;
5265
5316
  this.steps = 0;
5266
5317
  this.refused = null;
5267
5318
  this.track = track;
@@ -5284,6 +5335,17 @@ class Track {
5284
5335
  this.live("setEnvironment");
5285
5336
  setEnvironmentOn(this.track.ctx, environment, "the live track's");
5286
5337
  }
5338
+ setPhysicsParameters(name, patch) {
5339
+ this.live("setPhysicsParameters");
5340
+ this.parameters.set(name, patch);
5341
+ }
5342
+ readPhysicsParameters(name) {
5343
+ return this.parameters.read(name);
5344
+ }
5345
+ clearPhysicsParameters(name) {
5346
+ this.live("clearPhysicsParameters");
5347
+ this.parameters.clear(name);
5348
+ }
5287
5349
  physicsTranslate(x, y) {
5288
5350
  this.live("physicsTranslate");
5289
5351
  translateOn(this.track.ctx, x, y, "the live track's");
@@ -6215,6 +6277,8 @@ class Tracks {
6215
6277
  this.time = 0;
6216
6278
  this.refused = null;
6217
6279
  this.mixer = new Mixer(doc, plant);
6280
+ this.parameters = new LivePhysicsParameters(doc);
6281
+ this.path.ctx.parameters = this.parameters;
6218
6282
  }
6219
6283
  get trackTime() {
6220
6284
  return this.time;
@@ -6226,6 +6290,17 @@ class Tracks {
6226
6290
  this.live("setEnvironment");
6227
6291
  setEnvironmentOn(this.path.ctx, environment, "the live tracks'");
6228
6292
  }
6293
+ setPhysicsParameters(name, patch) {
6294
+ this.live("setPhysicsParameters");
6295
+ this.parameters.set(name, patch);
6296
+ }
6297
+ readPhysicsParameters(name) {
6298
+ return this.parameters.read(name);
6299
+ }
6300
+ clearPhysicsParameters(name) {
6301
+ this.live("clearPhysicsParameters");
6302
+ this.parameters.clear(name);
6303
+ }
6229
6304
  physicsTranslate(x, y) {
6230
6305
  this.live("physicsTranslate");
6231
6306
  translateOn(this.path.ctx, x, y, "the live tracks'");
@@ -6330,9 +6405,14 @@ class Tracks {
6330
6405
  ctx.time += this.clock === void 0 ? dt : this.clock(dt);
6331
6406
  const pose = new Pose(doc, this.plant);
6332
6407
  const animationTime = this.mixer.apply(pose);
6408
+ const entry = this.mixer.tracks[0] ?? null;
6409
+ const adjustment = this.adjust === void 0 ? void 0 : {
6410
+ callback: this.adjust,
6411
+ context: { trackTime: entry?.trackTime ?? null, animation: entry?.anim.name ?? null, globalTime: this.time }
6412
+ };
6333
6413
  const mixed = new Map(doc.slots.map((s, k) => [s.name, { deform: pose.slots[k].deform, seen: pose.slots[k].seen }]));
6334
6414
  return path.step(first, this.time, animationTime, {
6335
- world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjust: this.adjust }),
6415
+ world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjustment }),
6336
6416
  slots: (sliders, placeholders) => slotRowsFrom(doc, path.raw, sliders, placeholders, (_slot, _live) => {
6337
6417
  const s = pose.slots[doc.slots.indexOf(_slot)];
6338
6418
  return { placeholder: s.placeholder, light: s.light, dark: s.dark };
@@ -9104,12 +9184,16 @@ class RigPlayer {
9104
9184
  setsByName;
9105
9185
  activeSet = null;
9106
9186
  adjustFn = null;
9187
+ /** Current preview patches, reapplied through the public core API on replay; no edit history. */
9188
+ physicsPatches = /* @__PURE__ */ new Map();
9189
+ /** Public read snapshots belonging to a restored seek pose, not the core's private evaluation cache. */
9190
+ heldParameters = null;
9107
9191
  slotFilter = null;
9108
9192
  /** `slotFilter` as a predicate; every slot when null. */
9109
9193
  draws = () => true;
9110
9194
  /** Handed to every track the player opens; reads the current `adjust` so setting it later needs no reopen. */
9111
- adjustHook = (locals) => {
9112
- if (this.adjustFn !== null) this.adjustFn(locals);
9195
+ adjustHook = (locals, ctx) => {
9196
+ if (this.adjustFn !== null) this.adjustFn(locals, ctx);
9113
9197
  };
9114
9198
  listeners = {
9115
9199
  event: /* @__PURE__ */ new Set(),
@@ -9206,7 +9290,7 @@ class RigPlayer {
9206
9290
  this.animation = name;
9207
9291
  this.loop = loop;
9208
9292
  this.track = openTrack(this.view, name, { loop, reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9209
- this.setRawPose(this.track.first);
9293
+ this.setRawPose(this.initializeSingle(this.track));
9210
9294
  this.applyEnvironment(this.track);
9211
9295
  return;
9212
9296
  }
@@ -9312,6 +9396,7 @@ class RigPlayer {
9312
9396
  this.tracks = tracks;
9313
9397
  this.track = null;
9314
9398
  this.setRawPose(tracks.start());
9399
+ this.applyPhysicsParameters(tracks);
9315
9400
  this.applyEnvironment(tracks);
9316
9401
  if (time > 0) this.stepTo(time);
9317
9402
  return tracks;
@@ -9319,10 +9404,54 @@ class RigPlayer {
9319
9404
  /** Stores `raw` and applies `activeSet` when one is active. Zero work when `activeSet === null`. */
9320
9405
  setRawPose(raw) {
9321
9406
  this.rawPose = raw;
9407
+ this.heldParameters = null;
9322
9408
  this.pose = this.activeSet === null ? raw : applySetToPose(this.view, raw, this.activeSet);
9323
9409
  this.poseGeneration++;
9324
9410
  this.cachedIdentity = null;
9325
9411
  }
9412
+ /**
9413
+ * Sets a named physics constraint's pending patch, after keyed/slider values
9414
+ * and before the integrator on the next evaluation. No pose or clock change.
9415
+ * Unknown names/kinds/fields and non-finite values are refused atomically by
9416
+ * the core. Requires a live track (call play/queue first).
9417
+ */
9418
+ setPhysicsParameters(name, patch) {
9419
+ const live = this.requirePhysicsTrack("setPhysicsParameters", name);
9420
+ live.setPhysicsParameters(name, patch);
9421
+ const standing = live.readPhysicsParameters(name).override;
9422
+ if (standing !== null) this.physicsPatches.set(name, standing);
9423
+ this.clearSeekCache();
9424
+ }
9425
+ /** Pending override and the held pose's last active evaluation, detached. Does not evaluate a pose. */
9426
+ readPhysicsParameters(name) {
9427
+ const read = this.requirePhysicsTrack("readPhysicsParameters", name).readPhysicsParameters(name);
9428
+ if (this.heldParameters === null) return read;
9429
+ const effective = this.heldParameters.get(name)?.effective ?? null;
9430
+ return { override: read.override, effective: effective === null ? null : { ...effective } };
9431
+ }
9432
+ /** Removes only the patch; authored values resume next evaluation. Physics history is retained. */
9433
+ clearPhysicsParameters(name) {
9434
+ this.requirePhysicsTrack("clearPhysicsParameters", name).clearPhysicsParameters(name);
9435
+ this.physicsPatches.delete(name);
9436
+ this.clearSeekCache();
9437
+ }
9438
+ requirePhysicsTrack(operation, name) {
9439
+ const live = this.stepper();
9440
+ if (live === null) throw new Error(`${operation}(${JSON.stringify(name)}) before play: no live track`);
9441
+ return live;
9442
+ }
9443
+ /** No private parameter state is copied into the core's seek snapshots. */
9444
+ readParameters(live) {
9445
+ return new Map(this.view.constraints.filter((c) => c.kind === "physics").map((c) => [c.name, live.readPhysicsParameters(c.name)]));
9446
+ }
9447
+ applyPhysicsParameters(live) {
9448
+ for (const [name, patch] of this.physicsPatches) live.setPhysicsParameters(name, patch);
9449
+ }
9450
+ /** Like the environment, standing patches are pending after the ordinary first pose; no extra evaluation. */
9451
+ initializeSingle(live) {
9452
+ this.applyPhysicsParameters(live);
9453
+ return live.first;
9454
+ }
9326
9455
  /**
9327
9456
  * The world the rig's physics answers (#121): `wind` and `gravity`, as
9328
9457
  * spine-core's skeleton-level `windX/windY` and `gravityX/gravityY` — x to
@@ -9401,6 +9530,7 @@ class RigPlayer {
9401
9530
  restoreStepperState(stepper, exactHit.state);
9402
9531
  this.rawPose = exactHit.rawPose;
9403
9532
  this.pose = exactHit.pose;
9533
+ this.heldParameters = exactHit.parameters;
9404
9534
  this.poseGeneration++;
9405
9535
  this.cachedIdentity = null;
9406
9536
  return;
@@ -9412,24 +9542,32 @@ class RigPlayer {
9412
9542
  if (cache.frontierSteps <= SEEK_FINE_STEP_LIMIT || cache.frontierSteps % SEEK_COARSE_STRIDE === 0) {
9413
9543
  cache.grid.set(cache.frontierSteps, {
9414
9544
  state: captureStepperState(cache.frontier),
9415
- rawPose: raw
9545
+ rawPose: raw,
9546
+ parameters: this.readParameters(cache.frontier)
9416
9547
  });
9417
9548
  }
9418
9549
  }
9550
+ let parameters;
9419
9551
  const gridHit = cache.grid.get(steps);
9420
9552
  if (gridHit !== void 0) {
9421
9553
  restoreStepperState(stepper, gridHit.state);
9422
9554
  this.rawPose = gridHit.rawPose;
9555
+ parameters = gridHit.parameters;
9423
9556
  } else {
9424
9557
  const baseStep = steps - steps % SEEK_COARSE_STRIDE;
9425
9558
  const baseSnap = cache.grid.get(baseStep);
9426
9559
  restoreStepperState(stepper, baseSnap.state);
9427
9560
  this.rawPose = baseSnap.rawPose;
9428
9561
  for (let i = baseStep; i < steps; i++) this.rawPose = stepper.step(this.seekStep);
9562
+ parameters = this.readParameters(stepper);
9429
9563
  }
9430
9564
  const rest = time - stepper.trackTime;
9431
- if (rest > 0) this.rawPose = stepper.step(rest);
9565
+ if (rest > 0) {
9566
+ this.rawPose = stepper.step(rest);
9567
+ parameters = this.readParameters(stepper);
9568
+ }
9432
9569
  if (this.rawPose !== null) this.setRawPose(this.rawPose);
9570
+ this.heldParameters = parameters;
9433
9571
  if (this.rawPose !== null && this.pose !== null) {
9434
9572
  if (cache.exact.size >= SEEK_EXACT_LIMIT) {
9435
9573
  const oldest = cache.exact.keys().next().value;
@@ -9438,7 +9576,8 @@ class RigPlayer {
9438
9576
  cache.exact.set(time, {
9439
9577
  state: captureStepperState(stepper),
9440
9578
  rawPose: this.rawPose,
9441
- pose: this.pose
9579
+ pose: this.pose,
9580
+ parameters
9442
9581
  });
9443
9582
  }
9444
9583
  }
@@ -9459,16 +9598,17 @@ class RigPlayer {
9459
9598
  const tracks = openTracks(this.view, { reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9460
9599
  tracks.setAnimation(0, this.animation, { loop: this.loop, mixDuration: 0 });
9461
9600
  firstPose = tracks.start();
9601
+ this.applyPhysicsParameters(tracks);
9462
9602
  this.applyEnvironment(tracks);
9463
9603
  frontier = tracks;
9464
9604
  } else {
9465
9605
  const track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9466
- firstPose = track.first;
9606
+ firstPose = this.initializeSingle(track);
9467
9607
  this.applyEnvironment(track);
9468
9608
  frontier = track;
9469
9609
  }
9470
9610
  const grid = /* @__PURE__ */ new Map();
9471
- grid.set(0, { state: captureStepperState(frontier), rawPose: firstPose });
9611
+ grid.set(0, { state: captureStepperState(frontier), rawPose: firstPose, parameters: this.readParameters(frontier) });
9472
9612
  this.seekCache = {
9473
9613
  mode,
9474
9614
  frontier,
@@ -9486,11 +9626,12 @@ class RigPlayer {
9486
9626
  this.trackCount = 1;
9487
9627
  this.tracks = tracks;
9488
9628
  this.setRawPose(tracks.start());
9629
+ this.applyPhysicsParameters(tracks);
9489
9630
  this.applyEnvironment(tracks);
9490
9631
  return;
9491
9632
  }
9492
9633
  this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9493
- this.setRawPose(this.track.first);
9634
+ this.setRawPose(this.initializeSingle(this.track));
9494
9635
  this.applyEnvironment(this.track);
9495
9636
  }
9496
9637
  /** What is open: the tracks, the single track, or nothing before `play`. */