@volter/blender-engine 0.1.133 → 0.1.134

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.
@@ -68,20 +68,11 @@ export type WorkerRequest =
68
68
  * Given no `object`, the view layer's active object answers. READ ONLY:
69
69
  * pinning, unwrapping and selecting are edits. */
70
70
  | { id: number; op: 'uv-layout'; object?: string; uvLayer?: string }
71
- /** ONE MESH'S SKIN BINDING (`session.py`'s `rna_rig`) — the armature's bones
72
- * with their rest AND bind matrices, plus up to four weighted influences
73
- * per Blender vertex as base64 typed-array bytes. The BIND pose is the
74
- * pose the exported columns were evaluated at, which is what lets three.js
75
- * own playback without Blender's frame ever moving. READ ONLY. */
76
- | { id: number; op: 'rig'; object?: string }
77
71
  /** ONE ACTION AS A THREE.JS CLIP (`session.py`'s `rna_action_clip`) — per
78
72
  * bone, the LOCAL transform at every integer frame of the action's range,
79
73
  * plus the scene's frame range/fps and the summary row's key columns.
80
74
  * READ ONLY: keying, moving a key and setting a range are edits. */
81
75
  | { id: number; op: 'action-clip'; object?: string; bake?: boolean; action?: string }
82
- /** EVERY ARMATURE AND THE ACTIONS THAT CAN PLAY ON IT (`session.py`'s `rna_armature_actions`):
83
- * each action that animates one of its pose bones, assigned or not. READ ONLY. */
84
- | { id: number; op: 'armature-actions' }
85
76
  /** ONE RESTRICTION COLUMN, written (`session.py`'s `outliner_set`). A column
86
77
  * Blender draws on no row of that type is refused by name. */
87
78
  | { id: number; op: 'outliner-set'; path: string; column: string; value: boolean }
package/browser/rna.ts CHANGED
@@ -527,79 +527,6 @@ export interface BlenderUvLayout {
527
527
  } | null;
528
528
  }
529
529
 
