rig-play 1.4.0 → 1.5.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/dist/index.js CHANGED
@@ -377,7 +377,7 @@ function timelineIdentity(slot, shown) {
377
377
  return `${shown.skin}/${slot}/${shown.placeholder}`;
378
378
  }
379
379
  const nameOf = (a) => `${a.skin}/${a.slot}/${a.attachment}`;
380
- function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {}) {
380
+ function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {}, mixed) {
381
381
  const evalDeform = plant.deform ?? deformAt;
382
382
  const blendOf = plant.deformBlend ?? blendDeform;
383
383
  const evalFrame = plant.sequence ?? sequenceFrameAt;
@@ -388,9 +388,10 @@ function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {
388
388
  for (const slot of doc.slots) {
389
389
  const live = gate(doc, slot, active);
390
390
  let placeholder = placeholders.get(slot.name) ?? null;
391
- let deform = null;
391
+ const base = live ? mixed?.get(slot.name) : void 0;
392
+ let deform = base?.deform ?? null;
392
393
  let frame2 = null;
393
- const seen = [];
394
+ const seen = base === void 0 ? [] : [...base.seen];
394
395
  const state = () => {
395
396
  if (placeholder === null) return null;
396
397
  const s = resolve(doc, { ...slot, setup: placeholder });
@@ -2450,7 +2451,7 @@ function applySliderPhysics(r, at, alpha, target) {
2450
2451
  }
2451
2452
  }
2452
2453
  }
2453
- const clamp01$1 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
2454
+ const clamp01$2 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
2454
2455
  const COLOUR_CHANNELS = { rgba: [0, 1, 2, 3], rgb: [0, 1, 2], alpha: [3], rgba2: [0, 1, 2, 3, 4, 5, 6], rgb2: [0, 1, 2, 4, 5, 6] };
2455
2456
  function applySliderSlots(slot, pose, applications) {
2456
2457
  for (const app of applications) {
@@ -2465,8 +2466,8 @@ function applySliderSlots(slot, pose, applications) {
2465
2466
  const v = valuesAt$1(tl.keys, app.at);
2466
2467
  if (v === null) continue;
2467
2468
  COLOUR_CHANNELS[tl.kind].forEach((at, i) => {
2468
- if (at < 4) pose.light[at] = app.alpha === 1 ? clamp01$1(v[i]) : clamp01$1(pose.light[at] + (v[i] - pose.light[at]) * app.alpha);
2469
- else if (pose.dark !== null) pose.dark[at - 4] = app.alpha === 1 ? clamp01$1(v[i]) : clamp01$1(pose.dark[at - 4] + (v[i] - pose.dark[at - 4]) * app.alpha);
2469
+ if (at < 4) pose.light[at] = app.alpha === 1 ? clamp01$2(v[i]) : clamp01$2(pose.light[at] + (v[i] - pose.light[at]) * app.alpha);
2470
+ else if (pose.dark !== null) pose.dark[at - 4] = app.alpha === 1 ? clamp01$2(v[i]) : clamp01$2(pose.dark[at - 4] + (v[i] - pose.dark[at - 4]) * app.alpha);
2470
2471
  });
2471
2472
  }
2472
2473
  }
@@ -3922,23 +3923,15 @@ function adjustLocals(doc, bones, adjust) {
3922
3923
  }
3923
3924
  });
3924
3925
  }
3925
- const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
3926
+ const clamp01$1 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
3926
3927
  function posedSlots(doc, timelines, t, plant = {}, sliders = [], placeholders) {
3927
- const resolve = plant.shown ?? shownAttachment;
3928
3928
  const colour = plant.colour ?? readColour;
3929
- const blend = plant.blend ?? readBlend;
3930
- const round = plant.round ?? gridRound;
3931
3929
  const byName = new Map(timelines.slots.map((s) => [s.name, s]));
3932
- const rows = [];
3933
- const conflicts = [];
3934
- const active = activeBones(doc);
3935
- const gate = plant.slotTimelines ?? slotTimelinesApply;
3936
- for (const slot of doc.slots) {
3930
+ return slotRowsFrom(doc, plant, sliders, placeholders, (slot, live) => {
3937
3931
  let placeholder = slot.setup;
3938
3932
  const light = slot.color === void 0 ? [1, 1, 1, 1] : colour(slot.color);
3939
3933
  const setupDark = slot.dark === void 0 ? null : readColour(slot.dark);
3940
3934
  const dark = setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]];
3941
- const live = gate(doc, slot, active);
3942
3935
  for (const tl of live ? byName.get(slot.name)?.timelines ?? [] : []) {
3943
3936
  if (tl.kind === "attachment") {
3944
3937
  const key = keyAt(tl.keys, t, plant);
@@ -3950,10 +3943,26 @@ function posedSlots(doc, timelines, t, plant = {}, sliders = [], placeholders) {
3950
3943
  const channels = SLOT_COLOUR_TIMELINES[tl.kind].channels;
3951
3944
  channels.forEach((name, i) => {
3952
3945
  const at = { r: 0, g: 1, b: 2, a: 3, r2: 4, g2: 5, b2: 6 }[name];
3953
- if (at < 4) light[at] = v === null ? setupLight[at] : clamp01(v[i]);
3954
- else if (dark !== null && setupDark !== null) dark[at - 4] = v === null ? setupDark[at - 4] : clamp01(v[i]);
3946
+ if (at < 4) light[at] = v === null ? setupLight[at] : clamp01$1(v[i]);
3947
+ else if (dark !== null && setupDark !== null) dark[at - 4] = v === null ? setupDark[at - 4] : clamp01$1(v[i]);
3955
3948
  });
3956
3949
  }
3950
+ return { placeholder, light, dark };
3951
+ });
3952
+ }
3953
+ function slotRowsFrom(doc, plant, sliders, placeholders, baseOf) {
3954
+ const resolve = plant.shown ?? shownAttachment;
3955
+ const blend = plant.blend ?? readBlend;
3956
+ const round = plant.round ?? gridRound;
3957
+ const rows = [];
3958
+ const conflicts = [];
3959
+ const active = activeBones(doc);
3960
+ const gate = plant.slotTimelines ?? slotTimelinesApply;
3961
+ for (const slot of doc.slots) {
3962
+ const live = gate(doc, slot, active);
3963
+ const base = baseOf(slot, live);
3964
+ let placeholder = base.placeholder;
3965
+ const { light, dark } = base;
3957
3966
  placeholders?.set(slot.name, placeholder);
3958
3967
  const pose = { placeholder, light, dark };
3959
3968
  if (live) applySliderSlots(slot.name, pose, sliders);
@@ -3996,14 +4005,12 @@ function stepRows(posed) {
3996
4005
  return posed.rows;
3997
4006
  }
3998
4007
  function posedBoneStep(doc, timelines, t, plant, constraints, sliders, step) {
3999
- const active = activeBones(doc);
4000
4008
  const bones = posedBones(doc, timelines, t, plant);
4009
+ 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);
4010
+ const active = activeBones(doc);
4001
4011
  if (step?.adjust !== void 0) adjustLocals(doc, bones, step.adjust);
4002
4012
  let world = (plant.evaluate ?? worldTransforms)(bones, active);
4003
- if (constraints !== void 0 && step !== void 0) {
4004
- const records = stepPhysicsRecords(pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, active, step.ctx, step.before);
4005
- world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(records) : records, null, sliders, step.ctx, void 0, solverRules(plant.solver));
4006
- } else if (constraints !== void 0) {
4013
+ if (constraints !== void 0) {
4007
4014
  const setupRecords = constraintRecords(doc);
4008
4015
  const records = pathDeformed(doc, posedRecords(setupRecords, constraints, t), timelines, t, plant);
4009
4016
  const previous = setupRecords.some((r) => r.kind === "path") ? applyConstraints(doc.bones, (plant.evaluate ?? worldTransforms)(doc.bones, active), active, plant.constraints ? plant.constraints(setupRecords) : setupRecords, null, void 0, void 0, void 0, solverRules(plant.solver)) : null;
@@ -4012,6 +4019,20 @@ function posedBoneStep(doc, timelines, t, plant, constraints, sliders, step) {
4012
4019
  for (const b of bones) if (world.get(b.name) === void 0) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
4013
4020
  return { bones, world, active, round: plant.round ?? gridRound };
4014
4021
  }
4022
+ function steppedLocals(doc, bones, plant, records, physicsKeyed, t, sliders, step) {
4023
+ const active = activeBones(doc);
4024
+ if (step.adjust !== void 0) adjustLocals(doc, bones, step.adjust);
4025
+ let world = (plant.evaluate ?? worldTransforms)(bones, active);
4026
+ const stepped = stepPhysicsRecords(records(), physicsKeyed, t, active, step.ctx, step.before);
4027
+ world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(stepped) : stepped, null, sliders, step.ctx, void 0, solverRules(plant.solver));
4028
+ for (const b of bones) if (world.get(b.name) === void 0) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
4029
+ return { bones, world, active, round: plant.round ?? gridRound };
4030
+ }
4031
+ function steppedLocalsWorld(doc, bones, plant, sliders, step, rows) {
4032
+ const posed = steppedLocals(doc, bones, plant, () => constraintRecords(doc), [], 0, sliders, step);
4033
+ stepRows(posed);
4034
+ return posed.world;
4035
+ }
4015
4036
  function pathDeformed(doc, records, timelines, t, plant) {
4016
4037
  if (timelines.attachments.every((a) => a.deform === null)) return records;
4017
4038
  return records.map((r) => {
@@ -4929,6 +4950,14 @@ function openWalk(doc, anim, plant, reset, mode, shape, adjust) {
4929
4950
  const ctx = freshStepContext(plant.physicsStep);
4930
4951
  if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
4931
4952
  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 };
4953
+ if (reset === "setup") {
4954
+ const setup = poseSetup(doc, { ...plant, ...roundPlant(mode) }, ctx);
4955
+ const absent = setup.absent.filter(([block]) => block !== "setup.clipped");
4956
+ if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join("; ")}`);
4957
+ if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError("the raw walk's reset pose was not posed");
4958
+ ctx.phase = "update";
4959
+ return { track, first: shape.setup(doc, setup.world, setup.setup.slots, setup.setup.drawOrder, setup.shown, plant, mode) };
4960
+ }
4932
4961
  return { track, first: stepWalk(track, 0, true) };
4933
4962
  }
4934
4963
  function stepWalk(track, dt, first = false) {
@@ -4939,26 +4968,56 @@ function stepWalk(track, dt, first = false) {
4939
4968
  const t = mode.loop ? (mode.time ?? loopedTime)(trackTime, duration, trackTime - dt) : trackTime < duration ? trackTime : duration;
4940
4969
  const before = ctx.time;
4941
4970
  ctx.time += mode.clock === void 0 ? dt : mode.clock(dt, t - (first ? 0 : Math.max(last, 0)));
4971
+ const fire = plant.events ?? eventsFired;
4972
+ const keys = anim.timelines.events;
4973
+ const pose = poseStepFrom(doc, plant, resolve, shape, mode, ctx, first, trackTime, t, {
4974
+ 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 }),
4975
+ slots: (sliders, placeholders) => posedSlots(doc, anim.timelines, t, raw, sliders, placeholders),
4976
+ drawOrder: anim.timelines.drawOrder,
4977
+ at: t,
4978
+ sample: { timelines: anim.timelines, t },
4979
+ events: () => mode.loop && mode.live !== true ? [] : mode.loop ? fire(keys, last, t, rawNumber, t < last) : fire(keys, last, t, rawNumber)
4980
+ });
4981
+ track.last = t;
4982
+ return pose;
4983
+ }
4984
+ function poseStepFrom(doc, plant, resolve, shape, mode, ctx, first, trackTime, animationTime, posers) {
4942
4985
  const sliders = [];
4943
- const world = 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 });
4986
+ const world = posers.world(sliders);
4944
4987
  if (first) ctx.phase = "update";
4945
4988
  const placeholders = /* @__PURE__ */ new Map();
4946
- const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
4989
+ const slots = posers.slots(sliders, placeholders);
4947
4990
  if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join("; ")}`);
4948
4991
  const evalOrder = plant.drawOrder ?? drawOrderAt;
4949
- let order = evalOrder(doc.slots.length, anim.timelines.drawOrder, t) ?? doc.slots.map((_s, k) => k);
4992
+ let order = evalOrder(doc.slots.length, posers.drawOrder, posers.at) ?? doc.slots.map((_s, k) => k);
4950
4993
  for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
4951
4994
  const drawOrder = order.map((k) => doc.slots[k].name);
4952
- const states = attachmentStates(doc, resolve, placeholders, { timelines: anim.timelines, t }, sliders, plant);
4995
+ const states = attachmentStates(doc, resolve, placeholders, posers.sample, sliders, plant, posers.mixed);
4953
4996
  if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join("; ")}`);
4954
4997
  const rank = new Map(drawOrder.map((n, r) => [n, r]));
4955
4998
  const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
4956
- const fire = plant.events ?? eventsFired;
4957
- const keys = anim.timelines.events;
4958
- const events = () => mode.loop && mode.live !== true ? [] : (mode.loop ? fire(keys, last, t, rawNumber, t < last) : fire(keys, last, t, rawNumber)).map((e) => [e[0], finite(e[1], "event time"), e[2], finite(e[3], "event float"), e[4]]);
4959
- const pose = shape.step(doc, { trackTime, animationTime: t, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
4960
- track.last = t;
4961
- return pose;
4999
+ const events = () => posers.events().map((e) => [e[0], finite(e[1], "event time"), e[2], finite(e[3], "event float"), e[4]]);
5000
+ return shape.step(doc, { trackTime, animationTime, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
5001
+ }
5002
+ function openRawTracksPath(doc, plant) {
5003
+ const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
5004
+ if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
5005
+ const ctx = freshStepContext(plant.physicsStep);
5006
+ const raw = { ...plant, ...roundPlant(RAW_MODE) };
5007
+ const resolve = plant.shown ?? shownAttachment;
5008
+ return {
5009
+ ctx,
5010
+ raw,
5011
+ setupPose: () => {
5012
+ const setup = poseSetup(doc, raw, ctx);
5013
+ const absent = setup.absent.filter(([block]) => block !== "setup.clipped");
5014
+ if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join("; ")}`);
5015
+ if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError("the raw walk's reset pose was not posed");
5016
+ ctx.phase = "update";
5017
+ return RAW_SHAPE.setup(doc, setup.world, setup.setup.slots, setup.setup.drawOrder, setup.shown, plant, RAW_MODE);
5018
+ },
5019
+ step: (first, trackTime, animationTime, posers) => poseStepFrom(doc, plant, resolve, RAW_SHAPE, RAW_MODE, ctx, first, trackTime, animationTime, posers)
5020
+ };
4962
5021
  }
