@weasel-js/core 1.0.4 → 1.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,109 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 27dd91b: <!-- bump-approved: minor: Mike — new public API across two arcs (timeline + rig, and the @weasel-js/audio package); called explicitly in conversation on 2026-08-22: "the next version we push will be 1.1.0" -->
8
+
9
+ Add a keyframe timeline primitive and a hierarchical rig.
10
+
11
+ This adds public API surface.
12
+
13
+ `animator.timeline(opts)` registers like any other animation, so its playhead
14
+ responds to `pause`, `setTimeScale` and `cancelKey`. Sampled tracks are a pure
15
+ function of the playhead and reuse the tween interpolation contract; event
16
+ tracks fire only on forward playback and stay silent under `seek`; timeline
17
+ tracks nest, evaluated at the parent's playhead minus their offset.
18
+
19
+ The rig ships as `blendPoses` and `resolveSkeleton` over a `Skeleton` of joints
20
+ carrying their own TRS — not the scene's consumer-defined `TPose`, which may be
21
+ a bare AABB with no rotation term a joint chain can compose through. A pose is
22
+ local deltas from bind, so an absent joint or field means "no change".
23
+ Animating a rig is a `SampledTrack<Pose>` whose `interpolate` is `blendPoses` —
24
+ no rig-specific timeline machinery.
25
+
26
+ ### Patch Changes
27
+
28
+ - 0763205: Export the `mat3` namespace from the package entry
29
+
30
+ `resolveSkeleton` returns `Map<string, Mat3>` and `Mat3` was exported as a
31
+ type, but the operations that read one were not. Placing a bone tip meant
32
+ indexing the `Float32Array` by hand — `[m[0] * length + m[6], m[1] * length +
33
+ m[7]]` — which is the matrix layout leaking into consumer code.
34
+
35
+ `mat3` is now importable from `@weasel-js/core`, so that line is
36
+ `mat3.apply(m, length, 0)`. Alongside `apply` the namespace carries
37
+ `identity`, `multiply`, `translate`, `scale`, `invert` and `screenToClip`.
38
+
39
+ This is the renderer's 9-element column-major form, matching what
40
+ `uniformMatrix3fv` uploads. `@weasel-js/geom` exports its own `Mat3` — a
41
+ 6-element affine — with the same logical element order but a different array
42
+ shape; the two are not interchangeable.
43
+
44
+ - b65aadd: Undo restores z-order, and reorder ops survive a reload
45
+
46
+ Two defects in the op layer, found reviewing `core/ops` and `core/adapters`.
47
+
48
+ `createDeleteOp` captures the node's z-index and forwards it through
49
+ `invert()` so undo puts the node back where it was — but `SceneAdapter`
50
+ declared `insertNode(node)` with no index parameter, so the only
51
+ implementation that honored it was the one that had gone off-contract to
52
+ accept it. `arrayAdapter` appended unconditionally and `animateLifecycle`
53
+ dropped the argument while wrapping. Deleting a node and undoing therefore
54
+ moved it to the top of the paint order, and undoing a multi-delete reversed
55
+ the stack. The parameter is now part of the interface and both
56
+ implementations honor it.
57
+
58
+ `createReorderOp` — bring forward, send backward, bring to front, send to
59
+ back — built an op with no `name` and registered no factory, though `Op`'s
60
+ contract says kit-emitted ops always carry one. `History.serialize()` drops
61
+ any entry holding a nameless op, so all four silently vanished from the
62
+ persisted undo stack on reload while `moveToIndex`, which does register,
63
+ survived. They now serialize, with the per-parent before-order carried in
64
+ the op's args so a rebuilt op can still invert.
65
+
66
+ - 0c13967: An empty container no longer draws selection chrome at its own stored pose
67
+
68
+ `composeSelectionPose` and the overlay's container-aware bounds resolver both
69
+ document that a container with no leaves resolves to `null`. Neither could
70
+ return it: the leaf walk pushed any childless node, so an empty container
71
+ pushed _itself_, and the `leaves.length === 0` guard was unreachable. The
72
+ container's own stored pose then became the overlay bounds — the one value
73
+ this resolver exists to avoid, and at its most stale when nothing is left
74
+ inside to have moved it.
75
+
76
+ A childless node now counts as a leaf only when it is not itself a container.
77
+
78
+ - 83ba8b0: A finished timeline can be scrubbed back instead of going inert
79
+
80
+ A non-looping timeline that reached `duration` returned `finished` from its
81
+ tick and left the animator's table, but its handle kept answering `time()` and
82
+ `duration()` as though it were live. `seek()` moved a playhead nothing ticked —
83
+ no error, no state change. The only recovery was to build a new timeline.
84
+
85
+ `seek`, and an `edit` that extends the duration past the playhead, now
86
+ re-register the timeline and it plays on. It still finishes: an entry that
87
+ never retires would hold a slot, and the frame loop, open for every timeline
88
+ ever created.
89
+
90
+ - `onDone` fires once per arrival at the end, and again for a replay. A handler
91
+ that seeks back from inside `onDone` keeps the entry live rather than
92
+ stranding the replay it just started.
93
+ - Reviving re-registers under the same `cancelKey` without cancelling whoever
94
+ claimed that key meanwhile, and the revived timeline is still cancellable by
95
+ it.
96
+ - `cancel()` is final, including on a timeline that had already finished. No
97
+ seek or edit revives a cancelled one.
98
+ - A `pause()` or `setTimeScale()` taken while the timeline was off the table
99
+ applies when it comes back, so scrubbing a paused transport does not silently
100
+ start playback.
101
+ - @weasel-js/font@1.1.0
102
+ - @weasel-js/geom@1.1.0
103
+ - @weasel-js/gestures@1.1.0
104
+ - @weasel-js/history@1.1.0
105
+ - @weasel-js/modes@1.1.0
106
+
3
107
  ## 1.0.4