530
- /**
531
- * ONE MESH'S SKIN BINDING (`session.py`'s `rna_rig`) — what a
532
- * `THREE.SkinnedMesh` needs, and nothing Blender does not carry.
533
- *
534
- * WE VISUALIZE WITH THREE.JS, NOT BLENDER (owner rule, 2026-09-20). Blender
535
- * holds the animation as DATA; three.js plays it. This door and
536
- * {@link BlenderActionClip} are the whole of what crosses to make that true —
537
- * once per rig, rather than a depsgraph evaluation and a re-export per frame.
538
- *
539
- * THE BIND POSE IS `pose`, NOT `rest`, and that is the load-bearing fact: the
540
- * export door runs with `evaluate: True`, so the mesh columns the presenter
541
- * holds are the mesh ALREADY DEFORMED at Blender's current frame. A skeleton
542
- * bound in that same pose makes the skinning an identity there, so the picture
543
- * at the bind frame is exactly the frame Blender presented — and Blender's
544
- * `frame_current` never has to move again for playback.
545
- *
546
- * THE INDICES ARE BLENDER VERTEX INDICES. `DrawArrays.sourceVertex` maps a
547
- * drawn vertex back to its Blender one, and the presenter expands these arrays
548
- * through it (`blender-runtime-skin.ts`) exactly as I4's weight ramp does.
549
- */
550
- export interface BlenderRigBone {
551
- readonly name: string;
552
- readonly parent: string | null;
553
- /** `Bone.matrix_local` — the REST pose in the armature object's space,
554
- * row-major as four rows. Diagnostic here; the bind is `pose`. */
555
- readonly rest: readonly (readonly [number, number, number, number])[];
556
- /** `PoseBone.matrix` (`pchan->pose_mat`), the same matrix I4's bone overlay
557
- * draws — the BIND pose, in the armature object's space. */
558
- readonly pose: readonly (readonly [number, number, number, number])[];
559
- readonly length: number;
560
- readonly deform: boolean;
561
- readonly connected: boolean;
562
- }
563
-
564
- export interface BlenderRigBinding {
565
- readonly object: string | null;
566
- readonly mesh?: string;
567
- readonly armature: string | null;
568
- readonly armatureData?: string;
569
- readonly bones: readonly BlenderRigBone[];
570
- readonly vertexCount: number;
571
- /** `scene.frame_current` when the binding was read — the frame the exported
572
- * columns, and therefore the bind pose, belong to. */
573
- readonly frame?: number;
574
- readonly mode?: string;
575
- /** Bones whose pose is a SOLVER's rather than their channels'. The clip is
576
- * derived from the F-Curves alone, so a constrained bone's playback cannot
577
- * reproduce Blender's own answer; the view names them instead of drifting. */
578
- readonly constrainedBones?: readonly string[];
579
- /** Uint16, `vertexCount * 4`. */
580
- readonly skinIndexBase64?: string;
581
- /** Float32, `vertexCount * 4`, each vertex's four normalized to sum to one. */
582
- readonly skinWeightBase64?: string;
583
- readonly maxInfluences?: number;
584
- readonly truncatedVertices?: number;
585
- readonly unweightedVertices?: number;
586
- readonly unmappedGroups?: readonly string[];
587
- /** Why there is no binding, when there is none. Never a silent empty. */
588
- readonly reason?: string;
589
- }
590
-
591
- /** EVERY SKIN BINDING THE SCENE NEEDS, which is the PRESENTER's question: a
592
- * presented frame carries every object at once and the presenter has to know
593
- * which of its meshes are `THREE.SkinnedMesh`es before it builds them. With
594
- * `named` set, the list is that one mesh. */
595
- export interface BlenderRig {
596
- /** `scene.frame_current` when the bindings were read — the frame the
597
- * exported columns, and therefore every bind pose here, belong to. */
598
- readonly frame: number;
599
- readonly named: string | null;
600
- readonly rigs: readonly BlenderRigBinding[];
601
- }
602
-
603
530
  /**
604
531
  * ONE TRACK of {@link BlenderActionClip}: a bone's LOCAL `position`,
605
532
  * `quaternion` or `scale` sampled at every integer frame.
@@ -21,7 +21,6 @@ import type {
21
21
  BlenderNodeTree,
22
22
  BlenderOutlinerTree,
23
23
  BlenderOutlinerWrite,
24
- BlenderRig,
25
24
  BlenderRnaContext,
26
25
  BlenderRnaView,
27
26
  BlenderRnaWrite,
@@ -408,16 +407,6 @@ export class BlenderRuntime {
408
407
  })) as BlenderUvLayout;
409
408
  }
410
409
 
411
- /** ONE MESH'S SKIN BINDING (`./rna.ts`, `BlenderRig`) — the armature's
412
- * bones and the per-vertex influences a `THREE.SkinnedMesh` needs. Given no
413
- * `object`, the view layer's ACTIVE mesh answers. */
414
- async rig(options?: { object?: string }): Promise<BlenderRig> {
415
- await this.#ready();
416
- return (await this.#request({
417
- op: 'rig',
418
- ...(options?.object === undefined ? {} : { object: options.object }),
419
- })) as BlenderRig;
420
- }
421
410
 