4963
5022
  function openRawTrack(doc, animation, plant, reset, mode, adjust) {
4964
5023
  return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE, adjust);
@@ -4970,6 +5029,7 @@ const HELD = { loop: false, keep: false };
4970
5029
  const LOOPED = { loop: true, keep: false, live: true };
4971
5030
  function openTrack(doc, animation, options, plant = {}) {
4972
5031
  if (typeof options !== "object" || options === null || typeof options.loop !== "boolean") throw new CoreInputError(`the live track's options.loop is ${JSON.stringify(options?.loop)}, not true or false`);
5032
+ if (options.reset !== "animation" && options.reset !== "setup") throw new CoreInputError(`the live track's options.reset is ${JSON.stringify(options.reset)}, not "animation" or "setup"`);
4973
5033
  if (options.adjust !== void 0 && typeof options.adjust !== "function") throw new CoreInputError(`the live track's options.adjust is ${typeof options.adjust}, not a function`);
4974
5034
  const { track, first } = openRawTrack(doc, animation, plant, options.reset, options.loop ? LOOPED : HELD, options.adjust);
4975
5035
  return new Track(track, first);
@@ -5006,6 +5066,940 @@ class Track {
5006
5066
  }
5007
5067
  }
5008
5068
  }
5069
+ const CURRENT = 0;
5070
+ const SETUP = 1;
5071
+ const FIRST = 2;
5072
+ const MODE = 3;
5073
+ const HOLD = 4;
5074
+ const EMPTY = { name: null, duration: 0, timelines: [], ids: /* @__PURE__ */ new Set() };
5075
+ const BONE_PROPERTIES = {
5076
+ rotate: ["rotate"],
5077
+ translate: ["x", "y"],
5078
+ translatex: ["x"],
5079
+ translatey: ["y"],
5080
+ scale: ["scaleX", "scaleY"],
5081
+ scalex: ["scaleX"],
5082
+ scaley: ["scaleY"],
5083
+ shear: ["shearX", "shearY"],
5084
+ shearx: ["shearX"],
5085
+ sheary: ["shearY"],
5086
+ inherit: ["inherit"]
5087
+ };
5088
+ const COLOUR_PROPERTIES = { rgba: ["rgb", "alpha"], rgb: ["rgb"], alpha: ["alpha"], rgba2: ["rgb", "alpha", "rgb2"], rgb2: ["rgb", "rgb2"] };
5089
+ function mixAnimationOf(doc, name) {
5090
+ const anim = doc.animations.find((a) => a.name === name);
5091
+ if (anim === void 0) throw new CoreInputError(`animation "${name}" is not one of this document's [${doc.animations.map((a) => a.name).join(", ")}]`);
5092
+ const t = anim.timelines;
5093
+ const c = anim.constraints;
5094
+ const left = [];
5095
+ for (const [kind, n] of [["ik", c.ik.length], ["transform", c.transform.length], ["path", c.path.length], ["physics", c.physics + (c.physicsKeyed?.length ?? 0)], ["slider", c.slider.length]]) if (n > 0) left.push(`${kind} constraint timelines`);
5096
+ if (t.attachments.some((a) => a.sequence !== null)) left.push("a sequence timeline");
5097
+ if (t.drawOrder.length > 0) left.push("the draw order");
5098
+ for (const a of t.attachments) {
5099
+ if (a.deform === null) continue;
5100
+ const g = doc.skins.find((k) => k.name === a.skin)?.attachments[a.slot]?.[a.attachment]?.geometry;
5101
+ if (g?.kind === "path") left.push(`a deform of path attachment "${a.skin}/${a.slot}/${a.attachment}"`);
5102
+ }
5103
+ if (left.length > 0) throw new CoreInputError(`animation "${name}" keys ${left.join(", ")}, which the live tracks do not mix — they mix bone, slot colour, attachment and deform timelines, and fire events (issue #1336)`);
5104
+ const boneIndex2 = new Map(doc.bones.map((b, i) => [b.name, i]));
5105
+ const slotIndex = new Map(doc.slots.map((s, i) => [s.name, i]));
5106
+ const timelines = [];
5107
+ for (const target of t.slots) {
5108
+ const slot = slotIndex.get(target.name);
5109
+ for (const tl of target.timelines) {
5110
+ if (tl.kind === "attachment") timelines.push({ type: "attachment", slot, keys: tl.keys, ids: [`attachment|${target.name}`], instant: true });
5111
+ else timelines.push({ type: "colour", slot, kind: tl.kind, keys: tl.keys, ids: COLOUR_PROPERTIES[tl.kind].map((p) => `${p}|${target.name}`), instant: false });
5112
+ }
5113
+ }
5114
+ for (const target of t.bones) {
5115
+ const bone2 = boneIndex2.get(target.name);
5116
+ for (const tl of target.timelines) timelines.push({ type: "bone", bone: bone2, kind: tl.kind, keys: tl.keys, ids: BONE_PROPERTIES[tl.kind].map((p) => `${p}|${target.name}`), instant: tl.kind === "inherit" });
5117
+ }
5118
+ for (const a of t.attachments) {
5119
+ if (a.deform === null) continue;
5120
+ const identity = `${a.skin}/${a.slot}/${a.attachment}`;
5121
+ timelines.push({ type: "deform", identity, timeline: a, ids: [`deform|${a.slot}|${identity}`], instant: false });
5122
+ }
5123
+ if (t.events.length > 0) timelines.push({ type: "events", keys: t.events, ids: ["event"], instant: true });
5124
+ return { name, duration: t.duration, timelines, ids: new Set(timelines.flatMap((x) => x.ids)) };
5125
+ }
5126
+ function newEntry(anim, loop) {
5127
+ return {
5128
+ anim,
5129
+ loop,
5130
+ previous: null,
5131
+ next: null,
5132
+ mixingFrom: null,
5133
+ mixingTo: null,
5134
+ delay: 0,
5135
+ trackTime: 0,
5136
+ trackLast: -1,
5137
+ nextTrackLast: -1,
5138
+ trackEnd: Number.MAX_VALUE,
5139
+ animationLast: -1,
5140
+ nextAnimationLast: -1,
5141
+ mixTime: 0,
5142
+ mixDuration: 0,
5143
+ totalAlpha: 0,
5144
+ keepHold: false,
5145
+ timelineMode: [],
5146
+ timelineHoldMix: [],
5147
+ timelinesRotation: []
5148
+ };
5149
+ }
5150
+ function animationTimeOf(e) {
5151
+ if (!e.loop) return Math.min(e.trackTime + 0, e.anim.duration);
5152
+ const d = e.anim.duration;
5153
+ if (d === 0) return 0;
5154
+ return e.trackTime % d + 0;
5155
+ }
5156
+ function trackComplete(e) {
5157
+ const d = e.anim.duration;
5158
+ if (d !== 0) {
5159
+ if (e.loop) return d * (1 + (e.trackTime / d | 0));
5160
+ if (e.trackTime < d) return d;
5161
+ }
5162
+ return e.trackTime;
5163
+ }
5164
+ const signum = (v) => v > 0 ? 1 : v < 0 ? -1 : 0;
5165
+ const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
5166
+ const ATTACH_NONE = 0;
5167
+ const ATTACH_SETUP = 1;
5168
+ const ATTACH_RETAIN = 2;
5169
+ class Pose {
5170
+ constructor(doc, plant) {
5171
+ this.doc = doc;
5172
+ this.plant = plant;
5173
+ this.events = [];
5174
+ this.resolved = /* @__PURE__ */ new Map();
5175
+ this.bones = [...doc.bones];
5176
+ this.active = activeBones(doc);
5177
+ const colour = plant.colour ?? readColour;
5178
+ const gate = plant.slotTimelines ?? slotTimelinesApply;
5179
+ this.live = doc.slots.map((s) => gate(doc, s, this.active));
5180
+ this.slots = doc.slots.map((slot) => {
5181
+ const setupDark = slot.dark === void 0 ? null : readColour(slot.dark);
5182
+ return { placeholder: slot.setup, light: slot.color === void 0 ? [1, 1, 1, 1] : colour(slot.color), dark: setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]], deform: null, seen: [], attachmentState: ATTACH_NONE };
5183
+ });
5184
+ }
5185
+ /** Bone `i`'s locals to write: the setup bone copied on its first write, as `posedBones` copies a keyed one. */
5186
+ bone(i) {
5187
+ const b = this.bones[i];
5188
+ if (!this.active.has(b.name)) return null;
5189
+ if (b === this.doc.bones[i]) this.bones[i] = { ...b };
5190
+ return this.bones[i];
5191
+ }
5192
+ slotLive(i) {
5193
+ return this.live[i];
5194
+ }
5195
+ /** What `placeholder` shows in slot `i` (`Resolved`). */
5196
+ resolve(i, placeholder) {
5197
+ if (placeholder === null) return { key: null, vertex: false, timeline: null, vertices: null };
5198
+ const slot = this.doc.slots[i];
5199
+ const memo = `${i}\0${placeholder}`;
5200
+ const known = this.resolved.get(memo);
5201
+ if (known !== void 0) return known;
5202
+ const s = (this.plant.shown ?? shownAttachment)(this.doc, { ...slot, setup: placeholder });
5203
+ let out;
5204
+ if (s === null) out = { key: null, vertex: false, timeline: null, vertices: null };
5205
+ else if ("conflict" in s) out = { key: `conflict\0${placeholder}`, vertex: false, timeline: null, vertices: null };
5206
+ else out = resolvedOf(this.doc, slot, s);
5207
+ this.resolved.set(memo, out);
5208
+ return out;
5209
+ }
5210
+ /** `SlotPose.setAttachment`: a different attachment clears the deform unless both play the same record's timelines. */
5211
+ setAttachment(i, placeholder) {
5212
+ const s = this.slots[i];
5213
+ const was = this.resolve(i, s.placeholder);
5214
+ const now = this.resolve(i, placeholder);
5215
+ s.placeholder = placeholder;
5216
+ if (was.key === now.key) return;
5217
+ if (!now.vertex || !was.vertex || now.timeline !== was.timeline) s.deform = null;
5218
+ }
5219
+ }
5220
+ function resolvedOf(doc, slot, s) {
5221
+ const g = s.record.geometry;
5222
+ const key = `${s.skin}\0${s.placeholder}`;
5223
+ if (g === void 0 || g.kind === "region") return { key, vertex: false, timeline: null, vertices: null };
5224
+ if (g.kind === "linkedmesh") {
5225
+ const source = doc.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
5226
+ const vertices = source !== void 0 && source.kind !== "region" && source.kind !== "linkedmesh" ? source.vertices : null;
5227
+ return { key, vertex: true, timeline: s.record.timelines === true ? `${g.skin}/${g.slot}/${g.source}` : `${s.skin}/${slot.name}/${s.placeholder}`, vertices };
5228
+ }
5229
+ return { key, vertex: true, timeline: `${s.skin}/${slot.name}/${s.placeholder}`, vertices: g.vertices };
5230
+ }
5231
+ class Mixer {
5232
+ constructor(doc, plant) {
5233
+ this.doc = doc;
5234
+ this.plant = plant;
5235
+ this.tracks = [];
5236
+ this.animationsChanged = false;
5237
+ this.propertyIds = /* @__PURE__ */ new Map();
5238
+ this.anims = /* @__PURE__ */ new Map();
5239
+ }
5240
+ animation(name) {
5241
+ let a = this.anims.get(name);
5242
+ if (a === void 0) {
5243
+ a = mixAnimationOf(this.doc, name);
5244
+ this.anims.set(name, a);
5245
+ }
5246
+ return a;
5247
+ }
5248
+ changed() {
5249
+ this.animationsChanged = true;
5250
+ }
5251
+ expandToIndex(index) {
5252
+ while (this.tracks.length <= index) this.tracks.push(null);
5253
+ return this.tracks[index];
5254
+ }
5255
+ clearNext(entry) {
5256
+ entry.next = null;
5257
+ }
5258
+ setTrack(index, current) {
5259
+ const from = this.expandToIndex(index);
5260
+ this.tracks[index] = current;
5261
+ current.previous = null;
5262
+ if (from !== null) {
5263
+ from.next = null;
5264
+ current.mixingFrom = from;
5265
+ from.mixingTo = current;
5266
+ current.mixTime = 0;
5267
+ from.timelinesRotation.length = 0;
5268
+ }
5269
+ this.changed();
5270
+ }
5271
+ setAnimation(track, anim, loop) {
5272
+ let current = this.expandToIndex(track);
5273
+ if (current !== null) {
5274
+ if (current.nextTrackLast === -1 && current.anim === anim) {
5275
+ this.tracks[track] = current.mixingFrom;
5276
+ this.changed();
5277
+ this.clearNext(current);
5278
+ current = current.mixingFrom;
5279
+ } else this.clearNext(current);
5280
+ }
5281
+ const entry = newEntry(anim, loop);
5282
+ this.setTrack(track, entry);
5283
+ return entry;
5284
+ }
5285
+ addAnimation(track, anim, loop, delay) {
5286
+ let last = this.expandToIndex(track);
5287
+ if (last !== null) while (last.next !== null) last = last.next;
5288
+ const entry = newEntry(anim, loop);
5289
+ if (last === null) {
5290
+ this.setTrack(track, entry);
5291
+ if (delay < 0) delay = 0;
5292
+ } else {
5293
+ last.next = entry;
5294
+ entry.previous = last;
5295
+ if (delay <= 0) delay = Math.max(delay + this.complete(last) - entry.mixDuration, 0);
5296
+ }
5297
+ entry.delay = delay;
5298
+ return entry;
5299
+ }
5300
+ /** `TrackEntry.setMixDuration(mixDuration, delay)`. */
5301
+ setMixDuration(entry, mixDuration, delay) {
5302
+ entry.mixDuration = mixDuration;
5303
+ if (delay <= 0) delay = entry.previous === null ? 0 : Math.max(delay + this.complete(entry.previous) - mixDuration, 0);
5304
+ entry.delay = delay;
5305
+ }
5306
+ /** `getTrackComplete`, or 0 under the plant that reads a queued delay as "now". */
5307
+ complete(e) {
5308
+ return this.plant.queueDelayNow === true ? e.trackTime : trackComplete(e);
5309
+ }
5310
+ /** `TrackEntry.mix()`: `mixTime / mixDuration` clamped to 1, 1 over a mix of 0 — reversed under the plant. */
5311
+ mixOf(e) {
5312
+ let m;
5313
+ if (e.mixDuration === 0) m = 1;
5314
+ else {
5315
+ m = e.mixTime / e.mixDuration;
5316
+ if (m >= 1) m = 1;
5317
+ }
5318
+ return this.plant.mixReversed === true ? 1 - m : m;
5319
+ }
5320
+ update(delta) {
5321
+ const tracks = this.tracks;
5322
+ for (let i = 0, n = tracks.length; i < n; i++) {
5323
+ const current = tracks[i];
5324
+ if (current === null) continue;
5325
+ current.animationLast = current.nextAnimationLast;
5326
+ current.trackLast = current.nextTrackLast;
5327
+ let currentDelta = delta;
5328
+ if (current.delay > 0) {
5329
+ current.delay -= currentDelta;
5330
+ if (current.delay > 0) continue;
5331
+ currentDelta = -current.delay;
5332
+ current.delay = 0;
5333
+ }
5334
+ let next = current.next;
5335
+ if (next !== null) {
5336
+ const nextTime = current.trackLast - next.delay;
5337
+ if (nextTime >= 0) {
5338
+ next.delay = 0;
5339
+ next.trackTime += nextTime + delta;
5340
+ current.trackTime += currentDelta;
5341
+ this.setTrack(i, next);
5342
+ while (next.mixingFrom !== null) {
5343
+ next.mixTime += delta;
5344
+ next = next.mixingFrom;
5345
+ }
5346
+ continue;
5347
+ }
5348
+ } else if (current.trackLast >= current.trackEnd && current.mixingFrom === null) {
5349
+ tracks[i] = null;
5350
+ this.changed();
5351
+ this.clearNext(current);
5352
+ continue;
5353
+ }
5354
+ if (current.mixingFrom !== null && this.updateMixingFrom(current, delta)) {
5355
+ const from = current.mixingFrom;
5356
+ current.mixingFrom = null;
5357
+ if (from !== null) from.mixingTo = null;
5358
+ this.changed();
5359
+ }
5360
+ current.trackTime += currentDelta;
5361
+ }
5362
+ }
5363
+ updateMixingFrom(to, delta) {
5364
+ const from = to.mixingFrom;
5365
+ if (from === null) return true;
5366
+ const finished = this.updateMixingFrom(from, delta);
5367
+ from.animationLast = from.nextAnimationLast;
5368
+ from.trackLast = from.nextTrackLast;
5369
+ if (to.nextTrackLast !== -1 && to.mixTime >= to.mixDuration) {
5370
+ if (from.totalAlpha === 0 || to.mixDuration === 0) {
5371
+ to.mixingFrom = from.mixingFrom;
5372
+ if (from.mixingFrom !== null) from.mixingFrom.mixingTo = to;
5373
+ if (from.totalAlpha === 0) for (let next = to; next.mixingTo !== null; next = next.mixingTo) next.keepHold = true;
5374
+ this.changed();
5375
+ }
5376
+ return finished;
5377
+ }
5378
+ from.trackTime += delta;
5379
+ to.mixTime += delta;
5380
+ return false;
5381
+ }
5382
+ /** `AnimationState.apply` onto one step's pose, every track in order; returns track 0's current entry's animation time (0 where it applied none). */
5383
+ apply(pose) {
5384
+ if (this.animationsChanged) this.computeModes();
5385
+ let base = 0;
5386
+ const tracks = this.tracks;
5387
+ for (let i = 0, n = tracks.length; i < n; i++) {
5388
+ const current = tracks[i];
5389
+ if (current === null || current.delay > 0) continue;
5390
+ let alpha = 1;
5391
+ if (current.mixingFrom !== null) alpha *= this.applyMixingFrom(current, pose, i);
5392
+ else if (current.trackTime >= current.trackEnd && current.next === null) alpha = 0;
5393
+ const animationLast = current.animationLast;
5394
+ const animationTime = animationTimeOf(current);
5395
+ const timelines = current.anim.timelines;
5396
+ const count = timelines.length;
5397
+ if (i === 0 && alpha === 1) {
5398
+ for (const tl of timelines) {
5399
+ if (tl.type === "attachment") this.applyAttachment(pose, tl, animationTime, SETUP, true);
5400
+ else this.applyTimeline(pose, tl, animationLast, animationTime, true, 1, SETUP, false);
5401
+ }
5402
+ } else {
5403
+ const modes = current.timelineMode;
5404
+ const retain = alpha >= 0;
5405
+ const firstFrame = current.timelinesRotation.length !== count << 1;
5406
+ if (firstFrame) current.timelinesRotation.length = count << 1;
5407
+ timelines.forEach((tl, ii) => {
5408
+ const from = this.fromOf(modes[ii] & MODE, i);
5409
+ if (tl.type === "bone" && tl.kind === "rotate") this.applyRotate(pose, tl, animationTime, alpha, from, current.timelinesRotation, ii << 1, firstFrame);
5410
+ else if (tl.type === "attachment") this.applyAttachment(pose, tl, animationTime, from, retain);
5411
+ else this.applyTimeline(pose, tl, animationLast, animationTime, true, alpha, from, false);
5412
+ });
5413
+ }
5414
+ if (i === 0) base = animationTime;
5415
+ current.nextAnimationLast = animationTime;
5416
+ current.nextTrackLast = current.trackTime;
5417
+ }
5418
+ pose.slots.forEach((s, k) => {
5419
+ if (s.attachmentState === ATTACH_SETUP) pose.setAttachment(k, this.doc.slots[k].setup);
5420
+ });
5421
+ return base;
5422
+ }
5423
+ /** Where a timeline mixes from — `CURRENT` read as `SETUP` above track 0 under the plant. */
5424
+ fromOf(from, track) {
5425
+ return this.plant.higherTracksFromSetup === true && track > 0 && from === CURRENT ? SETUP : from;
5426
+ }
5427
+ applyMixingFrom(to, pose, track) {
5428
+ const from = to.mixingFrom;
5429
+ const fromMix = from.mixingFrom !== null ? this.applyMixingFrom(from, pose, track) : 1;
5430
+ const mix = this.mixOf(to);
5431
+ const a = 1 * fromMix;
5432
+ const keep = 1 - mix * 1;
5433
+ const alphaMix = a * (1 - mix);
5434
+ const alphaHold = keep > 0 ? alphaMix / keep : a;
5435
+ const timelines = from.anim.timelines;
5436
+ const count = timelines.length;
5437
+ const modes = from.timelineMode;
5438
+ const holdMix = from.timelineHoldMix;
5439
+ const firstFrame = from.timelinesRotation.length !== count << 1;
5440
+ if (firstFrame) from.timelinesRotation.length = count << 1;
5441
+ const animationLast = from.animationLast;
5442
+ const animationTime = animationTimeOf(from);
5443
+ const fire = this.plant.eventsUntilHalfMix === true && mix < 0.5;
5444
+ from.totalAlpha = 0;
5445
+ timelines.forEach((tl, i) => {
5446
+ const mode = modes[i];
5447
+ const mixFrom = this.fromOf(mode & MODE, track);
5448
+ let alpha;
5449
+ if ((mode & HOLD) !== 0 && this.plant.holdFaded !== true) {
5450
+ const h = holdMix[i];
5451
+ alpha = h === null || h === void 0 ? alphaHold : alphaHold * (1 - this.mixOf(h));
5452
+ } else alpha = alphaMix;
5453
+ from.totalAlpha += alpha;
5454
+ if (tl.type === "bone" && tl.kind === "rotate") this.applyRotate(pose, tl, animationTime, alpha, mixFrom, from.timelinesRotation, i << 1, firstFrame);
5455
+ else if (tl.type === "attachment") this.applyAttachment(pose, tl, animationTime, mixFrom, false);
5456
+ else this.applyTimeline(pose, tl, animationLast, animationTime, fire, alpha, mixFrom, true);
5457
+ });
5458
+ from.nextAnimationLast = animationTime;
5459
+ from.nextTrackLast = from.trackTime;
5460
+ return mix;
5461
+ }
5462
+ /** `_animationsChanged`: every track's entries' modes, oldest first, the property owners cleared after. */
5463
+ computeModes() {
5464
+ this.animationsChanged = false;
5465
+ this.tracks.forEach((top, i) => {
5466
+ if (top === null) return;
5467
+ let entry = top;
5468
+ while (entry.mixingFrom !== null) entry = entry.mixingFrom;
5469
+ do {
5470
+ this.computeHold(entry, i);
5471
+ entry = entry.mixingTo;
5472
+ } while (entry !== null);
5473
+ });
5474
+ this.propertyIds.clear();
5475
+ }
5476
+ computeHold(entry, track) {
5477
+ const timelines = entry.anim.timelines;
5478
+ const modes = entry.timelineMode;
5479
+ modes.length = timelines.length;
5480
+ const holdMix = entry.timelineHoldMix;
5481
+ holdMix.length = 0;
5482
+ const to = entry.mixingTo;
5483
+ timelines.forEach((tl, i) => {
5484
+ const from = this.propertyFrom(track, tl.ids);
5485
+ let mode;
5486
+ if (to === null || tl.instant || !hasAny(to.anim, tl.ids)) mode = from;
5487
+ else {
5488
+ mode = from | HOLD;
5489
+ for (let next = to.mixingTo; next !== null; next = next.mixingTo) {
5490
+ if (!hasAny(next.anim, tl.ids)) {
5491
+ if (next.mixDuration > 0) holdMix[i] = next;
5492
+ break;
5493
+ }
5494
+ }
5495
+ }
5496
+ if (entry.keepHold) mode = mode & ~HOLD | (modes[i] ?? 0) & HOLD;
5497
+ modes[i] = mode;
5498
+ });
5499
+ }
5500
+ /** `AnimationState.from`: `SETUP` for a property no one keyed yet, `FIRST` for one this track keyed first, `CURRENT` for one a lower track keyed. */
5501
+ propertyFrom(track, ids) {
5502
+ const owners = this.propertyIds;
5503
+ let from = SETUP;
5504
+ for (let i = 0, n = ids.length; i < n; i++) {
5505
+ const owner = owners.get(ids[i]);
5506
+ if (owner === void 0) owners.set(ids[i], track);
5507
+ else {
5508
+ if (owner !== track) {
5509
+ while (++i < n) if (!owners.has(ids[i])) owners.set(ids[i], track);
5510
+ return CURRENT;
5511
+ }
5512
+ from = FIRST;
5513
+ }
5514
+ }
5515
+ return from;
5516
+ }
5517
+ // -------------------------------------------------------------------------
5518
+ // the timelines' `apply`
5519
+ // -------------------------------------------------------------------------
5520
+ values(keys, t) {
5521
+ const index = (this.plant.search ?? keyIndexAt)(keys, t);
5522
+ if (index < 0) return null;
5523
+ const channel2 = this.plant.channel ?? channelAt;
5524
+ return keys[index].values.map((_v, c) => channel2(keys, index, c, t));
5525
+ }
5526
+ /** `Timeline.apply` for every timeline but an attachment's and a rotation mixed below full weight. */
5527
+ applyTimeline(pose, tl, lastTime, time, fire, alpha, from, out) {
5528
+ switch (tl.type) {
5529
+ case "bone":
5530
+ return this.applyBone(pose, tl, time, alpha, from, out);
5531
+ case "colour":
5532
+ return this.applyColour(pose, tl, time, alpha, from);
5533
+ case "deform":
5534
+ return this.applyDeform(pose, tl, time, alpha, from);
5535
+ case "events":
5536
+ if (fire) pose.events.push(...eventsFired(tl.keys, lastTime, time, rawNumber, time < lastTime));
5537
+ return;
5538
+ case "attachment":
5539
+ return;
5540
+ }
5541
+ }
5542
+ applyBone(pose, tl, time, alpha, from, out) {
5543
+ const bone2 = pose.bone(tl.bone);
5544
+ if (bone2 === null) return;
5545
+ const setup = this.doc.bones[tl.bone];
5546
+ if (tl.kind === "inherit") {
5547
+ const restore = () => {
5548
+ if (setup.inheritMode === void 0) delete bone2.inheritMode;
5549
+ else bone2.inheritMode = setup.inheritMode;
5550
+ };
5551
+ if (out) {
5552
+ if (from !== CURRENT) restore();
5553
+ return;
5554
+ }
5555
+ const index = (this.plant.search ?? keyIndexAt)(tl.keys, time);
5556
+ if (index < 0) {
5557
+ if (from !== CURRENT) restore();
5558
+ } else bone2.inheritMode = tl.keys[index].mode;
5559
+ return;
5560
+ }
5561
+ const v = this.values(tl.keys, time);
5562
+ const relative = (field, i) => {
5563
+ const s = setup[field] ?? 0;
5564
+ const c = bone2[field] ?? 0;
5565
+ bone2[field] = v === null ? beforeFirstKey(from, alpha, c, s) : from === SETUP ? s + v[i] * alpha : c + (v[i] + s - c) * alpha;
5566
+ };
5567
+ const pair = (fx, fy) => {
5568
+ const sx = setup[fx] ?? 0;
5569
+ const sy = setup[fy] ?? 0;
5570
+ if (v === null) {
5571
+ if (from === SETUP) {
5572
+ bone2[fx] = sx;
5573
+ bone2[fy] = sy;
5574
+ } else if (from === FIRST) {
5575
+ bone2[fx] = (bone2[fx] ?? 0) + (sx - (bone2[fx] ?? 0)) * alpha;
5576
+ bone2[fy] = (bone2[fy] ?? 0) + (sy - (bone2[fy] ?? 0)) * alpha;
5577
+ }
5578
+ return;
5579
+ }
5580
+ if (from === SETUP) {
5581
+ bone2[fx] = sx + v[0] * alpha;
5582
+ bone2[fy] = sy + v[1] * alpha;
5583
+ } else {
5584
+ bone2[fx] = (bone2[fx] ?? 0) + (sx + v[0] - (bone2[fx] ?? 0)) * alpha;
5585
+ bone2[fy] = (bone2[fy] ?? 0) + (sy + v[1] - (bone2[fy] ?? 0)) * alpha;
5586
+ }
5587
+ };
5588
+ const scale = (field, i) => {
5589
+ const s = setup[field] ?? 1;
5590
+ const c = bone2[field] ?? 1;
5591
+ bone2[field] = v === null ? beforeFirstKey(from, alpha, c, s) : scaleValue(v[i] * s, alpha, from, out, c, s);
5592
+ };
5593
+ switch (tl.kind) {
5594
+ case "rotate":
5595
+ relative("rotation", 0);
5596
+ break;
5597
+ case "translate":
5598
+ pair("x", "y");
5599
+ break;
5600
+ case "translatex":
5601
+ relative("x", 0);
5602
+ break;
5603
+ case "translatey":
5604
+ relative("y", 0);
5605
+ break;
5606
+ case "shear":
5607
+ pair("shearX", "shearY");
5608
+ break;
5609
+ case "shearx":
5610
+ relative("shearX", 0);
5611
+ break;
5612
+ case "sheary":
5613
+ relative("shearY", 0);
5614
+ break;
5615
+ case "scale":
5616
+ if (v === null) {
5617
+ const sx = setup.scaleX ?? 1;
5618
+ const sy = setup.scaleY ?? 1;
5619
+ if (from === SETUP) {
5620
+ bone2.scaleX = sx;
5621
+ bone2.scaleY = sy;
5622
+ } else if (from === FIRST) {
5623
+ bone2.scaleX = (bone2.scaleX ?? 1) + (sx - (bone2.scaleX ?? 1)) * alpha;
5624
+ bone2.scaleY = (bone2.scaleY ?? 1) + (sy - (bone2.scaleY ?? 1)) * alpha;
5625
+ }
5626
+ } else {
5627
+ bone2.scaleX = scaleValue(v[0] * (setup.scaleX ?? 1), alpha, from, out, bone2.scaleX ?? 1, setup.scaleX ?? 1);
5628
+ bone2.scaleY = scaleValue(v[1] * (setup.scaleY ?? 1), alpha, from, out, bone2.scaleY ?? 1, setup.scaleY ?? 1);
5629
+ }
5630
+ break;
5631
+ case "scalex":
5632
+ scale("scaleX", 0);
5633
+ break;
5634
+ case "scaley":
5635
+ scale("scaleY", 0);
5636
+ break;
5637
+ }
5638
+ }
5639
+ /** `applyRotateTimeline`: a rotation mixed below full weight turns the way it first turned. */
5640
+ applyRotate(pose, tl, time, alpha, from, rotations, i, firstFrame) {
5641
+ if (firstFrame) rotations[i] = 0;
5642
+ if (alpha === 1) {
5643
+ this.applyBone(pose, tl, time, 1, from, false);
5644
+ return;
5645
+ }
5646
+ const bone2 = pose.bone(tl.bone);
5647
+ if (bone2 === null) return;
5648
+ const setup = this.doc.bones[tl.bone].rotation ?? 0;
5649
+ const v = this.values(tl.keys, time);
5650
+ let r1;
5651
+ let r2;
5652
+ if (v === null) {
5653
+ if (from === SETUP) {
5654
+ bone2.rotation = setup;
5655
+ return;
5656
+ }
5657
+ if (from === CURRENT) return;
5658
+ r1 = bone2.rotation ?? 0;
5659
+ r2 = setup;
5660
+ } else {
5661
+ r1 = from === SETUP ? setup : bone2.rotation ?? 0;
5662
+ r2 = setup + v[0];
5663
+ }
5664
+ let total;
5665
+ let diff = r2 - r1;
5666
+ diff -= Math.ceil(diff / 360 - 0.5) * 360;
5667
+ if (diff === 0) total = rotations[i];
5668
+ else {
5669
+ let lastTotal;
5670
+ let lastDiff;
5671
+ if (firstFrame) {
5672
+ lastTotal = 0;
5673
+ lastDiff = diff;
5674
+ } else {
5675
+ lastTotal = rotations[i];
5676
+ lastDiff = rotations[i + 1] ?? Number.NaN;
5677
+ }
5678
+ const loops = lastTotal - lastTotal % 360;
5679
+ total = diff + loops;
5680
+ const current = diff >= 0;
5681
+ let dir = lastTotal >= 0;
5682
+ if (Math.abs(lastDiff) <= 90 && signum(lastDiff) !== signum(diff)) {
5683
+ if (Math.abs(lastTotal - loops) > 180) {
5684
+ total += 360 * signum(lastTotal);
5685
+ dir = current;
5686
+ } else if (loops !== 0) total -= 360 * signum(lastTotal);
5687
+ else dir = current;
5688
+ }
5689
+ if (dir !== current) total += 360 * signum(lastTotal);
5690
+ rotations[i] = total;
5691
+ }
5692
+ rotations[i + 1] = diff;
5693
+ bone2.rotation = r1 + total * alpha;
5694
+ }
5695
+ /** `applyAttachmentTimeline`: shown for good when retained, else for the deform timelines only and set back at the end of the apply. */
5696
+ applyAttachment(pose, tl, time, from, retain) {
5697
+ if (!pose.slotLive(tl.slot)) return;
5698
+ const s = pose.slots[tl.slot];
5699
+ if (!retain && s.attachmentState === ATTACH_RETAIN) return;
5700
+ const index = (this.plant.search ?? keyIndexAt)(tl.keys, time);
5701
+ let setup = index < 0;
5702
+ let name = null;
5703
+ if (!setup) {
5704
+ name = tl.keys[index].name ?? null;
5705
+ setup = !retain && name === null;
5706
+ }
5707
+ if (setup) {
5708
+ if (from === CURRENT) return;
5709
+ name = this.doc.slots[tl.slot].setup;
5710
+ }
5711
+ pose.setAttachment(tl.slot, name);
5712
+ if (retain) s.attachmentState = ATTACH_RETAIN;
5713
+ else if (!setup) s.attachmentState = ATTACH_SETUP;
5714
+ }
5715
+ applyColour(pose, tl, time, alpha, from) {
5716
+ if (!pose.slotLive(tl.slot)) return;
5717
+ const slot = this.doc.slots[tl.slot];
5718
+ const s = pose.slots[tl.slot];
5719
+ const l = s.light;
5720
+ const d = s.dark;
5721
+ const sl = slot.color === void 0 ? [1, 1, 1, 1] : (this.plant.colour ?? readColour)(slot.color);
5722
+ const sdFull = slot.dark === void 0 ? null : readColour(slot.dark);
5723
+ const sd = sdFull === null ? [0, 0, 0] : [sdFull[0], sdFull[1], sdFull[2]];
5724
+ const clampLight = () => {
5725
+ for (let k = 0; k < 4; k++) l[k] = clamp01(l[k]);
5726
+ };
5727
+ const v = this.values(tl.keys, time);
5728
+ const channels = SLOT_COLOUR_TIMELINES[tl.kind].channels.length;
5729
+ switch (tl.kind) {
5730
+ case "rgba": {
5731
+ if (v === null) {
5732
+ if (from === SETUP) for (let k = 0; k < 4; k++) l[k] = sl[k];
5733
+ else if (from === FIRST) {
5734
+ for (let k = 0; k < 4; k++) l[k] += (sl[k] - l[k]) * alpha;
5735
+ clampLight();
5736
+ }
5737
+ return;
5738
+ }
5739
+ if (alpha === 1) for (let k = 0; k < 4; k++) l[k] = v[k];
5740
+ else if (from === SETUP) for (let k = 0; k < 4; k++) l[k] = sl[k] + (v[k] - sl[k]) * alpha;
5741
+ else for (let k = 0; k < 4; k++) l[k] += (v[k] - l[k]) * alpha;
5742
+ clampLight();
5743
+ return;
5744
+ }
5745
+ case "rgb": {
5746
+ if (v === null) {
5747
+ if (from === SETUP) for (let k = 0; k < 3; k++) l[k] = sl[k];
5748
+ else if (from === FIRST) for (let k = 0; k < 3; k++) l[k] += (sl[k] - l[k]) * alpha;
5749
+ return;
5750
+ }
5751
+ const rgb = [v[0], v[1], v[2]];
5752
+ if (alpha !== 1) for (let k = 0; k < 3; k++) rgb[k] = from === SETUP ? sl[k] + (rgb[k] - sl[k]) * alpha : l[k] + (rgb[k] - l[k]) * alpha;
5753
+ for (let k = 0; k < 3; k++) l[k] = clamp01(rgb[k]);
5754
+ return;
5755
+ }
5756
+ case "alpha": {
5757
+ if (v === null) {
5758
+ if (from === SETUP) l[3] = sl[3];
5759
+ else if (from === FIRST) l[3] += (sl[3] - l[3]) * alpha;
5760
+ return;
5761
+ }
5762
+ let a = v[0];
5763
+ if (alpha !== 1) a = from === SETUP ? sl[3] + (a - sl[3]) * alpha : l[3] + (a - l[3]) * alpha;
5764
+ l[3] = clamp01(a);
5765
+ return;
5766
+ }
5767
+ case "rgba2":
5768
+ case "rgb2": {
5769
+ if (d === null) return;
5770
+ const withAlpha = tl.kind === "rgba2";
5771
+ const lightN = withAlpha ? 4 : 3;
5772
+ if (v === null) {
5773
+ if (from === SETUP) {
5774
+ for (let k = 0; k < lightN; k++) l[k] = sl[k];
5775
+ for (let k = 0; k < 3; k++) d[k] = sd[k];
5776
+ } else if (from === FIRST) {
5777
+ for (let k = 0; k < lightN; k++) l[k] += (sl[k] - l[k]) * alpha;
5778
+ if (withAlpha) clampLight();
5779
+ for (let k = 0; k < 3; k++) d[k] += (sd[k] - d[k]) * alpha;
5780
+ }
5781
+ return;
5782
+ }
5783
+ const light = v.slice(0, lightN);
5784
+ const dark = v.slice(lightN, channels);
5785
+ if (withAlpha) {
5786
+ if (alpha === 1) for (let k = 0; k < 4; k++) l[k] = light[k];
5787
+ else if (from === SETUP) {
5788
+ for (let k = 0; k < 4; k++) l[k] = sl[k] + (light[k] - sl[k]) * alpha;
5789
+ for (let k = 0; k < 3; k++) dark[k] = sd[k] + (dark[k] - sd[k]) * alpha;
5790
+ } else {
5791
+ for (let k = 0; k < 4; k++) l[k] += (light[k] - l[k]) * alpha;
5792
+ for (let k = 0; k < 3; k++) dark[k] = d[k] + (dark[k] - d[k]) * alpha;
5793
+ }
5794
+ clampLight();
5795
+ } else {
5796
+ if (alpha !== 1) {
5797
+ for (let k = 0; k < 3; k++) {
5798
+ light[k] = from === SETUP ? sl[k] + (light[k] - sl[k]) * alpha : l[k] + (light[k] - l[k]) * alpha;
5799
+ dark[k] = from === SETUP ? sd[k] + (dark[k] - sd[k]) * alpha : d[k] + (dark[k] - d[k]) * alpha;
5800
+ }
5801
+ }
5802
+ for (let k = 0; k < 3; k++) l[k] = clamp01(light[k]);
5803
+ }
5804
+ for (let k = 0; k < 3; k++) d[k] = clamp01(dark[k]);
5805
+ return;
5806
+ }
5807
+ }
5808
+ }
5809
+ /** `DeformTimeline.apply`: every live slot showing the record the timeline keys, its deform array mixed. */
5810
+ applyDeform(pose, tl, time, alpha, from) {
5811
+ const keys = tl.timeline.deform;
5812
+ if (keys === null) return;
5813
+ pose.slots.forEach((s, k) => {
5814
+ if (!pose.slotLive(k)) return;
5815
+ const shown = pose.resolve(k, s.placeholder);
5816
+ if (!shown.vertex || shown.timeline !== tl.identity || shown.vertices === null) return;
5817
+ const vertices = shown.vertices;
5818
+ if (!s.seen.includes(tl.identity)) s.seen.push(tl.identity);
5819
+ const target = (this.plant.deform ?? deformAt)(vertices, keys, time);
5820
+ let mixFrom = from;
5821
+ if (s.deform === null) mixFrom = SETUP;
5822
+ if (target === null) {
5823
+ if (mixFrom === SETUP) s.deform = null;
5824
+ else if (mixFrom === FIRST) {
5825
+ if (alpha === 1) {
5826
+ s.deform = null;
5827
+ return;
5828
+ }
5829
+ const current = s.deform;
5830
+ if (!vertices.weighted) {
5831
+ const setup = setupArray(vertices);
5832
+ s.deform = current.map((c, i) => c + (setup[i] - c) * alpha);
5833
+ } else {
5834
+ const keep = 1 - alpha;
5835
+ s.deform = current.map((c) => c * keep);
5836
+ }
5837
+ }
5838
+ return;
5839
+ }
5840
+ if (alpha === 1) s.deform = [...target];
5841
+ else if (mixFrom === SETUP) {
5842
+ if (!vertices.weighted) {
5843
+ const setup = setupArray(vertices);
5844
+ s.deform = target.map((v, i) => setup[i] + (v - setup[i]) * alpha);
5845
+ } else s.deform = target.map((v) => v * alpha);
5846
+ } else {
5847
+ const current = s.deform;
5848
+ s.deform = current.map((c, i) => c + (target[i] - c) * alpha);
5849
+ }
5850
+ });
5851
+ }
5852
+ }
5853
+ function beforeFirstKey(from, alpha, current, setup) {
5854
+ if (from === SETUP) return setup;
5855
+ if (from === FIRST) return current + (setup - current) * alpha;
5856
+ return current;
5857
+ }
5858
+ function scaleValue(value, alpha, from, out, current, setup) {
5859
+ if (alpha === 1) return value;
5860
+ let base = from === SETUP ? setup : current;
5861
+ if (out) return base + (Math.abs(value) * Math.sign(base) - base) * alpha;
5862
+ base = Math.abs(base) * Math.sign(value);
5863
+ return base + (value - base) * alpha;
5864
+ }
5865
+ function hasAny(anim, ids) {
5866
+ for (const id of ids) if (anim.ids.has(id)) return true;
5867
+ return false;
5868
+ }
5869
+ function trackIndex(track, call) {
5870
+ if (typeof track !== "number" || !Number.isInteger(track) || track < 0) throw new CoreInputError(`the live tracks' ${call} names track ${String(track)}, not a whole number at or above 0`);
5871
+ return track;
5872
+ }
5873
+ function finiteAtOrAbove0(v, what, call) {
5874
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) throw new CoreInputError(`the live tracks' ${call} ${what} is ${String(v)}, not a finite time at or above 0`);
5875
+ return v;
5876
+ }
5877
+ function loopOf(options, call) {
5878
+ if (typeof options !== "object" || options === null || typeof options.loop !== "boolean") throw new CoreInputError(`the live tracks' ${call} options.loop is ${JSON.stringify(options?.loop)}, not true or false`);
5879
+ return options.loop;
5880
+ }
5881
+ function delayOf(v, call) {
5882
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new CoreInputError(`the live tracks' ${call} delay is ${String(v)}, not a finite number`);
5883
+ return v;
5884
+ }
5885
+ function openTracks(doc, options, plant = {}) {
5886
+ if (typeof options !== "object" || options === null || options.reset !== "animation" && options.reset !== "setup") throw new CoreInputError(`the live tracks' options.reset is ${JSON.stringify(options?.reset)}, not "animation" or "setup"`);
5887
+ if (options.adjust !== void 0 && typeof options.adjust !== "function") throw new CoreInputError(`the live tracks' options.adjust is ${typeof options.adjust}, not a function`);
5888
+ return new Tracks(doc, options.reset, options.adjust, plant, openRawTracksPath(doc, plant));
5889
+ }
5890
+ class Tracks {
5891
+ constructor(doc, reset, adjust, plant, path) {
5892
+ this.doc = doc;
5893
+ this.reset = reset;
5894
+ this.adjust = adjust;
5895
+ this.plant = plant;
5896
+ this.path = path;
5897
+ this.started = false;
5898
+ this.steps = 0;
5899
+ this.time = 0;
5900
+ this.refused = null;
5901
+ this.mixer = new Mixer(doc, plant);
5902
+ }
5903
+ get trackTime() {
5904
+ return this.time;
5905
+ }
5906
+ live(call) {
5907
+ 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)`);
5908
+ }
5909
+ setAnimation(track, animation, options) {
5910
+ this.live("setAnimation");
5911
+ const i = trackIndex(track, "setAnimation");
5912
+ const loop = loopOf(options, "setAnimation");
5913
+ const mix = finiteAtOrAbove0(options.mixDuration, "mixDuration", "setAnimation");
5914
+ const entry = this.mixer.setAnimation(i, this.mixer.animation(animation), loop);
5915
+ entry.mixDuration = mix;
5916
+ }
5917
+ setEmptyAnimation(track, mixDuration) {
5918
+ this.live("setEmptyAnimation");
5919
+ const i = trackIndex(track, "setEmptyAnimation");
5920
+ const mix = finiteAtOrAbove0(mixDuration, "mixDuration", "setEmptyAnimation");
5921
+ const entry = this.mixer.setAnimation(i, EMPTY, false);
5922
+ entry.mixDuration = mix;
5923
+ entry.trackEnd = mix;
5924
+ }
5925
+ addAnimation(track, animation, options) {
5926
+ this.live("addAnimation");
5927
+ const i = trackIndex(track, "addAnimation");
5928
+ const loop = loopOf(options, "addAnimation");
5929
+ const mix = finiteAtOrAbove0(options.mixDuration, "mixDuration", "addAnimation");
5930
+ const delay = delayOf(options.delay, "addAnimation");
5931
+ const entry = this.mixer.addAnimation(i, this.mixer.animation(animation), loop, delay);
5932
+ this.mixer.setMixDuration(entry, mix, delay);
5933
+ }
5934
+ addEmptyAnimation(track, mixDuration, delay) {
5935
+ this.live("addEmptyAnimation");
5936
+ const i = trackIndex(track, "addEmptyAnimation");
5937
+ const mix = finiteAtOrAbove0(mixDuration, "mixDuration", "addEmptyAnimation");
5938
+ const d = delayOf(delay, "addEmptyAnimation");
5939
+ const entry = this.mixer.addAnimation(i, EMPTY, false, d);
5940
+ if (d <= 0) entry.delay = Math.max(entry.delay + entry.mixDuration - mix, 0);
5941
+ entry.mixDuration = mix;
5942
+ entry.trackEnd = mix;
5943
+ }
5944
+ current(track) {
5945
+ const e = this.mixer.tracks[trackIndex(track, "current")] ?? null;
5946
+ if (e === null) return null;
5947
+ return { animation: e.anim.name, loop: e.loop, trackTime: e.trackTime, animationTime: animationTimeOf(e), duration: e.anim.duration };
5948
+ }
5949
+ start() {
5950
+ this.live("start");
5951
+ if (this.started) throw new CoreInputError("the live tracks were started already; pose 0 is posed once");
5952
+ this.started = true;
5953
+ return this.guard("start", () => {
5954
+ if (this.reset === "setup") return this.path.setupPose();
5955
+ return this.pose(0, true);
5956
+ });
5957
+ }
5958
+ step(dt) {
5959
+ const n = this.steps + 1;
5960
+ this.live(`step ${n}`);
5961
+ if (!this.started) throw new CoreInputError(`the live tracks' step ${n} comes before start(), which poses pose 0`);
5962
+ if (typeof dt !== "number" || !Number.isFinite(dt) || dt < 0) throw new CoreInputError(`the live tracks' step ${n} is ${String(dt)}, not a finite time at or above 0`);
5963
+ return this.guard(`step ${n}`, () => {
5964
+ this.time += dt;
5965
+ this.mixer.update(dt);
5966
+ const pose = this.pose(dt, false);
5967
+ this.steps = n;
5968
+ return pose;
5969
+ });
5970
+ }
5971
+ /** `f`, the state refused for good if it throws once anything has moved. */
5972
+ guard(call, f) {
5973
+ try {
5974
+ return f();
5975
+ } catch (err) {
5976
+ this.refused = `${call} (${err instanceof Error ? err.message : String(err)})`;
5977
+ throw err;
5978
+ }
5979
+ }
5980
+ /** One pose: the entries applied over the setup pose, then the raw pose path (`openRawTracksPath`). */
5981
+ pose(dt, first) {
5982
+ const { doc, path } = this;
5983
+ const ctx = path.ctx;
5984
+ const before = ctx.time;
5985
+ ctx.time += dt;
5986
+ const pose = new Pose(doc, this.plant);
5987
+ const animationTime = this.mixer.apply(pose);
5988
+ const mixed = new Map(doc.slots.map((s, k) => [s.name, { deform: pose.slots[k].deform, seen: pose.slots[k].seen }]));
5989
+ return path.step(first, this.time, animationTime, {
5990
+ world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjust: this.adjust }),
5991
+ slots: (sliders, placeholders) => slotRowsFrom(doc, path.raw, sliders, placeholders, (_slot, _live) => {
5992
+ const s = pose.slots[doc.slots.indexOf(_slot)];
5993
+ return { placeholder: s.placeholder, light: s.light, dark: s.dark };
5994
+ }),
5995
+ drawOrder: [],
5996
+ at: 0,
5997
+ sample: null,
5998
+ mixed,
5999
+ events: () => pose.events
6000
+ });
6001
+ }
6002
+ }
5009
6003
  function documentPageLookup(pages) {
5010
6004
  const first = /* @__PURE__ */ new Map();
5011
6005
  for (const page of pages) {
@@ -7322,9 +8316,27 @@ class RigPlayer {
7322
8316
  read;
7323
8317
  skinName;
7324
8318
  seekStep;
8319
+ /**
8320
+ * The single live track (`openTrack`): what a plain `play` on track 0 runs
8321
+ * on, as every 1.x player did — one animation, every construct the core
8322
+ * poses, `seek` and a skin change by reopening. Null once the player is
8323
+ * composed.
8324
+ */
7325
8325
  track = null;
8326
+ /**
8327
+ * The live tracks (`openTracks`): opened by the first composing call — a
8328
+ * `play` with a mix or on another track, a `queue`, a `clear` — from the
8329
+ * single track's animation stepped to its time. The mixing is the Spine
8330
+ * runtime's, which does not mix a constraint, sequence, draw-order or
8331
+ * path-deform timeline, so such an animation is refused here and plays on
8332
+ * the single track only.
8333
+ */
8334
+ tracks = null;
8335
+ /** Track 0's animation and loop as `play` last set them — what `seek` and a skin change reopen. */
7326
8336
  animation = null;
7327
8337
  loop = true;
8338
+ /** One past the highest track touched — how many `current(track)` reads a step takes. */
8339
+ trackCount = 0;
7328
8340
  paused = false;
7329
8341
  pose = null;
7330
8342
  setup = null;
@@ -7388,23 +8400,118 @@ class RigPlayer {
7388
8400
  if (this.meshBackend) renderer.meshBackend = this.meshBackend;
7389
8401
  return renderer;
7390
8402
  }
7391
- /** The animation the track is playing, or null before `play`. */
8403
+ /** What track 0 plays, or null before `play` and after a `clear` of track 0 has mixed out. */
7392
8404
  get current() {
7393
- return this.animation;
8405
+ return this.tracks !== null ? this.tracks.current(0)?.animation ?? null : this.animation;
7394
8406
  }
7395
- /** The track time — the running sum of the steps since `play` (or the last `seek`). */
8407
+ /** The player's time — the running sum of the steps since `play` (or the last `seek`). */
7396
8408
  get time() {
7397
- return this.track?.trackTime ?? 0;
8409
+ return this.tracks?.trackTime ?? this.track?.trackTime ?? 0;
7398
8410
  }
7399
8411
  /**
7400
- * Opens `name` on the track at its first pose, the physics reset there. The
7401
- * pose is drawn on the next `frame`.
8412
+ * Plays `name` on a track — track 0 by default, which is what `play(name)`
8413
+ * has always meant: the first `play` opens the tracks at their first pose
8414
+ * with the physics reset there, and the pose is drawn on the next `frame`.
8415
+ * Later calls replace what the track plays: the entry it replaces mixes out
8416
+ * over `mix` seconds (0 cuts), and every entry queued on that track is
8417
+ * dropped. The pose the player holds is re-posed at once, so a held player
8418
+ * shows the change on its next `draw`. The mixing is the Spine runtime's,
8419
+ * which rig-c holds at tolerance 0; an animation keying what the tracks do
8420
+ * not mix — a constraint, a sequence, the draw order, a path's deform — is
8421
+ * refused here, by name, in the core's words.
7402
8422
  */
7403
8423
  play(name, options = {}) {
7404
- this.loop = options.loop ?? true;
7405
- this.animation = name;
8424
+ const track = options.track ?? 0;
8425
+ const loop = options.loop ?? true;
8426
+ const mix = options.mix ?? 0;
7406
8427
  this.paused = false;
7407
- this.open();
8428
+ if (this.tracks === null && track === 0 && mix === 0) {
8429
+ this.animation = name;
8430
+ this.loop = loop;
8431
+ this.track = openTrack(this.view, name, { loop, reset: "animation", adjust: this.adjustHook });
8432
+ this.pose = this.track.first;
8433
+ return;
8434
+ }
8435
+ const tracks = this.compose();
8436
+ tracks.setAnimation(track, name, { loop, mixDuration: mix });
8437
+ this.trackCount = Math.max(this.trackCount, track + 1);
8438
+ if (track === 0) {
8439
+ this.animation = name;
8440
+ this.loop = loop;
8441
+ }
8442
+ this.pose = tracks.step(0);
8443
+ }
8444
+ /**
8445
+ * Queues `name` after the last entry on `track` (`QueueOptions`): with
8446
+ * `delay` at or below 0 it starts when that entry completes less its own
8447
+ * `mix`, so a one-shot hands over to it across a mix; on an empty track it
8448
+ * is set. Before any `play`, the tracks are opened for it.
8449
+ */
8450
+ queue(name, options = {}) {
8451
+ const track = options.track ?? 0;
8452
+ const tracks = this.compose();
8453
+ tracks.addAnimation(track, name, { loop: options.loop ?? true, mixDuration: options.mix ?? 0, delay: options.delay ?? 0 });
8454
+ this.trackCount = Math.max(this.trackCount, track + 1);
8455
+ this.pose = tracks.step(0);
8456
+ }
8457
+ /**
8458
+ * Mixes `track` out to nothing over `mix` seconds — the layer or one-shot
8459
+ * gone and the tracks below showing through. Track 0 cleared shows the
8460
+ * setup pose. The pose the player holds is re-posed at once.
8461
+ */
8462
+ clear(track = 0, options = {}) {
8463
+ const tracks = this.compose();
8464
+ tracks.setEmptyAnimation(track, options.mix ?? 0);
8465
+ this.trackCount = Math.max(this.trackCount, track + 1);
8466
+ this.pose = tracks.step(0);
8467
+ }
8468
+ /**
8469
+ * Queues a mix out to nothing after the last entry on `track`, over `mix`
8470
+ * seconds; with `delay` 0 (the default) the mix ends as that entry
8471
+ * completes, so a one-shot queued this way plays once and fades out over
8472
+ * its last `mix` seconds.
8473
+ */
8474
+ queueClear(track = 0, options = {}) {
8475
+ const tracks = this.compose();
8476
+ tracks.addEmptyAnimation(track, options.mix ?? 0, options.delay ?? 0);
8477
+ this.trackCount = Math.max(this.trackCount, track + 1);
8478
+ this.pose = tracks.step(0);
8479
+ }
8480
+ /** `track`'s current entry, or null where it plays nothing (and before `play`). */
8481
+ entry(track = 0) {
8482
+ if (this.tracks !== null) {
8483
+ const view = this.tracks.current(track);
8484
+ return view === null ? null : { animation: view.animation, loop: view.loop, trackTime: view.trackTime, animationTime: view.animationTime, duration: view.duration };
8485
+ }
8486
+ const single = this.track;
8487
+ if (track !== 0 || single === null) return null;
8488
+ return { animation: this.animation, loop: this.loop, trackTime: single.trackTime, animationTime: single.animationTime, duration: single.duration };
8489
+ }
8490
+ /** Whether the player is composed: on the live tracks rather than the single track. */
8491
+ get composed() {
8492
+ return this.tracks !== null;
8493
+ }
8494
+ /**
8495
+ * The live tracks, opened by the first composing call. A single track in
8496
+ * play is carried over by replay: track 0 set to its animation, pose 0 taken
8497
+ * by `start()` (the player's — nothing steps before it) and the tracks
8498
+ * stepped to the single track's time in `seekStep` increments, so a physics
8499
+ * rig's history restarts there and nothing is fired on the way.
8500
+ */
8501
+ compose() {
8502
+ if (this.tracks !== null) return this.tracks;
8503
+ const tracks = openTracks(this.view, { reset: "animation", adjust: this.adjustHook });
8504
+ const single = this.track;
8505
+ const time = single?.trackTime ?? 0;
8506
+ if (single !== null && this.animation !== null) {
8507
+ tracks.setAnimation(0, this.animation, { loop: this.loop, mixDuration: 0 });
8508
+ this.trackCount = 1;
8509
+ }
8510
+ this.tracks = tracks;
8511
+ this.track = null;
8512
+ this.pose = tracks.start();
8513
+ if (time > 0) this.stepTo(time);
8514
+ return tracks;
7408
8515
  }
7409
8516
  /** Stops stepping; `frame` keeps drawing the pose it holds. */
7410
8517
  pause() {
@@ -7415,23 +8522,33 @@ class RigPlayer {
7415
8522
  this.paused = false;
7416
8523
  }
7417
8524
  /**
7418
- * Moves the track to `time`: the animation is reopened and stepped forward
7419
- * in `seekStep` increments (the last one shorter) until the track time
7420
- * reaches `time`. Events and completes crossed on the way are not fired.
8525
+ * Moves a single animation on track 0 to `time`: the tracks are reopened
8526
+ * with that animation and stepped forward in `seekStep` increments (the
8527
+ * last one shorter) until the time is reached. Events and completes crossed
8528
+ * on the way are not fired. It has a meaning for one animation on one
8529
+ * track only — the player cannot replay when a mix, a second track or a
8530
+ * queue was asked for — so after any of those it refuses by name; step the
8531
+ * player forward instead.
7421
8532
  */
7422
8533
  seek(time) {
7423
8534
  if (this.animation === null) throw new Error("seek before play: no animation is open");
7424
8535
  if (!(time >= 0) || !Number.isFinite(time)) throw new Error(`seek time must be a finite time at or above 0, got ${String(time)}`);
7425
- this.open();
8536
+ if (this.tracks !== null) throw new Error("seek is defined for a single animation on track 0; after a mix, another track, a queue or a clear the player cannot replay to a time — step it forward with frame/advance instead");
8537
+ this.reopen();
7426
8538
  this.stepTo(time);
7427
8539
  }
7428
- /** Steps the freshly opened track forward to `time` in `seekStep` increments, the last one shorter. */
8540
+ /** The single track opened again with track 0's animation as `play` last set it, at pose 0. */
8541
+ reopen() {
8542
+ this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook });
8543
+ this.pose = this.track.first;
8544
+ }
8545
+ /** Steps whatever is open forward to `time` in `seekStep` increments, the last one shorter. */
7429
8546
  stepTo(time) {
7430
- const track = this.track;
8547
+ const stepper = this.tracks ?? this.track;
7431
8548
  const steps = Math.floor(time / this.seekStep);
7432
- for (let i = 0; i < steps; i++) this.pose = track.step(this.seekStep);
7433
- const rest = time - track.trackTime;
7434
- if (rest > 0) this.pose = track.step(rest);
8549
+ for (let i = 0; i < steps; i++) this.pose = stepper.step(this.seekStep);
8550
+ const rest = time - stepper.trackTime;
8551
+ if (rest > 0) this.pose = stepper.step(rest);
7435
8552
  }
7436
8553
  on(type, listener) {
7437
8554
  const set = this.listeners[type];
@@ -7453,13 +8570,22 @@ class RigPlayer {
7453
8570
  * checks and nothing else.
7454
8571
  */
7455
8572
  advance(dt) {
7456
- const track = this.track;
7457
- if (track === null || this.pose === null) return;
8573
+ if (this.pose === null) return;
7458
8574
  const step = dt * this.timeScale;
7459
8575
  if (this.paused || !(step > 0)) return;
8576
+ const tracks = this.tracks;
8577
+ if (tracks !== null) {
8578
+ const before2 = [];
8579
+ for (let t = 0; t < this.trackCount; t++) before2.push(tracks.current(t));
8580
+ this.pose = tracks.step(step);
8581
+ this.dispatch(this.pose, before2, tracks);
8582
+ return;
8583
+ }
8584
+ const track = this.track;
8585
+ if (track === null) return;
7460
8586
  const before = track.trackTime;
7461
8587
  this.pose = track.step(step);
7462
- this.dispatch(this.pose, before, track);
8588
+ this.dispatchSingle(this.pose, before, track);
7463
8589
  }
7464
8590
  /** Draws the pose the player holds. */
7465
8591
  draw() {
@@ -7481,6 +8607,7 @@ class RigPlayer {
7481
8607
  }
7482
8608
  set skin(name) {
7483
8609
  if (name === this.skinName) return;
8610
+ if (this.tracks !== null) throw new Error("skin cannot change after a mix, another track, a queue or a clear: the player cannot replay those to the current time — set it before composing, or play again");
7484
8611
  const view = name === void 0 ? underNoSkin(this.read.doc) : underSkin(this.read.doc, name);
7485
8612
  this.view = view;
7486
8613
  this.source = frameSource(view, this.read.parsed);
@@ -7488,7 +8615,7 @@ class RigPlayer {
7488
8615
  this.setup = null;
7489
8616
  if (this.animation !== null && this.track !== null) {
7490
8617
  const time = this.track.trackTime;
7491
- this.open();
8618
+ this.reopen();
7492
8619
  this.stepTo(time);
7493
8620
  }
7494
8621
  }
@@ -7516,8 +8643,9 @@ class RigPlayer {
7516
8643
  * changed, then `draw`. Nothing before `play`.
7517
8644
  */
7518
8645
  repose() {
7519
- if (this.track === null || this.pose === null) return;
7520
- this.pose = this.track.step(0);
8646
+ const stepper = this.tracks ?? this.track;
8647
+ if (stepper === null || this.pose === null) return;
8648
+ this.pose = stepper.step(0);
7521
8649
  }
7522
8650
  /**
7523
8651
  * Where `name` is in the pose the player holds — the pose of the last
@@ -7600,27 +8728,15 @@ class RigPlayer {
7600
8728
  /** Removes the renderer's elements and hands its GPU pages back. The assets stay the caller's. */
7601
8729
  dispose() {
7602
8730
  this.renderer.dispose();
8731
+ this.tracks = null;
7603
8732
  this.track = null;
7604
8733
  this.pose = null;
7605
8734
  }
7606
- open() {
7607
- this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook });
7608
- this.pose = this.track.first;
7609
- }
7610
- /**
7611
- * The step's events, then its complete. The core fires the keys
7612
- * (`pose.events`, in the runtime's order across a wrap) and leaves
7613
- * `complete` to the player, which derives it from the track time and the
7614
- * duration: on a looping track, once on a step whose whole cycles rose past
7615
- * 0 (rig-c `src/core/track.ts`, *Across the wrap*); on a held one, on the
7616
- * step that reaches the duration. The runtime queues a looping complete
7617
- * between the keys before the wrap and the keys after it; this player
7618
- * dispatches it after all of the step's keys.
7619
- */
7620
- dispatch(pose, before, track) {
8735
+ /** The single track's step: its events (track 0) and its complete, as every 1.x player fired them. */
8736
+ dispatchSingle(pose, before, track) {
7621
8737
  if (this.listeners.event.size > 0) {
7622
8738
  for (const [name, time, int, float, string] of pose.events) {
7623
- const event = { name, time, int, float, string };
8739
+ const event = { name, time, int, float, string, track: 0 };
7624
8740
  for (const listener of this.listeners.event) listener(event);
7625
8741
  }
7626
8742
  }
@@ -7630,7 +8746,69 @@ class RigPlayer {
7630
8746
  const cycles = d > 0 ? Math.floor(after / d) : Infinity;
7631
8747
  const was = d > 0 ? Math.floor(before / d) : -Infinity;
7632
8748
  if (this.loop ? cycles > 0 && cycles > was : before < d && after >= d) {
7633
- const payload2 = { animation: this.animation };
8749
+ const payload2 = { animation: this.animation, track: 0 };
8750
+ for (const listener of this.listeners.complete) listener(payload2);
8751
+ }
8752
+ }
8753
+ }
8754
+ /**
8755
+ * The step's events, then its completes, each with its track. The core
8756
+ * fires a track's keys from its current entry (`pose.events`, in track
8757
+ * order and in the runtime's order across a wrap) — an entry mixing out
8758
+ * fires none — and leaves `complete` to the player, which derives it per
8759
+ * track from that track's current entry: on a looping entry, once on a step
8760
+ * whose whole cycles rose; on a held one, on the step that reaches its
8761
+ * duration. An entry replaced inside the step — by `play`, or by a queued
8762
+ * entry starting at its completion less the mix — is no longer current and
8763
+ * completes no more than it fires, which is the runtime's rule too.
8764
+ */
8765
+ dispatch(pose, before, tracks) {
8766
+ const after = [];
8767
+ for (let t = 0; t < this.trackCount; t++) after.push(tracks.current(t));
8768
+ const same = (b, a) => b !== null && a !== null && b.animation === a.animation && b.loop === a.loop && a.trackTime >= b.trackTime;
8769
+ if (this.listeners.event.size > 0 && pose.events.length > 0) {
8770
+ const counts = [];
8771
+ let total = 0;
8772
+ for (let t = 0; t < this.trackCount; t++) {
8773
+ const a = after[t];
8774
+ const b = before[t];
8775
+ let n = 0;
8776
+ if (a !== null && a.animation !== null) {
8777
+ const keys = this.view.animations.find((anim) => anim.name === a.animation)?.timelines.events ?? [];
8778
+ if (keys.length > 0) {
8779
+ const last = same(b, a) ? b.animationTime : -1;
8780
+ n = eventsFired(keys, last, a.animationTime, void 0, a.animationTime < last).length;
8781
+ }
8782
+ }
8783
+ counts.push(n);
8784
+ total += n;
8785
+ }
8786
+ const attributed = total === pose.events.length;
8787
+ let track = 0;
8788
+ let left = counts[0] ?? 0;
8789
+ for (const [name, time, int, float, string] of pose.events) {
8790
+ if (attributed) {
8791
+ while (left === 0 && track < counts.length - 1) {
8792
+ track++;
8793
+ left = counts[track];
8794
+ }
8795
+ left--;
8796
+ }
8797
+ const event = attributed ? { name, time, int, float, string, track } : { name, time, int, float, string };
8798
+ for (const listener of this.listeners.event) listener(event);
8799
+ }
8800
+ }
8801
+ if (this.listeners.complete.size > 0) {
8802
+ for (let t = 0; t < this.trackCount; t++) {
8803
+ const a = after[t];
8804
+ const b = before[t];
8805
+ if (!same(b, a) || a === null || a.animation === null) continue;
8806
+ const d = a.duration;
8807
+ const cycles = d > 0 ? Math.floor(a.trackTime / d) : Infinity;
8808
+ const was = d > 0 ? Math.floor(b.trackTime / d) : -Infinity;
8809
+ const fired = a.loop ? cycles > 0 && cycles > was : b.trackTime < d && a.trackTime >= d;
8810
+ if (!fired) continue;
8811
+ const payload2 = { animation: a.animation, track: t };
7634
8812
  for (const listener of this.listeners.complete) listener(payload2);
7635
8813
  }
7636
8814
  }