4
108
 
5
109
  ### Patch Changes
@@ -19,7 +19,7 @@ import { createHistory } from '@weasel-js/history';
19
19
  export * from '@weasel-js/history';
20
20
 
21
21
  // src/version.ts
22
- var VERSION = "1.0.4" ;
22
+ var VERSION = "1.1.0" ;
23
23
 
24
24
  // src/features/grid/roundToCell.ts
25
25
  function roundToCell(value, cellSize) {
@@ -5956,7 +5956,7 @@ var IDENTITY_POSE_COMPOSITION = {
5956
5956
  compose: (_parent, child) => child,
5957
5957
  decompose: (_parent, world) => world
5958
5958
  };
5959
- function composeWorldPose(adapter, id, compose) {
5959
+ function composeWorldPose(adapter, id, compose2) {
5960
5960
  const chain = [id];
5961
5961
  const seen = /* @__PURE__ */ new Set([id]);
5962
5962
  let cursor = adapter.getParent(id);
@@ -5968,7 +5968,7 @@ function composeWorldPose(adapter, id, compose) {
5968
5968
  }
5969
5969
  let world = adapter.getPose(chain[chain.length - 1]);
5970
5970
  for (let i = chain.length - 2; i >= 0; i--) {
5971
- world = compose(world, adapter.getPose(chain[i]));
5971
+ world = compose2(world, adapter.getPose(chain[i]));
5972
5972
  }
5973
5973
  return world;
5974
5974
  }
@@ -5982,9 +5982,9 @@ function composeRectPose(parent, child) {
5982
5982
  function translateRectPose(pose, dx, dy) {
5983
5983
  return { ...pose, x: pose.x + dx, y: pose.y + dy };
5984
5984
  }
5985
- function rebaseLocalPose(adapter, worldPose, newParentId, compose, decompose) {
5985
+ function rebaseLocalPose(adapter, worldPose, newParentId, compose2, decompose) {
5986
5986
  if (newParentId === null) return worldPose;
5987
- const parentWorld = composeWorldPose(adapter, newParentId, compose);
5987
+ const parentWorld = composeWorldPose(adapter, newParentId, compose2);
5988
5988
  return decompose(parentWorld, worldPose);
5989
5989
  }
5990
5990
  function decomposeRectPose(parent, world) {
@@ -5994,10 +5994,10 @@ function decomposeRectPose(parent, world) {
5994
5994
  y: world.y - parent.y
5995
5995
  };
5996
5996
  }
5997
- function worldPoseLookup(adapter, compose) {
5997
+ function worldPoseLookup(adapter, compose2) {
5998
5998
  return (id) => {
5999
5999
  try {
6000
- return composeWorldPose(adapter, id, compose);
6000
+ return composeWorldPose(adapter, id, compose2);
6001
6001
  } catch {
6002
6002
  return null;
6003
6003
  }
@@ -9055,9 +9055,17 @@ function arraysEqual(a, b) {
9055
9055
  return true;
9056
9056
  }
9057
9057
  function createPartitionedReorderOp(args) {
9058
- const { ids, fn, label } = args;
9059
- let restore = null;
9058
+ const { ids, direction, fn, label, prevOrders } = args;
9059
+ const argsForSerial = {
9060
+ ids,
9061
+ direction,
9062
+ label,
9063
+ prevOrders: prevOrders ? prevOrders.map((e) => ({ ...e, before: e.before.slice() })) : void 0
9064
+ };
9065
+ let restore = prevOrders ? prevOrders.map((e) => ({ ...e, before: e.before.slice() })) : null;
9060
9066
  return {
9067
+ name: "reorder",
9068
+ args: argsForSerial,
9061
9069
  label,
9062
9070
  apply(adapter) {
9063
9071
  const a = adapter;
@@ -9080,6 +9088,7 @@ function createPartitionedReorderOp(args) {
9080
9088
  mutated = true;
9081
9089
  }
9082
9090
  restore = snapshots;
9091
+ argsForSerial.prevOrders = snapshots.map((e) => ({ ...e, before: e.before.slice() }));
9083
9092
  return mutated;
9084
9093
  },
9085
9094
  invert() {
@@ -9111,10 +9120,17 @@ var REORDER_DIRECTIONS = {
9111
9120
  back: { fn: sendToBack, defaultLabel: "Send to back" }
9112
9121
  };
9113
9122
  function createReorderOp(args) {
9114
- const { ids, direction, label } = args;
9123
+ const { ids, direction, label, prevOrders } = args;
9115
9124
  const { fn, defaultLabel } = REORDER_DIRECTIONS[direction];
9116
- return createPartitionedReorderOp({ ids, fn, label: label ?? defaultLabel });
9125
+ return createPartitionedReorderOp({
9126
+ ids,
9127
+ direction,
9128
+ fn,
9129
+ label: label ?? defaultLabel,
9130
+ prevOrders
9131
+ });
9117
9132
  }
9133
+ registerOpFactory("reorder", (args) => createReorderOp(args));
9118
9134
  function createMoveToIndexOp(args) {
9119
9135
  const { ids, parentId, index, label } = args;
9120
9136
  const argsForSerial = {
@@ -16947,7 +16963,7 @@ function composeSelectionPose(opts) {
16947
16963
  const visit = (nid) => {
16948
16964
  const kids = getChildren(nid);
16949
16965
  if (kids.length === 0) {
16950
- out.push(nid);
16966
+ if (!isContainer(nid)) out.push(nid);
16951
16967
  return;
16952
16968
  }
16953
16969
  for (const k of kids) visit(k);
@@ -17000,7 +17016,7 @@ function makeContainerAwareBoundsResolver(getPose, getBounds, getChildren, isCon
17000
17016
  const visit = (nid) => {
17001
17017
  const kids = getChildren(nid);
17002
17018
  if (kids.length === 0) {
17003
- out.push(nid);
17019
+ if (!isContainer(nid)) out.push(nid);
17004
17020
  return;
17005
17021
  }
17006
17022
  for (const k of kids) visit(k);
@@ -26084,7 +26100,10 @@ function arrayAdapter(config) {
26084
26100
  },
26085
26101
  getParent,
26086
26102
  setParent,
26087
- insertNode: (obj) => setItems((items) => [...items, obj]),
26103
+ insertNode: (obj, index) => setItems((items) => {
26104
+ if (index === void 0 || index < 0 || index >= items.length) return [...items, obj];
26105
+ return [...items.slice(0, index), obj, ...items.slice(index)];
26106
+ }),
26088
26107
  removeNode: (id) => setItems((items) => items.filter((o) => o.id !== id)),
26089
26108
  getSelection,
26090
26109
  setSelection,
@@ -27370,6 +27389,225 @@ function makeBuilder(animator, timers, createSupervisor, watchCompletion, items,
27370
27389
  };
27371
27390
  }
27372
27391
 
27392
+ // src/animation/timeline/sampleTrack.ts
27393
+ function floorKeyIndex(keys, t) {
27394
+ let lo = 0;
27395
+ let hi = keys.length - 1;
27396
+ let found = -1;
27397
+ while (lo <= hi) {
27398
+ const mid2 = lo + hi >> 1;
27399
+ if (keys[mid2].t <= t) {
27400
+ found = mid2;
27401
+ lo = mid2 + 1;
27402
+ } else {
27403
+ hi = mid2 - 1;
27404
+ }
27405
+ }
27406
+ return found;
27407
+ }
27408
+ function sampleTrack(track, t, segmentCache) {
27409
+ const { keys } = track;
27410
+ if (keys.length === 0) return void 0;
27411
+ const i = floorKeyIndex(keys, t);
27412
+ if (i < 0) return keys[0].value;
27413
+ if (i >= keys.length - 1) return keys[keys.length - 1].value;
27414
+ const a = keys[i];
27415
+ const b = keys[i + 1];
27416
+ const span = b.t - a.t;
27417
+ const raw = span <= 0 ? 1 : (t - a.t) / span;
27418
+ const u = b.easing ? b.easing(raw) : raw;
27419
+ if (track.interpolator) {
27420
+ let fn = segmentCache?.get(i + 1);
27421
+ if (!fn) {
27422
+ fn = track.interpolator(a.value, b.value);
27423
+ segmentCache?.set(i + 1, fn);
27424
+ }
27425
+ return fn(u);
27426
+ }
27427
+ if (track.interpolate) return track.interpolate(a.value, b.value, u);
27428
+ if (typeof a.value === "number" && typeof b.value === "number") {
27429
+ return a.value + (b.value - a.value) * u;
27430
+ }
27431
+ throw new Error("sampleTrack: interpolate or interpolator is required for non-numeric keyframe values");
27432
+ }
27433
+
27434
+ // src/animation/timeline/createTimeline.ts
27435
+ function firstAfter(events, t) {
27436
+ let lo = 0;
27437
+ let hi = events.length;
27438
+ while (lo < hi) {
27439
+ const mid2 = lo + hi >> 1;
27440
+ if (events[mid2].t > t) hi = mid2;
27441
+ else lo = mid2 + 1;
27442
+ }
27443
+ return lo;
27444
+ }
27445
+ function trackEnd(track) {
27446
+ switch (track.kind) {
27447
+ case "sampled":
27448
+ return track.keys.length ? track.keys[track.keys.length - 1].t : 0;
27449
+ case "event":
27450
+ return track.events.length ? track.events[track.events.length - 1].t : 0;
27451
+ case "timeline":
27452
+ return track.at + tracksEnd(track.timeline.tracks, track.timeline.duration);
27453
+ }
27454
+ }
27455
+ function tracksEnd(tracks, explicit) {
27456
+ if (explicit != null) return explicit;
27457
+ let max = 0;
27458
+ for (const t of tracks) max = Math.max(max, trackEnd(t));
27459
+ return max;
27460
+ }
27461
+ function createTimeline(register, id, opts) {
27462
+ let duration = tracksEnd(opts.tracks, opts.duration);
27463
+ let offset = 0;
27464
+ let lastVirtual = 0;
27465
+ let playhead = 0;
27466
+ let prevPlayhead = -Infinity;
27467
+ let done = false;
27468
+ let live = true;
27469
+ let cancelled = false;
27470
+ let wantPaused = opts.autoplay === false;
27471
+ let wantScale = 1;
27472
+ const subscribers2 = /* @__PURE__ */ new Set();
27473
+ const loopOpt = opts.loop ?? false;
27474
+ let loopsLeft = loopOpt === true ? Infinity : loopOpt === false ? 0 : loopOpt;
27475
+ let caches = /* @__PURE__ */ new WeakMap();
27476
+ const cacheFor = (track) => {
27477
+ let c = caches.get(track);
27478
+ if (!c) {
27479
+ c = /* @__PURE__ */ new Map();
27480
+ caches.set(track, c);
27481
+ }
27482
+ return c;
27483
+ };
27484
+ const applySampled = (tracks, t) => {
27485
+ for (const track of tracks) {
27486
+ if (track.kind === "sampled") {
27487
+ const st = track;
27488
+ const v = sampleTrack(st, t, cacheFor(st));
27489
+ if (v !== void 0) st.onTick(v);
27490
+ } else if (track.kind === "timeline") {
27491
+ applySampled(track.timeline.tracks, t - track.at);
27492
+ }
27493
+ }
27494
+ };
27495
+ const fireEvents = (tracks, from, to) => {
27496
+ for (const track of tracks) {
27497
+ if (track.kind === "event") {
27498
+ const end = firstAfter(track.events, to);
27499
+ for (let i = firstAfter(track.events, from); i < end; i += 1) track.events[i].fire();
27500
+ } else if (track.kind === "timeline") {
27501
+ fireEvents(track.timeline.tracks, from - track.at, to - track.at);
27502
+ }
27503
+ }
27504
+ };
27505
+ const onWrap = () => {
27506
+ fireEvents(opts.tracks, prevPlayhead, duration);
27507
+ prevPlayhead = -Infinity;
27508
+ };
27509
+ const tick = (virtualNow) => {
27510
+ lastVirtual = virtualNow;
27511
+ playhead = virtualNow + offset;
27512
+ if (duration > 0 && playhead >= duration) {
27513
+ if (loopsLeft === Infinity) {
27514
+ const laps = Math.floor(playhead / duration);
27515
+ offset -= laps * duration;
27516
+ playhead -= laps * duration;
27517
+ onWrap();
27518
+ } else {
27519
+ while (playhead >= duration && loopsLeft > 0) {
27520
+ loopsLeft -= 1;
27521
+ offset -= duration;
27522
+ playhead -= duration;
27523
+ onWrap();
27524
+ }
27525
+ }
27526
+ }
27527
+ const finished = playhead >= duration;
27528
+ playhead = Math.min(playhead, duration);
27529
+ applySampled(opts.tracks, playhead);
27530
+ fireEvents(opts.tracks, prevPlayhead, playhead);
27531
+ prevPlayhead = playhead;
27532
+ if (finished && !done) {
27533
+ done = true;
27534
+ opts.onDone?.();
27535
+ }
27536
+ if (playhead < duration) return false;
27537
+ live = false;
27538
+ return true;
27539
+ };
27540
+ const onCancel = () => {
27541
+ cancelled = true;
27542
+ live = false;
27543
+ };
27544
+ const base = register({ id, cancelKey: opts.cancelKey, tick, onCancel });
27545
+ if (opts.autoplay === false) base.pause();
27546
+ const rearm = () => {
27547
+ if (cancelled || playhead >= duration) return;
27548
+ done = false;
27549
+ if (live) return;
27550
+ lastVirtual = 0;
27551
+ offset = playhead;
27552
+ live = true;
27553
+ register({ id, cancelKey: opts.cancelKey, keepExisting: true, tick, onCancel });
27554
+ if (wantPaused) base.pause();
27555
+ base.setTimeScale(wantScale);
27556
+ };
27557
+ return {
27558
+ ...base,
27559
+ // Not just `onCancel`: cancelling a finished timeline never reaches the
27560
+ // animator, and must still keep a later seek from reviving it.
27561
+ cancel() {
27562
+ onCancel();
27563
+ base.cancel();
27564
+ },
27565
+ // Playback intent is tracked here, not read back from the entry: a revived
27566
+ // one is a fresh registration, and defaults to running at scale 1.
27567
+ pause() {
27568
+ wantPaused = true;
27569
+ base.pause();
27570
+ },
27571
+ resume() {
27572
+ wantPaused = false;
27573
+ base.resume();
27574
+ },
27575
+ setTimeScale(s) {
27576
+ wantScale = s;
27577
+ base.setTimeScale(s);
27578
+ },
27579
+ isPaused: () => live ? base.isPaused() : wantPaused,
27580
+ seek(t) {
27581
+ offset = t - lastVirtual;
27582
+ playhead = t;
27583
+ prevPlayhead = t;
27584
+ rearm();
27585
+ },
27586
+ time: () => playhead,
27587
+ duration: () => duration,
27588
+ tracks: () => opts.tracks,
27589
+ edit(fn) {
27590
+ fn();
27591
+ caches = /* @__PURE__ */ new WeakMap();
27592
+ duration = tracksEnd(opts.tracks, opts.duration);
27593
+ rearm();
27594
+ for (const cb of subscribers2) {
27595
+ try {
27596
+ cb();
27597
+ } catch (err) {
27598
+ console.error("timeline: subscriber threw", err);
27599
+ }
27600
+ }
27601
+ },
27602
+ subscribe(cb) {
27603
+ subscribers2.add(cb);
27604
+ return () => {
27605
+ subscribers2.delete(cb);
27606
+ };
27607
+ }
27608
+ };
27609
+ }
27610
+
27373
27611
  // src/animation/colorRegistry.ts
27374
27612
  var ColorOverrideRegistry = class {
27375
27613
  map = /* @__PURE__ */ new Map();
@@ -27524,7 +27762,7 @@ function useAnimator(opts = {}) {
27524
27762
  }
27525
27763
  };
27526
27764
  const register = (seed) => {
27527
- if (seed.cancelKey != null) cancelByKey(seed.cancelKey);
27765
+ if (seed.cancelKey != null && !seed.keepExisting) cancelByKey(seed.cancelKey);
27528
27766
  const anim = seed;
27529
27767
  anim.paused = false;
27530
27768
  anim.timeScale = 1;
@@ -27763,6 +28001,7 @@ function useAnimator(opts = {}) {
27763
28001
  factory,
27764
28002
  staggerOpts
27765
28003
  )),
28004
+ timeline: (o) => createTimeline(register, nextId.current++, o),
27766
28005
  colorOverrides: colorOverrides.current,
27767
28006
  onTick: (cb) => {
27768
28007
  tickSubscribers.current.add(cb);
@@ -27834,8 +28073,8 @@ function animateLifecycle(adapter, animator, opts) {
27834
28073
  const ms = opts.ms ?? 200;
27835
28074
  return {
27836
28075
  ...adapter,
27837
- insertNode(object) {
27838
- adapter.insertNode(object);
28076
+ insertNode(object, index) {
28077
+ adapter.insertNode(object, index);
27839
28078
  if (!opts.enterFrom) return;
27840
28079
  const final = adapter.getPose(object.id);
27841
28080
  const start = opts.enterFrom(final);
@@ -28298,6 +28537,101 @@ function hslToRgbUnit(h, s, l) {
28298
28537
  return hslToRgb(h, s, l);
28299
28538
  }
28300
28539
 
28540
+ // src/animation/rig/types.ts
28541
+ var IDENTITY_JOINT = {
28542
+ x: 0,
28543
+ y: 0,
28544
+ rotation: 0,
28545
+ scaleX: 1,
28546
+ scaleY: 1
28547
+ };
28548
+
28549
+ // src/animation/rig/blendPoses.ts
28550
+ var TAU = Math.PI * 2;
28551
+ function shortestDelta(a, b) {
28552
+ let d = (b - a) % TAU;
28553
+ if (d > Math.PI) d -= TAU;
28554
+ if (d < -Math.PI) d += TAU;
28555
+ return d;
28556
+ }
28557
+ var FIELDS = ["x", "y", "scaleX", "scaleY"];
28558
+ function blendPoses(poses, weights) {
28559
+ if (poses.length !== weights.length) {
28560
+ throw new Error("blendPoses: poses and weights must have the same length");
28561
+ }
28562
+ const total = weights.reduce((s, w) => s + w, 0);
28563
+ if (total === 0) return {};
28564
+ const norm = weights.map((w) => w / total);
28565
+ const names = /* @__PURE__ */ new Set();
28566
+ for (const p of poses) for (const k of Object.keys(p)) names.add(k);
28567
+ const out = {};
28568
+ for (const name of names) {
28569
+ const joint = {};
28570
+ for (const field of FIELDS) {
28571
+ let acc = 0;
28572
+ for (let i = 0; i < poses.length; i += 1) {
28573
+ const v = poses[i][name]?.[field];
28574
+ acc += (v ?? IDENTITY_JOINT[field]) * norm[i];
28575
+ }
28576
+ joint[field] = acc;
28577
+ }
28578
+ const base = poses[0][name]?.rotation ?? 0;
28579
+ let rot = 0;
28580
+ for (let i = 0; i < poses.length; i += 1) {
28581
+ const v = poses[i][name]?.rotation ?? 0;
28582
+ rot += shortestDelta(base, v) * norm[i];
28583
+ }
28584
+ joint.rotation = base + rot;
28585
+ out[name] = joint;
28586
+ }
28587
+ return out;
28588
+ }
28589
+
28590
+ // src/animation/rig/resolveSkeleton.ts
28591
+ function compose(bind, delta3) {
28592
+ if (!delta3) return bind;
28593
+ return {
28594
+ x: bind.x + (delta3.x ?? 0),
28595
+ y: bind.y + (delta3.y ?? 0),
28596
+ rotation: bind.rotation + (delta3.rotation ?? 0),
28597
+ scaleX: bind.scaleX * (delta3.scaleX ?? 1),
28598
+ scaleY: bind.scaleY * (delta3.scaleY ?? 1)
28599
+ };
28600
+ }
28601
+ function toMat3(t) {
28602
+ const c = Math.cos(t.rotation);
28603
+ const s = Math.sin(t.rotation);
28604
+ const m = new Float32Array(9);
28605
+ m[0] = c * t.scaleX;
28606
+ m[1] = s * t.scaleX;
28607
+ m[2] = 0;
28608
+ m[3] = -s * t.scaleY;
28609
+ m[4] = c * t.scaleY;
28610
+ m[5] = 0;
28611
+ m[6] = t.x;
28612
+ m[7] = t.y;
28613
+ m[8] = 1;
28614
+ return m;
28615
+ }
28616
+ function resolveSkeleton(skeleton, pose) {
28617
+ const out = /* @__PURE__ */ new Map();
28618
+ for (const joint of skeleton.joints) {
28619
+ const local = toMat3(compose(joint.bind, pose[joint.name]));
28620
+ if (joint.parent == null) {
28621
+ out.set(joint.name, local);
28622
+ continue;
28623
+ }
28624
+ const parent = out.get(joint.parent);
28625
+ if (!parent) {
28626
+ throw new Error(
28627
+ `resolveSkeleton: joint "${joint.name}" names parent "${joint.parent}", which has not been resolved. Skeleton.joints must be in topological order.`
28628
+ );
28629
+ }
28630
+ out.set(joint.name, mat3.multiply(parent, local));
28631
+ }
28632
+ return out;
28633
+ }
28634
+
28301
28635
  // src/layout/snaps.ts
28302
28636
  function dist22(a, b) {
28303
28637
  const dx = a.x - b.x;
@@ -28586,6 +28920,6 @@ function mergeContributions(...bundles) {
28586
28920
  return out;
28587
28921
  }
28588
28922
 
28589
- export { ALWAYS, ANCHOR_HIT_BASE_PX, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, COARSE_TARGET_SCALE, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_STROKES, DEFAULT_DEBUG_THEME, DEFAULT_DEVICE_PROFILE, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_STYLE, DEFAULT_VELOCITY_DECAY, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_POSE_COMPOSITION, IntersectIcon, KIT_SHAPE_KINDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TextIcon, UnionIcon, UnknownIcon, VERSION, WeaselProvider, WeaselRenderer, aabbCenter, actionIs, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, alignedStrokeRect, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, bezierCubic, bezierQuadratic, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView2 as computeFitView, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMarkdownRenderer, createMoveToIndexOp, createNodeProperties, createNodeRouting, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createViewportLayer, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, defineTool, defineViewportTool, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic2 as easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, fillInPoseFrame, fillToBoundsFrame, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, fontString, forEachCoalesced, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getNodeShapes, getStylusData, gradientForBounds, gradientGeometry, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, isEditableTarget2 as isEditableTarget, isPatternSpec, lassoSelectAction, layoutMarkdown, lerpColorArray, lerpOklab, lerpOklch, linear, liveScope, makeViewportZoomAction, markdownToRuns, mat3, matchAlignment, matchesKeyBinding, meanScale, measureText, measureTextBounds, measuredWidth, mergeAlphaFromPrev, mergeContributions, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, nodeHasFlag, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, or, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerNodeShape, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveDeviceProfile, resolveFillPattern, resolvePatternSpec, resolveRuns, resolveTextStyle, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, runsToMarkdown, runsToPlainText, sampleGradientStops, sceneFromJSON, sceneNodeClientRect, sceneToAdapter, scopeBindings, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, setFlagOverRange, shapeCoversPoint, sliceAction, snapPoint, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textLineBoxes, tileGrid, toHex8, toRuns, translatePoseViaDescriptor, translateRectPose, tweenPose, tweenVertexColors, unionBounds, unionBoundsPath, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useOptionalActiveToolContext, usePenTool, usePencilTool, usePinchGesture, usePinchZoomTool, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewTween, useZoom, verticalAlignOffset, viewToMat3, viewToTransform, viewportDragPanAction, viewportZoomAction, viewportsAt, when, withAlpha01, withCoord, withGradientKind, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
28590
- //# sourceMappingURL=chunk-AYGSXNY3.js.map
28591
- //# sourceMappingURL=chunk-AYGSXNY3.js.map
28923
+ export { ALWAYS, ANCHOR_HIT_BASE_PX, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, COARSE_TARGET_SCALE, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_STROKES, DEFAULT_DEBUG_THEME, DEFAULT_DEVICE_PROFILE, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_STYLE, DEFAULT_VELOCITY_DECAY, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_JOINT, IDENTITY_POSE_COMPOSITION, IntersectIcon, KIT_SHAPE_KINDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TextIcon, UnionIcon, UnknownIcon, VERSION, WeaselProvider, WeaselRenderer, aabbCenter, actionIs, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, alignedStrokeRect, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, bezierCubic, bezierQuadratic, blendPoses, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView2 as computeFitView, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMarkdownRenderer, createMoveToIndexOp, createNodeProperties, createNodeRouting, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createViewportLayer, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, defineTool, defineViewportTool, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic2 as easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, fillInPoseFrame, fillToBoundsFrame, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, fontString, forEachCoalesced, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getNodeShapes, getStylusData, gradientForBounds, gradientGeometry, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, isEditableTarget2 as isEditableTarget, isPatternSpec, lassoSelectAction, layoutMarkdown, lerpColorArray, lerpOklab, lerpOklch, linear, liveScope, makeViewportZoomAction, markdownToRuns, mat3, matchAlignment, matchesKeyBinding, meanScale, measureText, measureTextBounds, measuredWidth, mergeAlphaFromPrev, mergeContributions, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, nodeHasFlag, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, or, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerNodeShape, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveDeviceProfile, resolveFillPattern, resolvePatternSpec, resolveRuns, resolveSkeleton, resolveTextStyle, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, runsToMarkdown, runsToPlainText, sampleGradientStops, sampleTrack, sceneFromJSON, sceneNodeClientRect, sceneToAdapter, scopeBindings, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, setFlagOverRange, shapeCoversPoint, sliceAction, snapPoint, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textLineBoxes, tileGrid, toHex8, toRuns, translatePoseViaDescriptor, translateRectPose, tweenPose, tweenVertexColors, unionBounds, unionBoundsPath, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useOptionalActiveToolContext, usePenTool, usePencilTool, usePinchGesture, usePinchZoomTool, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewTween, useZoom, verticalAlignOffset, viewToMat3, viewToTransform, viewportDragPanAction, viewportZoomAction, viewportsAt, when, withAlpha01, withCoord, withGradientKind, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
28924
+ //# sourceMappingURL=chunk-3B4QEB2G.js.map
28925
+ //# sourceMappingURL=chunk-3B4QEB2G.js.map