422
411
  /** ONE ACTION AS A THREE.JS CLIP (`./rna.ts`, `BlenderActionClip`). `bake:
423
412
  * false` answers the header and the summary row's key columns without the
@@ -433,11 +422,6 @@ export class BlenderRuntime {
433
422
  })) as BlenderActionClip;
434
423
  }
435
424
 
436
- /** Every armature and the actions that can play on it, assigned or not (a game's clip library). */
437
- async armatureActions(): Promise<{ armatures: { armature: string; current: string | null; actions: string[]; keys?: string }[]; scene?: { frameCurrent: number; frameStart: number; frameEnd: number; fps: number } }> {
438
- await this.#ready();
439
- return (await this.#request({ op: 'armature-actions' })) as { armatures: { armature: string; current: string | null; actions: string[]; keys?: string }[]; scene?: { frameCurrent: number; frameStart: number; frameEnd: number; fps: number } };
440
- }
441
425
 
442
426
  /** Write ONE restriction column (the eye, the render camera, a collection's
443
427
  * Exclude). A column Blender draws on no row of that type is refused by
@@ -1372,6 +1372,12 @@ def _armatures(view_layer):
1372
1372
  # `ARM_DRAW_MODE_*` switch, `:906-936`).
1373
1373
  "mode": obj.mode,
1374
1374
  "bones": _armature_bones(obj, locked),
1375
+ # THE ACTION IT PLAYS (`animation_data.action`): what the presenter's mixer plays on
1376
+ # this armature's skeleton, baked through the clip door at the revision the frame's
1377
+ # `actions` names for it.
1378
+ "action": (obj.animation_data.action.name
1379
+ if obj.animation_data is not None and obj.animation_data.action is not None
1380
+ else None),
1375
1381
  }
1376
1382
  return armatures
1377
1383
 
@@ -1642,6 +1648,9 @@ class Session:
1642
1648
  "scale_length": float(scene.unit_settings.scale_length),
1643
1649
  }
1644
1650
  frame["armatures"] = _armatures(view_layer)
1651
+ # THE SCENE'S CLOCK beside the frame number: what a Timeline draws and a mixer maps time by.
1652
+ frame["clock"] = {"start": int(scene.frame_start), "end": int(scene.frame_end),
1653
+ "fps": float(scene.render.fps) / float(scene.render.fps_base or 1.0)}
1645
1654
  frame["weights"] = _weights(scene, view_layer, frame, self._known)
1646
1655
  warnings = list(frame.get("warnings", ()))
1647
1656
  warnings.extend(_WARNINGS)
@@ -4617,31 +4626,6 @@ def _matrix_rows(matrix):
4617
4626
  return [[float(v) for v in row] for row in matrix]
4618
4627
 
4619
4628
 
4620
- def rna_rig(object_name=None):
4621
- """EVERY SKIN BINDING THE SCENE NEEDS -- or one named mesh's.
4622
-
4623
- A LIST, not a singleton, and that is what the PRESENTER's question is: a
4624
- presented frame carries every object at once, and the presenter has to know
4625
- which of its meshes are `THREE.SkinnedMesh`es before it builds them. Asking
4626
- per object would be one round trip per mesh; asking by NAME stays available
4627
- for a `cyclotron eval` that wants to read one.
4628
- """
4629
- scene_frame = int(bpy.context.scene.frame_current)
4630
- if object_name:
4631
- obj = bpy.data.objects.get(object_name)
4632
- if obj is None:
4633
- raise ValueError("The engine holds no object named %r" % (object_name,))
4634
- subjects = [obj]
4635
- else:
4636
- subjects = [obj for obj in bpy.context.view_layer.objects
4637
- if obj.type == "MESH" and obj.data is not None
4638
- and obj.find_armature() is not None]
4639
- rigs = []
4640
- for obj in subjects:
4641
- _asked({"checkpoint": "rig-object"})
4642
- rigs.append(_rig_of(obj))
4643
- return {"frame": scene_frame, "named": object_name, "rigs": rigs}
4644
-
4645
4629
 
4646
4630
  def _rig_of(obj):
4647
4631
  """ONE MESH'S SKIN BINDING: the armature's bones, and up to four weighted
@@ -4841,50 +4825,6 @@ def _summary_objects():
4841
4825
  return out
4842
4826
 
4843
4827
 
4844
- def rna_armature_actions():
4845
- """EVERY ARMATURE AND THE ACTIONS THAT CAN PLAY ON IT -- what a game picks its clips from.
4846
-
4847
- An action can play on an armature when it animates at least one of that armature's pose bones
4848
- (`pose.bones["<name>"]...` on a bone the armature has). Read off each action's F-Curves through
4849
- `_action_channelbag`, every channelbag, so an action assigned to nothing (a clip library: Idle,
4850
- Run, Attack beside each other in the file) is offered as well as the one each armature plays now.
4851
- READ ONLY."""
4852
- out = []
4853
- for arm in bpy.data.objects:
4854
- if arm.type != "ARMATURE" or arm.pose is None:
4855
- continue
4856
- bones = {pchan.name for pchan in arm.pose.bones}
4857
- current = arm.animation_data.action.name if (arm.animation_data and arm.animation_data.action) else None
4858
- names = []
4859
- keys, total = 0, 0.0
4860
- for action in bpy.data.actions:
4861
- curves = []
4862
- legacy = getattr(action, "fcurves", None)
4863
- if legacy is not None:
4864
- curves = list(legacy)
4865
- else:
4866
- for layer in getattr(action, "layers", ()):
4867
- for strip in getattr(layer, "strips", ()):
4868
- for bag in (getattr(strip, "channelbags", None) or ()):
4869
- curves.extend(bag.fcurves)
4870
- if any((m := _BONE_PATH.match(fc.data_path)) is not None and m.group(1) in bones for fc in curves):
4871
- names.append(action.name)
4872
- # A CHEAP SIGNATURE of the keys, so a reader can tell a key edit from no change
4873
- # without baking: the count and the sum of every key's frame and value.
4874
- for fc in curves:
4875
- for key in fc.keyframe_points:
4876
- keys += 1
4877
- total += float(key.co[0]) * 7.0 + float(key.co[1])
4878
- out.append({"armature": arm.name, "current": current, "actions": sorted(names),
4879
- "keys": "%d:%.6f" % (keys, total)})
4880
- scene = bpy.context.scene
4881
- # THE SCENE'S CLOCK TOO: a reader that skips a re-bind on an unchanged listing must still see
4882
- # the playhead and range move when an agent sets them.
4883
- return {"armatures": out, "scene": {"frameCurrent": int(scene.frame_current),
4884
- "frameStart": int(scene.frame_start), "frameEnd": int(scene.frame_end),
4885
- "fps": float(scene.render.fps) / float(scene.render.fps_base or 1.0)}}
4886
-
4887
-
4888
4828
  def rna_action_clip(object_name=None, bake=True, action_name=None):
4889
4829
  """ONE ACTION AS A THREE.JS CLIP: per bone, the LOCAL transform it has at
4890
4830
  every integer frame of the action's own range.
@@ -5248,13 +5188,44 @@ def rna_outliner(selected=None):
5248
5188
  parent_row["children"].append(duplicate)
5249
5189
  mine.append([duplicate, parent_row, parent_collection])
5250
5190
 
5191
+ # THE SELECTION KEEPS ITS ROWS PAST THE PAGE. A row is how the editor
5192
+ # addresses an object -- the viewport's outline and gizmo, the Properties
5193
+ # rail -- so a selected or active object, the caller's or the engine's, keeps
5194
+ # its row and every row above it however far down a long list it sits.
5195
+ # BOUNDED LIKE THE PAGE ITSELF: the active object always, then at most one
5196
+ # page of the selection, so selecting everything cannot turn one read back
5197
+ # into the whole tree the page exists to keep out (Canyon Comet, 1,493
5198
+ # objects selected: 1.77 MB a read uncapped, 142 KB capped).
5199
+ wanted = [active] if active is not None else []
5200
+ wanted += [o.name for o in view_layer.objects if o.select_get(view_layer=view_layer)]
5201
+ wanted += sorted(chosen)
5202
+ kept = set()
5203
+ for name in wanted:
5204
+ if len(kept) > _OUTLINER_PAGE:
5205
+ break
5206
+ kept.add(name)
5207
+ holding = set()
5208
+
5209
+ def hold(row):
5210
+ inside = row.get("struct") == "Object" and row.get("object") in kept
5211
+ for child in row.get("children", []):
5212
+ inside = hold(child) or inside
5213
+ if inside:
5214
+ holding.add(id(row))
5215
+ return inside
5216
+
5217
+ hold(scene_collection)
5218
+
5251
5219
  # Cap the hierarchy's visible child lists, not the collection's flat input.
5252
5220
  # Every parent participates in folding even when its child list needs a page.
5253
5221
  def page_tree(row):
5254
- children, more = _outliner_page(row.get("children", []))
5222
+ members = row.get("children", [])
5223
+ children, more = _outliner_page(members)
5224
+ beyond = [child for child in members[len(children):] if id(child) in holding]
5225
+ children = children + beyond
5255
5226
  row["children"] = children
5256
- if more:
5257
- row["more"] = more
5227
+ if more - len(beyond):
5228
+ row["more"] = more - len(beyond)
5258
5229
  for child in children:
5259
5230
  page_tree(child)
5260
5231
 
@@ -5511,12 +5482,8 @@ def _dispatch(request):
5511
5482
  # THE RIG AND CLIP DOORS (the Timeline): both READS, so neither presents
5512
5483
  # and neither owes a derivation. There is deliberately no writer beside
5513
5484
  # them -- keying, moving a key and setting a range are edits.
5514
- if op == "rig":
5515
- return rna_rig(request.get("object"))
5516
5485
  if op == "action-clip":
5517
5486
  return rna_action_clip(request.get("object"), request.get("bake", True), request.get("action"))
5518
- if op == "armature-actions":
5519
- return rna_armature_actions()
5520
5487
  if op == "outliner":
5521
5488
  return rna_outliner(request.get("selected"))
5522
5489
  if op == "outliner-set":
@@ -61,6 +61,8 @@ export const armatureSchema = z
61
61
  showInFront: z.boolean(),
62
62
  mode: z.string(),
63
63
  bones: z.array(boneSchema),
64
+ /** `animation_data.action`, the action it plays (`session.py` `_armatures`). */
65
+ action: z.string().nullable().optional(),
64
66
  })
65
67
  .strict();
66
68
 
@@ -0,0 +1,204 @@
1
+ /**
2
+ * THE SKELETONS A FRAME DESCRIBES, and the skinned meshes bound to them.
3
+ *
4
+ * WHERE THE SKIN COMES FROM: the export door itself (`bpy_web_export.cc`, `write_skin`). Every mesh
5
+ * an Armature modifier deforms arrives with a `skin` block: the armature's name, the mesh's vertex
6
+ * group names and, per EXPORTED vertex, its four strongest groups and their normalized weights. The
7
+ * frame's `armatures` already name every bone with its parent and its pose matrix
8
+ * (`PoseBone.matrix`, armature space). So one frame holds everything a skin needs, from one
9
+ * evaluation, and nothing here reads the engine or guesses whether what it holds is stale: a weight
10
+ * edit, a modifier added or removed, a new pose, each re-exports the mesh under a new revision (the
11
+ * door's skin digest moves it) and the skin is rebuilt with the geometry.
12
+ *
13
+ * THE BIND IS THE EXPORTED POSE. The exported vertices are already deformed, at the pose the frame
14
+ * names, so each bone's inverse is that pose's inverse: at Blender's own pose the skinning is an
15
+ * identity and the picture is exactly the one Blender evaluated. A clip played on the bones
16
+ * (`AnimationMixer` over `rig().bones`) moves the mesh from there. The inverses come from the
17
+ * frame's matrices, never from the bones' current transforms, so a bind taken while a clip is
18
+ * playing is still right.
19
+ *
20
+ * AN UNWEIGHTED VERTEX stays where it is, as in Blender: the door gives it the index one past the
21
+ * groups with weight 1, and that index is a bone at the armature's origin that never moves. A group
22
+ * no bone is named for weights nothing (it is that same still bone), as it deforms nothing in Blender.
23
+ */
24
+ import * as THREE from 'three';
25
+ import type { BlenderArmature } from './blender-runtime-armature';
26
+
27
+ /** The `skin` block of an exported mesh, as the door writes it (typed arrays by the worker). */
28
+ export interface FrameSkin {
29
+ readonly armature: string;
30
+ readonly groups: readonly string[];
31
+ readonly columns: { readonly joints: Uint16Array; readonly weights: Float32Array };
32
+ }
33
+
34
+ export interface ArmatureRig {
35
+ /** The armature this rig belongs to, by Blender object name. */
36
+ readonly armature: string;
37
+ /** The armature's presented object, which the bones hang under. */
38
+ readonly object: THREE.Object3D;
39
+ /** Every bone by Blender's own bone name (unsanitized: a game finds them by those names). */
40
+ readonly bones: ReadonlyMap<string, THREE.Bone>;
41
+ /** The action the file assigns this armature (`animation_data.action`), if any. */
42
+ readonly action: string | null;
43
+ }
44
+
45
+ interface Rig extends ArmatureRig {
46
+ readonly root: THREE.Group;
47
+ readonly still: THREE.Bone;
48
+ /** Bone names and parents: a change rebuilds the bones. */
49
+ readonly structure: string;
50
+ /** The pose matrices last applied, armature space, by bone name. */
51
+ pose: Map<string, THREE.Matrix4>;
52
+ poseKey: string;
53
+ action: string | null;
54
+ }
55
+
56
+ const matrixOf = (rows: readonly (readonly number[])[]): THREE.Matrix4 =>
57
+ new THREE.Matrix4().set(...(rows.flat() as Parameters<THREE.Matrix4['set']>));
58
+
59
+ /** Expand an exported mesh's per-vertex skin onto its DRAWN vertices (`blenderVertex`, the draw's
60
+ * map back to Blender's vertices), as the geometry's `skinIndex` and `skinWeight`. */
61
+ export function skinGeometry(geometry: THREE.BufferGeometry, skin: FrameSkin): void {
62
+ const source = geometry.getAttribute('blenderVertex');
63
+ if (!source) throw new Error(`A skinned mesh of ${skin.armature} was drawn without its vertex map`);
64
+ const drawn = source.count;
65
+ const indices = new Uint16Array(drawn * 4);
66
+ const weights = new Float32Array(drawn * 4);
67
+ for (let i = 0; i < drawn; i++) {
68
+ const vertex = source.getX(i);
69
+ indices.set(skin.columns.joints.subarray(vertex * 4, vertex * 4 + 4), i * 4);
70
+ weights.set(skin.columns.weights.subarray(vertex * 4, vertex * 4 + 4), i * 4);
71
+ }
72
+ geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(indices, 4));
73
+ geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(weights, 4));
74
+ geometry.userData['blenderSkin'] = { armature: skin.armature, groups: [...skin.groups] };
75
+ }
76
+
77
+ export class RuntimeSkeletons {
78
+ #rigs = new Map<string, Rig>();
79
+
80
+ /** The rig of one armature, or null when the frame names none by that name. */
81
+ rig(armature: string): ArmatureRig | null {
82
+ return this.#rigs.get(armature) ?? null;
83
+ }
84
+
85
+ rigs(): readonly ArmatureRig[] {
86
+ return [...this.#rigs.values()];
87
+ }
88
+
89
+ /**
90
+ * Bring the rigs to this frame: build an armature's bones when it is new or its bones changed,
91
+ * and pose them as Blender posed them WHEN THAT POSE CHANGED, so a clip a reader is playing is
92
+ * not reset by a frame that says nothing new about the pose.
93
+ */
94
+ apply(armatures: Readonly<Record<string, BlenderArmature>>,
95
+ objectFor: (name: string) => THREE.Object3D | null): void {
96
+ for (const [name, rig] of [...this.#rigs]) {
97
+ const armature = armatures[name];
98
+ const object = objectFor(name);
99
+ if (!armature || !object || object !== rig.object) {
100
+ rig.root.removeFromParent();
101
+ this.#rigs.delete(name);
102
+ }
103
+ }
104
+ for (const [name, armature] of Object.entries(armatures)) {
105
+ const object = objectFor(name);
106
+ if (!object) continue;
107
+ const structure = armature.bones.map((bone) => `${bone.name}\u0000${bone.parent ?? ''}`).join('\u0001');
108
+ let rig = this.#rigs.get(name);
109
+ if (rig && rig.structure !== structure) {
110
+ rig.root.removeFromParent();
111
+ rig = undefined;
112
+ }
113
+ if (!rig) {
114
+ const root = new THREE.Group();
115
+ root.name = `${name}:bones`;
116
+ const still = new THREE.Bone();
117
+ still.name = `${name}:still`;
118
+ root.add(still);
119
+ const bones = new Map<string, THREE.Bone>();
120
+ for (const declared of armature.bones) {
121
+ const bone = new THREE.Bone();
122
+ bone.name = declared.name;
123
+ bones.set(declared.name, bone);
124
+ }
125
+ for (const declared of armature.bones) {
126
+ const parent = declared.parent === null ? undefined : bones.get(declared.parent);
127
+ (parent ?? root).add(bones.get(declared.name)!);
128
+ }
129
+ object.add(root);
130
+ rig = { armature: name, object, root, still, bones, structure, pose: new Map(), poseKey: '', action: null };
131
+ this.#rigs.set(name, rig);
132
+ }
133
+ rig.action = armature.action ?? null;
134
+ const poseKey = armature.bones.map((bone) => bone.matrix.flat().join(',')).join(';');
135
+ if (poseKey !== rig.poseKey) {
136
+ rig.poseKey = poseKey;
137
+ rig.pose = new Map(armature.bones.map((bone) => [bone.name, matrixOf(bone.matrix)]));
138
+ RuntimeSkeletons.#pose(rig, armature);
139
+ }
140
+ }
141
+ }
142
+
143
+ /** Set every bone's local transform from the frame's armature-space pose matrices. */
144
+ static #pose(rig: Rig, armature: BlenderArmature): void {
145
+ for (const declared of armature.bones) {
146
+ const own = rig.pose.get(declared.name)!;
147
+ const parent = declared.parent === null ? undefined : rig.pose.get(declared.parent);
148
+ const local = parent ? parent.clone().invert().multiply(own) : own.clone();
149
+ const bone = rig.bones.get(declared.name)!;
150
+ local.decompose(bone.position, bone.quaternion, bone.scale);
151
+ }
152
+ }
153
+
154
+ /** Put Blender's pose back on an armature's bones, after a reader that played a clip lets go. */
155
+ restorePose(armature: string): void {
156
+ const rig = this.#rigs.get(armature);
157
+ if (!rig) return;
158
+ for (const [name, own] of rig.pose) {
159
+ const bone = rig.bones.get(name)!;
160
+ const parentBone = bone.parent instanceof THREE.Bone && bone.parent !== rig.still ? bone.parent : null;
161
+ const parent = parentBone ? rig.pose.get(parentBone.name) : undefined;
162
+ const local = parent ? parent.clone().invert().multiply(own) : own.clone();
163
+ local.decompose(bone.position, bone.quaternion, bone.scale);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * A mesh object's skinned form: a `THREE.SkinnedMesh` over its geometry, bound to its armature's
169
+ * rig at the pose the frame was exported in. Null when its geometry carries no skin or its
170
+ * armature is not in the frame (it then draws as the plain, exported mesh: Blender's own pose).
171
+ * The world matrices must be current (`root.updateMatrixWorld(true)`) before this is called.
172
+ */
173
+ bind(mesh: THREE.SkinnedMesh | THREE.Mesh): THREE.SkinnedMesh | null {
174
+ const skin = mesh.geometry.userData['blenderSkin'] as { armature: string; groups: string[] } | undefined;
175
+ const rig = skin ? this.#rigs.get(skin.armature) : undefined;
176
+ if (!skin || !rig) return null;
177
+ const skinned = (mesh as THREE.SkinnedMesh).isSkinnedMesh ? mesh as THREE.SkinnedMesh
178
+ : new THREE.SkinnedMesh(mesh.geometry, mesh.material);
179
+ if (skinned !== mesh) {
180
+ skinned.name = mesh.name;
181
+ skinned.matrixAutoUpdate = mesh.matrixAutoUpdate;
182
+ skinned.matrix.copy(mesh.matrix);
183
+ skinned.matrix.decompose(skinned.position, skinned.quaternion, skinned.scale);
184
+ skinned.userData = mesh.userData;
185
+ // A skin moves past its bind bounds: three computes them from the bind pose.
186
+ skinned.frustumCulled = false;
187
+ }
188
+ const armatureWorld = rig.object.matrixWorld;
189
+ const bones = [...skin.groups.map((group) => rig.bones.get(group) ?? rig.still), rig.still];
190
+ const inverses = bones.map((bone) => {
191
+ const pose = bone === rig.still ? undefined : rig.pose.get(bone.name);
192
+ const world = pose ? armatureWorld.clone().multiply(pose) : armatureWorld.clone();
193
+ return world.invert();
194
+ });
195
+ mesh.updateWorldMatrix(true, false);
196
+ skinned.bind(new THREE.Skeleton(bones, inverses), mesh.matrixWorld.clone());
197
+ return skinned;
198
+ }
199
+
200
+ dispose(): void {
201
+ for (const rig of this.#rigs.values()) rig.root.removeFromParent();
202
+ this.#rigs.clear();
203
+ }
204
+ }